summary refs log tree commit diff stats
path: root/src/main/java/ganarchy
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/ganarchy')
-rw-r--r--src/main/java/ganarchy/chewstuff/ChewComponent.java93
-rw-r--r--src/main/java/ganarchy/chewstuff/ChewComponents.java28
-rw-r--r--src/main/java/ganarchy/chewstuff/ChewStuff.java90
-rw-r--r--src/main/java/ganarchy/chewstuff/ChewableItem.java195
-rw-r--r--src/main/java/ganarchy/chewstuff/mixin/DesyncFix.java30
-rw-r--r--src/main/java/ganarchy/chewstuff/mixin/PotionMix.java67
6 files changed, 503 insertions, 0 deletions
diff --git a/src/main/java/ganarchy/chewstuff/ChewComponent.java b/src/main/java/ganarchy/chewstuff/ChewComponent.java
new file mode 100644
index 0000000..4b6dcd8
--- /dev/null
+++ b/src/main/java/ganarchy/chewstuff/ChewComponent.java
@@ -0,0 +1,93 @@
+package ganarchy.chewstuff;
+
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.codecs.RecordCodecBuilder;
+import dev.onyxstudios.cca.api.v3.component.ComponentV3;
+import net.minecraft.entity.effect.StatusEffect;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.nbt.NbtOps;
+import net.minecraft.util.registry.Registry;
+
+import java.util.*;
+
+/**
+ * Component for chews.
+ */
+public class ChewComponent implements ComponentV3 {
+    /**
+     * Codec for encoding/decoding this component.
+     */
+    public static final Codec<ChewComponent> CODEC = RecordCodecBuilder.create(
+        inst -> inst.group(
+            Codec.unboundedMap(
+                Registry.STATUS_EFFECT.getCodec(), Codec.INT
+            ).fieldOf("effects").forGetter(i -> i.effects),
+            Codec.list(
+                Registry.STATUS_EFFECT.getCodec()
+            ).fieldOf("applied").forGetter(i -> new ArrayList<>(i.applied)),
+            Codec.BOOL.fieldOf("sendUpdate").forGetter(i -> i.sendUpdate)
+        ).apply(inst, ChewComponent::new)
+    );
+    /**
+     * The effects we're currently consuming.
+     */
+    public Map<StatusEffect, Integer> effects;
+    /**
+     * The effects which have been applied and should be skipped.
+     */
+    public Set<StatusEffect> applied;
+    /**
+     * Whether to send updated effects to the client.
+     */
+    public boolean sendUpdate;
+
+    /**
+     * Creates a new ChewInfo with no effects.
+     */
+    public ChewComponent() {
+        this.effects = new HashMap<>();
+        this.applied = new HashSet<>();
+    }
+
+    /**
+     * Creates a new ChewInfo with the specified effects.
+     *
+     * @param effects The bound effects.
+     * @param applied The applied effects.
+     */
+    public ChewComponent(
+        Map<StatusEffect, Integer> effects,
+        List<StatusEffect> applied,
+        boolean sendUpdate
+    ) {
+        if (effects instanceof HashMap) {
+            this.effects = effects;
+        } else {
+            // ideally we'd just make the HashMap directly but DFU doesn't let
+            // you set the Map class.
+            // actually ideally we'd use IdentityHashMap but w/e.
+            this.effects = new HashMap<>(effects);
+        }
+        this.applied = new HashSet<>(applied);
+        this.sendUpdate = sendUpdate;
+    }
+
+    @Override
+    public void readFromNbt(NbtCompound tag) {
+        var info = ChewComponent.CODEC.parse(
+            NbtOps.INSTANCE,
+            tag
+        ).result().orElseGet(ChewComponent::new);
+
+        this.effects = info.effects;
+        this.applied = info.applied;
+    }
+
+    @Override
+    public void writeToNbt(NbtCompound tag) {
+        tag.copyFrom((NbtCompound) ChewComponent.CODEC.encodeStart(
+            NbtOps.INSTANCE,
+            this
+        ).result().orElseThrow());
+    }
+}
diff --git a/src/main/java/ganarchy/chewstuff/ChewComponents.java b/src/main/java/ganarchy/chewstuff/ChewComponents.java
new file mode 100644
index 0000000..427d86e
--- /dev/null
+++ b/src/main/java/ganarchy/chewstuff/ChewComponents.java
@@ -0,0 +1,28 @@
+package ganarchy.chewstuff;
+
+import dev.onyxstudios.cca.api.v3.component.ComponentKey;
+import dev.onyxstudios.cca.api.v3.component.ComponentRegistry;
+import dev.onyxstudios.cca.api.v3.entity.EntityComponentFactoryRegistry;
+import dev.onyxstudios.cca.api.v3.entity.EntityComponentInitializer;
+import dev.onyxstudios.cca.api.v3.entity.RespawnCopyStrategy;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.util.Identifier;
+
+public class ChewComponents implements EntityComponentInitializer {
+    public static final ComponentKey<ChewComponent> CHEW =
+        ComponentRegistry.getOrCreate(
+            new Identifier("chewstuff", "chew"), ChewComponent.class
+        );
+
+    @Override
+    public void registerEntityComponentFactories(
+        EntityComponentFactoryRegistry registry
+    ) {
+        registry.registerFor(
+            LivingEntity.class, CHEW, entity -> new ChewComponent()
+        );
+        registry.registerForPlayers(
+            CHEW, entity -> new ChewComponent(), RespawnCopyStrategy.ALWAYS_COPY
+        );
+    }
+}
diff --git a/src/main/java/ganarchy/chewstuff/ChewStuff.java b/src/main/java/ganarchy/chewstuff/ChewStuff.java
new file mode 100644
index 0000000..7bea0ec
--- /dev/null
+++ b/src/main/java/ganarchy/chewstuff/ChewStuff.java
@@ -0,0 +1,90 @@
+package ganarchy.chewstuff;
+
+import net.fabricmc.api.ModInitializer;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemGroup;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.registry.Registry;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The mod's entry point.
+ */
+public class ChewStuff implements ModInitializer {
+    /**
+     * The ChewStuff Logger.
+     */
+    public static final Logger LOGGER = LoggerFactory.getLogger("chewstuff");
+
+    /**
+     * Silicone. Not to be confused with silicon. Crafting material for
+     * chews.
+     */
+    public static final Item SILICONE = new Item(
+        new Item.Settings().group(ItemGroup.MISC)
+    );
+    /**
+     * 1 day, in ticks.
+     */
+    public static final int DAY_MULTIPLIER = 24 * 60 * 60 * 20;
+    /**
+     * Soft chew. Lasts 5 days, 1 effect slot.
+     */
+    public static final Item SOFT_CHEW = new ChewableItem(
+        1,
+        new Item.Settings().maxDamage(5 * DAY_MULTIPLIER).group(ItemGroup.TOOLS)
+    );
+    /**
+     * Medium chew. Lasts 6 days, 2 effect slots.
+     */
+    public static final Item MEDIUM_CHEW = new ChewableItem(
+        2,
+        new Item.Settings().maxDamage(6 * DAY_MULTIPLIER).group(ItemGroup.TOOLS)
+    );
+    /**
+     * Hard chew. Lasts 7 days, 3 effect slots.
+     */
+    public static final Item HARD_CHEW = new ChewableItem(
+        3,
+        new Item.Settings().maxDamage(7 * DAY_MULTIPLIER).group(ItemGroup.TOOLS)
+    );
+    /**
+     * The stat for chew effects.
+     */
+    public static final Identifier CHEW_EFFECTS = new Identifier(
+        "chewstuff", "effects"
+    );
+
+    @Override
+    public void onInitialize() {
+        // This code runs as soon as Minecraft is in a mod-load-ready state.
+        // However, some things (like resources) may still be uninitialized.
+        // Proceed with mild caution.
+
+        LOGGER.info("This software is made with love by a queer trans person.");
+
+        Registry.register(
+            Registry.ITEM,
+            new Identifier("chewstuff", "silicone"),
+            SILICONE
+        );
+        Registry.register(
+            Registry.ITEM,
+            new Identifier("chewstuff", "soft_chew"),
+            SOFT_CHEW
+        );
+        Registry.register(
+            Registry.ITEM,
+            new Identifier("chewstuff", "medium_chew"),
+            MEDIUM_CHEW
+        );
+        Registry.register(
+            Registry.ITEM,
+            new Identifier("chewstuff", "hard_chew"),
+            HARD_CHEW
+        );
+//        Registry.register(Registry.CUSTOM_STAT, CHEW_EFFECTS, CHEW_EFFECTS);
+//        Stats.CUSTOM.getOrCreateStat(CHEW_EFFECTS, StatFormatter.DEFAULT);
+    }
+}
diff --git a/src/main/java/ganarchy/chewstuff/ChewableItem.java b/src/main/java/ganarchy/chewstuff/ChewableItem.java
new file mode 100644
index 0000000..e3c8f60
--- /dev/null
+++ b/src/main/java/ganarchy/chewstuff/ChewableItem.java
@@ -0,0 +1,195 @@
+package ganarchy.chewstuff;
+
+import dev.emi.trinkets.api.SlotReference;
+import dev.emi.trinkets.api.TrinketItem;
+import dev.emi.trinkets.api.TrinketsApi;
+import net.fabricmc.loader.api.FabricLoader;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.entity.effect.StatusEffect;
+import net.minecraft.entity.effect.StatusEffectInstance;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Wearable;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.network.packet.s2c.play.EntityStatusEffectS2CPacket;
+import net.minecraft.server.network.ServerPlayerEntity;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * An item that can be chewed.
+ */
+public class ChewableItem extends TrinketItem implements Wearable {
+    /**
+     * The cooldown between activations, in ticks.
+     */
+    private static final int COOLDOWN = 5 * 20;
+    /**
+     * Number of effects to bind to.
+     */
+    private final int effects;
+
+    /**
+     * @param effects  Number of effects to bind to.
+     * @param settings The item settings.
+     */
+    public ChewableItem(int effects, Settings settings) {
+        super(settings);
+        this.effects = effects;
+    }
+
+    @Override
+    public void onEquip(ItemStack stack, SlotReference slot, LivingEntity entity) {
+        if (entity.world.isClient) {
+            return;
+        }
+        var maybeInfo = ChewComponents.CHEW.maybeGet(entity);
+        maybeInfo.ifPresent(info -> {
+            info.sendUpdate = true;
+        });
+    }
+
+    @Override
+    public void onUnequip(
+        ItemStack stack, SlotReference slot, LivingEntity entity
+    ) {
+        if (entity.world.isClient) {
+            return;
+        }
+        var maybeInfo = ChewComponents.CHEW.maybeGet(entity);
+        maybeInfo.ifPresent(info -> {
+            if (!info.effects.isEmpty()) {
+                resetEffects(entity, info);
+            }
+        });
+    }
+
+    @Override
+    public void tick(ItemStack stack, SlotReference slot, LivingEntity entity) {
+        if (entity.world.isClient) {
+            return;
+        }
+        if (entity instanceof PlayerEntity player) {
+            if (player.getItemCooldownManager().isCoolingDown(this)) {
+                return;
+            }
+        }
+        var maybeInfo = ChewComponents.CHEW.maybeGet(entity);
+        maybeInfo.ifPresent(info -> {
+            if (info.effects.isEmpty()) {
+                var effects = entity.getStatusEffects();
+                if (effects.size() >= this.effects) {
+                    effects = new ArrayList<>(effects);
+                    Collections.shuffle((List<?>) effects);
+                    info.effects = effects.stream().filter(
+                        effect -> !effect.isAmbient()
+                    ).limit(this.effects).collect(
+                        Collectors.toMap(
+                            StatusEffectInstance::getEffectType,
+                            StatusEffectInstance::getDuration,
+                            (i1, i2) -> {
+                                // don't crash the game in prod
+                                if (
+                                    FabricLoader.getInstance()
+                                        .isDevelopmentEnvironment()
+                                ) {
+                                    throw new IllegalStateException();
+                                } else {
+                                    return Integer.min(i1, i2);
+                                }
+                            },
+                            HashMap::new
+                        )
+                    );
+                    info.sendUpdate = true;
+                } else {
+                    return;
+                }
+            }
+            if (info.effects.size() != this.effects) {
+                return;
+            }
+            if (info.sendUpdate) {
+                sendEffectUpdate(entity, info);
+            }
+            var effects = info.effects.keySet();
+            for (StatusEffect effect : effects) {
+                if (info.effects.get(effect) <= 0) {
+                    continue;
+                }
+                var effectInstance = entity.getStatusEffect(effect);
+                if (effectInstance == null || effectInstance.isAmbient()) {
+                    info.effects.put(effect, 0);
+                    continue;
+                }
+                // don't wanna deal with LivingEntity internals.
+                if (effectInstance.getDuration() == 1) {
+                    continue;
+                }
+                if (!effectInstance.update(entity, () -> {})) {
+                    throw new IllegalStateException();
+                }
+            }
+            if (info.effects.values().stream().allMatch(i -> i == 0)) {
+                resetEffects(entity, info);
+            }
+            stack.damage(1, entity, livingEntity -> {
+                TrinketsApi.onTrinketBroken(stack, slot, livingEntity);
+            });
+        });
+    }
+
+    private void sendEffectUpdate(LivingEntity entity, ChewComponent info) {
+        if (entity instanceof ServerPlayerEntity player) {
+            for (StatusEffect effect : info.effects.keySet()) {
+                var instance = player.getStatusEffect(effect);
+                if (instance == null) {
+                    continue;
+                }
+                NbtCompound nbt = new NbtCompound();
+                instance.writeNbt(nbt);
+                nbt.putInt("Duration", instance.getDuration() / 2);
+                var toSend = StatusEffectInstance.fromNbt(nbt);
+                if (toSend != null) {
+                    player.networkHandler.sendPacket(
+                        new EntityStatusEffectS2CPacket(
+                            player.getId(),
+                            toSend
+                        )
+                    );
+                }
+            }
+        }
+    }
+
+    /**
+     * Stops tracking effects, enables the cooldown, and updates the player,
+     * as needed.
+     *
+     * @param entity The entity.
+     * @param info   The tracked effects.
+     */
+    private void resetEffects(LivingEntity entity, ChewComponent info) {
+        if (entity instanceof ServerPlayerEntity player) {
+            for (StatusEffect effect : info.effects.keySet()) {
+                var instance = player.getStatusEffect(effect);
+                if (instance == null) {
+                    continue;
+                }
+                player.networkHandler.sendPacket(
+                    new EntityStatusEffectS2CPacket(
+                        player.getId(),
+                        instance
+                    )
+                );
+            }
+            player.getItemCooldownManager().set(this, COOLDOWN);
+        }
+        info.effects.clear();
+        info.applied.clear();
+    }
+}
diff --git a/src/main/java/ganarchy/chewstuff/mixin/DesyncFix.java b/src/main/java/ganarchy/chewstuff/mixin/DesyncFix.java
new file mode 100644
index 0000000..7a62a41
--- /dev/null
+++ b/src/main/java/ganarchy/chewstuff/mixin/DesyncFix.java
@@ -0,0 +1,30 @@
+package ganarchy.chewstuff.mixin;
+
+import ganarchy.chewstuff.ChewComponents;
+import net.minecraft.server.network.ServerPlayerEntity;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+/**
+ * The mixin for server player entities.
+ */
+@Mixin(ServerPlayerEntity.class)
+public class DesyncFix {
+    /**
+     * Checks and corrects for desyncs.
+     */
+    @Inject(
+        at = @At("HEAD"),
+        method =
+            "onStatusEffectUpgraded(Lnet/minecraft/entity/effect/StatusEffectInstance;ZLnet/minecraft/entity/Entity;)V"
+    )
+    private void chewstuff_onDesync(CallbackInfo cbinfo) {
+        var self = (ServerPlayerEntity) (Object) this;
+        var maybeInfo = ChewComponents.CHEW.maybeGet(self);
+        maybeInfo.ifPresent(info -> {
+            info.sendUpdate = true;
+        });
+    }
+}
diff --git a/src/main/java/ganarchy/chewstuff/mixin/PotionMix.java b/src/main/java/ganarchy/chewstuff/mixin/PotionMix.java
new file mode 100644
index 0000000..5ed97ab
--- /dev/null
+++ b/src/main/java/ganarchy/chewstuff/mixin/PotionMix.java
@@ -0,0 +1,67 @@
+package ganarchy.chewstuff.mixin;
+
+import ganarchy.chewstuff.ChewComponents;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.entity.effect.StatusEffectInstance;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+/**
+ * The mixin for status effect instances.
+ */
+@Mixin(StatusEffectInstance.class)
+public class PotionMix {
+    /**
+     * Checks the apply effect and cancels it when we're force-ticking it.
+     */
+    @Inject(
+        at = @At("HEAD"),
+        method = "applyUpdateEffect(Lnet/minecraft/entity/LivingEntity;)V",
+        cancellable = true
+    )
+    private void chewstuff_checkApplyEffect(
+        LivingEntity entity, CallbackInfo cbinfo
+    ) {
+        StatusEffectInstance self = (StatusEffectInstance) (Object) this;
+        var maybeInfo = ChewComponents.CHEW.maybeGet(entity);
+        maybeInfo.ifPresent(info -> {
+            var effect = self.getEffectType();
+            if (info.effects.getOrDefault(effect, 0) <= 0) {
+                return;
+            }
+            if (info.applied.contains(effect)) {
+                info.applied.remove(effect);
+                cbinfo.cancel();
+            } else {
+                info.applied.add(effect);
+            }
+        });
+    }
+
+    /**
+     * Checks for effect ticks.
+     */
+    @Inject(
+        at = @At("HEAD"),
+        method =
+            "update(Lnet/minecraft/entity/LivingEntity;Ljava/lang/Runnable;)Z"
+    )
+    private void chewstuff_checkUpdate(
+        LivingEntity entity,
+        Runnable runnable,
+        CallbackInfoReturnable<Boolean> cbinfo
+    ) {
+        StatusEffectInstance self = (StatusEffectInstance) (Object) this;
+        var maybeInfo = ChewComponents.CHEW.maybeGet(entity);
+        maybeInfo.ifPresent(info -> {
+            var effect = self.getEffectType();
+            if (info.effects.getOrDefault(effect, 0) <= 0) {
+                return;
+            }
+            info.effects.computeIfPresent(effect, (k, v) -> v - 1);
+        });
+    }
+}