1. LockableComponent: remove duplicate "Lockable" tooltip line (ILockable.appendLockTooltip already handles lock status display) 2. ILockable/IHasResistance Javadoc: update @link refs from deleted V1 classes to V2 AbstractV2BondageItem/DataDrivenBondageItem 3. SettingsAccessor Javadoc: remove stale BindVariant @link references 4. DataDrivenBondageItem: update NECK block comment (remove branch ref) 5. Delete empty bondage3d/gags/ directory
8.4 KiB
D-01 Branch E : Resistance & Lock System Rework
Prérequis : Branch D (V1 cleanup) mergée. Branche :
feature/d01-branch-e-resistanceObjectif : Redesign complet du système de résistance/lock.
Nouveau modèle
Principes
- La résistance vient de l'item — définie dans le JSON via
ResistanceComponent, point final. - Le lock est binaire — on/off. Pas de "lock resistance" séparée. Le lock active la nécessité de struggle pour les items non-ARMS.
- ARMS = toujours actif — un bind aux bras nécessite toujours un struggle pour s'en libérer soi-même, locké ou non.
- Non-ARMS + pas locké = libre — un gag/blindfold/collar non-locké peut être retiré librement (par soi-même ou un autre joueur).
- Non-ARMS + locké = struggle requis — le lock active la résistance de l'item.
- Un autre joueur peut aider — retirer un item non-locké sur un autre joueur ne nécessite pas de struggle (aide).
Matrice de struggle
| Région | Locké ? | Self-remove | Autre joueur remove |
|---|---|---|---|
| ARMS | Non | Struggle (résistance item) | Libre (aide) |
| ARMS | Oui | Struggle (résistance item) | Struggle (résistance item) |
| Non-ARMS | Non | Libre | Libre |
| Non-ARMS | Oui | Struggle (résistance item) | Struggle (résistance item) |
Items organiques (slime, vine, web, tape)
Ces items sont "lockés par nature" — pas de padlock possible mais impossible à retirer sans struggle.
Option : Nouveau composant BuiltInLockComponent dans le JSON :
"components": {
"resistance": {"id": "slime"},
"built_in_lock": {}
}
BuiltInLockComponent :
blocksUnequip()retournetrue(comme un lock, mais sans padlock)ILockable.canAttachPadlock()retournefalse(déjà le cas pour les organiques)- L'item se comporte comme un ARMS bind : toujours struggle required
Alternative : Flag "always_locked": true sur la definition JSON. Plus simple, pas besoin de nouveau composant.
Problèmes actuels que ce rework corrige
P1. Singleton MAX scan
DataDrivenBondageItem.getBaseResistance(LivingEntity) retourne le MAX de tous les items data-driven équipés. Un gag résistance 50 hérite de la résistance 200 du chain bind.
Fix : Initialiser currentResistance dans le NBT à l'equip depuis ResistanceComponent.getBaseResistance(). Plus jamais de fallback au MAX scan runtime. Le getBaseResistance(LivingEntity) du singleton devient un no-op/fallback qui n'est plus utilisé par le struggle.
P2. isItemLocked() dead code
StruggleState.struggle() ne call jamais isItemLocked(). Le x10 penalty n'est jamais appliqué.
Fix : Supprimer le concept de "locked penalty". Avec le nouveau modèle, le lock active le struggle, il ne le ralentit pas. Si l'item est locké, il faut struggle avec la résistance complète de l'item. Si non locké (et non-ARMS), pas de struggle du tout.
P3. Lock resistance / item resistance déconnectés
ILockable.getLockResistance() vs IHasResistance.getBaseResistance() sont deux systèmes indépendants.
Fix : Supprimer ILockable.getLockResistance() / getCurrentLockResistance() / setCurrentLockResistance() / initializeLockResistance() / clearLockResistance(). La résistance du lockpick minigame utilise directement la résistance de l'item (ou un multiplicateur fixe).
P4. Dice-roll ignore le lock
Fix : Avec le nouveau modèle, le dice-roll ne change pas. C'est canStruggle() qui gate l'accès :
// StruggleBinds.canStruggle()
// ARMS: toujours struggle-able (self)
return true;
// StruggleCollar/StruggleAccessory.canStruggle()
// Non-ARMS: seulement si locké
return isLocked(stack) || hasBuiltInLock(stack);
Bugs pré-existants à corriger dans cette branche
B1. V1 ItemCollar.onUnequipped() — suppressed path skip unregister
Quand isRemovalAlertSuppressed() est true, ItemCollar.onUnequipped() return early SANS appeler CollarRegistry.unregisterWearer(). Entrées fantômes persistées.
Fichier : items/base/ItemCollar.java lignes 1382-1395
Fix : Ajouter unregisterWearer() dans le branch suppressed.
B2. DataDrivenItemRegistry.clear() pas synchronisé
clear() écrit SNAPSHOT = EMPTY sans acquérir RELOAD_LOCK. Race avec mergeAll().
Fichier : v2/bondage/datadriven/DataDrivenItemRegistry.java ligne 142
Fix : Synchroniser sur RELOAD_LOCK.
B3. V2TyingPlayerTask.heldStack reference stale
Le held item peut être remplacé entre début et fin du tying → item dupliqué.
Fichier : tasks/V2TyingPlayerTask.java ligne 80
Fix : Valider heldStack non-vide et matching avant equip dans onComplete().
B4. PlayerShockCollar ignore complètement les V2 collars
checkAutoShockCollar() dispatche exclusivement sur instanceof ItemShockCollarAuto et instanceof ItemGpsCollar. Les V2 data-driven collars avec ShockComponent ou GpsComponent ne déclenchent jamais les auto-shocks ni l'enforcement de zones GPS.
Fichier : state/components/PlayerShockCollar.java lignes 139-189
Fix : Utiliser CollarHelper.canShock(), CollarHelper.getShockInterval(), CollarHelper.hasGPS() pour la détection, avec fallback V1 pour les méthodes V1-specific (getSafeSpots()).
B5. EntityKidnapperMerchant.remove() memory leak
remove() appelle tradingPlayers.clear() mais ne nettoie PAS la playerToMerchant ConcurrentHashMap statique. Entrées stales accumulées sur les serveurs long-running.
Fichier : entities/EntityKidnapperMerchant.java ligne 966-981
Fix : Itérer tradingPlayers et appeler playerToMerchant.remove(uuid) avant le clear.
B6. Timer division potentiellement inversée (auto-shock)
PlayerShockCollar.java lignes 153-155 : collarShock.getInterval() / GameConstants.TICKS_PER_SECOND. Si Timer attend des ticks, la division réduit l'intervalle de 20x (shock toutes les 0.25s au lieu de 5s).
Fichier : state/components/PlayerShockCollar.java lignes 153-155 et 179-182
Fix : Vérifier le contrat du constructeur Timer. Si il attend des ticks, supprimer la division.
B7. StruggleState.isItemLocked() dead code
StruggleState.struggle() ne call JAMAIS isItemLocked(). Le penalty x10 pour les items padlockés n'est jamais appliqué.
Fichier : state/struggle/StruggleState.java ligne 53-133
Fix : Inclus dans le rework E2 (nouveau modèle resistance/lock).
Tâches
E1. Initialiser currentResistance à l'equip
Dans DataDrivenBondageItem.onEquipped() et les hooks V1 onEquipped() :
- Lire
ResistanceComponent.getBaseResistance()(ouIHasResistance.getBaseResistance()pour V1) - Écrire immédiatement dans le NBT via
setCurrentResistance(stack, base) - Élimine le MAX scan comme source d'initialisation
E2. Refactor canStruggle() — nouveau modèle
StruggleBinds.canStruggle(): ARMS → toujours true (self) si item existe- Nouveau
StruggleAccessory(ou refactor de StruggleCollar) : non-ARMS → true seulement si locké ou built-in lock - Supprimer
isItemLocked()penalty (dead code de toute façon)
E3. "Aide" — remove non-locké par un autre joueur
Modifier AbstractV2BondageItem.interactLivingEntity() :
- Si clic sur un joueur qui porte l'item ET item non-locké ET clicker n'a pas l'item en main → retirer l'item (aide)
- Ou via un packet dédié (clic droit main vide sur joueur attaché)
E4. BuiltInLockComponent ou flag always_locked
Pour les items organiques qui ne peuvent pas avoir de padlock mais nécessitent un struggle.
E5. Cleanup ILockable — supprimer lock resistance
Supprimer : getLockResistance(), getCurrentLockResistance(), setCurrentLockResistance(), initializeLockResistance(), clearLockResistance().
Le lockpick minigame utilise la résistance de l'item directement (ou un multiplicateur config).
E6. Fix bugs pré-existants (B1, B2, B3)
Vérification
- V2 bind résistance 50 + V2 gag résistance 80 : chacun a sa propre résistance (pas MAX)
- Gag non-locké → retirable sans struggle
- Gag locké → struggle avec résistance du gag
- Bind ARMS non-locké → self-struggle requis, autre joueur peut aider (libre)
- Bind ARMS locké → self-struggle requis, autre joueur aussi struggle
- Slime bind (built-in lock) → struggle obligatoire, pas de padlock possible
currentResistanceinitialisé dans NBT dès l'equip- CollarRegistry clean après removals légitimes
- Pas de duplication d'item via tying task