64 lines
3.3 KiB
TypeScript
64 lines
3.3 KiB
TypeScript
interface CommandNode {
|
|
flags?: {command_node_type?: number};
|
|
children?: number[];
|
|
redirectNode?: number;
|
|
extraNodeData?: {name?: string; parser?: string; properties?: {type?: number | string}};
|
|
}
|
|
interface CommandTree {nodes: CommandNode[]; rootIndex: number}
|
|
const trees = new WeakMap<object, CommandTree>();
|
|
|
|
export function remember_command_tree(instance: object, packet: unknown): void {
|
|
const tree = packet as CommandTree;
|
|
if (tree && Array.isArray(tree.nodes) && Number.isSafeInteger(tree.rootIndex)
|
|
&& tree.nodes.length <= 100_000 && tree.nodes[tree.rootIndex]) trees.set(instance, tree);
|
|
}
|
|
|
|
export function local_command_completion(instance: object, input: string, players: string[]): {matches: string[]; start: number; length: number} {
|
|
const start = input.search(/\S*$/);
|
|
const offset = start === 0 && input.startsWith('/') ? 1 : start;
|
|
const result = {matches: [] as string[], start: offset, length: input.length - offset};
|
|
const tree = trees.get(instance);
|
|
if (!tree || !input.startsWith('/')) return result;
|
|
// Walk complete arguments; only the final (possibly empty) token is being completed.
|
|
const tokens = input.slice(1, start || 1).trim().split(/\s+/).filter(Boolean);
|
|
let parents = [tree.rootIndex];
|
|
const children = (index: number): number[] => {
|
|
const node = tree.nodes[index];
|
|
const redirect = node?.redirectNode;
|
|
return Number.isSafeInteger(redirect) ? tree.nodes[redirect!]?.children ?? [] : node?.children ?? [];
|
|
};
|
|
for (const token of tokens) {
|
|
const next: number[] = [];
|
|
for (const parent of parents) {
|
|
for (const index of children(parent)) {
|
|
const node = tree.nodes[index];
|
|
if (node?.flags?.command_node_type === 1 && node.extraNodeData?.name === token) next.push(index);
|
|
else if (node?.flags?.command_node_type === 2) {
|
|
const parser = node.extraNodeData?.parser;
|
|
const kind = node.extraNodeData?.properties?.type;
|
|
// Quoted and greedy strings need Brigadier parsing; leave their suggestions to the server.
|
|
if (token.includes('"') || parser === 'minecraft:message' || parser === 'brigadier:string' && (kind === 2 || kind === 'greedy_phrase')) continue;
|
|
next.push(index);
|
|
}
|
|
}
|
|
}
|
|
parents = [...new Set(next)].slice(0, 256);
|
|
}
|
|
const prefix = input.slice(offset).toLowerCase();
|
|
for (const parent of parents) {
|
|
for (const index of children(parent)) {
|
|
const node = tree.nodes[index];
|
|
const data = node?.extraNodeData;
|
|
const suggestions = node?.flags?.command_node_type === 1 ? [data?.name]
|
|
: data?.parser === 'brigadier:bool' ? ['true', 'false']
|
|
: data?.parser === 'minecraft:entity' || data?.parser === 'minecraft:game_profile' ? players : [];
|
|
for (const match of suggestions) {
|
|
if (typeof match === 'string' && match.length && match.length <= 256
|
|
&& match.toLowerCase().startsWith(prefix) && !result.matches.includes(match)) result.matches.push(match);
|
|
if (result.matches.length >= 100) return result;
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|