Sommaire
Part 0 : quoi, pourquoi, comment ?
Hello,
Pour une confiture de jeux (game jam), j'ai essayé d'utiliser un moteur de jeux web peu utiliser : libfuse.
Que j'ai Hébergée sur le service de IIAS itch.io.
Et vu que je n'ai pas vu beaucoup de personnes utilisées ces technologies, je me suis dit, que ça pouvais valoir le coup de me forcer à passer mon aversion à écrire des longs trucs, et en parle.
Donc voici un pavé sûr ma dernière aventure de code.
Bon, passer cette présentation, on doit se dire que le mec qui écrit le journal est complément con :
libfuse, n'est pas un moteur de jeux.
Et itch.io est un site de jeux vidéo.
Pourtant, pour la Pirate Software - Game Jam 17 j'ai eu cette idée de faire un genre de Tamagotchi qui ne serait jouer que par manipulation de fichier.
Par exemple, pour nourrir notre personnage, on ferait "echo feed > my_game/character"
Pour connaitre son état de santé, il faudra faire "cat my_game/status"
Le thème de la jam étant "Only One", l'idée d'un jeu filesystem s'y prête bien, car on peut imaginer faire un jeu où l'on devrait gérer les envies d'un seul fichier dans un filesystem.
Contrainte : jeux doit être jouables en web
Mais bon, Fabrice Bellard l'a fait, et comme le dit ld.so, ça ne doit pas être un Big Fucking Deal.
Et donc après 2 semaines de travail aussi acharne que ne l'est le 4e cœur CPU de ma steam deck, lors d'une session intense de Dark Reign: The Future of War
J'ai fini par pondre cette chose
Le code ici : https://github.com/cosmo-ray/cringy-file
Le jeu est naze, mais avec un élan de motivation que j'aurais jamais, il pourrait ne pas l'être.
Part 1 : let's code
en 1er des globales utilisait pour le jeu :
On a besoins d'un système de score, pour ce faire, j'ai utilisé 2 variables globales :
uint64_t unhappy_life;
uint64_t happy_life;
unhappy_life sera soustrait à happy_life pour avoir le score
J'aurais pu utiliser une seule global, mais quand j'ai écrit le code, je m'étais dit, qu'il pourrait être intéressant d'avoir les deux informations.
Ça n'a servi à rien.
Pour le nom du fichier principal avec lesquels on va interagir,
on fait un "#define CRINGY_NAME "cringy""
De temps en temps, on va créer des fichiers avec des noms semi-aléatoire.
Le joueur auras à les supprimer pour ne pas perdre de points
Pour ce faire, je crée un compteur de "parasite", et un tableau de pointeur sur char qui continent des noms pouvant être utilisé par les fichiers parasite.
int nb_parasite;
const char *parasite_names[] = {
"Jean-Robet-the-annoying-file.txt",
"An-overexited-arch-user.BTW",
"ev&ng*li#n.avi",
"UwU.owo",
"baldur-s-door-2.exe",
"vim.notemacs",
"windows-user.boring",
"catgirl-dekimakura.png"
};
Vu que les parasite apparaissent de manière aléatoire, il nous faut un espace de stockage ou mettre les parasites en vie.
Je crée un tableau de pointeur sur char, qui contiendra soit NULL, soit le nom du fichier parasite.
"parasite_names" étant un tableau de taille indéfini, on utilise sizeof pour "MAX_PARASITE"
Ceci permet que si l'on veut rajouter un nouveau fichier de parasite, on a juste à modifier parasite_names et la taille du tableau parasite seras automatiquement mise à jour, sans avoir à la définir soit même. (enfin, je dis ça, j'ai gardé un nombre en dur pendant tout le développement et mit le sizeof à la fin quand ça servais plus)
#define MAX_PARASITES (int)(sizeof parasite_names / sizeof *parasite_names)
const char *parasites[MAX_PARASITES];
Pour finir, il nous faut un espace de stockage pour le contenu du fichier.
Vu que le fichier ne continent pas de données utilisateur, mais uniquement des informations renvoyaient par le filesystem, on peut utiliser un tableau de taille fixe.
Ici, on en utilise 3, "cringy_content" qui contient l'intégralité du contenu du fichier,
"status_buf" qui est utilisé pour contenir, la partie qui va changer en fonctions des commandes utilisateur.
"parasite_txt_buf" qui contient une chaîne de caractères, qui indique si notre fichier cringy est importunée par d'autres fichiers.
On utilise aussi "cringy_content_l", pour connaitre la taille réel de la chaîne de caractères contenue dans "cringy_content"
char status_buf[1024];
char parasite_txt_buf[1024];
char cringy_content[2048];
size_t cringy_content_l;
un fish-tre besoins de macro.
Le jeu marche sur un système de besoins auquel on doit répondre.
Pour ce faire, j'utilise un enum qui stock tous les besoins.
Sauf que claques besoins étant lié à plusieurs chaines de caractères expliquant quel sont les besoins en questions, et quel sont les choses que notre fichier ne veut pas, pour simplifier la définition de ces besoins,
j'ai utilisé une x-macro.
On a la création de plusieurs variables :
#define NEED(a, useless__, useless2__) a,
enum {
#include "need_tok.h"
NEED_CNT
};
#define NEED(useless__, s, useless2__) s,
char *need_str[] = {
#include "need_tok.h"
NULL
};
#define NEED(useless__, useless2__, s) s,
char *no_need_str[] = {
#include "need_tok.h"
"whatever you're trying to do !",
NULL
};
Et leur initialisation dans le fichier need_tok.h:
NEED(NEED_HUG, "cringy want an hug", "I felling lonely, i want an hug, not")
NEED(NEED_FOOD, "cringy is hungry", "I WANT FOOD, not")
NEED(NEED_BATH, "cringy is dirty", "I felling dirty, i don't want")
NEED(NEED_PLAY, "cringy is bored and wanna play", "I wanna play, not")
#undef NEED
On a donc, pour chaque besoin :
une enum pour le définir, une chaine de caractères pour exprimer son besoin, et une autre pour exprimer son mécontentement suite à une commande utilisateur, qui répondraient au mauvais besoin.
Fuse-t-il ce code pour initialiser ce Système de fichier
Initialiser la bibliothèque
Pour initialiser fuse, tout ce que l'on a à faire c'est d'appeler
fuse_main(argc, argv, &cringy_oper, NULL);
qui continent une structure de pointeur sur fonction, qui est initialisé ici :
static const struct fuse_operations cringy_oper = {
.init = cringy_init,
.getattr = cringy_getattr,
.readdir = cringy_readdir,
.open = cringy_open,
.read = cringy_read,
.unlink = cringy_unlink,
.write = cringy_write,
};
cringy_init : est appelé en 1er, et se contente d'initialiser des choses dont on aura besoins plus tard :
static void *cringy_init(struct fuse_conn_info *conn,
struct fuse_config *cfg)
{
(void) conn;
printf("init cringy %d !\n", (int)MAX_PARASITES);
init_time = time(NULL);
last_refresh = init_time;
srand(init_time);
make_text(FORCE_REFRESH);
cfg->kernel_cache = 0;
printf("%d - %d\n", getuid(), getgid());
return NULL;
}
Mais comment qu'on fait un ls
cringy_readdir est un callback qui permet de lister les fichiers existants:
ici, il est déclaré comme tel :
static int cringy_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
off_t offset, struct fuse_file_info *fi,
enum fuse_readdir_flags flags)
Et le code est assez simple :
{
(void) offset;
(void) fi;
(void) flags;
if (strcmp(path, "/") != 0)
return -ENOENT;
filler(buf, ".", NULL, 0, 0);
filler(buf, "..", NULL, 0, 0);
filler(buf, CRINGY_NAME, NULL, 0, 0);
for (int i = 0; i < MAX_PARASITES; ++i) {
if (parasites[i]) {
filler(buf, parasites[i], NULL, 0, 0);
}
}
return 0;
}
On remarquera ici l'utilisation de (void) servant à ignorer les warning de gcc pour me signaler des variables inutilisées, et à montrer mon incompétence à utiliser les CFLAGS de gcc correctement.
Lisons un peu
La lecture du fichier est elle assez facile.
Vu que l'on gère un seul fichier, qui a son contenu généré au fils des calls,
Tout ce que l'on à faire, c'est d'afficher son contenu.
Pour ce faire, étant donné que l'intégralité du contenu du fichier est stocké dans la variable cringy_content, un memcpy dans buf, pointeur sur char fournie par libfuse suffit.
On doit quand même faire attention à prendre en compte offset, et size
car un utilisateur ne lit pas forcément l'intégralité du fichier en une fois.
static int cringy_read(const char *path, char *buf, size_t size, off_t offset,
struct fuse_file_info *fi)
{
(void) fi;
if(strcmp(path+1, CRINGY_NAME) != 0)
return -ENOENT;
make_text(0);
if ((size_t)offset < cringy_content_l) {
if (offset + size > cringy_content_l)
size = cringy_content_l - offset;
memcpy(buf, cringy_content + offset, size);
} else
size = 0;
return size;
}
et si on apprenait à écrire (contrairement à moi)
Peu être la partie la plus complexe du code, ici l'on reçoit en paramètre
buf, size et off.
off est allègrement ignoré, vu que l'on ne prend en compte que des entrées de 3 ou 4 caractères, Si une personne a eu la bonne idée d'interagir avec cringy en fessant des write 1, ce n'est pas de bol.
Pour le reste, buf et size contiennent respectivement ce qui est écrit dans le fichier, et la taille de ce qui est envoyé.
Il faut faire attention, car buf ne contient pas forcément un \0 de fin (ce qui est fait avec strncmp à la place d'un simple strcmp).
Pour ce qui est de l'algorithmique, tout ce que l'on fait, c'est regarder si ce qui est dans buf, correspond à un besoin de cringy, et si oui, on regarde si celas correspond à son besoin actuel.
Ensuite, on incrémente les variables unhappy_life ou happy_life, et remplissons la variable status_buf en fonction.
/* define car l'on veut ignorer les '/n' */
#define cmp(buf, s) \
!strncmp(buf, s, size) || !strncmp(buf, s"\n", size)
static int cringy_write(const char *path, const char *buf, size_t size, off_t off, struct fuse_file_info *fi)
{
printf("write %s - %s\n", path, buf);
if (strcmp(path+1, CRINGY_NAME)) {
return -ENOENT;
}
if (cmp(buf, "food") || cmp(buf, "feed")) {
printf("give food\n");
if (current_need != NEED_FOOD) {
status = status_buf;
if (have_need_been_satisfied)
goto wrong;
sprintf(status_buf, "%s your disgusting food", no_need_str[current_need]);
unhappy_life += 5;
} else if (have_need_been_satisfied) {
status = "I've aleready eat :(";
unhappy_life += 5;
} else {
have_need_been_satisfied = 1;
status = "mon mon mon, thanks for the food";
happy_life += 40;
}
/* on a un code similaire pour chaque besoins, que je skipp ici, pour pas prendre trop de place */
} else if (cmp(buf, "help")) {
status = "you can, 'feed' me, 'hug' me, 'wash' me, and 'play' with me";
} else {
status = "what are you doing ?\nyou can ask for help, instead of doing that";
printf("unknow commande\n");
}
on remarque l'utilisation strncmp a la place de strncasecmp, qui aurait permit d'ignorer la case.
C'est du a une décision de game design très simple en fait, c'est car…
note a moi même: trouver une excuse bullshit avant d'envoyer mon journal
Le score ?
Pour le score j'ai fait une simple function.
J'ai trouve ça plus mieux de ne pas mettre le score directement, mais un texte indiquant l’état mental de notre fichier.
static const char *hapiness_text(void)
{
int happy_cnt = happy_life - unhappy_life;
if (happy_cnt < -100) {
return "very unhappy, and want to kill perself";
} else if (happy_cnt < -25) {
return "unhappy, and want you to do something";
} else if (happy_cnt < 0) {
return "slightly down";
} else if (happy_cnt < 20) {
return "okay";
} else if (happy_cnt < 50) {
return "mosly happy";
} else {
return "very happy";
}
}
make_text
Parce que à un moment il faut le générer le contenu, bah on a une function make_text appelée un peu de partout.
Un seul argument flag, qui peu contenir 0 ou FORCE_REFRESH.
Ça aurait donc pu être un _Bool et pas un flag, mais à un moment pendant le développement on pouvait avoir plus d'options.
La function fait 3 choses (function assez grosse et pas en lien avec fuse, donc je ne mets pas tout) :
0 : regarde combien de temps s'est passé depuis la dernière fois que la function a été appelée, s'il y a eu un "tour", mets flag à FORCE_REFRESH.
1 : si "flag & FORCE_REFRESH" n'est pas 0, on change le besoin de cringy, et s'il n'a pas été comblé, on augmente unhappy_life.
2 : on remplie cringy_content.
Pour remplir le fichier, on le fait en 2 calls, qui sont de simples et moches sprintf:
if (nb_parasite) {
sprintf(parasite_txt_buf, "there's %d parasited here", nb_parasite);
}
cringy_content_l = sprintf(cringy_content, "Hello :)\n"
"I live for %zu sec\n"
"cringy is: %s\n"
"%s\n"
"last thing cringy tell you:\n%s\n"
"\n%s\n", (size_t)t - (size_t)init_time,
hapiness_text(),
have_need_been_satisfied ? "cringy want nothing":
need_str[current_need], status,
nb_parasite ? parasite_txt_buf : "at last there's no parasites here");
sprintf c'est moche, ça peut faire du dépassement de buffer dans tous les sens, mais ici on a un buffer fixe, et aucune entrée utilisateur.
Part 2: Welcome to the internet
faire une VM en JS ?
Bon, je sais qu'avoir une VM Linux qui tourne en javascript, c'est faisable, Bellard la fait
Je veux dire, il y a même eu un journal là-dessus, ici: https://linuxfr.org/users/jiyuu/journaux/linux-dans-votre-navigateur-web il date de 2011.
Et je me rappelle aussi qu'à l'époque le code source n'était pas disponible.
Enfin, peut-être que ça a changé, et ça me fait un point de départ.
Je me retrouve donc à regarder les techincal note, je découvre que le projet est basé sur tinyemu, qui n'a pas eu de nouvelles versions depuis 2019.
Ceci dit, les sources sont disponibles, et donc ça pourrait éventuellement faire le taf.
Par contre, en regardant la section "Similar projects", je trouve une liste de 3 projets, dans ces trois projets un attire particulièrement mon attention.
v86
Comme on peut le voir, il y a beaucoup d'images, quelques Linux, mais aussi Haiku, ReactOS, des jeux boot sector, et même ToaruOS.
Et en plus de ça, le code est là, mais attention aux yeux, c'est en Rust. (et javascript)
Dans le readme, on a des choses très intéressantes, même une qui contient le nom le plus doux que tout Linuxien peut entendre, à savoir: Arch Linux
La route est donc toute tracée, on peut créer une image Arch Linux BTW personnalisée, et donc il est de mon honneur de Linuxien de faire une image Arch, car Arch est le début de mon amour sur Linux, Arch est ma faim de Linux, s'il y a Arch je ne peux que faire du Arch quelle que soit la difficulté.
Dieu a créé Arch Linux en 6 jours, a joué à extrem tux racer au 7ᵉ et à lancé ./mk-world.pl à 23h41.
Ceci dit, la doc dit de prendre pour exemple un Dockerfile pour Alpine.
Ça veut dire un Dockerfile Alpine tout prêt à l'emploi…
Je vais faire une image alpine.
Le build
Bon, 1ʳᵉ étape : build v86, il y a un makefile, je fais donc un "make", et…
Ça marche pas, j'ai une erreur avec npm bizarre, je connais pas bien npm donc…
Je vais dormir…
Le lendemain, à tête reposée et sans aucune motivation, je reprends, et j'essaye plusieurs choses, enfin non, j'essaye juste d'installer tout ce qui ressemble à une dépendance manquante… mais rien à y faire, après au moins 15mn de debug, je trouve pas.
Je re-vais dormir, il me reste 3 jours avant la fin de la jam, peut-être me bouger le cul, mais il y a une chose de bizarre quand je cherche mon erreur, y'a marqué sur le internet, que j'ai un npm trop vieux, pourtant le glorieux et parfait Arch Linux ne peut pas ne pas être à jour, et quand je regarde quel npm est installé, bah y'a une grosse version.
Néanmoins quand je fais npm --version, bah y'a la version 1/2 de la version installée (chose qui n'a rien à voir avec Ranma 1/2)
Lorsque me vient une idée : "env | grep npm"…
Bah je vois mon PATH apparaitre, et dans ce PATH y'a Emscripten, et je me rappelle que Emscripten a son npm à lui, un npm qui path pas.
Bon, 3 jours de perdus, que je règle avec un simple export…
Maintenant, make passe, je peux commencer à regarder comment build une image.
L'image
Donc il est temps de regarder notre exemple Arch Archpine Alpine
Je lance un build: et tout marche.
Ok, maintenant j'ai juste à build mon driver fuse, et à packager l'image.
Pour ça, rien de compliqué :
Je rajoute les packages besoins au build, et fuse dans le apk add ça donne ça:
RUN apk add openrc alpine-base agetty alpine-conf libc-dev linux-$KERNEL linux-firmware-none gcc make fuse3 git pkgconf fuse3-dev
Ensuite je build et rajoute mon binaire dans /bin:
RUN git clone https://github.com/cosmo-ray/cringy-file.git /root/cringe-file
RUN cd /root/cringe-file && make
RUN cp /root/cringe-file/cringe-fs /bin/
Maintenant, je peux tester, lancer mon image en local,
pour ça un "python -m http.server"
et dans mon browser j'ouvre l'example
Une fois ma VM Alpine lancée, un simple "modprobe fuse; cringe-fs cringe-space" permet de lancer le jeu dans le directory "cringe-space"
Bon bah tout semble bon, plus qu'à rajouter un script de démarrage :
RUN echo -e "#!/bin/sh\nmodprob fuse\nmkdir cringe-space\ncringe-fs cringe-space\necho look at what s in cringe-space/ hint: ls/cat/echo" > root/start-game.sh
Et un message pour expliquer à l'utilisateur comment lancer le jeu :
RUN echo -e "Hello, welcome\nto play this game you need to do './start-game.sh'\nthis will create the directory cringe-space, this directory is where the game happen, look at files insides, and make cringy happy :)\nList files: ls (so ls cringe-space for cringy directory)\nlook at a file: cat A_FILE\nremove a file: rm FILE\noutput stuff in a file: echo something > DST_FILE\nBTW you can use vi" > /etc/motd
Je fais un zip de tous les fichiers qui servent d'images, j'upload ça sur itch… Et ça marche pas, itch veut un zip de moins de 1000 fichiers et la manière dont v86 fait son image, c'est qu'il "encode" chaque fichier présent sur mon Alpine dans un json…
Ce qui veut dire que chaque binaire, chaque fichier de conf, chaque json présent sur ma distribution alpine va devenir un json, et c'est ça que lira l'émulateur wasm.
Je vais donc devoir réduire ce qu'il y a dans mon Alpine, drastiquement.
Boucheries
Attention, cette section contient de la malveillance envers les NonGnu/Linux
Au début je commence gentiment, je supprime les programmes installés pour les builds :
RUN apk del -f git gcc pkgconf fuse3-dev libc-dev make
Puis je rajoute --no-cache à apk.
Je supprime les sources de cringe-file.
Je regarde combien j'ai de fichiers : beaucoup, top.
Bon, l'image d'exemple Alpine sur v86 a Node.js d'installé, et je l'avais laissée pour permettre aux utilisateurs de mon jeu de scripter en JS.
Plus maintenant, mon Alpine se retrouve donc sans rien, avec seuls les éléments vitaux dont elle a besoin pour fonctionner.
Mais il y a toujours trop de fichiers, je vais devoir continuer de supprimer des trucs.
Je vais en conséquence devoir commencer à rentrer dans les organes d'Alpine :
Je supprime /etc/ssl, qui a besoin de ça de toutes facons ?
Mais ça suffit pas, alors je supprime d'autres choses qui n'ont pas l'air utiles à l'exécution d'Alpine comme /usr/sbin/setup-* ou /usr/share/apk qui ne pouvaient plus marcher sans les certs ssl.
Mais toujours rien, et donc malgré une Alpine bien amochée sur mon écran et des débuts de remords de la part de la personne derrière le clavier, je vais devoir continuer à entailler cette distro pas assez minimaliste pour moi.
Je vais devoir rentrer plus profondément, je vais devoir charcuter des modules noyaux.
Je finis ainsi mon travail de boucher en enlevant tout ce qui ressemble à du net /lib/modules/6.12.40-0-virt/kernel/net* et /lib/modules/6.12.40-0-virt/kernel/drivers/net* se font donc rm.
Je relance le Dockerfile, et re-regarde mon nombre de fichiers :
C'est bon, j'ai moins de 1000 fichiers, mon Alpine n'est plus que l'ombre d'elle-même, mourante, mais prête, prête à être envoyée sur itch.
Si vous aussi vous voulez découvrir le métier d'artisan boucher Alpiniste, voici la ligne utilisée :
RUN rm -rvf /root/cringe-file /lib/modules/6.12.40-0-virt/kernel/net* /etc/ssl /usr/share/apk /lib/modules/6.12.40-0-virt/kernel/drivers/net* /usr/sbin/setup-*
J'upload, l'image se lance, j'ai un shell !
Je modprod…. et ça marche pas.
Mon module est KO
"modprob fuse" ne marche plus, est-ce parce que j'ai supprimé la moitié de ce qui est présent dans /kernel ?
Ça semblerait bien étonnant que ça puisse poser un problème.
Je re-teste en local et ça marche.
J'essaye avec insmod /lib/modules/6.12.40-0-virt/kernel/fs/fuse/fuse.ko.gz en local et ça marche, peut-être un problème dans modprod.
Donc je fais la même commande sur itch, mais ça marche toujours pas…
Ceci dit, à regarder le module fuse de plus près, bah je vois qu'il finit par .gz, ça veut dire qu'il est compressé, et probablement que ziper un fichier compressé qui est aussi encodé bizarrement dans un json, ça peut mal se passer.
J'essaye donc de décompresser mon module, et là tout marche.
Je me retrouve alors à rajouter quelques lignes dans mon Dockerfile:
RUN gunzip /lib/modules/6.12.40-0-virt/kernel/fs/fuse/*ko.gz
RUN cp /lib/modules/6.12.40-0-virt/kernel/fs/fuse/* /root
Et remplace le modprod que j'avais mis dans mon script de lancement par un "insmod ./fuse.ko."
Je re-test, et enfin !, enfin je peux faire découvrir au monde ce jeu que moi-même, n'apprécie pas particulièrement.
Si vous voulez voir les fichiers utilisés pour build, ils sont ici: https://github.com/cosmo-ray/cringy-file/tree/master/alpine
Par contre, pour les créer, j'ai simplement modifié les exemples dans le repo v86, et ai copié ce que j'ai fait dans mon repository GitHub, avant d'écrire ce journal.
Conclusions
J'ai mis plus de temps à écrire cette merde ce journal qu'à coder ce truc.

Envoyer un commentaire
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.