omegga 1.17.2 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/dist/db/migrations/0001_shocking_the_captain.sql +4 -0
- package/dist/db/migrations/meta/0001_snapshot.json +753 -0
- package/dist/db/migrations/meta/_journal.json +7 -0
- package/dist/db/schema.js +10 -1
- package/dist/db/schema.js.map +1 -1
- package/dist/omegga/server.js +6 -4
- package/dist/omegga/server.js.map +1 -1
- package/dist/plugin.js.map +1 -1
- package/dist/webserver/backend/chatQuery.js +98 -0
- package/dist/webserver/backend/chatQuery.js.map +1 -0
- package/dist/webserver/backend/database.js +270 -5
- package/dist/webserver/backend/database.js.map +1 -1
- package/dist/webserver/backend/router/chat.js +21 -0
- package/dist/webserver/backend/router/chat.js.map +1 -1
- package/dist/webserver/backend/router/player.js +1 -0
- package/dist/webserver/backend/router/player.js.map +1 -1
- package/package.json +4 -3
- package/public/app.html +291 -48
- package/public/assets/IconShieldCheck-e9eEH-KF.js +2 -0
- package/public/assets/{IconWorld-DgiOjCuo.js → IconWorld-CMSFtQg-.js} +2 -2
- package/public/assets/IconWorldPlus-B3tINWGI.js +2 -0
- package/public/assets/IconX-Dnl42MJL.js +2 -0
- package/public/assets/app-Dfcj6I4Z.js +7 -0
- package/public/assets/auth-CNs90huc.js +2 -0
- package/public/assets/{index-B0LowBt1.js → index-B6mWKnn3.js} +2 -2
- package/public/assets/index-C6nSzQey.js +12 -0
- package/public/assets/{index-2cgXLhrt.js → index-NK6TBVas.js} +2 -2
- package/public/assets/shared/@components-DxLSzrzW.js +2 -0
- package/public/assets/shared/{@hooks-DlZ8TOOZ.js → @hooks-Df79JLiI.js} +2 -2
- package/public/assets/shared/@tabler_icons-react-CM_Q7eQ6.js +3 -0
- package/public/assets/shared/wouter-CPV7_rSk.js +2 -0
- package/public/auth.html +80 -5
- package/public/assets/IconShieldCheck-Bjlm8mX2.js +0 -2
- package/public/assets/IconWorldPlus-DJJNrT87.js +0 -2
- package/public/assets/IconX-CvmO0bbE.js +0 -2
- package/public/assets/app-Mh--ChqF.js +0 -7
- package/public/assets/auth-DQtYicf-.js +0 -2
- package/public/assets/index-BTSbuqTr.js +0 -12
- package/public/assets/shared/@components-DBMXoJMT.js +0 -2
- package/public/assets/shared/@tabler_icons-react-i-Q2c_A_.js +0 -3
- package/public/assets/shared/wouter-CUC1caxu.js +0 -2
package/dist/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","sources":["../src/plugin.ts"],"sourcesContent":["import { type EnvironmentPreset } from '@brickadia/presets';\nimport {\n type BRBanList,\n type BRPlayerNameCache,\n type BRRoleAssignments,\n type BRRoleSetup,\n} from '@brickadia/types';\nimport { type ConsoleCommands } from '@omegga/commands';\nimport {\n type IGamemode,\n type ILogMinigame,\n type IMinigameList,\n type IPlayerPositions,\n type IServerStatus,\n} from '@omegga/types';\nimport util from '@util';\nimport { type ReadSaveObject, type WriteSaveObject } from 'brs-js';\n\nexport const _OMEGGA_UTILS_IMPORT = util;\n/* eslint-disable no-var -- `var` is required to declare globalThis properties */\ndeclare global {\n export var Omegga: OmeggaLike;\n export var Player: StaticPlayer;\n export var OMEGGA_UTIL: typeof _OMEGGA_UTILS_IMPORT;\n}\n/* eslint-enable no-var */\n\nexport * from '@brickadia/types';\nexport * from '@omegga/types';\nexport type { ConsoleCommands } from '@omegga/commands';\nexport { OmeggaPlugin };\n\nexport type OL = OmeggaLike;\nexport type OP = OmeggaPlugin;\nexport type PC<T extends Record<string, unknown> = Record<string, unknown>> =\n PluginConfig<T>;\nexport type PS<T extends Record<string, unknown> = Record<string, unknown>> =\n PluginStore<T>;\nexport type PM = PluginMetrics;\n\nexport interface BrickBounds {\n minBound: [number, number, number];\n maxBound: [number, number, number];\n center: [number, number, number];\n}\n\n/** Created when a player clicks on a brick with an interact component */\nexport interface BrickInteraction {\n /** Brick name from catalog (Turkey Body, 4x Cube) */\n brick_name: string;\n /** Brick asset name; null when the display name is not recognized */\n brick_asset: string | null;\n /** Brick size; null when the display name is not recognized */\n brick_size: [number, number, number] | null;\n /** Player information, id, name, controller, and pawn */\n player: { id: string; name: string; controller: string; pawn: string };\n /** Brick center position */\n position: [number, number, number];\n\n /** message sent from a brick click interaction */\n message: string;\n /** data parsed from the line (if it starts with json:) */\n data: null | number | string | boolean | Record<string, unknown>;\n /** True when there was a json payload */\n json: boolean;\n /** True when there was a parse error */\n error: boolean;\n}\n\n/** AutoRestart options */\nexport type AutoRestartConfig = {\n players: boolean;\n announcement: boolean;\n saveWorld: boolean;\n};\n\nexport interface OmeggaPlayer {\n /** player name */\n name: string;\n\n /** player display name */\n displayName: string;\n\n /** player uuid */\n id: string;\n\n /** player controller id */\n controller: string;\n\n /** player state id */\n state: string;\n\n /**\n * Returns omegga\n */\n getOmegga(): OmeggaLike;\n\n /**\n * Clone a player\n */\n clone(): OmeggaPlayer;\n\n /**\n * Get raw player info (to feed into a constructor)\n */\n raw(): [string, string, string, string, string];\n\n /**\n * True if the player is the host\n */\n isHost(): boolean;\n\n /**\n * Clear player's bricks\n * @param quiet clear bricks quietly\n */\n clearBricks(quiet?: boolean): void;\n\n /**\n * Get player's roles, if any\n */\n getRoles(): readonly string[];\n\n /**\n * Get player's permissions in a map like `{\"Bricks.ClearOwn\": true, ...}`\n * @return permissions map\n */\n getPermissions(): Record<string, boolean>;\n\n /**\n * Get player's name color\n * @return 6 character hex string\n */\n getNameColor(): string;\n\n /**\n * Get the player's pawn; null when the player has no pawn\n * @return pawn\n */\n getPawn(): Promise<string | null>;\n\n /**\n * Get player's position; null when the player has no pawn\n * @return [x, y, z] coordinates\n */\n getPosition(): Promise<[number, number, number] | null>;\n\n /**\n * Gets a user's ghost brick info (by uuid, name, controller, or player object)\n * @return ghost brick data\n */\n getGhostBrick(): Promise<\n | {\n targetGrid: string;\n location: number[];\n orientation: string;\n }\n | undefined\n >;\n\n /**\n * gets a user's paint tool properties\n */\n getPaint(): Promise<\n | {\n materialIndex: string;\n materialAlpha: string;\n material: string;\n color: number[];\n }\n | undefined\n >;\n\n /**\n * gets whether or not the player is crouching\n */\n isCrouched(pawn?: string): Promise<boolean>;\n\n /**\n * gets whether or not the player is dead\n */\n isDead(pawn?: string): Promise<boolean>;\n\n /**\n * Gets the bounds of the template in the user's clipboard (bounds of original selection box)\n * @return template bounds\n */\n getTemplateBounds(): Promise<BrickBounds | undefined>;\n\n /**\n * Get bricks inside template bounds\n * @return BRS JS Save Data\n */\n getTemplateBoundsData(): Promise<ReadSaveObject | undefined>;\n\n /**\n * Load bricks at ghost brick location\n * @param saveData save data to load\n */\n loadDataAtGhostBrick(\n saveData: WriteSaveObject,\n options?: {\n rotate?: boolean;\n offX?: number;\n offY?: number;\n offZ?: number;\n quiet?: boolean;\n },\n ): Promise<void>;\n\n /**\n * Load bricks on this player's clipboard\n * @param saveName Save to load\n */\n loadBricks(saveName: string): void;\n\n /**\n * Load bricks on this player's clipboard passing save data\n * @param saveData save data to load\n */\n loadSaveData(\n saveData: WriteSaveObject,\n options?: {\n rotate?: boolean;\n offX?: number;\n offY?: number;\n offZ?: number;\n quiet?: boolean;\n },\n ): Promise<void>;\n\n /**\n * Kills this player\n */\n kill(): void;\n\n /**\n * Damages a player\n * @param amount Amount to damage\n */\n damage(amount: number): void;\n\n /**\n * Heal this player\n * @param amount to heal\n */\n heal(amount: number): void;\n\n /**\n * Gives a player an item\n * @param item Item name (Weapon_Bow)\n */\n giveItem(item: WeaponClass): void;\n\n /**\n * Removes an item from a player's inventory\n * @param item Item name (Weapon_Bow)\n */\n takeItem(item: WeaponClass): void;\n\n /**\n * Changes a player's team\n * @param teamIndex Team index\n */\n setTeam(teamIndex: number): void;\n\n /**\n * Changes a player's minigame\n * @param index Minigame index\n */\n setMinigame(index: number): void;\n\n /**\n * Changes a player's score\n * @param minigameIndex minigame index\n * @param score Score\n */\n setScore(minigameIndex: number, score: number): void;\n\n /**\n * Fetch a player's score\n * @param minigameIndex minigame index\n */\n getScore(minigameIndex: number): Promise<number>;\n\n /**\n * Set leaderboard value\n * @param key leaderboard key (Score, Kills, Deaths, CorrectGuesses)\n * @param value leaderboard value\n */\n setLeaderboard(key: string, value: number): void;\n\n /**\n * Get leaderboard value\n * @param key leaderboard key (Score, Kills, Deaths, CorrectGuesses)\n * @return leaderboard value\n */\n getLeaderboard(key: string): Promise<number | null>;\n}\n\nexport interface StaticPlayer {\n /**\n * get a player's roles, if any\n * @param omegga omegga instance\n * @param id player uuid\n * @return list of roles\n */\n getRoles(omegga: OmeggaLike, id: string): readonly string[];\n\n /**\n * get a player's permissions in a map like `{\"Bricks.ClearOwn\": true, ...}`\n * @param omegga Omegga instance\n * @param id player uuid\n * @return permissions map\n */\n getPermissions(omegga: OmeggaLike, id: string): Record<string, boolean>;\n\n /**\n * Kills a player\n * @param omegga Omegga instance\n * @param target Player or player name/id\n */\n kill(omegga: OmeggaLike, target: string | OmeggaPlayer): void;\n\n /**\n * Damages a player\n * @param omegga Omegga instance\n * @param target Player or player name/id\n * @param amount Damage amount\n */\n damage(\n omegga: OmeggaLike,\n target: string | OmeggaPlayer,\n amount: number,\n ): void;\n\n /**\n * Heal a player\n * @param omegga Omegga instance\n * @param target Player or player name/id\n * @param amount Heal amount\n */\n heal(omegga: OmeggaLike, target: string | OmeggaPlayer, amount: number): void;\n\n /**\n * Gives a player an item\n * @param omegga Omegga instance\n * @param target Player or player name/id\n * @param item Item name (Weapon_Bow)\n */\n giveItem(\n omegga: OmeggaLike,\n target: string | OmeggaPlayer,\n item: WeaponClass,\n ): void;\n\n /**\n * Removes an item from a player's inventory\n * @param omegga Omegga instance\n * @param target Player or player name/id\n * @param item Item name (Weapon_Bow)\n */\n takeItem(\n omegga: OmeggaLike,\n target: string | OmeggaPlayer,\n item: WeaponClass,\n ): void;\n\n /**\n * Changes a player's team\n * @param omegga Omegga instance\n * @param target Player or player name/id\n * @param teamIndex Team name? index?\n */\n setTeam(\n omegga: OmeggaLike,\n target: string | OmeggaPlayer,\n teamIndex: number,\n ): void;\n\n /**\n * Changes a player's minigame\n * @param omegga Omegga instance\n * @param target Player or player name/id\n * @param index Minigame index\n */\n setMinigame(\n omegga: OmeggaLike,\n target: string | OmeggaPlayer,\n index: number,\n ): void;\n\n /**\n * Changes a player's score\n * @param omegga Omegga instance\n * @param target Player or player name/id\n * @param minigameIndex minigame index\n * @param score Score\n */\n setScore(\n omegga: OmeggaLike,\n target: string | OmeggaPlayer,\n minigameIndex: number,\n score: number,\n ): void;\n\n /**\n * Fetches a player's score\n * @param omegga Omegga instance\n * @param target Player or player name/id\n * @param minigameIndex minigame index\n */\n getScore(\n omegga: OmeggaLike,\n target: string | OmeggaPlayer,\n minigameIndex: number,\n ): Promise<number>;\n\n /**\n * Set leaderboard value\n * @param omegga Omegga instance\n * @param target Player or player name/id\n * @param key leaderboard key (Score, Kills, Deaths, CorrectGuesses)\n * @param value leaderboard value\n */\n setLeaderboard(\n omegga: OmeggaLike,\n target: string | OmeggaPlayer,\n key: string,\n value: number,\n ): void;\n\n /**\n * Get leaderboard value\n * @param omegga Omegga instance\n * @param target Player or player name/id\n * @param key leaderboard key (Score, Kills, Deaths, CorrectGuesses)\n * @return leaderboard value\n */\n getLeaderboard(\n omegga: OmeggaLike,\n target: string | OmeggaPlayer,\n key: string,\n ): Promise<number | null>;\n}\n\nexport interface InjectedCommands {\n /** Get server status */\n getServerStatus(this: OmeggaLike): Promise<IServerStatus | null>;\n /**\n * Get a list of minigames and their indices\n * @deprecated minigames were replaced by a single gamemode (~CL14000); on\n * modern servers this returns at most one entry with an empty `owner`.\n * Prefer {@link InjectedCommands.getGamemode}.\n */\n listMinigames(this: OmeggaLike): Promise<IMinigameList>;\n // /** Get all pawn data */\n // getAllPawnData(this: OmeggaLike, fields: PawnDataField[]): void;\n /** Get all player positions and pawns */\n getAllPlayerPositions(this: OmeggaLike): Promise<IPlayerPositions>;\n /** Get minigames and members (one entry per gamemode on modern servers) */\n getMinigames(this: OmeggaLike): Promise<ILogMinigame[]>;\n /**\n * Get the single gamemode and its teams/players (modern servers, >=CL14000).\n * Returns null on older servers (use {@link InjectedCommands.getMinigames}).\n */\n getGamemode(this: OmeggaLike): Promise<IGamemode | null>;\n}\n\n/**\n * Event listener shape, mirroring node's EventEmitter. The rest parameter is\n * `any[]` for the same reason node's own is: it is both the type every listener\n * must be assignable to and the type an inline listener's parameters are\n * inferred from, so `unknown[]` would type a custom event's arguments as\n * `unknown` at every call site that does not hit a named overload.\n */\nexport type MockEventListener = (...args: any[]) => void;\n\nexport interface MockEventEmitter {\n addListener(event: string, listener: MockEventListener): this;\n emit(event: string, ...args: unknown[]): boolean;\n eventNames(): (string | symbol)[];\n getMaxListeners(): number;\n listenerCount(event: string): number;\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -- matches node's EventEmitter return type\n listeners(event: string): Function[];\n off(event: string, listener: MockEventListener): this;\n on(event: string, listener: MockEventListener): this;\n once(event: string, listener: MockEventListener): this;\n prependListener(event: string, listener: MockEventListener): this;\n prependOnceListener(event: string, listener: MockEventListener): this;\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -- matches node's EventEmitter return type\n rawListeners(event: string): Function[];\n removeAllListeners(event?: string): this;\n removeListener(event: string, listener: MockEventListener): this;\n setMaxListeners(maxListeners: number): this;\n\n on(event: 'close', listener: () => void): this;\n on(event: 'line', listener: (line: string) => void): this;\n on(event: 'start', listener: (info: { map: string }) => void): this;\n on(event: 'version', listener: (version: number) => void): this;\n on(event: 'unauthorized', listener: () => void): this;\n on(event: 'join', listener: (player: OmeggaPlayer) => void): this;\n on(event: 'leave', listener: (player: OmeggaPlayer) => void): this;\n on(event: 'chat', listener: (name: string, message: string) => void): this;\n on(event: 'mapchange', listener: (info: { map: string }) => void): this;\n on(event: 'autorestart', listener: (config: AutoRestartConfig) => void): this;\n on(\n event: 'interact',\n listener: (interaction: BrickInteraction) => void,\n ): this;\n on(\n event: 'minigamejoin',\n listener: (info: {\n player: { name: string; id: string };\n minigameName: string;\n }) => void,\n ): this;\n on(event: 'wirelog', listener: (raw: string) => void): this;\n on(event: `wirecmd:${string}`, listener: (...args: string[]) => void): this;\n on(\n event: 'wirecmd',\n listener: (cmd: string, ...args: string[]) => void,\n ): this;\n}\n\nexport interface OmeggaLike\n extends OmeggaCore,\n LogWrangling,\n InjectedCommands,\n MockEventEmitter {\n writeln(line: string): void;\n\n /** game CL version*/\n version: number;\n\n /**\n * version-resolved Brickadia console command names, nested by namespace.\n * e.g. `Omegga.Console.Bricks.Clear` resolves to the command string for the\n * running game version (\"Bricks.Clear\" or \"br.Bricks.Clear\")\n */\n Console: ConsoleCommands;\n\n /** verbose logging is enabled*/\n verbose: boolean;\n\n /** list of players */\n players: OmeggaPlayer[];\n\n /** server host */\n host?: { id: string; name: string };\n\n /** server is started */\n started: boolean;\n /** server is starting */\n starting: boolean;\n /** server is stopping */\n stopping: boolean;\n /** current map */\n currentMap: string;\n\n /** path to config files */\n configPath: string;\n /** path to saves */\n savePath: string;\n /** path to worlds */\n worldPath: string;\n /** path to prefabs */\n prefabPath: string;\n /** path to presets */\n presetPath: string;\n /** path to containing dir */\n path: string;\n /**\n * path to the directory containing the game server binary - null when the\n * install is launcher-managed and the binary location isn't known\n */\n binaryPath: string | null;\n\n /** get a plugin's name, documentation, and loaded status\n * If run in an unsafe plugin, the emitPlugin method sends events from\n * an \"unsafe\" plugin\n */\n getPlugin(name: string): Promise<PluginInterop | null>;\n}\n\nexport interface OmeggaCore {\n /**\n * get a list of players\n * @return list of players {id: uuid, name: name} objects\n */\n getPlayers(): {\n id: string;\n name: string;\n displayName: string;\n controller: string;\n state: string;\n }[];\n\n /**\n * find a player by name, id, controller, or state\n * @param target - name, id, controller, or state\n */\n getPlayer(target: string): OmeggaPlayer | null;\n\n /**\n * find a player by rough name, prioritize exact matches and get fuzzier\n * @param name player name, fuzzy\n */\n findPlayerByName(name: string): OmeggaPlayer | null;\n\n /**\n * get the host's ID\n * @return Host Id\n */\n getHostId(): string;\n\n /**\n * broadcast messages to chat\n * messages are broken by new line\n * multiple arguments are additional lines\n * all messages longer than 512 characters are deleted automatically, though omegga wouldn't have sent them anyway\n * @param messages unescaped chat messages to send. may need to wrap messages with quotes\n */\n broadcast(...messages: string[]): void;\n\n /**\n * whisper messages to a player's chat\n * messages are broken by new line\n * multiple arguments are additional lines\n * all messages longer than 512 characters are deleted automatically, though omegga wouldn't have sent them anyway\n * @param target - player identifier or player object\n * @param messages - unescaped chat messages to send. may need to wrap messages with quotes\n */\n whisper(target: string | OmeggaPlayer, ...messages: string[]): void;\n\n /**\n * prints text to the middle of a player's screen\n * all messages longer than 512 characters are deleted automatically\n * @param target - player identifier or player object\n * @param message - unescaped chat messages to send. may need to wrap messages with quotes\n */\n middlePrint(target: string | OmeggaPlayer, message: string): void;\n\n /**\n * Save a minigame preset based on a minigame index\n * @param index minigame index\n * @param name preset name\n */\n saveMinigame(index: number, name: string): void;\n\n /**\n * Delete a minigame\n * @param index minigame index\n */\n deleteMinigame(index: number): void;\n\n /**\n * Reset a minigame\n * @param index minigame index\n */\n resetMinigame(index: number): void;\n\n /**\n * Force the next round in a minigame\n * @param index minigame index\n */\n nextRoundMinigame(index: number): void;\n\n /**\n * Load an Minigame preset\n * @param presetName preset name\n * @param owner owner id/name\n */\n loadMinigame(presetName: string, owner?: string): void;\n\n /**\n * Get all presets in the minigame folder and child folders\n */\n getMinigamePresets(): string[];\n\n /**\n * Reset the environment settings\n */\n resetEnvironment(): void;\n\n /**\n * Save an environment preset\n * @param presetName preset name\n */\n saveEnvironment(presetName: string): Promise<void>;\n\n /** Save a temporary environment preset and return its contents */\n getEnvironmentData(): Promise<EnvironmentPreset | null>;\n\n /**\n * Read environment data as json; null when the preset is missing or invalid\n * @param presetName preset name\n */\n readEnvironmentData(presetName: string): EnvironmentPreset | null;\n\n /**\n * Load an environment preset\n * @param presetName preset name\n */\n loadEnvironment(presetName: string): void;\n\n /**\n * Load some environment preset data\n * @param preset preset data\n */\n loadEnvironmentData(\n preset:\n | EnvironmentPreset\n | NonNullable<EnvironmentPreset['data']>['groups'],\n ): void;\n\n /**\n * Get all presets in the environment folder and child folders\n */\n getEnvironmentPresets(): string[];\n\n /**\n * Clear a user's bricks (by uuid, name, controller, or player object)\n * @param target player or player identifier\n * @param quiet quietly clear bricks\n */\n clearBricks(target: string | { id: string }, quiet?: boolean): void;\n\n /**\n * Clear a region of bricks. On EA3 this routes to `br.World.ClearRegion`\n * and can optionally clear entities too.\n * @param region region to clear\n * @param options optional settings\n */\n clearRegion(\n region: {\n center: [number, number, number];\n extent: [number, number, number];\n },\n options?: {\n target?: string | OmeggaPlayer;\n /** clear bricks in the region (default true) */\n bricks?: boolean;\n /** also clear entities in the region (default false, EA3 only) */\n entities?: boolean;\n },\n ): void;\n\n /**\n * Clear all bricks on the server. On EA3 this routes to\n * `br.World.ClearAll` and can optionally clear entities too. A bare boolean\n * is accepted as the legacy `quiet` argument.\n * @param options quiet (or `{ quiet, bricks, entities }`)\n */\n clearAllBricks(\n options?:\n | boolean\n | { quiet?: boolean; bricks?: boolean; entities?: boolean },\n ): void;\n\n /**\n * Save bricks under a filename\n * @param saveName save file name\n * @param region region of bricks to save\n * @deprecated removed in Brickadia EA3 (no-op on newer servers) - save a\n * prefab with {@link savePrefabRegion} instead\n */\n saveBricks(\n saveName: string,\n region?: {\n center: [number, number, number];\n extent: [number, number, number];\n },\n ): void;\n\n /**\n * Save bricks under a filename, with a promise\n * @param saveName save file name\n * @param region region of bricks to save\n * @deprecated removed in Brickadia EA3 (no-op on newer servers) - save a\n * prefab with {@link savePrefabRegion} instead\n */\n saveBricksAsync(\n saveName: string,\n region?: {\n center: [number, number, number];\n extent: [number, number, number];\n },\n ): Promise<void>;\n\n /**\n * Load bricks on the server\n * @deprecated removed in Brickadia EA3 (no-op on newer servers) - load a\n * prefab with {@link loadPrefab} instead\n */\n loadBricks(\n saveName: string,\n options?: {\n offX?: number;\n offY?: number;\n offZ?: number;\n quiet?: boolean;\n correctPalette?: boolean;\n correctCustom?: boolean;\n },\n ): void;\n\n /**\n * Load bricks on the server into a player's clipbaord\n * @deprecated removed in Brickadia ~EA2 (no-op on newer servers) - use\n * {@link loadPrefabOnPlayer} instead\n */\n loadBricksOnPlayer(\n saveName: string,\n player: string | OmeggaPlayer,\n options?: {\n offX?: number;\n offY?: number;\n offZ?: number;\n correctPalette?: boolean;\n correctCustom?: boolean;\n },\n ): void;\n\n /**\n * Get all saves in the save folder and child folders\n */\n getSaves(): string[];\n\n /**\n * Checks if a save exists and returns an absolute path\n * @param saveName Save filename\n * @return Path to string, undefined if the save does not exist\n */\n getSavePath(saveName: string): string | undefined;\n\n /**\n * Get all worlds in the worlds folder and child folders\n */\n getWorlds(): string[];\n\n /**\n * Checks if a world exists and returns an absolute path\n * @param worldName World name\n * @return Path to string, undefined if the world does not exist\n */\n getWorldPath(worldName: string): string | undefined;\n\n /**\n * Get a list of revisions for a world\n * @param worldName World name\n */\n getWorldRevisions(\n worldName: string,\n ): Promise<{ index: number; date: Date; note: string }[]>;\n\n /**\n * Load a world by its name\n * @param worldName World name\n */\n loadWorld(worldName: string): Promise<boolean>;\n\n /**\n * Load a world at a specific revision\n * @param worldName World name\n */\n loadWorldRevision(worldName: string, revision: number): Promise<boolean>;\n\n /**\n * Save a world as a new name\n * @param worldName World name\n */\n saveWorldAs(worldName: string): Promise<boolean>;\n\n /**\n * Save the current world\n */\n saveWorld(): Promise<boolean>;\n\n /**\n * Create an empty world with the given name\n */\n createEmptyWorld(worldName: string): Promise<boolean>;\n\n /**\n * unsafely load save data (wrap in try/catch)\n * @param saveName save file name\n * @param saveData BRS JS Save data\n */\n writeSaveData(saveName: string, saveData: WriteSaveObject): void;\n\n /**\n * unsafely read save data (wrap in try/catch)\n * @param saveName save file name\n * @param nobricks only read save header data\n * @return BRS JS Save Data\n */\n readSaveData(saveName: string, nobricks?: boolean): ReadSaveObject;\n\n /**\n * load bricks from save data and resolve when game finishes loading\n * @param saveData BRS JS Save data\n * @deprecated removed in Brickadia EA3 (no-op on newer servers) - use the\n * prefab API ({@link loadPrefab}) instead\n */\n loadSaveData(\n saveData: WriteSaveObject,\n options?: {\n offX?: number;\n offY?: number;\n offZ?: number;\n quiet?: boolean;\n correctPalette?: boolean;\n correctCustom?: boolean;\n },\n ): Promise<void>;\n\n /**\n * load bricks from save data and resolve when game finishes loading\n * @param saveData BRS JS Save data\n * @param player Player name/id or player object\n * @deprecated removed in Brickadia ~EA2 (no-op on newer servers) - use the\n * prefab API ({@link loadPrefabOnPlayer}) instead\n */\n loadSaveDataOnPlayer(\n saveData: WriteSaveObject,\n player: string | OmeggaPlayer,\n options?: {\n offX?: number;\n offY?: number;\n offZ?: number;\n correctPalette?: boolean;\n correctCustom?: boolean;\n },\n ): Promise<void>;\n\n /**\n * get current bricks as save data\n * @deprecated removed in Brickadia EA3 (returns undefined on newer\n * servers) - use the prefab API instead\n */\n getSaveData(region?: {\n center: [number, number, number];\n extent: [number, number, number];\n }): Promise<ReadSaveObject | undefined>;\n\n /**\n * Get all prefabs in the prefabs folder and child folders (EA3)\n */\n getPrefabs(): string[];\n\n /**\n * Checks if a prefab exists and returns an absolute path (EA3)\n * @param prefabName Prefab filename\n * @return Path to string\n */\n getPrefabPath(prefabName: string): string | undefined;\n\n /**\n * Load a prefab into the world (EA3). `path` is a bundle path ref such\n * as `Prefabs/Uploads/<hash>.brz`.\n * @param path prefab bundle path ref\n * @param options placement options (offset, orientation, mirror axes, etc)\n */\n loadPrefab(\n path: string,\n options?: {\n offX?: number;\n offY?: number;\n offZ?: number;\n atOriginalPosition?: boolean;\n orientation?: number;\n rootEntityPersistentIndex?: number;\n /** bitmask: X=1 Y=2 Z=4 (e.g. 3 mirrors X and Y) */\n mirrorAxes?: number;\n overrideUserId?: string;\n },\n ): void;\n\n /**\n * Save the world (or a region of it) as a prefab (EA3).\n * @param path destination prefab bundle path ref (e.g. `Prefabs/MyPrefab.brz`)\n * @param options save options; omit `region` to capture the whole world\n */\n savePrefab(\n path: string,\n options?: {\n region?: {\n center: [number, number, number];\n extent: [number, number, number];\n };\n entities?: boolean;\n rootEntityPersistentIndex?: number;\n userId?: string;\n },\n ): void;\n\n /**\n * Save a prefab and resolve once the prefab file has been written to disk\n * (EA3).\n * @param path destination prefab bundle path ref\n * @param options same options as {@link savePrefab}\n * @returns absolute path to the written prefab, or null on timeout\n */\n savePrefabAsync(\n path: string,\n options?: {\n region?: {\n center: [number, number, number];\n extent: [number, number, number];\n };\n entities?: boolean;\n rootEntityPersistentIndex?: number;\n userId?: string;\n },\n ): Promise<string | null>;\n\n /**\n * Give a prefab to a player's inventory (EA3).\n * @param path prefab bundle path ref\n * @param player player name/id or player object\n * @param options give options (preserve ownership)\n */\n givePrefabToPlayer(\n path: string,\n player: string | OmeggaPlayer,\n options?: { preserveOwnership?: boolean },\n ): void;\n\n /**\n * Load a prefab onto a player (EA3, replaces {@link loadBricksOnPlayer}).\n * @param path prefab bundle path ref\n * @param player player name/id or player object\n * @param options give options (preserve ownership)\n */\n loadPrefabOnPlayer(\n path: string,\n player: string | OmeggaPlayer,\n options?: { preserveOwnership?: boolean },\n ): void;\n\n /**\n * Change server map\n * @param map Map name\n */\n changeMap(map: string): Promise<boolean>;\n\n /**\n * Get up-to-date role setup from RoleSetup.json\n */\n getRoleSetup(): BRRoleSetup;\n\n /**\n * Get up-to-date role assignments from RoleAssignment.json\n */\n getRoleAssignments(): BRRoleAssignments;\n\n /**\n * Get up-to-date ban list from BanList.json\n */\n getBanList(): BRBanList;\n\n /**\n * Get up-to-date name cache from PlayerNameCache.json\n */\n getNameCache(): BRPlayerNameCache;\n}\n\n/** A simple document store for plugins */\nexport interface PluginStore<\n Storage extends Record<string, unknown> = Record<string, unknown>,\n> {\n /** Get a value from plugin storage */\n get<T extends keyof Storage>(key: T): Promise<Storage[T]>;\n /** Set a value to plugin storage */\n set<T extends keyof Storage>(key: T, value: Storage[T]): Promise<void>;\n /** Delete a value from plugin storage */\n delete(key: string): Promise<void>;\n /** Wipe all values in plugin storage */\n wipe(): Promise<void>;\n /** Count entries in plugin storage */\n count(): Promise<number>;\n /** Get a list of keys in plugin storage */\n keys(): Promise<(keyof Storage)[]>;\n}\n\n/** A config representative of the config outlined in doc.json */\nexport type PluginConfig<\n T extends Record<string, unknown> = Record<string, unknown>,\n> = T;\n\n/** Label values attached to a plugin metric */\nexport type PluginMetricLabels = Record<string, string | number | boolean>;\n\n/** Options common to every plugin metric */\nexport interface PluginMetricOptions {\n /**\n * Metric name, exported as `omegga_plugin_<plugin>_<name>`. Must be\n * lowercase letters, digits, and underscores.\n */\n name: string;\n /** One-line description, shown in the scrape output */\n help?: string;\n /** Label names this metric may use (`plugin` is added automatically) */\n labels?: string[];\n}\n\n/** A value that only ever increases, such as a number of events */\nexport interface MetricCounter {\n /** Add to the count (default 1), optionally on a labelled series */\n inc(value?: number): void;\n inc(labels: PluginMetricLabels, value?: number): void;\n /** Discard every series */\n reset(): void;\n}\n\n/** A value that can go up and down, such as a queue length */\nexport interface MetricGauge {\n set(value: number): void;\n set(labels: PluginMetricLabels, value: number): void;\n inc(value?: number): void;\n inc(labels: PluginMetricLabels, value?: number): void;\n dec(value?: number): void;\n dec(labels: PluginMetricLabels, value?: number): void;\n /** Stop exporting one labelled series */\n remove(labels?: PluginMetricLabels): void;\n /**\n * Produce the value on demand instead of setting it. The callback runs when\n * the metric is collected, which is cheaper than setting it on a timer.\n */\n collect(fn: () => number | void): void;\n reset(): void;\n}\n\n/** Bucketed observations, such as durations */\nexport interface MetricHistogram {\n observe(value: number): void;\n observe(labels: PluginMetricLabels, value: number): void;\n /** Start a timer; call the returned function to observe elapsed seconds */\n startTimer(labels?: PluginMetricLabels): () => number;\n reset(): void;\n}\n\n/**\n * Prometheus metrics for a plugin, exported under `omegga_plugin_<plugin>_`.\n *\n * Handles are always returned, so no guards are needed: when the metrics\n * endpoint is disabled every call is a no-op.\n */\nexport interface PluginMetrics {\n /** Whether the metrics endpoint is enabled; handles work either way */\n readonly enabled: boolean;\n counter(opts: PluginMetricOptions): MetricCounter;\n gauge(opts: PluginMetricOptions): MetricGauge;\n histogram(\n opts: PluginMetricOptions & {\n /** Upper bounds in ascending order; `+Inf` is added automatically */\n buckets?: number[];\n },\n ): MetricHistogram;\n}\n\n/** An omegga plugin */\nexport default abstract class OmeggaPlugin<\n Config extends Record<string, unknown> = Record<string, unknown>,\n Storage extends Record<string, unknown> = Record<string, unknown>,\n> {\n omegga: OmeggaLike;\n config: PluginConfig<Config>;\n store: PluginStore<Storage>;\n /**\n * Prometheus metrics for this plugin; no-ops when metrics are disabled.\n *\n * Always supplied by the plugin loader. It is declared optional purely so\n * that plugins written before metrics existed still satisfy\n * `implements OmeggaPlugin`. Declare `metrics: PM` on your own class, as\n * the templates do for `omegga`, `config`, and `store`, to get the\n * non-optional type.\n */\n metrics?: PluginMetrics;\n\n constructor(\n omegga: OmeggaLike,\n config: PluginConfig<Config>,\n store: PluginStore<Storage>,\n metrics: PluginMetrics,\n ) {\n this.omegga = omegga;\n this.config = config;\n this.store = store;\n this.metrics = metrics;\n }\n\n /** Run when plugin starts, returns /commands it uses */\n abstract init(): Promise<void | { registeredCommands?: string[] }>;\n\n /** Run when plugin is stopped */\n abstract stop(): Promise<void>;\n\n /** Run when another plugin tries to interact with this plugin\n * @param event Event name\n * @param from Name of origin plugin\n * @return value other plugin expects\n */\n abstract pluginEvent?(\n event: string,\n from: string,\n ...args: unknown[]\n ): Promise<unknown>;\n}\n\nexport interface LogWrangling {\n /** Add a passive pattern on console output that invokes callback on match */\n addMatcher<T>(\n pattern: IMatcher<T>['pattern'],\n callback: IMatcher<T>['callback'],\n ): void;\n\n /**\n * Run an active pattern on console output that resolves a match.\n * T is the element type of the resolved matches - RegExpMatchArray for\n * RegExp patterns, the pattern's return type for function patterns.\n */\n addWatcher<T = RegExpMatchArray>(\n pattern: IWatcher<T>['pattern'],\n options?: {\n timeoutDelay?: number;\n bundle?: boolean;\n debounce?: boolean;\n afterMatchDelay?: number;\n last?: IWatcher<T>['last'];\n exec?: () => void;\n },\n ): Promise<T[]>;\n\n /**\n * Run a command and capture bundled output.\n * T is the element type of the resolved matches - RegExpMatchArray for\n * RegExp patterns, the pattern's return type for function patterns.\n */\n watchLogChunk<T = RegExpMatchArray>(\n cmd: string,\n pattern: IWatcher<T>['pattern'],\n options?: {\n first?: 'index' | ((match: T) => boolean);\n last?: IWatcher<T>['last'];\n afterMatchDelay?: number;\n timeoutDelay?: number;\n },\n ): Promise<T[]>;\n\n /** Run a command and capture bundled output for array functions */\n watchLogArray<\n Item extends Record<string, string> = Record<string, string>,\n Member extends Record<string, string> = Record<string, string>,\n >(\n cmd: string,\n itemPattern: RegExp,\n memberPattern: RegExp,\n ): Promise<{ item: Item; members: Member[] }[]>;\n}\n\nexport type WatcherPattern<T> = (\n line: string,\n match: RegExpMatchArray | null,\n) => T | RegExpMatchArray | null | undefined | '[OMEGGA_WATCHER_DONE]';\n\nexport type IMatcher<T> =\n | {\n pattern: RegExp;\n callback: (match: RegExpMatchArray) => boolean;\n }\n | {\n pattern: (line: string, match: RegExpMatchArray | null) => T;\n callback: (match: RegExpMatchArray) => T;\n };\n\nexport type IWatcher<T> = {\n bundle: boolean;\n debounce: boolean;\n timeoutDelay: number;\n afterMatchDelay: number;\n last: (match: T) => boolean;\n callback: () => void;\n resolve: (...args: T[]) => void;\n remove: () => void;\n done: () => void;\n timeout: ReturnType<typeof setTimeout>;\n} & (\n | {\n pattern: WatcherPattern<T>;\n matches: T[];\n }\n | {\n pattern: RegExp;\n matches: RegExpMatchArray[];\n }\n);\n\nexport declare type IPluginConfigDefinition = {\n description: string;\n} & (\n | {\n type: 'string' | 'password' | 'role';\n default: string;\n }\n | {\n type: 'boolean';\n default: boolean;\n }\n | {\n type: 'number';\n default: number;\n }\n | {\n type: 'enum';\n options: (string | number)[];\n default: string | number;\n }\n | {\n type: 'players';\n default: {\n id: string;\n name: string;\n };\n }\n | ({\n type: 'list';\n } & (\n | {\n itemType: 'string';\n default: string[];\n }\n | {\n itemType: 'number';\n default: number[];\n }\n | {\n itemType: 'enum';\n options: (string | number)[];\n default: string | number;\n }\n ))\n);\nexport interface IPluginCommandArgument {\n name: string;\n description: string;\n required?: boolean;\n}\nexport interface IPluginCommand {\n name: string;\n description: string;\n example?: string;\n args: IPluginCommandArgument[];\n}\nexport interface IPluginDocumentation {\n name: string;\n description: string;\n author: string;\n config: Record<string, IPluginConfigDefinition>;\n commands: IPluginCommand[];\n}\n\nexport interface PluginInterop {\n name: string;\n documentation: IPluginDocumentation | null;\n loaded: boolean;\n emitPlugin?(event: string, args: unknown[]): Promise<unknown>;\n}\n\nexport type WeaponClass =\n | 'Weapon_APCarbine'\n | 'Weapon_AntiMaterielRifle'\n | 'Weapon_ArmingSword'\n | 'Weapon_AssaultRifle'\n | 'Weapon_AutoShotgun'\n | 'Weapon_Battleaxe'\n | 'Weapon_Bazooka'\n | 'Weapon_BoltActionRifle'\n | 'Weapon_Bow'\n | 'Weapon_BoxPistol'\n | 'Weapon_BullpupRifle'\n | 'Weapon_BullpupSMG'\n | 'Weapon_ChargedLongsword'\n | 'Weapon_CrystalKalis'\n | 'Weapon_Derringer'\n | 'Weapon_Dynamite'\n | 'Weapon_FlintlockPistol'\n | 'Weapon_GrenadeLauncher'\n | 'Weapon_GuardPistol'\n | 'Weapon_Handaxe'\n | 'Weapon_HealthPotion'\n | 'Weapon_HeavyAssaultRifle'\n | 'Weapon_HeavySMG'\n | 'Weapon_HeroSword'\n | 'Weapon_HighPowerPistol'\n | 'Weapon_HoloBlade'\n | 'Weapon_HuntingShotgun'\n | 'Weapon_Ikakalaka'\n | 'Weapon_ImpactGrenade'\n | 'Weapon_ImpactGrenadeLauncher'\n | 'Weapon_ImpulseGrenade'\n | 'Weapon_Khopesh'\n | 'Weapon_Knife'\n | 'Weapon_LeverActionRifle'\n | 'Weapon_LightMachineGun'\n | 'Weapon_LongSword'\n | 'Weapon_MagnumPistol'\n | 'Weapon_MicroSMG'\n | 'Weapon_Minigun'\n | 'Weapon_Minigun'\n | 'Weapon_MissileLauncher'\n | 'Weapon_PDW'\n | 'Weapon_Pickaxe'\n | 'Weapon_PipeBomb'\n | 'Weapon_Pistol'\n | 'Weapon_PlasmaPistol'\n | 'Weapon_PlasmaSMG'\n | 'Weapon_PulseCarbine'\n | 'Weapon_PulseRifle'\n | 'Weapon_QuadLauncher'\n | 'Weapon_Revolver'\n | 'Weapon_RocketJumper'\n | 'Weapon_RocketLauncher'\n | 'Weapon_Sabre'\n | 'Weapon_SemiAutoRifle'\n | 'Weapon_ServiceRifle'\n | 'Weapon_Shotgun'\n | 'Weapon_SlugShotgun'\n | 'Weapon_Sniper'\n | 'Weapon_SodaCan'\n | 'Weapon_Spatha'\n | 'Weapon_SportingShotgun'\n | 'Weapon_StampedSMG'\n | 'Weapon_StandardSubmachineGun'\n | 'Weapon_StickGrenade'\n | 'Weapon_SubmachineGun'\n | 'Weapon_SuperShotgun'\n | 'Weapon_SuppressedAssaultRifle'\n | 'Weapon_SuppressedBullpupSMG'\n | 'Weapon_SuppressedGuardPistol'\n | 'Weapon_SuppressedHeavySMG'\n | 'Weapon_SuppressedMicroSMG'\n | 'Weapon_SuppressedPistol'\n | 'Weapon_SuppressedServiceRifle'\n | 'Weapon_TacticalSMG'\n | 'Weapon_TacticalShotgun'\n | 'Weapon_Tomahawk'\n | 'Weapon_TwinCannon'\n | 'Weapon_TypewriterSMG'\n | 'Weapon_Zweihander';\n"],"names":["util"],"mappings":";;;AAkBO,MAAM,uBAAuBA,MAAAA;AA0nCpC,MAA8B,aAG5B;AAAA,EAeA,YACE,QACA,QACA,OACA,SACA;AACA,SAAK,SAAS;AACd,SAAK,SAAS;AACd,SAAK,QAAQ;AACb,SAAK,UAAU;AAAA,EACjB;AAkBF;;;"}
|
|
1
|
+
{"version":3,"file":"plugin.js","sources":["../src/plugin.ts"],"sourcesContent":["import { type EnvironmentPreset } from '@brickadia/presets';\nimport {\n type BRBanList,\n type BRPlayerNameCache,\n type BRRoleAssignments,\n type BRRoleSetup,\n} from '@brickadia/types';\nimport { type ConsoleCommands } from '@omegga/commands';\nimport {\n type IGamemode,\n type ILogMinigame,\n type IMinigameList,\n type IPlayerPositions,\n type IServerStatus,\n} from '@omegga/types';\nimport util from '@util';\nimport { type ReadSaveObject, type WriteSaveObject } from 'brs-js';\n\nexport const _OMEGGA_UTILS_IMPORT = util;\n/* eslint-disable no-var -- `var` is required to declare globalThis properties */\ndeclare global {\n export var Omegga: OmeggaLike;\n export var Player: StaticPlayer;\n export var OMEGGA_UTIL: typeof _OMEGGA_UTILS_IMPORT;\n}\n/* eslint-enable no-var */\n\nexport * from '@brickadia/types';\nexport * from '@omegga/types';\nexport type { ConsoleCommands } from '@omegga/commands';\nexport { OmeggaPlugin };\n\nexport type OL = OmeggaLike;\nexport type OP = OmeggaPlugin;\nexport type PC<T extends Record<string, unknown> = Record<string, unknown>> =\n PluginConfig<T>;\nexport type PS<T extends Record<string, unknown> = Record<string, unknown>> =\n PluginStore<T>;\nexport type PM = PluginMetrics;\n\nexport interface BrickBounds {\n minBound: [number, number, number];\n maxBound: [number, number, number];\n center: [number, number, number];\n}\n\n/** Created when a player clicks on a brick with an interact component */\nexport interface BrickInteraction {\n /** Brick name from catalog (Turkey Body, 4x Cube) */\n brick_name: string;\n /** Brick asset name; null when the display name is not recognized */\n brick_asset: string | null;\n /** Brick size; null when the display name is not recognized */\n brick_size: [number, number, number] | null;\n /** Player information, id, name, controller, and pawn */\n player: { id: string; name: string; controller: string; pawn: string };\n /** Brick center position */\n position: [number, number, number];\n\n /** message sent from a brick click interaction */\n message: string;\n /** data parsed from the line (if it starts with json:) */\n data: null | number | string | boolean | Record<string, unknown>;\n /** True when there was a json payload */\n json: boolean;\n /** True when there was a parse error */\n error: boolean;\n}\n\n/** AutoRestart options */\nexport type AutoRestartConfig = {\n players: boolean;\n announcement: boolean;\n saveWorld: boolean;\n};\n\nexport interface OmeggaPlayer {\n /** player name */\n name: string;\n\n /** player display name */\n displayName: string;\n\n /** player uuid */\n id: string;\n\n /** player controller id */\n controller: string;\n\n /** player state id */\n state: string;\n\n /**\n * Returns omegga\n */\n getOmegga(): OmeggaLike;\n\n /**\n * Clone a player\n */\n clone(): OmeggaPlayer;\n\n /**\n * Get raw player info (to feed into a constructor)\n */\n raw(): [string, string, string, string, string];\n\n /**\n * True if the player is the host\n */\n isHost(): boolean;\n\n /**\n * Clear player's bricks\n * @param quiet clear bricks quietly\n */\n clearBricks(quiet?: boolean): void;\n\n /**\n * Get player's roles, if any\n */\n getRoles(): readonly string[];\n\n /**\n * Get player's permissions in a map like `{\"Bricks.ClearOwn\": true, ...}`\n * @return permissions map\n */\n getPermissions(): Record<string, boolean>;\n\n /**\n * Get player's name color\n * @return 6 character hex string\n */\n getNameColor(): string;\n\n /**\n * Get the player's pawn; null when the player has no pawn\n * @return pawn\n */\n getPawn(): Promise<string | null>;\n\n /**\n * Get player's position; null when the player has no pawn\n * @return [x, y, z] coordinates\n */\n getPosition(): Promise<[number, number, number] | null>;\n\n /**\n * Gets a user's ghost brick info (by uuid, name, controller, or player object)\n * @return ghost brick data\n */\n getGhostBrick(): Promise<\n | {\n targetGrid: string;\n location: number[];\n orientation: string;\n }\n | undefined\n >;\n\n /**\n * gets a user's paint tool properties\n */\n getPaint(): Promise<\n | {\n materialIndex: string;\n materialAlpha: string;\n material: string;\n color: number[];\n }\n | undefined\n >;\n\n /**\n * gets whether or not the player is crouching\n */\n isCrouched(pawn?: string): Promise<boolean>;\n\n /**\n * gets whether or not the player is dead\n */\n isDead(pawn?: string): Promise<boolean>;\n\n /**\n * Gets the bounds of the template in the user's clipboard (bounds of original selection box)\n * @return template bounds\n */\n getTemplateBounds(): Promise<BrickBounds | undefined>;\n\n /**\n * Get bricks inside template bounds\n * @return BRS JS Save Data\n */\n getTemplateBoundsData(): Promise<ReadSaveObject | undefined>;\n\n /**\n * Load bricks at ghost brick location\n * @param saveData save data to load\n */\n loadDataAtGhostBrick(\n saveData: WriteSaveObject,\n options?: {\n rotate?: boolean;\n offX?: number;\n offY?: number;\n offZ?: number;\n quiet?: boolean;\n },\n ): Promise<void>;\n\n /**\n * Load bricks on this player's clipboard\n * @param saveName Save to load\n */\n loadBricks(saveName: string): void;\n\n /**\n * Load bricks on this player's clipboard passing save data\n * @param saveData save data to load\n */\n loadSaveData(\n saveData: WriteSaveObject,\n options?: {\n rotate?: boolean;\n offX?: number;\n offY?: number;\n offZ?: number;\n quiet?: boolean;\n },\n ): Promise<void>;\n\n /**\n * Kills this player\n */\n kill(): void;\n\n /**\n * Damages a player\n * @param amount Amount to damage\n */\n damage(amount: number): void;\n\n /**\n * Heal this player\n * @param amount to heal\n */\n heal(amount: number): void;\n\n /**\n * Gives a player an item\n * @param item Item name (Weapon_Bow)\n */\n giveItem(item: WeaponClass): void;\n\n /**\n * Removes an item from a player's inventory\n * @param item Item name (Weapon_Bow)\n */\n takeItem(item: WeaponClass): void;\n\n /**\n * Changes a player's team\n * @param teamIndex Team index\n */\n setTeam(teamIndex: number): void;\n\n /**\n * Changes a player's minigame\n * @param index Minigame index\n */\n setMinigame(index: number): void;\n\n /**\n * Changes a player's score\n * @param minigameIndex minigame index\n * @param score Score\n */\n setScore(minigameIndex: number, score: number): void;\n\n /**\n * Fetch a player's score\n * @param minigameIndex minigame index\n */\n getScore(minigameIndex: number): Promise<number>;\n\n /**\n * Set leaderboard value\n * @param key leaderboard key (Score, Kills, Deaths, CorrectGuesses)\n * @param value leaderboard value\n */\n setLeaderboard(key: string, value: number): void;\n\n /**\n * Get leaderboard value\n * @param key leaderboard key (Score, Kills, Deaths, CorrectGuesses)\n * @return leaderboard value\n */\n getLeaderboard(key: string): Promise<number | null>;\n}\n\nexport interface StaticPlayer {\n /**\n * get a player's roles, if any\n * @param omegga omegga instance\n * @param id player uuid\n * @return list of roles\n */\n getRoles(omegga: OmeggaLike, id: string): readonly string[];\n\n /**\n * get a player's permissions in a map like `{\"Bricks.ClearOwn\": true, ...}`\n * @param omegga Omegga instance\n * @param id player uuid\n * @return permissions map\n */\n getPermissions(omegga: OmeggaLike, id: string): Record<string, boolean>;\n\n /**\n * Kills a player\n * @param omegga Omegga instance\n * @param target Player or player name/id\n */\n kill(omegga: OmeggaLike, target: string | OmeggaPlayer): void;\n\n /**\n * Damages a player\n * @param omegga Omegga instance\n * @param target Player or player name/id\n * @param amount Damage amount\n */\n damage(\n omegga: OmeggaLike,\n target: string | OmeggaPlayer,\n amount: number,\n ): void;\n\n /**\n * Heal a player\n * @param omegga Omegga instance\n * @param target Player or player name/id\n * @param amount Heal amount\n */\n heal(omegga: OmeggaLike, target: string | OmeggaPlayer, amount: number): void;\n\n /**\n * Gives a player an item\n * @param omegga Omegga instance\n * @param target Player or player name/id\n * @param item Item name (Weapon_Bow)\n */\n giveItem(\n omegga: OmeggaLike,\n target: string | OmeggaPlayer,\n item: WeaponClass,\n ): void;\n\n /**\n * Removes an item from a player's inventory\n * @param omegga Omegga instance\n * @param target Player or player name/id\n * @param item Item name (Weapon_Bow)\n */\n takeItem(\n omegga: OmeggaLike,\n target: string | OmeggaPlayer,\n item: WeaponClass,\n ): void;\n\n /**\n * Changes a player's team\n * @param omegga Omegga instance\n * @param target Player or player name/id\n * @param teamIndex Team name? index?\n */\n setTeam(\n omegga: OmeggaLike,\n target: string | OmeggaPlayer,\n teamIndex: number,\n ): void;\n\n /**\n * Changes a player's minigame\n * @param omegga Omegga instance\n * @param target Player or player name/id\n * @param index Minigame index\n */\n setMinigame(\n omegga: OmeggaLike,\n target: string | OmeggaPlayer,\n index: number,\n ): void;\n\n /**\n * Changes a player's score\n * @param omegga Omegga instance\n * @param target Player or player name/id\n * @param minigameIndex minigame index\n * @param score Score\n */\n setScore(\n omegga: OmeggaLike,\n target: string | OmeggaPlayer,\n minigameIndex: number,\n score: number,\n ): void;\n\n /**\n * Fetches a player's score\n * @param omegga Omegga instance\n * @param target Player or player name/id\n * @param minigameIndex minigame index\n */\n getScore(\n omegga: OmeggaLike,\n target: string | OmeggaPlayer,\n minigameIndex: number,\n ): Promise<number>;\n\n /**\n * Set leaderboard value\n * @param omegga Omegga instance\n * @param target Player or player name/id\n * @param key leaderboard key (Score, Kills, Deaths, CorrectGuesses)\n * @param value leaderboard value\n */\n setLeaderboard(\n omegga: OmeggaLike,\n target: string | OmeggaPlayer,\n key: string,\n value: number,\n ): void;\n\n /**\n * Get leaderboard value\n * @param omegga Omegga instance\n * @param target Player or player name/id\n * @param key leaderboard key (Score, Kills, Deaths, CorrectGuesses)\n * @return leaderboard value\n */\n getLeaderboard(\n omegga: OmeggaLike,\n target: string | OmeggaPlayer,\n key: string,\n ): Promise<number | null>;\n}\n\nexport interface InjectedCommands {\n /** Get server status */\n getServerStatus(this: OmeggaLike): Promise<IServerStatus | null>;\n /**\n * Get a list of minigames and their indices\n * @deprecated minigames were replaced by a single gamemode (~CL14000); on\n * modern servers this returns at most one entry with an empty `owner`.\n * Prefer {@link InjectedCommands.getGamemode}.\n */\n listMinigames(this: OmeggaLike): Promise<IMinigameList>;\n // /** Get all pawn data */\n // getAllPawnData(this: OmeggaLike, fields: PawnDataField[]): void;\n /** Get all player positions and pawns */\n getAllPlayerPositions(this: OmeggaLike): Promise<IPlayerPositions>;\n /** Get minigames and members (one entry per gamemode on modern servers) */\n getMinigames(this: OmeggaLike): Promise<ILogMinigame[]>;\n /**\n * Get the single gamemode and its teams/players (modern servers, >=CL14000).\n * Returns null on older servers (use {@link InjectedCommands.getMinigames}).\n */\n getGamemode(this: OmeggaLike): Promise<IGamemode | null>;\n}\n\n/**\n * Event listener shape, mirroring node's EventEmitter. The rest parameter is\n * `any[]` for the same reason node's own is: it is both the type every listener\n * must be assignable to and the type an inline listener's parameters are\n * inferred from, so `unknown[]` would type a custom event's arguments as\n * `unknown` at every call site that does not hit a named overload.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- see above\nexport type MockEventListener = (...args: any[]) => void;\n\nexport interface MockEventEmitter {\n addListener(event: string, listener: MockEventListener): this;\n emit(event: string, ...args: unknown[]): boolean;\n eventNames(): (string | symbol)[];\n getMaxListeners(): number;\n listenerCount(event: string): number;\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -- matches node's EventEmitter return type\n listeners(event: string): Function[];\n off(event: string, listener: MockEventListener): this;\n on(event: string, listener: MockEventListener): this;\n once(event: string, listener: MockEventListener): this;\n prependListener(event: string, listener: MockEventListener): this;\n prependOnceListener(event: string, listener: MockEventListener): this;\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -- matches node's EventEmitter return type\n rawListeners(event: string): Function[];\n removeAllListeners(event?: string): this;\n removeListener(event: string, listener: MockEventListener): this;\n setMaxListeners(maxListeners: number): this;\n\n on(event: 'close', listener: () => void): this;\n on(event: 'line', listener: (line: string) => void): this;\n on(event: 'start', listener: (info: { map: string }) => void): this;\n on(event: 'version', listener: (version: number) => void): this;\n on(event: 'unauthorized', listener: () => void): this;\n on(event: 'join', listener: (player: OmeggaPlayer) => void): this;\n on(event: 'leave', listener: (player: OmeggaPlayer) => void): this;\n on(event: 'chat', listener: (name: string, message: string) => void): this;\n on(event: 'mapchange', listener: (info: { map: string }) => void): this;\n on(event: 'autorestart', listener: (config: AutoRestartConfig) => void): this;\n on(\n event: 'interact',\n listener: (interaction: BrickInteraction) => void,\n ): this;\n on(\n event: 'minigamejoin',\n listener: (info: {\n player: { name: string; id: string };\n minigameName: string;\n }) => void,\n ): this;\n on(event: 'wirelog', listener: (raw: string) => void): this;\n on(event: `wirecmd:${string}`, listener: (...args: string[]) => void): this;\n on(\n event: 'wirecmd',\n listener: (cmd: string, ...args: string[]) => void,\n ): this;\n}\n\nexport interface OmeggaLike\n extends OmeggaCore,\n LogWrangling,\n InjectedCommands,\n MockEventEmitter {\n writeln(line: string): void;\n\n /** game CL version*/\n version: number;\n\n /**\n * version-resolved Brickadia console command names, nested by namespace.\n * e.g. `Omegga.Console.Bricks.Clear` resolves to the command string for the\n * running game version (\"Bricks.Clear\" or \"br.Bricks.Clear\")\n */\n Console: ConsoleCommands;\n\n /** verbose logging is enabled*/\n verbose: boolean;\n\n /** list of players */\n players: OmeggaPlayer[];\n\n /** server host */\n host?: { id: string; name: string };\n\n /** server is started */\n started: boolean;\n /** server is starting */\n starting: boolean;\n /** server is stopping */\n stopping: boolean;\n /** current map */\n currentMap: string;\n\n /** path to config files */\n configPath: string;\n /** path to saves */\n savePath: string;\n /** path to worlds */\n worldPath: string;\n /** path to prefabs */\n prefabPath: string;\n /** path to presets */\n presetPath: string;\n /** path to containing dir */\n path: string;\n /**\n * path to the directory containing the game server binary - null when the\n * install is launcher-managed and the binary location isn't known\n */\n binaryPath: string | null;\n\n /** get a plugin's name, documentation, and loaded status\n * If run in an unsafe plugin, the emitPlugin method sends events from\n * an \"unsafe\" plugin\n */\n getPlugin(name: string): Promise<PluginInterop | null>;\n}\n\nexport interface OmeggaCore {\n /**\n * get a list of players\n * @return list of players {id: uuid, name: name} objects\n */\n getPlayers(): {\n id: string;\n name: string;\n displayName: string;\n controller: string;\n state: string;\n }[];\n\n /**\n * find a player by name, id, controller, or state\n * @param target - name, id, controller, or state\n */\n getPlayer(target: string): OmeggaPlayer | null;\n\n /**\n * find a player by rough name, prioritize exact matches and get fuzzier\n * @param name player name, fuzzy\n */\n findPlayerByName(name: string): OmeggaPlayer | null;\n\n /**\n * get the host's ID\n * @return Host Id\n */\n getHostId(): string;\n\n /**\n * broadcast messages to chat\n * messages are broken by new line\n * multiple arguments are additional lines\n * all messages longer than 512 characters are deleted automatically, though omegga wouldn't have sent them anyway\n * @param messages unescaped chat messages to send. may need to wrap messages with quotes\n */\n broadcast(...messages: string[]): void;\n\n /**\n * whisper messages to a player's chat\n * messages are broken by new line\n * multiple arguments are additional lines\n * all messages longer than 512 characters are deleted automatically, though omegga wouldn't have sent them anyway\n * @param target - player identifier or player object\n * @param messages - unescaped chat messages to send. may need to wrap messages with quotes\n */\n whisper(target: string | OmeggaPlayer, ...messages: string[]): void;\n\n /**\n * prints text to the middle of a player's screen\n * all messages longer than 512 characters are deleted automatically\n * @param target - player identifier or player object\n * @param message - unescaped chat messages to send. may need to wrap messages with quotes\n */\n middlePrint(target: string | OmeggaPlayer, message: string): void;\n\n /**\n * Save a minigame preset based on a minigame index\n * @param index minigame index\n * @param name preset name\n */\n saveMinigame(index: number, name: string): void;\n\n /**\n * Delete a minigame\n * @param index minigame index\n */\n deleteMinigame(index: number): void;\n\n /**\n * Reset a minigame\n * @param index minigame index\n */\n resetMinigame(index: number): void;\n\n /**\n * Force the next round in a minigame\n * @param index minigame index\n */\n nextRoundMinigame(index: number): void;\n\n /**\n * Load an Minigame preset\n * @param presetName preset name\n * @param owner owner id/name\n */\n loadMinigame(presetName: string, owner?: string): void;\n\n /**\n * Get all presets in the minigame folder and child folders\n */\n getMinigamePresets(): string[];\n\n /**\n * Reset the environment settings\n */\n resetEnvironment(): void;\n\n /**\n * Save an environment preset\n * @param presetName preset name\n */\n saveEnvironment(presetName: string): Promise<void>;\n\n /** Save a temporary environment preset and return its contents */\n getEnvironmentData(): Promise<EnvironmentPreset | null>;\n\n /**\n * Read environment data as json; null when the preset is missing or invalid\n * @param presetName preset name\n */\n readEnvironmentData(presetName: string): EnvironmentPreset | null;\n\n /**\n * Load an environment preset\n * @param presetName preset name\n */\n loadEnvironment(presetName: string): void;\n\n /**\n * Load some environment preset data\n * @param preset preset data\n */\n loadEnvironmentData(\n preset:\n | EnvironmentPreset\n | NonNullable<EnvironmentPreset['data']>['groups'],\n ): void;\n\n /**\n * Get all presets in the environment folder and child folders\n */\n getEnvironmentPresets(): string[];\n\n /**\n * Clear a user's bricks (by uuid, name, controller, or player object)\n * @param target player or player identifier\n * @param quiet quietly clear bricks\n */\n clearBricks(target: string | { id: string }, quiet?: boolean): void;\n\n /**\n * Clear a region of bricks. On EA3 this routes to `br.World.ClearRegion`\n * and can optionally clear entities too.\n * @param region region to clear\n * @param options optional settings\n */\n clearRegion(\n region: {\n center: [number, number, number];\n extent: [number, number, number];\n },\n options?: {\n target?: string | OmeggaPlayer;\n /** clear bricks in the region (default true) */\n bricks?: boolean;\n /** also clear entities in the region (default false, EA3 only) */\n entities?: boolean;\n },\n ): void;\n\n /**\n * Clear all bricks on the server. On EA3 this routes to\n * `br.World.ClearAll` and can optionally clear entities too. A bare boolean\n * is accepted as the legacy `quiet` argument.\n * @param options quiet (or `{ quiet, bricks, entities }`)\n */\n clearAllBricks(\n options?:\n | boolean\n | { quiet?: boolean; bricks?: boolean; entities?: boolean },\n ): void;\n\n /**\n * Save bricks under a filename\n * @param saveName save file name\n * @param region region of bricks to save\n * @deprecated removed in Brickadia EA3 (no-op on newer servers) - save a\n * prefab with {@link savePrefabRegion} instead\n */\n saveBricks(\n saveName: string,\n region?: {\n center: [number, number, number];\n extent: [number, number, number];\n },\n ): void;\n\n /**\n * Save bricks under a filename, with a promise\n * @param saveName save file name\n * @param region region of bricks to save\n * @deprecated removed in Brickadia EA3 (no-op on newer servers) - save a\n * prefab with {@link savePrefabRegion} instead\n */\n saveBricksAsync(\n saveName: string,\n region?: {\n center: [number, number, number];\n extent: [number, number, number];\n },\n ): Promise<void>;\n\n /**\n * Load bricks on the server\n * @deprecated removed in Brickadia EA3 (no-op on newer servers) - load a\n * prefab with {@link loadPrefab} instead\n */\n loadBricks(\n saveName: string,\n options?: {\n offX?: number;\n offY?: number;\n offZ?: number;\n quiet?: boolean;\n correctPalette?: boolean;\n correctCustom?: boolean;\n },\n ): void;\n\n /**\n * Load bricks on the server into a player's clipbaord\n * @deprecated removed in Brickadia ~EA2 (no-op on newer servers) - use\n * {@link loadPrefabOnPlayer} instead\n */\n loadBricksOnPlayer(\n saveName: string,\n player: string | OmeggaPlayer,\n options?: {\n offX?: number;\n offY?: number;\n offZ?: number;\n correctPalette?: boolean;\n correctCustom?: boolean;\n },\n ): void;\n\n /**\n * Get all saves in the save folder and child folders\n */\n getSaves(): string[];\n\n /**\n * Checks if a save exists and returns an absolute path\n * @param saveName Save filename\n * @return Path to string, undefined if the save does not exist\n */\n getSavePath(saveName: string): string | undefined;\n\n /**\n * Get all worlds in the worlds folder and child folders\n */\n getWorlds(): string[];\n\n /**\n * Checks if a world exists and returns an absolute path\n * @param worldName World name\n * @return Path to string, undefined if the world does not exist\n */\n getWorldPath(worldName: string): string | undefined;\n\n /**\n * Get a list of revisions for a world\n * @param worldName World name\n */\n getWorldRevisions(\n worldName: string,\n ): Promise<{ index: number; date: Date; note: string }[]>;\n\n /**\n * Load a world by its name\n * @param worldName World name\n */\n loadWorld(worldName: string): Promise<boolean>;\n\n /**\n * Load a world at a specific revision\n * @param worldName World name\n */\n loadWorldRevision(worldName: string, revision: number): Promise<boolean>;\n\n /**\n * Save a world as a new name\n * @param worldName World name\n */\n saveWorldAs(worldName: string): Promise<boolean>;\n\n /**\n * Save the current world\n */\n saveWorld(): Promise<boolean>;\n\n /**\n * Create an empty world with the given name\n */\n createEmptyWorld(worldName: string): Promise<boolean>;\n\n /**\n * unsafely load save data (wrap in try/catch)\n * @param saveName save file name\n * @param saveData BRS JS Save data\n */\n writeSaveData(saveName: string, saveData: WriteSaveObject): void;\n\n /**\n * unsafely read save data (wrap in try/catch)\n * @param saveName save file name\n * @param nobricks only read save header data\n * @return BRS JS Save Data\n */\n readSaveData(saveName: string, nobricks?: boolean): ReadSaveObject;\n\n /**\n * load bricks from save data and resolve when game finishes loading\n * @param saveData BRS JS Save data\n * @deprecated removed in Brickadia EA3 (no-op on newer servers) - use the\n * prefab API ({@link loadPrefab}) instead\n */\n loadSaveData(\n saveData: WriteSaveObject,\n options?: {\n offX?: number;\n offY?: number;\n offZ?: number;\n quiet?: boolean;\n correctPalette?: boolean;\n correctCustom?: boolean;\n },\n ): Promise<void>;\n\n /**\n * load bricks from save data and resolve when game finishes loading\n * @param saveData BRS JS Save data\n * @param player Player name/id or player object\n * @deprecated removed in Brickadia ~EA2 (no-op on newer servers) - use the\n * prefab API ({@link loadPrefabOnPlayer}) instead\n */\n loadSaveDataOnPlayer(\n saveData: WriteSaveObject,\n player: string | OmeggaPlayer,\n options?: {\n offX?: number;\n offY?: number;\n offZ?: number;\n correctPalette?: boolean;\n correctCustom?: boolean;\n },\n ): Promise<void>;\n\n /**\n * get current bricks as save data\n * @deprecated removed in Brickadia EA3 (returns undefined on newer\n * servers) - use the prefab API instead\n */\n getSaveData(region?: {\n center: [number, number, number];\n extent: [number, number, number];\n }): Promise<ReadSaveObject | undefined>;\n\n /**\n * Get all prefabs in the prefabs folder and child folders (EA3)\n */\n getPrefabs(): string[];\n\n /**\n * Checks if a prefab exists and returns an absolute path (EA3)\n * @param prefabName Prefab filename\n * @return Path to string\n */\n getPrefabPath(prefabName: string): string | undefined;\n\n /**\n * Load a prefab into the world (EA3). `path` is a bundle path ref such\n * as `Prefabs/Uploads/<hash>.brz`.\n * @param path prefab bundle path ref\n * @param options placement options (offset, orientation, mirror axes, etc)\n */\n loadPrefab(\n path: string,\n options?: {\n offX?: number;\n offY?: number;\n offZ?: number;\n atOriginalPosition?: boolean;\n orientation?: number;\n rootEntityPersistentIndex?: number;\n /** bitmask: X=1 Y=2 Z=4 (e.g. 3 mirrors X and Y) */\n mirrorAxes?: number;\n overrideUserId?: string;\n },\n ): void;\n\n /**\n * Save the world (or a region of it) as a prefab (EA3).\n * @param path destination prefab bundle path ref (e.g. `Prefabs/MyPrefab.brz`)\n * @param options save options; omit `region` to capture the whole world\n */\n savePrefab(\n path: string,\n options?: {\n region?: {\n center: [number, number, number];\n extent: [number, number, number];\n };\n entities?: boolean;\n rootEntityPersistentIndex?: number;\n userId?: string;\n },\n ): void;\n\n /**\n * Save a prefab and resolve once the prefab file has been written to disk\n * (EA3).\n * @param path destination prefab bundle path ref\n * @param options same options as {@link savePrefab}\n * @returns absolute path to the written prefab, or null on timeout\n */\n savePrefabAsync(\n path: string,\n options?: {\n region?: {\n center: [number, number, number];\n extent: [number, number, number];\n };\n entities?: boolean;\n rootEntityPersistentIndex?: number;\n userId?: string;\n },\n ): Promise<string | null>;\n\n /**\n * Give a prefab to a player's inventory (EA3).\n * @param path prefab bundle path ref\n * @param player player name/id or player object\n * @param options give options (preserve ownership)\n */\n givePrefabToPlayer(\n path: string,\n player: string | OmeggaPlayer,\n options?: { preserveOwnership?: boolean },\n ): void;\n\n /**\n * Load a prefab onto a player (EA3, replaces {@link loadBricksOnPlayer}).\n * @param path prefab bundle path ref\n * @param player player name/id or player object\n * @param options give options (preserve ownership)\n */\n loadPrefabOnPlayer(\n path: string,\n player: string | OmeggaPlayer,\n options?: { preserveOwnership?: boolean },\n ): void;\n\n /**\n * Change server map\n * @param map Map name\n */\n changeMap(map: string): Promise<boolean>;\n\n /**\n * Get up-to-date role setup from RoleSetup.json\n */\n getRoleSetup(): BRRoleSetup;\n\n /**\n * Get up-to-date role assignments from RoleAssignment.json\n */\n getRoleAssignments(): BRRoleAssignments;\n\n /**\n * Get up-to-date ban list from BanList.json\n */\n getBanList(): BRBanList;\n\n /**\n * Get up-to-date name cache from PlayerNameCache.json\n */\n getNameCache(): BRPlayerNameCache;\n}\n\n/** A simple document store for plugins */\nexport interface PluginStore<\n Storage extends Record<string, unknown> = Record<string, unknown>,\n> {\n /** Get a value from plugin storage */\n get<T extends keyof Storage>(key: T): Promise<Storage[T]>;\n /** Set a value to plugin storage */\n set<T extends keyof Storage>(key: T, value: Storage[T]): Promise<void>;\n /** Delete a value from plugin storage */\n delete(key: string): Promise<void>;\n /** Wipe all values in plugin storage */\n wipe(): Promise<void>;\n /** Count entries in plugin storage */\n count(): Promise<number>;\n /** Get a list of keys in plugin storage */\n keys(): Promise<(keyof Storage)[]>;\n}\n\n/** A config representative of the config outlined in doc.json */\nexport type PluginConfig<\n T extends Record<string, unknown> = Record<string, unknown>,\n> = T;\n\n/** Label values attached to a plugin metric */\nexport type PluginMetricLabels = Record<string, string | number | boolean>;\n\n/** Options common to every plugin metric */\nexport interface PluginMetricOptions {\n /**\n * Metric name, exported as `omegga_plugin_<plugin>_<name>`. Must be\n * lowercase letters, digits, and underscores.\n */\n name: string;\n /** One-line description, shown in the scrape output */\n help?: string;\n /** Label names this metric may use (`plugin` is added automatically) */\n labels?: string[];\n}\n\n/** A value that only ever increases, such as a number of events */\nexport interface MetricCounter {\n /** Add to the count (default 1), optionally on a labelled series */\n inc(value?: number): void;\n inc(labels: PluginMetricLabels, value?: number): void;\n /** Discard every series */\n reset(): void;\n}\n\n/** A value that can go up and down, such as a queue length */\nexport interface MetricGauge {\n set(value: number): void;\n set(labels: PluginMetricLabels, value: number): void;\n inc(value?: number): void;\n inc(labels: PluginMetricLabels, value?: number): void;\n dec(value?: number): void;\n dec(labels: PluginMetricLabels, value?: number): void;\n /** Stop exporting one labelled series */\n remove(labels?: PluginMetricLabels): void;\n /**\n * Produce the value on demand instead of setting it. The callback runs when\n * the metric is collected, which is cheaper than setting it on a timer.\n */\n collect(fn: () => number | void): void;\n reset(): void;\n}\n\n/** Bucketed observations, such as durations */\nexport interface MetricHistogram {\n observe(value: number): void;\n observe(labels: PluginMetricLabels, value: number): void;\n /** Start a timer; call the returned function to observe elapsed seconds */\n startTimer(labels?: PluginMetricLabels): () => number;\n reset(): void;\n}\n\n/**\n * Prometheus metrics for a plugin, exported under `omegga_plugin_<plugin>_`.\n *\n * Handles are always returned, so no guards are needed: when the metrics\n * endpoint is disabled every call is a no-op.\n */\nexport interface PluginMetrics {\n /** Whether the metrics endpoint is enabled; handles work either way */\n readonly enabled: boolean;\n counter(opts: PluginMetricOptions): MetricCounter;\n gauge(opts: PluginMetricOptions): MetricGauge;\n histogram(\n opts: PluginMetricOptions & {\n /** Upper bounds in ascending order; `+Inf` is added automatically */\n buckets?: number[];\n },\n ): MetricHistogram;\n}\n\n/** An omegga plugin */\nexport default abstract class OmeggaPlugin<\n Config extends Record<string, unknown> = Record<string, unknown>,\n Storage extends Record<string, unknown> = Record<string, unknown>,\n> {\n omegga: OmeggaLike;\n config: PluginConfig<Config>;\n store: PluginStore<Storage>;\n /**\n * Prometheus metrics for this plugin; no-ops when metrics are disabled.\n *\n * Always supplied by the plugin loader. It is declared optional purely so\n * that plugins written before metrics existed still satisfy\n * `implements OmeggaPlugin`. Declare `metrics: PM` on your own class, as\n * the templates do for `omegga`, `config`, and `store`, to get the\n * non-optional type.\n */\n metrics?: PluginMetrics;\n\n constructor(\n omegga: OmeggaLike,\n config: PluginConfig<Config>,\n store: PluginStore<Storage>,\n metrics: PluginMetrics,\n ) {\n this.omegga = omegga;\n this.config = config;\n this.store = store;\n this.metrics = metrics;\n }\n\n /** Run when plugin starts, returns /commands it uses */\n abstract init(): Promise<void | { registeredCommands?: string[] }>;\n\n /** Run when plugin is stopped */\n abstract stop(): Promise<void>;\n\n /** Run when another plugin tries to interact with this plugin\n * @param event Event name\n * @param from Name of origin plugin\n * @return value other plugin expects\n */\n abstract pluginEvent?(\n event: string,\n from: string,\n ...args: unknown[]\n ): Promise<unknown>;\n}\n\nexport interface LogWrangling {\n /** Add a passive pattern on console output that invokes callback on match */\n addMatcher<T>(\n pattern: IMatcher<T>['pattern'],\n callback: IMatcher<T>['callback'],\n ): void;\n\n /**\n * Run an active pattern on console output that resolves a match.\n * T is the element type of the resolved matches - RegExpMatchArray for\n * RegExp patterns, the pattern's return type for function patterns.\n */\n addWatcher<T = RegExpMatchArray>(\n pattern: IWatcher<T>['pattern'],\n options?: {\n timeoutDelay?: number;\n bundle?: boolean;\n debounce?: boolean;\n afterMatchDelay?: number;\n last?: IWatcher<T>['last'];\n exec?: () => void;\n },\n ): Promise<T[]>;\n\n /**\n * Run a command and capture bundled output.\n * T is the element type of the resolved matches - RegExpMatchArray for\n * RegExp patterns, the pattern's return type for function patterns.\n */\n watchLogChunk<T = RegExpMatchArray>(\n cmd: string,\n pattern: IWatcher<T>['pattern'],\n options?: {\n first?: 'index' | ((match: T) => boolean);\n last?: IWatcher<T>['last'];\n afterMatchDelay?: number;\n timeoutDelay?: number;\n },\n ): Promise<T[]>;\n\n /** Run a command and capture bundled output for array functions */\n watchLogArray<\n Item extends Record<string, string> = Record<string, string>,\n Member extends Record<string, string> = Record<string, string>,\n >(\n cmd: string,\n itemPattern: RegExp,\n memberPattern: RegExp,\n ): Promise<{ item: Item; members: Member[] }[]>;\n}\n\nexport type WatcherPattern<T> = (\n line: string,\n match: RegExpMatchArray | null,\n) => T | RegExpMatchArray | null | undefined | '[OMEGGA_WATCHER_DONE]';\n\nexport type IMatcher<T> =\n | {\n pattern: RegExp;\n callback: (match: RegExpMatchArray) => boolean;\n }\n | {\n pattern: (line: string, match: RegExpMatchArray | null) => T;\n callback: (match: RegExpMatchArray) => T;\n };\n\nexport type IWatcher<T> = {\n bundle: boolean;\n debounce: boolean;\n timeoutDelay: number;\n afterMatchDelay: number;\n last: (match: T) => boolean;\n callback: () => void;\n resolve: (...args: T[]) => void;\n remove: () => void;\n done: () => void;\n timeout: ReturnType<typeof setTimeout>;\n} & (\n | {\n pattern: WatcherPattern<T>;\n matches: T[];\n }\n | {\n pattern: RegExp;\n matches: RegExpMatchArray[];\n }\n);\n\nexport declare type IPluginConfigDefinition = {\n description: string;\n} & (\n | {\n type: 'string' | 'password' | 'role';\n default: string;\n }\n | {\n type: 'boolean';\n default: boolean;\n }\n | {\n type: 'number';\n default: number;\n }\n | {\n type: 'enum';\n options: (string | number)[];\n default: string | number;\n }\n | {\n type: 'players';\n default: {\n id: string;\n name: string;\n };\n }\n | ({\n type: 'list';\n } & (\n | {\n itemType: 'string';\n default: string[];\n }\n | {\n itemType: 'number';\n default: number[];\n }\n | {\n itemType: 'enum';\n options: (string | number)[];\n default: string | number;\n }\n ))\n);\nexport interface IPluginCommandArgument {\n name: string;\n description: string;\n required?: boolean;\n}\nexport interface IPluginCommand {\n name: string;\n description: string;\n example?: string;\n args: IPluginCommandArgument[];\n}\nexport interface IPluginDocumentation {\n name: string;\n description: string;\n author: string;\n config: Record<string, IPluginConfigDefinition>;\n commands: IPluginCommand[];\n}\n\nexport interface PluginInterop {\n name: string;\n documentation: IPluginDocumentation | null;\n loaded: boolean;\n emitPlugin?(event: string, args: unknown[]): Promise<unknown>;\n}\n\nexport type WeaponClass =\n | 'Weapon_APCarbine'\n | 'Weapon_AntiMaterielRifle'\n | 'Weapon_ArmingSword'\n | 'Weapon_AssaultRifle'\n | 'Weapon_AutoShotgun'\n | 'Weapon_Battleaxe'\n | 'Weapon_Bazooka'\n | 'Weapon_BoltActionRifle'\n | 'Weapon_Bow'\n | 'Weapon_BoxPistol'\n | 'Weapon_BullpupRifle'\n | 'Weapon_BullpupSMG'\n | 'Weapon_ChargedLongsword'\n | 'Weapon_CrystalKalis'\n | 'Weapon_Derringer'\n | 'Weapon_Dynamite'\n | 'Weapon_FlintlockPistol'\n | 'Weapon_GrenadeLauncher'\n | 'Weapon_GuardPistol'\n | 'Weapon_Handaxe'\n | 'Weapon_HealthPotion'\n | 'Weapon_HeavyAssaultRifle'\n | 'Weapon_HeavySMG'\n | 'Weapon_HeroSword'\n | 'Weapon_HighPowerPistol'\n | 'Weapon_HoloBlade'\n | 'Weapon_HuntingShotgun'\n | 'Weapon_Ikakalaka'\n | 'Weapon_ImpactGrenade'\n | 'Weapon_ImpactGrenadeLauncher'\n | 'Weapon_ImpulseGrenade'\n | 'Weapon_Khopesh'\n | 'Weapon_Knife'\n | 'Weapon_LeverActionRifle'\n | 'Weapon_LightMachineGun'\n | 'Weapon_LongSword'\n | 'Weapon_MagnumPistol'\n | 'Weapon_MicroSMG'\n | 'Weapon_Minigun'\n | 'Weapon_Minigun'\n | 'Weapon_MissileLauncher'\n | 'Weapon_PDW'\n | 'Weapon_Pickaxe'\n | 'Weapon_PipeBomb'\n | 'Weapon_Pistol'\n | 'Weapon_PlasmaPistol'\n | 'Weapon_PlasmaSMG'\n | 'Weapon_PulseCarbine'\n | 'Weapon_PulseRifle'\n | 'Weapon_QuadLauncher'\n | 'Weapon_Revolver'\n | 'Weapon_RocketJumper'\n | 'Weapon_RocketLauncher'\n | 'Weapon_Sabre'\n | 'Weapon_SemiAutoRifle'\n | 'Weapon_ServiceRifle'\n | 'Weapon_Shotgun'\n | 'Weapon_SlugShotgun'\n | 'Weapon_Sniper'\n | 'Weapon_SodaCan'\n | 'Weapon_Spatha'\n | 'Weapon_SportingShotgun'\n | 'Weapon_StampedSMG'\n | 'Weapon_StandardSubmachineGun'\n | 'Weapon_StickGrenade'\n | 'Weapon_SubmachineGun'\n | 'Weapon_SuperShotgun'\n | 'Weapon_SuppressedAssaultRifle'\n | 'Weapon_SuppressedBullpupSMG'\n | 'Weapon_SuppressedGuardPistol'\n | 'Weapon_SuppressedHeavySMG'\n | 'Weapon_SuppressedMicroSMG'\n | 'Weapon_SuppressedPistol'\n | 'Weapon_SuppressedServiceRifle'\n | 'Weapon_TacticalSMG'\n | 'Weapon_TacticalShotgun'\n | 'Weapon_Tomahawk'\n | 'Weapon_TwinCannon'\n | 'Weapon_TypewriterSMG'\n | 'Weapon_Zweihander';\n"],"names":["util"],"mappings":";;;AAkBO,MAAM,uBAAuBA,MAAAA;AA2nCpC,MAA8B,aAG5B;AAAA,EAeA,YACE,QACA,QACA,OACA,SACA;AACA,SAAK,SAAS;AACd,SAAK,SAAS;AACd,SAAK,QAAQ;AACb,SAAK,UAAU;AAAA,EACjB;AAkBF;;;"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const ACTIONS = ["msg", "server", "leave", "join", "crash"];
|
|
4
|
+
const FILTER_KEYS = ["from", "role", "admin", "before", "after", "action"];
|
|
5
|
+
const tokenize = (input) => {
|
|
6
|
+
const tokens = [];
|
|
7
|
+
let current = "";
|
|
8
|
+
let quoted = false;
|
|
9
|
+
for (const char of input) {
|
|
10
|
+
if (char === '"') {
|
|
11
|
+
quoted = !quoted;
|
|
12
|
+
} else if (!quoted && /\s/.test(char)) {
|
|
13
|
+
if (current) tokens.push(current);
|
|
14
|
+
current = "";
|
|
15
|
+
} else {
|
|
16
|
+
current += char;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (current) tokens.push(current);
|
|
20
|
+
return tokens;
|
|
21
|
+
};
|
|
22
|
+
const parseQueryDate = (value) => {
|
|
23
|
+
const ymd = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
24
|
+
if (ymd) {
|
|
25
|
+
const [, year, month, day] = ymd;
|
|
26
|
+
const date = new Date(Number(year), Number(month) - 1, Number(day));
|
|
27
|
+
return Number.isNaN(date.getTime()) ? void 0 : date.getTime();
|
|
28
|
+
}
|
|
29
|
+
if (/^\d+$/.test(value)) return Number(value);
|
|
30
|
+
const parsed = new Date(value);
|
|
31
|
+
return Number.isNaN(parsed.getTime()) ? void 0 : parsed.getTime();
|
|
32
|
+
};
|
|
33
|
+
const parseChatQuery = (input) => {
|
|
34
|
+
const parsed = {
|
|
35
|
+
terms: [],
|
|
36
|
+
from: [],
|
|
37
|
+
roles: [],
|
|
38
|
+
admin: [],
|
|
39
|
+
actions: [],
|
|
40
|
+
filters: []
|
|
41
|
+
};
|
|
42
|
+
for (const token of tokenize(input)) {
|
|
43
|
+
const colon = token.indexOf(":");
|
|
44
|
+
const key = colon > 0 ? token.slice(0, colon).toLowerCase() : "";
|
|
45
|
+
const value = colon > 0 ? token.slice(colon + 1) : "";
|
|
46
|
+
if (!value) {
|
|
47
|
+
if (key === "admin" && !parsed.adminAny) {
|
|
48
|
+
parsed.adminAny = true;
|
|
49
|
+
parsed.filters.push({ key: "admin", value: "" });
|
|
50
|
+
} else if (token && !FILTER_KEYS.includes(key)) parsed.terms.push(token);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (key === "from") {
|
|
54
|
+
parsed.from.push(value);
|
|
55
|
+
parsed.filters.push({ key: "from", value });
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (key === "role") {
|
|
59
|
+
parsed.roles.push(value);
|
|
60
|
+
parsed.filters.push({ key: "role", value });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (key === "before" || key === "after") {
|
|
64
|
+
const date = parseQueryDate(value);
|
|
65
|
+
if (date === void 0) {
|
|
66
|
+
parsed.terms.push(token);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
parsed[key] = date;
|
|
70
|
+
parsed.filters.push({ key, value });
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (key === "admin") {
|
|
74
|
+
parsed.admin.push(value);
|
|
75
|
+
parsed.filters.push({ key: "admin", value });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (key === "action") {
|
|
79
|
+
const action = value.toLowerCase();
|
|
80
|
+
if (!ACTIONS.includes(action)) {
|
|
81
|
+
parsed.terms.push(token);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!parsed.actions.includes(action)) {
|
|
85
|
+
parsed.actions.push(action);
|
|
86
|
+
parsed.filters.push({ key: "action", value: action });
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
parsed.terms.push(token);
|
|
91
|
+
}
|
|
92
|
+
return parsed;
|
|
93
|
+
};
|
|
94
|
+
const isEmptyChatQuery = (q) => q.terms.length === 0 && q.from.length === 0 && q.roles.length === 0 && q.before === void 0 && q.after === void 0 && q.actions.length === 0 && q.admin.length === 0 && !q.adminAny;
|
|
95
|
+
exports.isEmptyChatQuery = isEmptyChatQuery;
|
|
96
|
+
exports.parseChatQuery = parseChatQuery;
|
|
97
|
+
exports.parseQueryDate = parseQueryDate;
|
|
98
|
+
//# sourceMappingURL=chatQuery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chatQuery.js","sources":["../../../src/webserver/backend/chatQuery.ts"],"sourcesContent":["/**\n * Parser for the chat history search box, in the shape discord and slack use:\n * `from:someone before:2026-01-01 words to look for`.\n *\n * Lives under the backend but is imported by the web UI too, so it must stay\n * free of node builtins and of anything that pulls in the database.\n */\n\nexport type ChatSearchAction = 'msg' | 'server' | 'leave' | 'join' | 'crash';\n\nconst ACTIONS: ChatSearchAction[] = ['msg', 'server', 'leave', 'join', 'crash'];\n\nconst FILTER_KEYS = ['from', 'role', 'admin', 'before', 'after', 'action'];\n\n/** A `key:value` pair the parser recognised, kept with the text that produced it. */\nexport interface ChatQueryFilter {\n key: 'from' | 'role' | 'admin' | 'before' | 'after' | 'action';\n /** the raw text after the colon, for echoing the term back in the UI */\n value: string;\n}\n\nexport interface ParsedChatQuery {\n /** words to match against the message body, with quotes stripped */\n terms: string[];\n /** names or ids to resolve to senders; empty means every sender */\n from: string[];\n /** server role names whose holders to limit the search to */\n roles: string[];\n /** epoch ms, exclusive upper bound */\n before?: number;\n /** epoch ms, exclusive lower bound */\n after?: number;\n /** kinds of entry to include; empty means the default of messages only */\n actions: ChatSearchAction[];\n /**\n * Names of omegga web users whose messages to keep, with `server` standing\n * for the server console. Empty means no such filter.\n */\n admin: string[];\n /** `admin:` with no name: anything omegga sent, whoever sent it. */\n adminAny?: boolean;\n /** every recognised filter in the order typed, for rendering chips */\n filters: ChatQueryFilter[];\n}\n\n/**\n * Splits on whitespace, but keeps `\"quoted phrases\"` together, including when\n * they follow a filter (`from:\"two words\"`). An unterminated quote runs to the\n * end of the input so the search still fires while the user is mid-word.\n */\nconst tokenize = (input: string): string[] => {\n const tokens: string[] = [];\n let current = '';\n let quoted = false;\n for (const char of input) {\n if (char === '\"') {\n quoted = !quoted;\n } else if (!quoted && /\\s/.test(char)) {\n if (current) tokens.push(current);\n current = '';\n } else {\n current += char;\n }\n }\n if (current) tokens.push(current);\n return tokens;\n};\n\n/**\n * `YYYY-MM-DD` is read in the viewer's own timezone rather than UTC, so\n * `before:2026-01-01` means midnight as the person typing it experiences it.\n * Anything else falls back to Date's own parsing; a bare epoch is accepted too.\n */\nexport const parseQueryDate = (value: string): number | undefined => {\n const ymd = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(value);\n if (ymd) {\n const [, year, month, day] = ymd;\n const date = new Date(Number(year), Number(month) - 1, Number(day));\n return Number.isNaN(date.getTime()) ? undefined : date.getTime();\n }\n if (/^\\d+$/.test(value)) return Number(value);\n const parsed = new Date(value);\n return Number.isNaN(parsed.getTime()) ? undefined : parsed.getTime();\n};\n\n/**\n * Reads a search box into filters and search terms. A `key:value` whose value\n * makes no sense (`before:soon`) stays a search term rather than erroring, so\n * pasting a url or a timestamp into the box never turns into a failed search.\n */\nexport const parseChatQuery = (input: string): ParsedChatQuery => {\n const parsed: ParsedChatQuery = {\n terms: [],\n from: [],\n roles: [],\n admin: [],\n actions: [],\n filters: [],\n };\n\n for (const token of tokenize(input)) {\n const colon = token.indexOf(':');\n const key = colon > 0 ? token.slice(0, colon).toLowerCase() : '';\n const value = colon > 0 ? token.slice(colon + 1) : '';\n\n if (!value) {\n // `admin:` on its own means everything omegga sent. every other bare\n // filter is one the user is still typing: picking a filter from the\n // dropdown inserts exactly that\n if (key === 'admin' && !parsed.adminAny) {\n parsed.adminAny = true;\n parsed.filters.push({ key: 'admin', value: '' });\n } else if (token && !FILTER_KEYS.includes(key)) parsed.terms.push(token);\n continue;\n }\n\n if (key === 'from') {\n parsed.from.push(value);\n parsed.filters.push({ key: 'from', value });\n continue;\n }\n\n if (key === 'role') {\n parsed.roles.push(value);\n parsed.filters.push({ key: 'role', value });\n continue;\n }\n\n if (key === 'before' || key === 'after') {\n const date = parseQueryDate(value);\n if (date === undefined) {\n parsed.terms.push(token);\n continue;\n }\n parsed[key] = date;\n parsed.filters.push({ key, value });\n continue;\n }\n\n if (key === 'admin') {\n parsed.admin.push(value);\n parsed.filters.push({ key: 'admin', value });\n continue;\n }\n\n if (key === 'action') {\n const action = value.toLowerCase() as ChatSearchAction;\n if (!ACTIONS.includes(action)) {\n parsed.terms.push(token);\n continue;\n }\n // repeating a filter widens the search rather than replacing it, so\n // `action:join action:leave` finds both\n if (!parsed.actions.includes(action)) {\n parsed.actions.push(action);\n parsed.filters.push({ key: 'action', value: action });\n }\n continue;\n }\n\n parsed.terms.push(token);\n }\n\n return parsed;\n};\n\n/** Wraps a value in quotes when it would otherwise tokenize as two terms. */\nexport const quoteChatValue = (value: string) =>\n /\\s/.test(value) ? `\"${value}\"` : value;\n\n/** Renders a filter back into the text the parser would read it from. */\nexport const formatChatFilter = (filter: ChatQueryFilter) =>\n `${filter.key}:${quoteChatValue(filter.value)}`;\n\n/** Whether a parsed query would narrow anything down at all. */\nexport const isEmptyChatQuery = (q: ParsedChatQuery) =>\n q.terms.length === 0 &&\n q.from.length === 0 &&\n q.roles.length === 0 &&\n q.before === undefined &&\n q.after === undefined &&\n q.actions.length === 0 &&\n q.admin.length === 0 &&\n !q.adminAny;\n"],"names":[],"mappings":";;AAUA,MAAM,UAA8B,CAAC,OAAO,UAAU,SAAS,QAAQ,OAAO;AAE9E,MAAM,cAAc,CAAC,QAAQ,QAAQ,SAAS,UAAU,SAAS,QAAQ;AAsCzE,MAAM,WAAW,CAAC,UAA4B;AAC5C,QAAM,SAAmB,CAAA;AACzB,MAAI,UAAU;AACd,MAAI,SAAS;AACb,aAAW,QAAQ,OAAO;AACxB,QAAI,SAAS,KAAK;AAChB,eAAS,CAAC;AAAA,IACZ,WAAW,CAAC,UAAU,KAAK,KAAK,IAAI,GAAG;AACrC,UAAI,QAAS,QAAO,KAAK,OAAO;AAChC,gBAAU;AAAA,IACZ,OAAO;AACL,iBAAW;AAAA,IACb;AAAA,EACF;AACA,MAAI,QAAS,QAAO,KAAK,OAAO;AAChC,SAAO;AACT;AAOO,MAAM,iBAAiB,CAAC,UAAsC;AACnE,QAAM,MAAM,4BAA4B,KAAK,KAAK;AAClD,MAAI,KAAK;AACP,UAAM,GAAG,MAAM,OAAO,GAAG,IAAI;AAC7B,UAAM,OAAO,IAAI,KAAK,OAAO,IAAI,GAAG,OAAO,KAAK,IAAI,GAAG,OAAO,GAAG,CAAC;AAClE,WAAO,OAAO,MAAM,KAAK,QAAA,CAAS,IAAI,SAAY,KAAK,QAAA;AAAA,EACzD;AACA,MAAI,QAAQ,KAAK,KAAK,EAAG,QAAO,OAAO,KAAK;AAC5C,QAAM,SAAS,IAAI,KAAK,KAAK;AAC7B,SAAO,OAAO,MAAM,OAAO,QAAA,CAAS,IAAI,SAAY,OAAO,QAAA;AAC7D;AAOO,MAAM,iBAAiB,CAAC,UAAmC;AAChE,QAAM,SAA0B;AAAA,IAC9B,OAAO,CAAA;AAAA,IACP,MAAM,CAAA;AAAA,IACN,OAAO,CAAA;AAAA,IACP,OAAO,CAAA;AAAA,IACP,SAAS,CAAA;AAAA,IACT,SAAS,CAAA;AAAA,EAAC;AAGZ,aAAW,SAAS,SAAS,KAAK,GAAG;AACnC,UAAM,QAAQ,MAAM,QAAQ,GAAG;AAC/B,UAAM,MAAM,QAAQ,IAAI,MAAM,MAAM,GAAG,KAAK,EAAE,YAAA,IAAgB;AAC9D,UAAM,QAAQ,QAAQ,IAAI,MAAM,MAAM,QAAQ,CAAC,IAAI;AAEnD,QAAI,CAAC,OAAO;AAIV,UAAI,QAAQ,WAAW,CAAC,OAAO,UAAU;AACvC,eAAO,WAAW;AAClB,eAAO,QAAQ,KAAK,EAAE,KAAK,SAAS,OAAO,IAAI;AAAA,MACjD,WAAW,SAAS,CAAC,YAAY,SAAS,GAAG,EAAG,QAAO,MAAM,KAAK,KAAK;AACvE;AAAA,IACF;AAEA,QAAI,QAAQ,QAAQ;AAClB,aAAO,KAAK,KAAK,KAAK;AACtB,aAAO,QAAQ,KAAK,EAAE,KAAK,QAAQ,OAAO;AAC1C;AAAA,IACF;AAEA,QAAI,QAAQ,QAAQ;AAClB,aAAO,MAAM,KAAK,KAAK;AACvB,aAAO,QAAQ,KAAK,EAAE,KAAK,QAAQ,OAAO;AAC1C;AAAA,IACF;AAEA,QAAI,QAAQ,YAAY,QAAQ,SAAS;AACvC,YAAM,OAAO,eAAe,KAAK;AACjC,UAAI,SAAS,QAAW;AACtB,eAAO,MAAM,KAAK,KAAK;AACvB;AAAA,MACF;AACA,aAAO,GAAG,IAAI;AACd,aAAO,QAAQ,KAAK,EAAE,KAAK,OAAO;AAClC;AAAA,IACF;AAEA,QAAI,QAAQ,SAAS;AACnB,aAAO,MAAM,KAAK,KAAK;AACvB,aAAO,QAAQ,KAAK,EAAE,KAAK,SAAS,OAAO;AAC3C;AAAA,IACF;AAEA,QAAI,QAAQ,UAAU;AACpB,YAAM,SAAS,MAAM,YAAA;AACrB,UAAI,CAAC,QAAQ,SAAS,MAAM,GAAG;AAC7B,eAAO,MAAM,KAAK,KAAK;AACvB;AAAA,MACF;AAGA,UAAI,CAAC,OAAO,QAAQ,SAAS,MAAM,GAAG;AACpC,eAAO,QAAQ,KAAK,MAAM;AAC1B,eAAO,QAAQ,KAAK,EAAE,KAAK,UAAU,OAAO,QAAQ;AAAA,MACtD;AACA;AAAA,IACF;AAEA,WAAO,MAAM,KAAK,KAAK;AAAA,EACzB;AAEA,SAAO;AACT;AAWO,MAAM,mBAAmB,CAAC,MAC/B,EAAE,MAAM,WAAW,KACnB,EAAE,KAAK,WAAW,KAClB,EAAE,MAAM,WAAW,KACnB,EAAE,WAAW,UACb,EAAE,UAAU,UACZ,EAAE,QAAQ,WAAW,KACrB,EAAE,MAAM,WAAW,KACnB,CAAC,EAAE;;;;"}
|
|
@@ -3,6 +3,7 @@ Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toString
|
|
|
3
3
|
const schema = require("../../db/schema.js");
|
|
4
4
|
const pattern = require("../../util/pattern.js");
|
|
5
5
|
const time = require("../../util/time.js");
|
|
6
|
+
const uuid = require("../../util/uuid.js");
|
|
6
7
|
const bcrypt = require("bcryptjs");
|
|
7
8
|
const chokidar = require("chokidar");
|
|
8
9
|
const drizzleOrm = require("drizzle-orm");
|
|
@@ -10,11 +11,17 @@ const EventEmitter = require("events");
|
|
|
10
11
|
const crypto = require("node:crypto");
|
|
11
12
|
const path = require("path");
|
|
12
13
|
const calendar = require("./calendar.js");
|
|
14
|
+
const chatQuery = require("./chatQuery.js");
|
|
13
15
|
const events = require("./events.js");
|
|
14
16
|
const permissions = require("./permissions.js");
|
|
15
17
|
const createPunchcard = () => Array.from({ length: 7 }).map(
|
|
16
18
|
() => Array.from({ length: 24 }).fill(0)
|
|
17
19
|
);
|
|
20
|
+
const SEARCH_COUNT_CAP = 1e3;
|
|
21
|
+
const LEGACY_CRASH_MESSAGES = ["Server error", "Server crashed, restarting..."];
|
|
22
|
+
const escapeLike = (value) => value.replace(/[\\%_]/g, (c) => "\\" + c);
|
|
23
|
+
const likePrefix = (value) => `${escapeLike(value)}%`;
|
|
24
|
+
const likeAnywhere = (value) => `%${escapeLike(value)}%`;
|
|
18
25
|
class Database extends EventEmitter {
|
|
19
26
|
constructor(options, omegga, sqlite, db) {
|
|
20
27
|
super();
|
|
@@ -516,6 +523,221 @@ class Database extends EventEmitter {
|
|
|
516
523
|
message: r.message
|
|
517
524
|
}));
|
|
518
525
|
}
|
|
526
|
+
/**
|
|
527
|
+
* Number of chat messages a player has sent. Join, leave, and server entries
|
|
528
|
+
* are not counted. Console and web messages from accounts with no linked
|
|
529
|
+
* player id are stored with an empty id, so they are never attributed.
|
|
530
|
+
*/
|
|
531
|
+
async getPlayerMessageCount(id) {
|
|
532
|
+
if (!id) return 0;
|
|
533
|
+
const row = this.db.select({ count: drizzleOrm.count() }).from(schema.chatLogs).where(
|
|
534
|
+
drizzleOrm.and(
|
|
535
|
+
// spelled to match chat_logs_user_id_action_created_idx, which is
|
|
536
|
+
// an expression index and is skipped if the expression differs
|
|
537
|
+
drizzleOrm.eq(drizzleOrm.sql`json_extract(${schema.chatLogs.user}, '$.id')`, id),
|
|
538
|
+
drizzleOrm.eq(schema.chatLogs.action, "msg")
|
|
539
|
+
)
|
|
540
|
+
).get();
|
|
541
|
+
return row?.count ?? 0;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Resolve `from:` terms to players. Matches ids exactly and names fuzzily,
|
|
545
|
+
* including names a player has since changed away from, so searching by the
|
|
546
|
+
* name someone used at the time still finds them.
|
|
547
|
+
*/
|
|
548
|
+
resolveChatSenders(names) {
|
|
549
|
+
const clauses = names.map((name) => {
|
|
550
|
+
if (uuid.match(name)) return drizzleOrm.eq(schema.playerHistory.id, name);
|
|
551
|
+
const pattern$1 = pattern.explode(name).source;
|
|
552
|
+
return drizzleOrm.sql`(
|
|
553
|
+
${schema.playerHistory.id} = ${name}
|
|
554
|
+
OR ${schema.playerHistory.name} REGEXP ${pattern$1}
|
|
555
|
+
OR ${schema.playerHistory.displayName} REGEXP ${pattern$1}
|
|
556
|
+
OR EXISTS (
|
|
557
|
+
SELECT 1 FROM json_each(${schema.playerHistory.nameHistory})
|
|
558
|
+
WHERE json_each.type = 'object'
|
|
559
|
+
AND (json_extract(json_each.value, '$.name') REGEXP ${pattern$1}
|
|
560
|
+
OR json_extract(json_each.value, '$.displayName') REGEXP ${pattern$1})
|
|
561
|
+
)
|
|
562
|
+
)`;
|
|
563
|
+
});
|
|
564
|
+
return this.db.select({
|
|
565
|
+
id: schema.playerHistory.id,
|
|
566
|
+
name: schema.playerHistory.name,
|
|
567
|
+
displayName: schema.playerHistory.displayName
|
|
568
|
+
}).from(schema.playerHistory).where(clauses.length > 1 ? drizzleOrm.or(...clauses) : clauses[0]).limit(25).all();
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Player ids currently holding any of the named roles, matched without
|
|
572
|
+
* regard to case.
|
|
573
|
+
*
|
|
574
|
+
* Role assignments are the server's present-day config, not a record of who
|
|
575
|
+
* held what when: someone promoted yesterday matches everything they ever
|
|
576
|
+
* said, and someone demoted matches none of it.
|
|
577
|
+
*/
|
|
578
|
+
resolveRoleMembers(roleNames) {
|
|
579
|
+
const wanted = new Set(roleNames.map((r) => r.toLowerCase()));
|
|
580
|
+
const assignments = this.omegga.getRoleAssignments?.()?.savedPlayerRoles ?? {};
|
|
581
|
+
return Object.entries(assignments).filter(
|
|
582
|
+
([, entry]) => (entry?.roles ?? []).some((role) => wanted.has(role.toLowerCase()))
|
|
583
|
+
).map(([id]) => id);
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Search the chat log with the query language in {@link parseChatQuery}.
|
|
587
|
+
* Terms are ANDed and match anywhere in a message.
|
|
588
|
+
*
|
|
589
|
+
* A query with words in it searches chat messages, since nothing else has
|
|
590
|
+
* words to match. A query built only of filters scopes the log instead, and
|
|
591
|
+
* returns joins, leaves and server entries alongside messages. `action:`
|
|
592
|
+
* overrides either way.
|
|
593
|
+
*
|
|
594
|
+
* Paginates on `created` alone, as {@link getChats} does, so two entries
|
|
595
|
+
* logged in the same millisecond can straddle a page boundary.
|
|
596
|
+
*/
|
|
597
|
+
async searchChats({
|
|
598
|
+
query,
|
|
599
|
+
count: limit = 50,
|
|
600
|
+
cursor,
|
|
601
|
+
sort = "newest"
|
|
602
|
+
}) {
|
|
603
|
+
const parsed = chatQuery.parseChatQuery(query);
|
|
604
|
+
const empty = {
|
|
605
|
+
chats: [],
|
|
606
|
+
senders: [],
|
|
607
|
+
filters: parsed.filters,
|
|
608
|
+
hasMore: false,
|
|
609
|
+
total: 0,
|
|
610
|
+
totalCapped: false,
|
|
611
|
+
context: {}
|
|
612
|
+
};
|
|
613
|
+
if (chatQuery.isEmptyChatQuery(parsed)) return empty;
|
|
614
|
+
const actions = parsed.actions.length > 0 ? parsed.actions : parsed.terms.length > 0 ? ["msg"] : ["msg", "server", "leave", "join", "crash"];
|
|
615
|
+
const byAction = drizzleOrm.inArray(schema.chatLogs.action, actions);
|
|
616
|
+
const conditions = [
|
|
617
|
+
// crashes were logged as ordinary server entries before they had an
|
|
618
|
+
// action of their own, so searching for them has to reach back to those
|
|
619
|
+
// too or a server's existing history looks crash-free
|
|
620
|
+
actions.includes("crash") ? drizzleOrm.or(
|
|
621
|
+
byAction,
|
|
622
|
+
drizzleOrm.and(
|
|
623
|
+
drizzleOrm.eq(schema.chatLogs.action, "server"),
|
|
624
|
+
drizzleOrm.inArray(schema.chatLogs.message, LEGACY_CRASH_MESSAGES)
|
|
625
|
+
)
|
|
626
|
+
) : byAction
|
|
627
|
+
];
|
|
628
|
+
let senders = [];
|
|
629
|
+
if (parsed.from.length > 0) {
|
|
630
|
+
senders = this.resolveChatSenders(parsed.from);
|
|
631
|
+
if (senders.length === 0) return empty;
|
|
632
|
+
conditions.push(
|
|
633
|
+
drizzleOrm.inArray(
|
|
634
|
+
drizzleOrm.sql`json_extract(${schema.chatLogs.user}, '$.id')`,
|
|
635
|
+
senders.map((s) => s.id)
|
|
636
|
+
)
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
if (parsed.roles.length > 0) {
|
|
640
|
+
const members = this.resolveRoleMembers(parsed.roles);
|
|
641
|
+
if (members.length === 0) return empty;
|
|
642
|
+
conditions.push(
|
|
643
|
+
drizzleOrm.inArray(drizzleOrm.sql`json_extract(${schema.chatLogs.user}, '$.id')`, members)
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
if (parsed.adminAny || parsed.admin.length > 0) {
|
|
647
|
+
const web = drizzleOrm.sql`json_extract(${schema.chatLogs.user}, '$.web')`;
|
|
648
|
+
const name = drizzleOrm.sql`lower(json_extract(${schema.chatLogs.user}, '$.name'))`;
|
|
649
|
+
const id = drizzleOrm.sql`json_extract(${schema.chatLogs.user}, '$.id')`;
|
|
650
|
+
conditions.push(
|
|
651
|
+
parsed.admin.length === 0 ? drizzleOrm.sql`${web} = 1` : drizzleOrm.and(
|
|
652
|
+
drizzleOrm.sql`${web} = 1`,
|
|
653
|
+
drizzleOrm.or(
|
|
654
|
+
drizzleOrm.inArray(
|
|
655
|
+
name,
|
|
656
|
+
parsed.admin.map((a) => a.toLowerCase())
|
|
657
|
+
),
|
|
658
|
+
drizzleOrm.inArray(id, parsed.admin)
|
|
659
|
+
)
|
|
660
|
+
)
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
for (const term of parsed.terms) {
|
|
664
|
+
conditions.push(
|
|
665
|
+
drizzleOrm.sql`${schema.chatLogs.message} LIKE ${likeAnywhere(term)} ESCAPE '\\'`
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
if (parsed.before)
|
|
669
|
+
conditions.push(drizzleOrm.lt(schema.chatLogs.created, parsed.before));
|
|
670
|
+
if (parsed.after)
|
|
671
|
+
conditions.push(drizzleOrm.gt(schema.chatLogs.created, parsed.after));
|
|
672
|
+
const cappedMatches = this.db.select({ one: drizzleOrm.sql`1` }).from(schema.chatLogs).where(drizzleOrm.and(...conditions)).limit(SEARCH_COUNT_CAP + 1).as("capped");
|
|
673
|
+
const counted = this.db.select({ total: drizzleOrm.count() }).from(cappedMatches).get()?.total ?? 0;
|
|
674
|
+
const newest = sort === "newest";
|
|
675
|
+
if (cursor !== void 0) {
|
|
676
|
+
conditions.push(
|
|
677
|
+
newest ? drizzleOrm.lt(schema.chatLogs.created, cursor) : drizzleOrm.gt(schema.chatLogs.created, cursor)
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
const rows = this.db.select().from(schema.chatLogs).where(drizzleOrm.and(...conditions)).orderBy((newest ? drizzleOrm.desc : drizzleOrm.asc)(schema.chatLogs.created)).limit(limit + 1).all();
|
|
681
|
+
const hasMore = rows.length > limit;
|
|
682
|
+
const page = rows.slice(0, limit);
|
|
683
|
+
const context = {};
|
|
684
|
+
for (const row of page) {
|
|
685
|
+
context[String(row.id)] = {
|
|
686
|
+
before: await this.getChatContext({
|
|
687
|
+
id: row.id,
|
|
688
|
+
direction: "before",
|
|
689
|
+
count: 1
|
|
690
|
+
}),
|
|
691
|
+
after: await this.getChatContext({
|
|
692
|
+
id: row.id,
|
|
693
|
+
direction: "after",
|
|
694
|
+
count: 1
|
|
695
|
+
})
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
return {
|
|
699
|
+
context,
|
|
700
|
+
total: Math.min(counted, SEARCH_COUNT_CAP),
|
|
701
|
+
totalCapped: counted > SEARCH_COUNT_CAP,
|
|
702
|
+
chats: page.map((r) => ({
|
|
703
|
+
type: "chat",
|
|
704
|
+
_id: String(r.id),
|
|
705
|
+
created: r.created,
|
|
706
|
+
instanceId: r.instanceId,
|
|
707
|
+
action: r.action,
|
|
708
|
+
user: r.user,
|
|
709
|
+
message: r.message
|
|
710
|
+
})),
|
|
711
|
+
senders,
|
|
712
|
+
filters: parsed.filters,
|
|
713
|
+
hasMore
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* The entries immediately before or after one chat log row, for expanding a
|
|
718
|
+
* search result outward a few lines at a time. Keyed on the row id rather
|
|
719
|
+
* than `created` so entries logged in the same millisecond stay in insertion
|
|
720
|
+
* order and none are skipped or repeated while paging outward.
|
|
721
|
+
*
|
|
722
|
+
* Returns oldest first in both directions. A short result means that end of
|
|
723
|
+
* the log has been reached.
|
|
724
|
+
*/
|
|
725
|
+
async getChatContext({
|
|
726
|
+
id,
|
|
727
|
+
direction,
|
|
728
|
+
count: count2 = 5
|
|
729
|
+
}) {
|
|
730
|
+
const rows = direction === "before" ? this.db.select().from(schema.chatLogs).where(drizzleOrm.lt(schema.chatLogs.id, id)).orderBy(drizzleOrm.desc(schema.chatLogs.id)).limit(count2).all().reverse() : this.db.select().from(schema.chatLogs).where(drizzleOrm.gt(schema.chatLogs.id, id)).orderBy(drizzleOrm.asc(schema.chatLogs.id)).limit(count2).all();
|
|
731
|
+
return rows.map((r) => ({
|
|
732
|
+
type: "chat",
|
|
733
|
+
_id: String(r.id),
|
|
734
|
+
created: r.created,
|
|
735
|
+
instanceId: r.instanceId,
|
|
736
|
+
action: r.action,
|
|
737
|
+
user: r.user,
|
|
738
|
+
message: r.message
|
|
739
|
+
}));
|
|
740
|
+
}
|
|
519
741
|
// get paginated players
|
|
520
742
|
async getPlayers({
|
|
521
743
|
count: limit = 50,
|
|
@@ -549,12 +771,48 @@ class Database extends EventEmitter {
|
|
|
549
771
|
const baseWhere = conditions.length > 0 ? drizzleOrm.and(...conditions) : void 0;
|
|
550
772
|
const sortCol = sort === "lastSeen" ? schema.playerHistory.lastSeen : sort === "created" ? schema.playerHistory.created : sort === "heartbeats" ? schema.playerHistory.heartbeats : sort === "sessions" ? schema.playerHistory.sessions : drizzleOrm.sql`${schema.playerHistory.name} COLLATE NOCASE`;
|
|
551
773
|
const orderDir = direction === 1 ? drizzleOrm.asc : drizzleOrm.desc;
|
|
774
|
+
const relevance = sort === "relevance" && search.length > 0 ? drizzleOrm.sql`CASE
|
|
775
|
+
WHEN ${schema.playerHistory.name} = ${search} COLLATE NOCASE THEN 0
|
|
776
|
+
WHEN ${schema.playerHistory.displayName} = ${search} COLLATE NOCASE THEN 1
|
|
777
|
+
WHEN ${schema.playerHistory.name} LIKE ${likePrefix(search)} ESCAPE '\\' THEN 2
|
|
778
|
+
WHEN ${schema.playerHistory.displayName} LIKE ${likePrefix(search)} ESCAPE '\\' THEN 3
|
|
779
|
+
WHEN ${schema.playerHistory.name} LIKE ${likeAnywhere(search)} ESCAPE '\\' THEN 4
|
|
780
|
+
WHEN ${schema.playerHistory.displayName} LIKE ${likeAnywhere(search)} ESCAPE '\\' THEN 5
|
|
781
|
+
ELSE 6
|
|
782
|
+
END` : null;
|
|
552
783
|
const totalResult = this.db.select({ count: drizzleOrm.count() }).from(schema.playerHistory).where(baseWhere).get();
|
|
553
|
-
const
|
|
784
|
+
const messageCount = drizzleOrm.sql`${this.db.select({ n: drizzleOrm.count() }).from(schema.chatLogs).where(
|
|
785
|
+
drizzleOrm.and(
|
|
786
|
+
drizzleOrm.eq(
|
|
787
|
+
drizzleOrm.sql`json_extract(${schema.chatLogs.user}, '$.id')`,
|
|
788
|
+
schema.playerHistory.id
|
|
789
|
+
),
|
|
790
|
+
drizzleOrm.eq(schema.chatLogs.action, "msg")
|
|
791
|
+
)
|
|
792
|
+
)}`;
|
|
793
|
+
const players = this.db.select({
|
|
794
|
+
id: schema.playerHistory.id,
|
|
795
|
+
name: schema.playerHistory.name,
|
|
796
|
+
displayName: schema.playerHistory.displayName,
|
|
797
|
+
nameHistory: schema.playerHistory.nameHistory,
|
|
798
|
+
ips: schema.playerHistory.ips,
|
|
799
|
+
created: schema.playerHistory.created,
|
|
800
|
+
lastSeen: schema.playerHistory.lastSeen,
|
|
801
|
+
lastInstanceId: schema.playerHistory.lastInstanceId,
|
|
802
|
+
heartbeats: schema.playerHistory.heartbeats,
|
|
803
|
+
sessions: schema.playerHistory.sessions,
|
|
804
|
+
instances: schema.playerHistory.instances,
|
|
805
|
+
messageCount
|
|
806
|
+
}).from(schema.playerHistory).where(baseWhere).orderBy(
|
|
807
|
+
...relevance ? [relevance, drizzleOrm.desc(schema.playerHistory.lastSeen)] : sort === "messages" ? [orderDir(messageCount)] : [orderDir(sortCol)]
|
|
808
|
+
).limit(limit).offset(limit * page).all();
|
|
554
809
|
const total = totalResult?.count ?? 0;
|
|
555
|
-
const result = players.map((p) =>
|
|
810
|
+
const result = players.map((p) => ({
|
|
811
|
+
...this._toUserHistory(p),
|
|
812
|
+
messageCount: p.messageCount
|
|
813
|
+
}));
|
|
556
814
|
if (search.length > 0 && page === 0) {
|
|
557
|
-
const exactRows = this.db.select().from(schema.playerHistory).where(
|
|
815
|
+
const exactRows = this.db.select({ row: schema.playerHistory, messageCount }).from(schema.playerHistory).where(
|
|
558
816
|
drizzleOrm.or(
|
|
559
817
|
drizzleOrm.eq(schema.playerHistory.id, search),
|
|
560
818
|
drizzleOrm.eq(schema.playerHistory.name, search),
|
|
@@ -563,10 +821,17 @@ class Database extends EventEmitter {
|
|
|
563
821
|
).all();
|
|
564
822
|
if (exactRows.length > 0) {
|
|
565
823
|
for (const res of exactRows) {
|
|
566
|
-
const idx = result.findIndex((p) => p.id === res.id);
|
|
824
|
+
const idx = result.findIndex((p) => p.id === res.row.id);
|
|
567
825
|
if (idx >= 0) result.splice(idx, 1);
|
|
568
826
|
}
|
|
569
|
-
result.splice(
|
|
827
|
+
result.splice(
|
|
828
|
+
0,
|
|
829
|
+
0,
|
|
830
|
+
...exactRows.map((r) => ({
|
|
831
|
+
...this._toUserHistory(r.row),
|
|
832
|
+
messageCount: r.messageCount
|
|
833
|
+
}))
|
|
834
|
+
);
|
|
570
835
|
}
|
|
571
836
|
}
|
|
572
837
|
return { pages: Math.ceil(total / limit), total, players: result };
|