"[...]ça serait génial d'avoir un truc comme le projet Félin" dans Arma3

 

C'est à partir de cette idée de deux heures du mat qu'est venue l'idée de créer un mod pour Arma3 le FPS créé par la firme Bohemia Interactive.

 

L'objectif semblait très simple sur le papier :

  1. Rajouter les indicateurs de ses alliés dans le HUD de Arma3 ainsi que leur état de santé (pour permettre au médic de secourir plus efficacement).
  2. Rajouter les indicateurs des ennemis détectés par son avatar dans le HUD de Arma3 sur les portées courtes 100m de nuit / 300m de jour.
  3. Permettre au chef de groupe d'avoir la caméra de ses équipes.
  4. Connaître la liste des équipements/munitions/état de santé/coordonnées grille de ses alliés dans une page web en quasi temps réel.
  5. Permettre d'envoyer des messages de la page web vers le HUD de Arma3 .
  6. Transcoder les pseudonymes des joueurs dans Arma3 par rapport à leur UID steam.

 

Arrivent les questions fondamentales :

  1. Je n'y connais rien en modding ni en script sous Arma3, je commence par quoi ?
    1. Follow the white rabbit...
  2. Quelles technologies sont nécessaires pour écrire un script dans Arma3 ?
    1. Le language de script pour arma3 à savoir .sqf et du C++ nécessaire dans les fichiers .cpp
    2. La librairie C#
    3. Le client lourd de synchronisation C#
    4. La page web et l'api PHP / AJAX / XML / XSLT / HTML5 / CSS3
  3. Comment générer l'interface entre un exécutable et le jeu Arma3 ?
    1. Via le script init.sqf qui appelle la DLL en C#

Quelles sont les étapes de ce projet pour Arma3 qui va nécessiter de connaître autant de languages programmation et de script ?

  1. Création de la DLL en C# interface avec Arma3
  2. Création de l'architecture de fichier du script qui sera appelé par arma3
  3. Compilation de notre fichier C++ de .cpp vers .bin
  4. Compilation du dossier du mod en .pbo
  5. Création des fonctions de base dans la DLL
  6. (partie 2) Création de l'exécutable qui gère la synchronisation avec l'API (et donc la page web)
  7. (partie 2) Création de la page web

Nous allons dans ce premier article, définir le nécessaire pour mettre les pieds dans le monde merveilleux de la création des mods sous Arma3 avec leur syntaxe de script SQF.

Création de la DLL qui sera interrogée par Arma3

Via visual studio création d'un nouveau projet (que nous appelons ici morrigan) en Bibliothèque de classes en C#.

Création d'une DLL csharp

Puis import du package NuGet UnmanagedExports.1.2.7.

Ajout d'un package nuget

Renommer la classe native en : DllEntry

Ajouter les espaces de noms nécessaires RGiesecke.DllExport (correspond au packet nuget ajouté précédemment).

using RGiesecke.DllExport;
using System;
using System.Runtime.InteropServices;
using System.Text;

 Notre code source correspond maintenant à ça : 

using RGiesecke.DllExport;
using System;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;

namespace Morrigan
{
    public class DllEntry
    {


    }
}

On rajoute 4 fonctions que je vais détailler par la suite.

 

Point d'entrée pour connaître la version de la DLL

RvExtensionVersion qui est appelé par le jeu pour connaître la version de l'exécutable. On notera bien que nous ne faisons rien en cas d'exception de manière à éviter un plantage du jeu en cas d'erreur.

[DllExport("RVExtensionVersion", CallingConvention = CallingConvention.Winapi)]
public static void RvExtensionVersion(StringBuilder output, int outputSize)
{
	try
	{
		output.Append("Morrigan v1.0.0.0");
	}
	catch (Exception)
	{
		//RAS
	}
}

Point d'entrée pour les commandes sans arguments

RvExtension est la fonction appelée par Arma3 pour répondre à des appels de fonction ne nécessitant aucun argument dans notre script SQF.

[DllExport("RVExtension", CallingConvention = CallingConvention.Winapi)]
public static void RvExtension(StringBuilder output, int outputSize, [MarshalAs(UnmanagedType.LPStr)] string function)
{
	try
	{
		//Fonction ne nécessitant pas de paramètres
	}
	catch (Exception)
	{

	}
}

Point d'entrée pour les commandes avec arguments

RVExtensionArgs est le point d'entrée pour les commandes appelées avec un tableau d'arguments dans notre script SQF. Celle-ci retourne 0 car ce n'est pas une void mais une fonction avec un entier attendu en résultat.

[DllExport("RVExtensionArgs", CallingConvention = CallingConvention.Winapi)]
public static int RvExtensionArgs(StringBuilder output, int outputSize, [MarshalAs(UnmanagedType.LPStr)] string function, 
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr, SizeParamIndex = 4)] string[] args, int argCount)
{
	try
	{
 		return 1;     
	}
	catch (Exception)
	{
		return 0;
	}

}

Le socle de base est posé pour la DLL. Nous y reviendrons par la suite pour créer ses fonctions.

Création de l'architecture des fichiers de script du mod Arma3

Le script suivant va générer l'architecture de base des fichiers nécessaires pour le mode.

 

Respectivement :

  • morrigan
    • $PREFIX$
    • config.cpp (avec son contenu il n'y à plus qu'à compiler)
    • init.sqf
mkdir Morrigan
cd Morrigan
mkdir addons
mkdir app
cd addons
mkdir morrigan
cd morrigan
echo morrigan>$PREFIX$
(
	echo class CfgPatches
	echo {
	echo	class morrigan
	echo	{
	echo		units[]={};
	echo		weapons[]={};
	echo		requiredAddons[]={};
	echo		author="Loic Gasnier";
	echo		authorUrl="https://analyse-innovation-solution.fr";
	echo		url="https://analyse-innovation-solution.fr";
	echo	};
	echo };
	echo class CfgFunctions
	echo {
	echo 	class MGN
	echo 	{
	echo 		class MGN_Functions
	echo 		{
	echo 			class Startup
	echo 			{
	echo 				postInit=1;
	echo 				file="\morrigan\init.sqf";
	echo 				description="Initialisation";
	echo 			};
	echo 		};
	echo 	};
	echo };
	echo class cfgMods
	echo {
	echo 	author="lga";
	echo 	timepacked="1509074123";
	echo };
)>config.cpp
echo //Morrigan script init v1.0>init.sqf

Compiler le fichier .cpp en .bin pour être interprétable par Arma3

Dans un premier temps, il va falloir télécharger les outils Arma 3 tools fournis par l'éditeur via le workshop. Une fois téléchargés et installés, il suffit de se rendre dans le dossier qui contient les outils (par défaut C:\Steam\steamapps\common\Arma 3 Tools). Puis dans le dossier CfgConvert on découvre alors plusieurs outils, dont cfgConvertGUI.exe.

 

Compilation d'un fichier cpp en bin

On l'ouvre on va chercher le fichier config.cpp précédemment créer et on clique sur "Convert to BIN". On a maintenant un fichier config.bin à côté du fichier config.cpp.

Appeler la DLL via notre script init.sqf dans Arma3

Nous allons commencer par un appel simple pour connaître la version de la DLL. Dans le init.sqf on ajoute dans notre fichier :

private _rst = 'Morrigan' callExtension ['get_str_version',[]];
private _version = (_rst select 0);
systemChat format["%1",_version];

La première ligne de notre script sqf appelle la bibliothèque de fonctions Morrigan_x64.dll en appelant la fonction "get_str_version" (qui n'existe pas pour le moment) et retourne un tableau de résultats.

La seconde ligne de notre script retourne l'index 0 du tableau retourné (qui va correspondre aux informations de version de la dll).

La troisième ligne de notre script affiche les informations de version dans le système de communication d'Arma3.

Compiler le mod en .pbo

Arma3 attend des fichiers en .pbo pour pouvoir utiliser le mod. Il faut tout d'abord télécharger l'outil pbo manager puis l'installer. Une fois terminé, le menu contextuel de Windows sur les dossiers dispose de l'entrée "PBO manager". On clique donc sur Pack into morrigan.pbo.

Compilation d'un .pbo

 Le dossier est maintenant compilé en un seul fichier .pbo. Nos scripts sont donc compilés et lisible par Arma3.

Créer la fonction get_str_version dans la DLL en C#

Nous rajoutons une nouvelle classe dans la solution "Orders_Functions"

Création d'une classe en csharp

On ajoute simplement le code suivant puis on constate que le constructeur prend deux paramètres : le nom de la fonction à appeler et un tableau d'arguments.

using System;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml.Linq;
using System.IO;
using System.Collections.Generic;
using System.Threading;
using System.Diagnostics;
using System.Text.RegularExpressions;


namespace Morrigan
{
    public class Orders_Functions
    {
        string _version = "1.0.0.0 (Update 31/10/2020 18:32)";
        private string _Name_Payload = String.Empty;
        private string[] _Args = new string[] { };
        private string _Config_Path = String.Empty;


        //Constructeur 
        public Orders_Functions(string in_function, string[] in_args = null)
        {
            try
            {
                this._Name_Payload = in_function.ToLower();
                if (in_args != null) this._Args = in_args;
                this._Config_Path = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "config.xml");
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        public int Execute(StringBuilder in_output)
        {
            try
            {
                switch (this._Name_Payload)
                {
                    case "get_str_version":
                        in_output.AppendLine("version " + _version);
                        break;
                    default:

                        break;
                }

                return 1;
            }
            catch (Exception)
            {
                return 0;
            }
        }
	}
}

Il faut maintenant alimenter les deux classes : RVExtensionArgs et RVExtension pour qu'elles appellent notre objet avec et sans paramètres.

 

[DllExport("RVExtension", CallingConvention = CallingConvention.Winapi)]
public static void RvExtension(StringBuilder output, int outputSize, [MarshalAs(UnmanagedType.LPStr)] string function)
{
	try
	{
		Orders_Functions Order = new Orders_Functions(function,null);
		Order.Execute(output);
	}
	catch (Exception)
	{

	}

}

[DllExport("RVExtensionArgs", CallingConvention = CallingConvention.Winapi)]
public static int RvExtensionArgs(StringBuilder output, int outputSize, [MarshalAs(UnmanagedType.LPStr)] string function,
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr, SizeParamIndex = 4)] string[] args, int argCount)
{
	try
	{
		Orders_Functions Order = new Orders_Functions(function, args);
		return Order.Execute(output);
	}
	catch (Exception)
	{
		return 0;
	}

}

On définit le build pour les plateformes x64 et en chemin de sortie l'emplacement de notre module : T:\Mods arma3\Morrigan\ on génère la solution. On arrive donc à la structure de fichiers suivante :

  • Morrigan
    • Morrigan_x64.dll
    • Morrigan_x64.pdb
    • app
    • addons
      • morrigan
        •  config.bin
        • config.cpp
        • init.sqf
        • $PREFIX$
      • morrigan.pbo

À partir de là, nous pouvons lancer le jeu  Arma3 et lui assigner notre mode manuellement.

 

Mod dans le launcheur arma3