cacao Posted July 5, 2020 Posted July 5, 2020 (edited) The command&events decorators (@command, @commandable, @event, @eventable) Information Installation Disadvantages Examples Sources Event decorator 1. Information: There is a snippet which helps to registry any commands/events to Rage API with the simple interface by using decorators. Library: rage-decorators [github] [npm-package] If you're using typescript, make sure there two options (experimentalDecorators, emitDecoratorMetadata) are true in your tsconfig.json: { "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true }, } 2. Installation: Via npm: $ npm i --save rage-decorators Via yarn: $ yarn add rage-decorators 3. Disadvantages: Disadvantage 1: the snippet requires rage-decorators package 4. Examples: Simple example: Скрытый текст Foo.ts import { command, commandable } from 'rage-decorators' // Tell the decorator that class should be resolved and register all their commands @commandable() class Foo { // Register a new command "foo" @command("foo") foo(player: PlayerMp, cmdDesc: string, bar?: string) { const text = "hello" + (bar || "") player.outputChatBox(text) } // Register two new commands "bar", "baz" @command(["bar", "baz"]) bar(player: PlayerMp, cmdDesc: string, bar?: string) { player.outputChatBox("Invoked command bar, allows usage bar/baz") } // "Register new command "player" with custom description @command("player", { desc: "Usage /{{cmdName}} [id]"}) playerExists(player: PlayerMp, cmdDesc: string, id: string) { if (typeof id === 'undefined') { player.outputChatBox(cmdDesc) } else { const has = !!mp.players.at(+id) player.outputChatBox('Player ' + (has ? 'exists': 'doesn\'t exists')) } } // Register new group command "/veh add" @command("add", "veh") vehAdd(): void { // code staff here... } // Throw an error 'Error: Duplicate command "veh"' @command("veh") veh(): void { } @command("remove", { group: "veh", desc: "Usage /{{groupName}} {{cmdName}} [id|code]" }) vehDelete(player: PlayerMp, cmdDesc: string, ..args: any[]): void { player.outputChatBox(cmdDesc) } } App.ts // we should import this package before our decorators import 'reflect-metadata' import { Foo } from './foo' // When an instance is created our commands is registered const foo = new Foo() My example of command list: Скрытый текст import { command, commandable, registeredCommands } from "rage-decorators" @commandable() class CommandManager { static readonly PER_PAGE = 10 private _list: string[] = [] private _maxPages: number = 0 constructor() { this.cmdlist = this.cmdlist.bind(this) } @command("cmdlist") cmdlist(player: PlayerMp, cmdDesc: string, pagination: string = "1"): void { const page = (+pagination || 1) > this.maxPages && this.maxPages || (+pagination || 1) const startIndex = (page - 1) * CommandManager.PER_PAGE , endIndex = page * CommandManager.PER_PAGE const cmds = this.list.slice(startIndex, endIndex) cmds.forEach((text, index) => player.outputChatBox(`[${startIndex+index+1}]: ` + text)) player.outputChatBox(`Page: ${page}, showing commands [${startIndex+1}..${startIndex+cmds.length}] of ${this.list.length}`) } private makeList(): void { let texts: string[] = [] registeredCommands.forEach((value, mainCmd) => { if (Array.isArray(value)) { let hasDescription = false const cmds = value.reduce((carry, { cmd, desc }) => { if (!hasDescription && desc) hasDescription = true return carry.concat(desc || cmd) }, [] as string[]) texts = [...texts, ...(hasDescription && cmds || cmds.map(cmdName => `/${mainCmd} ${cmdName}`))] } else { const { cmd, desc } = value texts = [...texts, ...(desc || cmd.map(cmdName => `/${cmdName}`))] } }) this._list = texts } get list(): string[] { if (!this._list.length) this.makeList() return this._list } get maxPages(): number { if (!this._maxPages) this._maxPages = Math.ceil(this.list.length / CommandManager.PER_PAGE) return this._maxPages } } export { CommandManager } App.ts // this is our entry script, so // import reflect-metadat before decorators import 'reflect-metadata' import { CommandManager } from './CommandManager // Our commands now are registered const commandManager = new CommandManager() Here is an example of /cmdlist 5. Source: Sources is now allowed on github repository and as npm package: https://github.com/READYTOMASSACRE/rage-decorators the source of decorators.ts (server-side only, for supporting events, client-side usage npm package) Скрытый текст /** * An interface of command entity */ export interface ICommand { cmd: string[], desc: string[] callable: string, group?: string } /** * A collection of commands */ export type CommandCollection = Map<string, ICommand | ICommand[]> /** * A storage of commands which is called by decorator command */ export const registeredCommands: CommandCollection = new Map<string, ICommand | ICommand[]>() /** * Resolve any commands which passed to classes commandable */ export const commandable = (): any => { return function (target: any): any { return class extends target { constructor(...args: any[]) { // first we must call an inherited constructor super(...args) // then we start record our commands to Rage API // check if commands has already registered if (!Reflect.getMetadata("design:cmdlist:init", target.prototype)) { const commands: CommandCollection = Reflect.getMetadata("design:cmdlist", target.prototype) || [] // register commands in Rage API commands.forEach((command, mainCmd) => { // check if the command is a group command if (Array.isArray(command)) { // flat array of commands const cmdList = command.reduce((carry, { cmd }) => carry.concat(cmd), [] as string[]) // flat array of descriptions const descList = command.reduce((carry, { desc }) => carry.concat(desc), [] as string[]) // make a group command mp.events.addCommand(mainCmd, (player: PlayerMp, fullText: string, ...args: string[]) => { const cmdName = args.shift() // check if current subcommand in command group // otherwise send a message to a player if (!cmdName || cmdList.indexOf(cmdName) === -1) { descList.forEach(text => player.outputChatBox(text)) } else { for (let { cmd, callable } of command) { // check if command exists if (cmd.indexOf(cmdName) !== -1) { // call registered command with [this] context of current class const descIndex = cmdList.findIndex(cmdname => cmdname === cmdName) this[callable](player, descList[descIndex], ...args) break } } } }) } else { // make a common command // and then call the registered command with [this] context of current class const { cmd, callable, desc } = command cmd.forEach((cmdName, cmdIndex) => mp.events.addCommand( cmdName, (player: PlayerMp, fullText: string, ...args: any[]) => this[callable](player, desc[cmdIndex], ...args) )) } }) // set flag to target.prototype that all their commands are registered Reflect.defineMetadata("design:cmdlist:init", true, target.prototype) } } } } } /** * Decorator for adding commands to RAGE API * * Supports templates in the desc param: * @template cmdName - name of command * @template groupName - name of group (if added in the additional params) * * @example {desc parameter}: `Usage: /{{cmdName}} id` * * @param {string | string[]} cmd - command(s) name, which will be added to mp.events.addCommand * @param {string | { group?: string, desc?: string }} params - additional params, add to group or add to description */ export const command = ( cmd: string | string[], params?: string | { group?: string, desc?: string } ): MethodDecorator => { let group: string | undefined = undefined let desc: string | undefined = undefined // detect which params are passed to function if (typeof params === 'string') { group = params } else if (params) { group = params.group desc = params.desc } // make sure we have an array in the cmd // and clean any duplicate cmds which passed into the params cmd = (Array.isArray(cmd) ? cmd : [cmd]) const cmds = cmd.filter((item, index) => cmd.indexOf(item) === index) // get a main cmd const mainCmd = group || cmds[0] /** @todo seterror, setlocale */ // throw erros if something has gone wrong if (!mainCmd) throw new Error("Wrong registry command") if (!group && registeredCommands.get(mainCmd)) throw new Error(`Duplicate command "${mainCmd}"`) if (group) { const command = registeredCommands.get(mainCmd) if (command) { if (Array.isArray(command)) { const flatCmds = command.reduce((carry, { cmd }) => carry.concat(cmd), [] as string[]) const intersect = cmds.filter(value => flatCmds.includes(value)) // make sure we won't add duplicate commands into group if (intersect.length) throw new Error(`Duplicate commands "${intersect.join(',')}" by group "${mainCmd}"`) } else { const { cmd } = command // make sure we won't add duplicate commands which intersect with group if (cmd.includes(mainCmd)) throw new Error(`Duplicate commands "${mainCmd}", trying to make a command group when a command has already existed`) } } } else { registeredCommands.forEach(value => { if (!Array.isArray(value)) { const { cmd } = value const intersect = cmds.filter(value => cmd.includes(value)) // make sure we won't add duplicate commands if (intersect.length) throw new Error(`Duplicate commands "${intersect.join(',')}"`) } }) } let description: string[] = [] // make the description of commands if (desc) { description = cmds.map(cmdname => { let newDescription = desc!.replace(/{{cmdName}}/, cmdname) if (group) newDescription = newDescription.replace(/{{groupName}}/, mainCmd) return newDescription }) } else { description = cmds.map(cmdname => `Usage /${group && (group + ' ' + cmdname) || cmdname}`) } // make a new object of command const newCommand: ICommand = { cmd: cmds, callable: "", desc: description, } // store in global storage our commands if (group) { const command = registeredCommands.get(mainCmd) registeredCommands.set(mainCmd, Array.isArray(command) ? [...command, newCommand] : [newCommand]) } else { registeredCommands.set(mainCmd, newCommand) } return function(target: Object, callableMethod: string | symbol, descriptor: TypedPropertyDescriptor<any>) { const targetCommands: CommandCollection = Reflect.getMetadata("design:cmdlist", target) || new Map<string, ICommand>() // throw error if we're trying add command to not callable value if (!(descriptor.value instanceof Function)) throw new Error(`Command "${mainCmd}" should be callable`) newCommand.callable = callableMethod.toString() // store in target context storage our commands to pass them into constructor // where we will can register them in Rage API if (group) { const command = targetCommands.get(mainCmd) targetCommands.set(mainCmd, Array.isArray(command) ? [...command, newCommand] : [newCommand]) } else { targetCommands.set(mainCmd, newCommand) } Reflect.defineMetadata("design:cmdlist", targetCommands, target) return descriptor } } the source of decorators.js (server-side only, for supporting events, client-side usage npm package) Скрытый текст "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.command = exports.commandable = exports.registeredCommands = void 0; exports.registeredCommands = new Map(); exports.commandable = () => { return function (target) { return class extends target { constructor(...args) { super(...args); if (!Reflect.getMetadata("design:cmdlist:init", target.prototype)) { const commands = Reflect.getMetadata("design:cmdlist", target.prototype) || []; commands.forEach((command, mainCmd) => { if (Array.isArray(command)) { const cmdList = command.reduce((carry, { cmd }) => carry.concat(cmd), []); const descList = command.reduce((carry, { desc }) => carry.concat(desc), []); mp.events.addCommand(mainCmd, (player, fullText, ...args) => { const cmdName = args.shift(); if (!cmdName || cmdList.indexOf(cmdName) === -1) { descList.forEach(text => player.outputChatBox(text)); } else { for (let { cmd, callable } of command) { if (cmd.indexOf(cmdName) !== -1) { const descIndex = cmdList.findIndex(cmdname => cmdname === cmdName); this[callable](player, descList[descIndex], ...args); break; } } } }); } else { const { cmd, callable, desc } = command; cmd.forEach((cmdName, cmdIndex) => mp.events.addCommand(cmdName, (player, fullText, ...args) => this[callable](player, desc[cmdIndex], ...args))); } }); Reflect.defineMetadata("design:cmdlist:init", true, target.prototype); } } }; }; }; exports.command = (cmd, params) => { let group = undefined; let desc = undefined; if (typeof params === 'string') { group = params; } else if (params) { group = params.group; desc = params.desc; } cmd = (Array.isArray(cmd) ? cmd : [cmd]); const cmds = cmd.filter((item, index) => cmd.indexOf(item) === index); const mainCmd = group || cmds[0]; if (!mainCmd) throw new Error("Wrong registry command"); if (!group && exports.registeredCommands.get(mainCmd)) throw new Error(`Duplicate command "${mainCmd}"`); if (group) { const command = exports.registeredCommands.get(mainCmd); if (command) { if (Array.isArray(command)) { const flatCmds = command.reduce((carry, { cmd }) => carry.concat(cmd), []); const intersect = cmds.filter(value => flatCmds.includes(value)); if (intersect.length) throw new Error(`Duplicate commands "${intersect.join(',')}" by group "${mainCmd}"`); } else { const { cmd } = command; if (cmd.includes(mainCmd)) throw new Error(`Duplicate commands "${mainCmd}", trying to make a command group when a command has already existed`); } } } else { exports.registeredCommands.forEach(value => { if (!Array.isArray(value)) { const { cmd } = value; const intersect = cmds.filter(value => cmd.includes(value)); if (intersect.length) throw new Error(`Duplicate commands "${intersect.join(',')}"`); } }); } let description = []; if (desc) { description = cmds.map(cmdname => { let newDescription = desc.replace(/{{cmdName}}/, cmdname); if (group) newDescription = newDescription.replace(/{{groupName}}/, mainCmd); return newDescription; }); } else { description = cmds.map(cmdname => `Usage /${group && (group + ' ' + cmdname) || cmdname}`); } const newCommand = { cmd: cmds, callable: "", desc: description, }; if (group) { const command = exports.registeredCommands.get(mainCmd); exports.registeredCommands.set(mainCmd, Array.isArray(command) ? [...command, newCommand] : [newCommand]); } else { exports.registeredCommands.set(mainCmd, newCommand); } return function (target, callableMethod, descriptor) { const targetCommands = Reflect.getMetadata("design:cmdlist", target) || new Map(); if (!(descriptor.value instanceof Function)) throw new Error(`Command "${mainCmd}" should be callable`); newCommand.callable = callableMethod.toString(); if (group) { const command = targetCommands.get(mainCmd); targetCommands.set(mainCmd, Array.isArray(command) ? [...command, newCommand] : [newCommand]); } else { targetCommands.set(mainCmd, newCommand); } Reflect.defineMetadata("design:cmdlist", targetCommands, target); return descriptor; }; }; 6. Event decorator: Event decorator is now avaliable in npm package, here an usaging example: import { eventable, event } from 'rage-decorators' @eventable() class Foo { @event("playerJoin") playerJoin(player: PlayerMp): void { console.log(`Player[${player.id}]${player.name} has joined to the server`) } } Edited July 6, 2020 by cacao v. 1.1.4, Added clientside support 3
cacao Posted July 5, 2020 Author Posted July 5, 2020 I've added npm package for more easier way to install them.
Recommended Posts
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now