convert entry files to ts but there are lots of errors

This commit is contained in:
SowinskiBraeden committed 2025-09-29 17:04:12 -07:00
1 parent afcf24115e
commit 77645c75b1
17 files changed
+1554 -982

No files matched your search

+44 -6
View File
@@ -18,6 +18,7 @@
"dotenv": "^16.0.3",
"form-data": "^4.0.0",
"mongodb": "^4.12.1",
"tsconfig-paths": "^4.2.0",
"winston": "^3.8.1"
},
"devDependencies": {
@@ -3352,6 +3353,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"license": "MIT",
"bin": {
"json5": "lib/cli.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -3511,7 +3524,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -4047,7 +4059,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
"integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -4284,6 +4295,20 @@
"strip-json-comments": "^2.0.0"
}
},
"node_modules/tsconfig-paths": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
"integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
"license": "MIT",
"dependencies": {
"json5": "^2.2.2",
"minimist": "^1.2.6",
"strip-bom": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tsconfig/node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
@@ -6977,6 +7002,11 @@
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true
},
"json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
},
"keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -7096,8 +7126,7 @@
"minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
},
"mkdirp": {
"version": "1.0.4",
@@ -7426,8 +7455,7 @@
"strip-bom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
"integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
"dev": true
"integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="
},
"strip-json-comments": {
"version": "3.1.1",
@@ -7578,6 +7606,16 @@
}
}
},
"tsconfig-paths": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
"integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
"requires": {
"json5": "^2.2.2",
"minimist": "^1.2.6",
"strip-bom": "^3.0.0"
}
},
"tslib": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
-559
View File
@@ -1,559 +0,0 @@
const { RegisterGlobalCommands, RegisterGuildCommands } = require("./util/RegisterSlashCommands");
const { Collection, Client, EmbedBuilder, Routes, InteractionResponseType, InteractionType, GatewayDispatchEvents } = require("discord.js");
const MongoClient = require("mongodb").MongoClient;
const { REST } = require("@discordjs/rest");
const Logger = require("./util/Logger");
const crypto = require("crypto");
// custom util imports
const { DownloadNitradoFile, CheckServerStatus, FetchServerSettings, PostServerSettings, NitradoCredentialStatus } = require("./util/NitradoAPI");
const { HandlePlayerLogs, HandleActivePlayersList } = require("./util/LogsHandler");
const { HandleKillfeed, UpdateLastDeathDate } = require("./util/KillfeedHandler");
const { HandleExpiredUAVs, HandleEvents, PlaceFireplaceInAlarm } = require("./util/AlarmsHandler");
const { decrypt } = require("./util/Cryptic");
const { GetWebhook, WebhookSend } = require("./util/WebhookHandler");
// Data structures imports
const { getDefaultPlayer, UpdatePlayer } = require("./database/player");
const { Missions } = require("./database/destinations");
const { GetGuild } = require("./database/guild");
const path = require("path");
const fs = require("fs");
const readline = require("readline");
const MINUTE = 60000; // 1 minute in milliseconds
const AUTO_RESTART_INTERVAL = 600000; // Set auto-restart interval 10 minutes (600,000ms)
class DayZR extends Client {
constructor(options, config) {
super(options);
this.config = config;
this.commands = new Collection();
this.interactionHandlers = new Collection();
this.logger = new Logger(path.join(__dirname, "..", "logs", "Logs.log"));
this.timer = this.config.Dev == "PROD." ? MINUTE * 5 : MINUTE / 4;
if (
this.config.Token === "" ||
this.config.SecretKey === "" ||
this.config.SecretIv === ""
) {
throw new TypeError(
"The config.js is not filled out. Please make sure nothing is blank, otherwise the bot will not work properly."
);
}
if (!["DEV.", "PROD."].includes(this.config.Dev)) {
throw new TypeError(
"The Dev version in the config.js does not match the allowed cases of \"DEV.\" or \"PROD.\""
);
}
// Generate secret hash with crypto to use for encryption
this.key = crypto
.createHash("sha512")
.update(this.config.SecretKey)
.digest("hex")
.substring(0, 32);
this.encryptionIV = crypto
.createHash("sha512")
.update(this.config.SecretIv)
.digest("hex")
.substring(0, 16);
this.db;
this.dbo;
this.databaseConnected = false;
this.arInterval = AUTO_RESTART_INTERVAL;
this.arIntervalIds = new Map();
this.playerSessions = new Map();
this.logHistory = new Map();
this.alarmPingQueue = new Map();
this.playerListMsgIds = new Map();
this.initialize();
this.LoadCommandsAndInteractionHandlers();
this.LoadEvents();
this.Ready = false;
this.activePlayersTick = 11;
this.ws.on(GatewayDispatchEvents.InteractionCreate, async (interaction) => {
const start = new Date().getTime();
if (interaction.type == InteractionType.ApplicationCommand) {
let GuildDB = await GetGuild(this, interaction.guild_id);
if (this.exists(GuildDB.Nitrado) && this.exists(GuildDB.Nitrado.Auth)) {
GuildDB.Nitrado.Auth = decrypt(
GuildDB.Nitrado.Auth,
this.config.EncryptionMethod,
this.key,
this.encryptionIV
);
}
const command = interaction.data.name.toLowerCase();
const args = interaction.data.options;
// Free unused armbands for related commands
if (["armbands", "claim", "factions"].includes(command)) {
for (const [factionID, data] of Object.entries(GuildDB.factionArmbands)) {
const guild = this.guilds.cache.get(GuildDB.serverID);
const role = guild.roles.cache.find(role => role.id == factionID);
if (!role) {
let query = {
$pull: { "server.usedArmbands": data.armband },
$unset: { [`server.factionArmbands.${factionID}`]: "" },
};
this.dbo.collection("guilds").updateOne({ "server.serverID": GuildDB.serverID }, query, (err, res) => {
if (err) return this.sendInternalError(interaction, err);
});
}
}
}
this.log(`Interaction [${interaction.guild_id}] - ${command}`);
const rest = new REST({ version: "10" }).setToken(this.config.Token);
// Easy to send response so ;)
interaction.guild = await this.guilds.fetch(interaction.guild_id);
const handleCallback = async (interactionType, message) => {
return await rest.post(Routes.interactionCallback(interaction.id, interaction.token), {
body: {
type: interactionType,
data: message,
}
});
}
// Nicely name our custom callback functions and pass correct type because discord is picky with numbers...
interaction.send = async (message) => handleCallback(InteractionResponseType.ChannelMessageWithSource, message);
interaction.deferReply = async (message) => handleCallback(InteractionResponseType.DeferredChannelMessageWithSource, message);
interaction.showModal = async (message) => handleCallback(InteractionResponseType.Modal, message);
interaction.editReply = async (message) => {
return await rest.patch(Routes.webhookMessage(this.application.id, interaction.token), {
body: message,
});
};
if (!this.databaseConnected) {
let dbFailedEmbed = new EmbedBuilder()
.setDescription(`**Internal Error:**\nUh Oh D: Its not you, its me.\nThe bot has failed to connect to the database 5 times!\nContact the Developers\nhttps://discord.gg/YCXhvy9uZw`)
.setColor(this.config.Colors.Red)
return interaction.send({ embeds: [dbFailedEmbed] });
}
let cmd = this.commands.get(command);
try {
cmd.SlashCommand.run(this, interaction, args, { GuildDB }, start); // start is only used in ping / stats command
} catch (err) {
this.sendInternalError(interaction, err);
}
}
});
}
log(Text) { this.logger.log(Text); }
error(Text) { this.logger.error(Text); }
async getDateEST(time) {
let timeArray = time.split(" ")[0].split(":");
let t = new Date(); // Get current date & time (UTC)
let f = new Date(t.getTime() - 4 * 3600000); // Convert UTC into EST time to roll back the day as necessary
f.setUTCHours(timeArray[0], timeArray[1], timeArray[2]); // Apply the supplied EST time to the converted date (EST is the timezone produced from the Nitrado logs).
return new Date(f.getTime() + 4 * 3600000); // Add EST time offset to return timestamp in UTC
}
async readLogs(guild) {
const fileStream = fs.createReadStream(`./logs/${guild.Nitrado.ServerID}-logs.ADM`);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
let lines = [];
for await (const line of rl) { lines.push(line); }
let logIndex = lines.indexOf(this.logHistory.get(guild.Nitrado.ServerID));
if (this.playerSessions.get(guild.Nitrado.ServerID).size === 0) {
let players = await this.dbo.collection("players").find({ "nitradoServerID": guild.Nitrado.ServerID }) // Get all players of this server
.toArray().then(all => all.filter(p => p.connected).map(p => p.connected = false)); // assume all players who were previously connected are not connected on init only.
for (let i = 0; i < players.length; i++) {
await UpdatePlayer(this, players[i])
}
}
for (let i = logIndex + 1; i < lines.length; i++) {
// Handle lines to skip
if (lines[i].includes("| ####")) continue;
if (lines[i].includes("(id=Unknown") || lines[i].includes("Player \"Unknown Entity\"")) continue;
if ((i - 1) >= 0 && lines[i] == lines[i - 1]) continue; // continue if this line is a duplicate of the last line
// Handle general logs
if (lines[i].includes("connected") || lines[i].includes("pos=<")) await HandlePlayerLogs(guild.Nitrado.ServerID, this, guild, lines[i], guild.combatLogTimer);
if (lines[i].includes("killed by Zmb") || lines[i].includes(">) died.")) await UpdateLastDeathDate(guild.Nitrado.ServerID, this, lines[i]); // Updates users last death date for non PVP deaths.
if (lines[i].includes(") placed Fireplace")) await PlaceFireplaceInAlarm(this, guild, lines[i]);
// Handle killfeed logs
if (
(lines[i].includes("killed by with") || lines[i].includes("killed by LandMineTrap")) || // Handle explosive deaths
(!(i + 1 >= lines.length) && lines[i + 1].includes("killed by") && lines[i].includes("TransportHit")) || // Handle vehicle deaths
(!(i + 1 >= lines.length) && lines[i + 1].includes("killed by Player") && lines[i].includes("hit by Player")) || // Handle PVP deaths
(lines[i].includes("killed by Player") && !lines[i - 1].includes("hit by Player")) // Handle deaths missing hit by log
) await HandleKillfeed(guild.Nitrado.ServerID, this, guild, lines[i]);
}
// Handle alarm pings
const maxEmbed = 10;
this.alarmPingQueue.forEach(queue => {
queue.forEach(async (data, channel_id) => {
const channel = this.GetChannel(channel_id);
if (!channel) return;
const NAME = "DayZ.R Zone Alert";
const webhook = await GetWebhook(this, NAME, channel_id);
data.forEach(async (embeds, role) => {
let embedArrays = [];
while (embeds.length > 0)
embedArrays.push(embeds.splice(0, maxEmbed));
for (let i = 0; i < embedArrays.length; i++) {
let content = { embeds: embedArrays[i] };
if (role != "-no-role-ping-") content.content = `<@&${role}>`;
WebhookSend(this, webhook, content);
// if (role == "-no-role-ping-") channel.send({ embeds: embedArrays[i] });
// else channel.send({ content: `<@&${role}>`, embeds: embedArrays[i] });
}
});
});
});
this.alarmPingQueue.set(guild.serverID, new Map()); // Clear alarm queue for this guild
const playerTemplate = /(.*) \| Player \"(.*)\" \(id=(.*) pos=<(.*)>\)/g;
let previouslyConnected = await this.dbo.collection("players").find({ "nitradoServerID": guild.Nitrado.ServerID })
.toArray().then(players => players.filter(p => p.connected)); // All players with connection log captured above and no disconnect log
let lastDetectedTime;
for (let i = lines.length - 1; i > 0; i--) {
if (lines[i].includes("PlayerList log:")) {
for (let j = i + 1; j < lines.length; j++) {
let line = lines[j];
if (line.includes("| ####")) break;
let data = [...line.matchAll(playerTemplate)][0];
if (!data) continue;
let info = {
time: data[1],
player: data[2],
playerID: data[3],
};
if (!this.exists(info.player) || !this.exists(info.playerID)) continue; // Skip this player if the player does not exist.
lastDetectedTime = await this.getDateEST(info.time);
let playerStat = await this.dbo.collection("players").findOne({ "playerID": info.playerID });
if (!this.exists(playerStat)) playerStat = getDefaultPlayer(info.player, info.playerID, guild.Nitrado.ServerID);
if (!previouslyConnected.includes(playerStat) && this.exists(playerStat.lastDisconnectionDate) && playerStat.lastDisconnectionDate !== null && playerStat.lastDisconnectionDate.getTime() > lastDetectedTime.getTime()) continue; // Skip this player if the lastDisconnectionDate time is later than the player log entry.
// Track adjusted sessions this instance has handled (e.g. no bot crashes or restarts).
if (this.playerSessions.get(guild.Nitrado.ServerID).has(info.playerID)) {
// Player is already in a session, update the session"s end time.
const session = this.playerSessions.get(guild.Nitrado.ServerID).get(info.playerID);
session.endTime = lastDetectedTime; // Update end time.
} else {
// Player is not in a session, create a new session.
const newSession = {
startTime: lastDetectedTime,
endTime: null, // Initialize end time as null.
};
this.playerSessions.get(guild.Nitrado.ServerID).set(info.playerID, newSession);
// Check if the player has been marked as connected before, but only if a session doesn"t exist
// in the map, indicating the connection was discovered in the logs during this session.
if (!previouslyConnected.includes(playerStat)) {
playerStat.connected = true;
playerStat.lastConnectionDate = lastDetectedTime; // Update last connection date.
}
}
await UpdatePlayer(this, playerStat);
}
break;
}
}
const lastLine = lines[lines.length - 1]
this.logHistory.set(guild.Nitrado.ServerID, lastLine);
this.dbo.collection("guilds").updateOne({ "server.serverID": guild.serverID }, { $set: { "server.lastLog": lastLine } }, (err, res) => {
if (err) this.error(`Failed to save last log to guild config [${guild.serverID}] for nitrado server [${guild.Nitrado.ServerID}]`);
});
}
async logsUpdateTimer(c) {
c.activePlayersTick++;
c.guilds.cache.forEach(async (guild) => {
let GuildDB = await GetGuild(c, guild.id);
/*
Note to self:
return statements do not prematurely exit out of a forEach loop like it does in a for loop.
*/
if (!c.exists(GuildDB.Nitrado)) return; // Continue if no nitrado credentials
if (GuildDB.Nitrado.Status == NitradoCredentialStatus.FAILED) return; // Continue if these credentials are marked as failed
const NitradoCred = {
ServerID: GuildDB.Nitrado.ServerID,
UserID: GuildDB.Nitrado.UserID,
Auth: decrypt(
GuildDB.Nitrado.Auth,
c.config.EncryptionMethod,
c.key,
c.encryptionIV
),
};
const response = await FetchServerSettings(NitradoCred, c, "logsUpdateTimer").then(res => res);
if (response == 1) {
c.dbo.collection("guilds").updateOne({ "server.serverID": GuildDB.serverID }, { $set: { "Nitrado.Status": NitradoCredentialStatus.FAILED } }, (err, _) => {
if (err) this.error(`Failed to update Nitrado status to failed. [${GuildDB.serverID}]`);
});
return;
};
const settings = response.data.gameserver;
// Update Nitrado DayZ Mission if change is detected
if (GuildDB.Nitrado.Mission !== Missions[settings.settings.config.mission]) {
c.dbo.collection("guilds").updateOne({ "server.serverID": GuildDB.serverID }, { $set: { "Nitrado.Mission": Missions[settings.settings.config.mission] } }, (err, res) => {
if (err) this.error(`Failed to save mission to guild config [${GuildDB.serverID}] for nitrado server [${GuildDB.Nitrado.ServerID}]`);
});
}
GuildDB.Nitrado.Mission = Missions[settings.settings.config.mission];
if (settings.game_specific.log_files.length == 0) return; // Ignore if no log files on Nitrado server
const filename = settings.game_specific.log_files.sort((a, b) => a.length - b.length)[0];
const path = `${settings.game_specific.path.slice(0, -1)}${filename.split(settings.game)[1]}`;
// Ensure Player List is logged for next update
const playerListEnabled = parseInt(settings.settings.config.adminLogPlayerList)
if (!playerListEnabled) PostServerSettings(NitradoCred, c, "config", "adminLogPlayerList", "1")
await DownloadNitradoFile(NitradoCred, c, path, `./logs/${NitradoCred.ServerID}-logs.ADM`).then(async (status) => {
if (status == 1) return c.error(`Failed to Download Nitrado Log Files - [${NitradoCred.ServerID}]`);
await c.readLogs(GuildDB).then(async () => {
HandleExpiredUAVs(c, GuildDB);
HandleEvents(c, GuildDB)
if (c.activePlayersTick == 12) await HandleActivePlayersList(NitradoCred, c, GuildDB);
})
});
});
}
async connectMongo(mongoURI, dbo) {
let failed = false;
let dbLogDir = path.join(__dirname, "..", "logs", "database-logs.json");
let databaselogs;
try {
databaselogs = JSON.parse(fs.readFileSync(dbLogDir));
} catch (err) {
databaselogs = {
attempts: 0,
connected: false,
};
}
if (databaselogs.attempts >= 5) {
this.error("Failed to connect to mongodb after multiple attempts");
return; // prevent further attempts
}
try {
// Connect to Mongo database.
this.db = await MongoClient.connect(mongoURI, { connectTimeoutMS: 1000 });
this.dbo = this.db.db(dbo);
this.log("Successfully connected to mongoDB");
databaselogs.connected = true;
databaselogs.attempts = 0; // reset attempts
this.databaseConnected = true;
} catch (err) {
databaselogs.attempts++;
databaselogs.connected = false;
let db = mongoURI.includes("@") ? mongoURI.split("@")[1] : mongoURI.split("//")[1];
db = db.includes("/") ? db.split("/")[0] : db;
this.error(`Failed to connect to mongodb (mongodb://${db}/${dbo}): attempt ${databaselogs.attempts} - ${err}`);
failed = true;
}
// write JSON string to a file
fs.writeFileSync(dbLogDir, JSON.stringify(databaselogs));
if (failed) process.exit(-1);
}
async initialize() {
// Wait for MongoDB to connect
await this.connectMongo(this.config.mongoURI, this.config.dbo);
if (!this.databaseConnected) return;
let guilds = await this.dbo.collection("guilds").find({}).toArray();
/*
Initialize auto restart for enabled servers
Initialize last logs
Initialize Player Sessions
*/
for (let i = 0; i < guilds.length; i++) {
if (!this.exists(guilds[i].Nitrado)) continue;
if (guilds[i].server.autoRestart) {
const NitradoCred = {
ServerID: guilds[i].Nitrado.ServerID,
UserID: guilds[i].Nitrado.UserID,
Auth: decrypt(
guilds[i].Nitrado.Auth,
this.config.EncryptionMethod,
this.key,
this.encryptionIV
)
};
this.arIntervalIds.set(guilds[i].server.serverID, setInterval(CheckServerStatus, this.arInterval, NitradoCred, this))
}
this.logHistory.set(guilds[i].Nitrado.ServerID, guilds[i].server.lastLog); // Using Nitrado Server ID over guild ID in case of future support for multiple nitrado servers in a single guild
this.playerSessions.set(guilds[i].Nitrado.ServerID, new Map()); // Same reason here as named above.
this.alarmPingQueue.set(guilds[i].server.serverID, new Map()); // Initialize alarm queue to be empty
this.playerListMsgIds.set(guilds[i].server.serverID, ""); // Initialize player list message ids
this.log(`[${guilds[i].server.serverID}] Initialized existing Nitrado Server: (${guilds[i].Nitrado.ServerID})`);
}
}
async initNewNitradoServer(guildId, Nitrado) {
let guild = await GetGuild(this, guildId)
if (guild.autoRestart) this.arIntervalIds.set(guildId, setInterval(CheckServerStatus, this.arInterval, Nitrado, this))
this.logHistory.set(Nitrado.ServerID, guild.lastLog);
this.playerSessions.set(Nitrado.ServerID, new Map());
this.alarmPingQueue.set(guildId, new Map());
this.playerListMsgIds.set(guildId, "");
this.log(`[${guildId}] Initialized new Nitrado`);
}
exists(n) { return typeof (n) == "number" ? !isNaN(n) : null != n && undefined != n && "" != n }
secondsToDhms(seconds) {
seconds = Number(seconds);
const d = Math.floor(seconds / (3600 * 24));
const h = Math.floor(seconds % (3600 * 24) / 3600);
const m = Math.floor(seconds % 3600 / 60);
const s = Math.floor(seconds % 60);
const dDisplay = d > 0 ? d + (d == 1 ? " day, " : " days, ") : "";
const hDisplay = h > 0 ? h + (h == 1 ? " hour, " : " hours, ") : "";
const mDisplay = m > 0 ? m + (m == 1 ? " minute, " : " minutes, ") : "";
const sDisplay = s > 0 ? s + (s == 1 ? " second" : " seconds") : "";
return dDisplay + hDisplay + mDisplay + sDisplay;
}
LoadCommandsAndInteractionHandlers() {
let CommandsDir = path.join(__dirname, "commands");
fs.readdir(CommandsDir, (err, files) => {
if (err) this.error(err);
else
files.forEach((file) => {
let cmd = require(CommandsDir + "/" + file);
if (!this.exists(cmd.name) || !this.exists(cmd.description))
return this.error(
"Unable to load Command: " +
file.split(".")[0] +
", Reason: File doesn't had name/desciption"
);
this.commands.set(file.split(".")[0].toLowerCase(), cmd);
if (this.exists(cmd.Interactions)) {
for (let [interaction, handler] of Object.entries(cmd.Interactions)) {
this.interactionHandlers.set(interaction, handler);
}
}
this.log("Command Loaded: " + file.split(".")[0]);
});
});
}
LoadEvents() {
let EventsDir = path.join(__dirname, "events");
fs.readdir(EventsDir, (err, files) => {
if (err) this.error(err);
else
files.forEach((file) => {
const event = require(EventsDir + "/" + file);
if (["interactionCreate", "guildMemberAdd"].includes(file.split(".")[0])) this.on(file.split(".")[0], i => event(this, i));
else this.on(file.split(".")[0], event.bind(null, this));
this.log("Event Loaded: " + file.split(".")[0]);
});
});
}
// Allows shorter lines of code elsewhere
GetChannel(channel_id) { return this.channels.cache.get(channel_id); }
sendError(Channel, Error) {
this.error(Error);
let embed = new EmbedBuilder()
.setColor(this.config.Red)
.setDescription(Error);
Channel.send(embed);
}
// Handles internal errors for slash commands. E.g failed to update database from slash command.
sendInternalError(Interaction, Error) {
this.error(Error);
const embed = new EmbedBuilder()
.setDescription(`**Internal Error:**\nUh Oh D: Its not you, its me.\nThis command has crashed\nContact the Developers\nhttps://discord.gg/YCXhvy9uZw`)
.setColor(this.config.Colors.Red)
try {
Interaction.send({ embeds: [embed] });
} catch {
Interaction.update({ embeds: [embed], components: [] });
}
}
// Calls register for guild and global commands
RegisterSlashCommands() {
RegisterGlobalCommands(this);
let p = Promise.resolve()
this.guilds.cache.forEach((guild) => {
p = p.then(() => {
RegisterGuildCommands(this, guild.id);
return new Promise((resolve) => {
setTimeout(resolve, 500);
})
})
});
}
build() {
this.login(this.config.Token);
}
}
module.exports = DayZR;
+926
View File
@@ -0,0 +1,926 @@
// Discord.js & REST API
import {
Collection,
Client,
EmbedBuilder,
Routes,
InteractionResponseType,
InteractionType,
GatewayDispatchEvents,
ClientOptions,
Guild,
ChatInputCommandInteraction,
Snowflake,
Role,
Message,
TextBasedChannel,
ButtonInteraction,
StringSelectMenuInteraction,
Channel,
} from "discord.js";
import { REST } from "@discordjs/rest";
// MongoDB
import { MongoClient } from "mongodb";
// Node.js built-ins
import * as path from "path";
import * as fs from "node:fs";
import * as readline from "node:readline";
import * as crypto from "crypto";
import { Readable } from "node:stream";
// Custom utilities
import Logger from "./util/Logger";
import { RegisterGlobalCommands, RegisterGuildCommands } from "./util/RegisterSlashCommands";
import {
DownloadNitradoFile,
CheckServerStatus,
FetchServerSettings,
PostServerSettings,
NitradoCredentialStatus,
} from "./util/NitradoAPI";
import { HandlePlayerLogs, HandleActivePlayersList } from "./util/LogsHandler";
import { HandleKillfeed, UpdateLastDeathDate } from "./util/KillfeedHandler";
import { HandleExpiredUAVs, HandleEvents, PlaceFireplaceInAlarm } from "./util/AlarmsHandler";
import { decrypt } from "./util/Cryptic";
import { GetWebhook, WebhookSend } from "./util/WebhookHandler";
import { Config, Colors } from "./config/config";
// Database structures
import { getDefaultPlayer, UpdatePlayer, Player } from "./database/player";
import { Missions, MissionKey } from "./database/destinations";
import { GetGuild, GuildConfig, NitradoConfig } from "./database/guild";
const MINUTE = 60000; // 1 minute in milliseconds
const AUTO_RESTART_INTERVAL = 600000; // Set auto-restart interval 10 minutes (600,000ms)
const STARTING_PLAYERS_TICK = 11;
// TODO: move this interface to LogsHandler when converted to ts
interface PlayerSession {
startTime: Date;
endTime: Date | null;
};
// TODO: put this somewhere else
interface CommandOptions {
};
interface InteractionHandler {
run: (
client: DayZR,
interaction: ChatInputCommandInteraction,
GuildDB: GuildConfig
) => Promise<void>;
}
interface Command {
name: string;
debug: boolean;
global: boolean;
description: string;
usage: string;
permissions: {
channel: Array<string>;
member: Array<string>;
};
options: Array<CommandOptions>;
SlashCommand: {
run: (
client: DayZR,
interaction: ChatInputCommandInteraction,
args: any, // Unsure of what this should be, come back to later
GuildDB: GuildConfig,
start?: number
) => Promise<void>;
};
Interactions?: {
[InteractoinName: string]: InteractionHandler;
}
}
export default class DayZR extends Client
{
private config: Config;
private logger: Logger;
private commands: Collection<string, Command>;
private interactionHandlers: Collection<string, InteractionHandler>;
private key: string;
private encryptionIV: string;
private db: any;
public dbo: any;
public databaseConnected: boolean;
private arInterval: number;
private arIntervalIds: Map<string, NodeJS.Timeout>;
private playerSessions: Map<string, Map<string, PlayerSession>>;
private logHistory: Map<string, string | null>;
private alarmPingQueue: Map<string, Map<string, Map<string, Array<EmbedBuilder>>>>;
private playerListMsgIds: Map<string, string>;
private Ready: boolean;
private activePlayersTick: number;
private timer: number;
constructor(options: ClientOptions, config: Config)
{
super(options);
this.config = config;
this.commands = new Collection();
this.interactionHandlers = new Collection();
this.logger = new Logger(path.join(__dirname, "..", "logs", "Logs.log"));
this.timer = this.config.Dev == "PROD." ? MINUTE * 5 : MINUTE / 4;
if (
this.config.Token === "" ||
this.config.SecretKey === "" ||
this.config.SecretIv === ""
) {
throw new TypeError(
"The config.js is not filled out. Please make sure nothing is blank, otherwise the bot will not work properly."
);
}
if (!["DEV.", "PROD."].includes(this.config.Dev)) {
throw new TypeError(
"The Dev version in the config.js does not match the allowed cases of \"DEV.\" or \"PROD.\""
);
}
// Generate secret hash with crypto to use for encryption
this.key = crypto
.createHash("sha512")
.update(this.config.SecretKey)
.digest("hex")
.substring(0, 32);
this.encryptionIV = crypto
.createHash("sha512")
.update(this.config.SecretIv)
.digest("hex")
.substring(0, 16);
this.databaseConnected = false;
this.arInterval = AUTO_RESTART_INTERVAL;
this.arIntervalIds = new Map();
this.playerSessions = new Map();
this.logHistory = new Map();
this.alarmPingQueue = new Map();
this.playerListMsgIds = new Map();
this.initialize();
this.LoadCommandsAndInteractionHandlers();
this.LoadEvents();
this.Ready = false;
this.activePlayersTick = STARTING_PLAYERS_TICK;
this.ws.on(
GatewayDispatchEvents.InteractionCreate,
async (interaction: ChatInputCommandInteraction) =>
{
const start: number = new Date().getTime();
if (interaction.type == InteractionType.ApplicationCommand)
{
if (!interaction.guildId) return;
let GuildDB: GuildConfig = await GetGuild(this, interaction.guildId);
if (this.exists(GuildDB.Nitrado) && this.exists(GuildDB.Nitrado.Auth))
{
GuildDB.Nitrado.Auth = decrypt(
GuildDB.Nitrado.Auth,
this.config.EncryptionMethod,
this.key,
this.encryptionIV
);
}
const command: string = interaction.commandName;
const args: any = interaction.options;
// Free unused armbands for related commands
if (["armbands", "claim", "factions"].includes(command))
{
for (const [factionID, data] of Object.entries(GuildDB.factionArmbands))
{
const guild: Guild | undefined = this.guilds.cache.get(GuildDB.serverID);
if (!guild) continue;
const role: Role | undefined = guild.roles.cache.find(role => role.id == factionID);
if (!role)
{
let query = {
$pull: { "server.usedArmbands": data.armband },
$unset: { [`server.factionArmbands.${factionID}`]: "" },
};
this.dbo.collection("guilds").updateOne(
{
"server.serverID": GuildDB.serverID
},
query,
(err: string) =>
{
if (err) return this.sendInternalError(interaction, err);
}
);
}
}
}
this.log(`Interaction [${interaction.guildId}] - ${command}`);
const rest = new REST({ version: "10" }).setToken(this.config.Token);
const normalizeMessage = (message: any) =>
{
if (message?.embeds) {
message = {
...message,
embeds: message.embeds.map((embed: EmbedBuilder | any) =>
embed instanceof EmbedBuilder ? embed.toJSON() : embed
),
};
}
return message;
};
// Easy to send response so ;)
const handleCallback = async (interactionType: InteractionResponseType, message: any): Promise<unknown> =>
{
message = normalizeMessage(message);
return await rest.post(Routes.interactionCallback(interaction.id, interaction.token),
{
body: {
type: interactionType,
data: message,
}
});
}
// Nicely name our custom callback functions and pass correct type because discord is picky with numbers...
interaction.send = async (message: Message): Promise<unknown> => handleCallback(InteractionResponseType.ChannelMessageWithSource, message);
interaction.deferReply = async (message: Message): Promise<unknown> => handleCallback(InteractionResponseType.DeferredChannelMessageWithSource, message);
interaction.showModal = async (message: Message): Promise<unknown> => handleCallback(InteractionResponseType.Modal, message);
interaction.editReply = async (message: Message): Promise<unknown> =>
{
if (!this.application) return;
return await rest.patch(Routes.webhookMessage(this.application.id, interaction.token), {
body: message,
});
};
if (!this.databaseConnected)
{
const dbFailedEmbed: EmbedBuilder = new EmbedBuilder()
.setDescription(`**Internal Error:**\nUh Oh D: Its not you, its me.\nThe bot has failed to connect to the database 5 times!\nContact the Developers\nhttps://discord.gg/YCXhvy9uZw`)
.setColor(Colors.Red)
return interaction.send({ embeds: [dbFailedEmbed] });
}
let cmd: Command | undefined = this.commands.get(command);
if (!cmd) return;
try {
cmd.SlashCommand.run(this, interaction, args, GuildDB, start); // start is only used in ping / stats command
} catch (err: any) {
this.sendInternalError(interaction, err);
}
}
});
}
private log(text: string): void { this.logger.log(text); }
public error(text: string): void { this.logger.error(text); }
public getDateEST(time: string): Date
{
// TODO: fix magic numbers <------------------------------------------------------------------------------------
let timeArray: Array<number> = time.split(" ")[0].split(":").map((v) => parseInt(v));
let t: Date = new Date(); // Get current date & time (UTC)
let f: Date = new Date(t.getTime() - 4 * 3600000); // Convert UTC into EST time to roll back the day as necessary
f.setUTCHours(timeArray[0], timeArray[1], timeArray[2]); // Apply the supplied EST time to the converted date (EST is the timezone produced from the Nitrado logs).
return new Date(f.getTime() + 4 * 3600000); // Add EST time offset to return timestamp in UTC
}
private async readLogs(guild: GuildConfig): Promise<void>
{
if (!guild.Nitrado) return;
const fileStream: fs.ReadStream = fs.createReadStream(`./logs/${guild.Nitrado.ServerID}-logs.ADM`);
const rl: readline.Interface = readline.createInterface({
input: fileStream as Readable,
crlfDelay: Infinity
});
let lines: Array<string> = [];
for await (const line of rl) { lines.push(line); }
const lastLog: string | null | undefined = this.logHistory.get(guild.Nitrado.ServerID);
let logIndex = -1;
if (lastLog != null)
{
logIndex = lines.indexOf(lastLog);
}
if (this.playerSessions.get(guild.Nitrado.ServerID)?.size === 0)
{
// Get all players of this server
let players: Array<Player> = await this.dbo.collection("players").find(
{
"nitradoServerID": guild.Nitrado.ServerID
}
).toArray().then((all: Array<Player>) =>
{
// assume all players who were previously connected are not connected on init only.
return all.filter(p => p.connected).map(p => p.connected = false);
}
);
for (let i = 0; i < players.length; i++)
{
await UpdatePlayer(this, players[i])
}
}
for (let i = logIndex + 1; i < lines.length; i++)
{
// TODO: tidy this up its just bad looking
// Handle lines to skip
if (lines[i].includes("| ####")) continue;
if (lines[i].includes("(id=Unknown") || lines[i].includes("Player \"Unknown Entity\"")) continue;
if ((i - 1) >= 0 && lines[i] == lines[i - 1]) continue; // continue if this line is a duplicate of the last line
// Handle general logs
if (lines[i].includes("connected") || lines[i].includes("pos=<")) await HandlePlayerLogs(guild.Nitrado.ServerID, this, guild, lines[i], guild.combatLogTimer);
if (lines[i].includes("killed by Zmb") || lines[i].includes(">) died.")) await UpdateLastDeathDate(guild.Nitrado.ServerID, this, lines[i]); // Updates users last death date for non PVP deaths.
if (lines[i].includes(") placed Fireplace")) await PlaceFireplaceInAlarm(this, guild, lines[i]);
// Handle killfeed logs
if (
(lines[i].includes("killed by with") || lines[i].includes("killed by LandMineTrap")) || // Handle explosive deaths
(!(i + 1 >= lines.length) && lines[i + 1].includes("killed by") && lines[i].includes("TransportHit")) || // Handle vehicle deaths
(!(i + 1 >= lines.length) && lines[i + 1].includes("killed by Player") && lines[i].includes("hit by Player")) || // Handle PVP deaths
(lines[i].includes("killed by Player") && !lines[i - 1].includes("hit by Player")) // Handle deaths missing hit by log
) await HandleKillfeed(guild.Nitrado.ServerID, this, guild, lines[i]);
}
// Handle alarm pings
const maxEmbed = 10;
this.alarmPingQueue.forEach(queue =>
{
queue.forEach(async (data, channel_id) =>
{
const channel = this.GetChannel(channel_id);
if (!channel) return;
const NAME = "DayZ.R Zone Alert";
const webhook = await GetWebhook(this, NAME, channel_id);
data.forEach(async (embeds, role) =>
{
let embedArrays: Array<Array<EmbedBuilder>> = [];
while (embeds.length > 0)
{
embedArrays.push(embeds.splice(0, maxEmbed));
}
for (let i = 0; i < embedArrays.length; i++)
{
interface AlarmEmbed
{
embeds: any;
content?: string;
};
let content: AlarmEmbed = { embeds: embedArrays[i] };
if (role != "-no-role-ping-") content.content = `<@&${role}>`;
WebhookSend(this, webhook, content);
// if (role == "-no-role-ping-") channel.send({ embeds: embedArrays[i] });
// else channel.send({ content: `<@&${role}>`, embeds: embedArrays[i] });
}
});
});
});
this.alarmPingQueue.set(guild.serverID, new Map()); // Clear alarm queue for this guild
const playerTemplate = /(.*) \| Player \"(.*)\" \(id=(.*) pos=<(.*)>\)/g;
let previouslyConnected = await this.dbo.collection("players").find(
{
"nitradoServerID": guild.Nitrado.ServerID
}
).toArray().then((players: Array<Player>) => {
// All players with connection log captured above and no disconnect log
return players.filter(p => p.connected);
});
let lastDetectedTime;
for (let i = lines.length - 1; i > 0; i--)
{
if (lines[i].includes("PlayerList log:"))
{
for (let j = i + 1; j < lines.length; j++)
{
let line = lines[j];
if (line.includes("| ####")) break;
let data = [...line.matchAll(playerTemplate)][0];
if (!data) continue;
let info = {
time: data[1],
player: data[2],
playerID: data[3],
};
// Skip this player if the player does not exist.
if (!this.exists(info.player) || !this.exists(info.playerID)) continue;
lastDetectedTime = await this.getDateEST(info.time);
let playerStat = await this.dbo.collection("players").findOne({ "playerID": info.playerID });
if (!this.exists(playerStat)) playerStat = getDefaultPlayer(info.player, info.playerID, guild.Nitrado.ServerID);
// Skip this player if the lastDisconnectionDate time is later than the player log entry.
if (!previouslyConnected.includes(playerStat) &&
this.exists(playerStat.lastDisconnectionDate) &&
playerStat.lastDisconnectionDate !== null &&
playerStat.lastDisconnectionDate.getTime() > lastDetectedTime.getTime()
) continue;
// Track adjusted sessions this instance has handled (e.g. no bot crashes or restarts).
if (this.playerSessions.get(guild.Nitrado.ServerID)?.has(info.playerID))
{
// Player is already in a session, update the session"s end time.
let session: PlayerSession | undefined = this.playerSessions.get(guild.Nitrado.ServerID)?.get(info.playerID);
if (!session) continue;
session.endTime = lastDetectedTime; // Update end time.
}
else
{
// Player is not in a session, create a new session.
const newSession = {
startTime: lastDetectedTime,
endTime: null, // Initialize end time as null.
};
this.playerSessions.get(guild.Nitrado.ServerID)?.set(info.playerID, newSession);
// Check if the player has been marked as connected before, but only if a session doesn"t exist
// in the map, indicating the connection was discovered in the logs during this session.
if (!previouslyConnected.includes(playerStat))
{
playerStat.connected = true;
playerStat.lastConnectionDate = lastDetectedTime; // Update last connection date.
}
}
await UpdatePlayer(this, playerStat);
}
break;
}
}
const lastLine = lines[lines.length - 1];
this.logHistory.set(guild.Nitrado.ServerID, lastLine);
this.dbo.collection("guilds").updateOne(
{
"server.serverID": guild.serverID
},
{
$set: { "server.lastLog": lastLine }
},
(err: string) =>
{
if (err)
{
this.error(`Failed to save last log to guild config [${guild.serverID}] for nitrado server [${guild.Nitrado?.ServerID}]`);
}
}
);
}
private async logsUpdateTimer(c: DayZR): Promise<void>
{
c.activePlayersTick++;
c.guilds.cache.forEach(async (guild) =>
{
let GuildDB = await GetGuild(c, guild.id);
/*
Note to self:
return statements do not prematurely exit out of a forEach loop like it does in a for loop.
*/
// Continue if no nitrado credentials
if (!c.exists(GuildDB.Nitrado)) return;
// Continue if these credentials are marked as failed
if (GuildDB.Nitrado.Status == NitradoCredentialStatus.FAILED) return;
const NitradoCred = {
ServerID: GuildDB.Nitrado.ServerID,
UserID: GuildDB.Nitrado.UserID,
Auth: decrypt(
GuildDB.Nitrado.Auth,
c.config.EncryptionMethod,
c.key,
c.encryptionIV
),
};
const response = await FetchServerSettings(NitradoCred, c, "logsUpdateTimer").then(res => res);
// TODO: fix magic number
if (response == 1)
{
c.dbo.collection("guilds").updateOne(
{
"server.serverID": GuildDB.serverID
},
{
$set: { "Nitrado.Status": NitradoCredentialStatus.FAILED }
},
(err: string) =>
{
if (err)
{
this.error(`Failed to update Nitrado status to failed. [${GuildDB.serverID}]`);
}
}
);
return;
};
const settings = response.data.gameserver;
// Update Nitrado DayZ Mission if change is detected
let missionKey: MissionKey = settings.settings.config.mission;
if (GuildDB.Nitrado.Mission !== Missions[missionKey])
{
c.dbo.collection("guilds").updateOne(
{
"server.serverID": GuildDB.serverID
},
{
$set: { "Nitrado.Mission": Missions[missionKey] }
},
(err: string) =>
{
if (err)
{
this.error(`Failed to save mission to guild config [${GuildDB.serverID}] for nitrado server [${GuildDB.Nitrado?.ServerID}]`)
};
}
);
}
GuildDB.Nitrado.Mission = Missions[missionKey];
// Ignore if no log files on Nitrado server
if (settings.game_specific.log_files.length == 0) return;
// Get file with longest file name (target log file)
const filename = settings.game_specific.log_files.sort((a: string, b: string) => a.length - b.length)[0];
const path = `${settings.game_specific.path.slice(0, -1)}${filename.split(settings.game)[1]}`;
// Ensure Player List is logged for next update
const playerListEnabled = parseInt(settings.settings.config.adminLogPlayerList)
if (!playerListEnabled) PostServerSettings(NitradoCred, c, "config", "adminLogPlayerList", "1")
await DownloadNitradoFile(NitradoCred, c, path, `./logs/${NitradoCred.ServerID}-logs.ADM`)
.then(async (status) =>
{
if (status == 1) return c.error(`Failed to Download Nitrado Log Files - [${NitradoCred.ServerID}]`);
await c.readLogs(GuildDB).then(async () =>
{
HandleExpiredUAVs(c, GuildDB);
HandleEvents(c, GuildDB)
// TODO: fix magic number
if (c.activePlayersTick == 12) await HandleActivePlayersList(NitradoCred, c, GuildDB);
})
});
});
}
async connectMongo(mongoURI: string, dbo: string): Promise<void>
{
let failed: boolean = false;
let dbLogDir: string = path.join(__dirname, "..", "logs", "database-logs.json");
interface DatabaseLog
{
attempts: number;
connected: boolean;
};
let databaselogs: DatabaseLog;
try
{
databaselogs = JSON.parse(
fs.readFileSync(dbLogDir).toString("utf-8")
);
}
catch (err)
{
databaselogs = {
attempts: 0,
connected: false,
};
}
if (databaselogs.attempts >= 5)
{
this.error("Failed to connect to mongodb after multiple attempts");
return; // prevent further attempts
}
try
{
// Connect to Mongo database.
this.db = new MongoClient(mongoURI, { connectTimeoutMS: 1000 });
await this.db.connect();
this.dbo = this.db.db(dbo);
this.log("Successfully connected to mongoDB");
databaselogs.connected = true;
databaselogs.attempts = 0; // reset attempts
this.databaseConnected = true;
}
catch (err)
{
databaselogs.attempts++;
databaselogs.connected = false;
let db = mongoURI.includes("@") ? mongoURI.split("@")[1] : mongoURI.split("//")[1];
db = db.includes("/") ? db.split("/")[0] : db;
this.error(`Failed to connect to mongodb (mongodb://${db}/${dbo}): attempt ${databaselogs.attempts} - ${err}`);
failed = true;
}
// write JSON string to a file
fs.writeFileSync(dbLogDir, JSON.stringify(databaselogs));
if (failed) process.exit(-1);
}
async initialize(): Promise<void>
{
// Wait for MongoDB to connect
await this.connectMongo(this.config.MongoURI, this.config.DBO);
if (!this.databaseConnected) return;
let guilds = await this.dbo.collection("guilds").find({}).toArray();
/*
Initialize auto restart for enabled servers
Initialize last logs
Initialize Player Sessions
*/
for (let i = 0; i < guilds.length; i++)
{
if (!this.exists(guilds[i].Nitrado)) continue;
if (guilds[i].server.autoRestart)
{
const NitradoCred = {
ServerID: guilds[i].Nitrado.ServerID,
UserID: guilds[i].Nitrado.UserID,
Auth: decrypt(
guilds[i].Nitrado.Auth,
this.config.EncryptionMethod,
this.key,
this.encryptionIV
)
};
this.arIntervalIds.set(guilds[i].server.serverID, setInterval(CheckServerStatus, this.arInterval, NitradoCred, this))
}
this.logHistory.set(guilds[i].Nitrado.ServerID, guilds[i].server.lastLog); // Using Nitrado Server ID over guild ID in case of future support for multiple nitrado servers in a single guild
this.playerSessions.set(guilds[i].Nitrado.ServerID, new Map()); // Same reason here as named above.
this.alarmPingQueue.set(guilds[i].server.serverID, new Map()); // Initialize alarm queue to be empty
this.playerListMsgIds.set(guilds[i].server.serverID, ""); // Initialize player list message ids
this.log(`[${guilds[i].server.serverID}] Initialized existing Nitrado Server: (${guilds[i].Nitrado.ServerID})`);
}
}
async initNewNitradoServer(
guildId: Snowflake,
Nitrado: NitradoConfig
): Promise<void>
{
let guild = await GetGuild(this, guildId)
if (guild.autoRestart)
{
this.arIntervalIds.set(guildId, setInterval(CheckServerStatus, this.arInterval, Nitrado, this))
}
this.logHistory.set(Nitrado.ServerID, guild.lastLog);
this.playerSessions.set(Nitrado.ServerID, new Map());
this.alarmPingQueue.set(guildId, new Map());
this.playerListMsgIds.set(guildId, "");
this.log(`[${guildId}] Initialized new Nitrado`);
}
exists<T> (n: T | null | undefined | "" | number): n is T
{
return typeof n === "number" ? !isNaN(n) : n !== null && n !== undefined && n !== "";
}
secondsToDhms(seconds: number): string
{
// TODO; holy magic numbers, my prof is gonna assasinate me if he sees this
seconds = Number(seconds);
const d = Math.floor(seconds / (3600 * 24));
const h = Math.floor(seconds % (3600 * 24) / 3600);
const m = Math.floor(seconds % 3600 / 60);
const s = Math.floor(seconds % 60);
const dDisplay = d > 0 ? d + (d == 1 ? " day, " : " days, ") : "";
const hDisplay = h > 0 ? h + (h == 1 ? " hour, " : " hours, ") : "";
const mDisplay = m > 0 ? m + (m == 1 ? " minute, " : " minutes, ") : "";
const sDisplay = s > 0 ? s + (s == 1 ? " second" : " seconds") : "";
return dDisplay + hDisplay + mDisplay + sDisplay;
}
LoadCommandsAndInteractionHandlers(): void
{
let CommandsDir = path.join(__dirname, "commands");
fs.readdir(CommandsDir, (err, files) =>
{
if (err)
{
this.error(err.message);
}
else
{
files.forEach((file) =>
{
let cmd: Command = require(CommandsDir + "/" + file);
if (!this.exists(cmd.name) || !this.exists(cmd.description))
{
return this.error(
"Unable to load Command: " +
file.split(".")[0] +
", Reason: File doesn't had name/desciption"
);
}
this.commands.set(file.split(".")[0].toLowerCase(), cmd);
if (this.exists(cmd.Interactions))
{
for (let [interaction, handler] of Object.entries(cmd.Interactions))
{
this.interactionHandlers.set(interaction, handler);
}
}
this.log("Command Loaded: " + file.split(".")[0]);
});
}
});
}
LoadEvents(): void
{
let EventsDir = path.join(__dirname, "events");
fs.readdir(EventsDir, (err, files) =>
{
if (err)
{
this.error(err.message);
}
else
{
files.forEach((file) =>
{
const event = require(EventsDir + "/" + file);
if (["interactionCreate", "guildMemberAdd"].includes(file.split(".")[0]))
{
this.on(file.split(".")[0], i => event(this, i));
}
else
{
this.on(file.split(".")[0], event.bind(null, this));
}
this.log("Event Loaded: " + file.split(".")[0]);
});
}
});
}
// Allows shorter lines of code elsewhere
GetChannel(channel_id: Snowflake): Channel | undefined
{
return this.channels.cache.get(channel_id);
}
sendError(channel: TextBasedChannel, error: string): void
{
this.error(error);
let embed: EmbedBuilder = new EmbedBuilder()
.setColor(Colors.Red)
.setDescription(error);
channel.send({ embeds: [embed] });
}
// Handles internal errors for slash commands. E.g failed to update database from slash command.
sendInternalError(
interaction: ChatInputCommandInteraction | ButtonInteraction | StringSelectMenuInteraction,
error: string
): void
{
this.error(error);
const embed: EmbedBuilder = new EmbedBuilder()
.setDescription(`**Internal Error:**\nUh Oh D: Its not you, its me.\nThis command has crashed\nContact the Developers\nhttps://discord.gg/YCXhvy9uZw`)
.setColor(Colors.Red)
if (interaction.isChatInputCommand())
{
interaction.send({ embeds: [embed] });
}
else if (interaction.isButton() || interaction.isStringSelectMenu())
{
interaction.update({ embeds: [embed], components: [] });
}
}
// Calls register for guild and global commands
RegisterSlashCommands(): void
{
RegisterGlobalCommands(this);
let p = Promise.resolve()
this.guilds.cache.forEach((guild) =>
{
p = p.then(() =>
{
RegisterGuildCommands(this, guild.id);
return new Promise((resolve) =>
{
setTimeout(resolve, 500); // <<--- TODO: MAGIC NUMBER!
});
});
});
}
build(): void
{
this.login(this.config.Token);
}
}
+4 -1
View File
@@ -7,7 +7,8 @@ export const enum DevelopmentBranch {
Prod = "PROD.",
};
export const enum Colors {
// switch to const later once removed Colors from config
export enum Colors {
Default = "#8a7c72",
DarkRed = "#ba0f0f",
Red = "#f55c5c",
@@ -25,6 +26,7 @@ export interface Config {
Dev: DevelopmentBranch;
Version: string;
Admins: Array<string>;
Colors: typeof Colors;
SupportServer: string;
Token: string;
SecretKey: string;
@@ -43,6 +45,7 @@ export const config: Config = {
Dev: <DevelopmentBranch>process.env.Dev || DevelopmentBranch.Dev,
Version: version,
Admins: ["362791661274660874", "329371697570381824"], // Admins of the bot
Colors: Colors, // only here for backwards compatibility with commands/*
SupportServer: "https://discord.gg/KVFJCvvFtK", // Support Server Link
Token: process.env.token || "", //Discord Bot Token
SecretKey: process.env.key || "01234567891",
-164
View File
@@ -1,164 +0,0 @@
module.exports = {
Armbands: [
{
name: "Black",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/8/82/ArmbandBlack.png/revision/latest?cb=20161127174754"
},
{
name: "Blue",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/bd/ArmbandBlue.png/revision/latest?cb=20161127174803"
},
{
name: "Green",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/ce/ArmbandGreen.png/revision/latest?cb=20161127174812"
},
{
name: "Orange",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/e6/ArmbandOrange.png/revision/latest?cb=20161127174846"
},
{
name: "Pink",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/f7/ArmbandPink.png/revision/latest?cb=20161127174854"
},
{
name: "Red",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/1/14/Armband.png/revision/latest?cb=20161127174901"
},
{
name: "Yellow",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/8/81/ArmbandYellow.png/revision/latest?cb=20161127174918"
},
{
name: "White",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c7/Armband_White.png/revision/latest?cb=20161127174926"
},
{
name: "Altis",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/ee/Flag_alti_co.png/revision/latest?cb=20200820222622"
},
{
name: "Asiain Pacific Alliance (APA)",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c2/Flag_apa_co.png/revision/latest?cb=20200820222623"
},
{
name: "Bear",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/e1/Flag_bear_co.png/revision/latest?cb=20200820222626"
},
{
name: "Bohemia Interactive",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/ee/Flag_bi_co.png/revision/latest?cb=20200820222627"
},
{
name: "Brain",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/7/7d/Flag_brain_co.png/revision/latest?cb=20200820222628"
},
{
name: "Chernarussian Defence Forces (CDF)",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/d/d6/Flag_cdf_co.png/revision/latest?cb=20200820222629"
},
{
name: "Chedaki (CHED)",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/9/96/Flag_ched_co.png/revision/latest?cb=20200820222630"
},
{
name: "CHEL",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/9/98/Flag_chel_co.png/revision/latest?cb=20200820222631"
},
{
name: "Chernarus",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/ef/Flag_chern_co.png/revision/latest?cb=20200820222632"
},
{
name: "Chernarus Mining Corporation (CMC)",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/d/da/Flag_cmc_co.png/revision/latest?cb=20200820222634"
},
{
name: "Rooster",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/4/44/Flag_cock_co.png/revision/latest?cb=20200820222635"
},
{
name: "DayZ",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/b2/Flag_dayz_co.png/revision/latest?cb=20200820222636"
},
{
name: "North Sahrani (DROS)",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/2/24/Flag_dros_co.png/revision/latest?cb=20200820222637"
},
{
name: "Fawn",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/d/d2/Flag_fawn_co.png/revision/latest/scale-to-width-down/1000?cb=20200820222639"
},
{
name: "Pirates",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/ab/Flag_jolly_co.png/revision/latest?cb=20200820222643"
},
{
name: "Cannibals",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/4/42/Flag_jolly_c_co.png/revision/latest?cb=20200820222641"
},
{
name: "South Sahrani (KOS)",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/a1/Flag_kos_co.png/revision/latest?cb=20200820222644"
},
{
name: "Livonia Army (LDF)",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c1/Flag_ldf_co.png/revision/latest?cb=20200820222645"
},
{
name: "Livonia",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/e6/Flag_livo_co.png/revision/latest?cb=20200820222647"
},
{
name: "NAPA",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/e4/Flag_napa_co.png/revision/latest?cb=20200820222648"
},
{
name: "Livonia Police",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/63/Flag_police_co.png/revision/latest?cb=20200820222649"
},
{
name: "TEC",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/ea/Flag_tec_co.png/revision/latest?cb=20200820222650"
},
{
name: "United Earth Coalition (UEC)",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/ca/Flag_uec_co.png/revision/latest?cb=20200820222651"
},
{
name: "Wolf",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/b2/Flag_wolf_co.png/revision/latest?cb=20200820222653"
},
{
name: "Zenit Radio Station",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/0/05/Flag_zenit_co.png/revision/latest?cb=20200820222654"
},
{
name: "Zombie Hunters",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/9/97/Flag_zhunters_co.png/revision/latest?cb=20200820222621"
},
{
name: "RSTA",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/2/20/Flag_rsta_co.png/revision/latest/scale-to-width-down/1000?cb=20210216191221"
},
{
name: "Refuge",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/8/8e/Flag_refuge_co.png/revision/latest/scale-to-width-down/1000?cb=20210216191205"
},
{
name: "Snake",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/5/54/Flag_snake_co.png/revision/latest/scale-to-width-down/1000?cb=20210216191234"
},
{
name: "Zagorky",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/7/75/Flag_zagorky_co.png/revision/latest/scale-to-width-down/1000?cb=20230619164704"
},
{
name: "Crook",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c8/Flag_crook_co.png/revision/latest/scale-to-width-down/1000?cb=20230619164705"
},
{
name: "Rex",
url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c5/Flag_rex_co.png/revision/latest/scale-to-width-down/1000?cb=20230619164706"
},
]
}
+93
View File
@@ -0,0 +1,93 @@
export enum ArmbandName
{
Black = "Black",
Blue = "Blue",
Green = "Green",
Orange = "Orange",
Pink = "Pink",
Red = "Red",
Yellow = "Yellow",
White = "White",
Altis = "Altis",
AsiainPacificAllianceAPA = "Asiain Pacific Alliance (APA)",
Bear = "Bear",
BohemiaInteractive = "Bohemia Interactive",
Brain = "Brain",
ChernarussianDefenceForcesCDF = "Chernarussian Defence Forces (CDF)",
ChedakiCHED = "Chedaki (CHED)",
CHEL = "CHEL",
Chernarus = "Chernarus",
ChernarusMiningCorporationCMC = "Chernarus Mining Corporation (CMC)",
Rooster = "Rooster",
DayZ = "DayZ",
NorthSahraniDROS = "North Sahrani (DROS)",
Fawn = "Fawn",
Pirates = "Pirates",
Cannibals = "Cannibals",
SouthSahraniKOS = "South Sahrani (KOS)",
LivoniaArmyLDF = "Livonia Army (LDF)",
Livonia = "Livonia",
NAPA = "NAPA",
LivoniaPolice = "Livonia Police",
TEC = "TEC",
UnitedEarthCoalitionUEC = "United Earth Coalition (UEC)",
Wolf = "Wolf",
ZenitRadioStation = "Zenit Radio Station",
ZombieHunters = "Zombie Hunters",
RSTA = "RSTA",
Refuge = "Refuge",
Snake = "Snake",
Zagorky = "Zagorky",
Crook = "Crook",
Rex = "Rex",
};
export interface Armband
{
name: ArmbandName;
url: string;
};
export const Armbands: Armband[] =
[
{ name: ArmbandName.Black, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/8/82/ArmbandBlack.png/revision/latest?cb=20161127174754" },
{ name: ArmbandName.Blue, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/bd/ArmbandBlue.png/revision/latest?cb=20161127174803" },
{ name: ArmbandName.Green, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/ce/ArmbandGreen.png/revision/latest?cb=20161127174812" },
{ name: ArmbandName.Orange, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/e6/ArmbandOrange.png/revision/latest?cb=20161127174846" },
{ name: ArmbandName.Pink, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/f7/ArmbandPink.png/revision/latest?cb=20161127174854" },
{ name: ArmbandName.Red, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/1/14/Armband.png/revision/latest?cb=20161127174901" },
{ name: ArmbandName.Yellow, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/8/81/ArmbandYellow.png/revision/latest?cb=20161127174918" },
{ name: ArmbandName.White, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c7/Armband_White.png/revision/latest?cb=20161127174926" },
{ name: ArmbandName.Altis, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/ee/Flag_alti_co.png/revision/latest?cb=20200820222622" },
{ name: ArmbandName.AsiainPacificAllianceAPA, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c2/Flag_apa_co.png/revision/latest?cb=20200820222623" },
{ name: ArmbandName.Bear, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/e1/Flag_bear_co.png/revision/latest?cb=20200820222626" },
{ name: ArmbandName.BohemiaInteractive, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/ee/Flag_bi_co.png/revision/latest?cb=20200820222627" },
{ name: ArmbandName.Brain, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/7/7d/Flag_brain_co.png/revision/latest?cb=20200820222628" },
{ name: ArmbandName.ChernarussianDefenceForcesCDF, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/d/d6/Flag_cdf_co.png/revision/latest?cb=20200820222629" },
{ name: ArmbandName.ChedakiCHED, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/9/96/Flag_ched_co.png/revision/latest?cb=20200820222630" },
{ name: ArmbandName.CHEL, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/9/98/Flag_chel_co.png/revision/latest?cb=20200820222631" },
{ name: ArmbandName.Chernarus, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/ef/Flag_chern_co.png/revision/latest?cb=20200820222632" },
{ name: ArmbandName.ChernarusMiningCorporationCMC, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/d/da/Flag_cmc_co.png/revision/latest?cb=20200820222634" },
{ name: ArmbandName.Rooster, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/4/44/Flag_cock_co.png/revision/latest?cb=20200820222635" },
{ name: ArmbandName.DayZ, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/b2/Flag_dayz_co.png/revision/latest?cb=20200820222636" },
{ name: ArmbandName.NorthSahraniDROS, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/2/24/Flag_dros_co.png/revision/latest?cb=20200820222637" },
{ name: ArmbandName.Fawn, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/d/d2/Flag_fawn_co.png/revision/latest/scale-to-width-down/1000?cb=20200820222639" },
{ name: ArmbandName.Pirates, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/ab/Flag_jolly_co.png/revision/latest?cb=20200820222643" },
{ name: ArmbandName.Cannibals, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/4/42/Flag_jolly_c_co.png/revision/latest?cb=20200820222641" },
{ name: ArmbandName.SouthSahraniKOS, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/a1/Flag_kos_co.png/revision/latest?cb=20200820222644" },
{ name: ArmbandName.LivoniaArmyLDF, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c1/Flag_ldf_co.png/revision/latest?cb=20200820222645" },
{ name: ArmbandName.Livonia, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/e6/Flag_livo_co.png/revision/latest?cb=20200820222647" },
{ name: ArmbandName.NAPA, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/e4/Flag_napa_co.png/revision/latest?cb=20200820222648" },
{ name: ArmbandName.LivoniaPolice, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/63/Flag_police_co.png/revision/latest?cb=20200820222649" },
{ name: ArmbandName.TEC, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/ea/Flag_tec_co.png/revision/latest?cb=20200820222650" },
{ name: ArmbandName.UnitedEarthCoalitionUEC, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/ca/Flag_uec_co.png/revision/latest?cb=20200820222651" },
{ name: ArmbandName.Wolf, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/b2/Flag_wolf_co.png/revision/latest?cb=20200820222653" },
{ name: ArmbandName.ZenitRadioStation, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/0/05/Flag_zenit_co.png/revision/latest?cb=20200820222654" },
{ name: ArmbandName.ZombieHunters, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/9/97/Flag_zhunters_co.png/revision/latest?cb=20200820222621" },
{ name: ArmbandName.RSTA, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/2/20/Flag_rsta_co.png/revision/latest/scale-to-width-down/1000?cb=20210216191221" },
{ name: ArmbandName.Refuge, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/8/8e/Flag_refuge_co.png/revision/latest/scale-to-width-down/1000?cb=20210216191205" },
{ name: ArmbandName.Snake, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/5/54/Flag_snake_co.png/revision/latest/scale-to-width-down/1000?cb=20210216191234" },
{ name: ArmbandName.Zagorky, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/7/75/Flag_zagorky_co.png/revision/latest/scale-to-width-down/1000?cb=20230619164704" },
{ name: ArmbandName.Crook, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c8/Flag_crook_co.png/revision/latest/scale-to-width-down/1000?cb=20230619164705" },
{ name: ArmbandName.Rex, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c5/Flag_rex_co.png/revision/latest/scale-to-width-down/1000?cb=20230619164706" },
];
+10 -6
View File
@@ -1,6 +1,7 @@
import { calculateVector } from "@util/Vector";
import { calculateVector } from "../util/Vector";
export const Missions = {
export const Missions =
{
"dayzOffline.chernarusplus": "Chernarus",
"dayzOffline.enoch": "Livonia",
"dayzOffline.sakhal": "Sakhal",
@@ -9,10 +10,11 @@ export const Missions = {
export type MissionKey = keyof typeof Missions;
export type MissionName = typeof Missions[MissionKey];
export type Position = [number, number];
export type Position = [number, number] | [];
// Calculates the nearest location to a given coordinate
export function nearest(pos: Position, mission: MissionName): string {
export function nearest(pos: Position, mission: MissionName): string
{
let tempDest;
let lastDist = 1000000;
let destination_dir;
@@ -28,13 +30,15 @@ export function nearest(pos: Position, mission: MissionName): string {
return lastDist > 500 ? `${destination_dir} of ${tempDest}` : `Near ${tempDest}`;
};
interface Destination {
interface Destination
{
name: string;
coord: Position;
}
// A curated list of destinations across DayZ Chernarus and Livonia
const destinations: Record<MissionName, Array<Destination>> = {
const destinations: Record<MissionName, Array<Destination>> =
{
Chernarus: [
{
name: "Sinystok",
+36 -20
View File
@@ -1,29 +1,43 @@
import { Snowflake } from "discord.js";
import DayZR from "../DayZRBot";
import { NitradoCredentialStatus } from "@util/NitradoAPI";
import { NitradoCredentialStatus } from "../util/NitradoAPI";
import { ArmbandName } from "./armbands";
import { MissionName } from "./destinations";
export const enum IntegerBoolean {
export const enum IntegerBoolean
{
FALSE,
TRUE
};
export interface UAV {
export interface UAV
{
// TODO: fill this out
};
export interface Alarm {
export interface Alarm
{
// TODO: fill this out
};
export interface NitradoConfig {
export interface NitradoConfig
{
ServerID: string;
UserID: string;
Auth: string;
Status: NitradoCredentialStatus;
Mission: MissionName,
};
interface GuildConfigAttributes {
serverID: Snowflake;
export interface FactionArmband
{
faction: Snowflake; // faction role ID
armband: ArmbandName;
}
interface GuildConfigAttributes
{
serverID: Snowflake; // Guild ID
lastLog: string | null;
serverName: string;
autoRestart: IntegerBoolean;
@@ -31,40 +45,41 @@ interface GuildConfigAttributes {
showKillfeedWeapon: IntegerBoolean;
purchaseUAV: IntegerBoolean;
purchaseEMP: IntegerBoolean;
allowedChannels: Array<Snowflake>;
allowedChannels: Array<Snowflake>; // Array of channel IDs
customChannelStatus?: boolean; // optional - generated outside of DB
hasBotAdmin?: boolean; // optional - generated outside of DB
killfeedChannel: Snowflake;
connectionLogsChannel: Snowflake;
activePlayersChannel: Snowflake;
welcomeChannel: Snowflake;
killfeedChannel: Snowflake; // Channel ID
connectionLogsChannel: Snowflake; // Channel ID
activePlayersChannel: Snowflake; // Channel ID
welcomeChannel: Snowflake; // Channel ID
factionArmbands: any;
factionArmbands: Record<Snowflake, FactionArmband>;
usedArmbands: Array<string>;
excludedRoles: Array<string>;
hasExcludedRoles?: boolean; // optional - generated outside of DB
botAdminRoles: Array<Snowflake>;
botAdminRoles: Array<Snowflake>; // Array of role IDs
alarms: Array<Alarm>;
events: Array<any>;
uavs: Array<UAV>;
incomeRoles: Array<Snowflake>;
incomeRoles: Array<Snowflake>; // Array of role IDs
incomeLimiter: number;
startingBalance: number;
uavPrice: number;
empPrice: number;
linkedGamertagRole: Snowflake;
memberRole: Snowflake;
adminRole: Snowflake;
linkedGamertagRole: Snowflake; // Role ID
memberRole: Snowflake; // Role ID
adminRole: Snowflake; // Role ID
combatLogTimer: number;
}
interface GuildConfigDB {
interface GuildConfigDB
{
server: GuildConfigAttributes;
Nitrado: NitradoConfig | null;
};
@@ -75,7 +90,8 @@ interface GuildConfigDB {
// Also this is so stupid, in the DB all server attributes are under server,
// while in the code we almost always have the attributes directly in top
// level of object, i.e. guild.startingBalance instead of guild.server.startingBalance
export interface GuildConfig extends GuildConfigAttributes {
export interface GuildConfig extends GuildConfigAttributes
{
Nitrado: NitradoConfig | null;
}
-131
View File
@@ -1,131 +0,0 @@
const { weapons } = require("./weapons");
// Creates a copy of an object to prevent mutation of parent (i.e BodyParts, createWeaponsObject)
const copy = (obj) => JSON.parse(JSON.stringify(obj));
const BodyParts = {
Head: 0,
Torso: 0,
RightArm: 0,
LeftArm: 0,
RightLeg: 0,
LeftLeg: 0,
};
const createWeaponsObject = (value) => {
const defaultWeapons = {};
for (const [_, weaponNames] of Object.entries(weapons)) {
for (const [name, _] of Object.entries(weaponNames)) {
defaultWeapons[name] = value;
}
}
return copy(defaultWeapons);
};
module.exports = {
UpdatePlayer: async (client, player, interaction = null) => {
/* Wrapping this function in a promise solves some bugs */
return new Promise(resolve => {
client.dbo.collection("players").updateOne(
{ "playerID": player.playerID },
{ $set: { ...player } },
{ upsert: true }, // Create player stat document if it does not exist
(err, _) => {
if (err) {
if (interaction == null) return client.error(`UpdatePlayer Error: ${err}`);
else return client.sendInternalError(interaction, `UpdatePlayer Error: ${err}`);
} else resolve();
}
);
});
},
getDefaultPlayer(gamertag, playerId, nitradoServerId) {
return {
// Identifiers
gamertag: gamertag,
playerID: playerId,
discordID: "",
nitradoServerID: nitradoServerId,
// General PVP Stats
KDR: 0.00,
kills: 0,
deaths: 0,
killStreak: 0,
bestKillStreak: 0,
longestKill: 0,
deathStreak: 0,
worstDeathStreak: 0,
// In depth PVP Stats
shotsLanded: 0,
timesShot: 0,
shotsLandedPerBodyPart: copy(BodyParts),
timesShotPerBodyPart: copy(BodyParts),
weaponStats: createWeaponsObject({
kills: 0,
deaths: 0,
shotsLanded: 0,
timesShot: 0,
shotsLandedPerBodyPart: copy(BodyParts),
timesShotPerBodyPart: copy(BodyParts),
}),
combatRating: 800,
highestCombatRating: 800,
lowestCombatRating: 800,
combatRatingHistory: [800],
// General Session Data
lastConnectionDate: null,
lastDisconnectionDate: null,
lastDamageDate: null,
lastDeathDate: null,
lastHitBy: null,
connected: false,
pos: [],
lastPos: [],
time: null,
lastTime: null,
// Session Stats
totalSessionTime: 0,
lastSessionTime: 0,
longestSessionTime: 0,
connections: 0,
// Other
bounties: [],
bountiesLength: 0,
}
},
insertPVPstats(player) {
player.shotsLanded = 0;
player.timesShot = 0;
player.shotsLandedPerBodyPart = copy(BodyParts);
player.timesShotPerBodyPart = copy(BodyParts);
player.weaponStats = createWeaponsObject({
kills: 0,
deaths: 0,
shotsLanded: 0,
timesShot: 0,
shotsLandedPerBodyPart: copy(BodyParts),
timesShotPerBodyPart: copy(BodyParts),
});
return player;
},
// If a new weapon is not in the existing weaponStats, this will add it.
createWeaponStats(player, weapon) {
player.weaponStats[weapon] = {
kills: 0,
deaths: 0,
shotsLanded: 0,
timesShot: 0,
shotsLandedPerBodyPart: copy(BodyParts),
timesShotPerBodyPart: copy(BodyParts),
}
return player;
}
}
+232
View File
@@ -0,0 +1,232 @@
import { ChatInputCommandInteraction } from "discord.js";
import { weapons, AllWeapons } from "./weapons";
import { Snowflake } from "discord.js";
import { Position } from "./destinations"
import DayZR from "../DayZRBot";
// Creates a copy of an object to prevent mutation of parent (i.e BodyParts, createWeaponsObject)
const copy = <T>(obj: T): T => JSON.parse(JSON.stringify(obj));
interface BodyPartStats
{
Head: number;
Torso: number;
RightArm: number;
LeftArm: number;
RightLeg: number;
LeftLeg: number;
}
interface IndividualWeaponStats
{
kills: number;
deaths: number;
shotsLanded: number;
timesShot: number;
shotsLandedPerBodyPart: BodyPartStats;
timesShotPerBodyPart: BodyPartStats;
}
const DefaultBodyPartStats: BodyPartStats =
{
Head: 0,
Torso: 0,
RightArm: 0,
LeftArm: 0,
RightLeg: 0,
LeftLeg: 0,
};
interface WeaponStats
{
[weaponName: string]: IndividualWeaponStats;
}
const createWeaponsObject = (defaultWeaponStats: IndividualWeaponStats): WeaponStats =>
{
const defaultWeapons: WeaponStats = {};
for (const [_, weaponNames] of Object.entries(weapons)) {
for (const [name, _] of Object.entries(weaponNames)) {
defaultWeapons[name] = defaultWeaponStats;
}
}
return copy(defaultWeapons);
};
export interface Player
{
// Identifiers
gamertag: string,
playerID: string,
discordID: Snowflake,
nitradoServerID: string,
// General PVP Stats
KDR: number,
kills: number,
deaths: number,
killStreak: number,
bestKillStreak: number,
longestKill: number,
deathStreak: number,
worstDeathStreak: number,
// In depth PVP Stats
shotsLanded: number,
timesShot: number,
shotsLandedPerBodyPart: BodyPartStats,
timesShotPerBodyPart: BodyPartStats,
weaponStats: WeaponStats,
combatRating: number,
highestCombatRating: number,
lowestCombatRating: number,
combatRatingHistory: Array<number>,
// General Session Data
lastConnectionDate: Date | null,
lastDisconnectionDate: Date | null,
lastDamageDate: Date | null,
lastDeathDate: Date | null,
lastHitBy: string | null,
connected: boolean,
pos: Position,
lastPos: Position,
time: string | null,
lastTime: string | null,
// Session Stats
totalSessionTime: number,
lastSessionTime: number,
longestSessionTime: number,
connections: number,
// Other
bounties: Array<any>, // TODO, define bounty object
bountiesLength: number,
}
export const UpdatePlayer = async(
client: DayZR,
player: Player,
interaction: ChatInputCommandInteraction | null = null
): Promise<void> =>
{
/* Wrapping this function in a promise solves some bugs */
return new Promise(resolve => {
client.dbo.collection("players").updateOne(
{ "playerID": player.playerID },
{ $set: { ...player } },
{ upsert: true }, // Create player stat document if it does not exist
(err: string) => {
if (err)
{
if (interaction == null) return client.error(`UpdatePlayer Error: ${err}`);
else return client.sendInternalError(interaction, `UpdatePlayer Error: ${err}`);
} else resolve();
}
);
});
};
// TODO: fix magic NUMBERS!!!
export const getDefaultPlayer = (
gamertag: string,
playerId: string,
nitradoServerId: string
): Player =>
{
return {
// Identifiers
gamertag: gamertag,
playerID: playerId,
discordID: "",
nitradoServerID: nitradoServerId,
// General PVP Stats
KDR: 0.00,
kills: 0,
deaths: 0,
killStreak: 0,
bestKillStreak: 0,
longestKill: 0,
deathStreak: 0,
worstDeathStreak: 0,
// In depth PVP Stats
shotsLanded: 0,
timesShot: 0,
shotsLandedPerBodyPart: copy(DefaultBodyPartStats),
timesShotPerBodyPart: copy(DefaultBodyPartStats),
weaponStats: createWeaponsObject({
kills: 0,
deaths: 0,
shotsLanded: 0,
timesShot: 0,
shotsLandedPerBodyPart: copy(DefaultBodyPartStats),
timesShotPerBodyPart: copy(DefaultBodyPartStats),
}),
combatRating: 800,
highestCombatRating: 800,
lowestCombatRating: 800,
combatRatingHistory: [ 800 ],
// General Session Data
lastConnectionDate: null,
lastDisconnectionDate: null,
lastDamageDate: null,
lastDeathDate: null,
lastHitBy: null,
connected: false,
pos: [],
lastPos: [],
time: null,
lastTime: null,
// Session Stats
totalSessionTime: 0,
lastSessionTime: 0,
longestSessionTime: 0,
connections: 0,
// Other
bounties: [],
bountiesLength: 0,
}
};
export const insertPVPstats = (player: Player): Player =>
{
player.shotsLanded = 0;
player.timesShot = 0;
player.shotsLandedPerBodyPart = copy(DefaultBodyPartStats);
player.timesShotPerBodyPart = copy(DefaultBodyPartStats);
player.weaponStats = createWeaponsObject(
{
kills: 0,
deaths: 0,
shotsLanded: 0,
timesShot: 0,
shotsLandedPerBodyPart: copy(DefaultBodyPartStats),
timesShotPerBodyPart: copy(DefaultBodyPartStats),
});
return player;
};
// If a new weapon is not in the existing weaponStats, this will add it.
export const createWeaponStats = (
player: Player,
weapon: AllWeapons
): Player =>
{
player.weaponStats[weapon] = {
kills: 0,
deaths: 0,
shotsLanded: 0,
timesShot: 0,
shotsLandedPerBodyPart: copy(DefaultBodyPartStats),
timesShotPerBodyPart: copy(DefaultBodyPartStats),
}
return player;
};
-77
View File
@@ -1,77 +0,0 @@
module.exports = {
weapons: {
handguns: {
"CR-75": "https://static.wikia.nocookie.net/dayz_gamepedia/images/4/40/CZ75.png/revision/latest/scale-to-width-down/112?cb=20210505021307",
"Deagle": "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/62/Deagle.png/revision/latest/scale-to-width-down/127?cb=20210512003023",
"Derringer": "https://static.wikia.nocookie.net/dayz_gamepedia/images/9/9f/Derringer_Black.png/revision/latest/scale-to-width-down/105?cb=20220521175445",
"FX-45": "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/fd/FNX45.png/revision/latest/scale-to-width-down/104?cb=20210505025055",
"IJ-70": "https://static.wikia.nocookie.net/dayz_gamepedia/images/2/26/MakarovIJ70.png/revision/latest/scale-to-width-down/92?cb=20210209000551",
"Kolt 1911": "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/f9/Colt1911.png/revision/latest/scale-to-width-down/112?cb=20210505030200",
"Longhorn": "https://static.wikia.nocookie.net/dayz_gamepedia/images/7/79/Longhorn.png/revision/latest/scale-to-width-down/222?cb=20220324214533",
"MK II": "https://static.wikia.nocookie.net/dayz_gamepedia/images/0/0d/MKII.png/revision/latest/scale-to-width-down/171?cb=20210210153348",
"Mlock-91": "https://static.wikia.nocookie.net/dayz_gamepedia/images/9/9b/Glock19.png/revision/latest/scale-to-width-down/121?cb=20210505024259",
"P1": "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/cc/P1.png/revision/latest/scale-to-width-down/120?cb=20220518204515",
"Revolver": "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/6d/Revolver.png/revision/latest/scale-to-width-down/148?cb=20210208232303",
"Signal Pistol": "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/a7/Flaregun.png/revision/latest/scale-to-width-down/107?cb=20210501150913",
},
shotguns: {
"BK-12": "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/cb/Izh18Shotgun.png/revision/latest/scale-to-width-down/256?cb=20220922184507",
"BK-133": "https://static.wikia.nocookie.net/dayz_gamepedia/images/5/5c/MP-133-Shotgun.png/revision/latest/scale-to-width-down/256?cb=20210210190104",
"BK-43": "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c7/Izh43Shotgun.png/revision/latest/scale-to-width-down/256?cb=20210210185835",
"Vaiga": "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c8/Vaiga.png/revision/latest/scale-to-width-down/256?cb=20220220185225",
},
subMachineGuns: {
"Bizon": "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/af/PP19.png/revision/latest/scale-to-width-down/251?cb=20220127132305",
"CR-61 Skorpion": "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/63/VZ61Scorpion.png/revision/latest/scale-to-width-down/222?cb=20220518204508",
"SG5-K": "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/fc/MP5-K.png/revision/latest/scale-to-width-down/158?cb=20220221011343",
"USG-45": "https://static.wikia.nocookie.net/dayz_gamepedia/images/d/d7/UMP45.png/revision/latest/scale-to-width-down/153?cb=20220221002354",
},
assaultRifles: {
"AUR A1": "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/e6/AugShort.png/revision/latest/scale-to-width-down/173?cb=20211104175243",
"AUR AX": "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/be/Aug.png/revision/latest/scale-to-width-down/233?cb=20211104182427",
"KA-101": "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/f2/AK101.png/revision/latest/scale-to-width-down/251?cb=20210207040122",
"KA-74": "https://static.wikia.nocookie.net/dayz_gamepedia/images/8/8b/AK74.png/revision/latest/scale-to-width-down/253?cb=20210505013141",
"KAS-74U": "https://static.wikia.nocookie.net/dayz_gamepedia/images/0/0b/AKS74U.png/revision/latest/scale-to-width-down/191?cb=20210505014222",
"KA-M": "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/6c/AKM.png/revision/latest/scale-to-width-down/244?cb=20210505011614",
"LE-MAS": "https://static.wikia.nocookie.net/dayz_gamepedia/images/2/21/FAMAS.png/revision/latest/scale-to-width-down/197?cb=20210902183114",
"M16-A2": "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/b3/M16-A2.png/revision/latest/scale-to-width-down/256?cb=20220221002601",
"M4-A1": "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/a1/M4A1.png/revision/latest/scale-to-width-down/223?cb=20220330014851",
"SVAL": "https://static.wikia.nocookie.net/dayz_gamepedia/images/3/39/ASVAL.png/revision/latest/scale-to-width-down/256?cb=20210208015731",
"Vikhr": "https://static.wikia.nocookie.net/dayz_gamepedia/images/0/0d/Vikhr.png/revision/latest/scale-to-width-down/173?cb=20240116163108"
},
battleRifles: {
"LAR": "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/e9/FAL.png/revision/latest/scale-to-width-down/256?cb=20220221001123",
},
boltActionRifles: {
"CR-527": "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/f0/CR527Wood.png/revision/latest/scale-to-width-down/256?cb=20220518204503 ",
"CR-550 Savanna": "https://static.wikia.nocookie.net/dayz_gamepedia/images/4/44/CR-550_Savanna.png/revision/latest/scale-to-width-down/256?cb=20220518204410",
"M70 Tundra": "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/62/Winchester70.png/revision/latest/scale-to-width-down/256?cb=20220517152918",
"Mosin 91/30": "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/a8/Mosin9130.png/revision/latest/scale-to-width-down/256?cb=20230126021955",
"Pioneer": "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/69/Scout.png/revision/latest/scale-to-width-down/256?cb=20220518204357",
"SSG 82": "https://static.wikia.nocookie.net/dayz_gamepedia/images/1/10/SSG82.png/revision/latest/scale-to-width-down/256?cb=20220922192455",
"VS-89": "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/ea/SV98.png/revision/latest/scale-to-width-down/256?cb=20240424164607",
},
breakActionRifles: {
"BK-18": "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/cb/IZH18_Rifle.png/revision/latest/scale-to-width-down/256?cb=20220517154121",
"Blaze": "https://static.wikia.nocookie.net/dayz_gamepedia/images/8/8a/Blaze_95_Double_Rifle_Wood.png/revision/latest/scale-to-width-down/256?cb=20220517154129",
},
leverActionRifles: {
"Repeater Carbine": "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/ce/Repeater.png/revision/latest/scale-to-width-down/256?cb=20220517154151",
},
marksmanRifles: {
"VSD": "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/a2/SVD_w._PSO-1.png/revision/latest/scale-to-width-down/256?cb=20220220235826",
"VSS": "https://static.wikia.nocookie.net/dayz_gamepedia/images/8/83/VSSVintorez.png/revision/latest/scale-to-width-down/256?cb=20210208202042",
},
semiAutomaticRifles: {
"DMR": "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/b4/M14.png/revision/latest/scale-to-width-down/350?cb=20231005142636",
"SK 59/66": "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/fe/SKS.png/revision/latest/scale-to-width-down/256?cb=20220517154633",
"Sporter 22": "https://static.wikia.nocookie.net/dayz_gamepedia/images/5/5b/Sporter_22_Wood.png/revision/latest/scale-to-width-down/256?cb=20220518204154",
},
other: {
"Crossbow": "https://static.wikia.nocookie.net/dayz_gamepedia/images/7/79/Crossbow.png/revision/latest/scale-to-width-down/212?cb=20180121164101",
"M79": "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/b7/M79.png/revision/latest/scale-to-width-down/256?cb=20220521184052",
},
},
weaponClassOf: (weapon) => Object.keys(module.exports.weapons).filter(c => weapon in module.exports.weapons[c])[0],
}
+149
View File
@@ -0,0 +1,149 @@
export type WeaponCategory =
| "handguns"
| "shotguns"
| "subMachineGuns"
| "assaultRifles"
| "battleRifles"
| "boltActionRifles"
| "breakActionRifles"
| "leverActionRifles"
| "marksmanRifles"
| "semiAutomaticRifles"
| "other";
export type HandgunWeapon =
| "CR-75" | "Deagle" | "Derringer" | "FX-45" | "IJ-70" | "Kolt 1911"
| "Longhorn" | "MK II" | "Mlock-91" | "P1" | "Revolver" | "Signal Pistol";
export type ShotgunWeapon =
| "BK-12" | "BK-133" | "BK-43" | "Vaiga";
export type SubMachineGunWeapon =
| "Bizon" | "CR-61 Skorpion" | "SG5-K" | "USG-45";
export type AssaultRifleWeapon =
| "AUR A1" | "AUR AX" | "KA-101" | "KA-74" | "KAS-74U" | "KA-M"
| "LE-MAS" | "M16-A2" | "M4-A1" | "SVAL" | "Vikhr";
export type BattleRifleWeapon = "LAR";
export type BoltActionRifleWeapon =
| "CR-527" | "CR-550 Savanna" | "M70 Tundra" | "Mosin 91/30"
| "Pioneer" | "SSG 82" | "VS-89";
export type BreakActionRifleWeapon = "BK-18" | "Blaze";
export type LeverActionRifleWeapon = "Repeater Carbine";
export type MarksmanRifleWeapon = "VSD" | "VSS";
export type SemiAutomaticRifleWeapon = "DMR" | "SK 59/66" | "Sporter 22";
export type OtherWeapon = "Crossbow" | "M79";
export interface Weapons
{
handguns: Record<HandgunWeapon, string>;
shotguns: Record<ShotgunWeapon, string>;
subMachineGuns: Record<SubMachineGunWeapon, string>;
assaultRifles: Record<AssaultRifleWeapon, string>;
battleRifles: Record<BattleRifleWeapon, string>;
boltActionRifles: Record<BoltActionRifleWeapon, string>;
breakActionRifles: Record<BreakActionRifleWeapon, string>;
leverActionRifles: Record<LeverActionRifleWeapon, string>;
marksmanRifles: Record<MarksmanRifleWeapon, string>;
semiAutomaticRifles: Record<SemiAutomaticRifleWeapon, string>;
other: Record<OtherWeapon, string>;
}
export type AllWeapons =
| HandgunWeapon
| ShotgunWeapon
| SubMachineGunWeapon
| AssaultRifleWeapon
| BattleRifleWeapon
| BoltActionRifleWeapon
| BreakActionRifleWeapon
| LeverActionRifleWeapon
| MarksmanRifleWeapon
| SemiAutomaticRifleWeapon
| OtherWeapon;
export const weapons: Weapons =
{
handguns: {
"CR-75": "https://static.wikia.nocookie.net/dayz_gamepedia/images/4/40/CZ75.png/revision/latest/scale-to-width-down/112?cb=20210505021307",
"Deagle": "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/62/Deagle.png/revision/latest/scale-to-width-down/127?cb=20210512003023",
"Derringer": "https://static.wikia.nocookie.net/dayz_gamepedia/images/9/9f/Derringer_Black.png/revision/latest/scale-to-width-down/105?cb=20220521175445",
"FX-45": "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/fd/FNX45.png/revision/latest/scale-to-width-down/104?cb=20210505025055",
"IJ-70": "https://static.wikia.nocookie.net/dayz_gamepedia/images/2/26/MakarovIJ70.png/revision/latest/scale-to-width-down/92?cb=20210209000551",
"Kolt 1911": "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/f9/Colt1911.png/revision/latest/scale-to-width-down/112?cb=20210505030200",
"Longhorn": "https://static.wikia.nocookie.net/dayz_gamepedia/images/7/79/Longhorn.png/revision/latest/scale-to-width-down/222?cb=20220324214533",
"MK II": "https://static.wikia.nocookie.net/dayz_gamepedia/images/0/0d/MKII.png/revision/latest/scale-to-width-down/171?cb=20210210153348",
"Mlock-91": "https://static.wikia.nocookie.net/dayz_gamepedia/images/9/9b/Glock19.png/revision/latest/scale-to-width-down/121?cb=20210505024259",
"P1": "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/cc/P1.png/revision/latest/scale-to-width-down/120?cb=20220518204515",
"Revolver": "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/6d/Revolver.png/revision/latest/scale-to-width-down/148?cb=20210208232303",
"Signal Pistol": "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/a7/Flaregun.png/revision/latest/scale-to-width-down/107?cb=20210501150913",
},
shotguns: {
"BK-12": "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/cb/Izh18Shotgun.png/revision/latest/scale-to-width-down/256?cb=20220922184507",
"BK-133": "https://static.wikia.nocookie.net/dayz_gamepedia/images/5/5c/MP-133-Shotgun.png/revision/latest/scale-to-width-down/256?cb=20210210190104",
"BK-43": "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c7/Izh43Shotgun.png/revision/latest/scale-to-width-down/256?cb=20210210185835",
"Vaiga": "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c8/Vaiga.png/revision/latest/scale-to-width-down/256?cb=20220220185225",
},
subMachineGuns: {
"Bizon": "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/af/PP19.png/revision/latest/scale-to-width-down/251?cb=20220127132305",
"CR-61 Skorpion": "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/63/VZ61Scorpion.png/revision/latest/scale-to-width-down/222?cb=20220518204508",
"SG5-K": "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/fc/MP5-K.png/revision/latest/scale-to-width-down/158?cb=20220221011343",
"USG-45": "https://static.wikia.nocookie.net/dayz_gamepedia/images/d/d7/UMP45.png/revision/latest/scale-to-width-down/153?cb=20220221002354",
},
assaultRifles: {
"AUR A1": "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/e6/AugShort.png/revision/latest/scale-to-width-down/173?cb=20211104175243",
"AUR AX": "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/be/Aug.png/revision/latest/scale-to-width-down/233?cb=20211104182427",
"KA-101": "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/f2/AK101.png/revision/latest/scale-to-width-down/251?cb=20210207040122",
"KA-74": "https://static.wikia.nocookie.net/dayz_gamepedia/images/8/8b/AK74.png/revision/latest/scale-to-width-down/253?cb=20210505013141",
"KAS-74U": "https://static.wikia.nocookie.net/dayz_gamepedia/images/0/0b/AKS74U.png/revision/latest/scale-to-width-down/191?cb=20210505014222",
"KA-M": "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/6c/AKM.png/revision/latest/scale-to-width-down/244?cb=20210505011614",
"LE-MAS": "https://static.wikia.nocookie.net/dayz_gamepedia/images/2/21/FAMAS.png/revision/latest/scale-to-width-down/197?cb=20210902183114",
"M16-A2": "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/b3/M16-A2.png/revision/latest/scale-to-width-down/256?cb=20220221002601",
"M4-A1": "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/a1/M4A1.png/revision/latest/scale-to-width-down/223?cb=20220330014851",
"SVAL": "https://static.wikia.nocookie.net/dayz_gamepedia/images/3/39/ASVAL.png/revision/latest/scale-to-width-down/256?cb=20210208015731",
"Vikhr": "https://static.wikia.nocookie.net/dayz_gamepedia/images/0/0d/Vikhr.png/revision/latest/scale-to-width-down/173?cb=20240116163108",
},
battleRifles: {
"LAR": "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/e9/FAL.png/revision/latest/scale-to-width-down/256?cb=20220221001123",
},
boltActionRifles: {
"CR-527": "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/f0/CR527Wood.png/revision/latest/scale-to-width-down/256?cb=20220518204503",
"CR-550 Savanna": "https://static.wikia.nocookie.net/dayz_gamepedia/images/4/44/CR-550_Savanna.png/revision/latest/scale-to-width-down/256?cb=20220518204410",
"M70 Tundra": "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/62/Winchester70.png/revision/latest/scale-to-width-down/256?cb=20220517152918",
"Mosin 91/30": "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/a8/Mosin9130.png/revision/latest/scale-to-width-down/256?cb=20230126021955",
"Pioneer": "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/69/Scout.png/revision/latest/scale-to-width-down/256?cb=20220518204357",
"SSG 82": "https://static.wikia.nocookie.net/dayz_gamepedia/images/1/10/SSG82.png/revision/latest/scale-to-width-down/256?cb=20220922192455",
"VS-89": "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/ea/SV98.png/revision/latest/scale-to-width-down/256?cb=20240424164607",
},
breakActionRifles: {
"BK-18": "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/cb/IZH18_Rifle.png/revision/latest/scale-to-width-down/256?cb=20220517154121",
"Blaze": "https://static.wikia.nocookie.net/dayz_gamepedia/images/8/8a/Blaze_95_Double_Rifle_Wood.png/revision/latest/scale-to-width-down/256?cb=20220517154129",
},
leverActionRifles: {
"Repeater Carbine": "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/ce/Repeater.png/revision/latest/scale-to-width-down/256?cb=20220517154151",
},
marksmanRifles: {
"VSD": "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/a2/SVD_w._PSO-1.png/revision/latest/scale-to-width-down/256?cb=20220220235826",
"VSS": "https://static.wikia.nocookie.net/dayz_gamepedia/images/8/83/VSSVintorez.png/revision/latest/scale-to-width-down/256?cb=20210208202042",
},
semiAutomaticRifles: {
"DMR": "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/b4/M14.png/revision/latest/scale-to-width-down/350?cb=20231005142636",
"SK 59/66": "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/fe/SKS.png/revision/latest/scale-to-width-down/256?cb=20220517154633",
"Sporter 22": "https://static.wikia.nocookie.net/dayz_gamepedia/images/5/5b/Sporter_22_Wood.png/revision/latest/scale-to-width-down/256?cb=20220518204154",
},
other: {
"Crossbow": "https://static.wikia.nocookie.net/dayz_gamepedia/images/7/79/Crossbow.png/revision/latest/scale-to-width-down/212?cb=20180121164101",
"M79": "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/b7/M79.png/revision/latest/scale-to-width-down/256?cb=20220521184052",
},
};
export const weaponClassOf = (weapon: AllWeapons): WeaponCategory | undefined => {
return (Object.keys(weapons) as WeaponCategory[]).find((category) => weapon in weapons[category]);
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { ShardingManager } from "discord.js";
import config from "./config/config";
import { config } from "./config/config";
// ShardingManager spawns node instances and we need to explicitely point to dist/bot.js
const manager: ShardingManager = new ShardingManager("./dist/bot.js", { token: config.Token });
+23
View File
@@ -0,0 +1,23 @@
import { bgYellow } from "colors";
import "discord.js";
/**
* Type augmentation allows us to add custom functions to the
* ChatInputCommandInteraction without typescript complaining
*/
declare module "discord.js" {
interface ChatInputCommandInteraction {
send(
content: string | { embeds?: (EmbedBuilder | APIEmbed)[]; [key: string]: any }
): Promise<unknown>;
deferReply(
content?: string | { embeds?: (EmbedBuilder | APIEmbed)[]; [key: string]: any }
): Promise<unknown>;
showModal(
content: string | { embeds?: (EmbedBuilder | APIEmbed)[]; [key: string]: any }
): Promise<unknown>;
editReply(
content: string | { embeds?: (EmbedBuilder | APIEmbed)[]; [key: string]: any }
): Promise<unknown>;
}
}
+12
View File
@@ -0,0 +1,12 @@
import { Readable } from "stream";
// cannot get a warning to go away, this is like the only solution
declare module "node:readline" {
import { Interface, ReadLineOptions } from "readline";
interface ReadLineOptionsFixed extends ReadLineOptions {
input: Readable; // 👈 force Node stream
}
export function createInterface(options: ReadLineOptionsFixed): Interface;
}
+17 -13
View File
@@ -1,40 +1,44 @@
const winston = require("winston");
const colors = require("colors");
import winston from "winston";
import colors from "colors";
class Logger {
constructor(LoggingFile) {
export default class Logger {
public logger: winston.Logger;
constructor(loggingFile: string)
{
this.logger = winston.createLogger({
transports: [new winston.transports.File({ filename: LoggingFile })],
transports: [new winston.transports.File({ filename: loggingFile })],
});
}
log(Text) {
log(text: string): void
{
let d = new Date();
this.logger.log({
level: "info",
message:
`${d.getHours()}:${d.getMinutes()} - ${d.getMonth() + 1}:${d.getDate()}:${d.getFullYear()} | Info: ` + Text
`${d.getHours()}:${d.getMinutes()} - ${d.getMonth() + 1}:${d.getDate()}:${d.getFullYear()} | Info: ` + text
});
console.log(
colors.green(
`${d.getMonth() + 1}:${d.getDate()}:${d.getFullYear()} - ${d.getHours()}:${d.getMinutes()}`
) + colors.yellow(" | Info: " + Text)
) + colors.yellow(" | Info: " + text)
);
}
error(Text) {
error(text: string): void
{
let d = new Date();
this.logger.log({
level: "error",
message:
`${d.getHours()}:${d.getMinutes()} - ${d.getMonth() + 1}:${d.getDate()}:${d.getFullYear()} | Error: ` + Text
`${d.getHours()}:${d.getMinutes()} - ${d.getMonth() + 1}:${d.getDate()}:${d.getFullYear()} | Error: ` + text
});
console.log(
colors.green(
`${d.getMonth() + 1}:${d.getDate()}:${d.getFullYear()} - ${d.getHours()}:${d.getMinutes()}`
) + colors.yellow(" | Error: ") + colors.red(Text)
) + colors.yellow(" | Error: ") + colors.red(text)
);
}
}
module.exports = Logger;
+7 -4
View File
@@ -10,12 +10,15 @@
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"baseUrl": "./src",
"lib": ["es2020"],
"types": ["node"],
"moduleResolution": "node",
"baseUrl": ".",
"paths": {
"@util/*": ["util/*"],
"@db/*": ["database/*"]
"@util/*": ["src/util/*"],
"@db/*": ["src/database/*"]
}
},
"include": ["src"],
"include": ["src", "src/types"],
"exclude": ["node_modules"]
}