diff options
author | SoniEx2 <endermoneymod@gmail.com> | 2022-08-17 21:21:26 -0300 |
---|---|---|
committer | SoniEx2 <endermoneymod@gmail.com> | 2022-08-17 21:24:57 -0300 |
commit | 2d0b363fe3179087de59d9ef4a2d14af21d89071 (patch) | |
tree | befdb1f281c3abd82c23d9f6855a3237170e47a5 /src/main/java | |
parent | 731e568ad487b8f7dfbedf22eaca9b12cb3e754d (diff) |
[Project] ChewStuff
Chew Necklaces for Minecraft!
Diffstat (limited to 'src/main/java')
-rw-r--r-- | src/main/java/ganarchy/chewstuff/ChewComponent.java | 93 | ||||
-rw-r--r-- | src/main/java/ganarchy/chewstuff/ChewComponents.java | 28 | ||||
-rw-r--r-- | src/main/java/ganarchy/chewstuff/ChewStuff.java | 90 | ||||
-rw-r--r-- | src/main/java/ganarchy/chewstuff/ChewableItem.java | 195 | ||||
-rw-r--r-- | src/main/java/ganarchy/chewstuff/mixin/DesyncFix.java | 30 | ||||
-rw-r--r-- | src/main/java/ganarchy/chewstuff/mixin/PotionMix.java | 67 | ||||
-rw-r--r-- | src/main/java/net/fabricmc/example/ExampleMod.java | 21 | ||||
-rw-r--r-- | src/main/java/net/fabricmc/example/mixin/ExampleMixin.java | 16 |
8 files changed, 503 insertions, 37 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); + }); + } +} diff --git a/src/main/java/net/fabricmc/example/ExampleMod.java b/src/main/java/net/fabricmc/example/ExampleMod.java deleted file mode 100644 index a964189..0000000 --- a/src/main/java/net/fabricmc/example/ExampleMod.java +++ /dev/null @@ -1,21 +0,0 @@ -package net.fabricmc.example; - -import net.fabricmc.api.ModInitializer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ExampleMod implements ModInitializer { - // This logger is used to write text to the console and the log file. - // It is considered best practice to use your mod id as the logger's name. - // That way, it's clear which mod wrote info, warnings, and errors. - public static final Logger LOGGER = LoggerFactory.getLogger("modid"); - - @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("Hello Fabric world!"); - } -} diff --git a/src/main/java/net/fabricmc/example/mixin/ExampleMixin.java b/src/main/java/net/fabricmc/example/mixin/ExampleMixin.java deleted file mode 100644 index 356cb38..0000000 --- a/src/main/java/net/fabricmc/example/mixin/ExampleMixin.java +++ /dev/null @@ -1,16 +0,0 @@ -package net.fabricmc.example.mixin; - -import net.fabricmc.example.ExampleMod; -import net.minecraft.client.gui.screen.TitleScreen; -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; - -@Mixin(TitleScreen.class) -public class ExampleMixin { - @Inject(at = @At("HEAD"), method = "init()V") - private void init(CallbackInfo info) { - ExampleMod.LOGGER.info("This line is printed by an example mod mixin!"); - } -} |