Jump to content

[Version 1.18.2, SOLVED] Custom status effect that changes the player's (or any entity's) size, Part II


LeeCrafts

Recommended Posts

Hello,

I have been able to properly increase the player's hitbox size whenever that player gains a custom status effect (named "Big"). But, I have yet to increase the player's render size.

CMIIW, this is how it should work:

  • Each (server) player should have its own capability that just stores a boolean, which I name isBig.
  • isBig is true if a player has the Big status effect. I am unsure if I have to check--for every tick--if the player has the effect.
  • Each player must send a packet to every other player's client. The packet stores the player's capability's provided NBT (which only contains a boolean, "isBig"). In other words, capability data must be synced across clients. 
    • Several events are needed for this: StartTracking, PlayerLoggedInEvent, PlayerRespawnEvent, and PlayerChangedDimensionEvent.
  • Finally, using RenderPlayerEvent.Pre, each client-side player's capability is checked. If isBig == true, then that player's render size is increased.

^ lol dont read this

 

Unfortunately, I have trouble executing this by code. (A lot of) relevant code is below:

// Capability interface
public interface IBig {
    void setBig(boolean isBig);
    boolean getBig();
    void sync(Big big, ServerPlayer player);
}

 

// Capability implementing the interface
public class Big implements IBig {
    private boolean isBig;
    public Big() {
        this.isBig = false;
    }

    @Override
    public void setBig(boolean isBig) {
        this.isBig = isBig;
    }

    @Override
    public boolean getBig() {
        return this.isBig;
    }

    @Override
    public void sync(Big big, ServerPlayer player) {
        // checks if player has the Big status effect
        big.setBig(player.getActiveEffectsMap() != null && player.hasEffect(ModEffects.BIG.get()));
      
        CompoundTag nbt = new CompoundTag();
        nbt.putBoolean("big", big.getBig());
        PacketHandler.INSTANCE.send(
                PacketDistributor.TRACKING_ENTITY_AND_SELF.noArg(),
                new ClientboundMobSizeUpdatePacket(nbt)
        );
    }
}

 

// Provider for the capability
public class BigProvider implements ICapabilitySerializable<CompoundTag> {

    private final Big big = new Big();
    private final LazyOptional<IBig> bigLazyOptional = LazyOptional.of(() -> big);

    @NotNull
    @Override
    public <T> LazyOptional<T> getCapability(@NotNull Capability<T> cap, @Nullable Direction side) {
//        return bigLazyOptional.cast();
        return ModCapabilities.BIG_CAPABILITY.orEmpty(cap, bigLazyOptional);
    }

    @Override
    public CompoundTag serializeNBT() {
        if (ModCapabilities.BIG_CAPABILITY == null) {
            return new CompoundTag();
        }
        CompoundTag nbt = new CompoundTag();
        nbt.putBoolean("big", big.getBig());
        return nbt;
    }

    @Override
    public void deserializeNBT(CompoundTag nbt) {
        if (ModCapabilities.BIG_CAPABILITY != null) {
            big.setBig(nbt.getBoolean("big"));
        }
    }

    public void invalidate() { bigLazyOptional.invalidate(); }
}

 

public class ModCapabilities {
    // other capabilities...
    
    // instance of the Big capability
    public static Capability<IBig> BIG_CAPABILITY = CapabilityManager.get(new CapabilityToken<>(){});

}

 

// Events for registering and attaching capabilities
@Mod.EventBusSubscriber(modid = ExampleMod.MOD_ID)
public class ModEvents {

    @SubscribeEvent
    public static void registerCapabilities(RegisterCapabilitiesEvent event) {
        event.register(IBig.class);
    }

    @SubscribeEvent
    public static void onAttachCapabilitiesEventBig(AttachCapabilitiesEvent<Entity> event) {
        if (event.getObject() instanceof Player && !event.getObject().getCommandSenderWorld().isClientSide) {
            BigProvider bigProvider = new BigProvider();
            event.addCapability(new ResourceLocation(ExampleMod.MOD_ID, "big"), bigProvider);
            event.addListener(bigProvider::invalidate);
        }
    }

}

 

// FMLCommonSetupEvent for initializing the packet handler
@Mod.EventBusSubscriber(modid = ExampleMod.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD)
public class ModCommonEvents {
    @SubscribeEvent
    public static void commonSetup(FMLCommonSetupEvent event) {
        event.enqueueWork(PacketHandler::init);
    }
}

 

// The packet handler class itself
public class PacketHandler {
    private static final String PROTOCOL_VERSION = "1";

    public static SimpleChannel INSTANCE = NetworkRegistry.newSimpleChannel(
            new ResourceLocation(ExampleMod.MOD_ID, "main"), () -> PROTOCOL_VERSION, PROTOCOL_VERSION::equals,
            PROTOCOL_VERSION::equals);

    private PacketHandler() {
    }

    public static void init() {
        int index = 0;
        INSTANCE.messageBuilder(ClientboundMobSizeUpdatePacket.class, index++, NetworkDirection.PLAY_TO_CLIENT)
                .encoder(ClientboundMobSizeUpdatePacket::encode).decoder(ClientboundMobSizeUpdatePacket::new)
                .consumer(ClientboundMobSizeUpdatePacket::handle).add();
    }

}

 

// Packet class
public class ClientboundMobSizeUpdatePacket {
    private CompoundTag nbt;
    public ClientboundMobSizeUpdatePacket(CompoundTag nbt) {
        this.nbt = nbt;
    }

    public ClientboundMobSizeUpdatePacket(FriendlyByteBuf buffer) {
        this(buffer.readNbt());
    }

    public void encode(FriendlyByteBuf buffer) {
        buffer.writeNbt(this.nbt);
    }

    public boolean handle(Supplier<NetworkEvent.Context> ctx) {
        final var success = new AtomicBoolean(false);
        ctx.get().enqueueWork(() -> {
            if (ctx.get().getDirection().getReceptionSide().isClient() && ctx.get().getDirection().getOriginationSide().isServer()) {
                // the player variable should be a client-side player whose capability is to be updated
                final LocalPlayer player = Minecraft.getInstance().player;
                DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> {
                    player.getCapability(ModCapabilities.BIG_CAPABILITY).ifPresent(iBig -> {
                        Big big = (Big) iBig;
                        big.setBig(new BigProvider().serializeNBT().getBoolean("big"));
                    });
                });
                success.set(true);
            }
        });
        ctx.get().setPacketHandled(success.get());
        return success.get();
    }
}

 

// Event handler for client-side players. This is where the render size change should happen.
@Mod.EventBusSubscriber(modid = ExampleMod.MOD_ID, value = Dist.CLIENT)
public class ModClientEvents {
    // must be client-side only
    @SubscribeEvent
    public static void big(RenderPlayerEvent.Pre event) {
        // Do I need to use Minecraft.getInstance() to get the player? Just wondering because we're dealing with the client side here.
//        Player player = event.getPlayer();
        final Player player = Minecraft.getInstance().level.getPlayerByUUID(event.getPlayer().getUUID());

        if (player != null) {
            player.getCapability(ModCapabilities.BIG_CAPABILITY).ifPresent(iBig -> {
                // Through some debugging, I have found out that this section never gets reached
              
                Big big = (Big) iBig;
                if (big.getBig()) {
                    event.getPoseStack().scale(8.0F, 2.0F, 8.0F);
                }
            });
        }
    }
}

 

// Event handlers for server-side players

// I must set the value to Dist.DEDICATED_SERVER otherwise a crash will occur
@Mod.EventBusSubscriber(modid = ExampleMod.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE, value = Dist.DEDICATED_SERVER)
public class ForgeCommonEvents {
    
    @SubscribeEvent
    public static void startTracking(PlayerEvent.StartTracking event) {
        Player player = (Player) event.getTarget();
        ServerPlayer target = (ServerPlayer) event.getPlayer();
        if (!player.level.isClientSide()) {
            player.getCapability(ModCapabilities.BIG_CAPABILITY).ifPresent(iBig -> {
                Big big = (Big) iBig;
              
                // checks if player has the Big status effect
                big.setBig(player.getActiveEffectsMap() != null && player.hasEffect(ModEffects.BIG.get()));
              
                CompoundTag nbt = new CompoundTag();
                nbt.putBoolean("big", big.getBig());
                PacketHandler.INSTANCE.send(
                        PacketDistributor.PLAYER.with(() -> target),
                        new ClientboundMobSizeUpdatePacket(nbt)
                );
            });
        }
    }

    @SubscribeEvent
    public static void playerLoggedIn(PlayerEvent.PlayerLoggedInEvent event) {
        ServerPlayer player = (ServerPlayer) event.getPlayer();
        if (!player.level.isClientSide()) {
            player.getCapability(ModCapabilities.BIG_CAPABILITY).ifPresent(iBig -> {
                Big big = (Big) iBig;
                iBig.sync(big, player);
            });
        }
    }

    @SubscribeEvent
    public static void playerRespawn(PlayerEvent.PlayerRespawnEvent event) {
        ServerPlayer player = (ServerPlayer) event.getPlayer();
        if (!player.level.isClientSide()) {
            player.getCapability(ModCapabilities.BIG_CAPABILITY).ifPresent(iBig -> {
                Big big = (Big) iBig;
                iBig.sync(big, player);
            });
        }
    }

    @SubscribeEvent
    public static void playerChangedDimension(PlayerEvent.PlayerChangedDimensionEvent event) {
        ServerPlayer player = (ServerPlayer) event.getPlayer();
        if (!player.level.isClientSide()) {
            player.getCapability(ModCapabilities.BIG_CAPABILITY).ifPresent(iBig -> {
                Big big = (Big) iBig;
                iBig.sync(big, player);
            });
        }
    }

}

 

When the player gets the Big status effect, it does not grow like it should. As indicated by the last section of code, the client-side player's capability is never present for some reason. Is my logic incorrect? I appreciate the help, thank you in advance.

Edited by LeeCrafts
Link to comment
Share on other sites

Thank you for the reply, I'll be testing the the needed changes.

Meanwhile...I don't know how I didn't find this out earlier, but the server player events (StartTracking, PlayerLoggedInEvent, PlayerRespawnEvent, and PlayerChangedDimensionEvent) never actually fire. Is it because the value argument in EventBusSubscriber is Dist.DEDICATED_SERVER? I have been testing my game via the ./gradlew runClient command, and I wonder if that's the reason.

Anyways, I removed the Dist.DEDICATED_SERVER part and the events do fire...but the game crashes right afterwards. The error message says:

java.lang.NullPointerException: Cannot invoke "net.minecraft.world.entity.Entity.getCommandSenderWorld()" because "entity" is null

...even when I check if event.getPlayer() is not null.

How do I solve this?

Edited by LeeCrafts
Link to comment
Share on other sites

The reason I'm using a capability is because I just want to learn how to sync capability data across clients in general. I feel like that's an important skill to learn.

To answer your other questions:

On 5/10/2022 at 3:36 AM, diesieben07 said:

If you expect your capability to be always present, why are you using ifPresent? This achieves nothing except waste CPU cycles.

In that case, I guess I should just use orElse() instead. That is, player.getCapability(ModCapabilities.BIG_CAPABILITY).orElse(new Big()). I am not entirely sure if this would be sufficient, though.

On 5/10/2022 at 3:36 AM, diesieben07 said:

What even? Why is this an AtomicBoolean?

Haha, I was following a tutorial on YouTube, and I guess using an AtomicBoolean would be a "safe" option.

On 5/10/2022 at 3:36 AM, diesieben07 said:

You completely ignore the data that was sent with your packet. Instead you create a new provider, which will have the default values, serialize it and use the serialized data... somehow? Why?

Perhaps I should change that line of code to big.setBig(nbt.getBoolean("big")).

Link to comment
Share on other sites

24 minutes ago, LeeCrafts said:

In that case, I guess I should just use orElse() instead. That is, player.getCapability(ModCapabilities.BIG_CAPABILITY).orElse(new Big()). I am not entirely sure if this would be sufficient, though.

you should use #orElseThrow or #ifPresent, since the Capability should/is always present

Edited by Luis_ST
Link to comment
Share on other sites

----- THIS IS WHERE MY SOLUTION IS -----

Solved. As diesieben07 mentioned, capabilities are not needed. So I no longer used them. My packet now stores a player's UUID and whether that player has the "big" effect. A client has an internal hash map to store the relevant data respective to each player.

UPDATE: Solved. See the most recent comments. Neither capabilities nor custom packets are needed. I used the vanilla packets ClientboundUpdateMobEffectPacket and ClientboundRemoveMobEffectPacket. After some testing on a server, I realized that I also did not need the PlayerEvent.StartTracking event.

Below is all the code I needed:

 

// edit: lol forgot to use SRG name
private static final Field dimensionsField = ObfuscationReflectionHelper.findField(Entity.class, "f_19815_");

public ModEvents() {
    dimensionsField.setAccessible(true);
}

// changing the living entity's HITBOX size
@SubscribeEvent
public static void entityHitboxSizeChange(LivingEvent.LivingUpdateEvent event) throws IllegalAccessException {
    LivingEntity livingEntity = event.getEntityLiving();
    if (livingEntity.getActiveEffectsMap() != null) {

        EntityDimensions entityDimensions = livingEntity.getDimensions(livingEntity.getPose());

        boolean isBig = livingEntity.hasEffect(ModEffects.BIG.get());

        // Refresh dimensions if the living entity does not have the "big" effect AND its AABB has not changed back to normal.
        double aabbWidth = roundDigits(livingEntity.getBoundingBox().getXsize(), 4);
        double bigWidth = roundDigits(8 * entityDimensions.width, 4);
        if (!isBig && aabbWidth == bigWidth) livingEntity.refreshDimensions();

        // change dimensions of living entity using reflections
        if (isBig) {
            dimensionsField.set(livingEntity, entityDimensions.scale(8.0F, 2.0F));
            EntityDimensions newEntityDimensions = (EntityDimensions) dimensionsField.get(livingEntity);
            livingEntity.setBoundingBox(newEntityDimensions.makeBoundingBox(
                    livingEntity.getX(),
                    livingEntity.getY(),
                    livingEntity.getZ()
            ));
        }

    }
}

// little helper function that rounds a double to n digits
private static double roundDigits(double num, int digits) {
    double tenPower = Math.pow(10, digits);
    return Math.round(num * tenPower) / tenPower;
}

 

// client-side event handler
// changing the living entity's RENDER size
@Mod.EventBusSubscriber(modid = ExampleMod.MOD_ID, value = Dist.CLIENT)
public class ModClientEvents {

    @SubscribeEvent
    public static void entityRenderSizeChange(RenderLivingEvent.Pre<LivingEntity, EntityModel<LivingEntity>> event) {
        LivingEntity livingEntity = event.getEntity();
        if (livingEntity != null && livingEntity.getActiveEffectsMap() != null) {
            if (livingEntity.hasEffect(ModEffects.BIG.get())) event.getPoseStack().scale(8.0F, 2.0F, 8.0F);
        }
    }

}

 

// Custom status effect class. addAttributeModifiers and removeAttributeModifiers are overridden because we want packets to be sent to tracking clients whenever a living entity gains or loses the custom status effect.
public class BigEffect extends MobEffect {

    public BigEffect(MobEffectCategory mobEffectCategory, int color) { super(mobEffectCategory, color); }

    @Override
    public boolean isDurationEffectTick(int pDuration, int pAmplifier) { return true; }

    @Override
    public void addAttributeModifiers(@NotNull LivingEntity pLivingEntity, @NotNull AttributeMap pAttributeMap, int pAmplifier) {
        MobEffectInstance mobEffectInstance = pLivingEntity.getEffect(ModEffects.BIG.get());
        if (mobEffectInstance != null) {
            PacketDistributor.TRACKING_ENTITY.with(() -> pLivingEntity).send(
                    new ClientboundUpdateMobEffectPacket(pLivingEntity.getId(), mobEffectInstance)
            );
        }
        super.addAttributeModifiers(pLivingEntity, pAttributeMap, pAmplifier);
    }

    @Override
    public void removeAttributeModifiers(@NotNull LivingEntity pLivingEntity, @NotNull AttributeMap pAttributeMap, int pAmplifier) {
        PacketDistributor.TRACKING_ENTITY.with(() -> pLivingEntity).send(
                new ClientboundRemoveMobEffectPacket(pLivingEntity.getId(), ModEffects.BIG.get())
        );
        super.removeAttributeModifiers(pLivingEntity, pAttributeMap, pAmplifier);
    }

}

 

It works, but I am not sure if it is the most graceful solution. That is, regarding the ClientboundMobSizeUpdatePacketHandler class. I feel like there is a less crude way than just using a HashMap and getting players from the client by their UUID's. If there is, please let me know.

Edited by LeeCrafts
Link to comment
Share on other sites

  • LeeCrafts changed the title to [Version 1.18.2, SOLVED] Custom status effect that changes the player's size, Part II

Edit: My working solution is in my previous comment.

 

Good to know. Then would I still need the my packet handler class? If so, I am having trouble in the init() method. When I insert ClientboundUpdateMobEffectPacket::handle (same for ClientboundRemoveMobEffectPacket) as the argument for consumer(), I get an error saying "Reference to 'handle' is ambiguous, both 'handle(ClientGamePacketListener)' and 'handle(T)' match". I understand what this error is, but I do not know how to fix it. 

And when I tried to run the code, I get a compiler error saying "incompatible types: Supplier<Context> cannot be converted to ClientGamePacketListener)". How would I solve this if ClientboundUpdateMobEffectPacket::handle takes in a ClientGamePacketListener argument instead of a Supplier<Context> argument?

// The packet handler class itself
public class PacketHandler {

    private static final String PROTOCOL_VERSION = "1";

    public static SimpleChannel INSTANCE = NetworkRegistry.newSimpleChannel(
            new ResourceLocation(ExampleMod.MOD_ID, "main"),
            () -> PROTOCOL_VERSION, PROTOCOL_VERSION::equals,
            PROTOCOL_VERSION::equals
    );

    private PacketHandler() {}

    public static void init() {
        int index = 0;
        // "Reference to 'handle' is ambiguous, both 'handle(ClientGamePacketListener)' and 'handle(T)' match"
        INSTANCE.messageBuilder(ClientboundUpdateMobEffectPacket.class, index++, NetworkDirection.PLAY_TO_CLIENT)
                .encoder(ClientboundUpdateMobEffectPacket::write).decoder(ClientboundUpdateMobEffectPacket::new)
                .consumer(ClientboundUpdateMobEffectPacket::handle).add();
        INSTANCE.messageBuilder(ClientboundRemoveMobEffectPacket.class, index++, NetworkDirection.PLAY_TO_CLIENT)
                .encoder(ClientboundRemoveMobEffectPacket::write).decoder(ClientboundRemoveMobEffectPacket::new)
                .consumer(ClientboundRemoveMobEffectPacket::handle).add();
    }

}

 

On the other hand, if I do not need this packet handler class, how else would I send packets? The only way I know is calling PacketHandler.INSTANCE.send() (and Level::sendPacketToServer, but this problem is about clientbound packets, not serverbound).

Edited by LeeCrafts
Link to comment
Share on other sites

  • LeeCrafts changed the title to [Version 1.18.2, SOLVED] Custom status effect that changes the player's (or any entity's) size, Part II

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Unfortunately, your content contains terms that we do not allow. Please edit your content to remove the highlighted words below.
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Announcements



×
×
  • Create New...

Important Information

By using this site, you agree to our Terms of Use.