Aller au contenu principal

Recette — Écran marchand à slots

Un écran de marchand avec des slots Minecraft interactifs (items déplaçables). Cette recette est intentionnellement complète : elle couvre le AbstractContainerMenu, l'enregistrement du MenuType via DeferredRegister, l'ApocalyContainerScreen, et l'ouverture correcte côté serveur.

La frontière client/serveur

Les slots Minecraft impliquent une synchronisation réseau entre le client et le serveur. Cette recette montre l'approche correcte pour un mod de production. Le TestContainerMenu du mod démo utilise MenuType = null — un raccourci valide uniquement pour les tests visuels. Ne reproduis pas le null en production.


Architecture

Serveur Client
─────── ──────
CoffreGuildeMenu (AbstractContainerMenu) → CoffreGuildeScreen (ApocalyContainerScreen)
MenuType enregistré via DeferredRegister ← ouvert via NetworkHooks.openScreen()

Étape 1 — Enregistrer le MenuType

package ca.monmod.common.registry;

import ca.monmod.client.screen.CoffreGuildeScreen;
import ca.monmod.common.menu.CoffreGuildeMenu;
import net.minecraft.world.inventory.MenuType;
import net.minecraftforge.common.extensions.IForgeMenuType;
import net.minecraftforge.network.IContainerFactory;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.registries.RegistryObject;

public class ModMenus {

// DeferredRegister déclaré dans ta classe principale de registres
public static final DeferredRegister<MenuType<?>> MENUS =
DeferredRegister.create(ForgeRegistries.MENU_TYPES, "monmod");

public static final RegistryObject<MenuType<CoffreGuildeMenu>> COFFRE_GUILDE =
MENUS.register("coffre_guilde",
() -> IForgeMenuType.create(CoffreGuildeMenu::fromNetwork));
}

Dans la classe principale du mod, enregistrer le DeferredRegister sur le bus du mod :

// Dans le constructeur de la classe principale (@Mod)
ModMenus.MENUS.register(modEventBus);

Étape 2 — Le container menu (côté commun / serveur)

package ca.monmod.common.menu;

import ca.monmod.common.registry.ModMenus;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.SimpleContainer;

public final class CoffreGuildeMenu extends AbstractContainerMenu {

// Stride : 18 px par slot + 2 px de marge = 20 px
public static final int SLOT_STRIDE = 20;

// Positions relatives au coin supérieur gauche du panneau (leftPos/topPos)
// X : PANEL_MARGIN (10 px) + 1 px marge slot = 11
// Y : HEADER (22 px) + DIVIDER_OFFSET (4 px) + marge interne (6 px) = 32
public static final int SLOT_ORIGIN_X = 11;
public static final int SLOT_ORIGIN_Y = 32;

public static final int COLS = 3;
public static final int ROWS = 3;

private final SimpleContainer storage;

/** Constructeur serveur — appelé quand le serveur ouvre le container. */
public CoffreGuildeMenu(int containerId, Inventory playerInventory) {
super(ModMenus.COFFRE_GUILDE.get(), containerId);

// 9 slots de stockage en grille 3×3
this.storage = new SimpleContainer(COLS * ROWS);
for (int row = 0; row < ROWS; row++) {
for (int col = 0; col < COLS; col++) {
int slotX = SLOT_ORIGIN_X + col * SLOT_STRIDE;
int slotY = SLOT_ORIGIN_Y + row * SLOT_STRIDE;
addSlot(new Slot(storage, row * COLS + col, slotX, slotY));
}
}
}

/** Constructeur client — appelé depuis le packet réseau (IForgeMenuType). */
public static CoffreGuildeMenu fromNetwork(int containerId, Inventory inv,
FriendlyByteBuf buf) {
return new CoffreGuildeMenu(containerId, inv);
// Si tu as des données supplémentaires à transmettre (ex. taille du coffre),
// lis-les depuis buf ici : int size = buf.readInt();
}

@Override
public boolean stillValid(Player player) {
// Valide tant que le joueur est en jeu — adapte selon ta logique métier
return true;
}

@Override
public ItemStack quickMoveStack(Player player, int index) {
// Shift-clic — implémenter selon tes règles de déplacement
// Ici : désactivé pour simplifier l'exemple
return ItemStack.EMPTY;
}
}

Étape 3 — L'écran côté client

package ca.monmod.client.screen;

import ca.tawess123.apocalyinterface.api.layout.Row;
import ca.tawess123.apocalyinterface.api.screen.ApocalyContainerScreen;
import ca.tawess123.apocalyinterface.api.widget.SlotGrid;
import ca.monmod.common.menu.CoffreGuildeMenu;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;

public final class CoffreGuildeScreen
extends ApocalyContainerScreen<CoffreGuildeMenu> {

public CoffreGuildeScreen(CoffreGuildeMenu menu, Inventory playerInventory) {
super(menu, playerInventory,
Component.translatable("monmod.screen.coffre"),
220, 120); // largeur, hauteur du panneau
}

@Override
protected void buildLayout(Row root) {
// leftPos et topPos sont disponibles ici (calculés par vanilla après super.init())
root.add(SlotGrid.builder()
.slots(this.menu.slots)
.screenOrigin(this.leftPos, this.topPos)
.build());
}
}

Étape 4 — Enregistrer l'écran côté client

// Dans le gestionnaire d'événements client (FMLClientSetupEvent ou ClientSetupEvent)
@SubscribeEvent
public static void onClientSetup(FMLClientSetupEvent event) {
event.enqueueWork(() ->
MenuScreens.register(ModMenus.COFFRE_GUILDE.get(), CoffreGuildeScreen::new)
);
}

Étape 5 — Ouvrir le container côté serveur

// Dans une commande, un handler de packet C2S, ou un écouteur d'interaction :
@SubscribeEvent
public static void onPlayerInteract(PlayerInteractEvent.RightClickBlock event) {
Player player = event.getEntity();
if (player instanceof ServerPlayer serverPlayer) {
// Ouvrir le container — Forge crée le packet réseau automatiquement
NetworkHooks.openScreen(serverPlayer,
new SimpleMenuProvider(
(id, inv, p) -> new CoffreGuildeMenu(id, inv),
Component.translatable("monmod.screen.coffre")
)
);
}
}
Pourquoi NetworkHooks.openScreen() et pas Minecraft.setScreen()

NetworkHooks.openScreen() s'exécute côté serveur et envoie un packet au client pour qu'il ouvre l'écran. Minecraft.setScreen() s'exécute côté client et ne crée pas de synchronisation réseau — incompatible avec les containers.


Clés i18n requises

{
"monmod.screen.coffre": "Coffre de Guilde"
}

Résultat

  • Panneau 220×120 px, style Apocaly
  • Grille 3×3 de slots avec fond stylé (remplace le fond gris vanilla)
  • Items déplaçables depuis l'inventaire du joueur
  • Synchronisation automatique via le système container vanilla de Forge

Étendre cette recette

BesoinModification
Ajouter des infos à gauche des slotsAjouter un Label ou Card dans buildLayout(Row root) avant le SlotGrid
Passer des données du serveur au clientEncoder dans FriendlyByteBuf dans le MenuProvider et décoder dans fromNetwork()
Slots joueur (inventaire)Ajouter 36 slots à CoffreGuildeMenu avec positions y ≥ 100
Grille 4×9 (coffre double)Changer ROWS = 9, COLS = 4 et ajuster panelHeight

Voir aussi