Technical Deep-Dive

UI Framework

Un framework UI entièrement server-side pour Minecraft Bedrock, construit par détournement du protocole réseau.

C#
UI
Architecture
Design System
Multijoueur
Full-Stack
Scroll

Présentation

Sur Minecraft Bedrock, il n'existe aucun moyen natif pour le serveur d'afficher une interface personnalisée. Pourtant, Paladium en propose des dizaines : boutiques, menus de navigation, menu de factions… Tout ça sans jamais modifier le client.

Pour rendre ça possible, j'ai conçu un framework UI entièrement server-side en détournant le protocole réseau du jeu. Il fournit une architecture full-stack en deux couches : le backend propose un système déclaratif pour créer des UI, et le frontend propose une bibliothèque d'assets prêts à l'emploi pour que toutes les UI partagent la même direction artistique.

Les contraintes

🔒

Thread-safe → fonctionner en multijoueur

📈

Scalable → supporter des dizaines d'UI différentes

🚫

Sans modifier le client

🌍

Cross-platform (mobile, console, PC)

Ne pas ralentir le serveur

👥

Utilisable par d'autres développeurs

Interface Paladium Menu
Exemple d'UI produite avec le framework

🔍 L'investigation

Avant d'inventer une solution, j'ai cherché ce que Minecraft mettait à ma disposition.

JSON-UI Le système d'interface de Minecraft

Minecraft Bedrock expose un système de déclaration d'interface appelé JSON-UI l'équivalent d'un HTML/CSS qui permet de modifier les écrans du jeu. Ce système peut lire des valeurs provenant du client via un mécanisme de Binding, un peu comme des variables réactives. Un sous-langage appelé Molang permet d'y ajouter de la logique conditionnelle.

Le problème

Les bindings ne peuvent lire que des valeurs imposées par le client. Le serveur peut en modifier certaines, mais c'est extrêmement limité.

Il existe cependant une valeur que le serveur peut librement modifier dans les conteneurs (coffres, fours…) : le nom personnalisé d'un item. Une simple string c'est peu, mais c'est suffisant pour tout construire.

Les trois axiomes du framework

📦

1 item

= 1 composant

🏷️

Nom de l'item

= la donnée envoyée au client

🖱️

Clic de l'item

= l'interaction côté serveur

C'est cette contrainte et ce levier qui définissent toute l'architecture qui suit.

🔓 Le détournement du protocole

Le hack fonctionne en 3 étapes. Chacune résout un problème précis imposé par les restrictions de Minecraft Bedrock.

Étape 1

⚙️Backend C#

Créer une entité fantôme

Pour afficher une interface, Minecraft a besoin d'un conteneur qui existe dans le monde : un coffre, un four, une entité… Problème : aucun de ces objets n'existe là où le joueur interagit. La solution : en faire apparaître un à la volée, invisible, uniquement pour lui.

J'ai choisi une entité plutôt qu'un bloc : les blocs sont limités à 27 slots, alors qu'une entité peut en avoir 140 ce qui permet de créer des interfaces bien plus riches.

Technique

Le serveur envoie un AddEntityPacket directement au joueur avec des métadonnées particulières : taille et scale à zéro, flag invisible, type de conteneur et nombre de slots. L'entité n'existe que dans la session du joueur pas dans le monde et disparaît à la fermeture de l'interface.

1var metadata = GetActorMetadata();
2metadata.Set(MetadataTypes.Width,         0.0f);   // Invisible
3metadata.Set(MetadataTypes.Height,        0.0f);
4metadata.Set(MetadataTypes.Scale,         0.0f);
5metadata.Set(MetadataTypes.ContainerType, (byte)windowType);
6metadata.SetFlag(EntityFlag.Invisible,    true);
7metadata.Set(MetadataTypes.ContainerSize, size);   // 140 slots
8
9var addEntityPacket = new AddEntityPacket() {
10    EntityUniqueId  = ...,
11    EntityRuntimeId = ...,
12    Position        = ...,
13    Metadata        = metadata,
14    ....
15};
16player.SendPacket(addEntityPacket);

Étape 2

⚙️Backend C#

Forcer l'ouverture de l'inventaire

L'entité fantôme existe maintenant côté client, mais le client ne sait pas qu'il doit l'ouvrir. Il n'existe aucun packet pour lui dire directement d'ouvrir l'inventaire d'une entité. Il a fallu ruser.

L'astuce : faire croire au client que l'entité fantôme est une monture et forcer le joueur à la "monter". Grâce à ça, forcer l'ouverture de l'inventaire du joueur a pour effet secondaire d'ouvrir l'inventaire de l'entité.

Technique

Deux packets suffisent : d'abord SetEntityLinkPacket pour simuler la relation de montage, puis ContainerOpenPacket pour ouvrir l'inventaire du joueur ce qui déclenche automatiquement l'ouverture du conteneur de l'entité.

1// Étape A  Le client croit que le joueur monte l'entité fantôme
2var entityLinkPacket = new SetEntityLinkPacket() {
3    Link = new EntityLinkData(
4        GetActorRuntimeIdentifier(),
5        player.GetRuntimeId(),
6        ...
7    )
8};
9player.SendPacket(entityLinkPacket);
10
11// Étape B  Ouvrir l'inventaire du joueur → ouvre l'inventaire de l'entité
12var openInventory = new ContainerOpenPacket() {
13    EntityUniqueId = GetActorRuntimeIdentifier(),
14    ...
15};
16player.SendPacket(openInventory);
Pourquoi ça marche : Minecraft gère le "riding" (montée sur une entité) en exposant l'inventaire de la monture au joueur. En simulant cette relation via le protocole, on ouvre n'importe quel conteneur virtuel sans jamais le faire exister dans le monde.

Étape 3

⚙️Backend C#

Peupler le conteneur

Le conteneur est ouvert mais vide. On le remplit d'items pas pour stocker des objets, mais pour encoder de l'information et capturer des interactions. Chaque item joue l'un de ces deux rôles :

🖱️ Interaction

Le client envoie un packet quand il tente de prendre ou déplacer un item. Côté serveur, on refuse l'action mais on exécute une logique custom c'est ainsi qu'on crée un bouton.

📡 Donnée

En modifiant le nom personnalisé de l'item ("true", "false", un chemin d'image…), le JSON-UI peut lire la valeur et la rendre visuellement.

⚙️ Architecture Backend

Le hack fonctionne, mais un hack seul ce n'est pas un framework. Il fallait construire quelque chose d'utilisable par d'autres développeurs lisible, extensible, et qui réponde aux contraintes posées au départ.

Partie 1

⚙️Backend C#

La brique de base AbstractUiComponent

La règle fondamentale du framework : chaque item dans le conteneur est un composant. Un bouton, un label, une barre de progression, un slot d'inventaire tout est un composant, et chaque composant occupe un ou plusieurs slots.

Technique

Tous les composants héritent de AbstractUiComponent, qui centralise les informations nécessaires au framework : son identifiant unique pour les mises à jour ciblées, son index dans le conteneur, son état de verrouillage, et surtout son SeveralContext le seul canal vers le client.

1public abstract class AbstractUiComponent : IUiComponent {
2    public string Id { get; set; }                         // Identifiant unique → update ciblé
3    public int Index { get; set; }                         // Position dans le conteneur
4    public bool Locked { get; set; } = true;               // Bloque l'interaction par défaut
5    public SeveralContext SeveralContext { get; set; }     // La donnée envoyée au client
6    public List<IUiComponent> AdditionalComponent { get; } // Sous-composants (voir plus bas)
7
8    public virtual int GetSize() => 1;   // 1 composant = 1 slot par défaut
9    public virtual void OnGuiInit() { } // Appelé au placement
10    public virtual void Update() { }    // Appelé au refresh
11}
SeveralContext est le seul canal de communication vers le client c'est lui qui sera transformé en nom d'item et lu par le JSON-UI.

Partie 2

⚙️Backend C#

Les comportements Interfaces combinables

Un composant peut se comporter de plusieurs façons : on peut cliquer dessus, y déposer un item, le serveur peut y placer quelque chose… Plutôt que de tout mettre dans une seule classe, chaque comportement est une interface qu'on combine à la carte. Un composant peut en implémenter plusieurs à la fois.

IButtonUiComponent

Réagit au clic (OnClick / OnClickAsync). Exemple : bouton d'achat.

ISlotUiComponent

Le joueur peut déposer/retirer un item (OnPlace / OnTake). Exemple : slot de stockage.

IItemStackHolderUiComponent

Le serveur place un item spécifique dans ce slot. Exemple : affichage d'item.

Technique ButtonUiComponent

Le bouton est l'exemple le plus simple : il stocke un nom (envoyé au client via le nom de l'item) et expose deux callbacks un synchrone pour les actions rapides, un asynchrone pour les appels BDD ou API.

1public sealed class ButtonUiComponent : AbstractUiComponent, IButtonUiComponent {
2    public ButtonUiComponent(string? name = null) {
3        if (!string.IsNullOrEmpty(name))
4            SeveralContext.StringValue = name; // Le nom = la donnée envoyée au client
5    }
6
7    public Func<OnClickEvent, bool>?       OnClick      { get; set; } // Sync
8    public Func<OnClickEvent, Task<bool>>? OnClickAsync { get; set; } // Async (BDD, API…)
9}
Un SlotUiComponent implémente simultanément ISlotUiComponent et IItemStackHolderUiComponent le joueur peut interagir ET le serveur peut pré-placer un item.

Partie 3

⚙️Backend C#

Les arbres de composants Pattern Composite

Certaines UIs ont besoin de gérer d'autres composants : une pagination contient des boutons, une catégorie contient des pages, une page contient des items. C'est une structure en arbre exactement comme le DOM HTML et chaque composant peut contenir des sous-composants via AdditionalComponent.

Exemple Arbre de pagination

PaginationUiComponent (occupe 23 slots au total)
├── [Page 1 : 20 boutons]
├── ButtonUiComponent "Épée en diamant" slot 10
├── ButtonUiComponent "Armure en fer" slot 11
└── VoidUiComponent × 2 (padding) slots 12-13
├── ButtonUiComponent " Précédent" slot 20
├── ButtonUiComponent "1/3" (compteur) slot 21
└── ButtonUiComponent "Suivant " slot 22

Technique Recherche récursive par ID

Pour retrouver un composant peu importe sa profondeur dans l'arbre, le framework utilise une recherche récursive exactement comme un document.getElementById() en JavaScript.

1private static bool TryGetElementRecursive<T>(
2    IEnumerable<IUiComponent> components, string id,
3    [NotNullWhen(true)] out T? result) where T : class, IUiComponent {
4
5    foreach (var component in components) {
6        if (component.Id == id && component is T match) {
7            result = match;
8            return true;
9        }
10        // Descendre dans les enfants
11        if (TryGetElementRecursive(component.AdditionalComponent, id, out result))
12            return true;
13    }
14    result = null;
15    return false;
16}
TryGetElement<ButtonUiComponent>("buy_sword") trouvera le bouton peu importe qu'il soit en page 3 d'une catégorie imbriquée sans que l'appelant connaisse la structure de l'arbre.

Partie 4

⚙️Backend C#

La navigation Pagination & Catégories

Une boutique peut avoir des centaines d'items. Impossible de tous les afficher en même temps dans 140 slots. Le framework intègre directement deux systèmes de navigation : la pagination pour faire défiler des pages, et les catégories pour organiser le contenu en onglets nommés. Les deux gèrent automatiquement leurs boutons.

Technique Pagination automatique

Le composant gère lui-même ses boutons Précédent/Suivant et son compteur de pages. Quand la page change, seuls les composants affectés sont mis à jour.

1AdditionalComponent.Add(new ButtonUiComponent("PreviousPagination") {
2    OnClick = _ => { PreviousPage(); return false; }
3});
4AdditionalComponent.Add(new ButtonUiComponent(CurrentIndex + "/" + PageCount));
5AdditionalComponent.Add(new ButtonUiComponent("NextPagination") {
6    OnClick = _ => { NextPage(); return false; }
7});

Technique Catégories nommées

1new CategoryUiComponent(this, new Dictionary<string, PageNavigator> {
2    { "Armes",   new PageNavigator(listeBoutons_Armes)   },
3    { "Armures", new PageNavigator(listeBoutons_Armures) },
4    { "Outils",  new PageNavigator(listeBoutons_Outils)  }
5});
Alignement automatique : si une page a 20 items et une autre 8, les boutons de navigation se décaleraient. Le framework complète automatiquement avec des VoidUiComponent (Singleton un seul objet partagé pour tous les slots vides).

Partie 5

⚙️Backend C#

Le chef d'orchestre GuiContainer

Toutes les briques existent, mais il faut quelqu'un pour les assembler. Le GuiContainer est ce chef d'orchestre : il crée l'entité fantôme, ouvre le conteneur, place les items, et gère les mises à jour. C'est le point d'entrée unique pour tout le cycle de vie d'une UI.

Technique Cycle de vie complet

player.Open(new ShopUi())
└── Render(player)
└── VirtualContainer.Send(player) Crée l'entité fantôme
└── Container.Open(player)
├── Réserve les slots 0-9 (retour, hub)
├── CreateContent(elements)
└── Pour chaque composant :
├── Components[index] = component
├── Applique le SeveralContext nom de l'item
└── component.OnGuiInit()
└── SendContent() Envoie tous les items au joueur

Mise à jour ciblée Un seul packet

Quand un label change (prix affiché, quantité en stock…), on n'a pas besoin de reconstruire toute l'UI. Le framework envoie uniquement le packet du composant modifié.

1// Mettre à jour un seul composant (1 seul packet réseau)
2ui.UpdateComponent(label);
3
4// Reconstruire entièrement (changement de page)
5ui.Rebuild();
Réponse directe à la contrainte "ne pas ralentir le serveur" : on n'envoie que ce qui a changé. Modifier un label = 1 packet. Changer de page = 1 rebuild ciblé.

✅ Résultat

Une UI complète en production

Une enclume custom permettant la fusion d'enchantements et la réparation d'items. Deux slots d'entrée, un slot de sortie, un label de coût mis à jour en temps réel. À la fermeture, les items non validés sont automatiquement rendus au joueur.

C'est un bon exemple pour voir comment toutes les briques s'assemblent déclaration des composants, réactivité par événements, mise à jour ciblée, gestion du cycle de vie.

Technique

1// UI d'enclume custom  en production sur Paladium
2public sealed class CustomAnvilUi : AbstractUi {
3
4    // ── Composants ─────────────────────────────────────────────────────────
5    private readonly SlotUiComponent  _firstInput;   // Item à améliorer
6    private readonly SlotUiComponent  _secondInput;  // Matériau / item de fusion
7    private readonly SlotUiComponent  _output;       // Résultat
8    private readonly LabelUiComponent _costLabel;    // Affiche le coût (LVL + $)
9
10    public CustomAnvilUi() {
11        _firstInput  = new SlotUiComponent(this);
12        _secondInput = new SlotUiComponent(this);
13        _output      = new SlotUiComponent(this);
14        _costLabel   = new LabelUiComponent("false") { Id = "cost" };
15
16        // ── Réactivité ─────────────────────────────────────────────────────
17        // Dès qu'un slot change → recalculer le résultat
18        _firstInput.OnPlace  += _ => { Compute(); return true; };
19        _firstInput.OnTake   += _ => { Compute(); return true; };
20        _secondInput.OnPlace += _ => { Compute(); return true; };
21        _secondInput.OnTake  += _ => { Compute(); return true; };
22
23        // Prendre l'output = valider et payer le coût
24        _output.OnTake += e => TryTakeOutput(e.Player);
25
26        // Redonner les items au joueur à la fermeture
27        Container.AddCloseListener(player => {
28            player.GiveItem(_firstInput.GetItemStack());
29            player.GiveItem(_secondInput.GetItemStack());
30        });
31    }
32
33    public override string Id => "custom_anvil";
34
35    // ── Logique ────────────────────────────────────────────────────────────
36    private void Compute() {
37        // Calcule enchantements + réparation, place l'item résultant dans l'output
38        _output.SetItemStack(/* item calculé */);
39
40        // Mise à jour ciblée : 1 seul packet, seulement le label change
41        _costLabel.SeveralContext.StringValue = $" {_levelCost} LVL  {Format(_moneyCost)} ";
42        UpdateComponent(_costLabel);
43    }
44
45    private bool TryTakeOutput(Player player) {
46        if (player.GetMoney() < _moneyCost) return false;
47        player.RemoveMoney(_moneyCost);
48        return true;
49    }
50
51    // ── Déclaration ────────────────────────────────────────────────────────
52    // Le framework mappe automatiquement chaque composant dans le conteneur
53    public override List<IUiComponent> GetElements() => [
54        _firstInput, _secondInput, _output, _costLabel
55    ];
56}
🧩

Composants déclaratifs

4 lignes pour déclarer toute l'UI. Le framework s'occupe du mapping slot ↔ composant.

Réactivité par événements

OnPlace / OnTake déclenchent Compute() pas de polling, pas de boucle.

📡

Mise à jour ciblée

UpdateComponent(_costLabel) envoie 1 packet. Le reste de l'UI n'est pas retouché.

🔒

Gestion du cycle de vie

AddCloseListener garantit que le joueur récupère ses items même s'il ferme sans valider.

⚙️ Backend C#🎨 Frontend JSON-UI

Les parties suivantes décrivent le côté client le Resource Pack JSON-UI qui lit les données encodées par le C# et les transforme en visuels.

🎨 Architecture Frontend

Le backend encode des données dans les noms d'items. Mais comment le JSON-UI les lit-il et les transforme-t-il en visuels ? C'est ici qu'intervient la partie la plus ingénieuse : un mini-langage d'expressions pour parser une simple string.

Partie 8

🎨Frontend JSON-UI

Les bindings Lire les données encodées

Côté client, le JSON-UI doit lire les données que le serveur a encodées dans les noms d'items, et les transformer en visuels. Problème : JSON-UI n'est pas JavaScript. C'est un mini-langage rudimentaire avec quelques opérateurs de string et d'arithmétique. Mais avec ces quelques outils, on peut implémenter substring, contains, split et toNumber sans aucune modification du client.

Image Extraire un chemin de texture

Le serveur envoie un chemin de texture dans le nom de l'item. Le JSON-UI doit l'extraire et l'utiliser comme source d'image. Le préfixe de formatage Minecraft (6 caractères) est soustrait pour ne garder que le chemin.

1"container_image": {
2    "type": "image",
3    "bindings": [
4        {
5            "binding_name": "#hover_text",
6            "binding_type": "collection",
7            "binding_collection_name": "container_items"
8        },
9        {
10            "binding_type": "view",
11            "source_property_name": "((#hover_text - ('%.6s' * #hover_text)) - '§r')",
12            "target_property_name": "#texture"
13        }
14    ]
15}
('%.6s' * #hover_text) extrait les 6 premiers caractères (le préfixe de formatage Minecraft). Le - les soustrait, ne laissant que le chemin de texture. C'est un substring(0, 6) imité avec printf.

Barre de progression Arithmétique sur les bindings

Le serveur envoie un nombre entre 0 et 100. Le JSON-UI doit le convertir en ratio de clip pour remplir la barre visuellement. Pas de cast natif on force la conversion avec * 1.

1"progress_fill@$bar_green": {
2    "clip_direction": "left",
3    "$clip_max": 100.0,
4    "bindings": [
5        {
6            "binding_name": "#hover_text",
7            "binding_type": "collection",
8            "binding_collection_name": "container_items"
9        },
10        {
11            // Strip préfixe + convertit string → nombre via * 1
12            "source_property_name": "(#hover_text - ('%.6s' * #hover_text) * 1)",
13            "target_property_name": "#string"
14        },
15        {
16            // #string = 75 → clip_ratio = (100-75)/100 = 0.25
17            "source_property_name": "(($clip_max - #string) / $clip_max)",
18            "target_property_name": "#clip_ratio"
19        }
20    ]
21}

Le * 1 force la conversion string→nombre. L'opérateur clip_ratio coupe l'image une barre de progression sans aucun code client custom.

Booléen Visibilité conditionnelle

Le serveur envoie "true" ou "false" dans le nom de l'item. Le JSON-UI doit afficher ou masquer un élément en conséquence sans fonction de comparaison native, en simulant un String.contains() avec la soustraction.

1{
2    // Visible si #hover_text != "false" et n'est pas vide
3    "true@$true": {
4        "bindings": [{
5            "binding_type": "view",
6            "source_property_name": "((#hover_text - 'false') = #hover_text and not (#hover_text = ''))",
7            "target_property_name": "#visible"
8        }]
9    }
10},
11{
12    // Visible si #hover_text contient "false"
13    "false@$false": {
14        "bindings": [{
15            "binding_type": "view",
16            "source_property_name": "(not ((#hover_text - 'false') = #hover_text))",
17            "target_property_name": "#visible"
18        }]
19    }
20}
(X - 'token') = X est un String.contains() implémenté avec la soustraction. Si retirer "token" ne change pas la string → "token" n'y était pas. C'est le même pattern pour les onglets actifs, le bouton retour, le bouton hub…

Multi-valeurs Le split() impossible

JSON-UI n'a aucune fonction split(). Pour envoyer plusieurs valeurs dans un seul nom d'item (ex : "Épée;500"), le framework implémente un split avec un compteur qui s'incrémente frame par frame jusqu'à trouver le séparateur.

1"dynamic_two_value": {
2    "property_bag": { "#search": 0, "#separator": ";" },
3    "bindings": [
4        // 1. Strip préfixe
5        { "source_property_name": "(#hover_text - ('%.6s' * #hover_text) * 1)",
6          "target_property_name": "#string" },
7
8        // 2. #search s'incrémente jusqu'à la position du séparateur
9        { "binding_condition": "always",
10          "source_property_name": "(#search + 1 * ( not (('%.'+#search+'s') * #string = #string) and (('%.'+#search+'s') * #string = ('%.'+#search+'s') * #string - #separator) ))",
11          "target_property_name": "#search" },
12
13        // 3. Valeur avant le séparateur
14        { "source_property_name": "((('%.'+#search+'s') * #string) - #separator)",
15          "target_property_name": "#value1" },
16
17        // 4. Valeur après le séparateur
18        { "source_property_name": "(#string - ( ('%.'+#search+'s') * #string))",
19          "target_property_name": "#value2" }
20    ]
21}
#search s'incrémente frame par frame jusqu'à trouver la position du ;. On utilise ('%.Ns' * string) comme substring(0, N). Exemple : "Épée;500" #value1 = "Épée", #value2 = "500". C'est un split() implémenté avec un compteur frame-by-frame.

Partie 9

🎨Frontend JSON-UI

Le moteur de rendu

Chaque composant backend a un type : bouton, texte, image, barre de progression… Mais côté client, tous sont des items dans un conteneur. Il faut un composant universel qui, selon le type du slot, choisisse le bon template de rendu et l'applique.

Technique dynamic_slot : le multiplexeur universel

pala_gui.json est le cœur de l'interprétation ~1 000 lignes de composants JSON-UI capables de lire les données d'un slot et de les transformer en éléments visuels. Son entrée principale, dynamic_slot, mappe un index de slot à un template de rendu.

1// Mappe un index de slot à un template de rendu
2"dynamic_slot": {
3    "type": "stack_panel",
4    "orientation": "horizontal",
5    "$index|default": 0,
6    "$size|default": [0, 0],
7    "$content|default": "",
8    "collection_name": "container_items",
9    "controls": [
10        {
11            "grid_item@$content": {
12                "collection_index": "$index"
13            }
14        }
15    ]
16}

$index correspond au slot du GuiContainer côté serveur. $content détermine quel template de rendu utiliser (texte, bouton, image, barre…).

Partie 10

🎨Frontend JSON-UI

Le design system La bibliothèque de composants

Avec des dizaines d'UIs à construire, chaque développeur ne peut pas réinventer le style à chaque fois. Le resource pack fournit une bibliothèque de composants réutilisables organisée en 5 namespaces comme Bootstrap ou Material UI, mais pour Minecraft.

Interface Paladium  Design System
Exemple d'UI produite avec le design system

Technique Les 5 namespaces

1pala_asset.*   = textures / icônes / fonds / éléments UI  (catalogue complet)
2pala_button.*  = boutons pré-stylés  (default / hover / pressed)
3pala_panel.*   = panels prêts à l'emploi  (header, background, close/back/hub, layout)
4pala_color.*   = palette RGBA centralisée  (50+ variables)
5common.*       = primitives de base partagées
Important : pala_asset.* (et pareil pour les autres) référence tout le catalogue d'assets disponibles. Les exemples ci-dessous illustrent la convention, pas l'exhaustivité.

pala_button Boutons pré-stylés

Chaque bouton définit 3 états visuels (default, hover, pressed) et hérite de default_button qui mappe automatiquement le clic JSON-UI vers le serveur. Un clic visuel devient un appel C# côté serveur.

1"green_button@default_button": {
2    "$default_button_texture": "textures/ui/.../paladium_button_green",
3    "$hover_button_texture":   "textures/ui/.../paladium_button_green_hover",
4    "$pressed_button_texture": "textures/ui/.../paladium_button_green_hover"
5}
6// back_button, home_button, close_button, red_button, select_button, arrow_left_button…

pala_panel Panels avec layout automatique

1 ligne suffit pour obtenir un panel complet avec titre, bouton fermer, retour, hub et zone de contenu injectable.

1"mon_menu@pala_panel.panel_large": {
2    "$title": "Ma Boutique",
3    "$content": "ma_boutique.contenu"
4}
5// panel_large, panel_extra_large, panel_medium, panel_popup, panel_chest_0/1…

pala_color Palette centralisée

50+ couleurs RGBA définies comme variables. Changer une couleur = 1 fichier modifié, toutes les UIs mises à jour.

1"$EF3926": [0.9373, 0.2235, 0.1490, 1],  // Rouge vif (Paladium)
2"$FFD101": [1,      0.8196, 0.0039, 1],  // Or
3"$3967FF": [0.2235, 0.4039, 1,      1],  // Bleu électrique
4// … et 47 autres

Galerie UIs produites avec le framework

Slide 1
Slide 2
Slide 3
Slide 4
Slide 5
Slide 6
Slide 7
Slide 8
Slide 9
Slide 10
Slide 11
Slide 12
Slide 13
Slide 14
Slide 15
Slide 16
Slide 17
Slide 18

Partie 10

🔗Full-Stack

Workflow d'équipe Backend ↔ Frontend séparés

Le framework a été pensé pour que deux développeurs puissent travailler en parallèle sur la même UI sans jamais se marcher dessus : l'un écrit la logique métier en C#, l'autre dispose les composants en JSON-UI. Le contrat entre les deux : un index de slot et un type de composant.

Backend C# La logique métier

1public class ShopUi : AbstractUi {
2    public override string Id => "shop_main";
3    public override IUi? BackButton => new HubUi();
4
5    public override List<IUiComponent> GetElements() => [
6        new LabelUiComponent("§6Boutique") { Id = "title" },
7        new ButtonUiComponent("§aÉpée") {
8            Id = "buy_sword",
9            OnClickAsync = async e => {
10                await ProcessPurchase(e.Player!, 500, Items.DiamondSword);
11                return true;
12            }
13        },
14        new ProgressBarUiComponent(75) { Id = "stock" },
15    ];
16}

Frontend JSON-UI Le rendu visuel

1"shop_screen@pala_panel.panel_large": {
2    "$title": "Boutique",
3    "$content": "shop.shop_content"
4}
5
6"shop_content": {
7    "type": "panel",
8    "controls": [
9        { "title@pala_gui.text":         { "$index": 12, "$size": [200, 14] } },
10        { "buy@pala_gui.button":         { "$index": 13, "$button_ref": "pala_button.green_button" } },
11        { "stock@pala_gui.progress_bar": { "$index": 14, "$bar_green": "pala_asset.bar_green" } }
12    ]
13}
✅ Les deux développeurs ne se marchent jamais dessus. Le backend gère la logique, le frontend gère le rendu.

Résumé des solutions

ContrainteSolutionGain
Pas d'API GUI native sur BedrockEntité virtuelle invisible + conteneur détournéGUI complète sans mod client
Pas de canal de données serveur→clientCustomName de l'item = canal de donnéesTexte, images, valeurs, booléens
JSON-UI ne sait pas parser des donnéesExpressions %.Ns, -, * 1 comme mini-langageSplit, substring, contains, toNumber
Pas de String.split() en JSON-UICompteur frame-by-frame #searchParsing multi-valeurs
Taille limitée (27 slots avec un bloc)Entité custom avec 140 slotsUIs complexes avec navigation
Clics multiples / spam asyncLock + PendingTask par slotAnti-dupe, thread-safe
Composants imbriquésPattern Composite + AdditionalComponentArbre de profondeur illimitée
Slots vides = objets dupliquésVoidUiComponent.Instance (Singleton)Un seul objet pour tous
Chaque dev réinvente le rendupala_gui / pala_button / pala_panelDesign system réutilisable
Couleurs incohérentes entre UIspala_color.json palette centraliséeIdentité visuelle garantie

Design patterns utilisés

PatternPourquoi
CompositeIUiComponent.AdditionalComponentComposants imbriqués récursivement
StrategyIButtonUiComponent, ISlotUiComponentComportements interchangeables via delegates
BuilderPalaUiBuilderTypeConstruction fluente de l'entité virtuelle
Template MethodAbstractUi.HandleRender()Séquence Init → Open commune
SingletonVoidUiComponent.InstanceUn seul objet pour tous les slots vides
FactoryPalaUiType.CreateContainer()Création découplée
ObserverJSON-UI Bindings #hover_textLe client observe les changements du conteneur
InterpreterExpressions de binding (%.Ns, -, * 1)Mini-langage pour parser les données

Ce que ce système m'a appris

  • 🔌

    Détourner un système existant

    Les conteneurs Minecraft → framework UI complet. Comprendre le protocole réseau sous-jacent ouvre des possibilités inattendues.

  • 📡

    Le CustomName comme canal universel

    Avec le bon encodage côté serveur et le bon parsing côté client, un simple nom d'item peut transmettre texte, nombres, booléens, chemins de textures et structures multi-valeurs.

  • 🧩

    La puissance des bindings Turing-incomplets

    Avec seulement %.Ns (substring), - (replace), * 1 (toNumber) et des comparaisons, on peut implémenter contains(), split(), des conditions, et de l'arithmétique.

  • 🏗️

    Investir dans le design system

    pala_gui, pala_button, pala_panel, pala_color rentabilisé dès la 3ème UI. L'équipe crée des écrans en minutes plutôt qu'en heures.

  • 🌳

    Le Pattern Composite pour les UIs

    Redoutablement efficace pour modéliser des interfaces, exactement comme le DOM. La récursivité sur AdditionalComponent permet un arbre de profondeur illimitée.

  • 🤝

    Séparer backend et frontend

    Même dans un contexte aussi contraint que Minecraft, la séparation permet à une équipe de travailler en parallèle sans conflits.

© 2026 Nathanaël Rallo. All rights reserved.