P3-20 : respawn/dim-change rehydrate (UX P0)
Wire ClientRigEquipmentHandler.rebuildBondageAnimations on : - ClientPlayerNetworkEvent.LoggingIn (client connects) - EntityJoinLevelEvent (spawn + dim change, filtered to LocalPlayer) PlayerRespawnEvent path already covered server-side via PlayerStateEventHandler.onPlayerRespawn -> V2EquipmentHelper.sync -> PacketSyncV2Equipment -> rebuild (hook P3-06). This listener fills the client-only gaps. Without this, a player who logs in with bondage items equipped (or teleports cross-dimension) sees their livingAnimations map empty until the next V2 capability sync or armor slot change arrives. Visible incoherence : item still equipped, bondage anim dropped, player plays vanilla WALK. Demo-killing regression. Handlers are idempotent and fire 2-3x at startup (login + spawn + first chunk load) — rebuildBondageAnimations is designed for that : resetLivingAnimations + re-apply current bindings, no state drift. Pure filter extracted to shouldRehydrate(entity, level, ePred, lPred) generic overload — mirrors the isBondageItem pattern in the sibling BondageEquipmentChangeListener. Lets tests bypass the MC bootstrap crash on Mockito.mock(LocalPlayer.class). Removed the TODO(P3-20) block in BondageEquipmentChangeListener that pointed to this task. 5 tests, all paths green (shouldRehydrate short-circuits, production predicate wiring + null-safety).
This commit is contained in:
@@ -203,12 +203,8 @@ public final class BondageEquipmentChangeListener {
|
|||||||
return bondagePredicate.test(stack);
|
return bondagePredicate.test(stack);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(P3-20) : Ajouter les sources "rehydrate" pour respawn / dimension
|
// Rehydrate sur login / dimension change / respawn : délégué à
|
||||||
// change / login, qui ne sont pas couvertes par PacketSyncV2Equipment
|
// BondageRehydrateListener (P3-20, task #63). PlayerEvent.PlayerRespawnEvent
|
||||||
// (race possible entre capability sync et client animator bootstrap) :
|
// est couvert côté serveur via PlayerStateEventHandler.onPlayerRespawn →
|
||||||
// - ClientPlayerNetworkEvent.LoggingIn → rebuild sur le LocalPlayer
|
// V2EquipmentHelper.sync → PacketSyncV2Equipment → rebuild (path P3-06).
|
||||||
// - EntityJoinLevelEvent → rebuild si player (remote /
|
|
||||||
// après dimension change)
|
|
||||||
// - PlayerEvent.PlayerRespawnEvent → rebuild post-respawn
|
|
||||||
// Scopé à P3-20 (task #63) pour garder la surface de ce PR minimale.
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.v2.client;
|
||||||
|
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import net.minecraft.client.player.LocalPlayer;
|
||||||
|
import net.minecraft.world.entity.Entity;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraftforge.api.distmarker.Dist;
|
||||||
|
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||||
|
import net.minecraftforge.client.event.ClientPlayerNetworkEvent;
|
||||||
|
import net.minecraftforge.event.entity.EntityJoinLevelEvent;
|
||||||
|
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||||
|
import net.minecraftforge.fml.common.Mod;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rehydrate client pour {@link ClientRigEquipmentHandler#rebuildBondageAnimations}
|
||||||
|
* sur les événements de cycle de vie lourds : login initial et changement de
|
||||||
|
* dimension / respawn chunk. Sans ça, la map {@code livingAnimations} du
|
||||||
|
* {@code ClientAnimator} reste vide (ou stale) jusqu'au prochain packet V2 ou
|
||||||
|
* changement d'équipement — UX pétée dès le 1er respawn/téléport.
|
||||||
|
*
|
||||||
|
* <h2>Rationale UX P0</h2>
|
||||||
|
* <p>Scénario cassant sans ce listener : le joueur porte un armbinder, meurt,
|
||||||
|
* respawn dans le world spawn. Le {@code PlayerPatch} côté client est
|
||||||
|
* ré-instancié (nouveau LocalPlayer post-respawn), son {@code ClientAnimator}
|
||||||
|
* a les defaults EF mais aucun binding bondage. L'item est toujours équipé
|
||||||
|
* (V2 capability préservée), mais visuellement le player joue WALK vanilla.
|
||||||
|
* Incohérence visuelle → immersion cassée → démo gameday ratée.</p>
|
||||||
|
*
|
||||||
|
* <h2>Cas couverts</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>{@link ClientPlayerNetworkEvent.LoggingIn}</b> — le client vient
|
||||||
|
* de se connecter au serveur (local ou distant). À ce stade, la
|
||||||
|
* capability V2 peut être vide (sync packet pas encore reçu) — le
|
||||||
|
* rebuild est alors no-op bénin. Filet de sécurité pour le cas où le
|
||||||
|
* sync packet arriverait après le bootstrap complet de l'animator.</li>
|
||||||
|
* <li><b>{@link EntityJoinLevelEvent}</b> (filtré sur {@link LocalPlayer})
|
||||||
|
* — fire au spawn et au changement de dimension. Note : fire aussi au
|
||||||
|
* premier chunk load (cas "first join" après login). On compte sur
|
||||||
|
* l'idempotence.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Cas hors-scope (déjà couverts)</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>{@code PlayerEvent.PlayerRespawnEvent}</b> — couvert côté serveur
|
||||||
|
* par {@code PlayerStateEventHandler.onPlayerRespawn} qui appelle
|
||||||
|
* {@code V2EquipmentHelper.sync(player)} → {@code PacketSyncV2Equipment}
|
||||||
|
* → {@code rebuildBondageAnimations} via le hook P3-06.</li>
|
||||||
|
* <li><b>Changement d'équipement bondage</b> — {@code PacketSyncV2Equipment}
|
||||||
|
* (P3-06, capability V2) + {@link BondageEquipmentChangeListener}
|
||||||
|
* (P3-06 bis, slots armor vanilla).</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Idempotence</h2>
|
||||||
|
* <p>Les deux handlers peuvent fire 2–3× au démarrage (login + spawn + first
|
||||||
|
* chunk load). {@link ClientRigEquipmentHandler#rebuildBondageAnimations} est
|
||||||
|
* idempotent par contrat : chaque appel repart de
|
||||||
|
* {@code resetLivingAnimations()} + re-applique les bindings courants.
|
||||||
|
* Overhead CPU négligeable (une poignée d'items équipés par player, itération
|
||||||
|
* {@code ArrayList} + {@code Map.put}). Pas de dedup tick-level.</p>
|
||||||
|
*
|
||||||
|
* <h2>Threading & side</h2>
|
||||||
|
* <p>Les deux events ne firent que côté client (Forge les émet uniquement
|
||||||
|
* depuis les paths client : {@code ClientPacketListener.handleLogin} pour
|
||||||
|
* {@code LoggingIn}, et {@code Level#addFreshEntity} côté client pour les
|
||||||
|
* entités qui rejoignent un {@code ClientLevel}). Le filtre
|
||||||
|
* {@link Level#isClientSide()} dans {@link #onEntityJoinLevel} est une garde
|
||||||
|
* défensive pour le cas où l'event fire côté serveur (le listener est
|
||||||
|
* {@code value = Dist.CLIENT} donc la classe ne charge pas sur serveur dédié,
|
||||||
|
* mais en solo intégré l'abonnement est actif sur les deux side-worlds d'une
|
||||||
|
* même JVM).</p>
|
||||||
|
*
|
||||||
|
* @see ClientRigEquipmentHandler#rebuildBondageAnimations
|
||||||
|
* @see BondageEquipmentChangeListener
|
||||||
|
* @see com.tiedup.remake.v2.bondage.network.PacketSyncV2Equipment
|
||||||
|
*/
|
||||||
|
@OnlyIn(Dist.CLIENT)
|
||||||
|
@Mod.EventBusSubscriber(
|
||||||
|
modid = TiedUpRigConstants.MODID,
|
||||||
|
bus = Mod.EventBusSubscriber.Bus.FORGE,
|
||||||
|
value = Dist.CLIENT
|
||||||
|
)
|
||||||
|
public final class BondageRehydrateListener {
|
||||||
|
|
||||||
|
private BondageRehydrateListener() {
|
||||||
|
// utility subscriber class
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler {@link ClientPlayerNetworkEvent.LoggingIn} — fire quand le
|
||||||
|
* client a fini de se connecter au serveur (après réception du packet
|
||||||
|
* {@code ClientboundLoginPacket}).
|
||||||
|
*
|
||||||
|
* <p>Appelle {@link ClientRigEquipmentHandler#rebuildBondageAnimations}
|
||||||
|
* sur le {@link LocalPlayer} fourni par l'event. À ce stade, la capability
|
||||||
|
* V2 est probablement vide — {@code getAllEquipped} retourne un map vide
|
||||||
|
* et le rebuild est un no-op bénin (animator.resetLivingAnimations + 0
|
||||||
|
* bindings). Le vrai rebuild arrive via
|
||||||
|
* {@code PacketSyncV2Equipment.handleOnClient} quand le serveur push la
|
||||||
|
* sync peu après.</p>
|
||||||
|
*
|
||||||
|
* <p><b>Pourquoi hook ici quand même ?</b> Filet de sécurité : si le
|
||||||
|
* sync packet est perdu ou retardé, on a au moins tenté un rebuild avec
|
||||||
|
* l'état courant. Et si l'animator n'est pas encore initialisé
|
||||||
|
* (race bootstrap), {@code rebuildBondageAnimations} retourne
|
||||||
|
* silencieusement (null-check cascade).</p>
|
||||||
|
*
|
||||||
|
* @param event l'event Forge ; {@link ClientPlayerNetworkEvent#getPlayer()}
|
||||||
|
* peut théoriquement être null (ex : event fire pendant
|
||||||
|
* {@code onDisconnect} mid-login) — on guard.
|
||||||
|
*/
|
||||||
|
@SubscribeEvent
|
||||||
|
public static void onLoggingIn(ClientPlayerNetworkEvent.LoggingIn event) {
|
||||||
|
LocalPlayer player = event.getPlayer();
|
||||||
|
if (player == null) return;
|
||||||
|
|
||||||
|
ClientRigEquipmentHandler.rebuildBondageAnimations(player);
|
||||||
|
|
||||||
|
TiedUpRigConstants.LOGGER.debug(
|
||||||
|
"[BondageRehydrateListener] Login rehydrate fired for {}",
|
||||||
|
player.getName().getString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler {@link EntityJoinLevelEvent} — fire au spawn d'une entité dans
|
||||||
|
* un {@code Level}. Filtré sur {@link LocalPlayer} uniquement : on ignore
|
||||||
|
* les NPCs, remote players et toute autre entité (leur rebuild passe par
|
||||||
|
* d'autres sources).
|
||||||
|
*
|
||||||
|
* <p><b>Sémantique Forge</b> : l'event fire dans
|
||||||
|
* {@code Level#addFreshEntity}, ce qui couvre :</p>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Spawn initial</b> après login (le {@code LocalPlayer} est
|
||||||
|
* ajouté au {@code ClientLevel}),</li>
|
||||||
|
* <li><b>Dimension change</b> (téléport Overworld ↔ Nether ↔ End : le
|
||||||
|
* {@code LocalPlayer} est removed de l'ancien level puis ajouté au
|
||||||
|
* nouveau — fire une fois par transition),</li>
|
||||||
|
* <li><b>Respawn après mort</b> (le client reçoit un nouveau
|
||||||
|
* {@code LocalPlayer} post-respawn ; même s'il est déjà couvert
|
||||||
|
* côté serveur via {@code PacketSyncV2Equipment}, un double-fire
|
||||||
|
* est idempotent).</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Le rebuild peut fire 2× au login (ce handler + {@link #onLoggingIn})
|
||||||
|
* — comportement attendu, idempotent.</p>
|
||||||
|
*
|
||||||
|
* @param event l'event Forge ; {@code getEntity()} et {@code getLevel()}
|
||||||
|
* sont contractuellement non-null.
|
||||||
|
*/
|
||||||
|
@SubscribeEvent
|
||||||
|
public static void onEntityJoinLevel(EntityJoinLevelEvent event) {
|
||||||
|
// Filter : uniquement le LocalPlayer courant. On ignore tous les NPCs
|
||||||
|
// et entities non-player ; les remote players (autres clients sur le
|
||||||
|
// serveur) voient leur rebuild via PacketSyncV2Equipment.handleOnClient
|
||||||
|
// quand leur capability sync arrive.
|
||||||
|
Entity entity = event.getEntity();
|
||||||
|
Level level = event.getLevel();
|
||||||
|
if (!shouldRehydrate(entity, level, IS_LOCAL_PLAYER, IS_CLIENT_SIDE)) return;
|
||||||
|
|
||||||
|
LocalPlayer player = (LocalPlayer) entity;
|
||||||
|
ClientRigEquipmentHandler.rebuildBondageAnimations(player);
|
||||||
|
|
||||||
|
TiedUpRigConstants.LOGGER.debug(
|
||||||
|
"[BondageRehydrateListener] EntityJoinLevel rehydrate fired for {} in level {}",
|
||||||
|
player.getName().getString(),
|
||||||
|
level.dimension().location()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Predicate production : une {@link Entity} est-elle un {@link LocalPlayer} ?
|
||||||
|
* Factorisé en constante pour permettre l'injection d'une variante test
|
||||||
|
* basée sur {@link Object} dummy — le pattern {@code Mockito.mock(LocalPlayer.class)}
|
||||||
|
* crashe sur {@code InternalError} car MC static init tente de charger le
|
||||||
|
* registre officiel hors runtime game.
|
||||||
|
*/
|
||||||
|
static final Predicate<Object> IS_LOCAL_PLAYER = e -> e instanceof LocalPlayer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Predicate production : un {@link Level} est-il côté client ? Extrait en
|
||||||
|
* constante pour la même raison (impossible de mocker {@link Level} sans
|
||||||
|
* bootstrap MC).
|
||||||
|
*/
|
||||||
|
static final Predicate<Object> IS_CLIENT_SIDE =
|
||||||
|
l -> l instanceof Level level && level.isClientSide();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Predicate pure testable — détermine si un event {@link EntityJoinLevelEvent}
|
||||||
|
* doit déclencher un rebuild. Vrai ssi :
|
||||||
|
* <ol>
|
||||||
|
* <li>{@code entity} matche {@code entityPredicate}
|
||||||
|
* (en prod : {@code instanceof LocalPlayer}),</li>
|
||||||
|
* <li>{@code level} matche {@code levelPredicate}
|
||||||
|
* (en prod : {@code instanceof Level && isClientSide()}).</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p>Overload générique {@code <E, L>} pour permettre les unit tests
|
||||||
|
* sans bootstrap MC. Les tests passent des {@code Object} dummy +
|
||||||
|
* predicates contrôlés ({@code ALWAYS_TRUE} / {@code ALWAYS_FALSE}),
|
||||||
|
* évitant le crash {@code Mockito.mock(LocalPlayer.class)} qui
|
||||||
|
* déclenche la static init MC (registries, block states, etc.).</p>
|
||||||
|
*
|
||||||
|
* <p><b>Null-safety</b> : les deux predicates sont responsables de leur
|
||||||
|
* propre null-handling. Les constantes prod {@link #IS_LOCAL_PLAYER} et
|
||||||
|
* {@link #IS_CLIENT_SIDE} short-circuit sur null via {@code instanceof}
|
||||||
|
* (qui retourne {@code false} pour null).</p>
|
||||||
|
*
|
||||||
|
* @param <E> type de l'entité ({@link Entity} en prod,
|
||||||
|
* {@link Object} en test)
|
||||||
|
* @param <L> type du level ({@link Level} en prod,
|
||||||
|
* {@link Object} en test)
|
||||||
|
* @param entity entity issue de l'event (tolère null)
|
||||||
|
* @param level level issu de l'event (tolère null)
|
||||||
|
* @param entityPredicate predicate "est-ce notre client player ?"
|
||||||
|
* @param levelPredicate predicate "est-ce un client-side level ?"
|
||||||
|
* @return {@code true} ssi les deux predicates retournent {@code true}
|
||||||
|
*/
|
||||||
|
static <E, L> boolean shouldRehydrate(
|
||||||
|
E entity,
|
||||||
|
L level,
|
||||||
|
Predicate<? super E> entityPredicate,
|
||||||
|
Predicate<? super L> levelPredicate
|
||||||
|
) {
|
||||||
|
if (!entityPredicate.test(entity)) return false;
|
||||||
|
return levelPredicate.test(level);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.v2.client;
|
||||||
|
|
||||||
|
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.function.Predicate;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests unitaires pour {@link BondageRehydrateListener} (P3-20, task #63).
|
||||||
|
*
|
||||||
|
* <h2>Stratégie</h2>
|
||||||
|
* <p>Les deux handlers publics
|
||||||
|
* ({@link BondageRehydrateListener#onLoggingIn} et
|
||||||
|
* {@link BondageRehydrateListener#onEntityJoinLevel}) nécessitent des events
|
||||||
|
* Forge + capability MC pour fire {@code rebuildBondageAnimations} — non
|
||||||
|
* testables sans bootstrap. La logique testable est donc isolée dans
|
||||||
|
* {@link BondageRehydrateListener#shouldRehydrate} (overload générique
|
||||||
|
* {@code <E, L>} qui permet de bypasser le bootstrap MC).</p>
|
||||||
|
*
|
||||||
|
* <p>Même pattern que {@link BondageEquipmentChangeListenerTest} : les
|
||||||
|
* entités/levels sont remplacés par des {@link Object} dummy et les
|
||||||
|
* predicates de filtre par des lambdas contrôlées. {@code Mockito.mock}
|
||||||
|
* sur {@code LocalPlayer}/{@code Level} crashe sur {@code InternalError}
|
||||||
|
* (MC static init tente de charger les registres hors runtime game).</p>
|
||||||
|
*/
|
||||||
|
class BondageRehydrateListenerTest {
|
||||||
|
|
||||||
|
/** Predicate qui considère chaque objet comme matchant. */
|
||||||
|
private static final Predicate<Object> ALWAYS_TRUE = o -> true;
|
||||||
|
|
||||||
|
/** Predicate qui considère chaque objet comme non-matchant. */
|
||||||
|
private static final Predicate<Object> ALWAYS_FALSE = o -> false;
|
||||||
|
|
||||||
|
// ========== shouldRehydrate (generic overload) ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity ne matche pas (ex : NPC, Zombie, remote player) → false.
|
||||||
|
* Le {@code levelPredicate} n'est PAS appelé (short-circuit).
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void shouldRehydrate_entityRejected_returnsFalse_levelPredicateNotInvoked() {
|
||||||
|
Predicate<Object> levelTripwire = l -> {
|
||||||
|
throw new AssertionError(
|
||||||
|
"levelPredicate ne doit pas etre appele apres entityPredicate false");
|
||||||
|
};
|
||||||
|
|
||||||
|
assertFalse(
|
||||||
|
BondageRehydrateListener.shouldRehydrate(
|
||||||
|
new Object(), new Object(), ALWAYS_FALSE, levelTripwire),
|
||||||
|
"entityPredicate false doit retourner false sans evaluer levelPredicate"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity matche (LocalPlayer) mais level server-side → false. Garde
|
||||||
|
* défensive : en solo intégré les deux sides coexistent dans la même
|
||||||
|
* JVM, on gate sur {@code isClientSide} pour ne rebuild que dans le
|
||||||
|
* pipeline client.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void shouldRehydrate_entityMatchLevelRejected_returnsFalse() {
|
||||||
|
assertFalse(
|
||||||
|
BondageRehydrateListener.shouldRehydrate(
|
||||||
|
new Object(), new Object(), ALWAYS_TRUE, ALWAYS_FALSE),
|
||||||
|
"LocalPlayer + server-side level doit retourner false"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity matche (LocalPlayer) ET level matche (client-side) → true.
|
||||||
|
* Cas nominal : le player local rejoint le {@code ClientLevel} (login,
|
||||||
|
* dim change, respawn chunk). Déclenche le rebuild.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void shouldRehydrate_bothMatch_returnsTrue() {
|
||||||
|
assertTrue(
|
||||||
|
BondageRehydrateListener.shouldRehydrate(
|
||||||
|
new Object(), new Object(), ALWAYS_TRUE, ALWAYS_TRUE),
|
||||||
|
"LocalPlayer + client-side level doit declencher rebuild"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Production predicates sanity ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Les predicates production {@link BondageRehydrateListener#IS_LOCAL_PLAYER}
|
||||||
|
* et {@link BondageRehydrateListener#IS_CLIENT_SIDE} existent et sont
|
||||||
|
* non-null. Garde contre un refactor qui les nullifierait silencieusement.
|
||||||
|
*
|
||||||
|
* <p>On vérifie aussi leur null-safety : {@code instanceof} retourne
|
||||||
|
* false pour null, donc aucun des deux predicates ne doit throw sur
|
||||||
|
* un null input.</p>
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void productionPredicates_areWiredAndNullSafe() {
|
||||||
|
assertNotNull(
|
||||||
|
BondageRehydrateListener.IS_LOCAL_PLAYER,
|
||||||
|
"IS_LOCAL_PLAYER doit etre non-null (wiring sanity)"
|
||||||
|
);
|
||||||
|
assertNotNull(
|
||||||
|
BondageRehydrateListener.IS_CLIENT_SIDE,
|
||||||
|
"IS_CLIENT_SIDE doit etre non-null (wiring sanity)"
|
||||||
|
);
|
||||||
|
// Null-safety via instanceof semantics
|
||||||
|
assertFalse(
|
||||||
|
BondageRehydrateListener.IS_LOCAL_PLAYER.test(null),
|
||||||
|
"IS_LOCAL_PLAYER(null) doit retourner false (instanceof null-safe)"
|
||||||
|
);
|
||||||
|
assertFalse(
|
||||||
|
BondageRehydrateListener.IS_CLIENT_SIDE.test(null),
|
||||||
|
"IS_CLIENT_SIDE(null) doit retourner false (instanceof null-safe)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Les predicates production appliqués à un {@link Object} dummy
|
||||||
|
* retournent false — ni Object n'est une {@code LocalPlayer}, ni un
|
||||||
|
* {@code Level}. Valide le {@code instanceof} check.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void productionPredicates_onDummyObject_returnFalse() {
|
||||||
|
Object dummy = new Object();
|
||||||
|
assertFalse(
|
||||||
|
BondageRehydrateListener.IS_LOCAL_PLAYER.test(dummy),
|
||||||
|
"Object dummy ne doit pas matcher IS_LOCAL_PLAYER"
|
||||||
|
);
|
||||||
|
assertFalse(
|
||||||
|
BondageRehydrateListener.IS_CLIENT_SIDE.test(dummy),
|
||||||
|
"Object dummy ne doit pas matcher IS_CLIENT_SIDE"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user