diff --git a/.editorconfig b/.editorconfig index 0030250..ff6d750 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,5 +1,3 @@ -root = true - [*] end_of_line=lf insert_final_newline=true diff --git a/.env.example b/.env.example index ee83b44..069ad80 100644 --- a/.env.example +++ b/.env.example @@ -33,6 +33,9 @@ DEBUGLEVEL=1 FREEZONE_PASS="" DEDIMANIA_PASS="" +# Use Celyans emotes in TMNF chat, for details: http://bit.ly/Celyans_emotes_sheet +CHAT_USE_EMOTES=false + # uncomment next line to force openplanet mode for tm2020: # FORCE_OP_MODE="OFFICIAL" diff --git a/.eslintrc.yml b/.eslintrc.yml deleted file mode 100644 index eba2376..0000000 --- a/.eslintrc.yml +++ /dev/null @@ -1,10 +0,0 @@ -root: true -env: - - es2020: true -extends: - - "plugin:drizzle/recommended" -parser: '@typescript-eslint/parser' -parserOptions: - project: './tsconfig.json' -plugins: - - drizzle \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..fa22b9a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "npm.exclude": "", + "search.exclude": { + "**/node_modules/**": true + }, + "files.exclude": { + "**/node_modules/**": true + }, + "prettier.printWidth": 180 +} \ No newline at end of file diff --git a/core/commandmanager.ts b/core/commandmanager.ts index a8eceb6..33534f5 100644 --- a/core/commandmanager.ts +++ b/core/commandmanager.ts @@ -1,12 +1,11 @@ -import ListWindow from "./ui/listwindow"; -import { escapeRegex, sleep } from "./utils"; +import ListWindow from './ui/listwindow'; +import { escapeRegex, sleep } from './utils'; import fs from 'fs'; export interface CallableCommand { (login: string, args: string[]): Promise; } - export interface ChatCommand { admin: boolean; callback: CallableCommand; @@ -14,7 +13,6 @@ export interface ChatCommand { trigger: string; } - /** * CommandManager class */ @@ -26,31 +24,49 @@ export default class CommandManager { * @ignore */ async beforeInit() { - this.addCommand("/help", async (login: string, args: string[]) => { - let help = "Available: \n"; - for (let command in this.commands) { - if (this.commands[command]?.admin) continue; - help += `¤cmd¤${this.commands[command]?.trigger} ¤white¤${this.commands[command]?.help}, `; - } - tmc.chat(help, login); - }, "Display help for command"); + this.addCommand( + '/help', + async (login: string, args: string[]) => { + let help = 'Available: \n'; + for (let command in this.commands) { + if (this.commands[command]?.admin) continue; + help += `¤cmd¤${this.commands[command]?.trigger} ¤white¤${this.commands[command]?.help}, `; + } + tmc.chat(help, login); + }, + 'Display help for command' + ); - this.addCommand("//help", async (login: string, args: string[]) => { - let help = "Available: \n"; - for (let command in this.commands) { - if (!this.commands[command]?.admin) continue; - help += `¤cmd¤${this.commands[command].trigger} ¤white¤${this.commands[command].help}, `; - } - tmc.chat(help, login); - }, "Display help for command"); + this.addCommand( + '//help', + async (login: string, args: string[]) => { + let help = 'Available: \n'; + for (let command in this.commands) { + if (!this.commands[command]?.admin) continue; + help += `¤cmd¤${this.commands[command].trigger} ¤white¤${this.commands[command].help}, `; + } + tmc.chat(help, login); + }, + 'Display help for command' + ); - this.addCommand("/serverlogin", async () => { }, "Display server login"); - this.addCommand("/version", async (login: string) => { - tmc.chat(`MiniController version: ${tmc.version}`, login); - }, "Display server versions"); - this.addCommand("//shutdown", async () => { process.exit() }, "Close MINIcontroller"); - this.addCommand("//plugins", this.cmdPluginManager.bind(this), "Open plugin manager"); - this.addCommand("//plugin", async (login: string, args: string[]) => { + this.addCommand('/serverlogin', async () => {}, 'Display server login'); + this.addCommand( + '/version', + async (login: string) => { + tmc.chat(`MiniController version: ${tmc.version}`, login); + }, + 'Display server versions' + ); + this.addCommand( + '//shutdown', + async () => { + process.exit(); + }, + 'Close MINIcontroller' + ); + //this.addCommand("//plugins", this.cmdPluginManager.bind(this), "Open plugin manager"); + /*this.addCommand("//plugin", async (login: string, args: string[]) => { if (args.length < 1) { tmc.chat("Valid options are: list, load, unload, reload", login); return; @@ -117,76 +133,80 @@ export default class CommandManager { } } }, "Manage plugins"); - tmc.addCommand("//admin", async (login: string, args: string[]) => { - if (args.length < 1) { - tmc.chat("¤white¤Valid options are: ¤cmd¤list¤white¤, ¤cmd¤add¤white¤, ¤cmd¤remove", login); - return; - } - const action = args[0]; - switch (action) { - case "list": { - let admins = "Admins: "; - for (let admin of tmc.admins) { - admins += `¤cmd¤${admin}¤white¤, `; - } - tmc.chat(admins, login); - break; + */ + tmc.addCommand( + '//admin', + async (login: string, args: string[]) => { + if (args.length < 1) { + tmc.chat('¤white¤Valid options are: ¤cmd¤list¤white¤, ¤cmd¤add¤white¤, ¤cmd¤remove', login); + return; } - case "add": { - if (args.length < 2) { - tmc.chat("¤info¤Please specify a login.", login); - return; + const action = args[0]; + switch (action) { + case 'list': { + let admins = 'Admins: '; + for (let admin of tmc.admins) { + admins += `¤cmd¤${admin}¤white¤, `; + } + tmc.chat(admins, login); + break; } - const admin = args[1]; - if (tmc.admins.includes(admin)) { - tmc.chat(`¤info¤Admin ¤white¤${admin}¤info¤ already exists.`, login); - return; + case 'add': { + if (args.length < 2) { + tmc.chat('¤info¤Please specify a login.', login); + return; + } + const admin = args[1]; + if (tmc.admins.includes(admin)) { + tmc.chat(`¤info¤Admin ¤white¤${admin}¤info¤ already exists.`, login); + return; + } + tmc.settingsMgr.addAdmin(admin); + tmc.chat(`¤info¤Admin ¤white¤${admin}¤info¤ added.`, login); + break; } - tmc.settingsMgr.addAdmin(admin); - tmc.chat(`¤info¤Admin ¤white¤${admin}¤info¤ added.`, login); - break; - } - case "remove": { - if (args.length < 2) { - tmc.chat("¤info¤Please specify a login.", login); - return; + case 'remove': { + if (args.length < 2) { + tmc.chat('¤info¤Please specify a login.', login); + return; + } + const admin = args[1]; + if (!tmc.admins.includes(admin)) { + tmc.chat(`¤info¤Admin ¤white¤${admin} ¤info¤does not exist.`, login); + return; + } + tmc.settingsMgr.removeAdmin(admin); + tmc.chat(`¤info¤Admin ¤white¤${admin} ¤info¤removed.`, login); + break; } - const admin = args[1]; - if (!tmc.admins.includes(admin)) { - tmc.chat(`¤info¤Admin ¤white¤${admin} ¤info¤does not exist.`, login); - return; + default: { + tmc.chat('¤white¤Valid options are: ¤cmd¤list¤white¤, ¤cmd¤add¤white¤, ¤cmd¤remove', login); } - tmc.settingsMgr.removeAdmin(admin); - tmc.chat(`¤info¤Admin ¤white¤${admin} ¤info¤removed.`, login); - break; } - default: { - tmc.chat("¤white¤Valid options are: ¤cmd¤list¤white¤, ¤cmd¤add¤white¤, ¤cmd¤remove", login); - } - } - }, "Manage admins"); + }, + 'Manage admins' + ); } - async cmdPluginManager(login: string, args: string[]) { const window = new PluginManagerWindow(login); window.size = { width: 160, height: 95 }; - window.title = "Plugins"; - let out:any[] = []; - let all:string[] = []; - let diff:string[] = []; - let plugins = fs.readdirSync(process.cwd() +"/core/plugins", { withFileTypes: true, recursive: true }); - plugins = plugins.concat(fs.readdirSync(process.cwd() + "/userdata/plugins", { withFileTypes: true, recursive: true })); + window.title = 'Plugins'; + let out: any[] = []; + let all: string[] = []; + let diff: string[] = []; + let plugins = fs.readdirSync(process.cwd() + '/core/plugins', { withFileTypes: true, recursive: true }); + plugins = plugins.concat(fs.readdirSync(process.cwd() + '/userdata/plugins', { withFileTypes: true, recursive: true })); for (const i in plugins) { const plugin = plugins[i]; if (plugin && plugin.isDirectory()) { - if (plugin.name.includes(".") || plugin.parentPath.includes(".")) continue; - if (plugin.name.includes("node_modules") || plugin.parentPath.includes("node_modules")) continue; - const path = plugin.parentPath.replace(process.cwd() + "/core/plugins", "").replace(process.cwd() + "/userdata/plugins", ""); - let pluginName = plugin.name.replaceAll("\\", "/"); - if (path != "") { - pluginName = (path.substring(1) +"/"+ plugin.name).replaceAll("\\", "/"); + if (plugin.name.includes('.') || plugin.parentPath.includes('.')) continue; + if (plugin.name.includes('node_modules') || plugin.parentPath.includes('node_modules')) continue; + const path = plugin.parentPath.replace(process.cwd() + '/core/plugins', '').replace(process.cwd() + '/userdata/plugins', ''); + let pluginName = plugin.name.replaceAll('\\', '/'); + if (path != '') { + pluginName = (path.substring(1) + '/' + plugin.name).replaceAll('\\', '/'); } all.push(pluginName); } @@ -197,16 +217,16 @@ export default class CommandManager { const deps = tmc.pluginDependecies.dependenciesOf(name); out.push({ pluginName: name, - depends: deps.join(", "), - active: tmc.plugins[name] ? "$0f0Yes" : "$f00No" + depends: deps.join(', '), + active: tmc.plugins[name] ? '$0f0Yes' : '$f00No' }); } for (const name of all.filter((value) => !diff.includes(value))) { out.push({ pluginName: name, - depends: "", - active: tmc.plugins[name] ? "$0f0Yes" : "$f00No" + depends: '', + active: tmc.plugins[name] ? '$0f0Yes' : '$f00No' }); } out = out.sort((a: any, b: any) => { @@ -215,21 +235,19 @@ export default class CommandManager { window.setItems(out); window.setColumns([ - { key: "active", title: "Running", width: 25, action: "toggle" }, - { key: "pluginName", title: "Plugin", width: 50 }, - { key: "depends", title: "Dependencies", width: 50 } + { key: 'active', title: 'Running', width: 25, action: 'toggle' }, + { key: 'pluginName', title: 'Plugin', width: 50 }, + { key: 'depends', title: 'Dependencies', width: 50 } ]); await window.display(); - } - /** * @ignore */ async afterInit() { - tmc.server.addListener("Trackmania.PlayerChat", this.onPlayerChat, this); + tmc.server.addListener('Trackmania.PlayerChat', this.onPlayerChat, this); } /** @@ -239,21 +257,19 @@ export default class CommandManager { * @param help help text * @param admin force admin */ - addCommand(command: string, callback: CallableCommand, help: string = "", admin?:boolean) { + addCommand(command: string, callback: CallableCommand, help: string = '', admin?: boolean) { if (admin === undefined) { - admin = command.startsWith("//"); - } - if (!this.commands[command]) { - this.commands[command] = { - trigger: command, - callback: callback, - admin: admin, - help: help, - } + admin = command.startsWith('//'); } - else { - tmc.cli(`¤white¤Command $fd0${command} ¤white¤already exists.`); + if (this.commands[command]) { + tmc.cli(`¤white¤Command $fd0${command} ¤white¤already exists, overriding it.`); } + this.commands[command] = { + trigger: command, + callback: callback, + admin: admin, + help: help + }; } /** * removes command from the command manager @@ -271,19 +287,21 @@ export default class CommandManager { * @param text */ async execute(login: string, text: string) { - if (text.startsWith("/")) { + if (text.startsWith('/')) { for (let command of Object.values(this.commands)) { - if (text.startsWith("//") && !tmc.admins.includes(login)) { - tmc.chat("¤error¤Not allowed.", login); + if (text.startsWith('//') && !tmc.admins.includes(login)) { + tmc.chat('¤error¤Not allowed.', login); return; } if (!command) continue; - let prefix = "[/]"; - if (command.trigger.startsWith("//")) prefix ="[/]{2}"; - const exp = new RegExp(`^${prefix}\\b${escapeRegex(command.trigger.replaceAll("/", ""))}\\b`, "i"); + let prefix = '[/]'; + if (command.trigger.startsWith('//')) prefix = '[/]{2}'; + const exp = new RegExp(`^${prefix}\\b${escapeRegex(command.trigger.replaceAll('/', ''))}\\b`, 'i'); if (exp.test(text)) { - const words = text.replace(command.trigger, "").trim(); - let params = (words.match(/(? word.replace(/^"(.+(?="$))"$/, '$1').replaceAll("\\", "")); + const words = text.replace(command.trigger, '').trim(); + let params = (words.match(/(? + word.replace(/^"(.+(?="$))"$/, '$1').replaceAll('\\', '') + ); await command.callback(login, params); return; } @@ -307,13 +325,13 @@ export default class CommandManager { class PluginManagerWindow extends ListWindow { async onAction(login: string, action: string, item: any) { - if (action == "toggle") { + if (action == 'toggle') { if (tmc.plugins[item.pluginName]) { - await tmc.chatCmd.execute(login, "//plugin unload " + item.pluginName); + await tmc.chatCmd.execute(login, '//plugin unload ' + item.pluginName); } else { - await tmc.chatCmd.execute(login, "//plugin load " + item.pluginName); + await tmc.chatCmd.execute(login, '//plugin load ' + item.pluginName); } - await tmc.chatCmd.execute(login, "//plugins"); + await tmc.chatCmd.execute(login, '//plugins'); return; } } diff --git a/core/mapmanager.ts b/core/mapmanager.ts index 4a89a48..4d9f17a 100644 --- a/core/mapmanager.ts +++ b/core/mapmanager.ts @@ -1,151 +1,150 @@ -import { chunkArray, clone } from "./utils"; - -export interface Map { - UId: string; - Name: string; - Author: string; - AuthorNickname?: string; - AuthorTime: number; - GoldTime: number; - SilverTime: number; - BronzeTime: number; - CopperPrize: number; - FileName: string; - Environnement: string; - Mood: string; - LapRace: boolean; - NbLaps: number; - NbCheckpoints: number; - Vehicle?: string; - [key: string]: any; -} - -/** - * MapManager class - */ -class MapManager { - private maps: { [key: string]: Map; }; - previousMap?: Map; - currentMap?: Map; - nextMap?: Map; - - /** - * @ignore - */ - constructor() { - this.maps = {}; - } - - /** - * Initialize the map manager - * @ignore - **/ - async init() { - this.maps = {}; - tmc.server.addListener("Trackmania.BeginMap", this.onBeginMap, this); - tmc.server.addListener("Trackmania.MapListModified", this.onMapListModified, this); - try { - this.currentMap = await tmc.server.call("GetCurrentMapInfo"); - this.nextMap = await tmc.server.call("GetNextMapInfo"); - } catch (e:any) { - tmc.cli("¤error¤" + e.message); - } - await this.syncMaplist(); - } - - /** @ignore */ - private async onBeginMap(data: any) { - this.previousMap = clone(this.currentMap); - this.currentMap = data[0]; - const index = Object.keys(this.maps).indexOf(data[0].UId); - const indexNext = (index + 1) % Object.keys(this.maps).length; - this.nextMap = Object.values(this.maps)[indexNext]; - } - - /** @ignore */ - private async onMapListModified(data: any) { - if (data[2] === true) { - await this.syncMaplist(); - } - } - - /** - * Sync the maplist with the server - */ - async syncMaplist() { - this.maps = {}; - - const chunckedMaps: any = chunkArray(await tmc.server.call("GetMapList", -1, 0), 100); - let method = "GetMapInfo"; - if (tmc.game.Name == "TmForever") method = "GetChallengeInfo"; - - for (const infos of chunckedMaps) { - let out:any[] = []; - - for (const map of infos) { - out.push([method, map.FileName]); - } - const res:any = await tmc.server.multicall(out) || []; - - for (const map of res) { - this.maps[map.UId] = map; - } - } - } - /** - * add map - * @param map - */ - addMap(map: Map) { - if (!this.maps[map.UId]) { - this.maps[map.UId] = map; - } - } - - /** - * remove map - * @param mapUId - */ - removeMap(mapUId: string) { - if (this.maps[mapUId]) { - delete this.maps[mapUId]; - } - } - - /** - * get maps - * @returns {Map[]} Returns the current maplist - */ - get(): Map[] { - return Object.values(this.maps); - } - - getMap(mapUid: string): Map | undefined { - return this.maps[mapUid]; - } - - /** - * get mapslist - * @returns {Map[]} Returns the current maplist - */ - getMaplist(): Map[] { - return this.get(); - } - - /** - * get map uids - * @returns {string[]} Returns the current map uids - */ - getUids(): string[] { - return Object.keys(this.maps); - } - - /** - * @returns {number} Returns the total number of maps present at server - */ - getMapCount(): number { - return Object.values(this.maps).length; - } -} - -export default MapManager; +import { chunkArray, clone } from "./utils"; + +export interface Map { + UId: string; + Name: string; + Author: string; + AuthorNickname?: string; + AuthorTime: number; + GoldTime: number; + SilverTime: number; + BronzeTime: number; + CopperPrize: number; + FileName: string; + Environnement: string; + Mood: string; + LapRace: boolean; + NbLaps: number; + NbCheckpoints: number; + Vehicle?: string; + [key: string]: any; +} + +/** + * MapManager class + */ +class MapManager { + private maps: { [key: string]: Map; }; + previousMap?: Map; + currentMap?: Map; + nextMap?: Map; + + /** + * @ignore + */ + constructor() { + this.maps = {}; + } + + /** + * Initialize the map manager + * @ignore + **/ + async init() { + this.maps = {}; + tmc.server.addListener("Trackmania.BeginMap", this.onBeginMap, this); + tmc.server.addListener("Trackmania.MapListModified", this.onMapListModified, this); + try { + this.currentMap = await tmc.server.call("GetCurrentMapInfo"); + this.nextMap = await tmc.server.call("GetNextMapInfo"); + } catch (e:any) { + tmc.cli("¤error¤" + e.message); + } + await this.syncMaplist(); + } + + /** @ignore */ + private async onBeginMap(data: any) { + this.previousMap = clone(this.currentMap); + this.currentMap = data[0]; + const index = Object.keys(this.maps).indexOf(data[0].UId); + const indexNext = (index + 1) % Object.keys(this.maps).length; + this.nextMap = Object.values(this.maps)[indexNext]; + } + + /** @ignore */ + private async onMapListModified(data: any) { + if (data[2] === true) { + await this.syncMaplist(); + } + } + + /** + * Sync the maplist with the server + */ + async syncMaplist() { + const chunckedMaps: any = chunkArray(await tmc.server.call("GetMapList", -1, 0), 100); + let method = "GetMapInfo"; + if (tmc.game.Name == "TmForever") method = "GetChallengeInfo"; + let newMaps = {}; + for (const infos of chunckedMaps) { + let out:any[] = []; + + for (const map of infos) { + out.push([method, map.FileName]); + } + const res:any = await tmc.server.multicall(out) || []; + + for (const map of res) { + newMaps[map.UId] = map; + } + } + this.maps = newMaps; + } + /** + * add map + * @param map + */ + addMap(map: Map) { + if (!this.maps[map.UId]) { + this.maps[map.UId] = map; + } + } + + /** + * remove map + * @param mapUId + */ + removeMap(mapUId: string) { + if (this.maps[mapUId]) { + delete this.maps[mapUId]; + } + } + + /** + * get maps + * @returns {Map[]} Returns the current maplist + */ + get(): Map[] { + return Object.values(this.maps); + } + + getMap(mapUid: string): Map | undefined { + return this.maps[mapUid]; + } + + /** + * get mapslist + * @returns {Map[]} Returns the current maplist + */ + getMaplist(): Map[] { + return this.get(); + } + + /** + * get map uids + * @returns {string[]} Returns the current map uids + */ + getUids(): string[] { + return Object.keys(this.maps); + } + + /** + * @returns {number} Returns the total number of maps present at server + */ + getMapCount(): number { + return Object.values(this.maps).length; + } +} + +export default MapManager; diff --git a/core/migrations/00-create-map.ts b/core/migrations/00-create-map.ts index 1d81218..41972f8 100644 --- a/core/migrations/00-create-map.ts +++ b/core/migrations/00-create-map.ts @@ -2,16 +2,16 @@ import { DataTypes } from 'sequelize'; import type { Migration } from '../../migrate'; export const up: Migration = async ({ context: sequelize }) => { - await sequelize.getQueryInterface().createTable('maps', { - uuid: { - type: DataTypes.STRING, - allowNull: false, - primaryKey: true, - }, - name: { - type: DataTypes.STRING, - allowNull: false, - }, + await sequelize.getQueryInterface().createTable('maps', { + uuid: { + type: DataTypes.STRING, + allowNull: false, + primaryKey: true + }, + name: { + type: DataTypes.STRING, + allowNull: false + }, author: { type: DataTypes.STRING, allowNull: false @@ -21,21 +21,20 @@ export const up: Migration = async ({ context: sequelize }) => { }, authorTime: { type: DataTypes.INTEGER, - allowNull:false + allowNull: false }, environment: { - type:DataTypes.STRING + type: DataTypes.STRING }, updatedAt: { type: DataTypes.DATE }, - createdAt: - { + createdAt: { type: DataTypes.DATE } - }); + }); }; export const down: Migration = async ({ context: sequelize }) => { - await sequelize.getQueryInterface().dropTable('maps'); -}; \ No newline at end of file + await sequelize.getQueryInterface().dropTable('maps'); +}; diff --git a/core/minicontrol.ts b/core/minicontrol.ts index dbf5261..e4907c1 100644 --- a/core/minicontrol.ts +++ b/core/minicontrol.ts @@ -1,559 +1,567 @@ -/* - MINIcontrol - server controller for Trackmania games - Copyright (C) 2024 Evo eSports e.V. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ -import {require} from 'tsx/cjs/api'; -import * as SentryType from '@sentry/node'; - -const Sentry = require('./sentry', import.meta.url); - -import PlayerManager, {Player} from './playermanager'; -import BillManager from './billmanager'; -import Server from './server'; -import UiManager from './uimanager'; -import MapManager from './mapmanager'; -import CommandManager, {type CallableCommand} from './commandmanager'; -import SettingsManager from './settingsmanager'; -import {getCallerName, processColorString, setMemStart} from './utils'; -import log from './log'; -import fs from 'fs'; -import Plugin from './plugins/index'; -import path from 'path'; -import {DepGraph} from 'dependency-graph'; -import semver from 'semver'; - -export interface GameStruct { - Name: string; - Version?: string; - Build?: string; -} - -/** - * MiniControl class - */ -class MiniControl { - /** - * The version of MiniControl. - */ - readonly brand: string = '$n$o$eeeMINI$o$z$s$abccontrol$z$s¤white¤'; - readonly version: string = process.env.npm_package_version || 'unknown'; - /** - * The start time of MiniControl. - */ - readonly startTime: string = Date.now().toString(); - /** - * The admins of MiniControl. - */ - admins: string[] = []; - /** - * The server object. - */ - server: Server; - /** - * The command manager. - */ - chatCmd: CommandManager; - /** - * The map manager. - */ - maps: MapManager; - /** - * The player manager. - */ - players: PlayerManager; - /** - * The UI manager. - */ - ui: UiManager; - /** - * The settings - */ - settings: any = {}; - /** - * The settings manager. - */ - settingsMgr: SettingsManager; - /** - * The colors - */ - colors: { [key: string]: string } = {}; - /** - * The plugins. - */ - plugins: { [key: string]: Plugin } = {}; - pluginDependecies: DepGraph = new DepGraph(); - billMgr: BillManager; - /** - * The game object. - */ - game: GameStruct; - mapsPath: string = ''; - storage: { [key: string]: any } = {}; - startComplete: boolean = false; - - constructor() { - console.time('Startup'); - this.server = new Server(); - this.maps = new MapManager(); - this.players = new PlayerManager(); - this.ui = new UiManager(); - this.chatCmd = new CommandManager(); - this.billMgr = new BillManager(); - this.settingsMgr = new SettingsManager(); - this.settingsMgr.load(); - this.settings = this.settingsMgr.settings; - this.colors = this.settingsMgr.colors; - this.admins = this.settingsMgr.admins; - this.game = {Name: ''}; - } - - /** - * Gets a player object from the player manager. - * @param login The login of the player. - * @returns A promise that resolves to the player object. - */ - async getPlayer(login: string): Promise { - return await this.players.getPlayer(login); - } - - /** - * Adds chat command - * @param command The command name, should start with / for public or // for admin only - * @param callback The callback function to execute when the command is triggered. - * @param help The help text for the command. - */ - addCommand(command: string, callback: CallableCommand, help: string = '') { - this.chatCmd.addCommand(command, callback, help); - } - - /** - * Removes chat command - * @param command The command name to remove. - */ - removeCommand(command: string) { - this.chatCmd.removeCommand(command); - } - - /** - * @param name name of the plugin folder in ./plugins - * @returns - */ - findPlugin(name: string): string | null { - const dirsToCheck = ['./core/plugins/', './userdata/plugins/']; - for (const dir of dirsToCheck) { - if (fs.existsSync(dir + name + '/index.ts')) { - return (dir + name).replaceAll('\\', '/'); - } - } - return null; - } - - /** - * Loads a plugin to runtime - * @param name name of the plugin folder in ./plugins - * @returns - */ - async loadPlugin(name: string) { - if (!this.plugins[name]) { - const pluginPath = this.findPlugin(name); - if (pluginPath == null) { - const msg = `¤gray¤Plugin ¤cmd¤${name}¤white¤ does not exist.`; - if (this.startComplete) { - this.cli(msg); - this.chat(msg); - } - return; - } - - let plugin: any; - const epoch = new Date().getTime(); - if (process.platform === 'win32') { - plugin = await import('file:///' + process.cwd() + '/' + pluginPath + "?stamp="+epoch); - } else { - plugin = await import(process.cwd() + '/' + pluginPath+ "?stamp="+epoch); - } - - if (plugin.default == undefined) { - const msg = `¤gray¤Plugin ¤cmd¤${name}¤error¤ failed to load. Plugin has no default export.`; - this.cli(msg); - this.chat(msg); - return; - } - if (!(plugin.default.prototype instanceof Plugin)) { - const msg = `¤gray¤Plugin ¤cmd¤${name}¤white¤ is not a valid plugin.`; - this.cli(msg); - this.chat(msg); - return; - } - - if (!this.pluginDependecies.hasNode(name)) { - this.pluginDependecies.addNode(name); - if (Reflect.has(plugin.default, 'depends')) { - for (const dependency of plugin.default.depends) { - if (!dependency.startsWith('game:')) { - this.pluginDependecies.addDependency(name, dependency); - } - } - } - } - - for (const depend of plugin.default.depends) { - if (depend.startsWith('game:')) { - const game = depend.split(':')[1]; - if (game != this.game.Name) { - const msg = `¤gray¤Plugin ¤cmd¤${name}¤white¤ not loaded. Game is not ¤cmd¤${game}¤white¤.`; - this.cli(msg); - if (this.startComplete) this.chat(msg); - return; - } - } - if (!this.pluginDependecies.hasNode(depend) && !depend.startsWith('game:')) { - const msg = `¤gray¤Plugin ¤cmd¤${name}¤white¤ failed to load. Missing dependency ¤cmd¤${depend}¤white¤.`; - this.cli(msg); - if (this.startComplete) this.chat(msg); - return; - } - } - - // load and init the plugin - try { - tmc.cli(`¤gray¤Loading ¤cmd¤${name}¤white¤...`); - const cls = new plugin.default(); - this.plugins[name] = cls; - await cls.onLoad(); - if (this.startComplete) { - await cls.onStart(); - this.chat(`¤gray¤Plugin ¤cmd¤${name} ¤white¤loaded!`); - } - this.cli('¤gray¤Success.'); - } catch (e: any) { - tmc.cli('¤gray¤Error while starting plugin ¤cmd¤' + name); - sentry.captureException(e, { - tags: { - section: 'initPlugin' - } - }); - console.log(e); - } - } else { - const msg = `¤gray¤Plugin ¤cmd¤${name}¤white¤ already loaded.`; - this.chat(msg); - this.cli(msg); - } - } - - /** - * unloads plugin from runtime, also checks for dependecies, runs onUnload and removes require cache - * @param unloadName name of the plugin folder in ./plugins - * @returns - */ - async unloadPlugin(unloadName: string) { - if (this.plugins[unloadName]) { - const deps = this.pluginDependecies.dependantsOf(unloadName); - if (deps.length > 0) { - const msg = `¤gray¤Plugin ¤cmd¤${unloadName}¤white¤ cannot be unloaded. It has a dependency of ¤cmd¤${deps.join(', ')}¤white¤.`; - this.cli(msg); - this.chat(msg); - return; - } - const pluginPath = this.findPlugin(unloadName); - if (pluginPath == null) { - const msg = `¤gray¤Plugin ¤cmd¤${unloadName}¤white¤ does not exist.`; - this.cli(msg); - this.chat(msg); - return; - } - - // unload - await this.plugins[unloadName].onUnload(); - // remove from dependecies - for (const dep of this.plugins[unloadName].getDepends()) { - this.pluginDependecies.removeDependency(unloadName, dep); - } - this.pluginDependecies.removeNode(unloadName); - delete this.plugins[unloadName]; - - /* disabled for now - const file = process.cwd() + pluginPath.replaceAll('.', '') + '/index.ts'; - if (require.cache[file]) { - delete require.cache[file]; - } else { - this.cli(`$fffFailed to remove require cache for ¤cmd¤${unloadName}¤white¤, hotreload will not work right.`); - } - */ - const msg = `¤gray¤Plugin ¤cmd¤${unloadName}¤white¤ unloaded.`; - this.cli(msg); - this.chat(msg); - } else { - const msg = `¤gray¤Plugin ¤cmd¤${unloadName}¤white¤ not loaded.`; - this.cli(msg); - this.chat(msg); - } - } - - /** - * send message to console - * @param object The object to log. - */ - cli(object: any) { - log.info(processColorString(object.toString())); - if (process.env.DEBUGLEVEL == '2') getCallerName(); - } - - /** - * log command to console if debug is enabled - * @param object The object to log. - */ - debug(object: any) { - if (process.env.DEBUG == 'true') { - const level = parseInt(process.env.DEBUGLEVEL || '1'); - if (level >= 1) log.debug(processColorString(object.toString())); - if (level >= 3) getCallerName(); - } - } - - /** - * Sends chat message to server - * @param text string to send to chat - * @param login {string | string[]} login(s) to send message to, if undefined sends to all players - */ - chat(text: string, login: undefined | string | string[] = undefined) { - if (login !== undefined) { - const msg = '$9ab$n>$z$s ' + text.toString(); - this.server.send('ChatSendServerMessageToLogin', processColorString(msg, '$z$s'), typeof login == 'string' ? login : login.join(',')); - } else { - const msg = '$9ab» ¤info¤' + text.toString(); - this.server.send('ChatSendServerMessage', processColorString(msg, '$z$s')); - } - } - - /** - * Runs MiniControl. - * @ignore Should not be called directly - */ - async run() { - if (this.startComplete) return; - const port = Number.parseInt(process.env.XMLRPC_PORT || '5000'); - this.cli(`¤info¤Starting ¤white¤MINIcontrol ${this.version}`); - this.cli(`¤info¤Using Node ¤white¤${process.version}`); - if (semver.gt("21.5.0", process.version)) { - this.cli("¤error¤Your Node version is too old. Must be atleast 21.5.0, please upgrade!"); - process.exit(1); - } - this.cli('¤info¤Connecting to Trackmania Dedicated server at ¤white¤' + (process.env.XMLRPC_HOST ?? '127.0.0.1') + ':' + port); - const status = await this.server.connect(process.env.XMLRPC_HOST ?? '127.0.0.1', port); - if (!status) { - this.cli("¤error¤Couldn't connect to server."); - process.exit(); - } - this.cli('¤info¤Connected to Trackmania Dedicated server.'); - try { - await this.server.call('Authenticate', process.env.XMLRPC_USER ?? 'SuperAdmin', process.env.XMLRPC_PASS ?? 'SuperAdmin'); - } catch (e: any) { - this.cli('¤error¤Authenticate to server failed.'); - this.cli(e.message); - process.exit(); - } - await this.server.fetchServerInfo(); - this.server.send('EnableCallbacks', true); - this.server.send('SendHideManialinkPage'); - this.game = await this.server.call('GetVersion'); - - if (this.game.Name == 'Trackmania') { - await this.server.call('SetApiVersion', '2023-04-16'); - this.mapsPath = await this.server.call('GetMapsDirectory'); - await this.server.callScript('XmlRpc.EnableCallbacks', 'true'); - } else if (this.game.Name == 'TmForever') { - this.mapsPath = await this.server.call('GetTracksDirectory'); - } else if (this.game.Name == 'ManiaPlanet') { - await this.server.call('SetApiVersion', '2013-04-16'); - this.mapsPath = await this.server.call('GetMapsDirectory'); - await this.server.callScript('XmlRpc.EnableCallbacks', 'true'); - try { - const settings = {S_UseLegacyXmlRpcCallbacks: false}; - tmc.server.send('SetModeScriptSettings', settings); - } catch (e: any) { - tmc.cli(e.message); - } - } - - await this.maps.init(); - await this.players.init(); - await this.ui.init(); - await this.beforeInit(); - setMemStart(); - } - - /** - * Executes tasks before MiniControl initialization. - * @ignore Shouldn't be called directly - */ - async beforeInit() { - await this.chatCmd.beforeInit(); - // load plugins - let plugins = fs.readdirSync(process.cwd().replaceAll('\\', '/') + '/core/plugins', {withFileTypes: true, recursive: true}); - plugins = plugins.concat(fs.readdirSync(process.cwd().replaceAll('\\', '/') + '/userdata/plugins', {withFileTypes: true, recursive: true})); - const exclude = process.env.EXCLUDED_PLUGINS?.split(',') || []; - let loadList: string[] = []; - for (const plugin of plugins) { - let include = plugin && plugin.name && plugin.isDirectory(); - if (plugin.name.includes('.') || plugin.parentPath.includes('.')) include = false; - if (plugin.name.includes('node_modules') || plugin.parentPath.includes('node_modules')) include = false; - const directory = plugin.parentPath - .replaceAll('\\', '/') - .replace(path.resolve('core', 'plugins').replaceAll('\\', '/'), '') - .replace(path.resolve('userdata', 'plugins').replaceAll('\\', '/'), ''); - if (include) { - let pluginName: string = plugin.name; - if (directory != '') { - pluginName = (directory + '/' + plugin.name).replaceAll('\\', '/'); - if (pluginName.startsWith('/')) pluginName = pluginName.substring(1); - } - for (const excludeName of exclude) { - if (excludeName == '') continue; - if (pluginName.startsWith(excludeName.trim())) { - include = false; - } - } - if (include) { - loadList.push(pluginName); - } - } - } - - // load metadata - // this.pluginDependecies.addNode("game:" + tmc.game.Name); - let dependencyByPlugin: any = {}; - - for (const name of loadList) { - const pluginName = this.findPlugin(name); - if (pluginName == null) { - const msg = `¤error¤Didn't find a plugin. resolved plugin name is null.`; - this.cli(msg); - continue; - } - let cls: any = null; - if (process.platform === 'win32') { - cls = await import('file:///' + process.cwd() + '/' + pluginName); - } else { - cls = await import(process.cwd() + '/' + pluginName); - } - - let plugin: any = cls.default; - - if (plugin == undefined) { - const msg = `¤gray¤Plugin ¤cmd¤${name}¤error¤ failed to load. Plugin has no default export.`; - this.cli(msg); - continue; - } - - if (!(plugin.prototype instanceof Plugin)) { - const msg = `¤gray¤Plugin ¤cmd¤${name}¤white¤ is not a valid plugin.`; - this.cli(msg); - continue; - } - - this.pluginDependecies.addNode(name); - if (Reflect.has(plugin, 'depends')) { - dependencyByPlugin[name] = plugin.depends; - } - } - - for (const name in dependencyByPlugin) { - for (const dependency of dependencyByPlugin[name]) { - if (!dependency.startsWith("game:")) { - try { - this.pluginDependecies.addDependency(name, dependency); - } catch (error) { - this.cli(error); - } - } - } - } - dependencyByPlugin = null; - - for (const plugin of this.pluginDependecies.overallOrder()) { - if (loadList.includes(plugin)) { - await this.loadPlugin(plugin); - } - } - - this.server.send('Echo', this.startTime, 'MiniControl'); - } - - /** - * Executes tasks after MiniControl initialization. - * @ignore Should not be called directly - * - */ - async afterStart() { - this.billMgr.afterInit(); - this.players.afterInit(); - await this.chatCmd.afterInit(); - await this.ui.afterInit(); - const msg = `¤info¤Welcome to ${this.brand} ¤info¤version ¤white¤${this.version}¤info¤!`; - this.chat(msg); - this.cli(msg); - this.startComplete = true; - for (const plugin of Object.values(this.plugins)) { - await plugin?.onStart(); - } - console.timeEnd('Startup'); - tmc.cli('¤success¤MiniControl started successfully.'); - } -} - -const tmc = new MiniControl(); - -declare global { - const tmc: MiniControl; - const sentry: typeof SentryType; -} - -(globalThis as any).tmc = tmc; -(globalThis as any).sentry = Sentry; - -(async () => { - try { - await tmc.run(); - } catch (e: any) { - tmc.cli('¤error¤' + e.message); - } -})(); - -process.on('SIGINT', function () { - tmc.server.send('SendHideManialinkPage', 0, false); - Sentry.close(2000).then(() => { - console.log("MINIcontrol exits successfully."); - process.exit(0); - }); -}); - -process.on('SIGTERM', () => { - tmc.server.send('SendHideManialinkPage', 0, false); - Sentry.close(2000).then(() => { - console.log("MINIcontrol exits succesfully."); - process.exit(0); - }); -}); - -process.on('uncaughtException', function (err) { - tmc.cli('¤error¤' + err.message); - console.log(err); - if (process.env['DEBUG'] == 'true') { - // process.exit(1); - } -}); +/* + MINIcontrol - server controller for Trackmania games + Copyright (C) 2024 Evo eSports e.V. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +import {require} from 'tsx/cjs/api'; +import * as SentryType from '@sentry/node'; + +const Sentry = require('./sentry', import.meta.url); + +import PlayerManager, {Player} from './playermanager'; +import BillManager from './billmanager'; +import Server from './server'; +import UiManager from './uimanager'; +import MapManager from './mapmanager'; +import CommandManager, {type CallableCommand} from './commandmanager'; +import SettingsManager from './settingsmanager'; +import {clone, getCallerName, processColorString, setMemStart} from './utils'; +import log from './log'; +import fs from 'fs'; +import Plugin from './plugins/index'; +import path from 'path'; +import {DepGraph} from 'dependency-graph'; +import semver from 'semver'; + +export interface GameStruct { + Name: string; + Version?: string; + Build?: string; +} + +/** + * MiniControl class + */ +class MiniControl { + /** + * The version of MiniControl. + */ + readonly brand: string = '$n$o$eeeMINI$o$z$s$abccontrol$z$s¤white¤'; + readonly version: string = process.env.npm_package_version || 'unknown'; + /** + * The start time of MiniControl. + */ + readonly startTime: string = Date.now().toString(); + /** + * The admins of MiniControl. + */ + admins: string[] = []; + /** + * The server object. + */ + server: Server; + /** + * The command manager. + */ + chatCmd: CommandManager; + /** + * The map manager. + */ + maps: MapManager; + /** + * The player manager. + */ + players: PlayerManager; + /** + * The UI manager. + */ + ui: UiManager; + /** + * The settings + */ + settings: any = {}; + /** + * The settings manager. + */ + settingsMgr: SettingsManager; + /** + * The colors + */ + colors: { [key: string]: string } = {}; + /** + * The plugins. + */ + plugins: { [key: string]: Plugin } = {}; + pluginDependecies: DepGraph = new DepGraph(); + billMgr: BillManager; + /** + * The game object. + */ + game: GameStruct; + mapsPath: string = ''; + storage: { [key: string]: any } = {}; + startComplete: boolean = false; + + constructor() { + console.time('Startup'); + this.server = new Server(); + this.maps = new MapManager(); + this.players = new PlayerManager(); + this.ui = new UiManager(); + this.chatCmd = new CommandManager(); + this.billMgr = new BillManager(); + this.settingsMgr = new SettingsManager(); + this.settingsMgr.load(); + this.settings = this.settingsMgr.settings; + this.colors = this.settingsMgr.colors; + this.admins = this.settingsMgr.admins; + this.game = {Name: ''}; + } + + /** + * Gets a player object from the player manager. + * @param login The login of the player. + * @returns A promise that resolves to the player object. + */ + async getPlayer(login: string): Promise { + return await this.players.getPlayer(login); + } + + /** + * Adds chat command + * @param command The command name, should start with / for public or // for admin only + * @param callback The callback function to execute when the command is triggered. + * @param help The help text for the command. + */ + addCommand(command: string, callback: CallableCommand, help: string = '') { + this.chatCmd.addCommand(command, callback, help); + } + + /** + * Removes chat command + * @param command The command name to remove. + */ + removeCommand(command: string) { + this.chatCmd.removeCommand(command); + } + + /** + * @param name name of the plugin folder in ./plugins + * @returns + */ + findPlugin(name: string): string | null { + const dirsToCheck = ['./core/plugins/', './userdata/plugins/']; + for (const dir of dirsToCheck) { + if (fs.existsSync(dir + name + '/index.ts')) { + return (dir + name).replaceAll('\\', '/'); + } + } + return null; + } + + /** + * Loads a plugin to runtime + * @param name name of the plugin folder in ./plugins + * @returns + */ + async loadPlugin(name: string) { + if (!this.plugins[name]) { + const pluginPath = this.findPlugin(name); + if (pluginPath == null) { + const msg = `¤gray¤Plugin ¤cmd¤${name}¤white¤ does not exist.`; + if (this.startComplete) { + this.cli(msg); + this.chat(msg); + } + return; + } + + let plugin: any; + const epoch = new Date().getTime(); + if (process.platform === 'win32') { + plugin = await import('file:///' + process.cwd() + '/' + pluginPath + "?stamp="+epoch); + } else { + plugin = await import(process.cwd() + '/' + pluginPath+ "?stamp="+epoch); + } + + if (plugin.default == undefined) { + const msg = `¤gray¤Plugin ¤cmd¤${name}¤error¤ failed to load. Plugin has no default export.`; + this.cli(msg); + this.chat(msg); + return; + } + if (!(plugin.default.prototype instanceof Plugin)) { + const msg = `¤gray¤Plugin ¤cmd¤${name}¤white¤ is not a valid plugin.`; + this.cli(msg); + this.chat(msg); + return; + } + + if (!this.pluginDependecies.hasNode(name)) { + this.pluginDependecies.addNode(name); + if (Reflect.has(plugin.default, 'depends')) { + for (const dependency of plugin.default.depends) { + if (!dependency.startsWith('game:')) { + this.pluginDependecies.addDependency(name, dependency); + } + } + } + } + + for (const depend of plugin.default.depends) { + if (depend.startsWith('game:')) { + const game = depend.split(':')[1]; + if (game != this.game.Name) { + const msg = `¤gray¤Plugin ¤cmd¤${name}¤white¤ not loaded. Game is not ¤cmd¤${game}¤white¤.`; + this.cli(msg); + if (this.startComplete) this.chat(msg); + return; + } + } + if (!this.pluginDependecies.hasNode(depend) && !depend.startsWith('game:')) { + const msg = `¤gray¤Plugin ¤cmd¤${name}¤white¤ failed to load. Missing dependency ¤cmd¤${depend}¤white¤.`; + this.cli(msg); + if (this.startComplete) this.chat(msg); + return; + } + } + + // load and init the plugin + try { + tmc.cli(`¤gray¤Loading ¤cmd¤${name}¤white¤...`); + const cls = new plugin.default(); + this.plugins[name] = cls; + await cls.onLoad(); + if (this.startComplete) { + await cls.onStart(); + this.chat(`¤gray¤Plugin ¤cmd¤${name} ¤white¤loaded!`); + } + this.cli('¤gray¤Success.'); + } catch (e: any) { + tmc.cli('¤gray¤Error while starting plugin ¤cmd¤' + name); + sentry.captureException(e, { + tags: { + section: 'initPlugin' + } + }); + console.log(e); + } + } else { + const msg = `¤gray¤Plugin ¤cmd¤${name}¤white¤ already loaded.`; + this.chat(msg); + this.cli(msg); + } + } + + /** + * unloads plugin from runtime, also checks for dependecies, runs onUnload and removes require cache + * @param unloadName name of the plugin folder in ./plugins + * @returns + */ + async unloadPlugin(unloadName: string) { + if (this.plugins[unloadName]) { + const deps = this.pluginDependecies.dependantsOf(unloadName); + if (deps.length > 0) { + const msg = `¤gray¤Plugin ¤cmd¤${unloadName}¤white¤ cannot be unloaded. It has a dependency of ¤cmd¤${deps.join(', ')}¤white¤.`; + this.cli(msg); + this.chat(msg); + return; + } + const pluginPath = this.findPlugin(unloadName); + if (pluginPath == null) { + const msg = `¤gray¤Plugin ¤cmd¤${unloadName}¤white¤ does not exist.`; + this.cli(msg); + this.chat(msg); + return; + } + + // unload + await this.plugins[unloadName].onUnload(); + // remove from dependecies + for (const dep of this.plugins[unloadName].getDepends()) { + this.pluginDependecies.removeDependency(unloadName, dep); + } + this.pluginDependecies.removeNode(unloadName); + delete this.plugins[unloadName]; + + /* disabled for now + const file = process.cwd() + pluginPath.replaceAll('.', '') + '/index.ts'; + if (require.cache[file]) { + delete require.cache[file]; + } else { + this.cli(`$fffFailed to remove require cache for ¤cmd¤${unloadName}¤white¤, hotreload will not work right.`); + } + */ + const msg = `¤gray¤Plugin ¤cmd¤${unloadName}¤white¤ unloaded.`; + this.cli(msg); + this.chat(msg); + } else { + const msg = `¤gray¤Plugin ¤cmd¤${unloadName}¤white¤ not loaded.`; + this.cli(msg); + this.chat(msg); + } + } + + /** + * send message to console + * @param object The object to log. + */ + cli(object: any) { + log.info(processColorString(object.toString())); + if (process.env.DEBUGLEVEL == '2') getCallerName(); + } + + /** + * log command to console if debug is enabled + * @param object The object to log. + */ + debug(object: any) { + if (process.env.DEBUG == 'true') { + const level = parseInt(process.env.DEBUGLEVEL || '1'); + if (level >= 1) log.debug(processColorString(object.toString())); + if (level >= 3) getCallerName(); + } + } + + /** + * Sends chat message to server + * @param text string to send to chat + * @param login {string | string[]} login(s) to send message to, if undefined sends to all players + */ + chat(text: string, login: undefined | string | string[] = undefined) { + if (login !== undefined) { + const msg = '$9ab$n>$z$s ' + text.toString(); + this.server.send('ChatSendServerMessageToLogin', processColorString(msg, '$z$s'), typeof login == 'string' ? login : login.join(',')); + } else { + const msg = '$9ab» ¤info¤' + text.toString(); + this.server.send('ChatSendServerMessage', processColorString(msg, '$z$s')); + } + } + + /** + * Runs MiniControl. + * @ignore Should not be called directly + */ + async run() { + if (this.startComplete) return; + const port = Number.parseInt(process.env.XMLRPC_PORT || '5000'); + this.cli(`¤info¤Starting ¤white¤MINIcontrol ${this.version}`); + this.cli(`¤info¤Using Node ¤white¤${process.version}`); + if (semver.gt("21.5.0", process.version)) { + this.cli("¤error¤Your Node version is too old. Must be atleast 21.5.0, please upgrade!"); + process.exit(1); + } + this.cli('¤info¤Connecting to Trackmania Dedicated server at ¤white¤' + (process.env.XMLRPC_HOST ?? '127.0.0.1') + ':' + port); + const status = await this.server.connect(process.env.XMLRPC_HOST ?? '127.0.0.1', port); + if (!status) { + this.cli("¤error¤Couldn't connect to server."); + process.exit(); + } + this.cli('¤info¤Connected to Trackmania Dedicated server.'); + try { + await this.server.call('Authenticate', process.env.XMLRPC_USER ?? 'SuperAdmin', process.env.XMLRPC_PASS ?? 'SuperAdmin'); + } catch (e: any) { + this.cli('¤error¤Authenticate to server failed.'); + this.cli(e.message); + process.exit(); + } + await this.server.fetchServerInfo(); + this.server.send('EnableCallbacks', true); + this.server.send('SendHideManialinkPage'); + this.game = await this.server.call('GetVersion'); + + if (this.game.Name == 'Trackmania') { + await this.server.call('SetApiVersion', '2023-04-16'); + this.mapsPath = await this.server.call('GetMapsDirectory'); + await this.server.callScript('XmlRpc.EnableCallbacks', 'true'); + } else if (this.game.Name == 'TmForever') { + this.mapsPath = await this.server.call('GetTracksDirectory'); + } else if (this.game.Name == 'ManiaPlanet') { + await this.server.call('SetApiVersion', '2013-04-16'); + this.mapsPath = await this.server.call('GetMapsDirectory'); + await this.server.callScript('XmlRpc.EnableCallbacks', 'true'); + try { + const settings = {S_UseLegacyXmlRpcCallbacks: false}; + tmc.server.send('SetModeScriptSettings', settings); + } catch (e: any) { + tmc.cli(e.message); + } + } + + await this.maps.init(); + await this.players.init(); + await this.ui.init(); + await this.beforeInit(); + } + + /** + * Executes tasks before MiniControl initialization. + * @ignore Shouldn't be called directly + */ + async beforeInit() { + await this.chatCmd.beforeInit(); + // load plugins + let plugins = fs.readdirSync(process.cwd().replaceAll('\\', '/') + '/core/plugins', {withFileTypes: true, recursive: true}); + plugins = plugins.concat(fs.readdirSync(process.cwd().replaceAll('\\', '/') + '/userdata/plugins', {withFileTypes: true, recursive: true})); + const exclude = process.env.EXCLUDED_PLUGINS?.split(',') || []; + let loadList: string[] = []; + for (const plugin of plugins) { + let include = plugin && plugin.name && plugin.isDirectory(); + if (plugin.name.includes('.') || plugin.parentPath.includes('.')) include = false; + if (plugin.name.includes('node_modules') || plugin.parentPath.includes('node_modules')) include = false; + const directory = plugin.parentPath + .replaceAll('\\', '/') + .replace(path.resolve('core', 'plugins').replaceAll('\\', '/'), '') + .replace(path.resolve('userdata', 'plugins').replaceAll('\\', '/'), ''); + if (include) { + let pluginName: string = plugin.name; + if (directory != '') { + pluginName = (directory + '/' + plugin.name).replaceAll('\\', '/'); + if (pluginName.startsWith('/')) pluginName = pluginName.substring(1); + } + for (const excludeName of exclude) { + if (excludeName == '') continue; + if (pluginName.startsWith(excludeName.trim())) { + include = false; + } + } + if (include) { + loadList.push(pluginName); + } + } + } + + // load metadata + // this.pluginDependecies.addNode("game:" + tmc.game.Name); + let dependencyByPlugin: any = {}; + + for (const name of loadList) { + const pluginName = this.findPlugin(name); + if (pluginName == null) { + const msg = `¤error¤Didn't find a plugin. resolved plugin name is null.`; + this.cli(msg); + continue; + } + let cls: any = null; + if (process.platform === 'win32') { + cls = await import('file:///' + process.cwd() + '/' + pluginName); + } else { + cls = await import(process.cwd() + '/' + pluginName); + } + + let plugin: any = cls.default; + + if (plugin == undefined) { + const msg = `¤gray¤Plugin ¤cmd¤${name}¤error¤ failed to load. Plugin has no default export.`; + this.cli(msg); + cls = undefined; + continue; + } + + if (!(plugin.prototype instanceof Plugin)) { + const msg = `¤gray¤Plugin ¤cmd¤${name}¤white¤ is not a valid plugin.`; + this.cli(msg); + cls = undefined; + plugin = undefined; + continue; + } + + this.pluginDependecies.addNode(name); + if (Reflect.has(plugin, 'depends')) { + dependencyByPlugin[name] = clone(plugin.depends); + } + cls = undefined; + plugin = undefined; + } + + for (const name in dependencyByPlugin) { + for (const dependency of dependencyByPlugin[name]) { + if (!dependency.startsWith("game:")) { + try { + this.pluginDependecies.addDependency(name, dependency); + } catch (error) { + this.cli(error); + } + } + } + } + dependencyByPlugin = null; + + for (const pluginName of this.pluginDependecies.overallOrder()) { + if (loadList.includes(pluginName)) { + await this.loadPlugin(pluginName); + } + } + + this.server.send('Echo', this.startTime, 'MiniControl'); + } + + /** + * Executes tasks after MiniControl initialization. + * @ignore Should not be called directly + * + */ + async afterStart() { + this.billMgr.afterInit(); + this.players.afterInit(); + await this.chatCmd.afterInit(); + await this.ui.afterInit(); + + const msg = `¤info¤Welcome to ${this.brand} ¤info¤version ¤white¤${this.version}¤info¤!`; + this.chat(msg); + this.cli(msg); + this.startComplete = true; + setMemStart(); + if (gc) gc(); + for (const plugin of Object.values(this.plugins)) { + await plugin?.onStart(); + } + console.timeEnd('Startup'); + tmc.cli('¤success¤MiniControl started successfully.'); + + } +} + +const tmc = new MiniControl(); + +declare global { + const tmc: MiniControl; + const sentry: typeof SentryType; +} + +(globalThis as any).tmc = tmc; +(globalThis as any).sentry = Sentry; + +(async () => { + try { + await tmc.run(); + } catch (e: any) { + tmc.cli('¤error¤' + e.message); + } +})(); + +process.on('SIGINT', function () { + tmc.server.send('SendHideManialinkPage', 0, false); + Sentry.close(2000).then(() => { + console.log("MINIcontrol exits successfully."); + process.exit(0); + }); +}); + +process.on('SIGTERM', () => { + tmc.server.send('SendHideManialinkPage', 0, false); + Sentry.close(2000).then(() => { + console.log("MINIcontrol exits succesfully."); + process.exit(0); + }); +}); + +process.on('uncaughtException', function (err) { + tmc.cli('¤error¤' + err.message); + console.log(err); + if (process.env['DEBUG'] == 'true') { + // process.exit(1); + } +}); diff --git a/core/playermanager.ts b/core/playermanager.ts index b35aa89..56f996f 100644 --- a/core/playermanager.ts +++ b/core/playermanager.ts @@ -1,189 +1,196 @@ -import { clone, sleep } from "./utils"; - -interface PlayerRanking { - Path: string; - Score: number; - Ranking: number; - TotalCount: number; -} - -interface Avatar { - FileName: string; - Checksum: string; -} - -interface LadderStats { - LastMatchScore: number; - NbrMAtchWins: number; - NbrMatchDraws: number; - nbrMatchLosses: number; - TeamName: string; -} - -/** - * Player class - */ -export class Player { - login: string = ""; - nickname: string = ""; - playerId: number = -1; - teamId: number = -1; - path = ""; - language = "en"; - clientVersion = ""; - iPAddress = ""; - downloadRate: number = -1; - uploadRate: number = -1; - isSpectator: boolean = false; - ladderRanking: number = 0; - ladderStats?: LadderStats; - avatar?: Avatar; - hoursSinceZoneInscription: number = -1; - /** 3 for united */ - onlineRights = -1; - isAdmin: boolean = false; - spectatorTarget: number = 0; - flags: number = 0; - [key: string]: any; // Add index signature - - async syncFromDetailedPlayerInfo(data: any) { - for (let key in data) { - let k = key[0].toLowerCase() + key.slice(1); - if (k == "nickName") { - k = "nickname"; - data[key] = data[key].replace(/[$][lh]\[.*?](.*?)([$][lh])?/i, "$1").replaceAll(/[$][lh]/gi, ""); - } - if (k == "flags") { - this.spectatorTarget = Math.floor(data.SpecatorStatus / 10000); - } - this[k] = data[key]; - } - this.isAdmin = tmc.admins.includes(data.Login); - } - - syncFromPlayerInfo(data: any) { - this.login = data.Login; - this.teamId = Number.parseInt(data.TeamId); - this.isSpectator = data.SpectatorStatus !== 0; - this.isAdmin = tmc.admins.includes(data.Login); - } - - set(key: string, value: any) { - this[key] = value; - } - -} -/** - * PlayerManager class - */ -export default class PlayerManager { - private players: any = {}; - - /** - * Initialize the player manager - * @returns {Promise} - * @ignore - */ - async init(): Promise { - tmc.server.addListener("Trackmania.PlayerInfoChanged", this.onPlayerInfoChanged, this); - const players = await tmc.server.call('GetPlayerList', -1, 0); - for (const data of players) { - if (data.PlayerId === 0) continue; - await this.getPlayer(data.Login); - } - } - - /** - * Called after the server has been initialized - * @ignore - */ - afterInit() { - tmc.server.addListener("Trackmania.PlayerConnect", this.onPlayerConnect, this); - tmc.server.addListener("Trackmania.PlayerDisconnect", this.onPlayerDisconnect, this); - } - - /** - * @ignore - */ - private async onPlayerConnect(data: any) { - const login = data[0]; - if (login) { - await sleep(100); // this is really needed to prevent fetch from server multiple times - const player = await this.getPlayer(login); - tmc.server.emit("TMC.PlayerConnect", player); - } else { - tmc.debug("¤error¤Unknown player tried to connect, ignored."); - } - } - - /** - * callback for when a player disconnects - * @ignore - * @param data data from the server - */ - private async onPlayerDisconnect(data: any) { - const login = data[0]; - if (login && this.players[login]) { - tmc.server.emit("TMC.PlayerDisconnect", clone(this.players[login])); - delete this.players[login]; - } else { - tmc.debug("¤Error¤Unknown player tried to disconnect or player not found at server. ignored.") - } - } - - /** - * get players objects - * @returns {Player[]} Returns clone of the current playerlist - */ - getAll(): Player[] { - return Object.values(this.players); - } - - /** - * get player by nickname - * @param nickname - * @returns {Player | null} Returns the player object or null if not found - */ - getPlayerbyNick(nickname: string): Player | null { - for (let player in this.players) { - if (this.players[player].nick == nickname) return this.players[player]; - } - return null; - } - - /** - * gets player object - * @param login - * @returns {Player} Returns the player object - */ - async getPlayer(login: string): Promise { - if (this.players[login]) return this.players[login]; - - try { - tmc.debug(`$888Player ${login} not found, fetching from server.`); - const data = await tmc.server.call("GetDetailedPlayerInfo", login); - const player = new Player(); - await player.syncFromDetailedPlayerInfo(data); - this.players[login] = player; - return player; - } catch (e: any) { - return new Player(); - } - } - - /** - * callback for when a player info changes - * @ignore - * @param data data from the server - * @returns - */ - private async onPlayerInfoChanged(data: any) { - data = data[0]; - if (data.PlayerId === 0) return; - if (this.players[data.Login]) { - this.players[data.Login].syncFromPlayerInfo(data); - } else { - await this.getPlayer(data.Login); - } - } -} +import { clone, sleep } from './utils'; + +interface PlayerRanking { + Path: string; + Score: number; + Ranking: number; + TotalCount: number; +} + +interface Avatar { + FileName: string; + Checksum: string; +} + +interface LadderStats { + LastMatchScore: number; + NbrMAtchWins: number; + NbrMatchDraws: number; + nbrMatchLosses: number; + TeamName: string; +} + +/** + * Player class + */ +export class Player { + login: string = ''; + nickname: string = ''; + playerId: number = -1; + teamId: number = -1; + path = ''; + language = 'en'; + clientVersion = ''; + iPAddress = ''; + downloadRate: number = -1; + uploadRate: number = -1; + isSpectator: boolean = false; + ladderRanking: number = 0; + ladderStats?: LadderStats; + avatar?: Avatar; + hoursSinceZoneInscription: number = -1; + /** 3 for united */ + onlineRights = -1; + isAdmin: boolean = false; + spectatorTarget: number = 0; + flags: number = 0; + [key: string]: any; // Add index signature + + async syncFromDetailedPlayerInfo(data: any) { + for (let key in data) { + let k = key[0].toLowerCase() + key.slice(1); + if (k == 'nickName') { + k = 'nickname'; + data[key] = data[key].replace(/[$][lh]\[.*?](.*?)([$][lh])?/i, '$1').replaceAll(/[$][lh]/gi, ''); + } + if (k == 'flags') { + this.spectatorTarget = Math.floor(data.SpecatorStatus / 10000); + } + this[k] = data[key]; + } + this.isAdmin = tmc.admins.includes(data.Login); + } + + syncFromPlayerInfo(data: any) { + this.login = data.Login; + this.teamId = Number.parseInt(data.TeamId); + this.isSpectator = data.SpectatorStatus !== 0; + this.isAdmin = tmc.admins.includes(data.Login); + } + + set(key: string, value: any) { + this[key] = value; + } + +} +/** + * PlayerManager class + */ +export default class PlayerManager { + private players: any = {}; + + /** + * Initialize the player manager + * @returns {Promise} + * @ignore + */ + async init(): Promise { + tmc.server.addListener('Trackmania.PlayerInfoChanged', this.onPlayerInfoChanged, this); + const players = await tmc.server.call('GetPlayerList', -1, 0); + for (const data of players) { + if (data.PlayerId === 0) continue; + await this.getPlayer(data.Login); + } + } + + /** + * Called after the server has been initialized + * @ignore + */ + afterInit() { + tmc.server.addListener('Trackmania.PlayerConnect', this.onPlayerConnect, this); + tmc.server.addListener('Trackmania.PlayerDisconnect', this.onPlayerDisconnect, this); + } + + /** + * @ignore + */ + private async onPlayerConnect(data: any) { + const login = data[0]; + if (login) { + if (this.players[login]) { + tmc.cli(`$888Player ${login} already connected, kicking player due a bug to allow them joining again.`); + await tmc.server.call('Kick', login, "You are already connected, please rejoin."); + return; + } + const player = await this.getPlayer(login); + tmc.server.emit('TMC.PlayerConnect', player); + } else { + tmc.debug('¤error¤Unknown player tried to connect, ignored.'); + } + } + + /** + * callback for when a player disconnects + * @ignore + * @param data data from the server + */ + private async onPlayerDisconnect(data: any) { + const login = data[0]; + if (login && this.players[login]) { + tmc.server.emit('TMC.PlayerDisconnect', clone(this.players[login])); + delete this.players[login]; + } else { + tmc.debug('¤Error¤Unknown player tried to disconnect or player not found at server. ignored.'); + } + } + + /** + * get players objects + * @returns {Player[]} Returns current playerlist + */ + getAll(): Player[] { + return Object.values(this.players); + } + + /** + * get player by nickname + * @param nickname + * @returns {Player | null} Returns the player object or null if not found + */ + getPlayerbyNick(nickname: string): Player | null { + for (let player in this.players) { + if (this.players[player].nick == nickname) return this.players[player]; + } + return null; + } + + /** + * gets player object + * @param login + * @returns {Player} Returns the player object + */ + async getPlayer(login: string): Promise { + if (this.players[login]) return this.players[login]; + + try { + tmc.debug(`$888Player "${login}" not found, fetching from server.`); + const data = await tmc.server.call('GetDetailedPlayerInfo', login); + const player = new Player(); + await player.syncFromDetailedPlayerInfo(data); + this.players[login] = player; + return player; + } catch (e: any) { + return new Player(); + } + } + + /** + * callback for when a player info changes + * @ignore + * @param data data from the server + * @returns + */ + private async onPlayerInfoChanged(data: any) { + data = data[0]; + if (data.PlayerId === 0) return; + if (this.players[data.Login]) { + this.players[data.Login].syncFromPlayerInfo(data); + } else { + // if player is joined, fetch detailed info + if (Math.floor(data.Flags / 100000000) % 10 === 1) { + await this.getPlayer(data.Login); + } + } + } +} diff --git a/core/plugins/admin/LocalMapsWindow.ts b/core/plugins/admin/LocalMapsWindow.ts index b6943bc..3f6a997 100644 --- a/core/plugins/admin/LocalMapsWindow.ts +++ b/core/plugins/admin/LocalMapsWindow.ts @@ -1,7 +1,7 @@ import ListWindow from '@core/ui/listwindow'; import { GBX, CGameCtnChallenge } from "gbx"; import { existsSync, promises as fspromises } from "fs"; -import { escape } from '@core/utils'; +import { htmlEntities } from '@core/utils'; export default class LocalMapsWindow extends ListWindow { @@ -16,13 +16,13 @@ export default class LocalMapsWindow extends ListWindow { file => { item.MapAuthor = file.mapInfo.author || ""; if (tmc.game.Name == "Trackmania") { - item.MapAuthor = escape(file.authorNickname || ""); + item.MapAuthor = htmlEntities(file.authorNickname || ""); } let name = file.mapName || ""; if (name.startsWith("")) { name = Buffer.from(name.replace("", ""), "latin1").toString('utf-8'); } - item.MapName = escape(name); + item.MapName = htmlEntities(name); } ); } catch (e: any) { diff --git a/core/plugins/admin/PlayerListsWindow.ts b/core/plugins/admin/PlayerListsWindow.ts index 6046b84..ea6db62 100644 --- a/core/plugins/admin/PlayerListsWindow.ts +++ b/core/plugins/admin/PlayerListsWindow.ts @@ -3,14 +3,21 @@ import ListWindow from "@core/ui/listwindow"; export default class PlayerListWindow extends ListWindow { async onAction(login: string, action: string, item: any) { + if (action == "UnBan") { + await tmc.chatCmd.execute(login, "//banlist remove " + item.Login); + await tmc.chatCmd.execute(login, "//banlist show"); + } + if (action == "UnIgnore") { + await tmc.chatCmd.execute(login, "//ignorelist remove " + item.Login); + await tmc.chatCmd.execute(login, "//ignorelist show"); + } if (action == "RemoveGuest") { await tmc.chatCmd.execute(login, "//guestlist remove " + item.Login); - await tmc.chatCmd.execute(login, "//gueslist show"); + await tmc.chatCmd.execute(login, "//guestlist show"); } - if (action == "RemoveBan") { + if (action == "RemoveBlacklist") { await tmc.chatCmd.execute(login, "//blacklist remove " + item.Login); await tmc.chatCmd.execute(login, "//blacklist show"); } } - } \ No newline at end of file diff --git a/core/plugins/admin/index.ts b/core/plugins/admin/index.ts index 002c262..a6fe87d 100644 --- a/core/plugins/admin/index.ts +++ b/core/plugins/admin/index.ts @@ -1,575 +1,971 @@ -import {castType, escape} from "@core/utils"; -import ModeSettingsWindow from "./ModeSettingsWindow"; -import Plugin from "@core/plugins"; -import fs from "fs"; -import LocalMapsWindow from "./LocalMapsWindow"; -import PlayerListsWindow from "./PlayerListsWindow"; -import fsPath from 'path'; -import {type Map} from "@core/mapmanager.ts"; - -export default class AdminPlugin extends Plugin { - - async onLoad() { - if (tmc.game.Name != "TmForever") { - tmc.addCommand("//modesettings", this.cmdModeSettings.bind(this), "Display mode settings"); - } - tmc.addCommand("//skip", async () => await tmc.server.call("NextMap"), "Skips Map"); - tmc.addCommand("//res", async () => await tmc.server.call("RestartMap"), "Restarts Map"); - tmc.addCommand("//kick", async (login: string, params: string[]) => { - const kickLogin:any = params.shift(); - if (!kickLogin) { - return tmc.chat("¤cmd¤//kick ¤info¤needs a login", login); - } - await tmc.server.call("Kick", kickLogin, params.join(" ")); - }, "Kicks player"); - tmc.addCommand("//ban", async (login: string, params: string[]) => { - if (!params[0]) { - return tmc.chat("¤cmd¤//ban ¤info¤needs a login", login); - } - await tmc.server.call("Ban", params[0]); - }, "Bans player"); - tmc.addCommand("//unban", async (login: string, params: string[]) => { - if (!params[0]) { - return tmc.chat("¤cmd¤//unban ¤info¤needs a login", login); - } - await tmc.server.call("Unban", params[0]); - }, "Unbans player"); - tmc.addCommand("//cancel", async () => await tmc.server.call("CancelVote"), "Cancels vote"); - tmc.addCommand("//er", async () => { - try { - tmc.server.send("ForceEndRound"); - } catch (err: any) { - tmc.chat("¤error¤" + err.message); - } - }, "Ends round"); - tmc.addCommand("//mode", async (login: string, params: string[]) => { - if (!params[0]) { - return tmc.chat("¤cmd¤//mode ¤info¤needs a mode", login); - } - if (tmc.game.Name == "TmForever") { - const modes: { [key: string]: number } = { - "rounds": 0, "ta": 1, "team": 2, "laps": 3, "stunts": 4, "cup": 5 - } - if (modes[params[0]] === undefined) { - return tmc.chat("¤cmd¤//mode ¤info¤needs a valid mode", login); - } - tmc.chat(`Gamemode set to ${params[0]}`); - await tmc.server.call("SetGameMode", modes[params[0]]); - } - - if (tmc.game.Name == "Trackmania") { - const scripts: { [key: string]: string } = { - "rounds": "Trackmania/TM_Rounds_Online.Script.txt", - "ta": "Trackmania/TM_TimeAttack_Online.Script.txt", - "team": "Trackmania/TM_Team_Online.Script.txt", - "laps": "Trackmania/TM_Laps_Online.Script.txt", - "cup": "Trackmania/TM_Cup_Online.Script.txt" - }; - if (scripts[params[0]] === undefined) { - return tmc.chat("¤cmd¤//mode ¤info¤needs a valid mode", login); - } - tmc.chat(`¤info¤Gamemode set to ¤white¤${params[0]}`); - await tmc.server.call("SetScriptName", scripts[params[0]]); - } - }, "Sets gamemode"); - tmc.addCommand("//setpass", async (login: string, params: string[]) => { - const newPass = params[0] || ""; - await tmc.server.call("SetServerPassword", newPass); - if (newPass == "") { - tmc.chat(`¤info¤Password removed`, login); - } else { - tmc.chat(`¤info¤Password set to "¤white¤${newPass}¤info¤"`, login); - } - }, "Sets server password"); - - tmc.addCommand("//setspecpass", async (login: string, params: string[]) => { - const newPass = params[0] || ""; - await tmc.server.call("SetServerPasswordForSpectator", newPass); - if (newPass == "") { - tmc.chat(`¤info¤Spectator password removed`, login); - } else { - tmc.chat(`¤info¤Spectator password set to "¤white¤${newPass}¤info¤"`, login); - } - }, "Sets spectator password"); - - tmc.addCommand("//warmup", async (login: string, params: string[]) => { - if (!params[0] && isNaN(Number.parseInt(params[0]))) { - return tmc.chat("¤cmd¤//warmup ¤info¤needs numeric value"); - } - tmc.chat(`¤info¤Warmup set to ¤white¤${params[0]}`, login); - await tmc.server.call("SetWarmUpDuration", Number.parseInt(params[0])); - }, "Sets warmup duration"); - - tmc.addCommand("//ignore", async (login: string, params: string[]) => { - if (!params[0]) { - return tmc.chat("¤cmd¤//ignore ¤info¤needs a login", login); - } - await tmc.server.call("Ignore", params[0]); - tmc.chat(`¤info¤Ignoring ¤white¤${params[0]}`, login); - }, "Ignores player"); - - tmc.addCommand("//unignore", async (login: string, params: string[]) => { - if (!params[0]) { - return tmc.chat("¤cmd¤//unignore ¤info¤needs a login", login); - } - await tmc.server.call("UnIgnore", params[0]); - tmc.chat(`¤info¤Unignored ¤white¤${params[0]}`, login); - }, "Unignores player"); - - tmc.addCommand("//talimit", async (login: string, params: string[]) => { - if (!params[0]) { - return tmc.chat("¤cmd¤//talimit ¤info¤needs numeric value in seconds"); - } - - if (tmc.game.Name == "TmForever") { - tmc.server.send("SetTimeAttackLimit", Number.parseInt(params[0]) * 1000); - tmc.chat(`¤info¤Timelimit set to ¤white¤${params[0]} ¤info¤seconds`); - return; - } - - if (tmc.game.Name == "Trackmania" || tmc.game.Name == "ManiaPlanet") { - const settings = {"S_TimeLimit": Number.parseInt(params[0])}; - tmc.server.send("SetModeScriptSettings", settings); - tmc.chat(`¤info¤Timelimit set to ¤white¤${params[0]} ¤info¤seconds`); - return; - } - - }, "Sets timelimit"); - - tmc.addCommand("//jump", async (login: string, params: string[]) => { - if (!params[0] && isNaN(Number.parseInt(params[0]))) { - return tmc.chat("¤cmd¤//jump ¤info¤needs numeric value"); - } - try { - let map: any; - if (params[0].toString().length < 5) { - let index = Number.parseInt(params[0]) - 1; - map = tmc.maps.getMaplist()[index]; - } else { - map = tmc.maps.getMaplist().find((m: any) => m.UId == params[0]); - } - if (map) { - tmc.chat(`¤info¤Jumped to ¤white¤${map.Name}¤info¤ by ¤white¤${map.AuthorNickname ? map.AuthorNickname : map.Author}`); - await tmc.server.call("ChooseNextMap", map.FileName); - tmc.server.send("NextMap"); - } else { - tmc.chat("¤error¤Couldn't find map", login) - } - } catch (err: any) { - tmc.chat(err.message, login); - } - }, "Jumps to map in playlist"); - - tmc.addCommand("//wml", async (login: string, params: string[]) => { - let file = "tracklist.txt"; - if (params[0]) file = params[0].replace(".txt", "") + ".txt" - try { - const answer = await tmc.server.call("SaveMatchSettings", "MatchSettings/" + file); - if (!answer) { - tmc.chat(`¤error¤Couldn't save matchsettings to ¤white¤${file}`, login); - return; - } - tmc.chat(`¤info¤Saved matchsettings to ¤white¤${file}`, login); - } catch (err: any) { - tmc.chat(err.message, login); - } - }, "Saves matchsettings"); - - tmc.addCommand("//rml", async (login: string, params: string[]) => { - let file = "tracklist"; - if (params[0]) file = params[0].replace(".txt", "") + ".txt"; - try { - const answer = await tmc.server.call("LoadMatchSettings", "MatchSettings/" + file); - if (!answer) { - tmc.chat(`¤error¤Couldn't read matchsettings from ¤white¤${file}`, login); - return; - } - await tmc.maps.syncMaplist(); - tmc.chat(`¤info¤Matchsettings read from ¤white¤${file}`, login); - } catch (err: any) { - tmc.chat(err.message, login); - } - }, "Reads matchsettings"); - tmc.addCommand("//shuffle", async (login: string, params: string[]) => { - try { - let maps: Map[] = await tmc.server.call("GetMapList", -1, 0); - maps = maps.sort(() => Math.random() - 0.5); - let toserver:string[] = []; - for (const map of maps) { - toserver.push(map.FileName); - } - await tmc.server.call("RemoveMapList", toserver); - tmc.server.send("AddMapList", toserver); - await tmc.maps.syncMaplist(); - tmc.chat(`¤info¤Maplist Shuffled.`); - } catch (err: any) { - tmc.chat("¤error¤" + err.message, login); - } - }, "Shuffles maplist"); - tmc.addCommand("//remove", async (login: string, params: string[]) => { - let map: any = tmc.maps.currentMap; - if (params[0] != undefined) { - if (params[0].toString().length < 5) { - let index = Number.parseInt(params[0]) - 1; - map = tmc.maps.getMaplist()[index]; - } else { - map = tmc.maps.getMaplist().find((m: any) => m.UId == params[0]); - } - } - - try { - if (!map) { - tmc.chat(`¤error¤Couldn't find map`, login); - return; - } - await tmc.server.call("RemoveMap", map.FileName); - tmc.maps.removeMap(map.UId); - tmc.chat(`¤info¤Removed map ¤white¤${map.Name} ¤info¤from the playlist.`, login); - } catch (err: any) { - tmc.chat(err.message, login); - } - }, "Removes map from playlist"); - - tmc.addCommand("//call", async (login: string, params: string[]) => { - const method = params.shift(); - if (method === undefined) { - return tmc.chat("¤cmd¤//call ¤info¤needs a method", login); - } - try { - let out: any = []; - for (let i = 0; i < params.length; i++) { - if (params[i].toLowerCase() == "true") out[i] = true; - else if (params[i].toLowerCase() == "false") out[i] = false; - else if (!isNaN(Number.parseInt(params[i]))) out[i] = Number.parseInt(params[i]); - else out[i] = params[i]; - } - const answer = await tmc.server.call(method, ...out); - tmc.chat(answer.toString(), login); - } catch (err: any) { - tmc.chat(err.message, login); - } - }, "Calls server method"); - tmc.addCommand("//wu", async (login: string, params: string[]) => { - if (tmc.game.Name == "TmForever") { - tmc.server.send("SetWarmUp", true); - } - }, "Starts warmup"); - tmc.addCommand("//endwu", async (login: string, params: string[]) => { - if (tmc.game.Name == "TmForever") { - tmc.server.send("SetWarmUp", false); - } else { - tmc.server.callScript("Trackmania.WarmUp.ForceStop"); - } - }, "end warmup"); - - - tmc.addCommand("//addlocal", this.cmdAddLocal.bind(this), "Adds local map to playlist"); - tmc.addCommand("//modecommand", async (login: string, params: string[]) => { - if (!params[0]) { - return tmc.chat("¤cmd¤//modecommand ¤info¤needs a command", login); - } - if (!params[1]) { - return tmc.chat(`¤cmd¤//modecommand ${params[0]} ¤info¤needs a parameter`, login); - } - const outCommand: any = {}; - outCommand["Command_" + params[0]] = castType(params[1]); - - try { - await tmc.server.call("SendModeScriptCommands", outCommand); - } catch (err: any) { - tmc.chat("¤error¤" + err.message, login); - } - }, "Send mode command"); - tmc.addCommand("//guestlist", this.cmdGuestlist.bind(this), "Manage Guestlist"); - tmc.addCommand("//blacklist", this.cmdBlacklist.bind(this), "Manage Blacklist"); - tmc.addCommand("//togglemute", this.cmdToggleMute.bind(this), "Toggle Mute"); - } - - async onUnload() { - tmc.removeCommand("//skip"); - tmc.removeCommand("//res"); - tmc.removeCommand("//kick"); - tmc.removeCommand("//ban"); - tmc.removeCommand("//unban"); - tmc.removeCommand("//cancel"); - tmc.removeCommand("//er"); - tmc.removeCommand("//mode"); - tmc.removeCommand("//setpass"); - tmc.removeCommand("//setspecpass"); - tmc.removeCommand("//warmup"); - tmc.removeCommand("//ignore"); - tmc.removeCommand("//unignore"); - tmc.removeCommand("//togglemute"); - tmc.removeCommand("//talimit"); - tmc.removeCommand("//jump"); - tmc.removeCommand("//wml"); - tmc.removeCommand("//rml"); - tmc.removeCommand("//shuffle"); - tmc.removeCommand("//remove"); - tmc.removeCommand("//call"); - tmc.removeCommand("//wu"); - tmc.removeCommand("//endwu"); - if (tmc.game.Name != "TmForever") { - tmc.removeCommand("//modesettings"); - tmc.removeCommand("//set"); - } - tmc.removeCommand("//addlocal"); - tmc.removeCommand("//modecommand"); - tmc.removeCommand("//guestlist"); - tmc.removeCommand("//blacklist"); - } - - async onStart(): Promise { - const menu = tmc.storage["menu"]; - if (menu) { - menu.addItem({ - category: "Map", - title: "Adm: Add", - action: "//addlocal", - admin: true, - }); - menu.addItem({ - category: "Map", - title: "Adm: Shuffle", - action: "//shuffle", - admin: true, - }); - menu.addItem({ - category: "Map", - title: "Adm: Write list", - action: "//wml", - admin: true - }); - menu.addItem({ - category: "Map", - title: "Adm: Skip", - action: "//skip", - admin: true - }); - menu.addItem({ - category: "Map", - title: "Adm: Restart", - action: "//res", - admin: true - }); - - - if (tmc.game.Name == "Trackmania") { - menu.addItem({ - category: "Server", - title: "ModeSettings", - action: "//modesettings", - admin: true - }); - } - } - } - - async cmdModeSettings(login: string, args: string[]) { - const window = new ModeSettingsWindow(login); - window.size = {width: 160, height: 95}; - window.title = "Mode Settings"; - const settings = await tmc.server.call("GetModeScriptSettings"); - let out: any = []; - for (const data in settings) { - out.push({ - setting: data, - value: settings[data], - type: typeof settings[data] - }); - } - window.setItems(out); - window.setColumns([ - {key: "setting", title: "Setting", width: 75}, - {key: "value", title: "Value", width: 50, type: "entry"}, - {key: "type", title: "Type", width: 25} - ]); - window.addApplyButtons(); - await window.display(); - } - - async cmdAddLocal(login: string, args: string[]) { - if (args.length < 1) { - const window = new LocalMapsWindow(login); - window.size = {width: 175, height: 95}; - let out:any = []; - for (let file of fs.readdirSync(tmc.mapsPath, {withFileTypes: true, recursive: true, encoding: "utf8"})) { - if (file.name.toLowerCase().endsWith(".gbx")) { - let name = escape(file.name.replaceAll(/[.](Map|Challenge)[.]Gbx/gi, "")); - let filename = fsPath.resolve(tmc.mapsPath, file.parentPath, file.name); - let path = file.parentPath.replace(tmc.mapsPath, ""); - out.push({ - File: filename, - FileName: name, - Path: path, - MapName: "", - MapAuthor: "", - }); - } - } - window.title = `Add Local Maps [${out.length}]`; - window.setItems(out); - window.setColumns([ - {key: "Path", title: "Path", width: 40}, - {key: "FileName", title: "Map File", width: 30, action: "Add"}, - {key: "MapName", title: "Name", width: 50, action: "Add"}, - {key: "MapAuthor", title: "Author", width: 35} - ]); - window.setActions(["Add"]); - await window.display(); - } else { - try { - await tmc.server.call("AddMapList", args); - await tmc.maps.syncMaplist(); - tmc.chat(`Added ${args.length} maps to the playlist`, login); - } catch (e: any) { - tmc.chat("Error: " + e.message, login); - } - } - } - - async cmdToggleMute(login: any, args: string[]) { - if (args.length < 1) { - tmc.chat("Usage: ¤cmd¤//togglemute ¤white¤", login); - return; - } - try { - let ignores = await tmc.server.call("GetIgnoreList", 1000, 0); - for (const ignore of ignores) { - if (ignore.Login == args[0]) { - tmc.server.send("UnIgnore", args[0]); - tmc.chat(`¤info¤UnIgnoring ¤white¤${args[0]}`, login); - return; - } - } - tmc.server.send("Ignore", args[0]); - tmc.chat(`¤info¤Ignoring ¤white¤${args[0]}`, login); - } catch (e: any) { - tmc.chat(e, login); - } - } - - async cmdSetSetting(login: any, args: string[]) { - if (args.length < 2) { - tmc.chat("Usage: ¤cmd¤//set ¤white¤ ", login); - return; - } - const setting = args[0]; - const value: string = args[1]; - - try { - await tmc.server.call("SetModeScriptSettings", {[setting]: castType(value)}); - } catch (e: any) { - tmc.chat("Error: " + e.message, login); - return; - } - tmc.chat(`Set ${setting} to ${value}`, login); - } - - async cmdGuestlist(login: string, args: string[]) { - if (args.length < 1) { - tmc.chat("Usage: ¤cmd¤//guestlist ¤white¤add ,remove , show, list", login); - return; - } - switch (args[0]) { - case "add": { - try { - if (!args[1]) return tmc.chat("¤error¤Missing madatory argument: ", login); - await tmc.server.call("LoadGuestList", "guestlist.txt"); - const res = await tmc.server.call("AddGuest", args[1]); - if (!res) return tmc.chat(`¤error¤Unable to add ¤white¤${args[1]}¤error¤ as guest.`, login); - tmc.server.send("SaveGuestList", "guestlist.txt"); - return tmc.chat(`¤info¤Guest added: ¤white¤${args[1]}`, login); - } catch (e: any) { - tmc.chat(`¤error¤${e.message}`, login); - return; - } - } - case "remove": { - try { - if (!args[1]) return tmc.chat("¤error¤Missing madatory argument: ", login); - await tmc.server.call("LoadGuestList", "guestlist.txt"); - const res = await tmc.server.call("RemoveGuest", args[1]); - if (!res) return tmc.chat(`¤error¤Unable to remove ¤white¤${args[1]}¤error¤ as guest.`, login); - tmc.server.send("SaveGuestList", "guestlist.txt"); - return tmc.chat(`¤info¤Guest removed: ¤white¤${args[1]}`, login); - } catch (e: any) { - tmc.chat(`¤error¤${e.message}`, login); - return; - } - } - case "show": - case "list": { - const window = new PlayerListsWindow(login); - window.title = "Guestlist"; - window.size = {width: 175, height: 95}; - window.setItems(await tmc.server.call("GetGuestList", -1, 0)); - window.setColumns([ - {key: "Login", title: "Login", width: 100}, - - ]); - window.setActions(["RemoveGuest"]); - await window.display(); - return; - } - default: { - tmc.chat("Usage: ¤cmd¤//guestlist ¤white¤add ,remove , show, list", login); - return; - } - } - } - - async cmdBlacklist(login: string, args: string[]) { - if (args.length < 1) { - tmc.chat("Usage: ¤cmd¤//blacklist ¤white¤add ,remove , show, list", login); - return; - } - switch (args[0]) { - case "add": { - try { - if (!args[1]) return tmc.chat("¤error¤Missing madatory argument: ", login); - await tmc.server.call("LoadBlackList", "blacklist.txt"); - const res = await tmc.server.call("BlackList", args[1]); - if (!res) return tmc.chat(`¤error¤Unable to add ¤white¤${args[1]}¤error¤ as guest.`, login); - tmc.server.send("SaveBlackList", "blacklist.txt"); - return tmc.chat(`¤info¤Added to blacklist: ¤white¤${args[1]}`, login); - } catch (e: any) { - tmc.chat(`¤error¤${e.message}`, login); - return; - } - } - case "remove": { - try { - if (!args[1]) return tmc.chat("¤error¤Missing madatory argument: ", login); - await tmc.server.call("LoadBlackList", "blacklist.txt"); - const res = await tmc.server.call("UnBlackList", args[1]); - if (!res) return tmc.chat(`¤error¤Unable to remove ¤white¤${args[1]}¤error¤ from blacklist`, login); - tmc.server.send("SaveBlackList", "blacklist.txt"); - return tmc.chat(`¤info¤Blacklist removed: ¤white¤${args[1]}`, login); - } catch (e: any) { - tmc.chat(`¤error¤${e.message}`, login); - return; - } - } - case "show": - case "list": { - const window = new PlayerListsWindow(login); - window.title = "Blacklist"; - window.size = {width: 175, height: 95}; - window.setItems(await tmc.server.call("GetBlackList", -1, 0)); - window.setColumns([ - {key: "Login", title: "Login", width: 100}, - - ]); - window.setActions(["RemoveBan"]); - await window.display(); - return; - } - default: { - tmc.chat("Usage: ¤cmd¤//blacklist ¤white¤add ,remove , show, list", login); - return; - } - } - } - -} +import { castType, htmlEntities } from '@core/utils'; +import ModeSettingsWindow from './ModeSettingsWindow'; +import Plugin from '@core/plugins'; +import fs from 'fs'; +import LocalMapsWindow from './LocalMapsWindow'; +import PlayerListsWindow from './PlayerListsWindow'; +import fsPath from 'path'; +import { type Map } from '@core/mapmanager.ts'; + +enum Mode { + Rounds = 0, + TimeAttack = 1, + Team = 2, + Laps = 3, + Stunts = 4, + Cup = 5 +} + +export default class AdminPlugin extends Plugin { + async onLoad() { + if (tmc.game.Name != 'TmForever') { + tmc.addCommand('//modesettings', this.cmdModeSettings.bind(this), 'Display mode settings'); + } + tmc.addCommand('//skip', async () => await tmc.server.call('NextMap'), 'Skips Map'); + tmc.addCommand('//res', async () => await tmc.server.call('RestartMap'), 'Restarts Map'); + tmc.addCommand( + '//kick', + async (login: string, params: string[]) => { + const kickLogin: any = params.shift(); + if (!kickLogin) { + return tmc.chat('¤cmd¤//kick ¤info¤needs a login', login); + } + await tmc.server.call('Kick', kickLogin, params.join(' ')); + }, + 'Kicks player' + ); + tmc.addCommand( + '//ban', + async (login: string, params: string[]) => { + if (!params[0]) { + return tmc.chat('¤cmd¤//ban ¤info¤needs a login', login); + } + await tmc.server.call('Ban', params[0]); + }, + 'Bans player' + ); + tmc.addCommand( + '//unban', + async (login: string, params: string[]) => { + if (!params[0]) { + return tmc.chat('¤cmd¤//unban ¤info¤needs a login', login); + } + await tmc.server.call('Unban', params[0]); + }, + 'Unbans player' + ); + tmc.addCommand('//cancel', async () => await tmc.server.call('CancelVote'), 'Cancels vote'); + tmc.addCommand( + '//er', + async () => { + try { + tmc.server.send('ForceEndRound'); + } catch (err: any) { + tmc.chat('¤error¤' + err.message); + } + }, + 'Ends round' + ); + tmc.addCommand( + '//mode', + async (login: string, params: string[]) => { + if (!params[0]) { + return tmc.chat('¤cmd¤//mode ¤info¤needs a mode', login); + } + if (tmc.game.Name == 'TmForever') { + const modes: { [key: string]: number } = { + rounds: 0, + ta: 1, + team: 2, + laps: 3, + stunts: 4, + cup: 5 + }; + if (modes[params[0]] === undefined) { + return tmc.chat('¤cmd¤//mode ¤info¤needs a valid mode', login); + } + tmc.chat(`Gamemode set to ${params[0]}`); + await tmc.server.call('SetGameMode', modes[params[0]]); + } + + if (tmc.game.Name == 'Trackmania') { + const scripts: { [key: string]: string } = { + rounds: 'Trackmania/TM_Rounds_Online.Script.txt', + ta: 'Trackmania/TM_TimeAttack_Online.Script.txt', + team: 'Trackmania/TM_Team_Online.Script.txt', + laps: 'Trackmania/TM_Laps_Online.Script.txt', + cup: 'Trackmania/TM_Cup_Online.Script.txt' + }; + if (scripts[params[0]] === undefined) { + return tmc.chat('¤cmd¤//mode ¤info¤needs a valid mode', login); + } + tmc.chat(`¤info¤Gamemode set to ¤white¤${params[0]}`); + await tmc.server.call('SetScriptName', scripts[params[0]]); + } + }, + 'Sets gamemode' + ); + tmc.addCommand( + '//password', + async (login: string, params: string[]) => { + const newPass = params[0] || ''; + await tmc.server.call('SetServerPassword', newPass); + await tmc.server.call('SetServerPasswordForSpectator', newPass); + if (newPass == '') { + tmc.chat(`¤info¤Password removed`, login); + } else { + tmc.chat(`¤info¤Password set to "¤white¤${newPass}¤info¤"`, login); + } + }, + 'Sets server password' + ); + + tmc.addCommand( + '//warmup', + async (login: string, params: string[]) => { + if (!params[0] && isNaN(Number.parseInt(params[0]))) { + return tmc.chat('¤cmd¤//warmup ¤info¤needs numeric value'); + } + tmc.chat(`¤info¤Warmup set to ¤white¤${params[0]}`, login); + await tmc.server.call('SetAllWarmUpDuration', Number.parseInt(params[0])); + }, + 'Sets warmup duration' + ); + tmc.addCommand( + '//servername', + async (login: string, params: string[]) => { + const newName = params.join(' '); + await tmc.server.call('SetServerName', newName); + tmc.chat(`¤info¤Servername set to ¤white¤${newName}`, login); + }, + "Sets server's name" + ); + tmc.addCommand( + '//servercomment', + async (login: string, params: string[]) => { + const newComment = params.join(' '); + await tmc.server.call('SetServerComment', newComment); + tmc.chat(`¤info¤Servercomment set to ¤white¤${newComment}`, login); + }, + 'set server comment' + ); + tmc.addCommand( + '//maxplayers', + async (login: string, params: string[]) => { + const newMax = Number.parseInt(params[0]); + if (newMax < 0) { + return tmc.chat('¤cmd¤setmaxplayers ¤info¤needs a positive numeric value', login); + } + await tmc.server.call('SetMaxPlayers', newMax); + tmc.chat(`¤info¤Max players set to ¤white¤${newMax}`, login); + }, + 'Sets max players' + ); + + tmc.addCommand( + '//ignore', + async (login: string, params: string[]) => { + if (!params[0]) { + return tmc.chat('¤cmd¤//ignore ¤info¤needs a login', login); + } + await tmc.server.call('Ignore', params[0]); + tmc.chat(`¤info¤Ignoring ¤white¤${params[0]}`, login); + }, + 'Ignores player' + ); + + tmc.addCommand( + '//unignore', + async (login: string, params: string[]) => { + if (!params[0]) { + return tmc.chat('¤cmd¤//unignore ¤info¤needs a login', login); + } + await tmc.server.call('UnIgnore', params[0]); + tmc.chat(`¤info¤Unignored ¤white¤${params[0]}`, login); + }, + 'Unignores player' + ); + + tmc.addCommand( + '//talimit', + async (login: string, params: string[]) => { + if (!params[0]) { + return tmc.chat('¤cmd¤//talimit ¤info¤needs numeric value in seconds'); + } + + if (tmc.game.Name == 'TmForever') { + const mode = await tmc.server.call('GetGameMode'); + if (mode == Mode.TimeAttack) { + tmc.server.send('SetTimeAttackLimit', Number.parseInt(params[0]) * 1000); + tmc.chat(`¤info¤Timelimit set to ¤white¤${params[0]} ¤info¤seconds`); + return; + } + if (mode == Mode.Laps) { + tmc.server.send('SetLapsTimeLimit', Number.parseInt(params[0]) * 1000); + tmc.chat(`¤info¤Timelimit set to ¤white¤${params[0]} ¤info¤seconds`); + return; + } + tmc.chat('¤error¤Timelimit not supported in this mode'); + return; + } + + if (tmc.game.Name == 'Trackmania' || tmc.game.Name == 'ManiaPlanet') { + const settings = { S_TimeLimit: Number.parseInt(params[0]) }; + tmc.server.send('SetModeScriptSettings', settings); + tmc.chat(`¤info¤Timelimit set to ¤white¤${params[0]} ¤info¤seconds`); + return; + } + }, + 'Sets timelimit' + ); + + tmc.addCommand( + '//jump', + async (login: string, params: string[]) => { + if (!params[0] && isNaN(Number.parseInt(params[0]))) { + return tmc.chat('¤cmd¤//jump ¤info¤needs numeric value'); + } + try { + let map: any; + if (params[0].toString().length < 5) { + let index = Number.parseInt(params[0]) - 1; + map = tmc.maps.getMaplist()[index]; + } else { + map = tmc.maps.getMaplist().find((m: any) => m.UId == params[0]); + } + if (map) { + tmc.chat(`¤info¤Jumped to ¤white¤${map.Name}¤info¤ by ¤white¤${map.AuthorNickname ? map.AuthorNickname : map.Author}`); + await tmc.server.call('ChooseNextMap', map.FileName); + tmc.server.send('NextMap'); + } else { + tmc.chat("¤error¤Couldn't find map", login); + } + } catch (err: any) { + tmc.chat(err.message, login); + } + }, + 'Jumps to map in playlist' + ); + + tmc.addCommand( + '//wml', + async (login: string, params: string[]) => { + let file = 'tracklist.txt'; + if (params[0]) file = params[0].replace('.txt', '') + '.txt'; + try { + const answer = await tmc.server.call('SaveMatchSettings', 'MatchSettings/' + file); + if (!answer) { + tmc.chat(`¤error¤Couldn't save matchsettings to ¤white¤${file}`, login); + return; + } + tmc.chat(`¤info¤Saved matchsettings to ¤white¤${file}`, login); + } catch (err: any) { + tmc.chat(err.message, login); + } + }, + 'Saves matchsettings' + ); + + tmc.addCommand( + '//rml', + async (login: string, params: string[]) => { + let file = 'tracklist'; + if (params[0]) file = params[0].replace('.txt', '') + '.txt'; + try { + const answer = await tmc.server.call('LoadMatchSettings', 'MatchSettings/' + file); + if (!answer) { + tmc.chat(`¤error¤Couldn't read matchsettings from ¤white¤${file}`, login); + return; + } + await tmc.maps.syncMaplist(); + tmc.chat(`¤info¤Matchsettings read from ¤white¤${file}`, login); + } catch (err: any) { + tmc.chat(err.message, login); + } + }, + 'Reads matchsettings' + ); + tmc.addCommand( + '//shuffle', + async (login: string, params: string[]) => { + try { + let maps: Map[] = await tmc.server.call('GetMapList', -1, 0); + maps = maps.sort(() => Math.random() - 0.5); + let toserver: string[] = []; + for (const map of maps) { + toserver.push(map.FileName); + } + await tmc.server.call('RemoveMapList', toserver); + tmc.server.send('AddMapList', toserver); + await tmc.maps.syncMaplist(); + tmc.chat(`¤info¤Maplist Shuffled.`); + } catch (err: any) { + tmc.chat('¤error¤' + err.message, login); + } + }, + 'Shuffles maplist' + ); + tmc.addCommand( + '//remove', + async (login: string, params: string[]) => { + let map: any = tmc.maps.currentMap; + if (params[0] != undefined) { + if (params[0].toString().length < 5) { + let index = Number.parseInt(params[0]) - 1; + map = tmc.maps.getMaplist()[index]; + } else { + map = tmc.maps.getMaplist().find((m: any) => m.UId == params[0]); + } + } + + try { + if (!map) { + tmc.chat(`¤error¤Couldn't find map`, login); + return; + } + await tmc.server.call('RemoveMap', map.FileName); + tmc.maps.removeMap(map.UId); + tmc.chat(`¤info¤Removed map ¤white¤${map.Name} ¤info¤from the playlist.`, login); + } catch (err: any) { + tmc.chat(err.message, login); + } + }, + 'Removes map from playlist' + ); + + tmc.addCommand( + '//call', + async (login: string, params: string[]) => { + const method = params.shift(); + if (method === undefined) { + return tmc.chat('¤cmd¤//call ¤info¤needs a method', login); + } + try { + let out: any = []; + for (let i = 0; i < params.length; i++) { + if (params[i].toLowerCase() == 'true') out[i] = true; + else if (params[i].toLowerCase() == 'false') out[i] = false; + else if (!isNaN(Number.parseInt(params[i]))) out[i] = Number.parseInt(params[i]); + else out[i] = params[i]; + } + const answer = await tmc.server.call(method, ...out); + tmc.chat(answer.toString(), login); + } catch (err: any) { + tmc.chat(err.message, login); + } + }, + 'Calls server method' + ); + tmc.addCommand( + '//wu', + async (login: string, params: string[]) => { + if (tmc.game.Name == 'TmForever') { + tmc.server.send('SetWarmUp', true); + } + }, + 'Starts warmup' + ); + tmc.addCommand( + '//endwu', + async (login: string, params: string[]) => { + if (tmc.game.Name == 'TmForever') { + tmc.server.send('SetWarmUp', false); + } else { + tmc.server.callScript('Trackmania.WarmUp.ForceStop'); + } + }, + 'end warmup' + ); + tmc.addCommand( + '//timeout', + async (login: string, params: string[]) => { + if (!params[0] && isNaN(Number.parseInt(params[0]))) { + return tmc.chat('¤cmd¤//timeout ¤info¤needs numeric value'); + } + try { + tmc.server.send('SetFinishTimeout', Number.parseInt(params[0]) * 1000); + tmc.chat(`¤info¤Timeout set to ¤white¤${params[0]} ¤info¤seconds`); + } catch (err: any) { + tmc.chat(err.message, login); + } + }, + 'Set finish timeout for rounds' + ); + tmc.addCommand( + '//cupwinners', + async (login: string, params: string[]) => { + if (!params[0] && isNaN(Number.parseInt(params[0]))) { + return tmc.chat('¤cmd¤//cupwinners ¤info¤needs numeric value'); + } + try { + tmc.server.send('SetCupNbWinners', Number.parseInt(params[0])); + tmc.chat(`¤info¤Cup winners set to ¤white¤${params[0]}`, login); + } catch (err: any) { + tmc.chat(err.message, login); + } + }, + 'Set cup winners' + ); + tmc.addCommand( + '//forceteam', + async (login: string, params: string[]) => { + if (!params[0]) { + return tmc.chat('¤cmd¤//forceteam ¤info¤needs a login', login); + } + if (!params[1]) { + return tmc.chat('¤cmd¤//forceteam ¤info¤needs a team', login); + } + try { + tmc.server.send('ForcePlayerTeam', params[0], params[1]); + tmc.chat(`¤info¤Forced ¤white¤${params[0]} ¤info¤to team ${params[1]}`, login); + } catch (err: any) { + tmc.chat(err.message, login); + } + }, + 'Force player to team' + ); + + tmc.addCommand( + '//pointlimit', + async (login: string, params: string[]) => { + if (!params[0] && isNaN(Number.parseInt(params[0]))) { + return tmc.chat('¤cmd¤//pointlimit ¤info¤needs numeric value'); + } + try { + const mode = await tmc.server.call('GetGameMode'); + switch (mode) { + case Mode.Team: { + tmc.server.send('SetTeamPointsLimit', Number.parseInt(params[0])); + tmc.chat(`¤info¤Team points limit set to ¤white¤${params[0]}`, login); + break; + } + case Mode.Rounds: { + tmc.server.send('SetRoundPointsLimit', Number.parseInt(params[0])); + tmc.chat(`¤info¤Rounds limit set to ¤white¤${params[0]}`, login); + break; + } + case Mode.Cup: { + tmc.server.send('SetCupPointsLimit', Number.parseInt(params[0])); + tmc.chat(`¤info¤Cup points limit set to ¤white¤${params[0]}`, login); + break; + } + } + } catch (err: any) { + tmc.chat(err.message, login); + } + }, + 'Set points limit' + ); + tmc.addCommand( + '//maxpoints', + async (login: string, params: string[]) => { + if (!params[0]) { + return tmc.chat('¤cmd¤//maxpoints ¤info¤needs value'); + } + try { + tmc.server.send('SetMaxPointsTeam', Number.parseInt(params[0])); + tmc.chat(`¤info¤Max points set to ¤white¤${params[0]}`, login); + } catch (err: any) { + tmc.chat(err.message, login); + } + }, + 'Set team max points' + ); + tmc.addCommand('//rpoints', async (login: string, params: string[]) => { + if (!params[0]) { + return tmc.chat('¤cmd¤//rpoints ¤info¤needs value'); + } + const array = params[0].split(','); + if (array.length != 2) { + return tmc.chat('¤cmd¤//rpoints ¤info¤needs atleast 2 values'); + } + let newArray: number[] = []; + for (let number of array) { + if (!isNaN(Number.parseInt(number.trim()))) { + newArray.push(parseInt(number.trim())); + } + } + try { + tmc.server.send( + 'SetRoundCustomPoints', + newArray.sort((a, b) => a - b) + ); + tmc.chat(`¤info¤Rounds points set to ¤white¤${params[0]}`, login); + } catch (err: any) { + tmc.chat(err.message, login); + } + }); + tmc.addCommand( + '//usenewrules', + async (login: string, params: string[]) => { + const mode = await tmc.server.call('GetGameMode'); + + if (params[0] == 'true') { + if (mode == Mode.Rounds) tmc.server.send('SetUseNewRulesRound', true); + if (mode == Mode.Team) tmc.server.send('SetUseNewRulesTeam', true); + tmc.chat(`¤info¤Using new rounds rules`, login); + } else if (params[0] == 'false') { + if (mode == Mode.Rounds) tmc.server.send('SetUseNewRulesRound', false); + if (mode == Mode.Team) tmc.server.send('SetUseNewRulesTeam', false); + tmc.chat(`¤info¤Using old rounds rules`, login); + } else { + tmc.chat('¤cmd¤//usenewrules ¤info¤needs a boolean value', login); + } + }, + 'Use new rounds rules' + ); + tmc.addCommand( + '//laps', + async (login: string, params: string[]) => { + if (!params[0] && isNaN(Number.parseInt(params[0]))) { + return tmc.chat('¤cmd¤//laps ¤info¤needs numeric value'); + } + try { + tmc.server.send('SetNbLaps', Number.parseInt(params[0])); + tmc.chat(`¤info¤Laps set to ¤white¤${params[0]}`, login); + } catch (err: any) { + tmc.chat(err.message, login); + } + }, + 'Set laps' + ); + + tmc.addCommand('//addlocal', this.cmdAddLocal.bind(this), 'Adds local map to playlist'); + tmc.addCommand( + '//modecommand', + async (login: string, params: string[]) => { + if (!params[0]) { + return tmc.chat('¤cmd¤//modecommand ¤info¤needs a command', login); + } + if (!params[1]) { + return tmc.chat(`¤cmd¤//modecommand ${params[0]} ¤info¤needs a parameter`, login); + } + const outCommand: any = {}; + outCommand['Command_' + params[0]] = castType(params[1]); + + try { + await tmc.server.call('SendModeScriptCommands', outCommand); + } catch (err: any) { + tmc.chat('¤error¤' + err.message, login); + } + }, + 'Send mode command' + ); + tmc.addCommand('//guestlist', this.cmdGuestlist.bind(this), 'Manage Guestlist'); + tmc.addCommand('//blacklist', this.cmdBlacklist.bind(this), 'Manage Blacklist'); + tmc.addCommand('//ignorelist', this.cmdIgnoreList.bind(this), 'Manage Ignorelist'); + tmc.addCommand('//banlist', this.cmdBanlist.bind(this), 'Manage Banlist'); + tmc.addCommand('//togglemute', this.cmdToggleMute.bind(this), 'Toggle Mute'); + } + + async onUnload() { + tmc.removeCommand('//timeout'); + tmc.removeCommand('//rlimit'); + tmc.removeCommand('//pointlimit'); + tmc.removeCommand('//usenewrules'); + tmc.removeCommand('//laps'); + tmc.removeCommand('//skip'); + tmc.removeCommand('//res'); + tmc.removeCommand('//kick'); + tmc.removeCommand('//ban'); + tmc.removeCommand('//unban'); + tmc.removeCommand('//cancel'); + tmc.removeCommand('//er'); + tmc.removeCommand('//mode'); + tmc.removeCommand('//setpass'); + tmc.removeCommand('//setspecpass'); + tmc.removeCommand('//warmup'); + tmc.removeCommand('//ignore'); + tmc.removeCommand('//unignore'); + tmc.removeCommand('//togglemute'); + tmc.removeCommand('//talimit'); + tmc.removeCommand('//jump'); + tmc.removeCommand('//wml'); + tmc.removeCommand('//rml'); + tmc.removeCommand('//shuffle'); + tmc.removeCommand('//remove'); + tmc.removeCommand('//call'); + tmc.removeCommand('//wu'); + tmc.removeCommand('//endwu'); + if (tmc.game.Name != 'TmForever') { + tmc.removeCommand('//modesettings'); + tmc.removeCommand('//set'); + } + tmc.removeCommand('//addlocal'); + tmc.removeCommand('//modecommand'); + tmc.removeCommand('//guestlist'); + tmc.removeCommand('//blacklist'); + } + + async onStart(): Promise { + const menu = tmc.storage['menu']; + if (menu) { + menu.addItem({ + category: 'Map', + title: 'Adm: Add', + action: '//addlocal', + admin: true + }); + menu.addItem({ + category: 'Map', + title: 'Adm: Shuffle', + action: '//shuffle', + admin: true + }); + menu.addItem({ + category: 'Map', + title: 'Adm: Write list', + action: '//wml', + admin: true + }); + menu.addItem({ + category: 'Map', + title: 'Adm: Skip', + action: '//skip', + admin: true + }); + menu.addItem({ + category: 'Map', + title: 'Adm: Restart', + action: '//res', + admin: true + }); + + if (tmc.game.Name == 'Trackmania') { + menu.addItem({ + category: 'Server', + title: 'ModeSettings', + action: '//modesettings', + admin: true + }); + } + } + } + + async cmdModeSettings(login: string, args: string[]) { + const window = new ModeSettingsWindow(login); + window.size = { width: 160, height: 95 }; + window.title = 'Mode Settings'; + const settings = await tmc.server.call('GetModeScriptSettings'); + let out: any = []; + for (const data in settings) { + out.push({ + setting: data, + value: settings[data], + type: typeof settings[data] + }); + } + window.setItems(out); + window.setColumns([ + { key: 'setting', title: 'Setting', width: 75 }, + { key: 'value', title: 'Value', width: 50, type: 'entry' }, + { key: 'type', title: 'Type', width: 25 } + ]); + window.addApplyButtons(); + await window.display(); + } + + async cmdAddLocal(login: string, args: string[]) { + if (args.length < 1) { + const window = new LocalMapsWindow(login); + window.size = { width: 175, height: 95 }; + let out: any = []; + for (let file of fs.readdirSync(tmc.mapsPath, { withFileTypes: true, recursive: true, encoding: 'utf8' })) { + if (file.name.toLowerCase().endsWith('.gbx')) { + let name = htmlEntities(file.name.replaceAll(/[.](Map|Challenge)[.]Gbx/gi, '')); + let filename = fsPath.resolve(tmc.mapsPath, file.parentPath, file.name); + let path = file.parentPath.replace(tmc.mapsPath, ''); + out.push({ + File: filename, + FileName: name, + Path: path, + MapName: '', + MapAuthor: '' + }); + } + } + window.title = `Add Local Maps [${out.length}]`; + window.setItems(out); + window.setColumns([ + { key: 'Path', title: 'Path', width: 40 }, + { key: 'FileName', title: 'Map File', width: 30, action: 'Add' }, + { key: 'MapName', title: 'Name', width: 50, action: 'Add' }, + { key: 'MapAuthor', title: 'Author', width: 35 } + ]); + window.setActions(['Add']); + await window.display(); + } else { + try { + await tmc.server.call('AddMapList', args); + await tmc.maps.syncMaplist(); + tmc.chat(`Added ${args.length} maps to the playlist`, login); + } catch (e: any) { + tmc.chat('Error: ' + e.message, login); + } + } + } + + async cmdToggleMute(login: any, args: string[]) { + if (args.length < 1) { + tmc.chat('Usage: ¤cmd¤//togglemute ¤white¤', login); + return; + } + try { + let ignores = await tmc.server.call('GetIgnoreList', 1000, 0); + for (const ignore of ignores) { + if (ignore.Login == args[0]) { + tmc.server.send('UnIgnore', args[0]); + tmc.chat(`¤info¤UnIgnoring ¤white¤${args[0]}`, login); + return; + } + } + tmc.server.send('Ignore', args[0]); + tmc.chat(`¤info¤Ignoring ¤white¤${args[0]}`, login); + } catch (e: any) { + tmc.chat(e, login); + } + } + + async cmdSetSetting(login: any, args: string[]) { + if (args.length < 2) { + tmc.chat('Usage: ¤cmd¤//set ¤white¤ ', login); + return; + } + const setting = args[0]; + const value: string = args[1]; + + try { + await tmc.server.call('SetModeScriptSettings', { [setting]: castType(value) }); + } catch (e: any) { + tmc.chat('Error: ' + e.message, login); + return; + } + tmc.chat(`Set ${setting} to ${value}`, login); + } + + async cmdGuestlist(login: string, args: string[]) { + if (args.length < 1) { + tmc.chat('Usage: ¤cmd¤//guestlist ¤white¤add ,remove , clear, show, list', login); + return; + } + switch (args[0]) { + case 'clear': { + try { + await tmc.server.call('CleanGuestList'); + tmc.chat('¤info¤Guestlist cleared', login); + } catch (e: any) { + tmc.chat('¤error¤' + e.message, login); + } + return; + } + case 'add': { + try { + if (!args[1]) return tmc.chat('¤error¤Missing madatory argument: ', login); + await tmc.server.call('LoadGuestList', 'guestlist.txt'); + const res = await tmc.server.call('AddGuest', args[1]); + if (!res) return tmc.chat(`¤error¤Unable to add ¤white¤${args[1]}¤error¤ as guest.`, login); + tmc.server.send('SaveGuestList', 'guestlist.txt'); + return tmc.chat(`¤info¤Guest added: ¤white¤${args[1]}`, login); + } catch (e: any) { + tmc.chat(`¤error¤${e.message}`, login); + return; + } + } + case 'remove': { + try { + if (!args[1]) return tmc.chat('¤error¤Missing madatory argument: ', login); + await tmc.server.call('LoadGuestList', 'guestlist.txt'); + const res = await tmc.server.call('RemoveGuest', args[1]); + if (!res) return tmc.chat(`¤error¤Unable to remove ¤white¤${args[1]}¤error¤ as guest.`, login); + tmc.server.send('SaveGuestList', 'guestlist.txt'); + return tmc.chat(`¤info¤Guest removed: ¤white¤${args[1]}`, login); + } catch (e: any) { + tmc.chat(`¤error¤${e.message}`, login); + return; + } + } + case 'show': + case 'list': { + const window = new PlayerListsWindow(login); + window.title = 'Guestlist'; + window.size = { width: 175, height: 95 }; + window.setItems(await tmc.server.call('GetGuestList', -1, 0)); + window.setColumns([{ key: 'Login', title: 'Login', width: 100 }]); + window.setActions(['RemoveGuest']); + await window.display(); + return; + } + default: { + tmc.chat('Usage: ¤cmd¤//guestlist ¤white¤add ,remove , show, list', login); + return; + } + } + } + async cmdBanlist(login: string, args: string[]) { + if (args.length < 1) { + tmc.chat('Usage: ¤cmd¤//banlist ¤white¤add ,remove , clear, show, list', login); + return; + } + switch (args[0]) { + case 'clear': { + try { + await tmc.server.call('CleanBanList'); + tmc.chat('¤info¤Banlist cleared', login); + } catch (e: any) { + tmc.chat('¤error¤' + e.message, login); + } + return; + } + case 'add': { + try { + if (!args[1]) return tmc.chat('¤error¤Missing madatory argument: ', login); + const res = await tmc.server.call('Ban', args[1]); + if (!res) return tmc.chat(`¤error¤Unable to ban ¤white¤${args[1]}¤error¤`, login); + return tmc.chat(`¤info¤Added to banlist: ¤white¤${args[1]}`, login); + } catch (e: any) { + tmc.chat(`¤error¤${e.message}`, login); + return; + } + } + case 'remove': { + try { + if (!args[1]) return tmc.chat('¤error¤Missing madatory argument: ', login); + const res = await tmc.server.call('Unban', args[1]); + if (!res) return tmc.chat(`¤error¤Unable to unban ¤white¤${args[1]}¤error¤`, login); + return tmc.chat(`¤info¤Removed from banlist: ¤white¤${args[1]}`, login); + } catch (e: any) { + tmc.chat(`¤error¤${e.message}`, login); + return; + } + } + case 'show': + case 'list': { + const window = new PlayerListsWindow(login); + window.title = 'BanList'; + window.size = { width: 175, height: 95 }; + window.setItems(await tmc.server.call('GetBanList', -1, 0)); + window.setColumns([{ key: 'Login', title: 'Login', width: 100 }]); + window.setActions(['UnBan']); + await window.display(); + return; + } + default: { + tmc.chat('Usage: ¤cmd¤//guestlist ¤white¤add ,remove ,clear, show, list', login); + return; + } + } + } + async cmdIgnoreList(login: string, args: string[]) { + if (args.length < 1) { + tmc.chat('Usage: ¤cmd¤//ignorelist ¤white¤add ,remove , clear, show, list', login); + return; + } + switch (args[0]) { + case 'clear': { + try { + await tmc.server.call('CleanIgnoreList'); + tmc.chat('¤info¤Ignorelist cleared', login); + } catch (e: any) { + tmc.chat('¤error¤' + e.message, login); + } + return; + } + case 'add': { + try { + if (!args[1]) return tmc.chat('¤error¤Missing madatory argument: ', login); + const res = await tmc.server.call('Ignore', args[1]); + if (!res) return tmc.chat(`¤error¤Unable to add ¤white¤${args[1]}¤error¤ as ignored player.`, login); + return tmc.chat(`¤info¤Player ignored: ¤white¤${args[1]}`, login); + } catch (e: any) { + tmc.chat(`¤error¤${e.message}`, login); + return; + } + } + case 'remove': { + try { + if (!args[1]) return tmc.chat('¤error¤Missing madatory argument: ', login); + const res = await tmc.server.call('UnIgnore', args[1]); + if (!res) return tmc.chat(`¤error¤Unable to remove ¤white¤${args[1]}¤error¤ from ignored players`, login); + return tmc.chat(`¤info¤Player unignored: ¤white¤${args[1]}`, login); + } catch (e: any) { + tmc.chat(`¤error¤${e.message}`, login); + return; + } + } + case 'show': + case 'list': { + const window = new PlayerListsWindow(login); + window.title = 'Guestlist'; + window.size = { width: 175, height: 95 }; + window.setItems(await tmc.server.call('GetIgnoreList', -1, 0)); + window.setColumns([{ key: 'Login', title: 'Login', width: 100 }]); + window.setActions(['UnIgnore']); + await window.display(); + return; + } + default: { + tmc.chat('Usage: ¤cmd¤//guestlist ¤white¤add ,remove , clear, show, list', login); + return; + } + } + } + + async cmdBlacklist(login: string, args: string[]) { + if (args.length < 1) { + tmc.chat('Usage: ¤cmd¤//blacklist ¤white¤add ,remove , clear, show, list', login); + return; + } + switch (args[0]) { + case 'clear': { + try { + await tmc.server.call('ClearBlackList'); + tmc.chat('¤info¤Blacklist cleared', login); + } catch (e: any) { + tmc.chat('¤error¤' + e.message, login); + } + return; + } + case 'add': { + try { + if (!args[1]) return tmc.chat('¤error¤Missing madatory argument: ', login); + await tmc.server.call('LoadBlackList', 'blacklist.txt'); + const res = await tmc.server.call('BlackList', args[1]); + if (!res) return tmc.chat(`¤error¤Unable to add ¤white¤${args[1]}¤error¤ as guest.`, login); + tmc.server.send('SaveBlackList', 'blacklist.txt'); + return tmc.chat(`¤info¤Added to blacklist: ¤white¤${args[1]}`, login); + } catch (e: any) { + tmc.chat(`¤error¤${e.message}`, login); + return; + } + } + case 'remove': { + try { + if (!args[1]) return tmc.chat('¤error¤Missing madatory argument: ', login); + await tmc.server.call('LoadBlackList', 'blacklist.txt'); + const res = await tmc.server.call('UnBlackList', args[1]); + if (!res) return tmc.chat(`¤error¤Unable to remove ¤white¤${args[1]}¤error¤ from blacklist`, login); + tmc.server.send('SaveBlackList', 'blacklist.txt'); + return tmc.chat(`¤info¤Blacklist removed: ¤white¤${args[1]}`, login); + } catch (e: any) { + tmc.chat(`¤error¤${e.message}`, login); + return; + } + } + case 'show': + case 'list': { + const window = new PlayerListsWindow(login); + window.title = 'Blacklist'; + window.size = { width: 175, height: 95 }; + window.setItems(await tmc.server.call('GetBlackList', -1, 0)); + window.setColumns([{ key: 'Login', title: 'Login', width: 100 }]); + window.setActions(['RemoveBlacklist']); + await window.display(); + return; + } + default: { + tmc.chat('Usage: ¤cmd¤//blacklist ¤white¤add ,remove , clear, show, list', login); + return; + } + } + } +} diff --git a/core/plugins/announces/index.ts b/core/plugins/announces/index.ts index 5fb893b..95d8e3e 100644 --- a/core/plugins/announces/index.ts +++ b/core/plugins/announces/index.ts @@ -43,7 +43,7 @@ export default class Announces extends Plugin { tmc.cli(msg); } - async onPlayerDisconnect(player: any) { + async onPlayerDisconnect(player: Player) { const msg = `¤white¤${player.nickname}¤info¤ leaves!`; tmc.chat(msg); tmc.cli(msg); diff --git a/core/plugins/chat/index.ts b/core/plugins/chat/index.ts index 4ab69f6..62cc755 100644 --- a/core/plugins/chat/index.ts +++ b/core/plugins/chat/index.ts @@ -1,4 +1,5 @@ import Plugin from '../index'; +import { emotesMap } from './tmnf_emojis'; export default class Chat extends Plugin { static depends: string[] = []; @@ -8,23 +9,32 @@ export default class Chat extends Plugin { async onLoad() { try { - this.pluginEnabled = await tmc.server.call("ChatEnableManualRouting", true, false) as boolean; - tmc.server.addListener("Trackmania.PlayerChat", this.onPlayerChat, this); - tmc.addCommand("//chat", this.cmdChat.bind(this), "Controls chat"); + this.pluginEnabled = (await tmc.server.call('ChatEnableManualRouting', true, false)) as boolean; + tmc.server.addListener('Trackmania.PlayerChat', this.onPlayerChat, this); + tmc.addCommand('//chat', this.cmdChat.bind(this), 'Controls chat'); + if (tmc.game.Name == 'TmForever') { + tmc.chatCmd.addCommand( + '/emotes', + async (login: string) => { + tmc.chat('$fffTo apply emotes for tmnf, $0cf$L[http://bit.ly/Celyans_emotes_sheet]Click here$l$z$s$fff for instructions!', login); + }, + 'How to apply emotes' + ); + } } catch (e: any) { this.pluginEnabled = false; - tmc.cli("ChatPlugin: ¤error¤ " + e.message); + tmc.cli('ChatPlugin: ¤error¤ ' + e.message); } } async onUnload() { try { - await tmc.server.call("ChatEnableManualRouting", false, false); + await tmc.server.call('ChatEnableManualRouting', false, false); } catch (e: any) { console.log(e.message); } - tmc.removeCommand("//chat"); - tmc.server.removeListener("Trackmania.PlayerChat", this.onPlayerChat); + tmc.removeCommand('//chat'); + tmc.server.removeListener('Trackmania.PlayerChat', this.onPlayerChat); this.pluginEnabled = false; } @@ -33,13 +43,13 @@ export default class Chat extends Plugin { tmc.chat(`¤info¤usage: ¤cmd¤//chat ¤info¤ or ¤cmd¤//chat `); return; } - if (params.length == 1 && ["on", "off"].includes(params[0])) { - this.publicChatEnabled = params[0] == "on"; - tmc.chat("¤info¤Public chat is ¤white¤" + params[0]); + if (params.length == 1 && ['on', 'off'].includes(params[0])) { + this.publicChatEnabled = params[0] == 'on'; + tmc.chat('¤info¤Public chat is ¤white¤' + params[0]); return; } - if (params.length == 2 && ["on", "off"].includes(params[1])) { - if (params[1] == "on") { + if (params.length == 2 && ['on', 'off'].includes(params[1])) { + if (params[1] == 'on') { const index = this.playersDisabled.indexOf(params[0]); this.playersDisabled.splice(index, 1); tmc.chat(`¤info¤Playerchat for ¤white¤${params[0]} ¤info¤is now ¤white¤on!`, login); @@ -49,26 +59,31 @@ export default class Chat extends Plugin { } return; } - tmc.chat("¤info¤usage: ¤cmd¤//chat ¤info¤or ¤cmd¤//chat ") + tmc.chat('¤info¤usage: ¤cmd¤//chat ¤info¤or ¤cmd¤//chat '); } async onPlayerChat(data: any) { if (!this.pluginEnabled) return; if (!this.publicChatEnabled && !tmc.admins.includes(data[1])) { - tmc.chat("Public chat is disabled.", data[1]); + tmc.chat('Public chat is disabled.', data[1]); return; } if (data[0] == 0) return; - if (data[2].startsWith("/")) return; + if (data[2].startsWith('/')) return; if (this.playersDisabled.includes(data[1])) { - tmc.chat("Your chat is disabled.", data[1]); + tmc.chat('Your chat is disabled.', data[1]); return; } const player = await tmc.getPlayer(data[1]); - const nick = player.nickname.replaceAll(/\$[iwozs]/ig, ""); - const text = data[2]; + const nick = player.nickname.replaceAll(/\$[iwozs]/gi, ''); + let text = data[2]; + if (tmc.game.Name == 'TmForever' && process.env.CHAT_USE_EMOTES == 'true') { + for (const data of emotesMap) { + text = text.replaceAll(new RegExp('\\b(' + data.emote.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')\\b', 'g'), '$z$fff' + data.glyph + '$s$ff0'); + } + } const msg = `${nick}$z$s$fff »$ff0 ${text}`; - tmc.server.send("ChatSendServerMessage", msg); + tmc.server.send('ChatSendServerMessage', msg); tmc.cli(msg); } } diff --git a/core/plugins/chat/tmnf_emojis.ts b/core/plugins/chat/tmnf_emojis.ts new file mode 100644 index 0000000..1347879 --- /dev/null +++ b/core/plugins/chat/tmnf_emojis.ts @@ -0,0 +1,136 @@ +export const emotesMap = [ + { emote: 'widepeepohappy', glyph: '时间自定' }, + { emote: 'prayge', glyph: '源' }, + { emote: 'monkaW', glyph: '专' }, + { emote: 'dentge', glyph: '内部' }, + { emote: 'pepega', glyph: '奖' }, + { emote: 'EZ', glyph: '章' }, + { emote: 'YEP', glyph: '音' }, + { emote: 'feelsstrongman', glyph: '效' }, + { emote: 'pepog', glyph: '位' }, + { emote: 'feelsgoodman', glyph: '置' }, + { emote: 'pepelaugh', glyph: '此' }, + { emote: 'copium', glyph: '挑' }, + { emote: 'wicked', glyph: '锁' }, + { emote: 'pausechamp', glyph: '尾' }, + { emote: 'sadge', glyph: '激' }, + { emote: 'xdd', glyph: '最' }, + { emote: 'omegalul', glyph: '佳' }, + { emote: 'POG', glyph: '义' }, + { emote: 'clueless', glyph: '家' }, + { emote: 'bal?lo?oncat', glyph: '资' }, + { emote: '5head', glyph: '表' }, + { emote: 'monka', glyph: '题列' }, + { emote: 'forsencd', glyph: '标' }, + { emote: 'weirdchamp', glyph: '文' }, + { emote: 'kekw', glyph: '战' }, + { emote: 'sadcatw', glyph: '住' }, + { emote: 'o7', glyph: '迹' }, + { emote: 'kappa', glyph: '目' }, + { emote: 'skull', glyph: '问' }, + { emote: 'smirks?cat', glyph: '普通' }, + { emote: 'joy', glyph: '轴' }, + { emote: 'sob', glyph: '本' }, + { emote: 'heart', glyph: '被' }, + { emote: 'loves?smile', glyph: '了' }, + { emote: 'salutings?face', glyph: '为' }, + { emote: 'business', glyph: '互' }, + { emote: 'hmmnotes', glyph: '点' }, + { emote: 'hypers', glyph: '我' }, + { emote: 'podcaster', glyph: '活' }, + { emote: 'catrose', glyph: '联' }, + { emote: 't3moment', glyph: '戏' }, + { emote: 'lule', glyph: '数' }, + { emote: 'sunglassescat', glyph: '据' }, + { emote: 'waiting', glyph: '交' }, + { emote: 'Life', glyph: '换' }, + { emote: 'clap', glyph: '系' }, + { emote: 'esimo', glyph: '统' }, + { emote: 'minionlmao', glyph: '网游' }, + { emote: 'cathuh', glyph: '对' }, + { emote: 'wajaja', glyph: '建' }, + { emote: 'thonk', glyph: '测' }, + { emote: 'pengun', glyph: '络' }, + { emote: 'angry', glyph: '连' }, + { emote: 'popcorn', glyph: '和' }, + { emote: 'fire', glyph: '们' }, + { emote: 'kms', glyph: '议' }, + { emote: 'tutel', glyph: '您' }, + { emote: 'pray', glyph: '试' }, + { emote: 'peepoBlush', glyph: '防' }, + { emote: 'peepoClown', glyph: '墙' }, + { emote: 'widepeepoHug', glyph: '报警按下' }, + { emote: 'Bedge', glyph: '成' }, + { emote: 'MonkaSteer', glyph: '道' }, + { emote: 'Chatting', glyph: '狂' }, + { emote: 'pepeCringe', glyph: '记' }, + { emote: 'peepoIgnore', glyph: '查' }, + { emote: 'goods?game', glyph: '比' }, + { emote: 'peepoMonster', glyph: '无' }, + { emote: 'peepopCorn', glyph: '访' }, + { emote: 'PETTHEPEEPO', glyph: '试' }, + { emote: 'gma', glyph: '务' }, + { emote: 'Hugg', glyph: '器' }, + { emote: 'Staree', glyph: '没' }, + { emote: 'monkaEyes', glyph: '有错误初' }, + { emote: 'Evilge', glyph: '化' }, + { emote: 'Demonge', glyph: '应' }, + { emote: 'FeelsBadMan', glyph: '要' }, + { emote: 'disbelief', glyph: '火' }, + { emote: 'pikawow', glyph: '车' }, + { emote: 'gay', glyph: '在' }, + { emote: 'fatass', glyph: '上次计重' }, + { emote: 'goggers', glyph: '竞' }, + { emote: 'sniffa', glyph: '飙' }, + { emote: 'D:', glyph: '影' }, + { emote: 'cowJAM', glyph: '录' }, + { emote: 'yippie', glyph: '到' }, + { emote: 'opice', glyph: '理' }, + { emote: 'war', glyph: '设' }, + { emote: 'HUH', glyph: '需' }, + { emote: 'bonk', glyph: '主' }, + { emote: 'noooo', glyph: '接' }, + { emote: 'brain', glyph: '可' }, + { emote: 'nerd', glyph: '能' }, + { emote: 'logodiscord', glyph: '会' }, + { emote: 'logotwitch', glyph: '导' }, + { emote: 'logoinstagram', glyph: '致' }, + { emote: 'logosnapchat', glyph: '的' }, + { emote: 'wave', glyph: '回' }, + { emote: 'raiseds?eyebrow', glyph: '键' }, + { emote: 'unorev', glyph: '新' }, + { emote: 'kys', glyph: '开' }, + { emote: 'getout', glyph: '始' }, + { emote: 'bryson', glyph: '赛' }, + { emote: 'cap', glyph: '完' }, + { emote: 'sexo', glyph: '子' }, + { emote: 'flushed', glyph: '看' }, + { emote: 'SCTrophie', glyph: '中' }, + { emote: 'POLICEMODE', glyph: '法' }, + { emote: 'slayyy', glyph: '答' }, + { emote: 'middles?finger', glyph: '不' }, + { emote: 'rofl', glyph: '正' }, + { emote: 'cheese', glyph: '确' }, + { emote: 'handshake', glyph: '未' }, + { emote: 'cooking', glyph: '知' }, + { emote: 'trolling', glyph: '代' }, + { emote: 'infinitycar', glyph: '受' }, + { emote: 'candles', glyph: '支' }, + { emote: 'candycane', glyph: '持' }, + { emote: 'carols', glyph: '版' }, + { emote: 'decorations', glyph: '地' }, + { emote: 'family', glyph: '用' }, + { emote: 'gift', glyph: '排' }, + { emote: 'lucky', glyph: '获' }, + { emote: 'magic', glyph: '得' }, + { emote: 'mass', glyph: '信' }, + { emote: 'preparations', glyph: '息' }, + { emote: 'sleigh', glyph: '语' }, + { emote: 'snowman', glyph: '言' }, + { emote: 'star', glyph: '登' }, + { emote: 'table', glyph: '口' }, + { emote: 'tree', glyph: '已' }, + { emote: 'turkey', glyph: '经' }, + { emote: 'wreath', glyph: '从' }, + { emote: 'yulelog', glyph: '另' } +]; diff --git a/core/plugins/database/index.ts b/core/plugins/database/index.ts index 70a7398..a7cc392 100644 --- a/core/plugins/database/index.ts +++ b/core/plugins/database/index.ts @@ -133,7 +133,7 @@ export default class GenericDb extends Plugin { async syncPlayer(player: PlayerType) { let dbPlayer = await Player.findByPk(player.login); if (dbPlayer == null) { - dbPlayer = await Player.create({ + await Player.create({ login: player.login, nickname: player.nickname, path: player.path @@ -144,10 +144,6 @@ export default class GenericDb extends Plugin { path: player.path }); } - if (dbPlayer && dbPlayer.customNick) { - tmc.cli('Setting nickname to ' + dbPlayer.customNick); - player.set('nickname', dbPlayer.customNick); - } } async syncPlayers() { diff --git a/core/plugins/debugtool/index.ts b/core/plugins/debugtool/index.ts index 9776ef3..68eb614 100644 --- a/core/plugins/debugtool/index.ts +++ b/core/plugins/debugtool/index.ts @@ -1,70 +1,73 @@ -import { memInfo } from "@core/utils"; -import Plugin from "@core/plugins"; -import tm from 'tm-essentials'; -import Widget from '@core/ui/widget'; - -export default class DebugTool extends Plugin { - widget: Widget | null = null; - intervalId: any = null; - - async onLoad() { - if (process.env.DEBUG == "true") { - this.widget = new Widget("core/plugins/debugtool/widget.twig"); - this.widget.pos = { x: 159, y: -60, z: 0 }; - tmc.addCommand("//addfake", this.cmdFakeUsers.bind(this), "Connect Fake users"); - tmc.addCommand("//removefake", this.cmdRemoveFakeUsers.bind(this), "Connect Fake users"); - } - tmc.addCommand("//mem", this.cmdMeminfo.bind(this)); - tmc.addCommand("//uptime", this.cmdUptime.bind(this)); - this.intervalId = setInterval(() => { - this.displayMemInfo(); - }, 60000) as any; - } - - async onStart() { - await this.displayMemInfo(); - } - - async onUnload() { - clearInterval(this.intervalId!); - tmc.removeCommand("//mem"); - tmc.removeCommand("//uptime"); - tmc.removeCommand("//addfake"); - tmc.removeCommand("//removefake"); - this.widget?.destroy(); - this.widget = null; - } - - async cmdRemoveFakeUsers(_login: string, _args: string[]) { - tmc.server.send("DisconnectFakePlayer", "*"); - } - - async cmdFakeUsers(_login: string, args: string[]) { - let count = Number.parseInt(args[0]) || 1; - if (count > 100) count = 100; - if (count < 1) count = 1; - for (let i = 0; i < count; i++) { - tmc.server.send("ConnectFakePlayer"); - } - } - - async cmdMeminfo(login: string, _args: string[]) { - const mem = memInfo(); - tmc.chat("¤info¤Memory usage: " + mem, login); - } - - async cmdUptime(login: string, _args: string[]) { - let diff = Date.now() - Number.parseInt(tmc.startTime); - tmc.chat("¤info¤Uptime: ¤white¤" + tm.Time.fromMilliseconds(diff).toTmString().replace(/[.]\d{3}/, ""), login); - } - - async displayMemInfo() { - const mem = memInfo(); - let start = Date.now() - Number.parseInt(tmc.startTime); - tmc.cli("¤info¤Memory usage: " + mem + " ¤info¤uptime: ¤white¤" + tm.Time.fromMilliseconds(start).toTmString().replace(/[.]\d{3}/, "")); - if (this.widget) { - this.widget.setData({ mem: mem }); - await this.widget.display(); - } - } -} +import { memInfo } from "@core/utils"; +import Plugin from "@core/plugins"; +import tm from 'tm-essentials'; +import Widget from '@core/ui/widget'; + +export default class DebugTool extends Plugin { + widget: Widget | null = null; + intervalId: any = null; + + async onLoad() { + if (process.env.DEBUG == "true") { + this.widget = new Widget("core/plugins/debugtool/widget.xml.twig"); + this.widget.pos = { x: 159, y: -60, z: 0 }; + if (tmc.game.Name != "TmForever") { + tmc.addCommand("//addfake", this.cmdFakeUsers.bind(this), "Connect Fake users"); + tmc.addCommand("//removefake", this.cmdRemoveFakeUsers.bind(this), "Connect Fake users"); + } + this.intervalId = setInterval(() => { + this.displayMemInfo(); + }, 30000) as any; + } + tmc.addCommand("//mem", this.cmdMeminfo.bind(this)); + tmc.addCommand("//uptime", this.cmdUptime.bind(this)); + + } + + async onStart() { + await this.displayMemInfo(); + } + + async onUnload() { + clearInterval(this.intervalId!); + tmc.removeCommand("//mem"); + tmc.removeCommand("//uptime"); + tmc.removeCommand("//addfake"); + tmc.removeCommand("//removefake"); + this.widget?.destroy(); + this.widget = null; + } + + async cmdRemoveFakeUsers(_login: string, _args: string[]) { + tmc.server.send("DisconnectFakePlayer", "*"); + } + + async cmdFakeUsers(_login: string, args: string[]) { + let count = Number.parseInt(args[0]) || 1; + if (count > 100) count = 100; + if (count < 1) count = 1; + for (let i = 0; i < count; i++) { + tmc.server.send("ConnectFakePlayer"); + } + } + + async cmdMeminfo(login: string, _args: string[]) { + const mem = memInfo(); + tmc.chat("¤info¤Memory usage: " + mem, login); + } + + async cmdUptime(login: string, _args: string[]) { + let diff = Date.now() - Number.parseInt(tmc.startTime); + tmc.chat("¤info¤Uptime: ¤white¤" + tm.Time.fromMilliseconds(diff).toTmString().replace(/[.]\d{3}/, ""), login); + } + + async displayMemInfo() { + const mem = memInfo(); + let start = Date.now() - Number.parseInt(tmc.startTime); + tmc.cli("¤info¤Memory usage: " + mem + " ¤info¤uptime: ¤white¤" + tm.Time.fromMilliseconds(start).toTmString().replace(/[.]\d{3}/, "")); + if (this.widget) { + this.widget.setData({ mem: mem }); + await this.widget.display(); + } + } +} diff --git a/core/plugins/debugtool/widget.twig b/core/plugins/debugtool/widget.xml.twig similarity index 75% rename from core/plugins/debugtool/widget.twig rename to core/plugins/debugtool/widget.xml.twig index 0a0b27e..466b56f 100644 --- a/core/plugins/debugtool/widget.twig +++ b/core/plugins/debugtool/widget.xml.twig @@ -1,4 +1,4 @@ -{% extends 'core/templates/widget.twig' %} +{% extends 'core/templates/widget.xml.twig' %} {% block content %}