moonbot.accounts — Scripts et Controllers¶
moonbot.accounts — Scripts et Controllers
23 fonctions dans ce namespace.
Index¶
Une API commune, deux structuresStructure d'un controllerLister ou cibler ses comptesHandle accountLire le personnageLire l'etat du botLire l'inventaireLire les metiersLire les sortsExemple — changer de script sur un seuilLire les statistiques d'un compteExemple — heures du jour par botConnexion d'un compteScript principal d'un compteTrouver scriptId, revision et contentHashReçu du script principalForme du reçuExemple — enchaîner deux scripts avec leurs reçusRetours des mutations d'un ControllerLogs du controllerHiérarchie des actionsIsolation et sécuritéExemple de supervision
Référence¶
Une API commune, deux structures¶
-- Script normal : dans move(), bank(), custom(), etc.
-- Controller : dans tick()
local accounts = moonbot.accounts:all()
moonbot.accounts est disponible dans les scripts normaux et dans les Controllers. Les noms de handles et d'opérations sont communs, mais leur admission et leurs valeurs de retour diffèrent selon le runtime. Un script normal conserve ses fonctions de trajet, tandis qu'un Controller retourne le descripteur périodique ci-dessous. L'API n'expose jamais les identifiants de connexion.
Structure d'un controller¶
return {
interval_seconds = 60,
run_immediately = true,
tick = function()
-- une passe courte
end,
}
Un Controller retourne ce descripteur au lieu de move()/bank(). La cadence se configure uniquement ici : le runner isolé la lit et la transmet automatiquement au serveur. Les ticks ne se chevauchent pas : n'ajoutez ni while true ni boucle d'attente, et placez les opérations moonbot.accounts dans tick, pas au niveau supérieur du fichier.
Lister ou cibler ses comptes¶
returns account[] / account | nil selon le runtime
all() retourne les comptes de l'utilisateur disponibles sur son worker. Dans un script normal, get(id) retourne nil si la cible est inconnue ou non autorisée. Dans un Controller, get(id) construit un handle paresseux sans RPC : la propriété est contrôlée au premier usage et un identifiant refusé produit alors une erreur générique. Chaque opération sensible revalide encore la cible.
Handle account¶
returns string / {read, write} / boolean / table
account est un handle de compte renvoyé par moonbot.accounts:all() (un par compte) ou moonbot.accounts:get(id) : une variable, pas un module. snapshot() expose l'état publié sans login ni mot de passe : connexion, jeu, combat, script, personnage et présence du profil. capabilities().write et writable() indiquent seulement une éligibilité dans le runtime courant : false interdit l'écriture, mais true n'est ni un verrou réservé ni la garantie que l'opération suivante réussira. Une action peut encore être bloquée ou préemptée par un acteur placé au-dessus d'elle.
Lire le personnage¶
account.character:name() / level() / classId() / breedName() / id() / hp() / hpMax() / energy() / energyMax() / kamas() / xp() / mapId() / cellId() / x() / y() / pods() / podsPercent() / server() / serverId() / isInFight()
returns string | number | boolean | table | nil
Ces getters lisent un snapshot du bot cible. Ils retournent nil quand aucun personnage n'est publie. Alias disponibles : breed(), lifePoints(), maxLifePoints(), lifePointsP() et position().
Lire l'etat du bot¶
returns string | boolean | nil
Permet de superviser un compte sans executer de code dans son script principal. Un Controller peut rendre nil pour un bot absent; l'adaptateur d'un script normal rend false pour les booléens et « Deconnecte » pour state(). Dans les deux cas, not account.bot:isConnected() couvre un bot absent ou deconnecte.
Lire l'inventaire¶
account.inventory:list() / countItem(templateId) / findByName(name) / totalQuantity() / pods() / state()
returns table / number / table | nil
Lecture pure du sac tenu a jour par le moteur : aucun Lua n'est execute dans le bot cible et aucune fenetre de jeu n'est ouverte. list() rend {id, uid, quantity, name, position, equipped, type, superType, typeName, price} par item ; countItem(templateId) additionne un template, equipe compris, et rend 0 s'il est absent. state() rend la table complete : items, count, totalQuantity, truncated, private, pods, podsPercent. Chaque appel est un aller-retour : pour plusieurs questions, lisez list() une fois et filtrez en Lua. Bot absent = nil, jamais une fiche vide. Plafond de 1000 lignes (voir truncated). La chaine effects (jets) reste hors de cette surface.
Lire les metiers¶
returns table / number | nil
list() rend {id, name, level, xp, xpNext, xpMin, xpCurrent, xpMax} par metier. level(jobId) rend 0 pour un metier non appris, pas nil, afin de comparer sans garde sur chaque branche. Memes regles que l'inventaire : bot absent = nil, plafond de 1000 lignes, fiche masquee pour un compte Script Prive.
Lire les sorts¶
returns table | nil
list() rend {id, name, level, position, zoneType} par sort connu. Utile pour verifier qu'un personnage possede bien le sort attendu avant de lancer un script de combat.
Exemple — changer de script sur un seuil¶
return {
interval_seconds = 60,
tick = function()
local account = moonbot.accounts:get(ACCOUNT_ID)
local bag = account.inventory:state()
if not bag then return end
if account.inventory:countItem(289) >= 500 then
local state = account.scripts:state()
if state.running then
account.scripts:stop()
return -- le slot doit etre libre avant ensureRunning
end
account.scripts:ensureRunning(CRAFT_ID, CRAFT_REVISION, CRAFT_HASH)
end
end,
}
Le Controller lit la fiche en direct, script en cours d'execution : rien ne l'oblige a arreter quoi que ce soit pour observer. Seul le changement de script exige un slot principal libre, d'ou le stop puis le retour : relisez scripts:state() au tick suivant plutot que d'enchainer les deux dans la meme passe.
Lire les statistiques d'un compte¶
returns table | nil
Memes champs, unites et fuseau que bot.sessionStats() et bot.connectionStats(). Disponible dans les scripts normaux et les Controllers, en lecture seule, uniquement pour les comptes autorises. nil signifie que le bot est absent ou que le suivi de connexion n'est pas encore initialise ; ne le remplacez pas par zero dans une moyenne. Un ancien worker doit etre mis a jour pour fournir cette API. Les donnees locales restent lisibles pendant une coupure du Control Plane sous les regles d'autorisation habituelles.
Exemple — heures du jour par bot¶
return {
interval_seconds = 60,
tick = function()
local total, count = 0, 0
for _, account in ipairs(moonbot.accounts:all()) do
local stats = account.bot:connectionStats()
if stats then
total = total + stats.todayHours
count = count + 1
global:printMessage(account:id() .. " : " .. tostring(stats.todayHours) .. " h aujourd'hui")
end
end
if count > 0 then
global:printMessage("Moyenne des bots disponibles : " .. tostring(total / count) .. " h aujourd'hui")
end
end,
}
Moyenne des heures du jour des bots dont un snapshot est disponible sur le worker de l'utilisateur. Le compte a zero heure est inclus si son snapshot existe ; un compte indisponible est exclu. Cette moyenne entre bots differe de averageDailyHours, qui est la moyenne par jour d'un seul bot depuis trackedFrom.
Connexion d'un compte¶
returns state: table; mutations: valeur dépendante du runtime
Le Lua ne reçoit jamais les identifiants de connexion. Dans un script normal, ensureConnected() attend l'entrée en jeu (ou la sélection manuelle) et disconnect() attend la fin de la génération exacte lorsqu'elle est observable. Un arrêt par email ou sans cible locale est quand même transmis au control-plane, mais retourne false car sa fin ne peut pas être prouvée dans cette VM; l'auto-arrêt retourne l'acceptation puisque la VM disparaît avec le bot. Dans un Controller, l'appel attend le résultat terminal fenced; un moteur déjà actif est un no-op, tandis que disconnect() passe toujours par l'autorité de cycle de vie pour couvrir une connexion en préparation. Relisez state() ou isConnected() à la passe suivante.
Script principal d'un compte¶
account.scripts:state() / account.scripts:ensureRunning(scriptId, revision, contentHash) / account.scripts:stop()
returns state: table; mutations: valeur dépendante du runtime
ensureRunning(scriptId) suffit : scriptId est le NOM du script dans ta bibliotheque, et revision/contentHash sont optionnels — sans eux, la version actuellement enregistree est lancee. Avec eux, la version est epinglee et toute difference rend script_revision_mismatch ou script_hash_mismatch (conflit terminal : la passe s'arrete). Dans un Controller, le control-plane resout ou compare la revision durable et le SHA-256 avant l'effet fence ; dans un script normal, revision reste une metadonnee et le retour est un booleen d'admission locale. Relis toujours scripts:state() ensuite.
Trouver scriptId, revision et contentHash¶
account.scripts:ensureRunning("MonScript")
-- ou, version epinglee :
account.scripts:ensureRunning("MonScript", 7, "<sha256>")
scriptId est le nom du script dans ta bibliotheque (pas un UUID) ; les anciens noms d'un script renomme marchent aussi. revision et contentHash sont optionnels : sans eux tu lances la version actuellement enregistree, donc ton Controller survit a une edition ordinaire de son script. Si tu veux epingler une version : lance le script une fois sur le compte depuis le dashboard puis lis account.scripts:state(), qui renvoie {script_id, revision, content_hash} — les valeurs exactes a recopier. Sinon par l'API : GET /api/v1/scripts donne name et revision, GET /api/v1/scripts/{name} donne le contenu dont tu prends le sha256 (contenu brut, impossible pour un script marketplace a source masquee). Les deux valeurs changent a chaque sauvegarde : un Controller epingle doit etre re-epingle apres chaque modification, c'est precisement ce que l'epinglage achete.
Reçu du script principal¶
returns table | nil
Dernier run du script principal du compte, même après un redémarrage du worker ou du bot ; nil si le compte n'a jamais lancé de script. status : running, succeeded (global:finishScript), failed (global:failScript ou erreur Lua), stopped (arrêt demandé : joueur, Controller, planning) ou interrupted (le run n'a pas pu se clore : worker redémarré, bot disparu). finished est vrai hors running, ok seulement pour succeeded. stop_reason précise la fin : finish_script, fail_script, script_end (fin naturelle du code), error, pods_full (pods pleins sans fonction bank()), stopped, worker_restart, replaced ou bot_stopped. main_script_run_id est le numéro du run dans la session du bot, le même que account.scripts:state().main_script_run_id : il dit si le reçu est celui du script qui tourne. La lecture ne consomme rien : le même reçu revient à chaque tick, retenez son run_id (par exemple dans moonbot.memory) pour ne le traiter qu'une fois. Compte « Script Privé » : private = true, result, error et checkpoint masqués. Disponible aussi dans un script principal via moonbot.accounts ; les 20 derniers runs de chaque compte sont visibles dans Controllers › Mémoire des scripts › Reçus.
Forme du reçu¶
{
run_id = "uuid", main_script_run_id = 3, script_id = "Elevage", revision = 7, content_hash = "…",
status = "running" | "succeeded" | "failed" | "stopped" | "interrupted",
finished = true, ok = true, -- ok : status == "succeeded"
result = {...}, error = nil, stop_reason = "finish_script",
checkpoint = {...}, checkpoint_at = 1759950000,
started_at = 1759940000, finished_at = 1759950000, age_seconds = 42,
private = false, -- compte « Script Privé » : result/error/checkpoint masqués
}
Horodatages en secondes Unix ; age_seconds donne l'ancienneté du reçu. result vient de global:finishScript(result) ou de global:failScript(message, result), checkpoint du dernier global:checkpoint(value) de ce compte pour ce script.
Exemple — enchaîner deux scripts avec leurs reçus¶
local ACCOUNT_ID = "…" -- id du compte (moonbot.accounts:all())
local ETAPES = { "Elevage", "Vente" } -- scripts, dans l'ordre
local CLE = "chaine:" .. ACCOUNT_ID -- { etape, run_id } dans moonbot.memory
return {
interval_seconds = 30,
tick = function()
local account = moonbot.accounts:get(ACCOUNT_ID)
local etat = moonbot.memory:get(CLE) or { etape = 1 }
local script = ETAPES[etat.etape]
if not script then return end -- chaîne terminée
local recu = account.scripts:receipt()
if not etat.run_id then -- étape pas encore lancée
if account.scripts:state().running then return end -- slot occupé : tick suivant
local r = account.scripts:ensureRunning(script)
if type(r) == "table" and r.ok == false then return end
etat.avant = recu and recu.run_id or "" -- reçu précédent, à ignorer
etat.run_id = "?"
moonbot.memory:set(CLE, etat)
return
end
if not recu then return end
if etat.run_id == "?" then -- reçu du nouveau run
if recu.run_id == etat.avant or recu.script_id ~= script then return end
etat.run_id = recu.run_id -- retenu : traité une seule fois
moonbot.memory:set(CLE, etat)
end
if recu.run_id ~= etat.run_id or not recu.finished then return end
if recu.ok then
global:printSuccess(script .. " terminé : " .. tostring(recu.result and recu.result.total))
moonbot.memory:set(CLE, { etape = etat.etape + 1 }) -- étape suivante
else
global:printError(script .. " : " .. recu.status .. " " .. tostring(recu.error or recu.stop_reason))
moonbot.memory:set(CLE, { etape = etat.etape }) -- même étape, relancée
end
end,
}
Le Controller lance le script de l'étape, attend son reçu puis passe à l'étape suivante. L'étape et le run_id attendu sont gardés dans moonbot.memory : ils survivent au redémarrage du Controller, et un reçu déjà traité n'est jamais compté deux fois. Le script lancé se termine par global:finishScript({ total = … }) ou global:failScript("motif").
Retours des mutations d'un Controller¶
-- succès : table d'effet ou nil
-- priorité : { ok=false, status="blocked"|"preempted"|"pending", code="PRIORITY_...", retryable=true }
returns table | nil, ou erreur
Une table de priorité est une valeur vraie en Lua : if not result ne détecte donc pas PRIORITY_BLOCKED. Testez type(result) == "table" and result.ok == false, puis journalisez result.code. Un refus ordinaire lève une erreur COMMAND_ERROR[code] : pcall l'attrape et tostring(err) rend ce texte. Écrire sur un compte hors du périmètre d'écriture du Controller (les comptes cochés dans son éditeur) est refusé : avec un bot à jour, c'est un refus ordinaire, COMMAND_ERROR[account_not_writable], et le Controller continue ; avec un bot plus ancien, l'erreur arrête le Controller, et le script de ce compte peut avoir été arrêté juste avant. Les conflits de fence, autorisations invalides et résultats ambigus restent des erreurs fail-closed et ne doivent pas être rejoués aveuglément.
Logs du controller¶
Les messages apparaissent en direct dans la console sous l'editeur du controller. Utilisez-les pour diagnostiquer chaque passe, sans y placer de secret ni les utiliser comme stockage.
Hiérarchie des actions¶
L'ordre complet est enregistré durablement et la sécurité système reste toujours au-dessus. Un worker automation-priority-v1 applique les rangs des actions humaines, Controllers, scripts normaux et tâches planifiées à la frontière d'effet. Un worker controller-priority-v1 plus ancien arbitre seulement les Controllers; les autres restent en mode legacy. Consultez le badge d'activation de l'onglet et contrôlez toujours le retour, puis l'état et les logs de la cible.
Isolation et sécurité¶
Chaque Controller s'exécute dans une VM limitée. L'API de comptes ne fournit ni accès système, ni chargement de code arbitraire, ni identifiants de connexion, et une cible hors de votre compte MoonBot est refusée.
Exemple de supervision¶
return {
interval_seconds = 60,
tick = function()
for _, account in ipairs(moonbot.accounts:all()) do
local state = account.bot:state()
global:printMessage(account:id() .. " : " .. tostring(state or "indisponible"))
if account:writable() and not account.bot:isConnected() then
local result = account.connection:ensureConnected()
if type(result) == "table" and result.ok == false then
global:printError(account:id() .. " : " .. tostring(result.code))
end
end
end
end,
}
Cet exemple vise un Controller : chaque passe relit la liste, journalise l'état et demande les reconnexions nécessaires. Il reconnaît explicitement une table de priorité, puis le tick suivant confirme le résultat observé. Dans un script normal, le même appel retourne un booléen après observation de l'état cible ou expiration du délai.