diff --git a/gradle.properties b/gradle.properties index 8fc3c37..afd8407 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ yarn_mappings=1.19+build.1 loader_version=0.14.7 # Mod Properties -mod_version=4.1 +mod_version=4.2 maven_group=dev.kir archives_base_name=sync diff --git a/src/main/java/dev/kir/sync/block/entity/TreadmillBlockEntity.java b/src/main/java/dev/kir/sync/block/entity/TreadmillBlockEntity.java index 7760f04..1b5d370 100644 --- a/src/main/java/dev/kir/sync/block/entity/TreadmillBlockEntity.java +++ b/src/main/java/dev/kir/sync/block/entity/TreadmillBlockEntity.java @@ -4,6 +4,7 @@ import dev.kir.sync.api.event.EntityFitnessEvents; import dev.kir.sync.config.SyncConfig; import dev.kir.sync.Sync; +import dev.kir.sync.easteregg.technoblade.TechnobladeManager; import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext; import net.minecraft.block.BlockState; import net.minecraft.block.DoubleBlockProperties; @@ -69,7 +70,13 @@ private void setRunner(Entity entity) { EntityFitnessEvents.START_RUNNING.invoker().onStartRunning(this.runner, this); } - if (this.world != null && !this.world.isClient) { + if (this.world == null) { + return; + } + + if (this.world.isClient) { + TechnobladeManager.refreshTechnobladeStatus(entity, this.pos); + } else { this.markDirty(); this.sync(); } diff --git a/src/main/java/dev/kir/sync/compat/cloth/SyncClothConfig.java b/src/main/java/dev/kir/sync/compat/cloth/SyncClothConfig.java index 1015330..8fb63ac 100644 --- a/src/main/java/dev/kir/sync/compat/cloth/SyncClothConfig.java +++ b/src/main/java/dev/kir/sync/compat/cloth/SyncClothConfig.java @@ -12,7 +12,9 @@ import net.minecraft.util.registry.Registry; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.UUID; import java.util.stream.Collectors; @Config(name = Sync.MOD_ID) @@ -83,6 +85,9 @@ public static SyncConfig getInstance() { @ConfigEntry.Gui.Tooltip(count = 2) public boolean updateTranslationsAutomatically = SyncConfig.super.updateTranslationsAutomatically(); + @ConfigEntry.Category(value = "easter_eggs") + @ConfigEntry.Gui.TransitiveObject + public EasterEggs easterEggs = new EasterEggs(); @Override public boolean enableInstantShellConstruction() { @@ -156,6 +161,71 @@ public boolean updateTranslationsAutomatically() { return this.updateTranslationsAutomatically; } + @Override + public boolean enableTechnobladeEasterEgg() { + return this.easterEggs.technoblade.enable; + } + + @Override + public boolean renderTechnobladeCape() { + return this.easterEggs.technoblade.renderCape; + } + + @Override + public boolean allowTechnobladeAnnouncements() { + return this.easterEggs.technoblade.allowAnnouncements; + } + + @Override + public boolean allowTechnobladeQuotes() { + return this.easterEggs.technoblade.allowQuotes; + } + + @Override + public int TechnobladeQuoteDelay() { + return this.easterEggs.technoblade.quoteDelay; + } + + @Override + public boolean isTechnoblade(UUID uuid) { + return this.easterEggs.technoblade.cache.contains(uuid); + } + + @Override + public void addTechnoblade(UUID uuid) { + this.easterEggs.technoblade.cache.add(uuid); + AutoConfig.getConfigHolder(SyncClothConfig.class).save(); + } + + @Override + public void removeTechnoblade(UUID uuid) { + this.easterEggs.technoblade.cache.remove(uuid); + AutoConfig.getConfigHolder(SyncClothConfig.class).save(); + } + + @Override + public void clearTechnobladeCache() { + this.easterEggs.technoblade.cache.clear(); + AutoConfig.getConfigHolder(SyncClothConfig.class).save(); + } + + public static class EasterEggs { + @ConfigEntry.Gui.CollapsibleObject + public TechnobladeEasterEgg technoblade = new TechnobladeEasterEgg(); + } + + public static class TechnobladeEasterEgg { + @ConfigEntry.Gui.RequiresRestart + public boolean enable = true; + public boolean renderCape = false; + public boolean allowAnnouncements = true; + public boolean allowQuotes = true; + public int quoteDelay = 1800; + + @ConfigEntry.Gui.Excluded + public HashSet cache = new HashSet<>(); + } + public static class EnergyMapEntry implements SyncConfig.EnergyMapEntry { @ConfigEntry.Gui.RequiresRestart public String entityId; diff --git a/src/main/java/dev/kir/sync/config/SyncConfig.java b/src/main/java/dev/kir/sync/config/SyncConfig.java index c1eba6e..5e9e31a 100644 --- a/src/main/java/dev/kir/sync/config/SyncConfig.java +++ b/src/main/java/dev/kir/sync/config/SyncConfig.java @@ -8,6 +8,7 @@ import net.minecraft.util.registry.Registry; import java.util.List; +import java.util.UUID; public interface SyncConfig { List DEFAULT_ENERGY_MAP = List.of( @@ -81,6 +82,43 @@ default boolean preserveOrigins() { return false; } + default boolean enableTechnobladeEasterEgg() { + return true; + } + + default boolean renderTechnobladeCape() { + // Techno hasn't worn a cape lately + return false; + } + + default boolean allowTechnobladeAnnouncements() { + return true; + } + + default boolean allowTechnobladeQuotes() { + return true; + } + + default int TechnobladeQuoteDelay() { + return 1800; + } + + default boolean isTechnoblade(UUID uuid) { + return false; + } + + default void addTechnoblade(UUID uuid) { + + } + + default void removeTechnoblade(UUID uuid) { + + } + + default void clearTechnobladeCache() { + + } + interface EnergyMapEntry { default String entityId() { return "minecraft:pig"; diff --git a/src/main/java/dev/kir/sync/easteregg/mixin/MixinEasterEggs.java b/src/main/java/dev/kir/sync/easteregg/mixin/MixinEasterEggs.java new file mode 100644 index 0000000..801245b --- /dev/null +++ b/src/main/java/dev/kir/sync/easteregg/mixin/MixinEasterEggs.java @@ -0,0 +1,80 @@ +package dev.kir.sync.easteregg.mixin; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; + +public final class MixinEasterEggs implements IMixinConfigPlugin { + @Override + public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { + String name = getPackageName(mixinClassName); + JsonObject config = this.getConfig(); + if ( + config.has(name) && config.get(name) instanceof JsonObject configEntry && + configEntry.has("enable") && configEntry.get("enable") instanceof JsonPrimitive enable && enable.isBoolean() + ) { + return enable.getAsBoolean(); + } + + return true; + } + + @Override + public void onLoad(String mixinPackage) { } + + @Override + public String getRefMapperConfig() { return null; } + + @Override + public List getMixins() { return null; } + + @Override + public void acceptTargets(Set myTargets, Set otherTargets) { } + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { } + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { } + + private static String getPackageName(String mixinClassName) { + int mixinStartI = mixinClassName.lastIndexOf("mixin."); + if (mixinStartI == -1) + return mixinClassName; + + int endI = mixinClassName.indexOf('.', mixinStartI + 6); + return mixinClassName.substring(mixinStartI + 6, endI); + } + + private JsonObject config; + private JsonObject getConfig() { + if (this.config == null) { + try { + Path path = Path.of("./config/sync.json"); + if (Files.isReadable(path)) { + String json = Files.readString(path); + this.config = (JsonObject)JsonParser.parseString(json); + } + } catch (Throwable e) { + this.config = null; + } + + if (this.config == null) { + this.config = new JsonObject(); + } + + if (this.config.has("easterEggs") && this.config.get("easterEggs") instanceof JsonObject easterEggs) { + this.config = easterEggs; + } + } + return this.config; + } +} diff --git a/src/main/java/dev/kir/sync/easteregg/mixin/technoblade/ClientWorldMixin.java b/src/main/java/dev/kir/sync/easteregg/mixin/technoblade/ClientWorldMixin.java new file mode 100644 index 0000000..d896c6e --- /dev/null +++ b/src/main/java/dev/kir/sync/easteregg/mixin/technoblade/ClientWorldMixin.java @@ -0,0 +1,105 @@ +package dev.kir.sync.easteregg.mixin.technoblade; + +import dev.kir.sync.easteregg.technoblade.Technoblade; +import dev.kir.sync.easteregg.technoblade.TechnobladeTransformable; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.sound.EntityTrackingSoundInstance; +import net.minecraft.client.sound.PositionedSoundInstance; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.Entity; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvent; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.random.Random; +import net.minecraft.util.profiler.Profiler; +import net.minecraft.util.registry.Registry; +import net.minecraft.util.registry.RegistryEntry; +import net.minecraft.util.registry.RegistryKey; +import net.minecraft.world.MutableWorldProperties; +import net.minecraft.world.World; +import net.minecraft.world.dimension.DimensionType; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; +import java.util.function.Supplier; + +@Environment(EnvType.CLIENT) +@Mixin(ClientWorld.class) +abstract class ClientWorldMixin extends World { + @Shadow + private @Final MinecraftClient client; + + private ClientWorldMixin(MutableWorldProperties properties, RegistryKey registryRef, RegistryEntry dimension, Supplier profiler, boolean isClient, boolean debugWorld, long seed, int maxChainedNeighborUpdates) { + super(properties, registryRef, dimension, profiler, isClient, debugWorld, seed, maxChainedNeighborUpdates); + } + + @Inject(method = "playSound(DDDLnet/minecraft/sound/SoundEvent;Lnet/minecraft/sound/SoundCategory;FFZJ)V", at = @At("HEAD"), cancellable = true) + private void playSound(double x, double y, double z, SoundEvent sound, SoundCategory category, float volume, float pitch, boolean useDistance, long seed, CallbackInfo ci) { + if (category != SoundCategory.NEUTRAL) { + return; + } + + List Technoblades = this.getEntitiesByClass(MobEntity.class, new Box(x - 0.1, y - 0.1, z - 0.1, x + 0.1, y + 0.1, z + 0.1), e -> e instanceof TechnobladeTransformable && ((TechnobladeTransformable)e).isTechnoblade()); + MobEntity entity = Technoblades.size() == 0 ? null : Technoblades.get(0); + if (entity == null) { + return; + } + + Technoblade Technoblade = ((TechnobladeTransformable)entity).asTechnoblade(); + sound = this.getTechnobladeSound(sound); + if (sound == null) { + ci.cancel(); + return; + } + + double distance = this.client.gameRenderer.getCamera().getPos().squaredDistanceTo(x, y, z); + PositionedSoundInstance positionedSoundInstance = new PositionedSoundInstance(sound, Technoblade.getSoundCategory(), volume, pitch, Random.create(seed), x, y, z); + if (useDistance && distance > 100) { + this.client.getSoundManager().play(positionedSoundInstance, (int)(Math.sqrt(distance) * 0.5)); + } else { + this.client.getSoundManager().play(positionedSoundInstance); + } + ci.cancel(); + } + + @Inject(method = "playSoundFromEntity", at = @At("HEAD"), cancellable = true) + private void playSoundFromEntity(PlayerEntity except, Entity entity, SoundEvent sound, SoundCategory category, float volume, float pitch, long seed, CallbackInfo ci) { + if (except != this.client.player || !(entity instanceof TechnobladeTransformable) || !((TechnobladeTransformable)entity).isTechnoblade()) { + return; + } + + Technoblade Technoblade = ((TechnobladeTransformable)entity).asTechnoblade(); + sound = this.getTechnobladeSound(sound); + if (sound == null) { + ci.cancel(); + return; + } + + this.client.getSoundManager().play(new EntityTrackingSoundInstance(sound, Technoblade.getSoundCategory(), volume, pitch, entity, seed)); + ci.cancel(); + } + + private @Nullable SoundEvent getTechnobladeSound(SoundEvent sound) { + Identifier originalSoundId = sound.getId(); + if (originalSoundId.getPath().endsWith(".ambient") || originalSoundId.getPath().endsWith(".death")) { + return null; + } + + SoundEvent fixedSound = Registry.SOUND_EVENT.get(new Identifier(originalSoundId.getNamespace(), originalSoundId.getPath().replaceFirst("\\.[^.]+", ".player"))); + if (fixedSound == null) { + fixedSound = sound; + } + return fixedSound; + } +} diff --git a/src/main/java/dev/kir/sync/easteregg/mixin/technoblade/EntityRenderDispatcherMixin.java b/src/main/java/dev/kir/sync/easteregg/mixin/technoblade/EntityRenderDispatcherMixin.java new file mode 100644 index 0000000..5a06b91 --- /dev/null +++ b/src/main/java/dev/kir/sync/easteregg/mixin/technoblade/EntityRenderDispatcherMixin.java @@ -0,0 +1,24 @@ +package dev.kir.sync.easteregg.mixin.technoblade; + +import dev.kir.sync.easteregg.technoblade.TechnobladeTransformable; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.entity.EntityRenderDispatcher; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArgs; +import org.spongepowered.asm.mixin.injection.invoke.arg.Args; + +@Environment(EnvType.CLIENT) +@Mixin(EntityRenderDispatcher.class) +abstract class EntityRenderDispatcherMixin { + @ModifyArgs(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/entity/EntityRenderDispatcher;renderShadow(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;Lnet/minecraft/entity/Entity;FFLnet/minecraft/world/WorldView;F)V")) + private void getShadowOpacity(Args args, Entity entity, double x, double y, double z, float yaw, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light) { + if (entity instanceof TechnobladeTransformable technobladeTransformable && technobladeTransformable.isTechnoblade()) { + args.set(6, 0f); + } + } +} diff --git a/src/main/java/dev/kir/sync/easteregg/mixin/technoblade/MobEntityMixin.java b/src/main/java/dev/kir/sync/easteregg/mixin/technoblade/MobEntityMixin.java new file mode 100644 index 0000000..440b2c9 --- /dev/null +++ b/src/main/java/dev/kir/sync/easteregg/mixin/technoblade/MobEntityMixin.java @@ -0,0 +1,34 @@ +package dev.kir.sync.easteregg.mixin.technoblade; + +import dev.kir.sync.Sync; +import dev.kir.sync.easteregg.technoblade.TechnobladeTransformable; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.world.World; +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; + +@Environment(EnvType.CLIENT) +@Mixin(MobEntity.class) +abstract class MobEntityMixin extends LivingEntity { + private MobEntityMixin(EntityType entityType, World world) { + super(entityType, world); + } + + @Inject(method = "tick", at = @At("RETURN")) + private void tick(CallbackInfo ci) { + if (!(this instanceof TechnobladeTransformable) || !((TechnobladeTransformable)this).isTechnoblade()) { + return; + } + + ((TechnobladeTransformable)this).asTechnoblade().tick(); + if (this.dead) { + Sync.getConfig().removeTechnoblade(this.uuid); + } + } +} diff --git a/src/main/java/dev/kir/sync/easteregg/mixin/technoblade/MobEntityRendererMixin.java b/src/main/java/dev/kir/sync/easteregg/mixin/technoblade/MobEntityRendererMixin.java new file mode 100644 index 0000000..1a96ad8 --- /dev/null +++ b/src/main/java/dev/kir/sync/easteregg/mixin/technoblade/MobEntityRendererMixin.java @@ -0,0 +1,33 @@ +package dev.kir.sync.easteregg.mixin.technoblade; + +import dev.kir.sync.easteregg.technoblade.Technoblade; +import dev.kir.sync.easteregg.technoblade.TechnobladeTransformable; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.entity.EntityRenderDispatcher; +import net.minecraft.client.render.entity.MobEntityRenderer; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.mob.MobEntity; +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; + +@Environment(EnvType.CLIENT) +@Mixin(MobEntityRenderer.class) +abstract class MobEntityRendererMixin { + @Inject(method = "render(Lnet/minecraft/entity/mob/MobEntity;FFLnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;I)V", at = @At("HEAD"), cancellable = true) + private void render(MobEntity mobEntity, float yaw, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, CallbackInfo ci) { + if (!(mobEntity instanceof TechnobladeTransformable technobladeTransformable) || !technobladeTransformable.isTechnoblade()) { + return; + } + + Technoblade Technoblade = technobladeTransformable.asTechnoblade(); + Technoblade.copyPose(mobEntity); + EntityRenderDispatcher renderDispatcher = MinecraftClient.getInstance().getEntityRenderDispatcher(); + renderDispatcher.render(Technoblade, 0, 0, 0, yaw, tickDelta, matrices, vertexConsumers, light); + ci.cancel(); + } +} diff --git a/src/main/java/dev/kir/sync/easteregg/mixin/technoblade/PigEntityMixin.java b/src/main/java/dev/kir/sync/easteregg/mixin/technoblade/PigEntityMixin.java new file mode 100644 index 0000000..22db06a --- /dev/null +++ b/src/main/java/dev/kir/sync/easteregg/mixin/technoblade/PigEntityMixin.java @@ -0,0 +1,72 @@ +package dev.kir.sync.easteregg.mixin.technoblade; + +import dev.kir.sync.easteregg.technoblade.Technoblade; +import dev.kir.sync.easteregg.technoblade.TechnobladeManager; +import dev.kir.sync.easteregg.technoblade.TechnobladeTransformable; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.block.BlockState; +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.passive.PigEntity; +import net.minecraft.sound.SoundEvent; +import net.minecraft.sound.SoundEvents; +import net.minecraft.util.math.BlockPos; +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; + +@Environment(EnvType.CLIENT) +@Mixin(PigEntity.class) +abstract class PigEntityMixin implements TechnobladeTransformable { + private Technoblade Technoblade = null; + + @Inject(method = "getAmbientSound", at = @At("HEAD"), cancellable = true) + private void getAmbientSound(CallbackInfoReturnable cir) { + if (this.Technoblade != null) { + cir.setReturnValue(SoundEvents.ENTITY_PLAYER_BREATH); + } + } + + @Inject(method = "getHurtSound", at = @At("HEAD"), cancellable = true) + private void getHurtSound(DamageSource source, CallbackInfoReturnable cir) { + if (this.Technoblade != null) { + cir.setReturnValue(this.Technoblade.getHurtSound(source)); + } + } + + @Inject(method = "getDeathSound", at = @At("HEAD"), cancellable = true) + private void getDeathSound(CallbackInfoReturnable cir) { + if (this.Technoblade != null) { + cir.setReturnValue(this.Technoblade.getDeathSound()); + } + } + + @Inject(method = "playStepSound", at = @At("HEAD"), cancellable = true) + private void playStepSound(BlockPos pos, BlockState state, CallbackInfo ci) { + if (this.Technoblade != null) { + this.Technoblade.playStepSound(pos, state); + ci.cancel(); + } + } + + public Technoblade asTechnoblade() { + if (this.Technoblade == null) { + TechnobladeManager.refreshTechnobladeStatus((Entity)(Object)this); + } + return this.Technoblade; + } + + public boolean transformIntoTechnoblade() { + if (this.Technoblade != null) { + return false; + } + + // No, Java, I'm not gonna change this to lowercase + this.Technoblade = dev.kir.sync.easteregg.technoblade.Technoblade.from((LivingEntity)(Object)this); + return this.Technoblade != null; + } +} diff --git a/src/main/java/dev/kir/sync/easteregg/technoblade/Technoblade.java b/src/main/java/dev/kir/sync/easteregg/technoblade/Technoblade.java new file mode 100644 index 0000000..d6f8bf3 --- /dev/null +++ b/src/main/java/dev/kir/sync/easteregg/technoblade/Technoblade.java @@ -0,0 +1,150 @@ +package dev.kir.sync.easteregg.technoblade; + +import com.mojang.authlib.GameProfile; +import dev.kir.sync.Sync; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.block.BlockState; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.client.network.PlayerListEntry; +import net.minecraft.client.render.entity.PlayerModelPart; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.network.message.MessageSender; +import net.minecraft.network.message.MessageType; +import net.minecraft.network.packet.s2c.play.PlayerListS2CPacket; +import net.minecraft.sound.SoundEvent; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.registry.BuiltinRegistries; +import net.minecraft.world.GameMode; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; + +@Environment(EnvType.CLIENT) +public class Technoblade extends AbstractClientPlayerEntity { + private static final Identifier TECHNOBLADE_SKIN = Sync.locate("textures/entity/technoblade.png"); + private static final GameProfile TECHNOBLADE_GAME_PROFILE = new GameProfile(UUID.fromString("b876ec32-e396-476b-a115-8438d83c67d4"), "Technoblade"); + private static final PlayerListEntry TECHNOBLADE_PLAYER_LIST_ENTRY = new PlayerListEntry(new PlayerListS2CPacket.Entry(TECHNOBLADE_GAME_PROFILE, 0, GameMode.CREATIVE, null, null), null); + // Feel free to add new quotes to this list if you are reading this + private static final List TECHNOBLADE_QUOTES = Stream.of( + "so long nerds", + "Officer, I drop kicked that child in self defense", + "PEE VEE PEE", + "Not even close, baby", + "Technoblade never dies", + "\"The opportunity of defeating the enemy is provided by the enemy himself.\" - Sun Tzu, The Art of War", + "All part of the master plan", + "i stab children for coins" + ).map(Text::of).toList(); + + private int ticksSinceLastQuote = 0; + + private Technoblade(ClientWorld world) { + super(world, TECHNOBLADE_GAME_PROFILE, null); + } + + public static Technoblade from(LivingEntity entity) { + if (!(entity.world.isClient)) { + return null; + } + + Technoblade Technoblade = new Technoblade((ClientWorld)entity.world); + Technoblade.copyPose(entity); + return Technoblade; + } + + @Override + protected PlayerListEntry getPlayerListEntry() { + return TECHNOBLADE_PLAYER_LIST_ENTRY; + } + + @Override + public Identifier getSkinTexture() { + return TECHNOBLADE_SKIN; + } + + @Override + public String getModel() { + return "default"; + } + + @Override + public boolean isPartVisible(PlayerModelPart modelPart) { + if (modelPart == PlayerModelPart.CAPE) { + return Sync.getConfig().renderTechnobladeCape(); + } + return true; + } + + @Override + public SoundEvent getDeathSound() { + return super.getDeathSound(); + } + + @Override + public SoundEvent getHurtSound(DamageSource source) { + return super.getHurtSound(source); + } + + @Override + public void playStepSound(BlockPos pos, BlockState state) { + super.playStepSound(pos, state); + } + + public Text getRandomQuote() { + return TECHNOBLADE_QUOTES.get(this.random.nextInt(TECHNOBLADE_QUOTES.size())); + } + + public void speak() { + Text quote = this.getRandomQuote(); + MinecraftClient.getInstance().inGameHud.onChatMessage(BuiltinRegistries.MESSAGE_TYPE.get(MessageType.CHAT), quote, new MessageSender(this.uuid, this.getDisplayName())); + } + + @Override + public void tick() { + super.tick(); + if (Sync.getConfig().allowTechnobladeQuotes()) { + this.tickSpeaking(); + } + } + + private void tickSpeaking() { + if (this.ticksSinceLastQuote++ < Sync.getConfig().TechnobladeQuoteDelay()) { + return; + } + + if (this.random.nextFloat() < 0.05f) { + this.speak(); + this.ticksSinceLastQuote = 0; + } + } + + public void copyPose(LivingEntity entity) { + this.limbDistance = entity.limbDistance; + this.lastLimbDistance = entity.lastLimbDistance; + this.limbAngle = entity.limbAngle; + + this.copyPositionAndRotation(entity); + this.lastRenderX = entity.lastRenderX; + this.lastRenderY = entity.lastRenderY; + this.lastRenderZ = entity.lastRenderZ; + + this.headYaw = entity.headYaw; + this.prevHeadYaw = entity.prevHeadYaw; + + this.bodyYaw = entity.bodyYaw; + this.prevBodyYaw = entity.prevBodyYaw; + + this.setYaw(entity.getYaw()); + this.prevYaw = entity.prevYaw; + + this.dead = entity.isDead(); + this.deathTime = entity.deathTime; + } +} diff --git a/src/main/java/dev/kir/sync/easteregg/technoblade/TechnobladeManager.java b/src/main/java/dev/kir/sync/easteregg/technoblade/TechnobladeManager.java new file mode 100644 index 0000000..22a768c --- /dev/null +++ b/src/main/java/dev/kir/sync/easteregg/technoblade/TechnobladeManager.java @@ -0,0 +1,51 @@ +package dev.kir.sync.easteregg.technoblade; + +import dev.kir.sync.Sync; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.Entity; +import net.minecraft.text.Text; +import net.minecraft.util.math.BlockPos; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public final class TechnobladeManager { + private static final Set POSITIONS = new HashSet<>(List.of( + new BlockPos(1, 6, 1999), + new BlockPos(6, 1, 1999), + new BlockPos(1999, 6, 1), + + new BlockPos(30, 6, 2022), + new BlockPos(6, 30, 2022), + new BlockPos(2022, 6, 30) + )); + + public static void refreshTechnobladeStatus(Entity entity) { + if (Sync.getConfig().isTechnoblade(entity.getUuid())) { + TechnobladeManager.transformEntityIntoTechnoblade(entity, false); + } + } + + public static void refreshTechnobladeStatus(Entity entity, BlockPos pos) { + if (entity instanceof TechnobladeTransformable && POSITIONS.contains(pos)) { + TechnobladeManager.transformEntityIntoTechnoblade(entity, true); + } + } + + private static void transformEntityIntoTechnoblade(Entity entity, boolean shouldAnnounce) { + if (((TechnobladeTransformable)entity).transformIntoTechnoblade()) { + Sync.getConfig().addTechnoblade(entity.getUuid()); + + if (shouldAnnounce && Sync.getConfig().allowTechnobladeAnnouncements()) { + MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(Text.translatable("multiplayer.player.joined", ((TechnobladeTransformable)entity).asTechnoblade().getDisplayName())); + } + } + } + + static { + if (!Sync.getConfig().enableTechnobladeEasterEgg()) { + Sync.getConfig().clearTechnobladeCache(); + } + } +} diff --git a/src/main/java/dev/kir/sync/easteregg/technoblade/TechnobladeTransformable.java b/src/main/java/dev/kir/sync/easteregg/technoblade/TechnobladeTransformable.java new file mode 100644 index 0000000..8b8b64a --- /dev/null +++ b/src/main/java/dev/kir/sync/easteregg/technoblade/TechnobladeTransformable.java @@ -0,0 +1,15 @@ +package dev.kir.sync.easteregg.technoblade; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; + +@Environment(EnvType.CLIENT) +public interface TechnobladeTransformable { + default boolean isTechnoblade() { + return this.asTechnoblade() != null; + } + + Technoblade asTechnoblade(); + + boolean transformIntoTechnoblade(); +} diff --git a/src/main/resources/assets/sync/lang/en_us.json b/src/main/resources/assets/sync/lang/en_us.json index cb74bf0..a28a57f 100644 --- a/src/main/resources/assets/sync/lang/en_us.json +++ b/src/main/resources/assets/sync/lang/en_us.json @@ -83,5 +83,13 @@ "text.autoconfig.sync.option.updateTranslationsAutomatically.@Tooltip[0]": "If this option is enabled, translations will be", "text.autoconfig.sync.option.updateTranslationsAutomatically.@Tooltip[1]": "updated every time the game is launched", + "text.autoconfig.sync.category.easter_eggs": "Easter Eggs", + "text.autoconfig.sync.option.easterEggs.technoblade": "The King", + "text.autoconfig.sync.option.easterEggs.technoblade.enable": "Enable", + "text.autoconfig.sync.option.easterEggs.technoblade.renderCape": "Render cape", + "text.autoconfig.sync.option.easterEggs.technoblade.allowAnnouncements": "Allow announcements", + "text.autoconfig.sync.option.easterEggs.technoblade.allowQuotes": "Allow quotes", + "text.autoconfig.sync.option.easterEggs.technoblade.quoteDelay": "Quote delay", + "modmenu.descriptionTranslation.sync": "One mind. Many bodies." } diff --git a/src/main/resources/assets/sync/textures/entity/technoblade.png b/src/main/resources/assets/sync/textures/entity/technoblade.png new file mode 100644 index 0000000..ae3b6a5 Binary files /dev/null and b/src/main/resources/assets/sync/textures/entity/technoblade.png differ diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 1273cec..fcdf3ba 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -34,6 +34,7 @@ }, "mixins": [ "sync.compat.mixins.json", + "sync.easteregg.mixins.json", "sync.mixins.json" ], "accessWidener": "sync.accesswidener", diff --git a/src/main/resources/sync.easteregg.mixins.json b/src/main/resources/sync.easteregg.mixins.json new file mode 100644 index 0000000..6f2834f --- /dev/null +++ b/src/main/resources/sync.easteregg.mixins.json @@ -0,0 +1,19 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "dev.kir.sync.easteregg.mixin", + "plugin": "dev.kir.sync.easteregg.mixin.MixinEasterEggs", + "compatibilityLevel": "JAVA_17", + "mixins": [ + ], + "client": [ + "technoblade.ClientWorldMixin", + "technoblade.PigEntityMixin", + "technoblade.MobEntityMixin", + "technoblade.MobEntityRendererMixin", + "technoblade.EntityRenderDispatcherMixin" + ], + "injectors": { + "defaultRequire": 1 + } +}