DigitalPulseSoftware / BurgWar

Burg'war est un jeu de plateforme/combat multijoueur en 2D écrit en C++17/Lua avec mon propre moteur de jeu : Nazara Engine.
MIT License
52 stars 9 forks source link

Discussions sur le modding #61

Open SirLynix opened 3 years ago

SirLynix commented 3 years ago

Depuis hier, Burg'War supporte le modding, c'est-à-dire un ensemble cohérent de fonctionnalités qui viennent s'ajouter (ou non) au jeu.

Le fonctionnement actuel est le suivant :

  1. Au lancement du jeu, la liste des mods est chargée en explorant le dossier de modding (par défaut mods/) et en essayant de charger le info.json de chaque mod (un dossier = un mod différent, le nom du dossier étant l'ID interne du mod), pour l'instant ce fichier info.json contient le nom, la description et l'auteur du mod.
  2. Au lancement d'un serveur, un certain nombre de mods peut être activé dans un ordre particulier (actuellement tous les mods sont chargés dans un ordre indéterminé, mais ça va évoluer).
  3. Lorsqu'un mod est activé, le contenu de ses dossiers assets et scripts vient se superposer au contenu des dossiers assets et scripts du jeu (via le système de fichier virtuel de Burg'War).

Cela signifie que les mods peuvent rajouter des fichiers Lua dans autorun, des entités, des modes de jeux et des armes (et à terme pourquoi pas des maps), ils peuvent également rajouter tous les assets qu'ils souhaitent.

En plus de ça, ils peuvent override des fichiers du jeu (changer un asset, un script, etc.), et par extension des fichiers d'autres mods (le dernier mod à être chargé est prioritaire).

Ça a été très simple à implémenter, et c'est un début, mais ça n'est pas optimal.

Voici une liste des problématiques dont j'aimerai parler dans ce fil de discussion :

Le support des dépendances

Actuellement les mods n'ont aucune dépendance.
Il pourrait être utile de permettre à un mod B de dépendre du mod A, afin de ne pas charger le mod B si A n'est pas présent (et logger une erreur du coup).

Quid des dépendances cycliques ? Si un gros mod est séparé en plusieurs petits mods, est-ce qu'on autorise le cas où A dépend de B et B dépend de A (pour que les deux ne chargent que si les deux sont présents).

L'override des fichiers

Chacun ayant développé des mods sur GMod se souvient de la règle "nomme tes fichiers de façon unique pour qu'ils ne se fassent pas réécrire par un autre mod", c'était vrai pour les assets, les scripts, etc.

C'est le fonctionnement actuel des mods de Burg'War, mais comme on me l'a fait remarquer on peut faire mieux que ça.

On pourrait faire en sorte que les assets et scripts de chaque mod soit propre à celui-ci, et que les fonctions interagissant avec un path fonctionnent de façon contextuelle.

Par exemple, si le mod X appelle la fonction assets.GetTexture("box.png"), le jeu va d'abord chercher dans le dossiers mods/X/assets/ le fichier box.png, et s'il ne le trouve pas va remonter jusqu'au dossier assets du jeu.
Avec une gestion des dépendances, on peut également avoir le mod Y cherchant dans son propre dossier, puis dans le dossier de chacune de ses dépendances (X) puis enfin dans le dossier du jeu.

Bien sûr du point de vue du script, cela serait totalement transparent.

Conséquences de ce fonctionnement:

  1. Si deux mods indépendants proposent un asset avec un nom identique, celui-ci ne causera pas de conflit, chaque mod étant capable de retrouver son propre asset.
  2. L'override d'asset/de scripts n'est plus possible, ce qui pourrait être intéressant à autoriser par la suite, en le rendant explicite par exemple (comment ?).
  3. Il faut revoir la façon dont les assets et scripts sont partagés avec le client pour que ça soit possible techniquement, les caches d'assets et de scripts ne seront toutefois pas affectés (grâce au hash qui fait partie de leur nom).

Un contexte Lua par mod ?

Pour rendre les mods parfaitement indépendants, il faudrait également soit interdire la modification des états globaux de Lua (définition ou override d'une variable globale), soit faire un contexte Lua (un lua_State*) par mod.
Cette seconde approche est la plus safe, mais également la plus coûteuse en mémoire (puisque chaque fonction du jeu doit alors être enregistrée pour chaque mod).

En revanche, séparer les contextes Lua pourrait ouvrir la voie à du multithreading (chaque contexte Lua étant de base monothreadé) par la suite.

L'ordre de chargement des mods

J'avais dans l'idée de proposer une interface à la RimWorlds pour activer/désactiver/paramétrer les mods, et potentiellement changer l'ordre dans lequel ils seraient chargés.

Cela a beaucoup moins d'importance si les mods sont totalement indépendants, excepté peut-être pour l'override de fichiers.

Les permissions des mods

Un point important du modding est également de ne pas laisser les mods faire n'importe quoi. Par exemple dans l'état actuel un mod pourrait utiliser les fonctions d'I/O de Lua pour créer / supprimer des fichiers sur l'ordinateur du joueur, ce qui est évidemment une faille de sécurité dont certains pourraient profiter si le jeu se popularise.

Pour contrer cela, chaque fonction Lua exposée depuis le C++ devrait être associée à un type de permission. Par exemple "filesystem" pour accéder au système de fichier directement, "assets" pour charger des assets, etc.
Les permissions requises par un mod seraient enregistrées dans le manifeste.

Le joueur pourrait ensuite accepter ou refuser, par mod, les permissions demandées.
Ce fonctionnement serait proche de ce que les OS téléphone proposent.

Les mods client-side

Il serait bon à terme d'autoriser des mods n'ayant d'existence que côté client, ceux-ci pourraient ajouter des fonctionnalités intéressantes en terme d'affichage, de statistiques, etc.

Néanmoins, pour éviter la triche, il faudrait que le serveur puisse autoriser ou interdire la présence de mods clients (en fonction du mode de jeu par exemple).

On pourrait aussi imaginer une façon plus précise d'autoriser ou interdire les mods clients, en définissant côté serveur une liste de permissions autorisées pour les mods clients (par exemple, on pourrait interdire la permission "input" nécessaire pour toucher au système d'inputs du jeu, bloquant les auto-aim).

Cela demanderait d'avoir des permissions beaucoup plus précises (et on pourrait alors dire que certaines seraient autorisées par défaut, pour éviter d'avoir à demander au joueur la permission d'accéder à des détails techniques, et ne demander son accord explicite que pour les permissions dangereuses, type chargement de DLL, accès au système de fichier, etc.).

Elanis commented 3 years ago

Quelques remarques qui me sont venues durant la lecture de l'issue:

SirLynix commented 3 years ago

Que se passe-t-il en cas d'ajout de mods alors que le jeu est lancé ? La question est intéressante si on intègre un système de téléchargement de mods dans le jeu, ou si une integration workshop est faite. Il peut tout simplement y avoir un update demandé par le script qui fait ce téléchargement, mais je pense que c'est un détail à considérer.

Actuellement les mods peuvent être rechargés à n'importe quel moment en dehors d'un match, donc la mise à jour (ou même le téléchargement via Steam Workshop) peut se faire en plein jeu. Techniquement ça devrait même être possible de hot-reload un mod en plein jeu, mais ça peut facilement exploser donc je préfère réserver ça aux développeurs pour les aider à développer des mods.

info.json devrait contenir la version du mod pour les dépendances, il faudra bien sûr intégrer un versionning (si basé sur semver, on devra dire si on accepte une version précise, toute une version mineure, toute une version majeure, etc.)

Très bon point que j'avais oublié, effectivement.

Pour les dépendances cycliques, je pense qu'il est possible "d'aplanir" la liste de dépendances, mais pareil qu'au dessus, il faudra faire attention aux conflits de versions

Oui, je commence à me dire que les dépendances cycliques devraient être interdites dans un premier temps, purement et simplement.

Il faudra en effet trouver un moyen explicite d'override une entité d'un mod loadé auparavant, sans se référer au nom, pour éviter que les noms génériques deviennent problématiques comme ca a été le cas avec les IDs dans les mods minecraft avant la creation de ForgeModLoader

Quelque chose comme GetEntityTable("mod::x::entity_machin")?

Les contextes séparés semblent une bonne idée, peut être que pour de l'optimisation, il serait possible de faire une solution hybride: ne dupliquer dans les contextes que les fonctions ayant des effets de bord.

En y réfléchissant, des contextes séparés seraient une mauvaise idée, car ça rendrait le partage d'informations entre les différents mods impossible (des trucs aussi cons qu'une entité créée par le mod X et récupérée par le mod Y dans un event de collision par exemple).
En revanche, on peut exploiter les metatables de Lua pour sandboxer chaque mod sans trop de problème.

Elanis commented 3 years ago

Actuellement les mods peuvent être rechargés à n'importe quel moment en dehors d'un match, donc la mise à jour (ou même le téléchargement via Steam Workshop) peut se faire en plein jeu. Techniquement ça devrait même être possible de hot-reload un mod en plein jeu, mais ça peut facilement exploser donc je préfère réserver ça aux développeurs pour les aider à développer des mods.

Très bon point que j'avais oublié, effectivement.

Nice !

Oui, je commence à me dire que les dépendances cycliques devraient être interdites dans un premier temps, purement et simplement.

A voir en effet :)

Quelque chose comme GetEntityTable("mod::x::entity_machin") ?

Je pensais a quelque chose du genre:

local entity = ScriptedEntity({
    Override = "entity_burger"
})

ou bien Replace, Extends, etc.

On spécifie l'entité que l'on veut remplacer, puis les attributs/méthodes remplacés avec leur definition. Mais ca peut poser problème si on souhaite juste désactiver l'entité d'un autre mod; où désactiver une méthode.

En y réfléchissant, des contextes séparés seraient une mauvaise idée, car ça rendrait le partage d'informations entre les différents mods impossible (des trucs aussi cons qu'une entité créée par le mod X et récupérée par le mod Y dans un event de collision par exemple). En revanche, on peut exploiter les metatables de Lua pour sandboxer chaque mod sans trop de problème.

En effet, ca peut être gênant. On verra à la pratique ce qui passe ou pas 😄

selkij commented 3 years ago

le info.json devrais aussi contenir une description qui serais optionelle bien sur avec un site avec peut-être les permissions de commandes avec biensur la liste des commandes.

SirLynix commented 3 years ago

La description y est déjà, mais un site peut se rajouter sans problème.

Pour ce qui est des commandes par contre, actuellement il n'y a aucune notion propre aux commandes, ça pourrait être utile d'organiser un peu plus ça, à voir comment, mais je pense que ça devrait faire l'objet d'une autre issue.