13 févr. 2021, 14:56

Hello !

Je suis un peu rouillé en modding, donc j’ai probablement fait des horreurs, mais j’essaie de refaire le mod Oversaturation (https://github.com/truenachtara/Oversaturation) en 1.16.x, pour m’amuser un peu plus dans ma survie solo… et c’est plus compliqué que prévu.

L’idée, c’est donc de remplacer la méthode addStats de la classe FoodStats pour retirer la limite de 20.
Malheureusement, l’attribut foodStats de PlayerEntity n’est plus public et n’a pas de setter.

J’ai donc essayé de créer une classe héritant de ServerPlayerEntity qui rajoute uniquement ce setter (moins je fais de modifications, mieux je me porte). Le contenu est le suivant :

public class BetterPlayerEntity extends ServerPlayerEntity {

    public BetterPlayerEntity(MinecraftServer server, ServerWorld worldIn, GameProfile profile) {
        super(server, worldIn, profile, new PlayerInteractionManager(worldIn));
    }

    public void setFoodStats(FoodStats stats) {
        this.foodStats = stats;
    }
}

Le new PlayerInteractionManager me permet de ne pas avoir tout un tas d’erreurs lors d’interactions avec le monde par la suite. Le code est inspiré de ce qui se trouve dans PlayerList#func_232644_a_, qui semble être la méthode utilisée pour le respawn d’un joueur.

Jusqu’ici, tout va bien, mais il faut maintenant que je remplace le joueur par mon nouveau joueur. J’utilise donc l’event EntityJoinWorldEvent pour modifier.

    @SubscribeEvent
    public void onEntityJoinWorld(EntityJoinWorldEvent event) {
        if (event.getEntity() instanceof ServerPlayerEntity){
            ServerPlayerEntity player = (ServerPlayerEntity) event.getEntity();
            FoodStats oldStats = player.getFoodStats();
            if (!(oldStats instanceof UncappedFoodStats)) {
                UncappedFoodStats newStats = new UncappedFoodStats();
                CompoundNBT foodnbt = new CompoundNBT();
                oldStats.write(foodnbt);
                newStats.read(foodnbt);

                BetterPlayerEntity playerEntity = new BetterPlayerEntity(player.server, player.getServerWorld(), player.getGameProfile());
                playerEntity.copyFrom(player, true);
                playerEntity.setFoodStats(newStats);
                player.copyFrom(playerEntity, true);

                playerEntity.remove(false);
            }
        }
    }

UncappedFoodStats est une classe héritant de FoodStats et modifiant uniquement la méthode addStats.
Le problème de cette méthode est que je dois créer une nouvelle instance de BetterPlayerEntity, qui va donc appeler les constructeurs de ServerPlayerEntity, PlayerEntity, LivingEntity, …
Je me retrouve donc avec deux instances du même joueur dans le monde, et je n’en veux qu’une seule.
Étant donné que la Map de PlayerList n’est pas accessible (map entre UUID et ServerPlayerEntity), réutiliser le joueur déjà créé à l’origine pour simplement lui copier mon joueur custom me semble être le plus simple. Le code fonctionne, mais de manière très temporaire (de moins d’une seconde à une vingtaine de secondes au plus, avant le crash).

Le problème vient sûrement du fait que deux entités correspondant au joueur existent, car j’ai un crash lors d’un tick (une NPE).

Voici les logs :

net.minecraft.crash.ReportedException: Ticking memory connection
	at net.minecraft.network.NetworkSystem.tick(NetworkSystem.java:154) ~[forge:?] {re:classloading}
	at net.minecraft.server.MinecraftServer.updateTimeLightAndEntities(MinecraftServer.java:898) ~[forge:?] {re:classloading,pl:accesstransformer:B}
	at net.minecraft.server.MinecraftServer.tick(MinecraftServer.java:820) ~[forge:?] {re:classloading,pl:accesstransformer:B}
	at net.minecraft.server.integrated.IntegratedServer.tick(IntegratedServer.java:84) ~[forge:?] {re:classloading,pl:runtimedistcleaner:A}
	at net.minecraft.server.MinecraftServer.func_240802_v_(MinecraftServer.java:663) ~[forge:?] {re:classloading,pl:accesstransformer:B}
	at net.minecraft.server.MinecraftServer.lambda$startServer$0(MinecraftServer.java:233) ~[forge:?] {re:classloading,pl:accesstransformer:B}
	at java.lang.Thread.run(Thread.java:748) [?:1.8.0_241] {}
Caused by: java.lang.NullPointerException
	at net.minecraft.item.crafting.ServerRecipeBook.sendPacket(ServerRecipeBook.java:60) ~[forge:?] {re:classloading}
	at net.minecraft.item.crafting.ServerRecipeBook.add(ServerRecipeBook.java:38) ~[forge:?] {re:classloading}
	at net.minecraft.entity.player.ServerPlayerEntity.unlockRecipes(ServerPlayerEntity.java:1076) ~[forge:?] {re:classloading,pl:accesstransformer:B}
	at net.minecraft.entity.player.ServerPlayerEntity.unlockRecipes(ServerPlayerEntity.java:1086) ~[forge:?] {re:classloading,pl:accesstransformer:B}
	at net.minecraft.advancements.AdvancementRewards.apply(AdvancementRewards.java:65) ~[forge:?] {re:classloading}
	at net.minecraft.advancements.PlayerAdvancements.grantCriterion(PlayerAdvancements.java:209) ~[forge:?] {re:classloading}
	at net.minecraft.advancements.ICriterionTrigger$Listener.grantCriterion(ICriterionTrigger.java:34) ~[forge:?] {re:classloading}
	at net.minecraft.advancements.criterion.AbstractCriterionTrigger.triggerListeners(AbstractCriterionTrigger.java:68) ~[forge:?] {re:classloading}
	at net.minecraft.advancements.criterion.InventoryChangeTrigger.trigger(InventoryChangeTrigger.java:56) ~[forge:?] {re:classloading}
	at net.minecraft.advancements.criterion.InventoryChangeTrigger.test(InventoryChangeTrigger.java:52) ~[forge:?] {re:classloading}
	at net.minecraft.entity.player.ServerPlayerEntity.sendSlotContents(ServerPlayerEntity.java:988) ~[forge:?] {re:classloading,pl:accesstransformer:B}
	at net.minecraft.inventory.container.Container.detectAndSendChanges(Container.java:141) ~[forge:?] {re:classloading}
	at net.minecraft.inventory.container.Container.addListener(Container.java:102) ~[forge:?] {re:classloading}
	at net.minecraft.entity.player.ServerPlayerEntity.addSelfToInternalCraftingInventory(ServerPlayerEntity.java:341) ~[forge:?] {re:classloading,pl:accesstransformer:B}
	at net.minecraft.server.management.PlayerList.initializeConnectionToPlayer(PlayerList.java:231) ~[forge:?] {re:classloading}
	at net.minecraft.network.login.ServerLoginNetHandler.tryAcceptPlayer(ServerLoginNetHandler.java:122) ~[forge:?] {re:classloading}
	at net.minecraft.network.login.ServerLoginNetHandler.tick(ServerLoginNetHandler.java:66) ~[forge:?] {re:classloading}
	at net.minecraft.network.NetworkManager.tick(NetworkManager.java:244) ~[forge:?] {re:classloading}
	at net.minecraft.network.NetworkSystem.tick(NetworkSystem.java:151) ~[forge:?] {re:classloading}
	... 6 more

J’ai bien songé à utiliser la méthode de respawn de PlayerList pour que la map prenne mon instance de joueur, mais il faut toujours que je supprime proprement l’autre joueur, et c’est ce qui me pose problème.
Du coup je suis un peu coincé pour la suite, je ne vois pas trop ce que je peux changer 😞

Merci d’avance pour votre aide !