export interface RichTextVariableDefinition {
/** Variable expression without the surrounding braces. */
key: string;
nameZh: string;
nameEn: string;
descriptionZh: string;
descriptionEn: string;
category: string;
/** Human-readable scope tags such as global, player-event, or schedule. */
scopes: readonly string[];
/** Complete placeholder ready for insertion, for example {server.tps}. */
example?: string;
/** Example resolved value, for example 20.0. */
sampleValue?: string;
formatHint?: string;
type?: string;
events?: readonly string[];
templateAllowed?: boolean;
conditionAllowed?: boolean;
sensitive?: boolean;
}
export const RICH_TEXT_VARIABLE_CATEGORY_LABELS: Readonly<Record<string, { zh: string; en: string }>> = {
legacy: { zh: '兼容变量', en: 'Compatibility' },
event: { zh: '触发事件', en: 'Trigger event' },
time: { zh: '日期与时间', en: 'Date & time' },
server: { zh: '服务器', en: 'Server' },
world: { zh: '世界', en: 'World' },
player: { zh: '玩家', en: 'Player' },
entity: { zh: '实体', en: 'Entity' },
command: { zh: '指令与参数', en: 'Command & arguments' },
schedule: { zh: '计划任务', en: 'Schedule' },
economy: { zh: '经济系统', en: 'Economy' },
trigger: { zh: '触发器运行', en: 'Trigger runtime' },
};
function eventPatternMatches(pattern: string, eventType: string): boolean {
const normalizedPattern = pattern.trim().toLowerCase();
const normalizedEvent = eventType.trim().toLowerCase();
if (!normalizedPattern || !normalizedEvent) return false;
// Split on wildcard runs first, then quote every regular-expression character
// in the literal fragments. This keeps catalog-provided event patterns data,
// rather than allowing them to become regular expressions themselves.
const expression = normalizedPattern.split(/\*+/).map((fragment) =>
fragment.replace(/[|\\{}()[\]^$+?.]/g, '\\$&')).join('.*');
return new RegExp(`^${expression}$`).test(normalizedEvent);
}
/** Returns whether a catalog variable is available for a concrete trigger event. */
export function variableAppliesToEvent(
variable: RichTextVariableDefinition,
eventType: string,
): boolean {
// `events` is authoritative on current servers. Falling back to `scopes`
// preserves compatibility with the legacy client-side catalog and previews.
const patterns = variable.events?.length ? variable.events : variable.scopes;
return patterns.some((pattern) => eventPatternMatches(pattern, eventType));
}
/**
* Conservative fallback used only when connected to an older server that does
* not expose `catalog.variables`. Every entry is supported by the legacy
* message renderer. New servers provide the authoritative 100+ item catalog.
*/
export const RICH_TEXT_VARIABLE_CATALOG: readonly RichTextVariableDefinition[] = Object.freeze([
{
key: 'player', nameZh: '玩家名(兼容)', nameEn: 'Player name (legacy)',
descriptionZh: '等同于 player.name;需要玩家事件上下文。', descriptionEn: 'Alias of player.name; requires player event context.',
category: 'legacy', scopes: ['player.*'], example: '{player}', type: 'string', templateAllowed: true,
},
{
key: 'online', nameZh: '在线人数(兼容)', nameEn: 'Online players (legacy)',
descriptionZh: '等同于 server.online。', descriptionEn: 'Alias of server.online.',
category: 'legacy', scopes: ['*'], example: '{online}', type: 'number', templateAllowed: true,
},
{
key: 'maxPlayers', nameZh: '人数上限(兼容)', nameEn: 'Player limit (legacy)',
descriptionZh: '等同于 server.maxPlayers。', descriptionEn: 'Alias of server.maxPlayers.',
category: 'legacy', scopes: ['*'], example: '{maxPlayers}', type: 'number', templateAllowed: true,
},
{
key: 'date', nameZh: '事件日期(兼容)', nameEn: 'Event date (legacy)',
descriptionZh: '计划任务使用配置时区,其他事件使用服务器时区。',
descriptionEn: 'Uses the configured schedule zone for schedules and the server zone otherwise.',
category: 'legacy', scopes: ['*'], example: '{date}', type: 'string', templateAllowed: true,
},
]);
export interface RichTextVariableDefinition {
/** Variable expression without the surrounding braces. */
key: string;
nameZh: string;
nameEn: string;
descriptionZh: string;
descriptionEn: string;
category: string;
/** Human-readable scope tags such as global, player-event, or schedule. */
scopes: readonly string[];
/** Complete placeholder ready for insertion, for example {server.tps}. */
example?: string;
/** Example resolved value, for example 20.0. */
sampleValue?: string;
formatHint?: string;
type?: string;
events?: readonly string[];
templateAllowed?: boolean;
conditionAllowed?: boolean;
sensitive?: boolean;
}
export const RICH_TEXT_VARIABLE_CATEGORY_LABELS: Readonly<Record<string, { zh: string; en: string }>> = {
legacy: { zh: '兼容变量', en: 'Compatibility' },
event: { zh: '触发事件', en: 'Trigger event' },
time: { zh: '日期与时间', en: 'Date & time' },
server: { zh: '服务器', en: 'Server' },
world: { zh: '世界', en: 'World' },
player: { zh: '玩家', en: 'Player' },
entity: { zh: '实体', en: 'Entity' },
command: { zh: '指令与参数', en: 'Command & arguments' },
schedule: { zh: '计划任务', en: 'Schedule' },
economy: { zh: '经济系统', en: 'Economy' },
trigger: { zh: '触发器运行', en: 'Trigger runtime' },
};
function eventPatternMatches(pattern: string, eventType: string): boolean {
const normalizedPattern = pattern.trim().toLowerCase();
const normalizedEvent = eventType.trim().toLowerCase();
if (!normalizedPattern || !normalizedEvent) return false;
// Split on wildcard runs first, then quote every regular-expression character
// in the literal fragments. This keeps catalog-provided event patterns data,
// rather than allowing them to become regular expressions themselves.
const expression = normalizedPattern.split(/\*+/).map((fragment) =>
fragment.replace(/[|\\{}()[\]^$+?.]/g, '\\$&')).join('.*');
return new RegExp(`^${expression}$`).test(normalizedEvent);
}
/** Returns whether a catalog variable is available for a concrete trigger event. */
export function variableAppliesToEvent(
variable: RichTextVariableDefinition,
eventType: string,
): boolean {
// `events` is authoritative on current servers. Falling back to `scopes`
// preserves compatibility with the legacy client-side catalog and previews.
const patterns = variable.events?.length ? variable.events : variable.scopes;
return patterns.some((pattern) => eventPatternMatches(pattern, eventType));
}
/**
* Conservative fallback used only when connected to an older server that does
* not expose `catalog.variables`. Every entry is supported by the legacy
* message renderer. New servers provide the authoritative 100+ item catalog.
*/
export const RICH_TEXT_VARIABLE_CATALOG: readonly RichTextVariableDefinition[] = Object.freeze([
{
key: 'player', nameZh: '玩家名(兼容)', nameEn: 'Player name (legacy)',
descriptionZh: '等同于 player.name;需要玩家事件上下文。', descriptionEn: 'Alias of player.name; requires player event context.',
category: 'legacy', scopes: ['player.*'], example: '{player}', type: 'string', templateAllowed: true,
},
{
key: 'online', nameZh: '在线人数(兼容)', nameEn: 'Online players (legacy)',
descriptionZh: '等同于 server.online。', descriptionEn: 'Alias of server.online.',
category: 'legacy', scopes: ['*'], example: '{online}', type: 'number', templateAllowed: true,
},
{
key: 'maxPlayers', nameZh: '人数上限(兼容)', nameEn: 'Player limit (legacy)',
descriptionZh: '等同于 server.maxPlayers。', descriptionEn: 'Alias of server.maxPlayers.',
category: 'legacy', scopes: ['*'], example: '{maxPlayers}', type: 'number', templateAllowed: true,
},
{
key: 'date', nameZh: '事件日期(兼容)', nameEn: 'Event date (legacy)',
descriptionZh: '计划任务使用配置时区,其他事件使用服务器时区。',
descriptionEn: 'Uses the configured schedule zone for schedules and the server zone otherwise.',
category: 'legacy', scopes: ['*'], example: '{date}', type: 'string', templateAllowed: true,
},
]);