Difference between revisions of "Tutorial 1.18 Episode 7"

From McJty Modding
Jump to: navigation, search
Line 146: Line 146:
 
     }
 
     }
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
 +
===Saved Data (ManaManager)===
 +
 +
In this tutorial we want to keep mana in every chunk. This mana is generated randomly for every chunk so we need to store it somewhere. You could use capabilities for this but because we will already be using capabilities for attaching mana to the player we're going to use another technique for storing it in the world. Minecraft has the SavedData class that you can use for this. It's basically a way to attach arbitrary data to a level which is also going to be persisted. Here is our ManaManager class ([https://github.com/McJty/TutorialV3/blob/main/src/main/java/com/example/tutorialv3/manasystem/data/ManaManager.java ManaManager.java on Github]). See the comments in this class for some explanation:
 +
 +
<syntaxhighlight lang="java">
 +
public class ManaManager extends SavedData {
 +
 +
    // For every chunk that we visisted already we store the mana currently available. Note that this is done in a lazy way. Chunks that we didn't visit will not have mana yet
 +
    private final Map<ChunkPos, Mana> manaMap = new HashMap<>();
 +
    private final Random random = new Random();
 +
 +
    // Keep a counter so that we don't send mana back to the client every tick
 +
    private int counter = 0;
 +
 +
    // This function can be used to get access to the mana manager for a given level. It can only be called server-side!
 +
    @Nonnull
 +
    public static ManaManager get(Level level) {
 +
        if (level.isClientSide) {
 +
            throw new RuntimeException("Don't access this client-side!");
 +
        }
 +
        // Get the vanilla storage manager from the level
 +
        DimensionDataStorage storage = ((ServerLevel)level).getDataStorage();
 +
        // Get the mana manager if it already exists. Otherwise create a new one. Note that both
 +
        // invocations of ManaManager::new actually refer to a different constructor. One without parameters
 +
        // and the other with a CompoundTag parameter
 +
        return storage.computeIfAbsent(ManaManager::new, ManaManager::new, "manamanager");
 +
    }
 +
 +
    @NotNull
 +
    private Mana getManaInternal(BlockPos pos) {
 +
        // Get the mana at a certain chunk. If this is the first time then we fill in the manaMap using computeIfAbsent
 +
        ChunkPos chunkPos = new ChunkPos(pos);
 +
        return manaMap.computeIfAbsent(chunkPos, cp -> new Mana(random.nextInt(ManaConfig.CHUNK_MAX_MANA.get()) + ManaConfig.CHUNK_MIN_MANA.get()));
 +
    }
 +
 +
    public int getMana(BlockPos pos) {
 +
        Mana mana = getManaInternal(pos);
 +
        return mana.getMana();
 +
    }
 +
 +
    public int extractMana(BlockPos pos) {
 +
        Mana mana = getManaInternal(pos);
 +
        int present = mana.getMana();
 +
        if (present > 0) {
 +
            mana.setMana(present-1);
 +
            setDirty();
 +
            return 1;
 +
        } else {
 +
            return 0;
 +
        }
 +
    }
 +
 +
    public void tick(Level level) {
 +
        counter--;
 +
        if (counter <= 0) {
 +
            counter = 10;
 +
            // Synchronize the mana to the players in this world
 +
            // todo expansion: keep the previous data that was sent to the player and only send if changed
 +
            level.players().forEach(player -> {
 +
                if (player instanceof ServerPlayer serverPlayer) {
 +
                    int playerMana = serverPlayer.getCapability(PlayerManaProvider.PLAYER_MANA)
 +
                            .map(PlayerMana::getMana)
 +
                            .orElse(-1);
 +
                    int chunkMana = getMana(serverPlayer.blockPosition());
 +
                    Messages.sendToPlayer(new PacketSyncManaToClient(playerMana, chunkMana), serverPlayer);
 +
                }
 +
            });
 +
 +
            // todo expansion: here it would be possible to slowly regenerate mana in chunks
 +
        }
 +
    }
 +
 +
    public ManaManager() {
 +
    }
 +
 +
    public ManaManager(CompoundTag tag) {
 +
        ListTag list = tag.getList("mana", Tag.TAG_COMPOUND);
 +
        for (Tag t : list) {
 +
            CompoundTag manaTag = (CompoundTag) t;
 +
            Mana mana = new Mana(manaTag.getInt("mana"));
 +
            ChunkPos pos = new ChunkPos(manaTag.getInt("x"), manaTag.getInt("z"));
 +
            manaMap.put(pos, mana);
 +
        }
 +
    }
 +
 +
    @Override
 +
    public CompoundTag save(CompoundTag tag) {
 +
        ListTag list = new ListTag();
 +
        manaMap.forEach((chunkPos, mana) -> {
 +
            CompoundTag manaTag = new CompoundTag();
 +
            manaTag.putInt("x", chunkPos.x);
 +
            manaTag.putInt("z", chunkPos.z);
 +
            manaTag.putInt("mana", mana.getMana());
 +
            list.add(manaTag);
 +
        });
 +
        tag.put("mana", list);
 +
        return tag;
 +
    }
 +
 +
}
 +
</syntaxhighlight>
 +
 +
 +
{{warning|1=SavedData is local to each level (dimension). If you want global data it's recommended to attach it to the overworld since it's easy to access that at all times}}

Revision as of 09:39, 7 March 2022

Links

Introduction

In this tutorial we explain various ways that you can store data and also communicate that data to the client. We will cover world data, player capabilities, and networking. In addition we also cover a new way to make render overlays (HUD's)

Key Bindings

In this tutorial we want a key binding that the player can press to gather mana from the chunk. How we will store and make this mana is for later but let's first make the key binding. First add the class to actually define the key binding (KeyBindings.java on Github). In this class we make a new keymapping which by default is assigned to the period key. The player can reconfigure this in the standard Minecraft options screen:

public class KeyBindings {

    public static final String KEY_CATEGORIES_TUTORIAL = "key.categories.tutorial";
    public static final String KEY_GATHER_MANA = "key.gatherMana";

    public static KeyMapping gatherManaKeyMapping;

    public static void init() {
        // Use KeyConflictContext.IN_GAME to indicate this key is meant for usage in-game
        gatherManaKeyMapping = new KeyMapping(KEY_GATHER_MANA, KeyConflictContext.IN_GAME, InputConstants.getKey("key.keyboard.period"), KEY_CATEGORIES_TUTORIAL);
        ClientRegistry.registerKeyBinding(gatherManaKeyMapping);
    }
}

We also need to call this method. We do that in ClientSetup.init() (ClientSetup.java on Github):

    public static void init(FMLClientSetupEvent event) {
        ...
        KeyBindings.init();
    }

In addition to the key binding we also need an input handler. That input handler will be called whenever the key is pressed. To do this we listen to the KeyInputEvent and when that event is received we consume the keypress (so it doesn't get used for something else) and send a message to the server:

18px-OOjs_UI_icon_notice-destructive.svg.png Warning: To also allow the player to bind this action on a mouse button you would also need to listen to InputEvent.MouseInputEvent
public class KeyInputHandler {

    public static void onKeyInput(InputEvent.KeyInputEvent event) {
        if (KeyBindings.gatherManaKeyMapping.consumeClick()) {
            Messages.sendToServer(new PacketGatherMana());
        }
    }
}

We need to register this event. Again edit ClientSetup.init() for that:

We also need to call this method. We do that in ClientSetup.init():

    public static void init(FMLClientSetupEvent event) {
        ...
        MinecraftForge.EVENT_BUS.addListener(KeyInputHandler::onKeyInput);
        KeyBindings.init();
    }

Networking

Whenever a key is pressed on the client we need to send a message to the server. The reason for that is that actual logic and the mana system will live on the server. To support networking add the following class (Messages.java on Github). This class is the main entry point for networking. It basically makes use of a SimpleChannel which is a helper class from Forge:

public class Messages {

    private static SimpleChannel INSTANCE;

    // Every packet needs a unique ID (unique for this channel)
    private static int packetId = 0;
    private static int id() {
        return packetId++;
    }

    public static void register() {
        // Make the channel. If needed you can do version checking here
        SimpleChannel net = NetworkRegistry.ChannelBuilder
                .named(new ResourceLocation(TutorialV3.MODID, "messages"))
                .networkProtocolVersion(() -> "1.0")
                .clientAcceptedVersions(s -> true)
                .serverAcceptedVersions(s -> true)
                .simpleChannel();

        INSTANCE = net;

        // Register all our packets. We only have one right now. The new message has a unique ID, an indication
        // of how it is going to be used (from client to server) and ways to encode and decode it. Finally 'handle'
        // will actually execute when the packet is received
        net.messageBuilder(PacketGatherMana.class, id(), NetworkDirection.PLAY_TO_SERVER)
                .decoder(PacketGatherMana::new)
                .encoder(PacketGatherMana::toBytes)
                .consumer(PacketGatherMana::handle)
                .add();
    }

    public static <MSG> void sendToServer(MSG message) {
        INSTANCE.sendToServer(message);
    }

    public static <MSG> void sendToPlayer(MSG message, ServerPlayer player) {
        INSTANCE.send(PacketDistributor.PLAYER.with(() -> player), message);
    }
}

And we also need the actual message (PacketGatherMana.java on Github). This message is sent from the client to the server and otherwise contains no data. That's why the toBytes() and constructors are empty. The handle() method is currently empty because we don't have the mana system yet:

public class PacketGatherMana {

    public static final String MESSAGE_NO_MANA = "message.nomana";

    public PacketGatherMana() {
    }

    public PacketGatherMana(FriendlyByteBuf buf) {
    }

    public void toBytes(FriendlyByteBuf buf) {
    }

    public boolean handle(Supplier<NetworkEvent.Context> supplier) {
        NetworkEvent.Context ctx = supplier.get();
        ctx.enqueueWork(() -> {
            // Here we are server side
            ...  TODO for later
        });
        return true;
    }
}

We also need to register our Messages class. Do that by calling Messages.register() from ModSetup (ModSetup on Github):

    public static void init(FMLCommonSetupEvent event) {
        ...
        Messages.register();
    }

Saved Data (ManaManager)

In this tutorial we want to keep mana in every chunk. This mana is generated randomly for every chunk so we need to store it somewhere. You could use capabilities for this but because we will already be using capabilities for attaching mana to the player we're going to use another technique for storing it in the world. Minecraft has the SavedData class that you can use for this. It's basically a way to attach arbitrary data to a level which is also going to be persisted. Here is our ManaManager class (ManaManager.java on Github). See the comments in this class for some explanation:

public class ManaManager extends SavedData {

    // For every chunk that we visisted already we store the mana currently available. Note that this is done in a lazy way. Chunks that we didn't visit will not have mana yet
    private final Map<ChunkPos, Mana> manaMap = new HashMap<>();
    private final Random random = new Random();

    // Keep a counter so that we don't send mana back to the client every tick
    private int counter = 0;

    // This function can be used to get access to the mana manager for a given level. It can only be called server-side!
    @Nonnull
    public static ManaManager get(Level level) {
        if (level.isClientSide) {
            throw new RuntimeException("Don't access this client-side!");
        }
        // Get the vanilla storage manager from the level
        DimensionDataStorage storage = ((ServerLevel)level).getDataStorage();
        // Get the mana manager if it already exists. Otherwise create a new one. Note that both
        // invocations of ManaManager::new actually refer to a different constructor. One without parameters
        // and the other with a CompoundTag parameter
        return storage.computeIfAbsent(ManaManager::new, ManaManager::new, "manamanager");
    }

    @NotNull
    private Mana getManaInternal(BlockPos pos) {
        // Get the mana at a certain chunk. If this is the first time then we fill in the manaMap using computeIfAbsent
        ChunkPos chunkPos = new ChunkPos(pos);
        return manaMap.computeIfAbsent(chunkPos, cp -> new Mana(random.nextInt(ManaConfig.CHUNK_MAX_MANA.get()) + ManaConfig.CHUNK_MIN_MANA.get()));
    }

    public int getMana(BlockPos pos) {
        Mana mana = getManaInternal(pos);
        return mana.getMana();
    }

    public int extractMana(BlockPos pos) {
        Mana mana = getManaInternal(pos);
        int present = mana.getMana();
        if (present > 0) {
            mana.setMana(present-1);
            setDirty();
            return 1;
        } else {
            return 0;
        }
    }

    public void tick(Level level) {
        counter--;
        if (counter <= 0) {
            counter = 10;
            // Synchronize the mana to the players in this world
            // todo expansion: keep the previous data that was sent to the player and only send if changed
            level.players().forEach(player -> {
                if (player instanceof ServerPlayer serverPlayer) {
                    int playerMana = serverPlayer.getCapability(PlayerManaProvider.PLAYER_MANA)
                            .map(PlayerMana::getMana)
                            .orElse(-1);
                    int chunkMana = getMana(serverPlayer.blockPosition());
                    Messages.sendToPlayer(new PacketSyncManaToClient(playerMana, chunkMana), serverPlayer);
                }
            });

            // todo expansion: here it would be possible to slowly regenerate mana in chunks
        }
    }

    public ManaManager() {
    }

    public ManaManager(CompoundTag tag) {
        ListTag list = tag.getList("mana", Tag.TAG_COMPOUND);
        for (Tag t : list) {
            CompoundTag manaTag = (CompoundTag) t;
            Mana mana = new Mana(manaTag.getInt("mana"));
            ChunkPos pos = new ChunkPos(manaTag.getInt("x"), manaTag.getInt("z"));
            manaMap.put(pos, mana);
        }
    }

    @Override
    public CompoundTag save(CompoundTag tag) {
        ListTag list = new ListTag();
        manaMap.forEach((chunkPos, mana) -> {
            CompoundTag manaTag = new CompoundTag();
            manaTag.putInt("x", chunkPos.x);
            manaTag.putInt("z", chunkPos.z);
            manaTag.putInt("mana", mana.getMana());
            list.add(manaTag);
        });
        tag.put("mana", list);
        return tag;
    }

}


18px-OOjs_UI_icon_notice-destructive.svg.png Warning: SavedData is local to each level (dimension). If you want global data it's recommended to attach it to the overworld since it's easy to access that at all times