Aller au contenu

moonbot.accounts — Scripts et Controllers

moonbot.accounts — Scripts et Controllers

23 fonctions dans ce namespace.

Index


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

moonbot.accounts:all() / moonbot.accounts:get(accountId: string)

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

account:id() / account:capabilities() / account:writable() / account:snapshot()

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

account.bot:state() / isInGame() / isFighting() / isConnected() / isPaused() / isRunning()

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

account.jobs:list() / level(jobId) / state()

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

account.spells:list() / state()

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

account.bot:sessionStats() / account.bot:connectionStats()

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

account.connection:state() / account.connection:ensureConnected() / account.connection:disconnect()

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

account.scripts:receipt()

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

print(...) / global:printMessage(...) / global:printSuccess(...) / global:printError(...) 

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 peut être enregistré dans « 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é

-- pas d'accès direct à io, os, require, au réseau ou aux credentials

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.