"[...]ç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
    • images
    • scripts
    • sounds
      • effects
      • musics
      • vox
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 };
	echo class CfgSounds {
	echo 
	echo 	class Demo_Sound
	echo 	{
	echo 	   name = "demo_sound";
	echo 	   sound[] = {"\morrigan\sounds\effects\demo.ogg", 1, 1}; 
	echo 	   titles[] = {};
	echo 	};
	echo };
	echo 
	echo class CfgMusic
	echo {
	echo 	tracks[]={};
	echo 	class Demo_Music
	echo 	{
	echo 		name = "demo_music_01";
	echo 		sound[] = {"\morrigan\sounds\musics\demo_music_01.ogg", db+0, 1.0};
	echo 	};
	echo };
	
)>config.cpp
echo //Morrigan script init v1.0>init.sqf
mkdir images
mkdir scripts
mkdir sounds
cd sounds 
mkdir effects
mkdir musics
mkdir vox

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

Ajout des améliorations spécifiques au module Arma3 :

 

Pour la suite nous allons utiliser le module CBA qui permet de gagner énormement de temps pour la création des actions communes à la majorité des modules.

Définir les actions suivant l'instance qui éxécute le script init.sqf d'initilisation du module

Revenons à notre fichier de script Arma3 init.sqf :

Nous allons commencer par définir comment réaliser une action si le script est éxécuté sur le serveur dédié/non dédié ou sur le client (joueur) :

if (isServer) exitWith {
	if (isDedicated || !hasInterface) exitWith {
		//Action à réaliser si serveur dédié sans interface utilisateur 
	}else{
		//Action à réaliser si serveur non dédié 
	};
}else{
	//Action à réaliser si client (joueur)
};


Action à réaliser quand un joueur spawn

Ensuite nous allons ajouter un événement à réaliser à chaque "spawn" du joueur.

if (isServer) exitWith {
	if (isDedicated || !hasInterface) exitWith {
		//Action à réaliser si serveur dédié sans interface utilisateur 
	}else{
		//Action à réaliser si serveur non dédié 
	};
}else{
	//Action à réaliser si client (joueur)

	[] spawn {
		//Action à réaliser à chaque spawn
	};
};

Nous allons maintenant créée une fonction "respawn" à appeler à chaque spawn du joueur. Cette fonction contiendra un bout de script qui permettra de dessiner à l'écran si l'instance du joueur est non null.

private _mod_name = 'Morrigan';

if (isServer) exitWith {
	if (isDedicated || !hasInterface) exitWith {
		//Action à réaliser si serveur dédié sans interface utilisateur 
	}else{
		//Action à réaliser si serveur non dédié 
	};
}else{
	//Action à réaliser si client (joueur)
	
	//Affiche le numéro de version
	private _rst = _mod_name callExtension ['get_str_version',[]];
	private _version = (_rst select 0);
	systemChat format["%1",_version];

	[] spawn {
		//Code à  appeler chaque spawn
		[] call respawn;
	};
	
	
	respawn = {
		//Code de la fonction de réaparition
		missionNamespace setVariable ["morrigan_handler_draw_3d",false];
		
		//Supprime l'instance de l'événement 3D si éxistant pour éviter le doublonnage de la création des objets 3D à l'écran 
		private _handler_draw_3d = missionNamespace getVariable "morrigan_handler_draw_3d";
		if(_handler_draw_3d != false) then { 
			[_handler_draw_3d] call CBA_fnc_removePerFrameHandler;
		};
		
		//Génère un objet PFH qui éxécutera un code à chaque image
		_handler_draw_3d = [ 
		{ 	
			 if(!isNull player) then {    
			 
			 };
		}, 
		0
		] call CBA_fnc_createPerFrameHandlerObject; 
			
		//Sauvegarde l'instance de l'événement 
		missionNamespace setVariable ["morrigan_handler_draw_3d",_handler_draw_3d];
	};
};



Dessiner des marqueurs sur les membres de l'équipe à l'écran

On commence par réaliser une groupe sur les membres de l'équipe du joueur à l'aide de units group player on parcours l'ensemble des membres ici appelé _x en excluant l'unité du joueur du lot. On récupère les informations que nous souhaitons (nom, position, distance,etc). Et on dessine enfin le tout à l'aide de la fonction drawIcon3D.

//Pour chaque membre de l'équipe du joueur 
_fire_squad_members = units group player;
{   
	//Si l'unité n'est pas le joueur 
	if(_x != player) then {
		//Le nom de l'unité 
		private _member_name = _x call BIS_fnc_getName; 
		
		//La position des yeux l'unité dans le référentiel 
		private _positions =  (ASLToAGL eyePos _x);
		
		//La position au dessus de la tête du personnage (avec un léger décalage) 
		private _top_position =[_positions select 0,_positions select 1,(_positions select 2)+0.55]; 
		
		//La position du centre du torse de l'unité 
		private _body_position = ASLToAGL getPosASL _x;
		
		//La distance en M entre le joueur et l'unité 
		private _distance = player distance _x;
		
		//L'icone qu'on utilisera comme marqueur à placer 
		private _icon = "a3\ui_f\data\gui\cfg\ranks\general_gs.paa";
		
		//La couleur du marqueur 
		private _color_rgba = [0,1,1,0.3];
		
		//La taille du marqueur 
		private _icon_size = 0.5;
		
		drawIcon3D [_icon, _color_rgba, _pos, _icon_size, _icon_size, 0, _member_name, 2, _font_size, 'PuristaLight','center',true];
	}; 
} forEach _fire_squad_members;  

On recompile notre script arma3 dans notre fichier pbo. On relance le jeu et les indicateurs sont bien présent à l'écran (attention cependant ils ne tiennent pas compte des spécialités, ne transcodent pas les noms des joueurs, on est sur des données brutes pour le moment).