Commit Graph

72 Commits

Author SHA1 Message Date
NotEvil
22d79a452b refactor(S-02/S-05): extract PlayerMovement component + fix thread safety
- Extract 11 movement fields from PlayerBindState into PlayerMovement component
- Replace volatile isStruggling/struggleStartTick pair with atomic StruggleSnapshot record
- Remove 5+2 misleading synchronized keywords (different monitors, all server-thread-only)
- Update all 36 MovementStyleManager field accesses to use getMovement() getters/setters
2026-04-15 16:48:22 +02:00
70f85b58a6 Merge pull request 'chore/audit-c01-i18n' (#12) from chore/audit-c01-i18n into develop
Reviewed-on: #12
2026-04-15 14:48:07 +00:00
NotEvil
c34bac11b0 fix(C-01): review fixes — missing keys, duplicate, GPS shock i18n
- Add missing keephead_enabled/disabled translation keys (BUG-001)
- Remove duplicate gui.tiedup.close key at line 403 (RISK-004)
- Fix GPS shock: use null addon + send GPS_ZONE_VIOLATION separately (RISK-001)
2026-04-15 14:08:41 +02:00
NotEvil
fa5cfb913c feat(C-01): i18n main commands — 148 translatable keys
Phase 3: Migrate Component.literal() in all remaining command files.
- NPCCommand (34), CellCommand (33), SocialCommand (16), CollarCommand (25),
  KeyCommand (18), BountyCommand (6), KidnapSetCommand (2), CaptivityDebugCommand (7),
  InventorySubCommand (3), TestAnimSubCommand (2), MasterTestSubCommand (7), DebtSubCommand (8)
- Strip all section sign color codes, use .withStyle(ChatFormatting)
- 148 new keys in en_us.json (command.tiedup.*)
- Debug/dynamic strings intentionally kept as literal
2026-04-15 13:54:26 +02:00
NotEvil
70965c2dda feat(C-01): i18n subcommands — 33 translatable keys
Phase 2: Migrate all Component.literal() in 5 subcommand files.
- BindCommands, GagCommands, BlindfoldCommands, CollarCommands, AccessoryCommands
- Strip \u00a7a section signs, use .withStyle(ChatFormatting.GREEN)
- Add 33 keys to en_us.json (command.tiedup.*)
- Shared error key: command.tiedup.error.no_state
2026-04-15 13:28:02 +02:00
NotEvil
0662739fe0 feat(C-01): i18n SystemMessageManager — 83 translatable keys
Phase 1: Core system message migration to Component.translatable().
- Replace getMessageTemplate() hardcoded strings with getTranslationKey() key derivation
- All send methods now use Component.translatable() with positional args
- Add 83 keys to en_us.json (msg.tiedup.system.*)
- Add sendTranslatable() convenience for external callers with string args
- Migrate 3 external getTemplate() callers (PlayerShockCollar, CellRegistryV2)
- Add resistance_suffix key for sendWithResistance()
2026-04-15 13:24:05 +02:00
ac72f6aae7 Merge pull request 'chore/quickwin-fix' (#11) from chore/quickwin-fix into develop
Reviewed-on: #11
2026-04-15 09:22:08 +00:00
NotEvil
d3bdb026f3 fix: restore missing /tiedup tie command registration (review) 2026-04-15 11:14:28 +02:00
NotEvil
c1e1f56058 refactor: split BondageSubCommand 1207L → 5 focused files (UC-01)
- BindCommands.java: tie, untie (156L)
- GagCommands.java: gag, ungag (140L)
- BlindfoldCommands.java: blindfold, unblind (142L)
- CollarCommands.java: collar, takecollar, enslave, free (288L)
- AccessoryCommands.java: putearplugs, takeearplugs, putclothes,
  takeclothes, fullyrestrain, adjust (469L)
- BondageSubCommand.java: thin delegator (19L)

Zero logic changes — purely mechanical code move.
2026-04-15 11:09:48 +02:00
NotEvil
f945e9449b feat(D-01/E): quickwins — debug toggle, HumanChairHelper move, collar equip
Q1: Remove F9 debug toggle from GltfAnimationApplier + delete dead
    GltfRenderLayer + remove keybind registration

Q2: Move HumanChairHelper from state/ to util/ — pure utility with
    no state dependency. 7 import updates.

Q3: Wire NECK collar equip flow in DataDrivenBondageItem:
    - Target must be tied up (V1 rule preserved)
    - Distance + line-of-sight validation
    - Owner added to NBT before equip via CollarHelper.addOwner()
    - V2EquipmentHelper handles conflict resolution
    - ModSounds.COLLAR_PUT played on success
    - OwnershipComponent.onEquipped registers in CollarRegistry
2026-04-15 10:57:01 +02:00
cc0ce89de5 Merge pull request 'feature/d01-branch-e-resistance' (#10) from feature/d01-branch-e-resistance into develop
Reviewed-on: #10
2026-04-15 01:45:58 +00:00
NotEvil
db407ee68f fix(D-01/E): padlock resistance double-count + knife progress persist (review)
BUG-001: PacketV2StruggleStart only adds padlock resistance bonus on
first session (when resistance >= base). Interrupted sessions that
persisted combined resistance no longer re-add the bonus, preventing
infinite resistance inflation.

BUG-002: PacketV2LockToggle UNLOCK clears knifeCutProgress and
accessoryStruggleResistance NBT keys, preventing partial knife-cut
progress from persisting across lock/unlock cycles.
2026-04-15 03:44:12 +02:00
NotEvil
d6bb030ad7 feat(D-01/E): resistance & lock system rework (E1-E7)
E1: Initialize currentResistance in NBT at equip time from
    ResistanceComponent — eliminates MAX-scan fallback bug

E2: BuiltInLockComponent for organic items (already committed)

E3: canStruggle refactor — new model:
    - ARMS: always struggle-able (no lock gating)
    - Non-ARMS: only if locked OR built-in lock
    - Removed dead isItemLocked() from StruggleState + overrides

E4: canUnequip already handled by BuiltInLockComponent.blocksUnequip()
    via ComponentHolder delegation

E5: Help/assist mechanic deferred (needs UI design)

E6: Removed lock resistance from ILockable (5 methods + NBT key deleted)
    - GenericKnife: new knifeCutProgress NBT for cutting locks
    - StruggleAccessory: accessoryStruggleResistance NBT replaces lock resistance
    - PacketV2StruggleStart: uses config-based padlock resistance
    - All lock/unlock packets cleaned of initializeLockResistance/clearLockResistance

E7: Fixed 3 pre-existing bugs:
    - B2: DataDrivenItemRegistry.clear() synchronized on RELOAD_LOCK
    - B3: V2TyingPlayerTask validates heldStack before equip (prevents duplication)
    - B5: EntityKidnapperMerchant.remove() cleans playerToMerchant map (memory leak)
2026-04-15 03:23:49 +02:00
NotEvil
199bf00aef feat(D-01/E): BuiltInLockComponent for organic items (E2)
- New BuiltInLockComponent: blocksUnequip() returns true (permanent lock)
- ComponentType: add BUILT_IN_LOCK enum value
- 8 organic item JSONs updated (slime, vine, web, tape variants)
- DataDrivenBondageItem: add hasBuiltInLock() static helper
2026-04-15 03:11:15 +02:00
4a3ff438c2 Merge pull request 'feature/d01-branch-d-cleanup' (#9) from feature/d01-branch-d-cleanup into develop
Reviewed-on: #9
2026-04-15 00:54:11 +00:00
NotEvil
4d128124e6 chore: gitignore docs/ to stop them from being tracked 2026-04-15 02:52:37 +02:00
NotEvil
3df979ceee fix(D-01/D): checkup cleanup — 5 issues resolved
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
2026-04-15 02:52:27 +02:00
NotEvil
a572513640 chore(D-01/D): remove dead V1 handleSelfAccessory method 2026-04-15 02:12:05 +02:00
NotEvil
dfa7024e21 fix(D-01/D): blindfold ID mismatch + dispenser V2 registration (review)
KidnapperTheme: fix "mask_blindfold" → "blindfold_mask" across 8 themes.
The incorrect ID produced ghost items with no definition.

DispenserBehaviors: register GenericBondageDispenseBehavior.forAnyDataDriven()
for the V2 DATA_DRIVEN_ITEM singleton. Dispatches by region from the stack's
definition. V1 per-variant registrations were deleted but V2 replacement
was missing.
2026-04-15 02:10:25 +02:00
NotEvil
9302b6ccaf chore: remove docs from branch D tracking 2026-04-15 01:59:16 +02:00
NotEvil
099cd0d984 feat(D-01/D): V1 cleanup — delete 28 files, ~5400 lines removed
D1: ThreadLocal alert suppression moved from ItemCollar to CollarHelper.
    onCollarRemoved() logic (kidnapper alert) moved to CollarHelper.

D2+D3: Deleted 17 V1 item classes + 4 V1-only interfaces:
  ItemBind, ItemGag, ItemBlindfold, ItemCollar, ItemEarplugs, ItemMittens,
  ItemColor, ItemClassicCollar, ItemShockCollar, ItemShockCollarAuto,
  ItemGpsCollar, ItemChokeCollar, ItemHood, ItemMedicalGag,
  IBondageItem, IHasGaggingEffect, IHasBlindingEffect, IAdjustable

D4: KidnapperTheme/KidnapperItemSelector/DispenserBehaviors migrated
    from variant enums to string-based DataDrivenItemRegistry IDs.

D5: Deleted 11 variant enums + Generic* factories + ItemBallGag3D:
  BindVariant, GagVariant, BlindfoldVariant, EarplugsVariant, MittensVariant,
  GenericBind, GenericGag, GenericBlindfold, GenericEarplugs, GenericMittens

D6: ModItems cleaned — all V1 bondage registrations removed.
D7: ModCreativeTabs rewritten — iterates DataDrivenItemRegistry.
D8+D9: All V2 helpers cleaned (V1 fallbacks removed), orphan imports removed.

Zero V1 bondage code references remain (only Javadoc comments).
All bondage items are now data-driven via 47 JSON definitions.
2026-04-15 01:55:16 +02:00
fccb99ef9a Merge pull request 'feature/d01-branch-c-migration' (#8) from feature/d01-branch-c-migration into develop
Reviewed-on: #8
2026-04-14 22:57:05 +00:00
NotEvil
b04497b5a1 chore: remove docs from branch C (should not be tracked here) 2026-04-15 00:55:02 +02:00
NotEvil
3515c89f82 fix(D-01/C): missing sync + worldgen empty registry race (review)
StruggleSessionManager: add V2EquipmentHelper.sync(player) after bind
resistance update to prevent data loss on server restart during struggle

HangingCagePiece: add fallback ResourceLocation arrays for worldgen when
DataDrivenItemRegistry is empty (race with reload listener on initial
world creation). Registry-first with hardcoded fallbacks.
2026-04-15 00:26:07 +02:00
NotEvil
3d61c9e9e6 feat(D-01/C): consumer migration — 85 files migrated to V2 helpers
Phase 1 (state): PlayerBindState, PlayerCaptorManager, PlayerEquipment,
  PlayerDataRetrieval, PlayerLifecycle, PlayerShockCollar, StruggleAccessory
Phase 2 (client): AnimationTickHandler, NpcAnimationTickHandler, 5 render
  handlers, DamselModel, 3 client mixins, SelfBondageInputHandler,
  SlaveManagementScreen, ActionPanel, SlaveEntryWidget, ModKeybindings
Phase 3 (entities): 28 entity/AI files migrated to CollarHelper,
  BindModeHelper, PoseTypeHelper, createStack()
Phase 4 (network): PacketSlaveAction, PacketMasterEquip,
  PacketAssignCellToCollar, PacketNpcCommand, PacketFurnitureForcemount
Phase 5 (events): RestraintTaskTickHandler, PetPlayRestrictionHandler,
  PlayerEnslavementHandler, ChatEventHandler, LaborAttackPunishmentHandler
Phase 6 (commands): BondageSubCommand, CollarCommand, NPCCommand,
  KidnapSetCommand
Phase 7 (compat): MCAKidnappedAdapter, MCA mixins
Phase 8 (misc): GagTalkManager, PetRequestManager, HangingCagePiece,
  BondageItemBlockEntity, TrappedChestBlockEntity, DispenserBehaviors,
  BondageItemLoaderUtility, RestraintApplicator, StruggleSessionManager,
  MovementStyleResolver, CampLifecycleManager

Some files retain dual V1/V2 checks (instanceof V1 || V2Helper) for
coexistence — V1-only branches removed in Branch D.
2026-04-15 00:16:50 +02:00
52d1044e9a Merge pull request 'feature/d01-branch-b-definitions' (#7) from feature/d01-branch-b-definitions into develop
Reviewed-on: #7
2026-04-14 20:23:53 +00:00
NotEvil
530b86a9a7 fix(D-01/B): hood missing MOUTH block + organic items lockable:false
- Hood: add MOUTH to blocked_regions — prevents double gag stacking
- 8 organic items (slime, vine, web, tape): set lockable:false at top
  level for consistency with can_attach_padlock:false
2026-04-14 17:59:27 +02:00
NotEvil
258223bf68 feat(D-01/B): 47 data-driven item definitions + cleanup test files
16 binds: ropes, armbinder, dogbinder, chain, ribbon, slime, vine_seed,
  web_bind, shibari, leather_straps, medical_straps, beam_cuffs,
  duct_tape, straitjacket, wrap, latex_sack

19 gags: cloth_gag, ropes_gag, cleave_gag, ribbon_gag, ball_gag,
  ball_gag_strap, tape_gag, wrap_gag, slime_gag, vine_gag, web_gag,
  panel_gag, beam_panel_gag, chain_panel_gag, latex_gag, tube_gag,
  bite_gag, sponge_gag, baguette_gag

2 blindfolds, 1 earplugs, 1 mittens
5 collars (classic, shock, shock_auto, gps, choke) with ownership component
3 combos (hood, medical_gag, ball_gag_3d)

All items config-driven via ResistanceComponent (id) and GaggingComponent
(material). Organic items have can_attach_padlock: false.

Removed 4 test files (test_component_gag, test_handcuffs, test_leg_cuffs).
2026-04-14 17:52:19 +02:00
NotEvil
679d7033f9 feat(D-01/B): add can_attach_padlock field to data-driven items (B0)
- DataDrivenItemDefinition: add canAttachPadlock boolean (default true)
- DataDrivenItemParser: parse "can_attach_padlock" from JSON
- DataDrivenBondageItem: add static canAttachPadlockTo(stack) method
- AnvilEventHandler: check V2 definition before allowing padlock attach
2026-04-14 17:47:32 +02:00
8bfd97ba57 Merge pull request 'feature/d01-branch-a-bridge' (#6) from feature/d01-branch-a-bridge into develop
Reviewed-on: #6
2026-04-14 15:19:20 +00:00
NotEvil
df56ebb6bc fix(D-01/A): adversarial review fixes — 4 logic bugs
1. NECK region explicitly blocked in interactLivingEntity() — prevents
   V2 collars equipping without ownership setup (latent, no JSONs yet)

2. Swap rollback safety: if re-equip fails after swap failure, drop the
   old bind as item entity instead of losing it silently

3. GaggingComponent: cache GagMaterial at construction time — eliminates
   valueOf() log spam on every chat message with misconfigured material

4. Dual-bind prevention: check both V1 isTiedUp() AND V2 region occupied
   in TyingInteractionHelper and PacketSelfBondage to prevent equipping
   V2 bind on top of V1 bind
2026-04-14 16:48:50 +02:00
NotEvil
b97bdf367e fix(D-01/A): V2 bind/collar resistance completely broken (CRITICAL)
PlayerEquipment.getCurrentBindResistance/setCurrentBindResistance and
getCurrentCollarResistance/setCurrentCollarResistance all checked
instanceof ItemBind/ItemCollar — V2 DataDrivenBondageItem silently
returned 0, making V2 items escapable in 1 struggle roll.

Fix: use instanceof IHasResistance which both V1 and V2 implement.

Also fix StruggleCollar.tighten() to read ResistanceComponent directly
for V2 collars instead of IHasResistance.getBaseResistance(entity)
which triggers the singleton MAX-scan across all equipped items.

Note: isItemLocked() dead code in StruggleState is a PRE-EXISTING bug
(x10 locked penalty never applied) — tracked for separate fix.
2026-04-14 16:44:59 +02:00
NotEvil
eb7f06bfc8 fix(D-01/A): 3 review bugs + null guards (BUG-001, BUG-002, BUG-003, RISK-003)
BUG-001: TyingInteractionHelper swap now checks V2EquipResult — on failure,
rolls back by re-equipping the old bind instead of leaving target untied

BUG-002: OwnershipComponent.onUnequipped no longer double-calls
unregisterWearer — onCollarRemoved already handles it. Suppressed-alert
path calls unregister directly since onCollarRemoved is skipped.

BUG-003: PacketSelfBondage handleV2SelfBind now clears completed tying
task from PlayerBindState to prevent blocking future tying interactions

RISK-003: StruggleCollar get/setResistanceState null-guard on player
2026-04-14 16:38:09 +02:00
NotEvil
5c4e4c2352 fix(D-01/A): double item consumption + unchecked cast in TyingInteractionHelper
QA-001: add instanceof V2TyingPlayerTask guard before cast to prevent
ClassCastException when a V1 TyingPlayerTask was still active

QA-002: remove stack.shrink(1) after tying completion — V2TyingPlayerTask
.onComplete() already consumes the held item via heldStack.shrink(1)
2026-04-14 16:35:05 +02:00
NotEvil
eee4825aba feat(D-01/A): NPC speed reduction for V2 items (A12)
- onEquipped: apply RestraintEffectUtils speed reduction for non-Player
  entities with ARMS region and legs bound. Full immobilization for
  WRAP/LATEX_SACK pose types.
- onUnequipped: remove speed reduction for non-Player entities
- Players use MovementStyleManager (V2 tick-based), not this legacy path
2026-04-14 16:07:41 +02:00
NotEvil
b359c6be35 feat(D-01/A): self-bondage region routing (A11)
- handleV2SelfBondage: split into region-based routing
  - NECK → blocked (cannot self-collar)
  - ARMS → handleV2SelfBind (tying task with progress bar)
  - Other → handleV2SelfAccessory (instant equip)
- handleV2SelfAccessory: arms-bound check via BindModeHelper,
  locked check for swap, V2EquipmentHelper for conflict resolution
2026-04-14 16:06:01 +02:00
NotEvil
19cc69985d feat(D-01/A): V2-aware struggle system (A9)
StruggleBinds:
- canStruggle(): instanceof ItemBind → BindModeHelper.isBindItem()
- isItemLocked(): instanceof ItemBind → instanceof ILockable (fixes R4)
- onAttempt(): instanceof ItemShockCollar → CollarHelper.canShock() (fixes R5)
- tighten(): reads ResistanceComponent directly for V2, avoids MAX scan bug

StruggleCollar:
- getResistanceState/setResistanceState: instanceof ItemCollar → IHasResistance
- canStruggle(): instanceof ItemCollar → CollarHelper.isCollar() + ILockable
- onAttempt(): shock check via CollarHelper.canShock()
- successAction(): unlock via ILockable
- tighten(): resistance via IHasResistance

All V1 items continue working through the same interfaces they already implement.
2026-04-14 15:47:20 +02:00
NotEvil
737a4fd59b feat(D-01/A): interaction routing + TyingInteractionHelper (A8)
- DataDrivenBondageItem.use(): shift+click cycles bind mode for ARMS items
- DataDrivenBondageItem.interactLivingEntity(): region-based routing
  - ARMS → TyingInteractionHelper (tying task with progress bar)
  - NECK → deferred to Branch C (no V2 collar JSONs yet)
  - Other regions → instant equip via parent AbstractV2BondageItem
- TyingInteractionHelper: extracted tying flow using V2TyingPlayerTask
  - Distance/LoS validation, swap if already tied, task lifecycle
2026-04-14 15:35:31 +02:00
NotEvil
b79225d684 feat(D-01/A): OwnershipComponent lifecycle hooks (A7)
- onEquipped: register collar owners in CollarRegistry (server-side only)
- onUnequipped: alert kidnappers + unregister from CollarRegistry
- Guards: client-side check, ServerLevel cast, empty owners skip, try-catch
- appendTooltip: nickname, owner count, shock/GPS/choke capabilities
- Delegates alert suppression to ItemCollar.isRemovalAlertSuppressed()
2026-04-14 15:29:31 +02:00
NotEvil
751bad418d feat(D-01/A): poseType, helpers, OWNERSHIP ComponentType (A4, A5, A6)
- DataDrivenItemDefinition: add poseType field, parsed from JSON "pose_type"
- PoseTypeHelper: resolves PoseType from V2 definition or V1 ItemBind fallback
- BindModeHelper: static bind mode NBT utilities (isBindItem, hasArmsBound,
  hasLegsBound, cycleBindModeId) — works for V1 and V2 items
- CollarHelper: complete static utility class for collar operations with
  dual-path V2/V1 dispatch (ownership, features, shock, GPS, choke, alert)
- ComponentType: add OWNERSHIP enum value
- OwnershipComponent: stub class (lifecycle hooks added in next commit)
2026-04-14 15:23:08 +02:00
NotEvil
b81d3eed95 feat(D-01/A): config-driven components + tooltip hook (A1, A2, A3)
- ResistanceComponent: resistanceId delegates to SettingsAccessor at runtime,
  fallback to hardcoded base for backward compat
- GaggingComponent: material field delegates to GagMaterial enum from ModConfig,
  explicit comprehension/range overrides take priority
- IItemComponent: add default appendTooltip() method
- ComponentHolder: iterate components for tooltip contribution
- 6 components implement appendTooltip (lockable, resistance, gagging, shock,
  gps, choking)
- DataDrivenBondageItem: call holder.appendTooltip() in appendHoverText()
2026-04-14 15:05:48 +02:00
fa4c332a10 Merge pull request 'feature/d01-component-system' (#5) from feature/d01-component-system into develop
Reviewed-on: #5
2026-04-14 00:54:16 +00:00
NotEvil
7bd840705a docs: add components section to ARTIST_GUIDE.md
Documents the 8 available gameplay components (lockable, resistance,
gagging, blinding, shock, gps, choking, adjustable) with config fields,
examples (GPS shock collar, adjustable blindfold), and usage tips.
2026-04-14 02:51:37 +02:00
NotEvil
90bc890b95 fix(D-01): synchronize reload paths and capture snapshot locally (RISK-001, RISK-002) 2026-04-14 02:46:09 +02:00
NotEvil
185ac63a44 feat(D-01): implement 5 remaining components (blinding, shock, gps, choking, adjustable) 2026-04-14 02:37:14 +02:00
NotEvil
dcc8493e5e fix(D-01): pre-built map for O(1) ComponentType.fromKey() lookup (RISK-005)
Replace linear values() scan with a static unmodifiable HashMap lookup.
While only 3 entries currently exist, this establishes the correct
pattern for when more component types are added.
2026-04-14 02:29:46 +02:00
NotEvil
bfcc20d242 fix(D-01): compact constructor defaults null componentConfigs to empty (RISK-004)
Add compact constructor to DataDrivenItemDefinition that defaults null
componentConfigs to Map.of(). This makes the field guaranteed non-null,
allowing removal of null checks in hasComponent() and
DataDrivenItemRegistry.buildComponentHolders().
2026-04-14 02:29:25 +02:00
NotEvil
3a81bb6e12 fix(D-01): clamp component config values to valid ranges (RISK-003)
- LockableComponent: lock_resistance clamped to >= 0
- ResistanceComponent: base resistance clamped to >= 0
- GaggingComponent: comprehension clamped to [0.0, 1.0], range to >= 0.0

Prevents nonsensical negative values from malformed JSON configs.
2026-04-14 02:28:42 +02:00
NotEvil
bb589d44f8 fix(D-01): warn on non-object component config, deep-copy configs (RISK-001, RISK-002)
- Deep-copy JsonObject configs via deepCopy() before storing in the
  definition to prevent external mutation of the parsed JSON tree
- Log a warning when a component config value is not a JsonObject,
  making misconfigured JSON easier to diagnose
2026-04-14 02:27:59 +02:00
NotEvil
456335e0dd fix(D-01): wire LockableComponent.lockResistance via getItemLockResistance() (BUG-003)
- Remove redundant blocksUnequip() from LockableComponent since
  AbstractV2BondageItem.canUnequip() already checks ILockable.isLocked()
- Add DataDrivenBondageItem.getItemLockResistance(ItemStack) that reads
  the per-item lock resistance from the LockableComponent, falling back
  to the global config value when absent
2026-04-14 02:27:37 +02:00