Création d’un ruleset FG – Partie 2 – Scripting

Vue d’ensemble

Scripter est ce qui permet d’effectuer une logique d’interface avancée et d’automatiser le système de jeu dans le cadre d’un ruleset. Les scripts peuvent être utilisés pour étendre les capacités de n’importe quelle windowclass ou de contrôle, en plus de prendre en charge les paquets globaux pour la logique partagée.

Les scripts Lua peuvent être soit globaux (vous les trouverez dans « base.xml », soit attachés à une windowclass ou à un contrôle. Les scripts globaux doivent être identifiables (en gros avoir un nom) et peuvent être utilisés dans toutes les autres occurrences de scripts. 

Chaque fenêtre reçoit une instance propre du script Lua attaché à sa windowclass. Pareil pour les contrôles. De plus, le client FG fournit un certain nombre de scripts globaux intégrés pour permettre l’interaction avec le client (Common, DB, Debug, …). 

Si un bloc de script est défini dans le cadre d’une définition d’objet, alors le code fonctionne dans le contexte de cet objet FG, et toute API associée à cet objet FG peut être appelée directement.

En outre, il existe un certain nombre d’objets qui peuvent être demandés par l’API FG et avec lesquels il est possible d’interagir en dehors des instances et des contrôles. Il s’agit notamment des nœuds de base de données, des objets à glisser/déposer (dragdata), des tokens et des widgets.

Fantasy Grounds utilise le langage de script Lua. Chaque objet de l’interface FG ou accessible par l’API FG a une API Lua spécifique qui est définie, et quasi chaque objet de l’API FG peut être créé dynamiquement par des scripts.

Utilisation de la référence de l’API du ruleset

La documentation de référence des rulesets détaille chacun des objets FG scriptables, ainsi que les paquets globaux intégrés au client FG. 

Chaque objet et paquet possède un ensemble unique d’API correspondant au type d’objet ou de paquet. On fera un glossaire à la fin de cette partie.

Voici comment les sections d’API sont réparties :

  • Paquets (packages) : Paquets globaux intégrés.
  • Objets (objects) : Objets FG Lua accessibles uniquement par l’API.
  • Éléments : Objets FG Lua qui peuvent être définis comme des actifs ou des contrôles, et qui permettent de fournir des environnements de scripting.
  • Actifs et paramètres : Autres actifs ou paramètres qui peuvent être définis dans un ruleset.

Utilisation de scripts

Les blocs de script définis dans les rulesets peuvent être contenus à trois endroits : les windowclass, les contrôles et les script globaux.

Dans tous les cas, les blocs de script sont définis à l’aide de balises <script>. Le corps du bloc de script peut être spécifié comme le contenu textuel de la balise, ou dans un fichier externe auquel il est fait référence à l’aide de l’attribut « file ». Par exemple : 

<script file="campaign/scripts/charselect_host_list.lua" />

Si le script est donné comme le contenu textuel de la balise, le bloc entier est traité comme une seule ligne en raison des détails du traitement XML. Dans ce cas, les restrictions suivantes s’appliquent.

  • Les messages d’erreur imprimés sur la console pointeront toujours vers la ligne 1, ce qui les rend généralement moins utiles
  • La syntaxe de commentaire « –[ ] » doit être utilisée à la place de la syntaxe « –« .
  • Les caractères spéciaux XML tels que « < » et « > » doivent être échappés pour ne pas interférer avec le traitement XML

Scripts dans les windowclass

Les blocs de script des windowclass sont appliqués à chaque instance de fenêtre créée en fonction de la classe, et étendent l’interface FG Lua d’icelle. 

La balise de script doit être située comme un enfant direct de la balise <windowclass>, comme l’illustre l’exemple suivant (« campaign/campaign_chars.xml », définition de la windowclass « charselect_host_entry ») : 

    <windowclass name="charselect_host_entry">
        <frame>charselectentry</frame>
        <sizelimits>
            <maximum width="300" />
        </sizelimits>
        <script file="campaign/scripts/charselect_host_entry.lua" />
        <sheetdata>
        …
        </sheetdata>
    </windowclass>

Scripts dans les contrôles

Les blocs de script pour les contrôles individuels sont appliqués au contrôle dans chaque instance de fenêtre en fonction de la windowclass qui les contient. 

Les scripts de contrôle étendent l’interface du type de contrôle. La balise <script> doit être un enfant direct de la balise de définition du contrôle. Par exemple, dans la même windowclass, le bouton export est un contrôle auquel on adjoint un script de cette façon : 

<button_exportchar name="iexport">
    <anchored to="base" position="insidebottomright" offset="10,10" />
    <script>
          function onButtonPress()  
             CampaignDataManager.exportChar(window.getDatabaseNode());
          end
    </script>
</button_exportchar>

La fonction est évènementielle : quand on clique sur le bouton avec la petite flèche bleue (onButtonPress), on fait appelle à la fonction exportChar du paquet CampaignDataManager, en lui passant en paramètre le résultat de la fonction « window.getDatabaseNode() » qui donne l’objet courant (ici le perso à exporter). 

Scripts globaux (packages)

Les paquets sont des constructions de script génériques accessibles dans le ruleset, similaires aux paquets de la bibliothèque standard Lua. Ils sont accessibles à partir d’autres blocs de script par leur nom, qui doit être défini par l’attribut « name » de la balise <script>. La spécification du nom n’est pas obligatoire – s’il est omis, le contenu du script peut toujours être exécuté à l’aide de la fonction onInit. Mais c’est bien plus pratique. L’exemple précédent fait référence à un paquet « CampaignDataManager », qui est défini dans le fichier base.xml à la ligne 49: 

<script name="CampaignDataManager" file="scripts/manager_campaigndata.lua">

Portée des blocs de script

La portée globale de l’environnement de script n’est pas accessible aux scripts définis par l’utilisateur. Au lieu de cela, chaque bloc de script reçoit son propre environnement et le bloc entier est évalué et exécuté en traitant cet environnement comme global. 

Cela signifie que toutes les fonctions et variables définies dans un bloc de script peuvent être utilisées à l’intérieur du bloc comme si elles étaient globales, et que tous les autres blocs de script sont incapables de modifier directement les variables d’un autre bloc.

Pour accéder à d’autres blocs de script, tels que d’autres contrôles dans la même fenêtre, de nombreux environnements de blocs de script contiennent des variables spéciales qui peuvent être utilisées pour accéder à des environnements connexes. 

Ces variables sont détaillées pour chaque élément séparément dans la documentation de référence, et le seront dans le glossaire de l’API, mais les plus courantes sont résumées ci-dessous.

  • [nom_commande] = Les environnements de script relatifs à une fenêtre ont une variable nommée de la même manière qu’un contrôle pour chacun de ses contrôles pointant vers l’environnement du bloc de script du contrôle (à condition que le contrôle soit nommé).
  • window = Les environnements de script des contrôle ont une variable appelée « window » pointant vers l’environnement de la fenêtre parente.
  • windowlist = Les contrôles de type windowlist ont une variable appelée « windowlist » pointant vers l’environnement du contrôle d’icelle.
  • parentcontrol = La fenêtre contenue dans un contrôle (subwindow) a une variable appelée « parentcontrol » pointant sur l’environnement du contrôle conteneur.
  • subwindow = Le contrôle conteneur (subwindow) contenant une fenêtre a une variable « subwindow » pointant sur l’environnement de la fenêtre contenue.
  • super = Lorsque des scripts sont superposés (layered), la variable « super » est définie et peut accéder à un script sur lequel un objet est basé. 
  • self = fait toujours référence à l’objet le plus haut placé programmatiquement , même s’il est appelé depuis une couche inférieure.
  • [package_name] = Tout package intégré (voir référence API FG) ou tout script global défini pour le ruleset.

Programmation événementielle

De nombreux éléments contiennent des fonctions d’événement, indiquées comme telles par la dénomination « événement » dans le document de l’API. Ces fonctions sont automatiquement appelées par le système lorsque certains événements se produisent.

Pour tirer parti des événements, il faut créer un bloc de script étendant l’élément et définissant la fonction d’événement. Si la fonction est présente, elle est appelée lorsque les conditions de l’événement sont remplies. Aucune autre action ne doit être entreprise pour que l’événement fonctionne.

Par exemple, la fonciton « onButtonPress » de l’exemple précédent (le contrôle « export » de la séléction des PJ) est une fonction évènementielle, qui se déclenche quand on appuie sur le bouton export. Généralement les noms de ces fonctions sont explicites sur leur déclencheur. En voici une petite liste qui regroupe les fonctions les plus courantes :

  • onDrop : quand on dépose un objet sur le contrôle. Par exemple dans la windowclass « char_attribute » du CoreRPG on a la fonction onDrop qui sert à attribuer un certain nombre de dés sur un attribut : 
function onDrop(x, y, draginfo)
  local sDragType = draginfo.getType();
  if sDragType == "dice" then
    local aDropDice = draginfo.getDieList();
    for _,vDie in ipairs(aDropDice) do
      dice.addDie(vDie.type);
    end
    return true;
  end
end

Les arguments passes à la fonction sont la position de l’élément sur le bureau (x,y) et les données déposées (draginfo). De là le code récupère le type de la données déposée, contrôle que les données sont bien de type « dice » (des dés quoi) et il ajoute les dés en question à l’attribut si c’est bien le cas

  • onDragStart : quand on a commencé à glisser des données (quand la souris bouge), par exemple, toujours pour les attributs de la fiche de perso, quand on sélectionne les dés et qu’on les glisse : 
function onDragStart(button, x, y, draginfo)  
  if label.isEmpty() and dice.isEmpty() and bonus.getValue() == 0 then
    return nil;
  end

Ce premier contrôle regarde si toutes les valeurs glissées sont vides et le cas échéant termine l’opération. Ensuite, le code va vérifier que la variable dice n’est pas vide. Si elle l’est, elle va juste placer dans les données transportée la description, le nom de l’attribut et la valeur du bonus attribué :

if dice.isEmpty() then
  draginfo.setType("number");
  draginfo.setDescription(label.getValue());
  draginfo.setStringData(label.getValue());
  draginfo.setNumberData(bonus.getValue());

Si la variable dice contient effectivement des objet « dés », elle va créer un objet rRoll qui contiendra les informations précédentes et le donner en paramètre à la fonction performAction du paquet ActionsManager, et ce sont les données qui seront passées à la fonction onDrop qui recevra les jolis dés que vous verrez apparaître sur votre curseur.

 else
   local rRoll = { sType = "dice", sDesc = label.getValue(), aDice = dice.getDice(), nMod = bonus.getValue() };
   ActionsManager.performAction(draginfo, nil, rRoll);
 end
 return true;
end
  • onDoubleClick : ce qui se produit quand vous double-cliquez. Toujours dans le même bloc : 
function onDoubleClick(x, y)  
  if dice.isEmpty() then
    ModifierStack.addSlot(label.getValue(), bonus.getValue());
  else
    local rRoll = { sType = "dice", sDesc = label.getValue(), aDice = dice.getDice(), nMod = bonus.getValue() };
    ActionsManager.performAction(nil, nil, rRoll);
  end
  return true;
end

Si vous double cliquez sur un attribut de la fiche sans dés il est ajouté sur la pile des modificateurs, sinon les données sont passées en paramètre à la fonction performAction du paquet ActionsManager. Notez que comme il ne s’agit pas d’une fonction relative à du glisser déposer, le jet est ici envoyé automatiquement au lieu d’être suspendu.

  • onGainFocus/onLoseFocus : quand l’élément devient actif / inactif (quand vous cliquez sur un champ pour changer sa valeur typiquement, il devient le champ actif, quand vous appuyez sur tab pour passer au suivant, il devient inactif. Par exemple dans « campaign/record_char_main.xml », quand vous cliquez sur le champ « bonus », il passe en surbrillance grâce à cette fonction :
function onGainFocus()
  window.setFrame("rowshade");
end

Et il perd sa surbrillance grâce à cet appel : 

function onLoseFocus()
  window.setFrame(nil);
end
  • onInit : cette fonction se lance lorsqu’on instancie un élément. Par exemple dans le script « campaign/scripts/encounter_npc.lua », la fonction onInit définit ce qui va se passer à l’ouverture d’une rencontre préparée : 
function onInit()
    synchToCount();
    synchTokenView();
end

Ici, on voit que la fonction synchToCount est appellée à l’ouverture de la fenêtre de rencontre (elle va permettre de synchroniser le nombre de cartes sur lesquels des éléments de la rencontre sont placés), puis la fonction synchTokenView (qui permet d’afficher les token sur la carte selon qu’on soit MJ ou PJ).

  • onLockChange : permet de définir des actions quand le verouillage d’un élément est activé. Par exemple dans « campaign/scirpts/parcel.lua », la fonction passe tous les éléments en readonly et rend invisible ce qui est vide : 
function onLockChanged()
    if header.subwindow then
        header.subwindow.update();
    end
 
    local bReadOnly = WindowManager.getReadOnlyState(getDatabaseNode());
    
    if bReadOnly then
        coins_iedit.setValue(0);
        items_iedit.setValue(0);
    end
    coins_iedit.setVisible(not bReadOnly);
    items_iedit.setVisible(not bReadOnly);
 
    coins.setReadOnly(bReadOnly);
    for _,w in pairs(coins.getWindows()) do
        w.amount.setReadOnly(bReadOnly);
        w.description.setReadOnly(bReadOnly);
    end
 
    items.setReadOnly(bReadOnly);
    for _,w in pairs(items.getWindows()) do
        if w.count then
            w.count.setReadOnly(bReadOnly);
        end
        w.name.setReadOnly(bReadOnly);
        w.nonid_name.setReadOnly(bReadOnly);
    end
end
  • onUpdate : survient lorsqu’un élément est mis à jour. Par exemple quand vous passez un élément du statut identifié à non identifié, voici le code qui est exécuté (« campaign/scripts/button_record_isidentified.lua » :
function onUpdate()
    if bUpdating then
        return;
    end
    bUpdating = true;
    local nValue = DB.getValue(nodeSrc, "isidentified", nDefault);
    if nValue == 0 then
        setValue(0);
    else
        setValue(1);
    end
    bUpdating = false;
end

L’élément “isidentified »  du nœud local de la base de données (par exemple l’item courant que vous ne voulez pas que vos joueurs identifient comme le sceptre de Rascarkapac ou inversement) est stocké dans la valeur nValue, et sa valeur passée à « l’inverse » de la valuer précédente (0 si c’est non identifiée, 1 si c’est identifié).

  • onMenuSelection : permet de définir ce qui va se passer quand vous cliquez sur des éléments du menu. Par exemple dans « ct/ct_host.xml », la windowclass « ct_target » définit le fonctionnement du bouton permettant de gérer les cibles d’une créature. Elle contient le bloc suivant : 
function onMenuSelection(selection, subselection)
  if selection == 4 then
  if subselection == 3 then
    TargetingManager.setCTFactionTargets(window.getDatabaseNode());
  elseif subselection == 5 then
    TargetingManager.setCTFactionTargets(window.getDatabaseNode(), true);
    end
  end
end

Pour la séléction 4 (le bouton Targetting), il y a deux sous menus 3 (Target all allies) et 5 (Target all non allies). Le sous menu 3 lance la fonction setCTFactionTargets du paquet TargetingManager avec en paramètre le nœud courant dans la base de données (le combat tracker et les créatures qu’il contient), le sous menu 5 également mais avec en prime l’option True (qui permet à la fonction en quesiton de discriminer les créatures considérées comme « friendly » pour les PJ).

  • onValueChanged : permet de définir ce qu’il se passe lors d’un changement de valeurs. Par exemple dans la windowclass « char_attribute » de la page « campaign/ record_char_main.xml », si on met à jour la valeur de dés d’un attribut et qu’il se retrouve à 0, on rend invisible l’élément dé (ce qui est très contre-intuitif pour des gens qui ne connaissent pas le soft et ne vont jamais penser à glisser des dés sur un champ qui ne les y invite pas remarquez ;]) : 
function onValueChanged()
  setVisible(not isEmpty());
end
  • onButtonPress : vu plus haut, ce qui arrive quand on clique sur un bouton.
  • onValue : utilisée principalement pour les dés, définit ce qu’il se passe quand le résultat du dé est instancié. Vous verrez ça dans les dés customs, par exemple pour le d3 vu plus haut :
<customdie name="d3">
  <model>d6</model>
  <menuicon>customdice</menuicon>
  <script>
  function onValue(result)
    return math.ceil(result/2);
  end
  </script>
</customdie>
  • onClickDown/onClickRelease : Ce qu’il se passe quand on appuie et relache un clic. Pour quand on appuie, ça renvoie true en général, mais des fois ça permet de modifier une variable. La plupart du temps c’est quand même onClickRelease qui va être rempli soyons franc. Par exemple dans le template « portrait_charlocal » , on a le bloc suivant qui va permettre d’ouvrir la fenetre de selection des portraits au moment du clic sur l’icône de portrait : 
function onClickDown(button, x, y)
  return true;
end
                
function onClickRelease(button, x, y)
  if not window.getDatabaseNode().isReadOnly() then
    local nodeChar = window.getDatabaseNode();
    if nodeChar then
      local wnd = Interface.openWindow("portraitselection", "");
      if wnd then
        wnd.SetLocalNode(nodeChar);
      end
    end
  end
end
  • onHover : ce qu’il se passe quand vous passez par-dessus un élément. Par exemple quand vous passer par-dessus le nom d’un item dans la fenêtre idoine, ça souligne le nom : 
function onHover(bHover)
    setUnderline(bHover, -1);
end

Utilisation de « handler » (gestionnaire en français)

Certaines interfaces de script définissent des fonctions de gestionnaire, identifiées par la dénomination « handler » dans la documentation de référence. 

La définition que vous trouverez dans le wiktionnaire est : 

module, sous-routine gérant une situation particulière comme une exception dans un processus

Un handler est différent d’un événement à trois égards 

  • Premièrement, il n’y a pas de limite au nombre de scripts qui peuvent recevoir un événement unique déclenché par un handler. 
  • Deuxièmement, un handler nécessite une déclaration explicite pour fonctionner sur une routine définie par l’utilisateur. 
  • Troisièmement, la fonction handler définie par l’utilisateur n’a pas besoin d’être située dans le bloc de script étendant un objet définissant une fonction de gestionnaire, mais peut se trouver dans n’importe quel environnement de script.

Pour enregistrer des handler sur une fonction définie par l’utilisateur, utilisez l’opérateur d’affectation comme s’il s’agissait d’une variable de fonction. 

L’exemple suivant définit une fonction de gestionnaire (updateOwner) et définit le handler « onObserverUpdate» d’un nœud de base de données pour qu’il pointe sur cette fonction :

function updateOwner()
    local sOwner = getDatabaseNode().getOwner();
    if sOwner then
        owner.setValue(Interface.getString("charselect_label_ownedby") .. " " .. sOwner);
    else
        owner.setValue("");
    end
end

function onInit()
...
    node.onObserverUpdate = updateOwner;
    updateOwner();
...
end

(il s’agit de la fonction qui permet d’assigner un propriétaire à un PJ dans « campaign/ scripts/charselect_host_entry.lua » pour les curieux)

Techniquement, la routine effectuée n’est pas une opération régulière. Un handler peut recevoir une fonction handler pour chaque environnement de bloc de script. Par conséquent, un nœud de base de données unique pourrait être surveillé par un certain nombre de fonctions individuelles différentes, par exemple dans des contrôles différents.

L’affectation est effectuée sur la base de l’environnement de la fonction assignée comme handler. Cela conduit à une façon simple d’utiliser les gestionnaires dans la plupart des cas. 

Un cas requiert cependant une attention particulière : celui où le handler doit être réinitialisé, c’est-à-dire supprimé. De telles situations doivent être traitées en assignant une autre fonction pour recevoir les événements du handler. En général, cette fonction est une fonction qui ne fait rien. La façon la plus simple d’y parvenir est de passer dans une fonction en ligne.

Pour reprendre l’exemple du wiki officiel (parce que j’en trouve pas de non vides dans CoreRPG) : 

node.onUpdate = function() end;

Les Registres

Deux registres sont disponibles dans l’environnement de script. Ce sont des « bases de données » persistantes qui sont stockées lorsque la session de campagne est fermée, et restaurées lorsqu’elle est redémarrée. 

Toutes les données des registres sont spécifiques à l’installation locale et ne sont pas transmises par le réseau aux utilisateurs connectés. Cela fait que les registres sont privilégiés pour stocker les données de préférences des utilisateurs et les données relatives à l’état des constructions de script. En gros les données volatiles.

Les registres peuvent contenir des tableaux, des nombres, des chaînes de caractères et des booléens. Les autres types de données ne seront pas conservés, et leur utilisation pourrait entraîner un comportement imprévisible.

Le registre de campagne (CampaignRegistry) est spécifique à une campagne, et n’est pas chargée ou visible à partir d’autres campagnes. Le registre de la campagne est l’emplacement préférée pour toute donnée qui est liée à la base de données de la campagne ou qui fonctionne à partir de celle-ci. Il est également entièrement spécifique à et contrôlé par le ruleset utilisées dans la campagne.

Le registre global (GlobalRegistry) est partagé par tous les rulesets et les campagnes utilisés avec l’installation sur un seul ordinateur.

Attention : Lorsque vous modifiez le registre global, pour éviter toute interférence avec d’autres ruleset, essayez toujours d’inclure une sous-table identifiant le ruleset ou le contexte dans lequel les valeurs du registre global sont stockées. Une pratique consiste à créer une table identifiée avec la clé obtenue par la fonction API User.getRulesetName et à utiliser cette table pour les données réelles.

Changements de lua spécifiques au sandbox de Fantasy Grounds

Comme la plupart des environnements de script intégrés dans les applications, l’environnement de script Lua de Fantasy Grounds a un accès limité aux variables, fonctions et bibliothèques Lua qui peuvent poser des problèmes de sécurité. 

Les variables et fonctions globales suivantes ne sont PAS disponibles dans le sandbox FG :

  • _G
  • dofile
  • getfenv
  • getmetatable
  • load
  • loadfile
  • rawequal
  • rawget
  • rawset
  • setfenv
  • setmetatable

Les bibliothèques standard suivantes ne sont également PAS disponibles dans le sandbox FG :

  • io
  • os (sauf os.clock, os.date, os.time, et os.difftime)
  • debug
  • packet

Les fonctions suivantes ont été modifiées dans le sandbox FG :

  • print – Envoi de la sortie vers la console d’application FG
  • type – Étendu pour couvrir les types d’objets FG Lua

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *