Files
AzerothAI/core/ale.ts
T
2026-09-22 23:18:27 -04:00

81 lines
3.4 KiB
TypeScript

export type AleScalar = string|number|boolean|null;
export type AleOperatorRequest =
| {kind:'command';command:string}
| {kind:'global';method:string;args?:AleScalar[]}
| {kind:'player';player:string;method:string;args?:AleScalar[]}
| {kind:'playerbotAI';player:string;method:string;args?:AleScalar[]}
| {kind:'raw';source:string};
const IDENT=/^[A-Za-z_][A-Za-z0-9_]*$/;
export function luaString(value:string){
return '"' + value
.replace(/\\/g,'\\\\')
.replace(/"/g,'\\"')
.replace(/\r/g,'\\r')
.replace(/\n/g,'\\n') + '"';
}
function luaValue(value:AleScalar):string{
if(value===null)return 'nil';
if(typeof value==='string')return luaString(value);
if(typeof value==='number'){
if(!Number.isFinite(value))throw new Error('ALE numeric arguments must be finite');
return String(value);
}
return value?'true':'false';
}
function method(name:string){
if(!IDENT.test(name))throw new Error('Invalid ALE method name');
return name;
}
function args(values:AleScalar[]|undefined){
return (values??[]).map(luaValue).join(',');
}
export function compileAleOperatorRequest(input:AleOperatorRequest,rawEnabled:boolean){
if(input.kind==='command'){
if(!input.command.trim()||input.command.length>500)throw new Error('Invalid server command');
return `RunCommand(${luaString(input.command.trim())}); return "command dispatched"`;
}
if(input.kind==='global'){
return `local fn=_G[${luaString(method(input.method))}]; if type(fn)~="function" then error("Unknown global ALE method") end; return fn(${args(input.args)})`;
}
if(input.kind==='player'){
return `local p=GetPlayerByName(${luaString(input.player)}); if not p then error("Player not found") end; return p:${method(input.method)}(${args(input.args)})`;
}
if(input.kind==='playerbotAI'){
return `local p=GetPlayerByName(${luaString(input.player)}); if not p then error("Player not found") end; local ai=p:GetBotAI(); if not ai then error("PlayerBotAI not found") end; return ai:${method(input.method)}(${args(input.args)})`;
}
if(!rawEnabled)throw new Error('Raw ALE execution is disabled. Set ALE_RAW_EXEC_ENABLED=true on the private server to enable the full operator surface.');
if(!input.source.trim()||input.source.length>12000)throw new Error('Invalid raw ALE source');
return input.source;
}
export function wrapAleCommand(id:string,source:string){
const qid=luaString(id);
return `local __command_id=${qid}
local function __stringify(v)
if v==nil then return "nil" end
local t=type(v)
if t=="string" or t=="number" or t=="boolean" then return tostring(v) end
return "<"..t.."> "..tostring(v)
end
local __ok,__result=pcall(function()
${source}
end)
if not (__ok and type(__result)=="table" and __result.azerothaiDeferred==true) then
AzerothAIComplete(__command_id,__ok,__stringify(__result))
end
`;
}
export const ALE_SURFACE = {
gameClasses:['ALEQuery','Achievement','Aura','BattleGround','ChatHandler','Corpse','Creature','GameObject','GemPropertiesEntry','Global','Group','Guild','Item','ItemTemplate','Loot','Map','Object','Pet','Player','Quest','Roll','Spell','SpellEntry','SpellInfo','Ticket','Transport','Unit','Vehicle','WorldObject','WorldPacket'],
playerbotClasses:['PlayerBot','PlayerBotAI','PlayerBotFactory','PlayerBotHolder','PlayerBotMgr','PlayerBotsMgr','RandomPlayerBotMgr'],
operatorModes:['server-command','global-method','player-method','playerbot-ai-method','raw-lua'],
modelAccess:'none'
} as const;