P3-19 : RIG debug overlay (F6 toggle)
Scope reduced to overlay only — E2E integration test deferred. Overlay displays current living motion + composite motion, equipped bondage count, livingAnimations bindings count, active layers w/ priority + anim. Critical for dev-visible feedback when placeholder identity anims don't change the visual rig but the pipeline is running correctly. Keybind : F6 (configurable in Controls menu, category TiedUp!). F6 chosen because unused by vanilla and preserves F3 for debug screen. No new accessor needed on Animator — getLivingAnimations() already exists (returns ImmutableMap.copyOf, safe for debug read). Layer priority reading falls back on Layer.toString() parsing for composite layers where the priority field is protected ; base layer uses the public getBaseLayerPriority(). Tests : 6 pure unit tests (toggle flag, null-player branch, motionName on vanilla+tiedup enums+null). Real render path is MC-runtime-bound and validated gameday only.
This commit is contained in:
@@ -125,6 +125,18 @@ public class ModKeybindings {
|
|||||||
CATEGORY
|
CATEGORY
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RIG debug overlay keybinding (P3-19) - Toggle the F3+B-style debug
|
||||||
|
* overlay that displays rig state in real-time.
|
||||||
|
* Default: F6 (unused by vanilla Minecraft).
|
||||||
|
*/
|
||||||
|
public static final KeyMapping RIG_DEBUG_KEY = new KeyMapping(
|
||||||
|
"key.tiedup.rig_debug",
|
||||||
|
InputConstants.Type.KEYSYM,
|
||||||
|
org.lwjgl.glfw.GLFW.GLFW_KEY_F6, // Default key: F6
|
||||||
|
CATEGORY
|
||||||
|
);
|
||||||
|
|
||||||
/** Track last sent state to avoid spamming packets */
|
/** Track last sent state to avoid spamming packets */
|
||||||
private static boolean lastForceSeatState = false;
|
private static boolean lastForceSeatState = false;
|
||||||
|
|
||||||
@@ -149,7 +161,8 @@ public class ModKeybindings {
|
|||||||
event.register(BOUNTY_KEY);
|
event.register(BOUNTY_KEY);
|
||||||
event.register(FORCE_SEAT_KEY);
|
event.register(FORCE_SEAT_KEY);
|
||||||
event.register(TIGHTEN_KEY);
|
event.register(TIGHTEN_KEY);
|
||||||
TiedUpMod.LOGGER.info("Registered {} keybindings", 7);
|
event.register(RIG_DEBUG_KEY);
|
||||||
|
TiedUpMod.LOGGER.info("Registered {} keybindings", 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== STRUGGLE MINI-GAME (uses vanilla movement keys) ====================
|
// ==================== STRUGGLE MINI-GAME (uses vanilla movement keys) ====================
|
||||||
@@ -280,6 +293,15 @@ public class ModKeybindings {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RIG debug overlay toggle (P3-19) — F6 by default
|
||||||
|
while (RIG_DEBUG_KEY.consumeClick()) {
|
||||||
|
boolean nowOn = com.tiedup.remake.rig.debug.RigDebugOverlay.toggle();
|
||||||
|
TiedUpMod.LOGGER.debug(
|
||||||
|
"[CLIENT] RIG debug overlay: {}",
|
||||||
|
nowOn ? "ENABLED" : "DISABLED"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
357
src/main/java/com/tiedup/remake/rig/debug/RigDebugOverlay.java
Normal file
357
src/main/java/com/tiedup/remake/rig/debug/RigDebugOverlay.java
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.debug;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.client.gui.Font;
|
||||||
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
|
import net.minecraft.client.player.LocalPlayer;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraftforge.api.distmarker.Dist;
|
||||||
|
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||||
|
import net.minecraftforge.client.event.RenderGuiOverlayEvent;
|
||||||
|
import net.minecraftforge.client.gui.overlay.VanillaGuiOverlay;
|
||||||
|
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||||
|
import net.minecraftforge.fml.common.Mod;
|
||||||
|
|
||||||
|
import com.tiedup.remake.core.TiedUpMod;
|
||||||
|
import com.tiedup.remake.rig.anim.LivingMotion;
|
||||||
|
import com.tiedup.remake.rig.anim.client.ClientAnimator;
|
||||||
|
import com.tiedup.remake.rig.anim.client.Layer;
|
||||||
|
import com.tiedup.remake.rig.anim.types.StaticAnimation;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.patch.PlayerPatch;
|
||||||
|
import com.tiedup.remake.rig.patch.TiedUpCapabilities;
|
||||||
|
import com.tiedup.remake.v2.BodyRegionV2;
|
||||||
|
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
|
||||||
|
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
|
||||||
|
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P3-19 — debug overlay textuel inspiré de F3+B (vanilla debug hitboxes).
|
||||||
|
*
|
||||||
|
* <p>Affiche en temps réel l'état du pipeline RIG du {@link LocalPlayer} :</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code currentLivingMotion} + {@code currentCompositeMotion}</li>
|
||||||
|
* <li>Nombre d'items bondage équipés (data-driven)</li>
|
||||||
|
* <li>Nombre de bindings {@code livingAnimations} dans l'animator</li>
|
||||||
|
* <li>Liste des layers actifs (base + composite) avec priority + anim courante</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p><b>Objectif dev</b> : distinguer pipeline broken vs pipeline OK mais anim
|
||||||
|
* identity invisible (cas placeholder JSON pré-Phase 4 où aucune anim Blender
|
||||||
|
* co-authored n'est encore bound). Sans ce feedback visuel, impossible de
|
||||||
|
* vérifier gameday que {@code addLivingAnimation(WALK_BOUND, ...)} a bien
|
||||||
|
* pushé le binding.</p>
|
||||||
|
*
|
||||||
|
* <h2>Usage</h2>
|
||||||
|
* <p>Toggle via la keybind {@code key.tiedup.rig_debug} (default F6, configurable
|
||||||
|
* dans le menu Controls, catégorie TiedUp!). L'overlay ne render que lorsque
|
||||||
|
* {@link #DEBUG_OVERLAY_ENABLED} est {@code true}.</p>
|
||||||
|
*
|
||||||
|
* <h2>Testabilité</h2>
|
||||||
|
* <p>{@link #buildOverlayLines(Player)} est package-private et pur — testable
|
||||||
|
* unit avec un player null ou un player sans patch. Le rendu effectif
|
||||||
|
* ({@link #onRenderOverlay}) nécessite MC runtime (GuiGraphics, Font,
|
||||||
|
* Minecraft.getInstance) et n'est validable qu'en gameday.</p>
|
||||||
|
*
|
||||||
|
* <h2>Perf</h2>
|
||||||
|
* <p>Coût négligeable quand {@link #DEBUG_OVERLAY_ENABLED} = false (early
|
||||||
|
* return). Quand actif, le build lit la capability + itère les layers composite
|
||||||
|
* (5 priorities max) → O(1) par frame.</p>
|
||||||
|
*/
|
||||||
|
@OnlyIn(Dist.CLIENT)
|
||||||
|
@Mod.EventBusSubscriber(
|
||||||
|
modid = TiedUpMod.MOD_ID,
|
||||||
|
bus = Mod.EventBusSubscriber.Bus.FORGE,
|
||||||
|
value = Dist.CLIENT
|
||||||
|
)
|
||||||
|
public final class RigDebugOverlay {
|
||||||
|
|
||||||
|
/** Toggle global — flipped par la keybind dans {@code ModKeybindings#onClientTick}. */
|
||||||
|
private static boolean DEBUG_OVERLAY_ENABLED = false;
|
||||||
|
|
||||||
|
/** Padding gauche/haut de l'overlay en pixels guiScaled. */
|
||||||
|
private static final int MARGIN = 4;
|
||||||
|
|
||||||
|
/** Hauteur d'une ligne en pixels (vanilla font = 9, +3 pour l'espacement). */
|
||||||
|
private static final int LINE_HEIGHT = 12;
|
||||||
|
|
||||||
|
/** Couleur par défaut (blanc), drop shadow activé dans drawString. */
|
||||||
|
private static final int COLOR_DEFAULT = 0xFFFFFFFF;
|
||||||
|
|
||||||
|
private RigDebugOverlay() {
|
||||||
|
// utility class — enregistrée via @EventBusSubscriber
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== TOGGLE STATE ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flip l'état de l'overlay. Appelé depuis le handler de keybind dans
|
||||||
|
* {@code ModKeybindings}.
|
||||||
|
*
|
||||||
|
* @return le nouvel état (après toggle)
|
||||||
|
*/
|
||||||
|
public static boolean toggle() {
|
||||||
|
DEBUG_OVERLAY_ENABLED = !DEBUG_OVERLAY_ENABLED;
|
||||||
|
return DEBUG_OVERLAY_ENABLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lecture de l'état courant (utilisé en tests). */
|
||||||
|
public static boolean isEnabled() {
|
||||||
|
return DEBUG_OVERLAY_ENABLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reset state — uniquement pour les tests. */
|
||||||
|
static void resetEnabledForTesting() {
|
||||||
|
DEBUG_OVERLAY_ENABLED = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== RENDER EVENT ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook render — s'attache au post-hotbar overlay (non-critical placement
|
||||||
|
* qui ne perturbe pas les HUD vanilla de gameplay).
|
||||||
|
*/
|
||||||
|
@SubscribeEvent
|
||||||
|
public static void onRenderOverlay(RenderGuiOverlayEvent.Post event) {
|
||||||
|
if (!DEBUG_OVERLAY_ENABLED) return;
|
||||||
|
|
||||||
|
// On attache sur le HOTBAR overlay pour ne render qu'une fois par frame
|
||||||
|
// (choisi arbitrairement — n'importe quel overlay post-render ferait
|
||||||
|
// l'affaire, HOTBAR est présent dans 100% des contextes gameplay).
|
||||||
|
if (event.getOverlay() != VanillaGuiOverlay.HOTBAR.type()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
LocalPlayer player = mc.player;
|
||||||
|
if (player == null) return;
|
||||||
|
|
||||||
|
GuiGraphics graphics = event.getGuiGraphics();
|
||||||
|
Font font = mc.font;
|
||||||
|
if (font == null) return;
|
||||||
|
|
||||||
|
List<String> lines = buildOverlayLines(player);
|
||||||
|
renderLines(graphics, font, lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render effectif des lignes en haut-gauche. Extrait pour pouvoir être
|
||||||
|
* appelé depuis un contexte non-event (future devtool).
|
||||||
|
*/
|
||||||
|
private static void renderLines(GuiGraphics graphics, Font font, List<String> lines) {
|
||||||
|
int y = MARGIN;
|
||||||
|
for (String line : lines) {
|
||||||
|
graphics.drawString(font, line, MARGIN, y, COLOR_DEFAULT, true);
|
||||||
|
y += LINE_HEIGHT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== PURE LOGIC (testable) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construit la liste de lignes à afficher. Pur (à l'exception de la lecture
|
||||||
|
* de la capability) — testable unit avec un player null.
|
||||||
|
*
|
||||||
|
* <p>Contrat null-safety :</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code player == null} → liste avec header + "player: null"</li>
|
||||||
|
* <li>patch absent → header + "patch: null"</li>
|
||||||
|
* <li>animator absent (server side / pas ClientAnimator) → header
|
||||||
|
* + "animator: null"</li>
|
||||||
|
* <li>nominal : header + motion + items + bindings + layers</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @return liste non-null, jamais vide (header toujours présent)
|
||||||
|
*/
|
||||||
|
static List<String> buildOverlayLines(Player player) {
|
||||||
|
List<String> lines = new ArrayList<>();
|
||||||
|
lines.add("§l[TiedUp RIG Debug]§r");
|
||||||
|
|
||||||
|
if (player == null) {
|
||||||
|
lines.add("§cplayer: null§r");
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerPatch<?> patch = TiedUpCapabilities.getPlayerPatch(player);
|
||||||
|
if (patch == null) {
|
||||||
|
lines.add("§cpatch: null§r");
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientAnimator animator = patch.getClientAnimator();
|
||||||
|
if (animator == null) {
|
||||||
|
lines.add("§canimator: null§r");
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
appendMotionLines(lines, patch);
|
||||||
|
appendItemCountLine(lines, player);
|
||||||
|
appendBindingsCountLine(lines, animator);
|
||||||
|
appendLayerLines(lines, animator);
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append les lignes {@code motion:} et {@code composite:} pour le patch
|
||||||
|
* fourni. Extrait pour lisibilité du {@link #buildOverlayLines} pipeline.
|
||||||
|
*/
|
||||||
|
private static void appendMotionLines(List<String> lines, PlayerPatch<?> patch) {
|
||||||
|
LivingMotion motion = patch.currentLivingMotion;
|
||||||
|
LivingMotion composite = patch.currentCompositeMotion;
|
||||||
|
lines.add(String.format("motion: §a%s§r", motionName(motion)));
|
||||||
|
lines.add(String.format("composite: §a%s§r", motionName(composite)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compte les items data-driven équipés. Un armbinder occupant 3 régions
|
||||||
|
* compte pour 1 (dédup identity via {@code distinct()} sur
|
||||||
|
* {@code identityHashCode} des stacks).
|
||||||
|
*/
|
||||||
|
private static void appendItemCountLine(List<String> lines, Player player) {
|
||||||
|
Map<BodyRegionV2, ItemStack> equipped = V2EquipmentHelper.getAllEquipped(player);
|
||||||
|
long count = equipped.values().stream()
|
||||||
|
.filter(s -> s != null && !s.isEmpty())
|
||||||
|
.filter(s -> DataDrivenItemRegistry.get(s) != null)
|
||||||
|
.mapToInt(System::identityHashCode)
|
||||||
|
.distinct()
|
||||||
|
.count();
|
||||||
|
lines.add(String.format("items: §e%d§r", count));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compte les bindings {@code livingAnimations} actifs dans l'animator.
|
||||||
|
* Utilise l'API publique {@link com.tiedup.remake.rig.anim.Animator#getLivingAnimations()}
|
||||||
|
* qui retourne une {@code ImmutableMap.copyOf} — safe à lire sans
|
||||||
|
* modification.
|
||||||
|
*/
|
||||||
|
private static void appendBindingsCountLine(List<String> lines, ClientAnimator animator) {
|
||||||
|
int bindings = animator.getLivingAnimations().size();
|
||||||
|
lines.add(String.format("bindings: §e%d§r", bindings));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append une ligne header {@code layers:} puis une ligne par layer actif
|
||||||
|
* (base + composite non-off).
|
||||||
|
*
|
||||||
|
* <p>Format ligne layer : {@code " [BASE|priority] anim_name"} — avec
|
||||||
|
* indentation 2 espaces pour visual grouping sous le header.</p>
|
||||||
|
*/
|
||||||
|
private static void appendLayerLines(List<String> lines, ClientAnimator animator) {
|
||||||
|
lines.add("§7layers:§r");
|
||||||
|
|
||||||
|
// Collect layer descriptions via iterAllLayers. On ne peut pas early-
|
||||||
|
// return depuis un Consumer, on collecte tout puis filtre.
|
||||||
|
List<String> layerLines = new ArrayList<>();
|
||||||
|
Consumer<Layer> layerCollector = layer -> {
|
||||||
|
if (layer.isOff() && !isBaseLayer(layer)) {
|
||||||
|
// Skip composite layers off — le base est toujours rendu,
|
||||||
|
// même s'il tourne en IDLE.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
layerLines.add(describeLayer(layer));
|
||||||
|
};
|
||||||
|
|
||||||
|
animator.iterAllLayers(layerCollector);
|
||||||
|
|
||||||
|
if (layerLines.isEmpty()) {
|
||||||
|
lines.add(" §8(none)§r");
|
||||||
|
} else {
|
||||||
|
lines.addAll(layerLines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format une ligne de description pour un {@link Layer}. Public-ish
|
||||||
|
* (package-private) pour testabilité.
|
||||||
|
*
|
||||||
|
* <p>Format :</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Base layer : {@code " [BASE/PRIORITY] anim_registry_name"}</li>
|
||||||
|
* <li>Composite layer : {@code " [COMP/PRIORITY] anim_registry_name"}</li>
|
||||||
|
* <li>Anim null/empty : {@code "(empty)"}</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
static String describeLayer(Layer layer) {
|
||||||
|
String kind = isBaseLayer(layer) ? "BASE" : "COMP";
|
||||||
|
String priority = layerPriorityName(layer);
|
||||||
|
String animName = currentAnimationName(layer);
|
||||||
|
return String.format(" §7[%s/%s]§r §b%s§r", kind, priority, animName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne le nom d'une {@link LivingMotion}. Pour un enum classique
|
||||||
|
* (LivingMotions, TiedUpLivingMotions), {@code toString()} renvoie
|
||||||
|
* {@code name()}. Pour null, retourne {@code "null"}.
|
||||||
|
*/
|
||||||
|
static String motionName(LivingMotion motion) {
|
||||||
|
return motion != null ? motion.toString() : "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne le nom du registre de l'animation courante jouée par le layer,
|
||||||
|
* ou {@code "(empty)"} si le player n'a pas d'anim (layer off / fraichement
|
||||||
|
* initialisé).
|
||||||
|
*/
|
||||||
|
private static String currentAnimationName(Layer layer) {
|
||||||
|
if (layer.animationPlayer == null || layer.animationPlayer.isEmpty()) {
|
||||||
|
return "(empty)";
|
||||||
|
}
|
||||||
|
AssetAccessor<? extends com.tiedup.remake.rig.anim.types.DynamicAnimation> anim =
|
||||||
|
layer.animationPlayer.getAnimation();
|
||||||
|
if (anim == null) return "(null)";
|
||||||
|
try {
|
||||||
|
var rl = anim.registryName();
|
||||||
|
return rl != null ? rl.toString() : "(unnamed)";
|
||||||
|
} catch (Exception e) {
|
||||||
|
// registryName() peut throw pour EMPTY_ANIMATION / LinkAnimation
|
||||||
|
// / LayerOffAnimation ; on fallback sur le class name.
|
||||||
|
return anim.getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne le nom de la priority d'un layer. Pour la base layer, on lit
|
||||||
|
* {@link Layer.BaseLayer#getBaseLayerPriority()}. Pour les composite,
|
||||||
|
* on lit le champ priority (protected — accessed via public describeLayer
|
||||||
|
* helper). Priority peut être null sur la base layer si elle n'a pas encore
|
||||||
|
* été initialisée (ne devrait jamais arriver après postInit, mais défensif).
|
||||||
|
*/
|
||||||
|
private static String layerPriorityName(Layer layer) {
|
||||||
|
if (layer instanceof Layer.BaseLayer baseLayer) {
|
||||||
|
Layer.Priority p = baseLayer.getBaseLayerPriority();
|
||||||
|
return p != null ? p.name() : "?";
|
||||||
|
}
|
||||||
|
// Pour un Layer composite, priority est protected — on passe par
|
||||||
|
// toString() en fallback et on parse. Plus simple : on regarde dans
|
||||||
|
// quelle entry du map il se trouve. Mais on n'a pas accès au map.
|
||||||
|
// Compromis pragmatique : le toString() de Layer inclut déjà la priority.
|
||||||
|
// Exemple : " Composite Layer(HIGH) : ..." → on extrait HIGH.
|
||||||
|
String toString = layer.toString();
|
||||||
|
int open = toString.indexOf('(');
|
||||||
|
int close = toString.indexOf(')');
|
||||||
|
if (open >= 0 && close > open) {
|
||||||
|
return toString.substring(open + 1, close);
|
||||||
|
}
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Teste si un {@link Layer} est la base layer. On ne peut pas utiliser
|
||||||
|
* {@code instanceof BaseLayer} car certains tests fournissent des mocks ;
|
||||||
|
* pragmatique : instanceof + null-check.
|
||||||
|
*/
|
||||||
|
private static boolean isBaseLayer(Layer layer) {
|
||||||
|
return layer instanceof Layer.BaseLayer;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -134,6 +134,7 @@
|
|||||||
"key.tiedup.bounties": "Bounty List",
|
"key.tiedup.bounties": "Bounty List",
|
||||||
"key.tiedup.force_seat": "Force Seat",
|
"key.tiedup.force_seat": "Force Seat",
|
||||||
"key.tiedup.tighten": "Tighten Binds",
|
"key.tiedup.tighten": "Tighten Binds",
|
||||||
|
"key.tiedup.rig_debug": "Toggle RIG Debug Overlay",
|
||||||
|
|
||||||
"gui.tiedup.adjust_position": "Adjust Position",
|
"gui.tiedup.adjust_position": "Adjust Position",
|
||||||
"gui.tiedup.bondage_inventory": "Bondage Inventory",
|
"gui.tiedup.bondage_inventory": "Bondage Inventory",
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.debug;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.LivingMotion;
|
||||||
|
import com.tiedup.remake.rig.anim.LivingMotions;
|
||||||
|
import com.tiedup.remake.rig.anim.TiedUpLivingMotions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests unitaires pour {@link RigDebugOverlay} (P3-19).
|
||||||
|
*
|
||||||
|
* <p>Les tests ne couvrent que la logique pure testable sans MC runtime :
|
||||||
|
* <ul>
|
||||||
|
* <li>toggle flag state</li>
|
||||||
|
* <li>null-player early return dans {@link RigDebugOverlay#buildOverlayLines}</li>
|
||||||
|
* <li>motionName pour enum + null</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Le rendu effectif (GuiGraphics, Font, iteration layers non-null) nécessite
|
||||||
|
* un client Minecraft live — validable uniquement en gameday.</p>
|
||||||
|
*/
|
||||||
|
class RigDebugOverlayTest {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void ensureOff() {
|
||||||
|
RigDebugOverlay.resetEnabledForTesting();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void resetAfter() {
|
||||||
|
RigDebugOverlay.resetEnabledForTesting();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== toggle state ==========
|
||||||
|
|
||||||
|
/** L'état initial doit être false (overlay off par défaut). */
|
||||||
|
@Test
|
||||||
|
void isEnabled_defaultIsFalse() {
|
||||||
|
assertFalse(RigDebugOverlay.isEnabled(),
|
||||||
|
"Overlay doit etre off au bootstrap — on active a la demande via keybind");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Toggle flip l'état ; retour = nouvel état (true après un flip). */
|
||||||
|
@Test
|
||||||
|
void toggle_flipsState() {
|
||||||
|
assertFalse(RigDebugOverlay.isEnabled());
|
||||||
|
|
||||||
|
boolean afterFirst = RigDebugOverlay.toggle();
|
||||||
|
assertTrue(afterFirst, "Apres 1er toggle depuis off, retour = true");
|
||||||
|
assertTrue(RigDebugOverlay.isEnabled());
|
||||||
|
|
||||||
|
boolean afterSecond = RigDebugOverlay.toggle();
|
||||||
|
assertFalse(afterSecond, "Apres 2e toggle, retour = false");
|
||||||
|
assertFalse(RigDebugOverlay.isEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== buildOverlayLines null-safety ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link RigDebugOverlay#buildOverlayLines(net.minecraft.world.entity.player.Player)}
|
||||||
|
* avec null ne doit pas throw — early return avec le header + "player: null".
|
||||||
|
*
|
||||||
|
* <p>Ce test ne référence JAMAIS une classe MC runtime autre que
|
||||||
|
* {@code Player} (qui n'est que dans la signature). Appeler avec null
|
||||||
|
* n'exécute pas le code qui touche les capabilities.</p>
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void buildOverlayLines_nullPlayer_returnsErrorLineNoThrow() {
|
||||||
|
List<String> lines = assertDoesNotThrow(() -> RigDebugOverlay.buildOverlayLines(null));
|
||||||
|
assertNotNull(lines);
|
||||||
|
assertEquals(2, lines.size(),
|
||||||
|
"Header + player: null — pas d'autre ligne quand player est null");
|
||||||
|
assertTrue(lines.get(0).contains("TiedUp RIG Debug"),
|
||||||
|
"Premiere ligne = header");
|
||||||
|
assertTrue(lines.get(1).contains("player: null"),
|
||||||
|
"Deuxieme ligne = marqueur d'erreur player null");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== motionName ==========
|
||||||
|
|
||||||
|
/** Motion non-null → toString() (= enum name() pour LivingMotions vanilla). */
|
||||||
|
@Test
|
||||||
|
void motionName_vanillaEnum_returnsName() {
|
||||||
|
LivingMotion walk = LivingMotions.WALK;
|
||||||
|
assertEquals("WALK", RigDebugOverlay.motionName(walk));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Motion TiedUp enum → toString() idem. */
|
||||||
|
@Test
|
||||||
|
void motionName_tiedUpEnum_returnsName() {
|
||||||
|
LivingMotion bound = TiedUpLivingMotions.values()[0]; // premier motion custom
|
||||||
|
assertEquals(bound.toString(), RigDebugOverlay.motionName(bound),
|
||||||
|
"TiedUpLivingMotions est aussi un enum → toString() = name()");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Motion null → "null" (ne throw pas). */
|
||||||
|
@Test
|
||||||
|
void motionName_nullMotion_returnsLiteralNull() {
|
||||||
|
assertEquals("null", RigDebugOverlay.motionName(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user