Modules implementation

This commit is contained in:
2026-03-08 15:55:44 +01:00
parent d68e47d1cc
commit ceeb64282f
6 changed files with 581 additions and 0 deletions
@@ -0,0 +1,80 @@
package org.saturnclient.impl.modules;
import net.minecraft.client.MinecraftClient;
import net.minecraft.entity.Entity;
import net.minecraft.util.hit.EntityHitResult;
import net.minecraft.util.hit.HitResult;
import net.minecraft.entity.LivingEntity;
import org.saturnclient.common.module.EntityModule;
import org.saturnclient.modules.HealthRenderState;
/**
* Fabric implementation of {@link EntityModule}.
*
* Crosshair targeting is resolved via {@link MinecraftClient#crosshairTarget}.
* Entity state for nametag rendering is supplied at render time by
* {@link org.saturnclient.modules.mixins.LivingEntityRendererMixin} which
* writes into the render state; {@link NametagsMixin} then constructs
* a {@link RenderStateEntityState} from the already-extracted snapshot.
*/
public class EntityModuleFabric implements EntityModule {
private final MinecraftClient mc;
public EntityModuleFabric(MinecraftClient mc) {
this.mc = mc;
}
@Override
public boolean isTargetingLivingEntity() {
HitResult target = mc.crosshairTarget;
if (target == null || target.getType() != HitResult.Type.ENTITY)
return false;
Entity hit = ((EntityHitResult) target).getEntity();
return hit instanceof LivingEntity;
}
// ---------------------------------------------------------------
// Render-state adapter
// ---------------------------------------------------------------
/**
* Wraps a {@link HealthRenderState} (already injected into the
* Minecraft render-state object) as a platform-neutral
* {@link EntityState} for use by {@link NametagsFeature}.
*
* This is constructed inside {@link NametagsMixin} where both the
* render-state mixin interface and the entity name are available.
*/
public static final class RenderStateEntityState implements EntityState {
private final String customName;
private final HealthRenderState hrs;
public RenderStateEntityState(String customName, HealthRenderState hrs) {
this.customName = customName;
this.hrs = hrs;
}
@Override
public String getCustomName() {
return customName;
}
@Override
public float getHealth() {
return hrs.saturn$getHealth();
}
@Override
public float getMaxHealth() {
return hrs.saturn$getMaxHealth();
}
@Override
public EntityType getEntityType() {
return hrs.saturn$getEntityType();
}
}
}
@@ -0,0 +1,63 @@
package org.saturnclient.impl.modules;
import net.minecraft.client.MinecraftClient;
import org.saturnclient.common.provider.ModuleProvider;
import org.saturnclient.common.module.*;
/**
* Fabric implementation of {@link ModuleProvider}.
*
* Instantiated once during client startup (e.g. in the mod initialiser)
* and passed to every feature constructor. All five sub-modules share
* the same {@link MinecraftClient} reference so there is no
* inconsistency between modules reading state within the same tick.
*
* <pre>{@code
* ModuleProvider provider = new ModuleProviderFabric(MinecraftClient.getInstance());
* FeatureManager.register(new AutoSprintFeature(provider));
* FeatureManager.register(new CoordinatesFeature(provider));
* // …
* }</pre>
*/
public class ModuleProviderFabric implements ModuleProvider {
private final PlayerModule player;
private final WorldModule world;
private final EntityModule entity;
private final RenderModule render;
private final NetworkModule network;
public ModuleProviderFabric(MinecraftClient mc) {
this.player = new PlayerModuleFabric(mc);
this.world = new WorldModuleFabric(mc);
this.entity = new EntityModuleFabric(mc);
this.render = new RenderModuleFabric(mc);
this.network = new NetworkModuleFabric(mc);
}
@Override
public PlayerModule player() {
return player;
}
@Override
public WorldModule world() {
return world;
}
@Override
public EntityModule entity() {
return entity;
}
@Override
public RenderModule render() {
return render;
}
@Override
public NetworkModule network() {
return network;
}
}
@@ -0,0 +1,37 @@
package org.saturnclient.impl.modules;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.PlayerListEntry;
import org.saturnclient.common.module.NetworkModule;
/**
* Fabric implementation of {@link NetworkModule}.
*
* Connection presence is checked via the network handler.
* Ping is read from the player's {@link PlayerListEntry} which the
* server populates; returns {@code -1} when unavailable.
*/
public class NetworkModuleFabric implements NetworkModule {
private final MinecraftClient mc;
public NetworkModuleFabric(MinecraftClient mc) {
this.mc = mc;
}
@Override
public boolean hasNetwork() {
return mc.getNetworkHandler() != null;
}
@Override
public int getPing() {
if (mc.player == null || mc.getNetworkHandler() == null)
return -1;
PlayerListEntry entry = mc.getNetworkHandler().getPlayerListEntry(mc.player.getUuid());
return entry != null ? entry.getLatency() : -1;
}
}
@@ -0,0 +1,301 @@
package org.saturnclient.impl.modules;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.entity.effect.StatusEffectInstance;
import net.minecraft.item.ItemStack;
import org.saturnclient.common.module.PlayerModule;
import org.saturnclient.common.ref.asset.SpriteRef;
import org.saturnclient.common.ref.game.ItemStackRef;
import org.saturnclient.feature.features.StatusEffectsFeature;
import org.saturnclient.saturnclient.SaturnClient;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Fabric implementation of {@link PlayerModule}.
*
* All access to {@link MinecraftClient} and {@link ClientPlayerEntity}
* is encapsulated here. Features only ever see the {@link PlayerModule}
* interface; they never import Minecraft classes directly.
*
* Every method guards against a missing player and returns a safe
* default so features are free of null checks.
*/
public class PlayerModuleFabric implements PlayerModule {
private final MinecraftClient mc;
public PlayerModuleFabric(MinecraftClient mc) {
this.mc = mc;
}
// ---------------------------------------------------------------
// Internal helper — never returns null
// ---------------------------------------------------------------
/** Returns the local player, or {@code null} if one is not loaded. */
private ClientPlayerEntity player() {
return mc.player;
}
// ---------------------------------------------------------------
// Presence
// ---------------------------------------------------------------
@Override
public boolean hasPlayer() {
return mc.player != null;
}
// ---------------------------------------------------------------
// Position
// ---------------------------------------------------------------
@Override
public int getX() {
return hasPlayer() ? (int) player().getX() : 0;
}
@Override
public int getY() {
return hasPlayer() ? (int) player().getY() : 0;
}
@Override
public int getZ() {
return hasPlayer() ? (int) player().getZ() : 0;
}
// ---------------------------------------------------------------
// Input / movement
// ---------------------------------------------------------------
@Override
public boolean isForwardPressed() {
return mc.options.forwardKey.isPressed();
}
@Override
public boolean isBackPressed() {
return mc.options.backKey.isPressed();
}
@Override
public boolean isLeftPressed() {
return mc.options.leftKey.isPressed();
}
@Override
public boolean isRightPressed() {
return mc.options.rightKey.isPressed();
}
@Override
public boolean isJumpPressed() {
return mc.options.jumpKey.isPressed();
}
@Override
public boolean isAttackPressed() {
return mc.options.attackKey.isPressed();
}
@Override
public boolean isUsePressed() {
return mc.options.useKey.isPressed();
}
@Override
public boolean isSneaking() {
return hasPlayer() && player().isSneaking();
}
@Override
public boolean isUsingItem() {
return hasPlayer() && player().isUsingItem();
}
@Override
public boolean hasHorizontalCollision() {
return hasPlayer() && player().horizontalCollision;
}
@Override
public boolean isOnGround() {
return hasPlayer() && player().isOnGround();
}
@Override
public void setSprinting(boolean sprinting) {
if (hasPlayer())
player().setSprinting(sprinting);
}
// ---------------------------------------------------------------
// Velocity
// ---------------------------------------------------------------
@Override
public Velocity getVelocity() {
if (!hasPlayer())
return Velocity.ZERO;
var v = player().getVelocity();
return new Velocity(v.x, v.y, v.z);
}
// ---------------------------------------------------------------
// Stats
// ---------------------------------------------------------------
@Override
public float getHealth() {
return hasPlayer() ? player().getHealth() : 0f;
}
// ---------------------------------------------------------------
// Equipment
// ---------------------------------------------------------------
@Override
public ItemStackRef getMainHand() {
return wrap(hasPlayer() ? player().getMainHandStack() : ItemStack.EMPTY);
}
@Override
public ItemStackRef getHelmet() {
return wrap(hasPlayer() ? player().getInventory().armor.get(3) : ItemStack.EMPTY);
}
@Override
public ItemStackRef getChestplate() {
return wrap(hasPlayer() ? player().getInventory().armor.get(2) : ItemStack.EMPTY);
}
@Override
public ItemStackRef getLeggings() {
return wrap(hasPlayer() ? player().getInventory().armor.get(1) : ItemStack.EMPTY);
}
@Override
public ItemStackRef getBoots() {
return wrap(hasPlayer() ? player().getInventory().armor.get(0) : ItemStack.EMPTY);
}
// ---------------------------------------------------------------
// Dummy equipment
// ---------------------------------------------------------------
@Override
public ItemStackRef getDummyMainHand() {
return wrap(hasPlayer() ? player().getInventory().armor.get(3) : ItemStack.EMPTY);
}
@Override
public ItemStackRef getDummyHelmet() {
return wrap(hasPlayer() ? player().getInventory().armor.get(3) : ItemStack.EMPTY);
}
@Override
public ItemStackRef getDummyChestplate() {
return wrap(hasPlayer() ? player().getInventory().armor.get(2) : ItemStack.EMPTY);
}
@Override
public ItemStackRef getDummyLeggings() {
return wrap(hasPlayer() ? player().getInventory().armor.get(1) : ItemStack.EMPTY);
}
@Override
public ItemStackRef getDummyBoots() {
return wrap(hasPlayer() ? player().getInventory().armor.get(0) : ItemStack.EMPTY);
}
// ---------------------------------------------------------------
// Status effects
// ---------------------------------------------------------------
@Override
public List<? extends StatusEffectsFeature.EffectView> getActiveEffects() {
if (!hasPlayer())
return Collections.emptyList();
var effects = player().getActiveStatusEffects().values();
List<FabricEffectView> result = new ArrayList<>(effects.size());
for (StatusEffectInstance instance : effects) {
result.add(new FabricEffectView(instance));
}
return result;
}
@Override
public List<? extends StatusEffectsFeature.EffectView> getDummyEffects() {
if (!hasPlayer())
return Collections.emptyList();
var effects = player().getActiveStatusEffects().values();
List<FabricEffectView> result = new ArrayList<>(effects.size());
for (StatusEffectInstance instance : effects) {
result.add(new FabricEffectView(instance));
}
return result;
}
// ---------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------
private static ItemStackRef wrap(ItemStack stack) {
return (ItemStackRef) (Object) stack;
}
// ---------------------------------------------------------------
// Inner — EffectView adapter
// ---------------------------------------------------------------
/**
* Bridges a Fabric {@link StatusEffectInstance} to the
* platform-neutral {@link StatusEffectsFeature.EffectView}.
*/
public static final class FabricEffectView implements StatusEffectsFeature.EffectView {
private final StatusEffectInstance instance;
public FabricEffectView(StatusEffectInstance instance) {
this.instance = instance;
}
@Override
public boolean shouldShowIcon() {
return instance.shouldShowIcon();
}
/**
* Returns the Minecraft sprite identifier for this effect's icon.
* The platform-specific renderer knows how to draw a
* {@link net.minecraft.util.Identifier}.
*/
@Override
public SpriteRef getIcon() {
return (SpriteRef) SaturnClient.client.getStatusEffectSpriteManager().getSprite(instance.getEffectType());
}
@Override
public boolean isInfinite() {
return instance.isInfinite();
}
@Override
public String getInfiniteText() {
return "∞";
}
@Override
public int getDurationSeconds() {
return instance.getDuration() / 20; // ticks → seconds
}
}
}
@@ -0,0 +1,64 @@
package org.saturnclient.impl.modules;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.option.Perspective;
import org.saturnclient.common.module.RenderModule;
/**
* Fabric implementation of {@link RenderModule}.
*
* Window dimensions and FPS are read directly from
* {@link MinecraftClient}. Camera perspective control delegates to
* {@link net.minecraft.client.option.GameOptions}.
*/
public class RenderModuleFabric implements RenderModule {
private final MinecraftClient mc;
public RenderModuleFabric(MinecraftClient mc) {
this.mc = mc;
}
// ---------------------------------------------------------------
// Window
// ---------------------------------------------------------------
@Override
public int getScaledWindowWidth() {
return mc.getWindow().getScaledWidth();
}
@Override
public int getScaledWindowHeight() {
return mc.getWindow().getScaledHeight();
}
// ---------------------------------------------------------------
// Performance
// ---------------------------------------------------------------
@Override
public int getFps() {
return mc.getCurrentFps();
}
// ---------------------------------------------------------------
// Camera perspective
// ---------------------------------------------------------------
@Override
public boolean isFirstPerson() {
return mc.options.getPerspective() == Perspective.FIRST_PERSON;
}
@Override
public void setFirstPerson() {
mc.options.setPerspective(Perspective.FIRST_PERSON);
}
@Override
public void setThirdPersonBack() {
mc.options.setPerspective(Perspective.THIRD_PERSON_BACK);
}
}
@@ -0,0 +1,36 @@
package org.saturnclient.impl.modules;
import net.minecraft.client.MinecraftClient;
import org.saturnclient.common.module.WorldModule;
/**
* Fabric implementation of {@link WorldModule}.
*
* Reads the client world's time directly from {@link MinecraftClient}.
* Returns safe defaults when no world is loaded.
*/
public class WorldModuleFabric implements WorldModule {
private final MinecraftClient mc;
public WorldModuleFabric(MinecraftClient mc) {
this.mc = mc;
}
@Override
public boolean hasWorld() {
return mc.world != null;
}
/**
* Returns the total world age in ticks, or {@code 0} when no world
* is loaded. Used by {@link TpsFeature#onTimePacket} (which is
* called from the mixin and stores its own copy) and by
* {@link DayCounterFeature} for the day count.
*/
@Override
public long getWorldAge() {
return hasWorld() ? mc.world.getTime() : 0L;
}
}