Ordonnancement
Dix fonctions pour faire quelque chose plus tard, ou régulièrement — en bloquant le script ou non, comme vous voulez.
La différence avec la famille Sleep est là : SleepSec(30) arrête votre script trente secondes, alors que ExecIn(30000, f) le laisse continuer et appellera f dans trente secondes, à côté.
Les dix
| Fonction | Rend | |
|---|---|---|
ExecIn(ms, f) | 1 : une fonction d'annulation | Appelle f une fois, dans ms millisecondes. |
ExecAt(quand, f) | 1 : une fonction d'annulation | Appelle f une fois, à l'heure dite. |
IntervalExec(ms, f) | 1 : une fonction d'annulation | Appelle f toutes les ms millisecondes, indéfiniment. |
CronExec(horaire, f) | 2 : un numéro et une erreur | Appelle f selon un horaire. |
RangeCronExec(début, fin, libellé, f) | 2 : un numéro et une erreur | Appelle f une fois par jour, à une heure tirée au hasard dans le créneau. |
RemoveCron(numéro) | 1 : un booléen | Retire une tâche posée par les deux précédentes. |
ExecInCh(ms, f) | 1 : un canal | Comme ExecIn, mais le canal se ferme après f. |
ExecAtCh(quand, f) | 1 : un canal | Comme ExecAt, mais le canal se ferme après f. |
ExecInSync(ms, f) | 1 : une fonction d'annulation | Attend, puis appelle f, sans rendre la main. |
ExecAtSync(quand, f) | rien | Attend l'heure dite, puis appelle f. |
annuler = ExecIn(60000, func() { Print("une minute plus tard") })
// … plus loin, si l'on change d'avis :
annuler()
Les trois premières rendent une fonction d'annulation. Gardez-la si vous comptez arrêter la tâche avant son terme ; sinon ignorez-la, tout s'arrête de toute façon quand le script s'arrête.
L'horaire de CronExec
Trois écritures sont acceptées.
| Écriture | Sens |
|---|---|
"0 30 3 * * *" | Cron à six champs, la seconde en tête : tous les jours à 3 h 30. |
"@16h43" | Tous les jours à 16 h 43. |
"16:43:12" | Tous les jours à 16 h 43 et 12 secondes. |
Les deux dernières sont des raccourcis pour l'horaire quotidien, qui est le cas le plus courant.
id, err = CronExec("@03h30", func() {
Print("rapport de la nuit")
})
if err != nil { LogError("horaire refusé :", err) }
Attention au champ des secondes : un cron classique en compte cinq, celui-ci en compte six. "30 3 * * *" sera donc refusé, avec une erreur qui le dit.
Attendre son propre rendez-vous
Les trois premières rendent la main aussitôt. Les quatre suivantes vous laissent choisir.
Print("avant")
<-ExecInCh(60000, func() { Print("une minute plus tard") })
Print("après") // ne s'écrit qu'une fois le rappel terminé
Le canal se ferme après que la fonction a fini. C'est la forme qu'emploie l'outil de référence pour rentrer une flotte à une heure précise :
<-ExecAtCh("03:17:00", func() {
f = NewFleet()
f.SetOrigin("M:1:2:3")
f.SetDestination("M:1:2:4")
f.SetSpeed(TEN_PERCENT)
f.SetMission(PARK)
f.SetAllResources()
f.SendNow()
})
Un canal plutôt qu'un appel direct, parce que vous choisissez alors quand attendre : posez dix rendez-vous, puis attendez-les dans l'ordre qui vous arrange.
Les versions Sync font la même chose sans canal, quand vous n'avez rien à choisir :
ExecAtSync("03:17:00", func() { RentrerLaFlotte() })
Print("la flotte est rentrée")
ExecInSync rend une fonction d'annulation, comme le déclare l'outil de référence. Elle n'annule rien : quand votre script la reçoit, l'appel est déjà fait. Elle existe pour qu'un script recopié ne s'arrête pas dessus.
Deux garde-fous
Le canal se ferme aussi quand le script s'arrête, sans exécuter le rappel. Sans cela, arrêter à midi un script posé sur <-ExecAtCh("03:00:00", …) l'aurait laissé pendu quinze heures — et l'extinction du robot avec lui.
Un horaire illisible rend un canal déjà fermé, et la raison part au journal. Vous reprenez la main tout de suite, sans que le rappel soit parti. Pendre indéfiniment sur une faute de frappe serait la pire des deux réponses.
Un créneau plutôt qu'une heure
CronExec("@03h30", f) appelle f à trois heures trente pile, chaque nuit. C'est exactement ce qu'on ne veut pas d'une action qui doit passer inaperçue : une seconde toujours identique se remarque.
RangeCronExec prend deux bornes et tire l'heure au hasard entre elles, tous les jours.
id, err = RangeCronExec("03:00:00", "03:45:00", "rentrée de nuit", func() {
Print("la flotte rentre")
})
Le troisième argument est un libellé à vous. Il s'écrit sur la console au moment où la tâche est posée, puis à chaque fois qu'elle part : c'est ce qui vous permet de suivre trois créneaux sans les confondre.
rentrée de nuit - 03:00:00 ... 03:45:00 ← à la pose, hors du créneau
rentrée de nuit - 03:12:47 ← à la pose, déjà dans le créneau
rentrée de nuit ← au moment de l'appel
Si vous posez le créneau alors qu'il est déjà ouvert, le rendez-vous est pris dans le temps qui reste, et non remis au lendemain. Un script relancé à 3 h 05 sur un créneau de 3 h à 3 h 45 partira donc cette nuit-là.
Un créneau qui franchit minuit est accepté. RangeCronExec("23:00:00", "01:00:00", …) dure deux heures, comme on l'attend, et non moins vingt-deux.
Les bornes s'écrivent hh:mm ou hh:mm:ss, comme partout ailleurs. Une borne illisible rend une erreur sur-le-champ, avant que rien ne soit posé.
Retirer une tâche
id, err = CronExec("@03h30", f)
// … plus tard
if RemoveCron(id) {
Print("décrochée")
} else {
Print("ce numéro ne désigne plus rien")
}
RemoveCron rend un booléen : vrai si le numéro désignait une tâche, faux sinon. L'outil de référence ne rend rien ; un script recopié tel quel — la ligne RemoveCron(id) seule — marche pareil, et celui qui veut savoir peut lire la réponse.
C'est le seul moyen de distinguer un retrait réel d'un numéro périmé, et cela compte : un script qui croit avoir décroché sa tâche de nuit et se trompe la verra partir quand même.
Les numéros sont propres à votre script et distincts les uns des autres. Retirer un créneau annule aussi son rendez-vous du jour, s'il en avait un en attente.
Une tâche posée par une fonction de !global.ank est celle du script qui appelle la fonction. Une aide comme func Chaque(ms, f) { return IntervalExec(ms, f) } pose donc la tâche du script qui s'en sert : elle s'arrête avec lui, ses échecs s'écrivent dans sa console, et son numéro se retire depuis ce script. Depuis un go, c'est pareil : le go appartient au script qui l'a lancé, même depuis une tâche ou depuis un autre go. Ce que !global.ank pose lui-même, à son premier niveau ou dans ses propres tâches, reste à lui, même quand un script le recharge par StartScript("!global.ank").
RemoveCron écrit dans une fonction de !global.ank cherche le numéro d'abord parmi les tâches de !global.ank, puis parmi celles du script qui appelle la fonction. Une aide func ArreterGlobal() { return RemoveCron(GID) } retire donc bien la tâche du fichier global, même appelée par un script qui a posé sa propre tâche sous le même numéro. Chaque script numérote ses tâches à partir de 1 : pour qu'une aide retire une tâche qu'elle a posée pour vous, retirez-la plutôt depuis votre script.
Ce qui se passe quand la tâche échoue
Votre fonction tourne à côté du script, dans son propre fil. Si elle échoue, elle ne fait tomber ni le script ni le robot : l'erreur part au journal sous tâche planifiée en échec, avec le nom du script et la raison.
Une panique du robot lui-même, elle, est journalisée séparément sous panique dans une tâche planifiée. La pile n'est jointe que si Go reconnaît la panique comme une faute d'exécution (index hors bornes, carte nulle, pointeur nul) ; dans le cas plus rare d'une panique portant une autre valeur, la ligne apparaît sans pile. Si vous voyez panique dans une tâche planifiée, c'est un défaut de notre côté et non du vôtre : signalez-la, pile ou non.
IntervalExec et l'intervalle nul
IntervalExec(0, f) // ✗ ne répétera rien
Un intervalle nul ou négatif ne répète rien. Le robot l'écrit au journal plutôt que de laisser croire que la tâche tourne. Écrivez un intervalle franc.
Attendre indéfiniment
Un script qui ne fait que planifier des tâches se termine aussitôt, et ses tâches meurent avec lui. OnQuitCh sert à le retenir : ce canal se ferme quand le script s'arrête.
IntervalExec(600000, func() { Print("toutes les dix minutes") })
CronExec("@04h00", func() { Print("chaque nuit") })
<-OnQuitCh // le script attend ici jusqu'à ce qu'on l'arrête
C'est l'ossature de la plupart des scripts de surveillance : on planifie, puis on bloque.
Réagir à un événement
Un script peut être réveillé par le robot au lieu d'interroger en boucle. Il ne paie alors aucune requête pour attendre, et apprend l'attaque à l'instant où le Defender la voit, non au tour suivant.
Les fonctions attendent, avec un délai :
| Fonction | Rend | |
|---|---|---|
WaitAttack(secondes) | 2 : l'attaque et un booléen | Attend une attaque. Le booléen dit si elle est venue. |
WaitAttackGone(secondes) | 2 : l'attaque et un booléen | Attend qu'une attaque disparaisse. |
WaitChatMessage(secondes) | 2 : le message et un booléen | Attend qu'un joueur écrive. |
WaitFleet(secondes) | 2 : la flotte et un booléen | Attend qu'un worker envoie une flotte. |
WaitHunterTargetActive(secondes) | 2 : la bascule et un booléen | Attend qu'une cible revienne devant son écran. |
WaitHunterTargetIdle(secondes) | 2 : la bascule et un booléen | Attend qu'une cible le quitte. |
Les canaux, eux, ne s'appellent pas : on y reçoit. Ce sont des valeurs, à lire avec <-.
| Valeur | Porte | |
|---|---|---|
OnAttackCh | un canal d'attaques | Les attaques détectées. |
OnAttackGoneCh | un canal d'attaques | Celles qui disparaissent. |
OnStateChangeCh | un canal d'états | Les changements d'état du compte. |
OnChatMessageReceivedCh | un canal de messages | Ce que les joueurs écrivent. |
OnFleetCh | un canal de flottes | Les flottes que les workers envoient. |
OnHunterTargetActiveCh | un canal de bascules | Les cibles qui reviennent. |
OnHunterTargetIdleCh | un canal de bascules | Les cibles qui s'en vont. |
Un délai de zéro attend sans limite, jusqu'à l'arrêt du script. C'est la forme qu'on veut dans une garde qui tourne la nuit.
for {
a, vu = WaitAttack(0)
if !vu { break }
LogWarn("attaque de", a.AttackerName, "sur", a.DestinationName)
if a.ArriveIn < 120 {
LogError("impact dans", a.ArriveIn, "secondes")
}
}
Répondre à un joueur
C'est ce qu'aucune boucle ne pouvait faire.
for {
m, vu = WaitChatMessage(0)
if !vu { break } // le script s'arrête
if !m.Private() { continue } // le salon d'alliance parle beaucoup
LogInfo(m.SenderName, "écrit :", m.Text)
SendMessage(m.SenderID, "je suis occupé, écris plus tard")
}
Le message porte SenderID, SenderName, AssociationID, Text et Date, comme chez l'outil de référence. Private() est en plus : elle dit si le message vous était adressé à vous seul, et c'est la question qui décide de tout — un script qui répondrait à tout le salon d'alliance serait vite insupportable. AssociationID vaut zéro sur un message privé.
Le robot tient déjà cette connexion depuis l'ouverture du compte : le canal ne coûte aucune requête, et le message arrive dans la seconde.
Savoir qu'une flotte est partie
for {
f, vu = WaitFleet(0)
if !vu { break }
if f.Kind == "expedition" {
Print("expédition vers", f.Destination, "flotte", f.FleetID)
}
}
Kind nomme le worker qui l'envoie : repatriate, expedition, evacuation, colonization, espionage, discovery, phalanx, scheduled flight, supply, distribution, ghost. C'est par lui qu'on filtre.
Les onze, et non huit — supply, distribution et ghost manquaient à cette liste. Un filtre écrit sur une liste incomplète laisse passer précisément ce qu'il voulait écarter, et rien ne le dit.
Origin et FleetID sont parfois vides. L'espionnage et la découverte n'ont pas d'origine à donner ; c'est la position visée qui compte pour eux. Un champ vide veut dire « ce worker ne le dit pas », pas « il n'y en a pas ».
Ce canal dit qu'une flotte est PARTIE, jamais qu'elle est arrivée. C'est pourquoi il ne s'appelle pas OnRepatriateCompletedCh, que l'outil de référence déclare : le robot ne sait pas dire quand un transport se pose, et un canal nommé « completed » qui se déclencherait au départ mentirait à chaque script qui l'écoute pour compter ce qui rentre.
Guetter une cible
Les deux moments d'une surveillance, et ils s'écoutent séparément.
// Le moment d'agir : la place se libère.
e, vu = WaitHunterTargetIdle(0)
if vu { Print(e.PlayerName, "vient de partir") }
// Le moment de rentrer : il revient.
e, vu = WaitHunterTargetActive(0)
if vu { RappelerLaFlotte() }
La bascule porte PlayerID, PlayerName, AllianceTag et PreviousActivity, comme chez l'outil de référence, plus Activity et Active qui disent l'état d'après et le sens du changement.
Ce que ces nombres veulent dire. Le jeu montre 15 tant que le joueur est devant son écran, puis le nombre de minutes depuis son départ, et plus rien du tout — 0 — passé sa fenêtre d'affichage. Un réveil est donc un passage à 15 ; un départ, un passage qui en sort.
Les deux canaux ne se mélangent jamais. Sans ce partage, un script qui rappelle sa flotte au réveil d'une cible la rappellerait aussi quand elle s'en va, c'est-à-dire au pire moment.
Ils ne dépendent d'aucun réglage. L'alerte de l'interface, elle, reste sous la case du panneau ; un script à qui vous avez confié une surveillance n'a pas à attendre qu'on la coche.
OnHunterTargetIdleCh n'existe pas chez l'outil de référence, qui n'expose que le réveil. Le départ est pourtant ce que le chasseur guette, puisque c'est lui qui ouvre la fenêtre : le taire aurait été un oubli, pas une décision.
Pour dire qui surveiller, voyez Piloter les workers.
Pourquoi WaitAttack plutôt qu'un select
Le moteur de scripts n'a pas de select. L'écrire rend une erreur de syntaxe, dans toutes ses formes. C'est pourtant le motif que la documentation de l'outil de référence montre partout : recopié tel quel, il ne démarre pas.
Une réception simple, elle, fonctionne :
a = <-OnAttackCh
Print(a.AttackerName)
Mais elle bloque jusqu'à ce qu'il arrive quelque chose : pas de délai, pas de seconde condition. WaitAttack fait ce que vous auriez écrit dans un select, et rend en second un booléen. Sans lui, vous ne pourriez pas distinguer une attaque d'une absence d'attaque, la valeur nulle des deux étant la même.
Le canal se ferme quand le script s'arrête : la réception rend alors la main plutôt que de pendre.
Ce que Kepler remplit, et ce qu'il laisse vide
L'attaque rendue porte AttackerName, Origin, Destination, DestinationName, ArrivalTime, MissionType, Missiles, et ArriveIn, ce dernier calculé au moment où vous le lisez.
Restent vides, et c'est délibéré : ID, AttackerID et UnionID, que le Defender ne garde pas parce qu'il travaille sur la cible et l'heure d'impact ; et Ships, parce que le robot ne connaît la composition que sous forme de noms traduits par le jeu, jamais d'identifiants.
Un seul canal pour la disparition
L'outil de référence en a deux, OnAttackDoneCh et OnAttackCancelledCh. Kepler n'en expose qu'un, OnAttackGoneCh, parce que le robot voit qu'une attaque n'est plus là sans savoir dire si elle a frappé ou si l'attaquant l'a rappelée. Offrir leurs deux noms ferait tomber les deux sur le même événement, et un script qui écoute les deux traiterait deux fois la même attaque.
Ce qui arrive entre deux attentes n'est pas perdu
C'est le point qui rend la boucle ci-dessus honnête.
for {
a, vu = WaitAttack(0)
if !vu { break }
TraiterLAlerte(a) // deux secondes, disons
}
Pendant ces deux secondes, votre script n'attend pas. Une seconde attaque qui tomberait là est gardée, et le tour suivant la trouve : chaque fonction d'attente garde son canal d'un appel à l'autre, avec seize événements en réserve.
Elle en ouvrait un neuf à chaque appel jusqu'au 05/09/2026, et jetait donc tout ce qui arrivait dans les creux — sans rien dire.
Seize, et pas plus. Au-delà, voyez juste en dessous.
Chaque fonction garde le sien. WaitAttack et WaitChatMessage ne se réveillent pas l'une l'autre.
Si votre script ne lit pas assez vite
Les événements qu'il ne prend pas sont jetés, et le journal le dit une fois. C'est voulu : le même bus alimente l'interface de tous les comptes, et un script endormi sur son canal figerait le robot entier. Mieux vaut rater une alerte qu'arrêter la maison.
Retirer, en résumé
Pour une tâche différée ou répétée, gardez la fonction d'annulation que rendent ExecIn, ExecAt et IntervalExec. Pour une tâche à l'horaire, RemoveCron et son numéro. Et de toute façon, tout s'arrête quand le script s'arrête.