Technical Deep-Dive
UI Framework
Un framework UI entièrement server-side pour Minecraft Bedrock, construit par détournement du protocole réseau.
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

🔍 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);É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.
IButtonUiComponentRéagit au clic (OnClick / OnClickAsync). Exemple : bouton d'achat.
ISlotUiComponentLe joueur peut déposer/retirer un item (OnPlace / OnTake). Exemple : slot de stockage.
IItemStackHolderUiComponentLe 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}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});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é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.
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-UILes 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-UILe 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-UILe 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.

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éespala_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 autresGalerie UIs produites avec le framework


















Partie 10
🔗Full-StackWorkflow 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}Résumé des solutions
| Contrainte | Solution | Gain |
|---|---|---|
| Pas d'API GUI native sur Bedrock | Entité virtuelle invisible + conteneur détourné | GUI complète sans mod client |
| Pas de canal de données serveur→client | CustomName de l'item = canal de données | Texte, images, valeurs, booléens |
| JSON-UI ne sait pas parser des données | Expressions %.Ns, -, * 1 comme mini-langage | Split, substring, contains, toNumber |
| Pas de String.split() en JSON-UI | Compteur frame-by-frame #search | Parsing multi-valeurs |
| Taille limitée (27 slots avec un bloc) | Entité custom avec 140 slots | UIs complexes avec navigation |
| Clics multiples / spam async | Lock + PendingTask par slot | Anti-dupe, thread-safe |
| Composants imbriqués | Pattern Composite + AdditionalComponent | Arbre de profondeur illimitée |
| Slots vides = objets dupliqués | VoidUiComponent.Instance (Singleton) | Un seul objet pour tous |
| Chaque dev réinvente le rendu | pala_gui / pala_button / pala_panel | Design system réutilisable |
| Couleurs incohérentes entre UIs | pala_color.json palette centralisée | Identité visuelle garantie |
Design patterns utilisés
| Pattern | Où | Pourquoi |
|---|---|---|
| Composite | IUiComponent.AdditionalComponent | Composants imbriqués récursivement |
| Strategy | IButtonUiComponent, ISlotUiComponent | Comportements interchangeables via delegates |
| Builder | PalaUiBuilderType | Construction fluente de l'entité virtuelle |
| Template Method | AbstractUi.HandleRender() | Séquence Init → Open commune |
| Singleton | VoidUiComponent.Instance | Un seul objet pour tous les slots vides |
| Factory | PalaUiType.CreateContainer() | Création découplée |
| Observer | JSON-UI Bindings #hover_text | Le client observe les changements du conteneur |
| Interpreter | Expressions 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.