From cae349522503ad6d55395ca9ed22ba49a0c10e66 Mon Sep 17 00:00:00 2001 From: iceBear67 Date: Sun, 14 Jul 2024 19:51:31 +0800 Subject: [PATCH] add(feat): elytra speed meter --- .../io/ib67/sfcraft/SFCraftInitializer.java | 1 + .../io/ib67/sfcraft/callback/SFCallbacks.java | 6 + .../mixin/server/PlayerEntityMixin.java | 50 +++--- .../mixin/server/ServerPlayerEntityMixin.java | 41 +++++ .../mixin/server/bridge/EntityBridge.java | 17 ++ .../module/ElytraSpeedMeterModule.java | 156 ++++++++++++++++++ .../io/ib67/sfcraft/util/ColorHelper.java | 39 +++++ src/main/resources/sfcraft.mixins.json | 2 + 8 files changed, 286 insertions(+), 26 deletions(-) create mode 100644 src/main/java/io/ib67/sfcraft/mixin/server/ServerPlayerEntityMixin.java create mode 100644 src/main/java/io/ib67/sfcraft/mixin/server/bridge/EntityBridge.java create mode 100644 src/main/java/io/ib67/sfcraft/module/ElytraSpeedMeterModule.java create mode 100644 src/main/java/io/ib67/sfcraft/util/ColorHelper.java diff --git a/src/main/java/io/ib67/sfcraft/SFCraftInitializer.java b/src/main/java/io/ib67/sfcraft/SFCraftInitializer.java index 86db8aa..61b2588 100644 --- a/src/main/java/io/ib67/sfcraft/SFCraftInitializer.java +++ b/src/main/java/io/ib67/sfcraft/SFCraftInitializer.java @@ -40,6 +40,7 @@ public class SFCraftInitializer extends GuiceModInitializer { registerFeature(MotdModule.class); registerFeature(WelcomeModule.class); registerFeature(OfflineExemptModule.class); + registerFeature(ElytraSpeedMeterModule.class); registerFeature(PlayLimitModule.class); registerFeature(BackModule.class); registerFeature(ManagementModule.class); diff --git a/src/main/java/io/ib67/sfcraft/callback/SFCallbacks.java b/src/main/java/io/ib67/sfcraft/callback/SFCallbacks.java index 816b402..da0efda 100644 --- a/src/main/java/io/ib67/sfcraft/callback/SFCallbacks.java +++ b/src/main/java/io/ib67/sfcraft/callback/SFCallbacks.java @@ -33,6 +33,12 @@ public interface SFCallbacks { l -> (p, a) -> forEach(l, i -> i.onSwitchIdle(p, a))); Event PLAYER_SNEAKING = EventFactory.createArrayBacked(PlayerSneakingCallback.class, l -> (p, a) -> forEach(l, i -> i.onSneaking(p, a))); + Event PLAYER_FLYING = EventFactory.createArrayBacked(PlayerFlyingCallback.class, + l -> (p, t, f) -> forEach(l, i -> i.onFlyingTick(p, t, f))); + + interface PlayerFlyingCallback { + void onFlyingTick(PlayerEntity player, long flyingTick, boolean flying); + } interface PlayerSneakingCallback { void onSneaking(PlayerEntity player, boolean sneak); diff --git a/src/main/java/io/ib67/sfcraft/mixin/server/PlayerEntityMixin.java b/src/main/java/io/ib67/sfcraft/mixin/server/PlayerEntityMixin.java index adec5bb..d34acb5 100644 --- a/src/main/java/io/ib67/sfcraft/mixin/server/PlayerEntityMixin.java +++ b/src/main/java/io/ib67/sfcraft/mixin/server/PlayerEntityMixin.java @@ -1,41 +1,39 @@ package io.ib67.sfcraft.mixin.server; -import com.mojang.datafixers.util.Either; import io.ib67.sfcraft.callback.SFCallbacks; -import net.minecraft.entity.damage.DamageSource; +import io.ib67.sfcraft.mixin.server.bridge.EntityBridge; import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.server.network.ServerPlayerEntity; -import net.minecraft.util.Unit; -import net.minecraft.util.math.BlockPos; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; +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.CallbackInfo; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -@Mixin(ServerPlayerEntity.class) -public class PlayerEntityMixin { - @Shadow - private long lastActionTime; +@Mixin(PlayerEntity.class) +public abstract class PlayerEntityMixin { + private static final int THRESHLD_OF_ELYTRA_FLY = 2; + @Unique + private long flyTime; - @Inject(method = "onDeath", at = @At("TAIL")) - public void onDeath(DamageSource damageSource, CallbackInfo ci) { - SFCallbacks.PLAYER_DEATH.invoker().onPlayerDeath((ServerPlayerEntity) (Object) this, damageSource); - } - - @Inject(method = "trySleep", at = @At("HEAD"), cancellable = true) - public void onSleep(BlockPos pos, CallbackInfoReturnable> cir) { - var result = SFCallbacks.PLAYER_SLEEP.invoker().onPlayerSleep((ServerPlayerEntity) (Object) this, pos); - if (result.left().isPresent()) { - cir.setReturnValue(result); + @Inject(method = "tickMovement", at = @At("TAIL")) + public void tickMovement(CallbackInfo ci) { + var eb = ((EntityBridge) $this()); + if (eb.sfcraft$getFlag(EntityBridge.sfcraft$getFlyingFlagIndex())) { + flyTime++; + if (flyTime > 20 * THRESHLD_OF_ELYTRA_FLY) { // todo config + SFCallbacks.PLAYER_FLYING.invoker().onFlyingTick($this(), flyTime - 20*THRESHLD_OF_ELYTRA_FLY, true); + } + } else { + if (flyTime != 0) { + SFCallbacks.PLAYER_FLYING.invoker().onFlyingTick($this(), flyTime, false); + } + flyTime = 0; } } - @Inject(method = "updateLastActionTime", at = @At("HEAD")) - public void leaveAFK(CallbackInfo ci) { - if (lastActionTime == Long.MAX_VALUE) { - SFCallbacks.PLAYER_IDLE.invoker().onSwitchIdle((ServerPlayerEntity) (Object) this, false); - } + @Unique + public PlayerEntity $this() { + return (PlayerEntity) (Object) this; } + } diff --git a/src/main/java/io/ib67/sfcraft/mixin/server/ServerPlayerEntityMixin.java b/src/main/java/io/ib67/sfcraft/mixin/server/ServerPlayerEntityMixin.java new file mode 100644 index 0000000..96c59f5 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/mixin/server/ServerPlayerEntityMixin.java @@ -0,0 +1,41 @@ +package io.ib67.sfcraft.mixin.server; + +import com.mojang.datafixers.util.Either; +import io.ib67.sfcraft.callback.SFCallbacks; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.Unit; +import net.minecraft.util.math.BlockPos; +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 org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ServerPlayerEntity.class) +public class ServerPlayerEntityMixin { + @Shadow + private long lastActionTime; + + @Inject(method = "onDeath", at = @At("TAIL")) + public void onDeath(DamageSource damageSource, CallbackInfo ci) { + SFCallbacks.PLAYER_DEATH.invoker().onPlayerDeath((ServerPlayerEntity) (Object) this, damageSource); + } + + @Inject(method = "trySleep", at = @At("HEAD"), cancellable = true) + public void onSleep(BlockPos pos, CallbackInfoReturnable> cir) { + var result = SFCallbacks.PLAYER_SLEEP.invoker().onPlayerSleep((ServerPlayerEntity) (Object) this, pos); + if (result.left().isPresent()) { + cir.setReturnValue(result); + } + } + + @Inject(method = "updateLastActionTime", at = @At("HEAD")) + public void leaveAFK(CallbackInfo ci) { + if (lastActionTime == Long.MAX_VALUE) { + SFCallbacks.PLAYER_IDLE.invoker().onSwitchIdle((ServerPlayerEntity) (Object) this, false); + } + } +} diff --git a/src/main/java/io/ib67/sfcraft/mixin/server/bridge/EntityBridge.java b/src/main/java/io/ib67/sfcraft/mixin/server/bridge/EntityBridge.java new file mode 100644 index 0000000..30d63eb --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/mixin/server/bridge/EntityBridge.java @@ -0,0 +1,17 @@ +package io.ib67.sfcraft.mixin.server.bridge; + +import net.minecraft.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(Entity.class) +public interface EntityBridge { + @Accessor("FALL_FLYING_FLAG_INDEX") + static int sfcraft$getFlyingFlagIndex() { + throw new AssertionError(); + } + + @Invoker("getFlag") + boolean sfcraft$getFlag(int index); +} diff --git a/src/main/java/io/ib67/sfcraft/module/ElytraSpeedMeterModule.java b/src/main/java/io/ib67/sfcraft/module/ElytraSpeedMeterModule.java new file mode 100644 index 0000000..6e0707a --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/module/ElytraSpeedMeterModule.java @@ -0,0 +1,156 @@ +package io.ib67.sfcraft.module; + +import io.ib67.sfcraft.ServerModule; +import io.ib67.sfcraft.callback.SFCallbacks; +import io.ib67.sfcraft.util.ColorHelper; +import it.unimi.dsi.fastutil.objects.*; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ElytraItem; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Colors; +import net.minecraft.util.math.BlockPos; + +import java.awt.*; +import java.text.DecimalFormat; + +public class ElytraSpeedMeterModule extends ServerModule { + private static final int BAR_LEN = 20; + private static final int MAX_NORMAL_SPEED = 6; + private static final Color BAR_LEFT = new Color(85, 85, 255); + private static final Color BAR_RIGHT = new Color(255, 87, 51); + private static final Text[] ELYTRA_DURABILITY = new Text[100]; + private static final Text[] PROGRESS_BARS = new Text[BAR_LEN]; + private static final Text[] Y_METER = new Text[256]; + private final Object2ObjectMap playerFlyMap = new Object2ObjectOpenHashMap<>(); + private final Object2FloatMap lastMeasuredSpeed = new Object2FloatOpenHashMap<>(); + + static { + for (int i = 1; i <= BAR_LEN; i++) { + PROGRESS_BARS[i - 1] = buildProgressBar(i); + } + for (int i = 1; i <= 256; i++) { + Y_METER[i - 1] = makeYMeter(i); + } + for (int i = 1; i <= 100; i++) { + ELYTRA_DURABILITY[i - 1] = Text.literal("ELYTRA: " + i + "%").withColor(getDamageColor(i)); + } + } + + @Override + public void onInitialize() { + SFCallbacks.PLAYER_FLYING.register(this::onFlying); + ServerPlayConnectionEvents.DISCONNECT.register(this::onDisconnect); + } + + private void onDisconnect(ServerPlayNetworkHandler serverPlayNetworkHandler, MinecraftServer minecraftServer) { + clean(serverPlayNetworkHandler.getPlayer()); + } + + private void clean(ServerPlayerEntity player) { + playerFlyMap.remove(player); + lastMeasuredSpeed.removeFloat(player); + } + + private void onFlying(PlayerEntity player, long f, boolean b) { + if (b) { + var text = Text.literal(""); + text.append(generateYMeter(f, player)); + text.append(generateSpeedMeter(f, player)); + text.append(generateDurabilityMeter(f, player)); + player.sendMessage(text, true); + } else { + clean((ServerPlayerEntity) player); + } + } + + private Text generateDurabilityMeter(long f, PlayerEntity player) { + var stack = player.getInventory().getArmorStack(PlayerInventory.ARMOR_SLOTS[2]); + if (stack.isDamageable() && stack.isDamaged()) { + var remaining = stack.getMaxDamage() - stack.getDamage(); + var percent = (int) Math.ceil(((double) remaining / stack.getMaxDamage()) * 100); + return ELYTRA_DURABILITY[percent]; + } + return Text.literal(" ELYTRA: INFINITY").withColor(Colors.BLUE); + } + + private static int getDamageColor(int percent) { + if (percent > 70) { + return Colors.GREEN; + } else if (percent > 50) { + return Colors.LIGHT_YELLOW; + } else if (percent > 30) { + return Colors.LIGHT_RED; + } + return Colors.RED; + } + + private Text generateYMeter(long f, PlayerEntity player) { + var y = player.getBlockY(); + if (y <= 256 && y > 0) { + return Y_METER[y - 1]; + } else { + return makeYMeter(y); + } + } + + private static Text makeYMeter(int y) { + return Text.literal("Y: " + y + " | ").withColor(getYColor(y)); + } + + private static int getYColor(int y) { + if (y < 120) { + return Colors.YELLOW; + } else if (y <= 180) { + return Colors.GREEN; + } else { + return Colors.BLUE; + } + } + + private Text generateSpeedMeter(long flyTime, PlayerEntity player) { + final var MEASURE_TIME = 5; + if (!playerFlyMap.containsKey(player)) { + playerFlyMap.put(player, player.getBlockPos()); + return Text.empty(); + } + float speed; + if (flyTime % MEASURE_TIME == 0) { + var pos = playerFlyMap.get(player); + var distance = player.squaredDistanceTo(pos.getX(), pos.getY(), pos.getZ()); + speed = (float) (Math.abs(Math.round(distance / Math.pow(MEASURE_TIME, 2) * 100) * 0.01)); + lastMeasuredSpeed.put(player, speed); + playerFlyMap.put(player, player.getBlockPos()); + } else { + speed = lastMeasuredSpeed.getFloat(player); + } + var percentToSix = speed / MAX_NORMAL_SPEED; + var result = Text.literal("SPD: ["); + + + int steps = (int) Math.ceil(percentToSix * BAR_LEN); + if (steps > BAR_LEN - 1) { + steps = BAR_LEN - 1; + } + result.append(PROGRESS_BARS[steps]); + result.append("] " + Math.ceil(speed * 100) / 100 + "/s | "); + if (speed > MAX_NORMAL_SPEED) { + result.withColor(Colors.LIGHT_RED); + } + return result; + } + + private static Text buildProgressBar(int steps) { + var result = Text.literal(""); + for (int i = 0; i < steps; i++) { + var color = ColorHelper.interpolate(ColorHelper.GradientType.HSV, BAR_LEFT, BAR_RIGHT, (float) ((double) i / BAR_LEN)); + result.append(Text.literal("|").withColor(color.getRGB())); + } + result.append(Text.literal("|".repeat(BAR_LEN - steps)).withColor(Colors.LIGHT_GRAY)); + return result; + } +} diff --git a/src/main/java/io/ib67/sfcraft/util/ColorHelper.java b/src/main/java/io/ib67/sfcraft/util/ColorHelper.java new file mode 100644 index 0000000..29d426e --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/util/ColorHelper.java @@ -0,0 +1,39 @@ +package io.ib67.sfcraft.util; + +import java.awt.*; + +public class ColorHelper { + + public enum GradientType { + LINEAR, + HSV + } + + public static Color interpolate(GradientType type, Color color1, Color color2, float t) { + switch (type) { + case LINEAR: + return linearInterpolation(color1, color2, t); + case HSV: + return hsvInterpolation(color1, color2, t); + default: + throw new IllegalArgumentException("Unknown gradient type: " + type); + } + } + + private static Color linearInterpolation(Color color1, Color color2, float t) { + int r = (int) (color1.getRed() + (color2.getRed() - color1.getRed()) * t); + int g = (int) (color1.getGreen() + (color2.getGreen() - color1.getGreen()) * t); + int b = (int) (color1.getBlue() + (color2.getBlue() - color1.getBlue()) * t); + int a = (int) (color1.getAlpha() + (color2.getAlpha() - color1.getAlpha()) * t); + return new Color(r, g, b, a); + } + + private static Color hsvInterpolation(Color color1, Color color2, float t) { + float[] hsv1 = Color.RGBtoHSB(color1.getRed(), color1.getGreen(), color1.getBlue(), null); + float[] hsv2 = Color.RGBtoHSB(color2.getRed(), color2.getGreen(), color2.getBlue(), null); + float h = (hsv1[0] + (hsv2[0] - hsv1[0]) * t); + float s = (hsv1[1] + (hsv2[1] - hsv1[1]) * t); + float v = (hsv1[2] + (hsv2[2] - hsv1[2]) * t); + return Color.getHSBColor(h, s, v); + } +} diff --git a/src/main/resources/sfcraft.mixins.json b/src/main/resources/sfcraft.mixins.json index 6653856..8ddbe6f 100644 --- a/src/main/resources/sfcraft.mixins.json +++ b/src/main/resources/sfcraft.mixins.json @@ -11,6 +11,7 @@ "server.fixes.EndermanEntityMixin$ChaseGoalMixin", "server.bridge.ServerWorldBridge", "server.bridge.ServerPlayerBridge", + "server.bridge.EntityBridge", "server.MinecraftServerMixin", "server.ServerPlayNetworkHandlerMixin", "server.SleepManagerMixin", @@ -19,6 +20,7 @@ "server.AbstractFurnaceBlockMixin", "server.ServerHandshakeNetworkHandlerMixin", "server.LegacyQueryHandlerMixin", + "server.ServerPlayerEntityMixin", "server.PlayerEntityMixin", "server.ServerLoginNetworkHandlerMixin" ],