Episode 10
Back: Index
Introduction
In this tutorial we explain how commands work. How you can use networking to communicate between client and server and how to open a GUI that is not a container.
Top level command
First we make a ModCommands class which is responsible for registering all our commands:
public class ModCommands {
public static void register(CommandDispatcher<CommandSource> dispatcher) {
LiteralCommandNode<CommandSource> cmdTut = dispatcher.register(
Commands.literal(MyTutorial.MODID)
.then(CommandTest.register(dispatcher))
.then(CommandSpawner.register(dispatcher))
);
dispatcher.register(Commands.literal("tut").redirect(cmdTut));
}
}
In this example we have a top-level command which (by convention) is called the same as our modid. To make things easier we also register this command under the 'tut' alias.
Then there are two sub commands: 'test' and 'spawn'.
To actually register this top-level command you have to add this class:
public class ForgeEventHandlers {
@SubscribeEvent
public void serverLoad(FMLServerStartingEvent event) {
ModCommands.register(event.getCommandDispatcher());
}
}
and register it in ModSetup like this:
public void init() {
MinecraftForge.EVENT_BUS.register(new ForgeEventHandlers());
}
Simple test command
First we have a very simple 'test' command that just tells 'Hello World' to the player:
public class CommandTest implements Command<CommandSource> {
private static final CommandTest CMD = new CommandTest();
public static ArgumentBuilder<CommandSource, ?> register(CommandDispatcher<CommandSource> dispatcher) {
return Commands.literal("test")
.requires(cs -> cs.hasPermissionLevel(0))
.executes(CMD);
}
@Override
public int run(CommandContext<CommandSource> context) throws CommandSyntaxException {
context.getSource().sendFeedback(new StringTextComponent("Hello world!"), false);
return 0;
}
}
The spawn command
The 'spawn' command does a bit more:
public class CommandSpawner implements Command<CommandSource> {
private static final CommandSpawner CMD = new CommandSpawner();
public static ArgumentBuilder<CommandSource, ?> register(CommandDispatcher<CommandSource> dispatcher) {
return Commands.literal("spawn")
.requires(cs -> cs.hasPermissionLevel(0))
.executes(CMD);
}
@Override
public int run(CommandContext<CommandSource> context) throws CommandSyntaxException {
ServerPlayerEntity player = context.getSource().asPlayer();
Networking.INSTANCE.sendTo(new PacketOpenGui(), player.connection.netManager, NetworkDirection.PLAY_TO_CLIENT);
return 0;
}
}
Because commands only run server-side we need to send a message to the client so that we can open our GUI.
Networking setup
To set up networking lets create a new Networking class:
public class Networking {
public static SimpleChannel INSTANCE;
private static int ID = 0;
public static int nextID() {
return ID++;
}
public static void registerMessages() {
INSTANCE = NetworkRegistry.newSimpleChannel(new ResourceLocation(MyTutorial.MODID, "mytutorial"), () -> "1.0", s -> true, s -> true);
INSTANCE.registerMessage(nextID(),
PacketOpenGui.class,
PacketOpenGui::toBytes,
PacketOpenGui::new,
PacketOpenGui::handle);
INSTANCE.registerMessage(nextID(),
PacketSpawn.class,
PacketSpawn::toBytes,
PacketSpawn::new,
PacketSpawn::handle);
}
}
In this class we first create a new simple channel for our mod and then register two messages to it. The first one is PacketOpenGui which is responsible for opening our gui as a result of the 'spawn' command. The second one is PacketSpawn which is a message from the client (gui) to the server to actually spawn our mob. The first parameter to registerMessage is a unique ID (unique for your message handler). The next parameters are used to decode/encode your message (to a byte buffer) and to actually handle it.
Opening a gui
The PacketOpenGui message is really simple. It doesn't have any data in it so most methods are empty.
public class PacketOpenGui {
public PacketOpenGui(PacketBuffer buf) {
}
public void toBytes(PacketBuffer buf) {
}
public PacketOpenGui() {
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx.get().enqueueWork(SpawnerScreen::open);
ctx.get().setPacketHandled(true);
}
}
When handling a message be aware that this is done in a separate network thread. So don't access the Minecraft world or anything like that there. The best way to handle this is to use enqueueWork which takes care of handling your code in the right thread. Also don't forget to set setPacketHandled to true.
The only thing we do in this message is to open our gui with a method in SpawnerScreen (see later).
Spawning a mob
When we press a spawn button in the gui a message will be sent to the server indicating what to spawn:
public class PacketSpawn {
private final String id;
private final DimensionType type;
private final BlockPos pos;
public PacketSpawn(PacketBuffer buf) {
id = buf.readString();
type = DimensionType.getById(buf.readInt());
pos = buf.readBlockPos();
}
public PacketSpawn(String id, DimensionType type, BlockPos pos) {
this.id = id;
this.type = type;
this.pos = pos;
}
public void toBytes(PacketBuffer buf) {
buf.writeString(id);
buf.writeInt(type.getId());
buf.writeBlockPos(pos);
}
public void handle(Supplier<NetworkEvent.Context> ctx) {
ctx.get().enqueueWork(() -> {
ServerWorld spawnWorld = ctx.get().getSender().world.getServer().getWorld(type);
EntityType<?> entityType = ForgeRegistries.ENTITIES.getValue(new ResourceLocation(id));
if (entityType == null) {
throw new IllegalStateException("This cannot happen! Unknown id '" + id + "'!");
}
entityType.spawn(spawnWorld, null, null, pos, SpawnReason.SPAWN_EGG, true, true);
});
ctx.get().setPacketHandled(true);
}
}
In this example we could just as well have used the player position to spawn our mob but to illustrate how you can add more data we actually send over the spawn position and dimension from the client. Note that using the numeric id of a DimensionType is normally not good. But for networking this is the most optimal way to send the dimension data over the network and network packets should always be as small as possible.
The actual GUI
public class SpawnerScreen extends Screen {
private static final int WIDTH = 179;
private static final int HEIGHT = 151;
private ResourceLocation GUI = new ResourceLocation(MyTutorial.MODID, "textures/gui/spawner_gui.png");
public SpawnerScreen() {
super(new StringTextComponent("Spawn something"));
}
@Override
protected void init() {
int relX = (this.width - WIDTH) / 2;
int relY = (this.height - HEIGHT) / 2;
addButton(new Button(relX + 10, relY + 10, 160, 20, "Skeleton", button -> spawn("minecraft:skeleton")));
addButton(new Button(relX + 10, relY + 37, 160, 20, "Zombie", button -> spawn("minecraft:zombie")));
addButton(new Button(relX + 10, relY + 64, 160, 20, "Cow", button -> spawn("minecraft:cow")));
addButton(new Button(relX + 10, relY + 91, 160, 20, "Sheep", button -> spawn("minecraft:sheep")));
addButton(new Button(relX + 10, relY + 118, 160, 20, "Chicken", button -> spawn("minecraft:chicken")));
}
@Override
public boolean isPauseScreen() {
return false;
}
private void spawn(String id) {
Networking.INSTANCE.sendToServer(new PacketSpawn(id, minecraft.player.dimension, minecraft.player.getPosition()));
minecraft.displayGuiScreen(null);
}
@Override
public void render(int mouseX, int mouseY, float partialTicks) {
GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F);
this.minecraft.getTextureManager().bindTexture(GUI);
int relX = (this.width - WIDTH) / 2;
int relY = (this.height - HEIGHT) / 2;
this.blit(relX, relY, 0, 0, WIDTH, HEIGHT);
super.render(mouseX, mouseY, partialTicks);
}
public static void open() {
Minecraft.getInstance().displayGuiScreen(new SpawnerScreen());
}
}
Because our gui doesn't depend on a container we extend it from Screen instead of ContainerScreen. We also use the vanilla Button system here. To actually spawn our mob we need to be on the server again and that's why the 'spawn' method sends a PacketSpawn message to the server with all the needed information. After that the screen is closed.
The 'open' method at the end is responsible for opening the gui and is called from our PacketOpenGui message.