#Better way to make effects not removable by drinking milk

1 messages · Page 1 of 1 (latest)

vernal patrol
#

Is there a better way to do this?


import me.emafire003.dev.lightwithin.status_effects.LightActiveEffect;
import me.emafire003.dev.lightwithin.status_effects.LightFatigueEffect;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.effect.StatusEffect;
import net.minecraft.entity.effect.StatusEffectInstance;
import net.minecraft.item.Item;
import net.minecraft.item.MilkBucketItem;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;

import java.util.ConcurrentModificationException;

import static me.emafire003.dev.lightwithin.LightWithin.LOGGER;

@Mixin(MilkBucketItem.class)
public abstract class MilkNotCleanLightEffectsMixin extends Item {

    public MilkNotCleanLightEffectsMixin(Settings settings) {
        super(settings);
    }

    @Redirect(at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/LivingEntity;clearStatusEffects()Z"), method = "finishUsing")
    public boolean clearStatusExceptLights(LivingEntity instance){
        try{
            if (instance.getWorld().isClient) {
                return false;
            } else {
                for(StatusEffectInstance status : instance.getActiveStatusEffects().values()){
                    StatusEffect type = status.getEffectType();
                    if(!(type instanceof LightActiveEffect || type instanceof LightFatigueEffect)){
                        instance.removeStatusEffect(type);
                    }
                }
                return true;
            }

        }catch (ConcurrentModificationException e){
            LOGGER.warn("There has been an issue while removing a status effect with a milk bucket, probably nothing to worry about");
            e.printStackTrace();
            return true;
        }
    }
}```
sick nymph
#

...that doesn't seem like a very good way to accomplish that at all

#

It totally ruins mod compat, because then only light within effects will not be cleared

#

And if that mod is loaded, there's nothing you can do about it

#

So to answer your question, yes there is a better way

#

I would inject into clearStatusEffects WrapWithCondition the iterator.remove call if the status effect is your status effect and a boolean is true

#

Then I would inject at the head and tail of milk bucket's finishUsing method and set the boolean to true, then false respectively

vernal patrol
#

Could you give an example?

sick nymph
# vernal patrol Could you give an example?
// some boolean in one of your non-mixin classes
boolean bool = false;
// mixin class for MilkBucketItem
@Inject(method = "finishUsing", at = @At("HEAD"))
private void setBoolTrue(){
  bool = true;
}

@Inject(method = "finishUsing", at = @At("RETURN"))
private void resetBool(){
  bool = false;
}

// mixin class for LivingEntity
    @WrapWithCondition(method = "clearStatusEffects", at = @At(value = "INVOKE", target = "Ljava/util/Iterator;remove()V"))
private boolean noRemove(/*whatever the args are*/) {
  if (bool && /* check if the status effect is a non-removable status effect */) {
    return false;
  }
  return true;
}
vernal patrol
#

i'll try and understand this Xd

#

wait how do I inject?

sick nymph
#

@Inject

vernal patrol
#

would this work

#

import net.minecraft.item.Item;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;

@Mixin(net.minecraft.item.MilkBucketItem.class)
public class MilkBucketItem extends Item {
    public MilkBucketItem(Settings settings) {
        super(settings);
    }

    @Inject(method = "finishUsing", at = @At("HEAD"))
    private void setBoolTrue(){
        bool = true;
    }

    @Inject(method = "finishUsing", at = @At("RETURN"))
    private void resetBool(){
        bool = false;
    }
}
sick nymph
#

Yes

#

But you actually need a bool variable

wicked wyvern
#

Uh. It's my mod. But there is an issue with this approach, I can't get the status effect instance to check it. I would need to get the attributesEffectsWhatever map, but that would probably cause a concurrent modification exeception.

I even tried readding the effects after they have been cleared, but there is another concurrent mod execpetion and also the onRemoved would be triggered twice.

Any other ideas?

proud valve
#

you could use that bool with a mixin to LivingEntity#clearStatusEffects

wicked wyvern
#

As I've said, I can't do that since I can't get which status effects is getting cleared inside that method's loop/iterator. And i'm not even sure I can do it with locals

proud valve
#

It Looks Posible.

#

but idk

wicked wyvern
#

Nope, I'd need to capture either the iterator or the map, overriding them anyway :(

proud valve
#

why wouldn't you inject right before onStatusEffectRemoved and cancel the remove if the effect is the one you want?

#

i got it to do this.

#

but that may not be what you want to do

#

and then you could store the current status effect

#

and check if it is the one you want to get rid of

#

and cancle remove or not based on that

#

and then reset it all at the return of clear status effect

proud valve
#
package maceonic.mixininterface;

import net.minecraft.entity.LivingEntity;

import java.util.Set;

public interface IGettableMilkingPlayers {
    Set<LivingEntity> getMilkingPlayers();
}
#
package maceonic.mixin;

import net.minecraft.entity.LivingEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.item.MilkBucketItem;
import net.minecraft.world.World;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;

import java.util.HashSet;
import java.util.Set;

@Mixin(MilkBucketItem.class)
public class MilkBucketMixin implements IGettableMilkingPlayers {
    @Unique
    Set<LivingEntity> milkingPlayers = new HashSet<>();
    @Inject(method = "finishUsing", at = @At("HEAD"))
    private void startEnd(ItemStack stack, World world, LivingEntity user, CallbackInfoReturnable<ItemStack> cir) {
        milkingPlayers.add(user);
    }
    @Inject(method = "finishUsing", at = @At("RETURN"))
    private void endEnd(ItemStack stack, World world, LivingEntity user, CallbackInfoReturnable<ItemStack> cir) {
        milkingPlayers.remove(user);
    }

    @Unique
    public Set<LivingEntity> getMilkingPlayers() {
        return milkingPlayers;
    }
}
#

something like that?

#

let me look at what it comes out as

#

seems to work, see what you think

#
package maceonic.event;

import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.minecraft.entity.LivingEntity;
import net.minecraft.util.ActionResult;

public interface EffectClearCallback {
     Event<EffectClearCallback> EFFECT_CLEAR_EVENT = EventFactory.createArrayBacked(EffectClearCallback.class, (listeners) -> (context) -> {
        for (EffectClearCallback listener : listeners) {
            ActionResult result = listener.onEffectClear(context);
            if (result != ActionResult.PASS)
                return result;
        }
        return ActionResult.PASS;
    });
    ActionResult onEffectClear(Context context);
    record Context(LivingEntity affected, boolean clearingWithMilk, StatusEffectInstance effectInstance) {
        @Override
        public LivingEntity affected() {
            return affected;
        }
        public boolean withMilk() {
            return clearingWithMilk;
        }
        @Override
        public StatusEffectInstance effectInstance() {
            return effectInstance;
        }
    }
}
#

ping me if you don't like this, I'm interested in this

proud valve
#
@Mixin(LivingEntity.class)
@Debug(export = true)
public class LivingEntityMixin {
    @Unique
    Boolean shouldClearNextEffect = null;
    @WrapWithCondition(method = "clearStatusEffects", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/LivingEntity;onStatusEffectRemoved(Lnet/minecraft/entity/effect/StatusEffectInstance;)V"))
    public boolean shouldClearEffect(LivingEntity instance, StatusEffectInstance effect) {
        ActionResult res = EffectClearCallback.EFFECT_CLEAR_EVENT.invoker().onEffectClear(new EffectClearCallback.Context((LivingEntity) (Object) this, ((IGettableMilkingPlayers)Items.MILK_BUCKET).getMilkingPlayers().contains(instance), effect));
        if (res == ActionResult.FAIL) {
            shouldClearNextEffect = false;
            return false;
        }
        shouldClearNextEffect = true;
        return true;
    }
    @WrapWithCondition(method = "clearStatusEffects", at = @At(value = "INVOKE", target = "Ljava/util/Iterator;remove()V"))
    public boolean shouldRemove(Iterator<StatusEffectInstance> instance) {
        return shouldClearNextEffect;
    }
    @Inject(method = "clearStatusEffects", at = @At("RETURN"))
    public void reset(CallbackInfoReturnable<Boolean> cir) {
        shouldClearNextEffect = null;
    }
}
formal moss
#

how can i make with this my mod custom effects not be clear ?

formal moss
#

Well im new to minecraft coding and i wonder how could i make two of my custom effect not clear by milk drinking ?

proud valve
#

You Would Write Mixins to determine whether or not to cancel.

#

this thread is discussing the best way to write said mixins

#

we have not come to a conclusion yet.

#

I'm Fairly certain that the op is asleep rn, so probably tomorrow we will know if this works.

#

@formal moss

formal moss
#

okay ty

wicked wyvern
#

Uh. It might work. I thought that it would still remove the effect because of the iterator.remove() thing that comes after the onEffectRemovedThing,

this.onStatusEffectRemoved((StatusEffectInstance)iterator.next());
iterator.remove();```
But it seems to be using a packet instead? I guess i'll try. 
If it doesn't work I will probably cancel the effect from clearing every time except when using the /effect clear command or something

EDIT: Nevermind, i saw the other bit about the iterator.remove() after writing this message
#

Also i'll probably just use a "hasDrunkMilk" or something in the LivingEntity itself instead of a list/set

wicked wyvern
#

I've slimmed it down, without the Events and stuff,

MilkMixin Class

@Mixin(MilkBucketItem.class)
public abstract class MilkNotCleanLightEffectsMixin extends Item {

    public MilkNotCleanLightEffectsMixin(Settings settings) {
        super(settings);
    }

    //@Inject(method = "finishUsing", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/LivingEntity;clearStatusEffects()Z"))
    @Inject(method = "finishUsing", at = @At(value = "HEAD"))
    public void setDrinkingMilk(ItemStack stack, World world, LivingEntity user, CallbackInfoReturnable<ItemStack> cir){
        IPreventLightEffectClearEntity entity = (IPreventLightEffectClearEntity) user;
        entity.lightWithin$setHasDrunkMilk(true);
    }

}```
#

PreventEffectClearMixin class thingy

@Mixin(LivingEntity.class)
public abstract class PreventLightEffectClearLivingEntityMixin extends Entity implements IPreventLightEffectClearEntity {
    public PreventLightEffectClearLivingEntityMixin(EntityType<?> type, World world) {
        super(type, world);
    }

    @Unique
    boolean hasDrunkMilk = false;
    @Unique
    boolean clearNextEffect = true;

    @Unique
    @Override
    public boolean lightWithin$getHasDrunkMilk(){
        return hasDrunkMilk;
    }

    @Unique
    @Override
    public void lightWithin$setHasDrunkMilk(boolean b){
        hasDrunkMilk = b;
    }

    @WrapWithCondition(method = "clearStatusEffects", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/LivingEntity;onStatusEffectRemoved(Lnet/minecraft/entity/effect/StatusEffectInstance;)V"))
    public boolean shouldClearEffect(LivingEntity instance, StatusEffectInstance effect) {
        if(this.lightWithin$getHasDrunkMilk()){
            RegistryEntry<StatusEffect> type = effect.getEffectType();
            if(type.matchesKey(LightEffects.LIGHT_ACTIVE.getKey().get()) || type.matchesKey(LightEffects.LIGHT_FATIGUE.getKey().get())){
                clearNextEffect = false;
                return false;
            }

        }
        clearNextEffect = true;
        return true;
    }


    @WrapWithCondition(method = "clearStatusEffects", at = @At(value = "INVOKE", target = "Ljava/util/Iterator;remove()V"))
    public boolean shouldRemove(Iterator<StatusEffectInstance> instance) {
        return clearNextEffect;
    }

    @Inject(method = "clearStatusEffects", at = @At("RETURN"))
    public void reset(CallbackInfoReturnable<Boolean> cir) {
        clearNextEffect = true;
        this.lightWithin$setHasDrunkMilk(false);
    }

}```
#

And the interface:

public interface IPreventEffectClearEntity {
    boolean lightWithin$getHasDrunkMilk();
    void lightWithin$setHasDrunkMilk(boolean b);
}```
#

Note: it works but i've seen a "An unexcpeted error occoured" while using /effect clear sometimes randomly, not sure why, i'll look into it. But generally it works.

Edit: it seems to happen with effects that add a new effect to the user on thei onRemove method. Fixable by using MinecraftServer.exectue() while adding the effect to the onRemove

sick nymph
#

Because none of these solutions look optimal

proud valve
#

what if two players clear at the same time, but one is drinking and one is not

sick nymph
#

That won't happen because the players are ticked one at the time

#

And also, it will be a threadlocal so it won't matter

#

If you want to, you could just attach the boolean to the entity like Emafire did above

wicked wyvern
#

Just cuz i'm curios, what's unoptimal about my solution?

#

Ah ok it's just like the one I ended up using but with the Boolean in another class.

sick nymph
#

I think optimal was not the word I was looking for

#

I think it was more of an unnecessarily complicated thing

proud valve