add(feat): elytra speed meter
This commit is contained in:
8 files changed
+285
-25
No files matched your search
@@ -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);
|
||||
|
||||
@@ -33,6 +33,12 @@ public interface SFCallbacks {
|
||||
l -> (p, a) -> forEach(l, i -> i.onSwitchIdle(p, a)));
|
||||
Event<PlayerSneakingCallback> PLAYER_SNEAKING = EventFactory.createArrayBacked(PlayerSneakingCallback.class,
|
||||
l -> (p, a) -> forEach(l, i -> i.onSneaking(p, a)));
|
||||
Event<PlayerFlyingCallback> 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);
|
||||
|
||||
@@ -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 = "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);
|
||||
}
|
||||
|
||||
@Inject(method = "trySleep", at = @At("HEAD"), cancellable = true)
|
||||
public void onSleep(BlockPos pos, CallbackInfoReturnable<Either<PlayerEntity.SleepFailureReason, Unit>> cir) {
|
||||
var result = SFCallbacks.PLAYER_SLEEP.invoker().onPlayerSleep((ServerPlayerEntity) (Object) this, pos);
|
||||
if (result.left().isPresent()) {
|
||||
cir.setReturnValue(result);
|
||||
} 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Either<PlayerEntity.SleepFailureReason, Unit>> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<PlayerEntity, BlockPos> playerFlyMap = new Object2ObjectOpenHashMap<>();
|
||||
private final Object2FloatMap<PlayerEntity> 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
],
|
||||
|
||||
Reference in new issue
Block a user