Compare commits
34 Commits
chore/audi
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 37da2c1716 | |||
|
|
06018e6c9b | ||
|
|
b47f642532 | ||
| cc8adfe015 | |||
|
|
f37600783a | ||
|
|
168c0675bb | ||
|
|
a3287b7db8 | ||
|
|
e56e6dd551 | ||
|
|
806a1e732d | ||
|
|
3d57d83a5b | ||
|
|
229fc66340 | ||
|
|
b0766fecc6 | ||
|
|
9dfd2d1724 | ||
|
|
3f6e04edb0 | ||
|
|
ca4cbcad12 | ||
|
|
17269f51f8 | ||
|
|
c0c53f9504 | ||
|
|
eb759fefff | ||
|
|
8af58b5dd5 | ||
|
|
d7f8bf6c72 | ||
|
|
6dad447c05 | ||
|
|
7ef85b4e92 | ||
|
|
ad74d320be | ||
| 5788f39d9f | |||
|
|
27c86bc831 | ||
|
|
f4aa5ffdc5 | ||
| ea14fc2cec | |||
|
|
4e136cff96 | ||
| 683eeec11f | |||
|
|
fd60086322 | ||
|
|
9b2c5dec8e | ||
|
|
371a138b71 | ||
|
|
d75b74f9f9 | ||
| bce0598059 |
@@ -104,6 +104,7 @@ minecraft {
|
||||
|
||||
// Mixin config arg
|
||||
args '-mixin.config=tiedup.mixins.json'
|
||||
args '-mixin.config=tiedup-compat.mixins.json'
|
||||
}
|
||||
|
||||
server {
|
||||
@@ -116,6 +117,7 @@ minecraft {
|
||||
|
||||
// Mixin config arg
|
||||
args '-mixin.config=tiedup.mixins.json'
|
||||
args '-mixin.config=tiedup-compat.mixins.json'
|
||||
}
|
||||
|
||||
// Additional client instances for multiplayer testing
|
||||
@@ -155,6 +157,7 @@ sourceSets.main.resources { srcDir 'src/generated/resources' }
|
||||
mixin {
|
||||
add sourceSets.main, 'tiedup.refmap.json'
|
||||
config 'tiedup.mixins.json'
|
||||
config 'tiedup-compat.mixins.json'
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -263,7 +266,7 @@ tasks.named('jar', Jar).configure {
|
||||
'Implementation-Version' : project.jar.archiveVersion,
|
||||
'Implementation-Vendor' : mod_authors,
|
||||
'Implementation-Timestamp': new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"),
|
||||
'MixinConfigs' : 'tiedup.mixins.json'
|
||||
'MixinConfigs' : 'tiedup.mixins.json,tiedup-compat.mixins.json'
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -96,16 +96,17 @@ PlayerArmature ← armature root object (never keyframe this)
|
||||
**Never animate:** `PlayerArmature` — it's the armature root object, not a bone.
|
||||
|
||||
**Everything else follows this rule:**
|
||||
- Your item **always** controls bones in its declared regions.
|
||||
- Your item **can also** animate free bones (not owned by any other equipped item).
|
||||
- **Standard animations** (`Idle`, `Struggle`, `Walk`): your item controls ONLY bones in its declared regions. Keyframes on other bones are ignored.
|
||||
- **`Full` animations** (`FullIdle`, `FullStruggle`, `FullWalk`): your item also controls free bones (body, legs — not owned by another item). See [Full-Body Animations](#full-body-animations-naming-convention).
|
||||
- **Head is protected by default**: vanilla head tracking is preserved unless your item owns a head region (HEAD, EYES, EARS, MOUTH). In `Full` animations, head stays protected. Use `FullHead` prefix (e.g., `FullHeadStruggle`) to explicitly opt into head animation as a free bone.
|
||||
- Your item **cannot** override bones owned by another equipped item.
|
||||
|
||||
| Bone | Who Controls It |
|
||||
|------|----------------|
|
||||
| `body` / `torso` | Context layer by default. Your item if it owns TORSO or WAIST, or if `body` is free. |
|
||||
| `head` | Vanilla head tracking by default. Your item if it owns HEAD, EYES, EARS, or MOUTH. |
|
||||
| Arms (`*UpperArm`, `*LowerArm`) | Vanilla by default. Your item if it owns ARMS or HANDS. |
|
||||
| Legs (`*UpperLeg`, `*LowerLeg`) | Context layer by default. Your item if it owns LEGS or FEET. |
|
||||
| `body` / `torso` | Context layer by default. Your item if it owns TORSO or WAIST. Also available as a free bone in `Full` animations. |
|
||||
| `head` | **Vanilla head tracking by default.** Your item if it owns HEAD, EYES, EARS, or MOUTH. Available as a free bone ONLY in `FullHead` animations (e.g., `FullHeadStruggle`). |
|
||||
| Arms (`*UpperArm`, `*LowerArm`) | Vanilla by default. Your item if it owns ARMS or HANDS. Also available as free bones in `Full` animations. |
|
||||
| Legs (`*UpperLeg`, `*LowerLeg`) | Context layer by default. Your item if it owns LEGS or FEET. Also available as free bones in `Full` animations. |
|
||||
|
||||
**Note:** `torso` and `body` both map to the same internal part. Prefer animating `body` — using `torso` produces the same result but is less intuitive.
|
||||
|
||||
@@ -160,7 +161,7 @@ Three regions are **global** — they encompass sub-regions:
|
||||
|
||||
Use the **TiedUp! Blender Template** (provided with the mod). It contains:
|
||||
- The correct `PlayerArmature` skeleton with all 11 bones
|
||||
- A reference player mesh (Steve + Alex) for scale — toggle visibility as needed
|
||||
- A reference player mesh (Slime model) for scale — toggle visibility as needed
|
||||
- Pre-named Action slots
|
||||
|
||||
### Guidelines
|
||||
@@ -183,7 +184,8 @@ Weight paint your mesh to the skeleton bones it should follow.
|
||||
|
||||
### Rules
|
||||
|
||||
- **Only paint to the 11 standard bones.** Any other bones in your Blender file will be ignored by the mod.
|
||||
- **Paint to the 11 standard bones** for parts that should follow the player's body.
|
||||
- **You can also use custom bones** for decorative elements like chains, ribbons, pendants, or twist bones. Custom bones follow their parent in the bone hierarchy (rest pose) — they won't animate independently, but they move with the body part they're attached to. This is useful for better weight painting and mesh deformation.
|
||||
- **Paint to the bones of your regions.** Handcuffs (ARMS region) should be weighted to `rightUpperArm`, `rightLowerArm`, `leftUpperArm`, `leftLowerArm`.
|
||||
- **You can paint to bones outside your regions** for smooth deformation. For example, handcuffs might have small weights on `body` near the shoulder area for smoother bending. This is fine — the weight painting is about mesh deformation, not animation control.
|
||||
- **Normalize your weights.** Each vertex's total weights across all bones must sum to 1.0. Blender does this by default.
|
||||
@@ -193,6 +195,7 @@ Weight paint your mesh to the skeleton bones it should follow.
|
||||
- For rigid items (metal cuffs), use hard weights — each vertex fully assigned to one bone.
|
||||
- For flexible items (rope, leather), blend weights between adjacent bones for smooth bending.
|
||||
- The chain between handcuffs? Weight it 50/50 to both arms, or use a separate mesh element weighted to `body`.
|
||||
- Custom bones are great for chains, dangling locks, or decorative straps — add a bone parented to a standard bone, weight your mesh to it, and it'll follow the parent's movement automatically.
|
||||
|
||||
---
|
||||
|
||||
@@ -258,9 +261,6 @@ Declare default colors per channel in your item JSON:
|
||||
"tintable_1": "#FF0000",
|
||||
"tintable_2": "#C0C0C0"
|
||||
},
|
||||
"animation_bones": {
|
||||
"idle": []
|
||||
},
|
||||
"pose_priority": 10,
|
||||
"escape_difficulty": 3
|
||||
}
|
||||
@@ -309,19 +309,19 @@ The `PlayerArmature|` prefix is Blender's convention for armature-scoped actions
|
||||
| Gag (MOUTH) | *(none)* | No pose change — mesh only |
|
||||
| Straitjacket (ARMS+TORSO) | Arms + body (+ legs if free) | Arms crossed, slight forward lean, optional waddle |
|
||||
|
||||
**Why only your bones?** The mod's 2-layer system activates your keyframes for bones in your declared regions. But there's a nuance: **free bones** (bones not owned by any equipped item) can also be animated by your item.
|
||||
**Why only your bones?** In standard animations (`Idle`, `Struggle`), the mod only uses keyframes for bones in your declared regions. Keyframes on other bones are ignored — safe to leave them in your GLB.
|
||||
|
||||
For example: if a player wears only a straitjacket (ARMS+TORSO), the legs are "free" — no item claims them. If your straitjacket's GLB has leg keyframes (e.g., a waddle walk), the mod will use them. But if the player also wears ankle cuffs (LEGS), those leg keyframes are ignored — the ankle cuffs take over.
|
||||
To animate free bones (body, legs not owned by another item), use the `Full` prefix — see [Full-Body Animations](#full-body-animations-naming-convention). For example, a straitjacket's `FullWalk` can animate legs for a waddle, but only if no ankle cuffs are equipped.
|
||||
|
||||
**The rule:** Your item always controls its own bones. It can also animate free bones if your GLB has keyframes for them. It can never override another item's bones.
|
||||
**The rule:** Standard animations = owned bones only. `Full` animations = owned + free bones. `FullHead` animations = owned + free + head. Your item can never override another item's bones.
|
||||
|
||||
### Idle is a Single-Frame Pose
|
||||
### Animation Frames
|
||||
|
||||
`Idle` should be a **static pose** — one keyframe at frame 0. The mod loops it as a held position.
|
||||
**Frame 0 is the base pose** — the minimum every animation must have. The mod always has a valid pose to display from frame 0.
|
||||
|
||||
```
|
||||
Frame 0: Pose all owned bones → done.
|
||||
```
|
||||
**Multi-frame animations** (loops, transitions) are supported by the GLB parser — all frames are parsed and stored. However, **multi-frame playback is not yet implemented** in the item animation converter (`GltfPoseConverter`). Currently only frame 0 is read at runtime. Multi-frame playback is a high-priority feature — see `docs/TODO.md`.
|
||||
|
||||
**What this means for artists now:** Design your animations with full multi-frame loops (struggle thrashing, walk cycles, breathing idles). Put the most representative pose at frame 0. When multi-frame playback ships, your animations will work immediately without re-exporting.
|
||||
|
||||
### Optional Animations
|
||||
|
||||
@@ -329,13 +329,13 @@ Beyond `Idle`, you can provide animations for specific contexts. All are optiona
|
||||
|
||||
| Animation Name | Context | Notes |
|
||||
|----------------|---------|-------|
|
||||
| `Idle` | Standing still | **Required.** Single-frame pose. |
|
||||
| `Struggle` | Player is struggling | Multi-frame loop. 20-40 frames recommended. |
|
||||
| `Idle` | Standing still | **Required.** Single-frame or looped. Frame 0 = base pose. |
|
||||
| `Struggle` | Player is struggling | Multi-frame loop recommended. 20-40 frames. |
|
||||
| `Walk` | Player is walking | Multi-frame loop synced to walk speed. |
|
||||
| `Sneak` | Player is sneaking | Single-frame or short loop. |
|
||||
| `SitIdle` | Sitting (chair, minecart) | Single-frame pose. |
|
||||
| `SitIdle` | Sitting (chair, minecart) | Single-frame or looped. |
|
||||
| `SitStruggle` | Sitting + struggling | Multi-frame loop. |
|
||||
| `KneelIdle` | Kneeling | Single-frame pose. |
|
||||
| `KneelIdle` | Kneeling | Single-frame or looped. |
|
||||
| `KneelStruggle` | Kneeling + struggling | Multi-frame loop. |
|
||||
| `Crawl` | Crawling (dog pose) | Multi-frame loop. |
|
||||
|
||||
@@ -350,21 +350,27 @@ If an animation doesn't exist in your GLB, the mod looks for alternatives. At ea
|
||||
|
||||
```
|
||||
SIT + STRUGGLE:
|
||||
FullSitStruggle → SitStruggle → FullStruggle → Struggle
|
||||
→ FullSit → Sit → FullStruggle → Struggle → FullIdle → Idle
|
||||
FullHeadSitStruggle → FullSitStruggle → SitStruggle
|
||||
→ FullHeadStruggle → FullStruggle → Struggle
|
||||
→ FullHeadSit → FullSit → Sit
|
||||
→ FullHeadIdle → FullIdle → Idle
|
||||
|
||||
KNEEL + STRUGGLE:
|
||||
FullKneelStruggle → KneelStruggle → FullStruggle → Struggle
|
||||
→ FullKneel → Kneel → FullStruggle → Struggle → FullIdle → Idle
|
||||
FullHeadKneelStruggle → FullKneelStruggle → KneelStruggle
|
||||
→ FullHeadStruggle → FullStruggle → Struggle
|
||||
→ FullHeadKneel → FullKneel → Kneel
|
||||
→ FullHeadIdle → FullIdle → Idle
|
||||
|
||||
SIT + IDLE: FullSitIdle → SitIdle → FullSit → Sit → FullIdle → Idle
|
||||
KNEEL + IDLE: FullKneelIdle → KneelIdle → FullKneel → Kneel → FullIdle → Idle
|
||||
SNEAK: FullSneak → Sneak → FullIdle → Idle
|
||||
WALK: FullWalk → Walk → FullIdle → Idle
|
||||
STAND STRUGGLE: FullStruggle → Struggle → FullIdle → Idle
|
||||
STAND IDLE: FullIdle → Idle
|
||||
SIT + IDLE: FullHeadSitIdle → FullSitIdle → SitIdle → ... → FullHeadIdle → FullIdle → Idle
|
||||
KNEEL + IDLE: FullHeadKneelIdle → FullKneelIdle → KneelIdle → ... → FullHeadIdle → FullIdle → Idle
|
||||
SNEAK: FullHeadSneak → FullSneak → Sneak → FullHeadIdle → FullIdle → Idle
|
||||
WALK: FullHeadWalk → FullWalk → Walk → FullHeadIdle → FullIdle → Idle
|
||||
STAND STRUGGLE: FullHeadStruggle → FullStruggle → Struggle → FullHeadIdle → FullIdle → Idle
|
||||
STAND IDLE: FullHeadIdle → FullIdle → Idle
|
||||
```
|
||||
|
||||
At each step, `FullHead` is tried first (full body + head), then `Full` (full body, head preserved), then standard (owned bones only).
|
||||
|
||||
In practice, most items only need `Idle`. Add `FullWalk` or `FullStruggle` when your item changes how the whole body moves.
|
||||
|
||||
**Practical impact:** If you only provide `Idle`, your item works in every context. The player will hold the Idle pose while sitting, kneeling, sneaking, etc. It won't look perfect, but it will work. Add more animations over time to polish the experience.
|
||||
@@ -394,18 +400,19 @@ Some items affect the entire body — not just their declared regions. A straitj
|
||||
|
||||
**Convention:** Prefix the animation name with `Full`.
|
||||
|
||||
| Standard Name | Full-Body Name | What Changes |
|
||||
|--------------|---------------|-------------|
|
||||
| `Idle` | `FullIdle` | Owned bones + body lean, leg stance |
|
||||
| `Walk` | `FullWalk` | Owned bones + leg waddle, body sway |
|
||||
| `Struggle` | `FullStruggle` | Owned bones + full-body thrashing |
|
||||
| `Sneak` | `FullSneak` | Owned bones + custom sneak posture |
|
||||
| Standard Name | Full-Body Name | Full + Head | What Changes |
|
||||
|--------------|---------------|-------------|-------------|
|
||||
| `Idle` | `FullIdle` | `FullHeadIdle` | Owned bones + body lean, leg stance (+ head if Head variant) |
|
||||
| `Walk` | `FullWalk` | `FullHeadWalk` | Owned bones + leg waddle, body sway (+ head if Head variant) |
|
||||
| `Struggle` | `FullStruggle` | `FullHeadStruggle` | Owned bones + full-body thrashing (+ head if Head variant) |
|
||||
| `Sneak` | `FullSneak` | `FullHeadSneak` | Owned bones + custom sneak posture (+ head if Head variant) |
|
||||
|
||||
**In Blender:**
|
||||
```
|
||||
PlayerArmature|Idle ← region-only: just arms for handcuffs
|
||||
PlayerArmature|FullWalk ← full-body: arms + legs waddle + body bob
|
||||
PlayerArmature|FullStruggle ← full-body: everything moves
|
||||
PlayerArmature|Idle ← region-only: just arms for handcuffs
|
||||
PlayerArmature|FullWalk ← full-body: arms + legs waddle + body bob
|
||||
PlayerArmature|FullStruggle ← full-body: body + legs thrash (head free)
|
||||
PlayerArmature|FullHeadStruggle ← full-body + head: everything moves including head
|
||||
```
|
||||
|
||||
**How the mod resolves this:**
|
||||
@@ -423,7 +430,9 @@ PlayerArmature|FullStruggle ← full-body: everything moves
|
||||
| `Sneak` | Default sneak lean is fine | Your item changes how sneaking looks |
|
||||
|
||||
**Key points:**
|
||||
- `Full` animations include keyframes for ALL bones you want to control (owned + free).
|
||||
- **Standard animations** (`Idle`, `Struggle`, `Walk`) only animate your item's **owned bones** (from `regions`). Any keyframes on other bones are ignored. This is safe — you can keyframe everything in Blender without worrying about side effects.
|
||||
- **`Full` animations** (`FullIdle`, `FullStruggle`, `FullWalk`) additionally enable **free bones** (body, legs — bones not owned by any other equipped item). This is how you create waddle walks, full-body struggles, etc.
|
||||
- **Head is protected by default.** In `Full` animations, the `head` bone is NOT enabled as a free bone — vanilla head tracking (mouse look) is preserved. To animate the head in a Full animation, add `Head` to the animation name: `FullHeadStruggle`, `FullHeadIdle`, etc. Items that own a head region (HEAD, EYES, EARS, MOUTH) always control head regardless of naming.
|
||||
- Free bones in `Full` animations are only used when no other item owns them.
|
||||
- You can provide BOTH: `Idle` (region-only) and `FullWalk` (full-body). The mod picks the right one per context.
|
||||
- `FullIdle` is rarely needed — most items only need a full-body version for movement animations.
|
||||
@@ -630,7 +639,8 @@ In your JSON definition, separate the mesh from the animations:
|
||||
- [ ] Actions are named `PlayerArmature|Idle`, `PlayerArmature|Struggle`, etc.
|
||||
- [ ] Mesh is weight-painted to skeleton bones only
|
||||
- [ ] Weights are normalized
|
||||
- [ ] No orphan bones (extra bones not in the standard 11 are ignored but add file size)
|
||||
- [ ] Custom bones (if any) are parented to a standard bone in the hierarchy
|
||||
- [ ] Your item mesh is named `Item` in Blender (recommended — ensures the mod picks the correct mesh if your file has multiple objects)
|
||||
- [ ] Materials/textures are applied (the GLB bakes them in)
|
||||
- [ ] Scale is correct (1 Blender unit = 1 Minecraft block = 16 pixels)
|
||||
|
||||
@@ -648,9 +658,6 @@ Every item needs a JSON file that declares its gameplay properties. The mod scan
|
||||
"display_name": "Rope Gag",
|
||||
"model": "mycreator:models/gltf/rope_gag.glb",
|
||||
"regions": ["MOUTH"],
|
||||
"animation_bones": {
|
||||
"idle": []
|
||||
},
|
||||
"pose_priority": 10,
|
||||
"escape_difficulty": 2,
|
||||
"lockable": false
|
||||
@@ -759,18 +766,19 @@ The `movement_style` changes how the player physically moves — slower speed, d
|
||||
| `supports_color` | bool | No | Whether this item has tintable zones. Default: `false` |
|
||||
| `tint_channels` | object | No | Default colors per tintable zone: `{"tintable_1": "#FF0000"}` |
|
||||
| `icon` | string | No | Inventory sprite model (see [Inventory Icons](#inventory-icons) below) |
|
||||
| `animations` | string/object | No | `"auto"` (default) or explicit name mapping |
|
||||
| `movement_style` | string | No | Movement restriction: `"waddle"`, `"shuffle"`, `"hop"`, or `"crawl"` |
|
||||
| `movement_modifier` | object | No | Override speed/jump for the movement style (requires `movement_style`) |
|
||||
| `creator` | string | No | Author/creator name, shown in the item tooltip |
|
||||
| `animation_bones` | object | Yes | Per-animation bone whitelist (see below) |
|
||||
| `animation_bones` | object | No | Per-animation bone whitelist (see below). If omitted, all owned bones are enabled for all animations. |
|
||||
| `components` | object | No | Gameplay behavior components (see [Components](#components-gameplay-behaviors) below) |
|
||||
|
||||
### animation_bones (required)
|
||||
### animation_bones (optional)
|
||||
|
||||
Declares which bones each named animation is allowed to control for this item. This enables fine-grained per-animation bone filtering: an item might own `body` via its regions but only want the "idle" animation to affect the arms.
|
||||
Fine-grained control over which bones each animation is allowed to affect. Most items don't need this — if omitted, all owned bones (from your `regions`) are enabled for all animations automatically.
|
||||
|
||||
**Format:** A JSON object where each key is an animation name (matching the GLB animation names) and each value is an array of bone names.
|
||||
**When to use it:** When your item owns bones via its regions but you only want specific animations to affect specific bones. For example, an item owning ARMS + TORSO might only want the "idle" pose to affect the arms, and only the "struggle" animation to also move the body.
|
||||
|
||||
**Format:** A JSON object where each key is an animation name and each value is an array of bone names.
|
||||
|
||||
**Valid bone names:** `head`, `body`, `rightArm`, `leftArm`, `rightLeg`, `leftLeg`
|
||||
|
||||
@@ -784,7 +792,7 @@ Declares which bones each named animation is allowed to control for this item. T
|
||||
|
||||
At runtime, the effective bones for a given animation clip are computed as the **intersection** of `animation_bones[clipName]` and the item's owned parts (from region conflict resolution). If the clip name is not listed in `animation_bones`, the item falls back to using all its owned parts.
|
||||
|
||||
This field is **required**. Items without `animation_bones` will be rejected by the parser.
|
||||
**If omitted entirely:** All owned bones are enabled for all animations. This is the correct default for most items — you only need `animation_bones` for advanced per-animation filtering.
|
||||
|
||||
### Components (Gameplay Behaviors)
|
||||
|
||||
@@ -823,9 +831,6 @@ Items without the `"components"` field work normally — components are entirely
|
||||
"display_name": "GPS Shock Collar",
|
||||
"model": "mycreator:models/gltf/gps_shock_collar.glb",
|
||||
"regions": ["NECK"],
|
||||
"animation_bones": {
|
||||
"idle": []
|
||||
},
|
||||
"pose_priority": 10,
|
||||
"escape_difficulty": 5,
|
||||
"components": {
|
||||
@@ -1014,14 +1019,36 @@ Use your own namespace (e.g., `mycreator`) to avoid conflicts with the base mod
|
||||
|
||||
---
|
||||
|
||||
## Validation & Debugging
|
||||
|
||||
The mod includes built-in tools to help you catch issues with your GLB files.
|
||||
|
||||
### `/tiedup validate` Command
|
||||
|
||||
Run `/tiedup validate` in-game (client-side command) to see a diagnostic report for all loaded GLBs:
|
||||
|
||||
- **RED** errors — item won't work (missing GLB, invalid file, no skin)
|
||||
- **YELLOW** warnings — item works but something looks wrong (bone typo, multiple meshes, no Idle animation)
|
||||
- **GRAY** info — informational (custom bones detected, vertex count)
|
||||
|
||||
Filter by item: `/tiedup validate tiedup:leather_armbinder`
|
||||
|
||||
The validation runs automatically on every resource reload (F3+T). Check your game log for a summary line: `[GltfValidation] Validated N GLBs: X passed, Y with warnings, Z with errors`.
|
||||
|
||||
### Mesh Naming Convention
|
||||
|
||||
If your GLB contains multiple meshes, name your item mesh `Item` in Blender. The mod prioritizes a mesh named `Item` over other meshes. If no `Item` mesh is found, the last non-`Player` mesh is used (backward compatible, but may pick the wrong one in multi-mesh files).
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### Skeleton Issues
|
||||
|
||||
| Mistake | Symptom | Fix |
|
||||
|---------|---------|-----|
|
||||
| Bone name typo (`RightUpperArm` instead of `rightUpperArm`) | Mesh doesn't follow that bone | Names are **camelCase**, not PascalCase. Check exact spelling. |
|
||||
| Extra bones in the armature | No visible issue (ignored), larger file | Delete non-standard bones before export |
|
||||
| Bone name typo (`RightUpperArm` instead of `rightUpperArm`) | WARN in log with suggestion: "did you mean 'rightUpperArm'?" — bone treated as custom | Names are **camelCase**, not PascalCase. Check exact spelling. Run `/tiedup validate` to see warnings. |
|
||||
| Extra bones in the armature | Custom bones follow their parent in rest pose | Intentional custom bones are fine (chains, decorations). Unintentional ones add file size — delete them. |
|
||||
| Missing `PlayerArmature` root | Mesh renders at wrong position | Rename your armature root to `PlayerArmature` |
|
||||
| Animating `body` bone without TORSO region | Body keyframes used only if `body` is free (no other item owns it) | Declare TORSO/WAIST region if you always want to control body, or use `Full` animations for free-bone effects |
|
||||
|
||||
@@ -1029,11 +1056,11 @@ Use your own namespace (e.g., `mycreator`) to avoid conflicts with the base mod
|
||||
|
||||
| Mistake | Symptom | Fix |
|
||||
|---------|---------|-----|
|
||||
| Action not prefixed with `PlayerArmature\|` | Animation not found, falls back to first clip | Rename: `Idle` → `PlayerArmature\|Idle` |
|
||||
| Action not prefixed with `PlayerArmature\|` | Animation not found, falls back to first clip | Rename: `Idle` → `PlayerArmature\|Idle`. Note: the mod strips any `ArmatureName\|` prefix, so custom armature names also work. |
|
||||
| Wrong case (`idle` instead of `Idle`) | Animation not found | Use exact PascalCase: `Idle`, `SitIdle`, `KneelStruggle` |
|
||||
| Variant gap (`.1`, `.2`, `.4` — missing `.3`) | Only .1 and .2 are used | Number sequentially with no gaps |
|
||||
| Animating bones outside your regions | Keyframes silently ignored | Only animate bones in your declared regions |
|
||||
| Multi-frame Idle | Works but wastes resources | Idle should be a single keyframe at frame 0 |
|
||||
| Multi-frame animations play as static pose | Multi-frame playback not yet implemented — only frame 0 is used | Design full animations now (they'll work when playback ships). Ensure frame 0 is a good base pose. |
|
||||
|
||||
### Weight Painting Issues
|
||||
|
||||
@@ -1041,7 +1068,7 @@ Use your own namespace (e.g., `mycreator`) to avoid conflicts with the base mod
|
||||
|---------|---------|-----|
|
||||
| Vertices not weighted to any bone | Part of mesh stays frozen in space | Weight paint everything to at least one bone |
|
||||
| Weights not normalized | Mesh stretches or compresses oddly | Blender > Weights > Normalize All |
|
||||
| Weighted to a non-standard bone | That part of mesh stays frozen | Only weight to the 11 standard bones |
|
||||
| Weighted to a non-standard bone | Mesh follows parent bone in rest pose | This is OK if intentional (custom bones). If not, re-weight to a standard bone. |
|
||||
|
||||
### JSON Issues
|
||||
|
||||
@@ -1073,9 +1100,6 @@ A collar sits on the neck. It doesn't change the player's pose.
|
||||
"display_name": "Leather Collar",
|
||||
"model": "mycreator:models/gltf/leather_collar.glb",
|
||||
"regions": ["NECK"],
|
||||
"animation_bones": {
|
||||
"idle": []
|
||||
},
|
||||
"pose_priority": 5,
|
||||
"escape_difficulty": 3,
|
||||
"lockable": true
|
||||
@@ -1548,11 +1572,16 @@ NEVER DO:
|
||||
|
||||
GOOD TO KNOW:
|
||||
→ Only Idle is required. Everything else has fallbacks.
|
||||
→ animation_bones is optional. Omit it and all owned bones work for all animations.
|
||||
→ Templates let you skip animation entirely.
|
||||
→ Free bones (not owned by any item) CAN be animated by your GLB.
|
||||
→ Custom bones are supported — add chain/ribbon/twist bones parented to standard bones.
|
||||
→ Free bones (body, legs) can be animated using Full-prefixed animations (FullWalk, FullStruggle).
|
||||
→ Head is protected — use FullHead prefix (FullHeadStruggle) to also animate the head.
|
||||
→ Bones owned by another equipped item are always ignored.
|
||||
→ The mod handles sitting, sneaking, walking — you don't have to.
|
||||
→ Context GLBs in tiedup_contexts/ replace default postures.
|
||||
→ Name your item mesh "Item" in Blender for explicit selection in multi-mesh files.
|
||||
→ Run /tiedup validate in-game to check your GLBs for issues.
|
||||
→ Slim model is optional. Steve mesh works on Alex (minor clipping).
|
||||
→ Textures bake into the GLB. No separate file needed.
|
||||
```
|
||||
|
||||
@@ -244,7 +244,7 @@ public class BlockKidnapBomb
|
||||
beTag.contains("collar")
|
||||
) {
|
||||
tooltip.add(
|
||||
Component.literal("Loaded:").withStyle(
|
||||
Component.translatable("block.tiedup.kidnap_bomb.loaded").withStyle(
|
||||
ChatFormatting.YELLOW
|
||||
)
|
||||
);
|
||||
@@ -282,12 +282,12 @@ public class BlockKidnapBomb
|
||||
);
|
||||
} else {
|
||||
tooltip.add(
|
||||
Component.literal("Empty").withStyle(ChatFormatting.GREEN)
|
||||
Component.translatable("block.tiedup.kidnap_bomb.empty").withStyle(ChatFormatting.GREEN)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
tooltip.add(
|
||||
Component.literal("Empty").withStyle(ChatFormatting.GREEN)
|
||||
Component.translatable("block.tiedup.kidnap_bomb.empty").withStyle(ChatFormatting.GREEN)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,7 +328,7 @@ public class BlockRopeTrap extends Block implements EntityBlock, ICanBeLoaded {
|
||||
// Check if armed
|
||||
if (beTag.contains("bind")) {
|
||||
tooltip.add(
|
||||
Component.literal("Armed").withStyle(
|
||||
Component.translatable("block.tiedup.trap.armed").withStyle(
|
||||
ChatFormatting.DARK_RED
|
||||
)
|
||||
);
|
||||
@@ -366,14 +366,14 @@ public class BlockRopeTrap extends Block implements EntityBlock, ICanBeLoaded {
|
||||
);
|
||||
} else {
|
||||
tooltip.add(
|
||||
Component.literal("Disarmed").withStyle(
|
||||
Component.translatable("block.tiedup.trap.disarmed").withStyle(
|
||||
ChatFormatting.GREEN
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
tooltip.add(
|
||||
Component.literal("Disarmed").withStyle(ChatFormatting.GREEN)
|
||||
Component.translatable("block.tiedup.trap.disarmed").withStyle(ChatFormatting.GREEN)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ public class BlockTrappedChest extends ChestBlock implements ICanBeLoaded {
|
||||
beTag.contains("collar")
|
||||
) {
|
||||
tooltip.add(
|
||||
Component.literal("Armed").withStyle(
|
||||
Component.translatable("block.tiedup.trap.armed").withStyle(
|
||||
ChatFormatting.DARK_RED
|
||||
)
|
||||
);
|
||||
@@ -231,14 +231,14 @@ public class BlockTrappedChest extends ChestBlock implements ICanBeLoaded {
|
||||
);
|
||||
} else {
|
||||
tooltip.add(
|
||||
Component.literal("Disarmed").withStyle(
|
||||
Component.translatable("block.tiedup.trap.disarmed").withStyle(
|
||||
ChatFormatting.GREEN
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
tooltip.add(
|
||||
Component.literal("Disarmed").withStyle(ChatFormatting.GREEN)
|
||||
Component.translatable("block.tiedup.trap.disarmed").withStyle(ChatFormatting.GREEN)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,9 +382,8 @@ public class BountyManager extends SavedData {
|
||||
server
|
||||
.getPlayerList()
|
||||
.broadcastSystemMessage(
|
||||
Component.literal("[Bounty] " + message).withStyle(
|
||||
ChatFormatting.GOLD
|
||||
),
|
||||
Component.translatable("msg.tiedup.bounty.broadcast", message)
|
||||
.withStyle(ChatFormatting.GOLD),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ public final class CampLifecycleManager {
|
||||
);
|
||||
} else {
|
||||
// Offline: full escape via PrisonerService (no grace period needed)
|
||||
com.tiedup.remake.prison.service.PrisonerService.get().escape(
|
||||
com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
|
||||
level,
|
||||
prisonerId,
|
||||
"camp death"
|
||||
@@ -250,10 +250,8 @@ public final class CampLifecycleManager {
|
||||
|
||||
// Notify prisoner
|
||||
prisoner.sendSystemMessage(
|
||||
Component.literal("Your captor has died. You are FREE!").withStyle(
|
||||
ChatFormatting.GREEN,
|
||||
ChatFormatting.BOLD
|
||||
)
|
||||
Component.translatable("msg.tiedup.camp.captor_died")
|
||||
.withStyle(ChatFormatting.GREEN, ChatFormatting.BOLD)
|
||||
);
|
||||
|
||||
// Grant grace period (5 minutes = 6000 ticks)
|
||||
@@ -261,9 +259,8 @@ public final class CampLifecycleManager {
|
||||
manager.release(prisoner.getUUID(), level.getGameTime(), 6000);
|
||||
|
||||
prisoner.sendSystemMessage(
|
||||
Component.literal(
|
||||
"You have 5 minutes of protection from kidnappers."
|
||||
).withStyle(ChatFormatting.AQUA)
|
||||
Component.translatable("msg.tiedup.camp.grace_period")
|
||||
.withStyle(ChatFormatting.AQUA)
|
||||
);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
|
||||
@@ -640,7 +640,7 @@ public class CellRegistryV2 extends SavedData {
|
||||
currentState ==
|
||||
com.tiedup.remake.prison.PrisonerState.IMPRISONED
|
||||
) {
|
||||
com.tiedup.remake.prison.service.PrisonerService.get().escape(
|
||||
com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
|
||||
level,
|
||||
id,
|
||||
"offline_cleanup"
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.tiedup.remake.client;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import com.tiedup.remake.client.gui.screens.AdjustmentScreen;
|
||||
import com.tiedup.remake.client.gui.screens.UnifiedBondageScreen;
|
||||
import com.tiedup.remake.core.ModConfig;
|
||||
import com.tiedup.remake.core.SettingsAccessor;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.items.base.ILockable;
|
||||
import com.tiedup.remake.v2.bondage.CollarHelper;
|
||||
@@ -315,7 +315,7 @@ public class ModKeybindings {
|
||||
// Check if player has bind equipped
|
||||
if (state.isTiedUp()) {
|
||||
// Has bind - struggle against it
|
||||
if (ModConfig.SERVER.struggleMiniGameEnabled.get()) {
|
||||
if (SettingsAccessor.isStruggleMiniGameEnabled()) {
|
||||
// New: Start struggle mini-game
|
||||
ModNetwork.sendToServer(
|
||||
new PacketV2StruggleStart(BodyRegionV2.ARMS)
|
||||
|
||||
@@ -72,34 +72,43 @@ public final class GlbAnimationResolver {
|
||||
String prefix = context.getGlbContextPrefix(); // "Sit", "Kneel", "Sneak", "Walk", ""
|
||||
String variant = context.getGlbVariant(); // "Idle" or "Struggle"
|
||||
|
||||
// 1. Exact match: "FullSitIdle" then "SitIdle" (with variants)
|
||||
// 1. Exact match: "FullHeadSitIdle" then "FullSitIdle" then "SitIdle" (with variants)
|
||||
// FullHead variants opt-in to head animation (see GltfPoseConverter.enableSelectiveParts)
|
||||
String exact = prefix + variant;
|
||||
if (!exact.isEmpty()) {
|
||||
String picked = pickWithVariants(data, "Full" + exact);
|
||||
String picked = pickWithVariants(data, "FullHead" + exact);
|
||||
if (picked != null) return picked;
|
||||
picked = pickWithVariants(data, "Full" + exact);
|
||||
if (picked != null) return picked;
|
||||
picked = pickWithVariants(data, exact);
|
||||
if (picked != null) return picked;
|
||||
}
|
||||
|
||||
// 2. For struggles: try "FullStruggle" then "Struggle" (with variants)
|
||||
// 2. For struggles: try "FullHeadStruggle" then "FullStruggle" then "Struggle" (with variants)
|
||||
if (context.isStruggling()) {
|
||||
String picked = pickWithVariants(data, "FullStruggle");
|
||||
String picked = pickWithVariants(data, "FullHeadStruggle");
|
||||
if (picked != null) return picked;
|
||||
picked = pickWithVariants(data, "FullStruggle");
|
||||
if (picked != null) return picked;
|
||||
picked = pickWithVariants(data, "Struggle");
|
||||
if (picked != null) return picked;
|
||||
}
|
||||
|
||||
// 3. Context-only: "FullSit" then "Sit" (with variants)
|
||||
// 3. Context-only: "FullHead{prefix}" then "Full{prefix}" then "{prefix}" (with variants)
|
||||
if (!prefix.isEmpty()) {
|
||||
String picked = pickWithVariants(data, "Full" + prefix);
|
||||
String picked = pickWithVariants(data, "FullHead" + prefix);
|
||||
if (picked != null) return picked;
|
||||
picked = pickWithVariants(data, "Full" + prefix);
|
||||
if (picked != null) return picked;
|
||||
picked = pickWithVariants(data, prefix);
|
||||
if (picked != null) return picked;
|
||||
}
|
||||
|
||||
// 4. Variant-only: "FullIdle" then "Idle" (with variants)
|
||||
// 4. Variant-only: "FullHeadIdle" then "FullIdle" then "Idle" (with variants)
|
||||
{
|
||||
String picked = pickWithVariants(data, "Full" + variant);
|
||||
String picked = pickWithVariants(data, "FullHead" + variant);
|
||||
if (picked != null) return picked;
|
||||
picked = pickWithVariants(data, "Full" + variant);
|
||||
if (picked != null) return picked;
|
||||
picked = pickWithVariants(data, variant);
|
||||
if (picked != null) return picked;
|
||||
|
||||
@@ -7,16 +7,12 @@ import com.tiedup.remake.client.animation.PendingAnimationManager;
|
||||
import com.tiedup.remake.client.animation.context.AnimationContext;
|
||||
import com.tiedup.remake.client.animation.context.AnimationContextResolver;
|
||||
import com.tiedup.remake.client.animation.context.RegionBoneMapper;
|
||||
import com.tiedup.remake.client.animation.util.AnimationIdBuilder;
|
||||
import com.tiedup.remake.client.events.CellHighlightHandler;
|
||||
import com.tiedup.remake.client.events.LeashProxyClientHandler;
|
||||
import com.tiedup.remake.client.gltf.GltfAnimationApplier;
|
||||
import com.tiedup.remake.client.state.ClothesClientCache;
|
||||
import com.tiedup.remake.client.state.MovementStyleClientState;
|
||||
import com.tiedup.remake.client.state.PetBedClientState;
|
||||
import com.tiedup.remake.items.base.PoseType;
|
||||
import com.tiedup.remake.v2.bondage.BindModeHelper;
|
||||
import com.tiedup.remake.v2.bondage.PoseTypeHelper;
|
||||
import com.tiedup.remake.util.HumanChairHelper;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
@@ -205,30 +201,11 @@ public class AnimationTickHandler {
|
||||
context,
|
||||
allOwnedParts
|
||||
);
|
||||
// Clear V1 tracking so transition back works
|
||||
// Clear legacy tracking so transition back works
|
||||
AnimationStateRegistry.getLastAnimId().remove(uuid);
|
||||
} else {
|
||||
// V1 fallback
|
||||
if (GltfAnimationApplier.hasActiveState(player)) {
|
||||
GltfAnimationApplier.clearV2Animation(player);
|
||||
}
|
||||
String animId = buildAnimationId(player, state);
|
||||
String lastId = AnimationStateRegistry.getLastAnimId().get(
|
||||
uuid
|
||||
);
|
||||
if (!animId.equals(lastId)) {
|
||||
boolean success = BondageAnimationManager.playAnimation(
|
||||
player,
|
||||
animId
|
||||
);
|
||||
if (success) {
|
||||
AnimationStateRegistry.getLastAnimId().put(
|
||||
uuid,
|
||||
animId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// No V2 items with GLB models — nothing to animate.
|
||||
// Items without data-driven GLB definitions are not animated.
|
||||
} else if (wasTied) {
|
||||
// Was tied, now free - stop all animations
|
||||
if (GltfAnimationApplier.hasActiveState(player)) {
|
||||
@@ -242,53 +219,6 @@ public class AnimationTickHandler {
|
||||
AnimationStateRegistry.getLastTiedState().put(uuid, isTied);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build animation ID from player's current state (V1 path).
|
||||
*/
|
||||
private static String buildAnimationId(
|
||||
Player player,
|
||||
PlayerBindState state
|
||||
) {
|
||||
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
|
||||
PoseType poseType = PoseTypeHelper.getPoseType(bind);
|
||||
|
||||
// Human chair mode: override DOG pose to HUMAN_CHAIR (straight limbs)
|
||||
poseType = HumanChairHelper.resolveEffectivePose(poseType, bind);
|
||||
|
||||
// Derive bound state from V2 regions (works client-side, synced via capability)
|
||||
boolean armsBound = V2EquipmentHelper.isRegionOccupied(
|
||||
player,
|
||||
BodyRegionV2.ARMS
|
||||
);
|
||||
boolean legsBound = V2EquipmentHelper.isRegionOccupied(
|
||||
player,
|
||||
BodyRegionV2.LEGS
|
||||
);
|
||||
|
||||
// V1 fallback: if no V2 regions are set but player is tied, derive from bind mode NBT
|
||||
if (!armsBound && !legsBound && BindModeHelper.isBindItem(bind)) {
|
||||
armsBound = BindModeHelper.hasArmsBound(bind);
|
||||
legsBound = BindModeHelper.hasLegsBound(bind);
|
||||
}
|
||||
|
||||
boolean isStruggling = state.isStruggling();
|
||||
boolean isSneaking = player.isCrouching();
|
||||
boolean isMoving =
|
||||
player.getDeltaMovement().horizontalDistanceSqr() > 1e-6;
|
||||
|
||||
// Build animation ID with sneak and movement support
|
||||
return AnimationIdBuilder.build(
|
||||
poseType,
|
||||
armsBound,
|
||||
legsBound,
|
||||
null,
|
||||
isStruggling,
|
||||
true,
|
||||
isSneaking,
|
||||
isMoving
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Player logout event - cleanup animation data.
|
||||
*/
|
||||
|
||||
@@ -82,7 +82,7 @@ public class NpcAnimationTickHandler {
|
||||
* (base posture) and an item layer (GLB-driven bones). Sitting and kneeling are
|
||||
* handled by the context resolver, so the V2 path now covers all postures.
|
||||
*
|
||||
* <p>V1 fallback: if no V2 GLB model is found, falls back to JSON-based
|
||||
* <p>Legacy fallback: if no GLB model is found, falls back to JSON-based
|
||||
* PlayerAnimator animations via {@link BondageAnimationManager}.
|
||||
*/
|
||||
private static void updateNpcAnimation(AbstractTiedUpNpc entity) {
|
||||
@@ -120,7 +120,7 @@ public class NpcAnimationTickHandler {
|
||||
);
|
||||
lastNpcAnimId.remove(uuid);
|
||||
} else {
|
||||
// V1 fallback: JSON-based PlayerAnimator animations
|
||||
// Legacy fallback: JSON-based PlayerAnimator animations
|
||||
if (GltfAnimationApplier.hasActiveState(entity)) {
|
||||
GltfAnimationApplier.clearV2Animation(entity);
|
||||
}
|
||||
@@ -148,7 +148,7 @@ public class NpcAnimationTickHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build animation ID for an NPC from its current state (V1 path).
|
||||
* Build animation ID for an NPC from its current state (legacy JSON path).
|
||||
*/
|
||||
private static String buildNpcAnimationId(AbstractTiedUpNpc entity) {
|
||||
// Determine position prefix for SIT/KNEEL poses
|
||||
@@ -175,7 +175,7 @@ public class NpcAnimationTickHandler {
|
||||
BodyRegionV2.LEGS
|
||||
);
|
||||
|
||||
// V1 fallback: if no V2 regions set but NPC has a bind, derive from bind mode NBT
|
||||
// Legacy fallback: if no V2 regions set but NPC has a bind, derive from bind mode NBT
|
||||
if (!armsBound && !legsBound && BindModeHelper.isBindItem(bind)) {
|
||||
armsBound = BindModeHelper.hasArmsBound(bind);
|
||||
legsBound = BindModeHelper.hasLegsBound(bind);
|
||||
|
||||
@@ -103,29 +103,37 @@ public final class GlbParser {
|
||||
JsonObject skin = skins.get(0).getAsJsonObject();
|
||||
JsonArray skinJoints = skin.getAsJsonArray("joints");
|
||||
|
||||
// Filter skin joints to only include known deforming bones
|
||||
List<Integer> filteredJointNodes = new ArrayList<>();
|
||||
int[] skinJointRemap = new int[skinJoints.size()]; // old skin index -> new filtered index
|
||||
java.util.Arrays.fill(skinJointRemap, -1);
|
||||
// Accept all skin joints (no filtering — custom bones are supported)
|
||||
List<Integer> allJointNodes = new ArrayList<>();
|
||||
for (int j = 0; j < skinJoints.size(); j++) {
|
||||
int nodeIdx = skinJoints.get(j).getAsInt();
|
||||
JsonObject node = nodes.get(nodeIdx).getAsJsonObject();
|
||||
String name = node.has("name")
|
||||
? node.get("name").getAsString()
|
||||
: "joint_" + j;
|
||||
if (GltfBoneMapper.isKnownBone(name)) {
|
||||
skinJointRemap[j] = filteredJointNodes.size();
|
||||
filteredJointNodes.add(nodeIdx);
|
||||
} else {
|
||||
LOGGER.debug(
|
||||
"[GltfPipeline] Skipping non-deforming bone: '{}' (node {})",
|
||||
name,
|
||||
nodeIdx
|
||||
);
|
||||
// Strip armature prefix (e.g., "MyRig|body" -> "body")
|
||||
if (name.contains("|")) {
|
||||
name = name.substring(name.lastIndexOf('|') + 1);
|
||||
}
|
||||
// Log info for non-MC bones
|
||||
if (!GltfBoneMapper.isKnownBone(name)) {
|
||||
String suggestion = GltfBoneMapper.suggestBoneName(name);
|
||||
if (suggestion != null) {
|
||||
LOGGER.warn(
|
||||
"[GltfPipeline] Unknown bone '{}' in {} — did you mean '{}'? (treated as custom bone)",
|
||||
name, debugName, suggestion
|
||||
);
|
||||
} else {
|
||||
LOGGER.info(
|
||||
"[GltfPipeline] Custom bone '{}' in {} (will follow parent hierarchy in rest pose)",
|
||||
name, debugName
|
||||
);
|
||||
}
|
||||
}
|
||||
allJointNodes.add(nodeIdx);
|
||||
}
|
||||
|
||||
int jointCount = filteredJointNodes.size();
|
||||
int jointCount = allJointNodes.size();
|
||||
String[] jointNames = new String[jointCount];
|
||||
int[] parentJointIndices = new int[jointCount];
|
||||
Quaternionf[] restRotations = new Quaternionf[jointCount];
|
||||
@@ -135,19 +143,23 @@ public final class GlbParser {
|
||||
int[] nodeToJoint = new int[nodes.size()];
|
||||
java.util.Arrays.fill(nodeToJoint, -1);
|
||||
for (int j = 0; j < jointCount; j++) {
|
||||
int nodeIdx = filteredJointNodes.get(j);
|
||||
int nodeIdx = allJointNodes.get(j);
|
||||
nodeToJoint[nodeIdx] = j;
|
||||
}
|
||||
|
||||
// Read joint names, rest pose, and build parent mapping
|
||||
java.util.Arrays.fill(parentJointIndices, -1);
|
||||
for (int j = 0; j < jointCount; j++) {
|
||||
int nodeIdx = filteredJointNodes.get(j);
|
||||
int nodeIdx = allJointNodes.get(j);
|
||||
JsonObject node = nodes.get(nodeIdx).getAsJsonObject();
|
||||
|
||||
jointNames[j] = node.has("name")
|
||||
String rawName = node.has("name")
|
||||
? node.get("name").getAsString()
|
||||
: "joint_" + j;
|
||||
// Strip armature prefix consistently
|
||||
jointNames[j] = rawName.contains("|")
|
||||
? rawName.substring(rawName.lastIndexOf('|') + 1)
|
||||
: rawName;
|
||||
|
||||
// Rest rotation
|
||||
if (node.has("rotation")) {
|
||||
@@ -192,7 +204,6 @@ public final class GlbParser {
|
||||
}
|
||||
|
||||
// -- Inverse Bind Matrices --
|
||||
// IBM accessor is indexed by original skin joint order, so we pick the filtered entries
|
||||
Matrix4f[] inverseBindMatrices = new Matrix4f[jointCount];
|
||||
if (skin.has("inverseBindMatrices")) {
|
||||
int ibmAccessor = skin.get("inverseBindMatrices").getAsInt();
|
||||
@@ -202,12 +213,9 @@ public final class GlbParser {
|
||||
binData,
|
||||
ibmAccessor
|
||||
);
|
||||
for (int origJ = 0; origJ < skinJoints.size(); origJ++) {
|
||||
int newJ = skinJointRemap[origJ];
|
||||
if (newJ >= 0) {
|
||||
inverseBindMatrices[newJ] = new Matrix4f();
|
||||
inverseBindMatrices[newJ].set(ibmData, origJ * 16);
|
||||
}
|
||||
for (int j = 0; j < jointCount; j++) {
|
||||
inverseBindMatrices[j] = new Matrix4f();
|
||||
inverseBindMatrices[j].set(ibmData, j * 16);
|
||||
}
|
||||
} else {
|
||||
for (int j = 0; j < jointCount; j++) {
|
||||
@@ -215,20 +223,36 @@ public final class GlbParser {
|
||||
}
|
||||
}
|
||||
|
||||
// -- Find mesh (ignore "Player" mesh, take LAST non-Player) --
|
||||
// WORKAROUND: Takes the LAST non-Player mesh because modelers may leave prototype meshes
|
||||
// in the .glb. Revert to first non-Player mesh once modeler workflow is standardized.
|
||||
// -- Find mesh by convention, then fallback --
|
||||
// Priority: 1) mesh named "Item", 2) last non-Player mesh
|
||||
int targetMeshIdx = -1;
|
||||
String selectedMeshName = null;
|
||||
int nonPlayerCount = 0;
|
||||
if (meshes != null) {
|
||||
for (int mi = 0; mi < meshes.size(); mi++) {
|
||||
JsonObject mesh = meshes.get(mi).getAsJsonObject();
|
||||
String meshName = mesh.has("name")
|
||||
? mesh.get("name").getAsString()
|
||||
: "";
|
||||
if ("Item".equals(meshName)) {
|
||||
targetMeshIdx = mi;
|
||||
selectedMeshName = meshName;
|
||||
break; // Convention match — use it
|
||||
}
|
||||
if (!"Player".equals(meshName)) {
|
||||
targetMeshIdx = mi;
|
||||
selectedMeshName = meshName;
|
||||
nonPlayerCount++;
|
||||
}
|
||||
}
|
||||
if (nonPlayerCount > 1 && !"Item".equals(selectedMeshName)) {
|
||||
LOGGER.warn(
|
||||
"[GltfPipeline] {} non-Player meshes found in {} — using '{}'. "
|
||||
+ "Name your mesh 'Item' for explicit selection.",
|
||||
nonPlayerCount, debugName,
|
||||
selectedMeshName != null ? selectedMeshName : "(unnamed)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// -- Parse root material names (for tint channel detection) --
|
||||
@@ -320,15 +344,10 @@ public final class GlbParser {
|
||||
binData,
|
||||
attributes.get("JOINTS_0").getAsInt()
|
||||
);
|
||||
// Remap vertex joint indices from original skin order to filtered order
|
||||
// No remap needed — all joints are kept, indices match directly.
|
||||
// Guard against out-of-range joint indices.
|
||||
for (int i = 0; i < primJoints.length; i++) {
|
||||
int origIdx = primJoints[i];
|
||||
if (origIdx >= 0 && origIdx < skinJointRemap.length) {
|
||||
primJoints[i] =
|
||||
skinJointRemap[origIdx] >= 0
|
||||
? skinJointRemap[origIdx]
|
||||
: 0;
|
||||
} else {
|
||||
if (primJoints[i] < 0 || primJoints[i] >= jointCount) {
|
||||
primJoints[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.tiedup.remake.client.animation.context.GlbAnimationResolver;
|
||||
import com.tiedup.remake.client.animation.context.RegionBoneMapper;
|
||||
import dev.kosmx.playerAnim.core.data.KeyframeAnimation;
|
||||
import java.util.HashSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -126,11 +127,13 @@ public final class GltfAnimationApplier {
|
||||
String enabledKey = canonicalPartsKey(ownership.enabledParts());
|
||||
String partsKey = ownedKey + ";" + enabledKey;
|
||||
|
||||
// Build composite state key to avoid redundant updates
|
||||
// Build composite state key to detect context changes.
|
||||
// NOTE: This key does NOT include the variant name — that is resolved fresh
|
||||
// each time the context changes, enabling random variant selection.
|
||||
String stateKey = animSource + "|" + context.name() + "|" + partsKey;
|
||||
String currentKey = activeStateKeys.get(entity.getUUID());
|
||||
if (stateKey.equals(currentKey)) {
|
||||
return true; // Already active, no-op
|
||||
return true; // Same context, same parts — no need to re-resolve
|
||||
}
|
||||
|
||||
// === Layer 1: Context animation (base body posture) ===
|
||||
@@ -153,26 +156,36 @@ public final class GltfAnimationApplier {
|
||||
return false;
|
||||
}
|
||||
|
||||
KeyframeAnimation itemAnim = itemAnimCache.get(itemCacheKey);
|
||||
// Resolve animation data first (needed for variant resolution)
|
||||
GltfData animData = GlbAnimationResolver.resolveAnimationData(
|
||||
modelLoc,
|
||||
animationSource
|
||||
);
|
||||
if (animData == null) {
|
||||
LOGGER.warn(
|
||||
"[GltfPipeline] Failed to load animation GLB: {}",
|
||||
animSource
|
||||
);
|
||||
failedLoadKeys.add(itemCacheKey);
|
||||
activeStateKeys.put(entity.getUUID(), stateKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resolve which named animation to use (with fallback chain + variant selection).
|
||||
// This must happen BEFORE the cache lookup because variant selection is random —
|
||||
// we want a fresh random pick each time the context changes, not a permanently
|
||||
// cached first pick.
|
||||
String glbAnimName = GlbAnimationResolver.resolve(
|
||||
animData,
|
||||
context
|
||||
);
|
||||
|
||||
// Include the resolved animation name in the cache key so different variants
|
||||
// (Struggle.1 vs Struggle.2) get separate cache entries.
|
||||
String variantCacheKey = itemCacheKey + "#" + (glbAnimName != null ? glbAnimName : "default");
|
||||
|
||||
KeyframeAnimation itemAnim = itemAnimCache.get(variantCacheKey);
|
||||
if (itemAnim == null) {
|
||||
GltfData animData = GlbAnimationResolver.resolveAnimationData(
|
||||
modelLoc,
|
||||
animationSource
|
||||
);
|
||||
if (animData == null) {
|
||||
LOGGER.warn(
|
||||
"[GltfPipeline] Failed to load animation GLB: {}",
|
||||
animSource
|
||||
);
|
||||
failedLoadKeys.add(itemCacheKey);
|
||||
activeStateKeys.put(entity.getUUID(), stateKey);
|
||||
return false;
|
||||
}
|
||||
// Resolve which named animation to use (with fallback chain + variant selection)
|
||||
String glbAnimName = GlbAnimationResolver.resolve(
|
||||
animData,
|
||||
context
|
||||
);
|
||||
// Pass both owned parts and enabled parts (owned + free) for selective enabling
|
||||
itemAnim = GltfPoseConverter.convertSelective(
|
||||
animData,
|
||||
@@ -180,7 +193,7 @@ public final class GltfAnimationApplier {
|
||||
ownership.thisParts(),
|
||||
ownership.enabledParts()
|
||||
);
|
||||
itemAnimCache.put(itemCacheKey, itemAnim);
|
||||
itemAnimCache.put(variantCacheKey, itemAnim);
|
||||
}
|
||||
|
||||
BondageAnimationManager.playDirect(entity, itemAnim);
|
||||
@@ -239,7 +252,40 @@ public final class GltfAnimationApplier {
|
||||
}
|
||||
|
||||
// === Layer 2: Composite item animation ===
|
||||
String compositeCacheKey = "multi#" + stateKey;
|
||||
// Pre-resolve animation data and variant names for all items BEFORE cache lookup.
|
||||
// This ensures random variant selection happens fresh on each context change,
|
||||
// and each variant combination gets its own cache entry.
|
||||
record ResolvedItem(
|
||||
GltfData animData,
|
||||
String glbAnimName,
|
||||
RegionBoneMapper.V2ItemAnimInfo info
|
||||
) {}
|
||||
|
||||
List<ResolvedItem> resolvedItems = new ArrayList<>();
|
||||
StringBuilder variantKeyBuilder = new StringBuilder("multi#").append(stateKey);
|
||||
|
||||
for (RegionBoneMapper.V2ItemAnimInfo item : items) {
|
||||
ResourceLocation animSource =
|
||||
item.animSource() != null ? item.animSource() : item.modelLoc();
|
||||
|
||||
GltfData animData = GlbAnimationResolver.resolveAnimationData(
|
||||
item.modelLoc(), item.animSource()
|
||||
);
|
||||
if (animData == null) {
|
||||
LOGGER.warn(
|
||||
"[GltfPipeline] Failed to load GLB for multi-item: {}",
|
||||
animSource
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
String glbAnimName = GlbAnimationResolver.resolve(animData, context);
|
||||
resolvedItems.add(new ResolvedItem(animData, glbAnimName, item));
|
||||
variantKeyBuilder.append('#')
|
||||
.append(glbAnimName != null ? glbAnimName : "default");
|
||||
}
|
||||
|
||||
String compositeCacheKey = variantKeyBuilder.toString();
|
||||
|
||||
if (failedLoadKeys.contains(compositeCacheKey)) {
|
||||
activeStateKeys.put(entity.getUUID(), stateKey);
|
||||
@@ -261,29 +307,13 @@ public final class GltfAnimationApplier {
|
||||
|
||||
boolean anyLoaded = false;
|
||||
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
RegionBoneMapper.V2ItemAnimInfo item = items.get(i);
|
||||
for (ResolvedItem resolved : resolvedItems) {
|
||||
RegionBoneMapper.V2ItemAnimInfo item = resolved.info();
|
||||
GltfData animData = resolved.animData();
|
||||
String glbAnimName = resolved.glbAnimName();
|
||||
ResourceLocation animSource =
|
||||
item.animSource() != null
|
||||
? item.animSource()
|
||||
: item.modelLoc();
|
||||
item.animSource() != null ? item.animSource() : item.modelLoc();
|
||||
|
||||
GltfData animData = GlbAnimationResolver.resolveAnimationData(
|
||||
item.modelLoc(),
|
||||
item.animSource()
|
||||
);
|
||||
if (animData == null) {
|
||||
LOGGER.warn(
|
||||
"[GltfPipeline] Failed to load GLB for multi-item: {}",
|
||||
animSource
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
String glbAnimName = GlbAnimationResolver.resolve(
|
||||
animData,
|
||||
context
|
||||
);
|
||||
GltfData.AnimationClip rawClip;
|
||||
if (glbAnimName != null) {
|
||||
rawClip = animData.getRawAnimation(glbAnimName);
|
||||
@@ -298,9 +328,7 @@ public final class GltfAnimationApplier {
|
||||
// if the item declares per-animation bone filtering.
|
||||
Set<String> effectiveParts = item.ownedParts();
|
||||
if (glbAnimName != null && !item.animationBones().isEmpty()) {
|
||||
Set<String> override = item
|
||||
.animationBones()
|
||||
.get(glbAnimName);
|
||||
Set<String> override = item.animationBones().get(glbAnimName);
|
||||
if (override != null) {
|
||||
Set<String> filtered = new HashSet<>(override);
|
||||
filtered.retainAll(item.ownedParts());
|
||||
@@ -311,19 +339,13 @@ public final class GltfAnimationApplier {
|
||||
}
|
||||
|
||||
GltfPoseConverter.addBonesToBuilder(
|
||||
builder,
|
||||
animData,
|
||||
rawClip,
|
||||
effectiveParts
|
||||
builder, animData, rawClip, effectiveParts
|
||||
);
|
||||
anyLoaded = true;
|
||||
|
||||
LOGGER.debug(
|
||||
"[GltfPipeline] Multi-item: {} -> owned={}, effective={}, anim={}",
|
||||
animSource,
|
||||
item.ownedParts(),
|
||||
effectiveParts,
|
||||
glbAnimName
|
||||
animSource, item.ownedParts(), effectiveParts, glbAnimName
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import net.minecraft.client.model.HumanoidModel;
|
||||
import net.minecraft.client.model.geom.ModelPart;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Maps glTF bone names to Minecraft HumanoidModel parts.
|
||||
@@ -110,4 +111,25 @@ public final class GltfBoneMapper {
|
||||
public static boolean isKnownBone(String boneName) {
|
||||
return BONE_TO_PART.containsKey(boneName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all known bone names for validation/suggestion purposes.
|
||||
*/
|
||||
public static Set<String> knownBoneNames() {
|
||||
return BONE_TO_PART.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest a known bone name for a case-insensitive match.
|
||||
* Returns null if no case-insensitive match is found.
|
||||
*/
|
||||
@Nullable
|
||||
public static String suggestBoneName(String unknownBone) {
|
||||
for (String known : BONE_TO_PART.keySet()) {
|
||||
if (known.equalsIgnoreCase(unknownBone)) {
|
||||
return known;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ public final class GltfClientSetup {
|
||||
ProfilerFiller profiler
|
||||
) {
|
||||
GltfCache.clearCache();
|
||||
GltfSkinCache.clearAll();
|
||||
GltfAnimationApplier.invalidateCache();
|
||||
GltfMeshRenderer.clearRenderTypeCache();
|
||||
// Reload context GLB animations from resource packs FIRST,
|
||||
@@ -117,7 +118,43 @@ public final class GltfClientSetup {
|
||||
LOGGER.info(
|
||||
"[GltfPipeline] Data-driven item reload listener registered"
|
||||
);
|
||||
|
||||
// GLB structural validation (runs after item definitions are loaded)
|
||||
event.registerReloadListener(new com.tiedup.remake.client.gltf.diagnostic.GlbValidationReloadListener());
|
||||
LOGGER.info("[GltfPipeline] GLB validation reload listener registered");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FORGE bus event subscribers for entity lifecycle cleanup.
|
||||
* Removes skin cache entries when entities leave the level, preventing memory leaks.
|
||||
*/
|
||||
@Mod.EventBusSubscriber(
|
||||
modid = "tiedup",
|
||||
bus = Mod.EventBusSubscriber.Bus.FORGE,
|
||||
value = Dist.CLIENT
|
||||
)
|
||||
public static class ForgeBusEvents {
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onEntityLeaveLevel(
|
||||
net.minecraftforge.event.entity.EntityLeaveLevelEvent event
|
||||
) {
|
||||
if (event.getLevel().isClientSide()) {
|
||||
GltfSkinCache.removeEntity(event.getEntity().getId());
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onRegisterClientCommands(
|
||||
net.minecraftforge.client.event.RegisterClientCommandsEvent event
|
||||
) {
|
||||
com.tiedup.remake.commands.ValidateGlbCommand.register(
|
||||
event.getDispatcher()
|
||||
);
|
||||
LOGGER.info(
|
||||
"[GltfPipeline] Client command /tiedup validate registered"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,6 +225,101 @@ public final class GltfMeshRenderer extends RenderStateShard {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Two-pass skinned renderer with cache support.
|
||||
*
|
||||
* <p><b>Pass 1</b> (skippable): if {@code cachedPositions} is null, skin every
|
||||
* unique vertex into flat {@code float[]} arrays (positions and normals).
|
||||
* If cached arrays are provided, Pass 1 is skipped entirely.</p>
|
||||
*
|
||||
* <p><b>Pass 2</b> (always): iterate the index buffer, read skinned data from
|
||||
* the arrays, and emit to the {@link VertexConsumer}.</p>
|
||||
*
|
||||
* @param data parsed glTF data
|
||||
* @param jointMatrices computed joint matrices from skinning engine
|
||||
* @param poseStack current pose stack
|
||||
* @param buffer multi-buffer source
|
||||
* @param packedLight packed light value
|
||||
* @param packedOverlay packed overlay value
|
||||
* @param renderType the RenderType to use for rendering
|
||||
* @param cachedPositions previously cached skinned positions, or null to compute fresh
|
||||
* @param cachedNormals previously cached skinned normals, or null to compute fresh
|
||||
* @return {@code new float[][] { positions, normals }} for the caller to cache
|
||||
*/
|
||||
public static float[][] renderSkinnedWithCache(
|
||||
GltfData data,
|
||||
Matrix4f[] jointMatrices,
|
||||
PoseStack poseStack,
|
||||
MultiBufferSource buffer,
|
||||
int packedLight,
|
||||
int packedOverlay,
|
||||
RenderType renderType,
|
||||
float[] cachedPositions,
|
||||
float[] cachedNormals
|
||||
) {
|
||||
int vertexCount = data.vertexCount();
|
||||
float[] positions;
|
||||
float[] normals;
|
||||
|
||||
// -- Pass 1: Skin all unique vertices (skipped when cache hit) --
|
||||
if (cachedPositions != null && cachedNormals != null) {
|
||||
positions = cachedPositions;
|
||||
normals = cachedNormals;
|
||||
} else {
|
||||
positions = new float[vertexCount * 3];
|
||||
normals = new float[vertexCount * 3];
|
||||
|
||||
float[] outPos = new float[3];
|
||||
float[] outNormal = new float[3];
|
||||
Vector4f tmpPos = new Vector4f();
|
||||
Vector4f tmpNorm = new Vector4f();
|
||||
|
||||
for (int v = 0; v < vertexCount; v++) {
|
||||
GltfSkinningEngine.skinVertex(
|
||||
data, v, jointMatrices, outPos, outNormal, tmpPos, tmpNorm
|
||||
);
|
||||
positions[v * 3] = outPos[0];
|
||||
positions[v * 3 + 1] = outPos[1];
|
||||
positions[v * 3 + 2] = outPos[2];
|
||||
normals[v * 3] = outNormal[0];
|
||||
normals[v * 3 + 1] = outNormal[1];
|
||||
normals[v * 3 + 2] = outNormal[2];
|
||||
}
|
||||
}
|
||||
|
||||
// -- Pass 2: Emit vertices from arrays to VertexConsumer --
|
||||
Matrix4f pose = poseStack.last().pose();
|
||||
Matrix3f normalMat = poseStack.last().normal();
|
||||
VertexConsumer vc = buffer.getBuffer(renderType);
|
||||
|
||||
int[] indices = data.indices();
|
||||
float[] texCoords = data.texCoords();
|
||||
|
||||
for (int idx : indices) {
|
||||
float px = positions[idx * 3];
|
||||
float py = positions[idx * 3 + 1];
|
||||
float pz = positions[idx * 3 + 2];
|
||||
|
||||
float nx = normals[idx * 3];
|
||||
float ny = normals[idx * 3 + 1];
|
||||
float nz = normals[idx * 3 + 2];
|
||||
|
||||
float u = texCoords[idx * 2];
|
||||
float v = texCoords[idx * 2 + 1];
|
||||
|
||||
vc
|
||||
.vertex(pose, px, py, pz)
|
||||
.color(255, 255, 255, 255)
|
||||
.uv(u, 1.0f - v)
|
||||
.overlayCoords(packedOverlay)
|
||||
.uv2(packedLight)
|
||||
.normal(normalMat, nx, ny, nz)
|
||||
.endVertex();
|
||||
}
|
||||
|
||||
return new float[][] { positions, normals };
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a skinned glTF mesh with per-primitive tint colors.
|
||||
*
|
||||
|
||||
@@ -208,12 +208,14 @@ public final class GltfPoseConverter {
|
||||
}
|
||||
}
|
||||
|
||||
// Selective: enable owned parts always, free parts only if they have keyframes
|
||||
// Selective: enable owned parts always, free parts only for "Full" animations
|
||||
// that explicitly opt into full-body control.
|
||||
enableSelectiveParts(
|
||||
builder,
|
||||
ownedParts,
|
||||
enabledParts,
|
||||
partsWithKeyframes
|
||||
partsWithKeyframes,
|
||||
animName
|
||||
);
|
||||
|
||||
KeyframeAnimation anim = builder.build();
|
||||
@@ -554,22 +556,42 @@ public final class GltfPoseConverter {
|
||||
*
|
||||
* <ul>
|
||||
* <li>Owned parts: always enabled (the item controls these bones)</li>
|
||||
* <li>Free parts WITH keyframes: enabled (the GLB has animation data for them)</li>
|
||||
* <li>Free parts WITHOUT keyframes: disabled (no data to animate, pass through to context)</li>
|
||||
* <li>Free parts WITH keyframes AND "Full" animation: enabled (explicit opt-in to full-body)</li>
|
||||
* <li>Free parts without "Full" prefix: disabled (prevents accidental bone hijacking)</li>
|
||||
* <li>Other items' parts: disabled (pass through to their own layer)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>The "Full" prefix convention (FullIdle, FullStruggle, FullWalk) is the artist's
|
||||
* explicit declaration that this animation is designed to control the entire body,
|
||||
* not just the item's owned regions. Without this prefix, free bones are never enabled,
|
||||
* even if the GLB contains keyframes for them. This prevents accidental bone hijacking
|
||||
* when an artist keyframes all bones in Blender by default.</p>
|
||||
*
|
||||
* @param builder the animation builder with keyframes already added
|
||||
* @param ownedParts parts the item explicitly owns (always enabled)
|
||||
* @param enabledParts parts the item may animate (owned + free)
|
||||
* @param partsWithKeyframes parts that received actual animation data from the GLB
|
||||
* @param animName resolved animation name (checked for "Full" prefix)
|
||||
*/
|
||||
private static void enableSelectiveParts(
|
||||
KeyframeAnimation.AnimationBuilder builder,
|
||||
Set<String> ownedParts,
|
||||
Set<String> enabledParts,
|
||||
Set<String> partsWithKeyframes
|
||||
Set<String> partsWithKeyframes,
|
||||
String animName
|
||||
) {
|
||||
// Free bones are only enabled for "Full" animations (FullIdle, FullStruggle, etc.)
|
||||
// The "gltf_" prefix is added by convertClipSelective, so check for "gltf_Full"
|
||||
boolean isFullBodyAnimation = animName != null &&
|
||||
animName.startsWith("gltf_Full");
|
||||
// Head is protected by default — only enabled as a free bone when the animation
|
||||
// name contains "Head" (e.g., FullHeadStruggle, FullHeadIdle).
|
||||
// This lets artists opt-in per animation without affecting the item's regions.
|
||||
// FullHead prefix (e.g., FullHeadStruggle) opts into head as a free bone.
|
||||
// Use startsWith to avoid false positives (e.g., FullOverhead, FullAhead).
|
||||
boolean allowFreeHead = isFullBodyAnimation &&
|
||||
animName.startsWith("gltf_FullHead");
|
||||
|
||||
String[] allParts = {
|
||||
"head",
|
||||
"body",
|
||||
@@ -588,13 +610,20 @@ public final class GltfPoseConverter {
|
||||
// Always enable owned parts — the item controls these bones
|
||||
part.fullyEnablePart(false);
|
||||
} else if (
|
||||
isFullBodyAnimation &&
|
||||
enabledParts.contains(partName) &&
|
||||
partsWithKeyframes.contains(partName)
|
||||
partsWithKeyframes.contains(partName) &&
|
||||
(!"head".equals(partName) || allowFreeHead)
|
||||
) {
|
||||
// Free part WITH keyframes: enable so the GLB animation drives it
|
||||
// Full-body animation: free part WITH keyframes — enable.
|
||||
// The "Full" prefix is the artist's explicit opt-in to animate
|
||||
// bones outside their declared regions.
|
||||
// Head is protected by default (preserves vanilla head tracking).
|
||||
// Use "Head" in the animation name (e.g., FullHeadStruggle) to
|
||||
// explicitly opt-in to head control for that animation.
|
||||
part.fullyEnablePart(false);
|
||||
} else {
|
||||
// Other item's part, or free part without keyframes: disable.
|
||||
// Non-Full animation, other item's part, or free part without keyframes.
|
||||
// Disabled parts pass through to the lower-priority context layer.
|
||||
part.setEnabled(false);
|
||||
}
|
||||
|
||||
121
src/main/java/com/tiedup/remake/client/gltf/GltfSkinCache.java
Normal file
121
src/main/java/com/tiedup/remake/client/gltf/GltfSkinCache.java
Normal file
@@ -0,0 +1,121 @@
|
||||
package com.tiedup.remake.client.gltf;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
|
||||
/**
|
||||
* Skinning result cache that avoids re-skinning when a player's pose hasn't changed.
|
||||
*
|
||||
* <p>Uses a dirty-flag approach: each cache entry stores the raw int bits of
|
||||
* every float input (joint euler angles) that drove the last skinning pass.
|
||||
* On the next frame, if all bits match exactly, the cached skinned positions
|
||||
* and normals are reused (skipping the expensive LBS loop).
|
||||
*
|
||||
* <p>Bit comparison via {@link Float#floatToRawIntBits(float)} avoids epsilon
|
||||
* drift: a pose is "unchanged" only when the inputs are identical down to the
|
||||
* bit (idle, AFK, paused animation frame).
|
||||
*/
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public final class GltfSkinCache {
|
||||
|
||||
private record CacheKey(int entityId, ResourceLocation modelLoc) {}
|
||||
|
||||
private static final class Entry {
|
||||
int[] lastInputBits;
|
||||
float[] skinnedPositions;
|
||||
float[] skinnedNormals;
|
||||
}
|
||||
|
||||
private static final Map<CacheKey, Entry> cache = new HashMap<>();
|
||||
|
||||
private GltfSkinCache() {}
|
||||
|
||||
/**
|
||||
* Check whether the pose inputs are bit-identical to the last cached skinning pass.
|
||||
*
|
||||
* @param entityId the entity's numeric ID
|
||||
* @param modelLoc the model ResourceLocation (distinguishes multiple items on one entity)
|
||||
* @param currentInputs flat array of float inputs that drove joint matrix computation
|
||||
* @return true if every input bit matches the cached entry (safe to reuse cached data)
|
||||
*/
|
||||
public static boolean isPoseUnchanged(
|
||||
int entityId,
|
||||
ResourceLocation modelLoc,
|
||||
float[] currentInputs
|
||||
) {
|
||||
CacheKey key = new CacheKey(entityId, modelLoc);
|
||||
Entry entry = cache.get(key);
|
||||
if (entry == null || entry.lastInputBits == null) return false;
|
||||
if (entry.lastInputBits.length != currentInputs.length) return false;
|
||||
for (int i = 0; i < currentInputs.length; i++) {
|
||||
if (
|
||||
entry.lastInputBits[i]
|
||||
!= Float.floatToRawIntBits(currentInputs[i])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a skinning result in the cache, replacing any previous entry for the same key.
|
||||
*
|
||||
* @param entityId the entity's numeric ID
|
||||
* @param modelLoc the model ResourceLocation
|
||||
* @param poseInputs the float inputs that produced these results (will be bit-snapshotted)
|
||||
* @param positions skinned vertex positions (will be cloned)
|
||||
* @param normals skinned vertex normals (will be cloned)
|
||||
*/
|
||||
public static void store(
|
||||
int entityId,
|
||||
ResourceLocation modelLoc,
|
||||
float[] poseInputs,
|
||||
float[] positions,
|
||||
float[] normals
|
||||
) {
|
||||
CacheKey key = new CacheKey(entityId, modelLoc);
|
||||
Entry entry = cache.computeIfAbsent(key, k -> new Entry());
|
||||
entry.lastInputBits = new int[poseInputs.length];
|
||||
for (int i = 0; i < poseInputs.length; i++) {
|
||||
entry.lastInputBits[i] = Float.floatToRawIntBits(poseInputs[i]);
|
||||
}
|
||||
entry.skinnedPositions = positions.clone();
|
||||
entry.skinnedNormals = normals.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve cached skinned positions, or null if no cache entry exists.
|
||||
*/
|
||||
public static float[] getCachedPositions(
|
||||
int entityId,
|
||||
ResourceLocation modelLoc
|
||||
) {
|
||||
Entry entry = cache.get(new CacheKey(entityId, modelLoc));
|
||||
return entry != null ? entry.skinnedPositions : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve cached skinned normals, or null if no cache entry exists.
|
||||
*/
|
||||
public static float[] getCachedNormals(
|
||||
int entityId,
|
||||
ResourceLocation modelLoc
|
||||
) {
|
||||
Entry entry = cache.get(new CacheKey(entityId, modelLoc));
|
||||
return entry != null ? entry.skinnedNormals : null;
|
||||
}
|
||||
|
||||
/** Clear the entire cache (called on resource reload). */
|
||||
public static void clearAll() {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
/** Remove all cache entries for a specific entity (called on entity leave). */
|
||||
public static void removeEntity(int entityId) {
|
||||
cache.entrySet().removeIf(e -> e.getKey().entityId == entityId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.tiedup.remake.client.gltf.diagnostic;
|
||||
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* A single diagnostic finding from GLB validation.
|
||||
*/
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public record GlbDiagnostic(
|
||||
ResourceLocation source,
|
||||
@Nullable ResourceLocation itemDef,
|
||||
Severity severity,
|
||||
String code,
|
||||
String message
|
||||
) {
|
||||
public enum Severity { ERROR, WARNING, INFO }
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.tiedup.remake.client.gltf.diagnostic;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
|
||||
/**
|
||||
* Static client-side registry of GLB validation results.
|
||||
* Populated during resource reload, queried by /tiedup validate command.
|
||||
*/
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public final class GlbDiagnosticRegistry {
|
||||
|
||||
private static final Map<ResourceLocation, GlbValidationResult> results =
|
||||
new LinkedHashMap<>();
|
||||
|
||||
private GlbDiagnosticRegistry() {}
|
||||
|
||||
public static void clear() {
|
||||
results.clear();
|
||||
}
|
||||
|
||||
public static void addResult(GlbValidationResult result) {
|
||||
results.put(result.source(), result);
|
||||
}
|
||||
|
||||
public static Collection<GlbValidationResult> getAll() {
|
||||
return Collections.unmodifiableCollection(results.values());
|
||||
}
|
||||
|
||||
public static List<GlbValidationResult> getErrors() {
|
||||
return results.values().stream()
|
||||
.filter(r -> !r.passed())
|
||||
.toList();
|
||||
}
|
||||
|
||||
public static GlbValidationResult get(ResourceLocation source) {
|
||||
return results.get(source);
|
||||
}
|
||||
|
||||
public static int size() {
|
||||
return results.size();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.tiedup.remake.client.gltf.diagnostic;
|
||||
|
||||
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
|
||||
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.components.toasts.SystemToast;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.Resource;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
import net.minecraft.server.packs.resources.SimplePreparableReloadListener;
|
||||
import net.minecraft.util.profiling.ProfilerFiller;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* Reload listener that validates all data-driven item GLB models on resource
|
||||
* reload (F3+T or startup).
|
||||
*
|
||||
* <p>Must be registered AFTER {@link com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemReloadListener}
|
||||
* so that the item registry is populated before validation runs.</p>
|
||||
*
|
||||
* <p>On each reload:
|
||||
* <ol>
|
||||
* <li>Clears the {@link GlbDiagnosticRegistry}</li>
|
||||
* <li>Iterates all {@link DataDrivenItemDefinition}s</li>
|
||||
* <li>For each definition with a model location, attempts to open the GLB
|
||||
* resource and runs {@link GlbValidator#validate} on it</li>
|
||||
* <li>Records results into the diagnostic registry and logs a summary</li>
|
||||
* </ol>
|
||||
*/
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public class GlbValidationReloadListener
|
||||
extends SimplePreparableReloadListener<Void>
|
||||
{
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("GltfValidation");
|
||||
|
||||
@Override
|
||||
protected Void prepare(
|
||||
ResourceManager resourceManager,
|
||||
ProfilerFiller profiler
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void apply(
|
||||
Void nothing,
|
||||
ResourceManager resourceManager,
|
||||
ProfilerFiller profiler
|
||||
) {
|
||||
GlbDiagnosticRegistry.clear();
|
||||
|
||||
Collection<DataDrivenItemDefinition> definitions =
|
||||
DataDrivenItemRegistry.getAll();
|
||||
|
||||
if (definitions.isEmpty()) {
|
||||
LOGGER.warn(
|
||||
"[GltfValidation] No data-driven item definitions found — skipping GLB validation"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
int passed = 0;
|
||||
int withWarnings = 0;
|
||||
int withErrors = 0;
|
||||
|
||||
for (DataDrivenItemDefinition def : definitions) {
|
||||
ResourceLocation modelLoc = def.modelLocation();
|
||||
if (modelLoc == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Optional<Resource> resourceOpt =
|
||||
resourceManager.getResource(modelLoc);
|
||||
|
||||
if (resourceOpt.isEmpty()) {
|
||||
// GLB file not found in any resource pack
|
||||
GlbValidationResult missingResult = GlbValidationResult.of(
|
||||
modelLoc,
|
||||
List.of(new GlbDiagnostic(
|
||||
modelLoc,
|
||||
def.id(),
|
||||
GlbDiagnostic.Severity.ERROR,
|
||||
"MISSING_GLB",
|
||||
"GLB file not found: " + modelLoc
|
||||
+ " (referenced by item " + def.id() + ")"
|
||||
))
|
||||
);
|
||||
GlbDiagnosticRegistry.addResult(missingResult);
|
||||
withErrors++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try (InputStream stream = resourceOpt.get().open()) {
|
||||
GlbValidationResult result =
|
||||
GlbValidator.validate(stream, modelLoc);
|
||||
GlbDiagnosticRegistry.addResult(result);
|
||||
|
||||
if (!result.passed()) {
|
||||
withErrors++;
|
||||
} else if (hasWarnings(result)) {
|
||||
withWarnings++;
|
||||
} else {
|
||||
passed++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
GlbValidationResult errorResult = GlbValidationResult.of(
|
||||
modelLoc,
|
||||
List.of(new GlbDiagnostic(
|
||||
modelLoc,
|
||||
def.id(),
|
||||
GlbDiagnostic.Severity.ERROR,
|
||||
"GLB_READ_ERROR",
|
||||
"Failed to read GLB file: " + e.getMessage()
|
||||
))
|
||||
);
|
||||
GlbDiagnosticRegistry.addResult(errorResult);
|
||||
withErrors++;
|
||||
}
|
||||
}
|
||||
|
||||
int total = passed + withWarnings + withErrors;
|
||||
LOGGER.info(
|
||||
"[GltfValidation] Validated {} GLBs: {} passed, {} with warnings, {} with errors",
|
||||
total, passed, withWarnings, withErrors
|
||||
);
|
||||
|
||||
// Show toast notification for errors so artists don't have to check logs
|
||||
if (withErrors > 0) {
|
||||
int errorCount = withErrors;
|
||||
Minecraft.getInstance().tell(() ->
|
||||
Minecraft.getInstance().getToasts().addToast(
|
||||
SystemToast.multiline(
|
||||
Minecraft.getInstance(),
|
||||
SystemToast.SystemToastIds.PACK_LOAD_FAILURE,
|
||||
Component.literal("TiedUp! GLB Validation"),
|
||||
Component.literal(errorCount + " model(s) have errors. Run /tiedup validate")
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasWarnings(GlbValidationResult result) {
|
||||
return result.diagnostics().stream()
|
||||
.anyMatch(d -> d.severity() == GlbDiagnostic.Severity.WARNING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.tiedup.remake.client.gltf.diagnostic;
|
||||
|
||||
import java.util.List;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
|
||||
/**
|
||||
* Validation result for a single GLB file.
|
||||
*/
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public record GlbValidationResult(
|
||||
ResourceLocation source,
|
||||
List<GlbDiagnostic> diagnostics,
|
||||
boolean passed
|
||||
) {
|
||||
/** Create a result, computing passed from diagnostics. */
|
||||
public static GlbValidationResult of(
|
||||
ResourceLocation source,
|
||||
List<GlbDiagnostic> diagnostics
|
||||
) {
|
||||
boolean passed = diagnostics.stream()
|
||||
.noneMatch(d -> d.severity() == GlbDiagnostic.Severity.ERROR);
|
||||
return new GlbValidationResult(source, List.copyOf(diagnostics), passed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
package com.tiedup.remake.client.gltf.diagnostic;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.tiedup.remake.client.gltf.GltfBoneMapper;
|
||||
import com.tiedup.remake.client.gltf.diagnostic.GlbDiagnostic.Severity;
|
||||
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
|
||||
/**
|
||||
* Lightweight structural validator for GLB files.
|
||||
*
|
||||
* <p>Reads only the 12-byte header and the JSON chunk (first chunk) — never
|
||||
* touches the binary mesh data. Produces a list of {@link GlbDiagnostic}
|
||||
* findings that can range from hard errors (invalid header, missing skin) to
|
||||
* informational notes (custom bone names).</p>
|
||||
*
|
||||
* <p>All methods are static; the class cannot be instantiated.</p>
|
||||
*/
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public final class GlbValidator {
|
||||
|
||||
private static final int GLB_MAGIC = 0x46546C67; // "glTF"
|
||||
private static final int GLB_VERSION = 2;
|
||||
private static final int CHUNK_JSON = 0x4E4F534A; // "JSON"
|
||||
|
||||
private GlbValidator() {}
|
||||
|
||||
/**
|
||||
* Validate a GLB file by reading its header and JSON chunk.
|
||||
*
|
||||
* @param input the raw GLB byte stream (will be fully consumed)
|
||||
* @param source resource location of the GLB file (used in diagnostics)
|
||||
* @return a validation result containing all findings
|
||||
*/
|
||||
public static GlbValidationResult validate(
|
||||
InputStream input,
|
||||
ResourceLocation source
|
||||
) {
|
||||
List<GlbDiagnostic> diagnostics = new ArrayList<>();
|
||||
|
||||
JsonObject root;
|
||||
try {
|
||||
root = readJsonChunk(input, source, diagnostics);
|
||||
} catch (Exception e) {
|
||||
diagnostics.add(new GlbDiagnostic(
|
||||
source, null, Severity.ERROR, "INVALID_GLB_HEADER",
|
||||
"Failed to read GLB header or JSON chunk: " + e.getMessage()
|
||||
));
|
||||
return GlbValidationResult.of(source, diagnostics);
|
||||
}
|
||||
|
||||
// If readJsonChunk added an ERROR, root will be null
|
||||
if (root == null) {
|
||||
return GlbValidationResult.of(source, diagnostics);
|
||||
}
|
||||
|
||||
validateSkins(root, source, diagnostics);
|
||||
validateMeshes(root, source, diagnostics);
|
||||
validateAnimations(root, source, diagnostics);
|
||||
|
||||
return GlbValidationResult.of(source, diagnostics);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cross-reference a validation result against an item definition.
|
||||
* Stub for future checks (e.g. region/bone coverage, animation name
|
||||
* matching against definition-declared poses).
|
||||
*
|
||||
* @param result the prior structural validation result
|
||||
* @param def the item definition to cross-reference
|
||||
* @return additional diagnostics (currently empty)
|
||||
*/
|
||||
public static List<GlbDiagnostic> validateAgainstDefinition(
|
||||
GlbValidationResult result,
|
||||
DataDrivenItemDefinition def
|
||||
) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Header + JSON chunk extraction //
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
/** Maximum GLB file size the validator will accept (50 MB). */
|
||||
private static final long MAX_GLB_SIZE = 50L * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Parse the GLB header and extract the JSON chunk root object.
|
||||
* Returns null and adds ERROR diagnostics on failure.
|
||||
*/
|
||||
private static JsonObject readJsonChunk(
|
||||
InputStream input,
|
||||
ResourceLocation source,
|
||||
List<GlbDiagnostic> diagnostics
|
||||
) throws Exception {
|
||||
// Read the 12-byte header first to check totalLength before
|
||||
// committing to readAllBytes (OOM guard for malformed GLBs).
|
||||
byte[] header = input.readNBytes(12);
|
||||
if (header.length < 12) {
|
||||
diagnostics.add(new GlbDiagnostic(
|
||||
source, null, Severity.ERROR, "INVALID_GLB_HEADER",
|
||||
"File too small to contain a GLB header (" + header.length + " bytes)"
|
||||
));
|
||||
return null;
|
||||
}
|
||||
|
||||
ByteBuffer headerBuf = ByteBuffer.wrap(header).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
// -- Header (12 bytes) --
|
||||
int magic = headerBuf.getInt();
|
||||
if (magic != GLB_MAGIC) {
|
||||
diagnostics.add(new GlbDiagnostic(
|
||||
source, null, Severity.ERROR, "INVALID_GLB_HEADER",
|
||||
String.format("Bad magic number: 0x%08X (expected 0x%08X)", magic, GLB_MAGIC)
|
||||
));
|
||||
return null;
|
||||
}
|
||||
|
||||
int version = headerBuf.getInt();
|
||||
if (version != GLB_VERSION) {
|
||||
diagnostics.add(new GlbDiagnostic(
|
||||
source, null, Severity.ERROR, "INVALID_GLB_HEADER",
|
||||
"Unsupported GLB version " + version + " (expected " + GLB_VERSION + ")"
|
||||
));
|
||||
return null;
|
||||
}
|
||||
|
||||
int totalLength = headerBuf.getInt();
|
||||
|
||||
// OOM guard: reject files that declare a size exceeding the cap.
|
||||
// totalLength is a signed int, so treat negative values as > 2 GB.
|
||||
if (totalLength < 0 || Integer.toUnsignedLong(totalLength) > MAX_GLB_SIZE) {
|
||||
diagnostics.add(new GlbDiagnostic(
|
||||
source, null, Severity.ERROR, "GLB_TOO_LARGE",
|
||||
"GLB declares totalLength=" + Integer.toUnsignedLong(totalLength)
|
||||
+ " bytes which exceeds the " + (MAX_GLB_SIZE / (1024 * 1024))
|
||||
+ " MB safety cap — aborting validation"
|
||||
));
|
||||
return null;
|
||||
}
|
||||
|
||||
// Now read the remainder (totalLength includes the 12-byte header)
|
||||
int remaining = totalLength - 12;
|
||||
if (remaining < 0) {
|
||||
diagnostics.add(new GlbDiagnostic(
|
||||
source, null, Severity.ERROR, "INVALID_GLB_HEADER",
|
||||
"GLB totalLength (" + totalLength + ") is smaller than the header itself"
|
||||
));
|
||||
return null;
|
||||
}
|
||||
byte[] restBytes = input.readNBytes(remaining);
|
||||
ByteBuffer buf = ByteBuffer.wrap(restBytes).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
// -- First chunk must be JSON --
|
||||
if (buf.remaining() < 8) {
|
||||
diagnostics.add(new GlbDiagnostic(
|
||||
source, null, Severity.ERROR, "INVALID_GLB_HEADER",
|
||||
"File truncated: no chunk header after GLB header"
|
||||
));
|
||||
return null;
|
||||
}
|
||||
|
||||
int jsonChunkLength = buf.getInt();
|
||||
int jsonChunkType = buf.getInt();
|
||||
|
||||
if (jsonChunkType != CHUNK_JSON) {
|
||||
diagnostics.add(new GlbDiagnostic(
|
||||
source, null, Severity.ERROR, "INVALID_GLB_HEADER",
|
||||
String.format(
|
||||
"First chunk is not JSON: type 0x%08X (expected 0x%08X)",
|
||||
jsonChunkType, CHUNK_JSON
|
||||
)
|
||||
));
|
||||
return null;
|
||||
}
|
||||
|
||||
if (buf.remaining() < jsonChunkLength) {
|
||||
diagnostics.add(new GlbDiagnostic(
|
||||
source, null, Severity.ERROR, "INVALID_GLB_HEADER",
|
||||
"File truncated: JSON chunk declares " + jsonChunkLength
|
||||
+ " bytes but only " + buf.remaining() + " remain"
|
||||
));
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] jsonBytes = new byte[jsonChunkLength];
|
||||
buf.get(jsonBytes);
|
||||
String jsonStr = new String(jsonBytes, StandardCharsets.UTF_8);
|
||||
|
||||
return JsonParser.parseString(jsonStr).getAsJsonObject();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Skin / bone validation //
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
private static void validateSkins(
|
||||
JsonObject root,
|
||||
ResourceLocation source,
|
||||
List<GlbDiagnostic> diagnostics
|
||||
) {
|
||||
if (!root.has("skins")) {
|
||||
diagnostics.add(new GlbDiagnostic(
|
||||
source, null, Severity.ERROR, "NO_SKINS",
|
||||
"GLB has no 'skins' array — skinned mesh rendering requires at least one skin"
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
JsonArray skins = root.getAsJsonArray("skins");
|
||||
if (skins.size() == 0) {
|
||||
diagnostics.add(new GlbDiagnostic(
|
||||
source, null, Severity.ERROR, "NO_SKINS",
|
||||
"GLB 'skins' array is empty — skinned mesh rendering requires at least one skin"
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate bones in the first skin
|
||||
JsonObject skin = skins.get(0).getAsJsonObject();
|
||||
if (!skin.has("joints")) {
|
||||
return;
|
||||
}
|
||||
|
||||
JsonArray joints = skin.getAsJsonArray("joints");
|
||||
JsonArray nodes = root.has("nodes") ? root.getAsJsonArray("nodes") : null;
|
||||
if (nodes == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int j = 0; j < joints.size(); j++) {
|
||||
int nodeIdx = joints.get(j).getAsInt();
|
||||
if (nodeIdx < 0 || nodeIdx >= nodes.size()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
JsonObject node = nodes.get(nodeIdx).getAsJsonObject();
|
||||
if (!node.has("name")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String rawName = node.get("name").getAsString();
|
||||
// Strip armature prefix (e.g. "MyRig|body" -> "body")
|
||||
String boneName = rawName.contains("|")
|
||||
? rawName.substring(rawName.lastIndexOf('|') + 1)
|
||||
: rawName;
|
||||
|
||||
if (GltfBoneMapper.isKnownBone(boneName)) {
|
||||
// OK — known bone, no diagnostic needed
|
||||
continue;
|
||||
}
|
||||
|
||||
String suggestion = GltfBoneMapper.suggestBoneName(boneName);
|
||||
if (suggestion != null) {
|
||||
diagnostics.add(new GlbDiagnostic(
|
||||
source, null, Severity.WARNING, "BONE_TYPO_SUGGESTION",
|
||||
"Bone '" + boneName + "' is not recognized — did you mean '"
|
||||
+ suggestion + "'?"
|
||||
));
|
||||
} else {
|
||||
diagnostics.add(new GlbDiagnostic(
|
||||
source, null, Severity.INFO, "UNKNOWN_BONE",
|
||||
"Bone '" + boneName + "' is not a standard MC bone (treated as custom bone)"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Mesh validation //
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
private static void validateMeshes(
|
||||
JsonObject root,
|
||||
ResourceLocation source,
|
||||
List<GlbDiagnostic> diagnostics
|
||||
) {
|
||||
if (!root.has("meshes")) {
|
||||
return;
|
||||
}
|
||||
|
||||
JsonArray meshes = root.getAsJsonArray("meshes");
|
||||
|
||||
// Count non-Player meshes
|
||||
int nonPlayerCount = 0;
|
||||
for (int mi = 0; mi < meshes.size(); mi++) {
|
||||
JsonObject mesh = meshes.get(mi).getAsJsonObject();
|
||||
String meshName = mesh.has("name")
|
||||
? mesh.get("name").getAsString()
|
||||
: "";
|
||||
if (!"Player".equals(meshName)) {
|
||||
nonPlayerCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (nonPlayerCount > 1) {
|
||||
diagnostics.add(new GlbDiagnostic(
|
||||
source, null, Severity.WARNING, "MULTI_MESH_AMBIGUITY",
|
||||
nonPlayerCount + " non-Player meshes found — name your item mesh 'Item' "
|
||||
+ "for explicit selection"
|
||||
));
|
||||
}
|
||||
|
||||
// Check WEIGHTS_0 on the mesh that GlbParser would actually select:
|
||||
// 1) mesh named "Item", 2) last non-Player mesh.
|
||||
JsonObject targetMesh = null;
|
||||
String targetMeshName = null;
|
||||
for (int mi = 0; mi < meshes.size(); mi++) {
|
||||
JsonObject mesh = meshes.get(mi).getAsJsonObject();
|
||||
String meshName = mesh.has("name")
|
||||
? mesh.get("name").getAsString()
|
||||
: "";
|
||||
if ("Item".equals(meshName)) {
|
||||
targetMesh = mesh;
|
||||
targetMeshName = meshName;
|
||||
break; // Convention match — same as GlbParser
|
||||
}
|
||||
if (!"Player".equals(meshName)) {
|
||||
targetMesh = mesh;
|
||||
targetMeshName = meshName;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetMesh != null && targetMesh.has("primitives")) {
|
||||
JsonArray primitives = targetMesh.getAsJsonArray("primitives");
|
||||
if (primitives.size() > 0) {
|
||||
JsonObject firstPrimitive = primitives.get(0).getAsJsonObject();
|
||||
if (firstPrimitive.has("attributes")) {
|
||||
JsonObject attributes = firstPrimitive.getAsJsonObject("attributes");
|
||||
if (!attributes.has("WEIGHTS_0")) {
|
||||
String meshLabel = targetMeshName != null
|
||||
? "'" + targetMeshName + "'"
|
||||
: "(unnamed)";
|
||||
diagnostics.add(new GlbDiagnostic(
|
||||
source, null, Severity.WARNING, "NO_WEIGHTS",
|
||||
"Selected mesh " + meshLabel
|
||||
+ " first primitive has no WEIGHTS_0 attribute "
|
||||
+ "— skinning will not work correctly"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Animation validation //
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
private static void validateAnimations(
|
||||
JsonObject root,
|
||||
ResourceLocation source,
|
||||
List<GlbDiagnostic> diagnostics
|
||||
) {
|
||||
if (!root.has("animations")) {
|
||||
diagnostics.add(new GlbDiagnostic(
|
||||
source, null, Severity.WARNING, "NO_IDLE_ANIMATION",
|
||||
"GLB has no 'animations' array — no Idle animation found"
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
JsonArray animations = root.getAsJsonArray("animations");
|
||||
if (animations.size() == 0) {
|
||||
diagnostics.add(new GlbDiagnostic(
|
||||
source, null, Severity.WARNING, "NO_IDLE_ANIMATION",
|
||||
"GLB 'animations' array is empty — no Idle animation found"
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
boolean hasIdle = false;
|
||||
for (int ai = 0; ai < animations.size(); ai++) {
|
||||
JsonObject anim = animations.get(ai).getAsJsonObject();
|
||||
if (!anim.has("name")) {
|
||||
continue;
|
||||
}
|
||||
String animName = anim.get("name").getAsString();
|
||||
// Strip armature prefix (e.g. "Armature|Idle" -> "Idle")
|
||||
if (animName.contains("|")) {
|
||||
animName = animName.substring(animName.lastIndexOf('|') + 1);
|
||||
}
|
||||
if ("Idle".equals(animName)) {
|
||||
hasIdle = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasIdle) {
|
||||
diagnostics.add(new GlbDiagnostic(
|
||||
source, null, Severity.WARNING, "NO_IDLE_ANIMATION",
|
||||
"No animation named 'Idle' found — the default rest pose may not display correctly"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,7 +134,7 @@ public class ProgressOverlay {
|
||||
*/
|
||||
private static ProgressInfo getActiveProgress(PlayerBindState state) {
|
||||
// Check client tying task
|
||||
PlayerStateTask tyingTask = state.getClientTyingTask();
|
||||
PlayerStateTask tyingTask = state.tasks().getClientTyingTask();
|
||||
if (tyingTask != null && !tyingTask.isOutdated()) {
|
||||
float progress = tyingTask.getProgress();
|
||||
Component text = getTyingText(tyingTask);
|
||||
@@ -142,7 +142,7 @@ public class ProgressOverlay {
|
||||
}
|
||||
|
||||
// Check client untying task
|
||||
PlayerStateTask untyingTask = state.getClientUntyingTask();
|
||||
PlayerStateTask untyingTask = state.tasks().getClientUntyingTask();
|
||||
if (untyingTask != null && !untyingTask.isOutdated()) {
|
||||
float progress = untyingTask.getProgress();
|
||||
Component text = getUntyingText(untyingTask);
|
||||
@@ -150,7 +150,7 @@ public class ProgressOverlay {
|
||||
}
|
||||
|
||||
// Check client feeding task
|
||||
PlayerStateTask feedingTask = state.getClientFeedingTask();
|
||||
PlayerStateTask feedingTask = state.tasks().getClientFeedingTask();
|
||||
if (feedingTask != null && !feedingTask.isOutdated()) {
|
||||
float progress = feedingTask.getProgress();
|
||||
Component text = getFeedingText(feedingTask);
|
||||
|
||||
@@ -198,12 +198,12 @@ public abstract class BaseAdjustmentScreen extends BaseScreen {
|
||||
int totalWidth = buttonWidth * 3 + MARGIN_S * 2;
|
||||
int actionStartX = this.leftPos + (this.imageWidth - totalWidth) / 2;
|
||||
|
||||
resetButton = Button.builder(Component.literal("0"), b -> resetValue())
|
||||
resetButton = Button.builder(Component.translatable("gui.tiedup.adjustment.btn.reset"), b -> resetValue())
|
||||
.bounds(actionStartX, actionY, buttonWidth, BUTTON_HEIGHT)
|
||||
.build();
|
||||
this.addRenderableWidget(resetButton);
|
||||
|
||||
decrementButton = Button.builder(Component.literal("-0.25"), b ->
|
||||
decrementButton = Button.builder(Component.translatable("gui.tiedup.adjustment.btn.decrement"), b ->
|
||||
slider.decrement()
|
||||
)
|
||||
.bounds(
|
||||
@@ -215,7 +215,7 @@ public abstract class BaseAdjustmentScreen extends BaseScreen {
|
||||
.build();
|
||||
this.addRenderableWidget(decrementButton);
|
||||
|
||||
incrementButton = Button.builder(Component.literal("+0.25"), b ->
|
||||
incrementButton = Button.builder(Component.translatable("gui.tiedup.adjustment.btn.increment"), b ->
|
||||
slider.increment()
|
||||
)
|
||||
.bounds(
|
||||
@@ -235,14 +235,14 @@ public abstract class BaseAdjustmentScreen extends BaseScreen {
|
||||
int totalWidth = buttonWidth * 3 + MARGIN_S * 2;
|
||||
int scaleStartX = this.leftPos + (this.imageWidth - totalWidth) / 2;
|
||||
|
||||
scaleResetButton = Button.builder(Component.literal("1x"), b ->
|
||||
scaleResetButton = Button.builder(Component.translatable("gui.tiedup.adjustment.btn.scale_reset"), b ->
|
||||
applyScale(AdjustmentHelper.DEFAULT_SCALE)
|
||||
)
|
||||
.bounds(scaleStartX, scaleY, buttonWidth, BUTTON_HEIGHT)
|
||||
.build();
|
||||
this.addRenderableWidget(scaleResetButton);
|
||||
|
||||
scaleDecrementButton = Button.builder(Component.literal("-0.1"), b ->
|
||||
scaleDecrementButton = Button.builder(Component.translatable("gui.tiedup.adjustment.btn.scale_decrement"), b ->
|
||||
applyScale(currentScaleValue - AdjustmentHelper.SCALE_STEP)
|
||||
)
|
||||
.bounds(
|
||||
@@ -254,7 +254,7 @@ public abstract class BaseAdjustmentScreen extends BaseScreen {
|
||||
.build();
|
||||
this.addRenderableWidget(scaleDecrementButton);
|
||||
|
||||
scaleIncrementButton = Button.builder(Component.literal("+0.1"), b ->
|
||||
scaleIncrementButton = Button.builder(Component.translatable("gui.tiedup.adjustment.btn.scale_increment"), b ->
|
||||
applyScale(currentScaleValue + AdjustmentHelper.SCALE_STEP)
|
||||
)
|
||||
.bounds(
|
||||
|
||||
@@ -565,7 +565,7 @@ public class CellManagerScreen extends BaseScreen {
|
||||
if (cell.prisoners.isEmpty()) {
|
||||
graphics.drawString(
|
||||
this.font,
|
||||
Component.literal(" \u2514\u2500 ")
|
||||
Component.translatable("gui.tiedup.cell_manager.tree_prefix")
|
||||
.append(
|
||||
Component.translatable(
|
||||
"gui.tiedup.cell_manager.label.empty"
|
||||
|
||||
@@ -548,12 +548,9 @@ public class CommandWandScreen extends Screen {
|
||||
}
|
||||
|
||||
private void addFollowDistanceCycleButton(int x, int y, int width) {
|
||||
String modeText = switch (followDistanceMode) {
|
||||
case "HEEL" -> "H";
|
||||
case "CLOSE" -> "C";
|
||||
case "FAR" -> "F";
|
||||
default -> "?";
|
||||
};
|
||||
Component modeLabel = Component.translatable(
|
||||
"gui.tiedup.command_wand.follow_distance.abbrev." + followDistanceMode.toLowerCase()
|
||||
);
|
||||
|
||||
Component tooltip = Component.translatable(
|
||||
"gui.tiedup.command_wand.follow_distance.tooltip"
|
||||
@@ -571,7 +568,7 @@ public class CommandWandScreen extends Screen {
|
||||
)
|
||||
);
|
||||
|
||||
Button btn = Button.builder(Component.literal(modeText), b ->
|
||||
Button btn = Button.builder(modeLabel, b ->
|
||||
cycleFollowDistance()
|
||||
)
|
||||
.bounds(x, y, width, BUTTON_HEIGHT)
|
||||
@@ -857,7 +854,7 @@ public class CommandWandScreen extends Screen {
|
||||
) {
|
||||
graphics.renderTooltip(
|
||||
this.font,
|
||||
Component.literal((int) hunger + "%"),
|
||||
Component.translatable("gui.tiedup.command_wand.percent", (int) hunger),
|
||||
mouseX,
|
||||
mouseY
|
||||
);
|
||||
@@ -871,7 +868,7 @@ public class CommandWandScreen extends Screen {
|
||||
) {
|
||||
graphics.renderTooltip(
|
||||
this.font,
|
||||
Component.literal((int) rest + "%"),
|
||||
Component.translatable("gui.tiedup.command_wand.percent", (int) rest),
|
||||
mouseX,
|
||||
mouseY
|
||||
);
|
||||
|
||||
@@ -216,24 +216,24 @@ public class MerchantTradingScreen extends BaseScreen {
|
||||
int goldIngots = countItemInInventory(player, Items.GOLD_INGOT);
|
||||
int goldNuggets = countItemInInventory(player, Items.GOLD_NUGGET);
|
||||
|
||||
Component goldText = Component.literal("Your Gold: ")
|
||||
Component goldText = Component.translatable("gui.tiedup.merchant.your_gold")
|
||||
.append(
|
||||
Component.literal(goldIngots + "x ").withStyle(style ->
|
||||
Component.translatable("gui.tiedup.merchant.gold_amount", goldIngots).withStyle(style ->
|
||||
style.withColor(0xFFFFD700)
|
||||
)
|
||||
)
|
||||
.append(
|
||||
Component.literal("⚜ ").withStyle(style ->
|
||||
Component.translatable("gui.tiedup.merchant.gold_icon").withStyle(style ->
|
||||
style.withColor(0xFFFFD700)
|
||||
)
|
||||
)
|
||||
.append(
|
||||
Component.literal("+ " + goldNuggets + "x ").withStyle(
|
||||
Component.translatable("gui.tiedup.merchant.nugget_amount", goldNuggets).withStyle(
|
||||
style -> style.withColor(0xFFFFA500)
|
||||
)
|
||||
)
|
||||
.append(
|
||||
Component.literal("✦").withStyle(style ->
|
||||
Component.translatable("gui.tiedup.merchant.nugget_icon").withStyle(style ->
|
||||
style.withColor(0xFFFFA500)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -53,7 +53,7 @@ public class NpcInventoryScreen
|
||||
Inventory playerInventory,
|
||||
Component title
|
||||
) {
|
||||
super(menu, playerInventory, Component.literal(menu.getNpcName()));
|
||||
super(menu, playerInventory, Component.translatable("gui.tiedup.npc_inventory.title_name", menu.getNpcName()));
|
||||
// Calculate rows from NPC inventory size
|
||||
this.npcRows = (menu.getNpcSlotCount() + 8) / 9;
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ public class ActionPanel extends AbstractWidget {
|
||||
private int hoveredIndex = -1;
|
||||
|
||||
public ActionPanel(int x, int y, int width, int height) {
|
||||
super(x, y, width, height, Component.literal("Actions"));
|
||||
super(x, y, width, height, Component.translatable("gui.tiedup.action_panel"));
|
||||
}
|
||||
|
||||
public void setMode(ScreenMode mode) {
|
||||
|
||||
@@ -51,7 +51,7 @@ public class ItemPickerOverlay extends AbstractWidget {
|
||||
private int screenHeight;
|
||||
|
||||
public ItemPickerOverlay() {
|
||||
super(0, 0, 0, 0, Component.literal("Item Picker"));
|
||||
super(0, 0, 0, 0, Component.translatable("gui.tiedup.item_picker"));
|
||||
this.active = false;
|
||||
this.visible = false;
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ public class RegionTabBar extends AbstractWidget {
|
||||
private LivingEntity targetEntity;
|
||||
|
||||
public RegionTabBar(int x, int y, int width) {
|
||||
super(x, y, width, TAB_HEIGHT, Component.literal("Tab Bar"));
|
||||
super(x, y, width, TAB_HEIGHT, Component.translatable("gui.tiedup.tab_bar.label"));
|
||||
}
|
||||
|
||||
public void setOnTabChanged(Consumer<BodyTab> callback) {
|
||||
|
||||
@@ -40,7 +40,7 @@ public class StatusBarWidget extends AbstractWidget {
|
||||
private static final int CLOSE_BTN_HEIGHT = 22;
|
||||
|
||||
public StatusBarWidget(int x, int y, int width, int height) {
|
||||
super(x, y, width, height, Component.literal("Status Bar"));
|
||||
super(x, y, width, height, Component.translatable("gui.tiedup.status_bar"));
|
||||
}
|
||||
|
||||
public void setMode(ActionPanel.ScreenMode mode) {
|
||||
|
||||
@@ -21,7 +21,7 @@ import org.joml.Matrix4f;
|
||||
*/
|
||||
public class NpcFishingBobberRenderer extends EntityRenderer<NpcFishingBobber> {
|
||||
|
||||
private static final ResourceLocation TEXTURE = new ResourceLocation(
|
||||
private static final ResourceLocation TEXTURE = ResourceLocation.parse(
|
||||
"textures/entity/fishing_hook.png"
|
||||
);
|
||||
private static final RenderType RENDER_TYPE = RenderType.entityCutout(
|
||||
|
||||
@@ -96,15 +96,15 @@ public class KidnapSetCommand {
|
||||
|
||||
// Collars (data-driven)
|
||||
ItemStack classicCollars = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(
|
||||
new net.minecraft.resources.ResourceLocation("tiedup", "classic_collar"));
|
||||
net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("tiedup", "classic_collar"));
|
||||
classicCollars.setCount(4);
|
||||
given += giveItem(player, classicCollars);
|
||||
ItemStack shockCollars = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(
|
||||
new net.minecraft.resources.ResourceLocation("tiedup", "shock_collar"));
|
||||
net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("tiedup", "shock_collar"));
|
||||
shockCollars.setCount(2);
|
||||
given += giveItem(player, shockCollars);
|
||||
ItemStack gpsCollars = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(
|
||||
new net.minecraft.resources.ResourceLocation("tiedup", "gps_collar"));
|
||||
net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("tiedup", "gps_collar"));
|
||||
gpsCollars.setCount(2);
|
||||
given += giveItem(player, gpsCollars);
|
||||
|
||||
@@ -160,7 +160,7 @@ public class KidnapSetCommand {
|
||||
private static int giveDataDrivenItems(ServerPlayer player, String itemName, int count) {
|
||||
int given = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
ItemStack stack = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", itemName));
|
||||
ItemStack stack = DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", itemName));
|
||||
if (!stack.isEmpty()) {
|
||||
giveItem(player, stack);
|
||||
given++;
|
||||
|
||||
@@ -499,7 +499,7 @@ public class NPCCommand {
|
||||
|
||||
npc.equip(
|
||||
BodyRegionV2.ARMS,
|
||||
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ropes"))
|
||||
DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "ropes"))
|
||||
);
|
||||
context
|
||||
.getSource()
|
||||
@@ -530,7 +530,7 @@ public class NPCCommand {
|
||||
|
||||
npc.equip(
|
||||
BodyRegionV2.MOUTH,
|
||||
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "cloth_gag"))
|
||||
DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "cloth_gag"))
|
||||
);
|
||||
context
|
||||
.getSource()
|
||||
@@ -563,7 +563,7 @@ public class NPCCommand {
|
||||
|
||||
npc.equip(
|
||||
BodyRegionV2.EYES,
|
||||
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_blindfold"))
|
||||
DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "classic_blindfold"))
|
||||
);
|
||||
context
|
||||
.getSource()
|
||||
@@ -596,7 +596,7 @@ public class NPCCommand {
|
||||
|
||||
npc.equip(
|
||||
BodyRegionV2.NECK,
|
||||
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_collar"))
|
||||
DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "classic_collar"))
|
||||
);
|
||||
context
|
||||
.getSource()
|
||||
@@ -647,11 +647,11 @@ public class NPCCommand {
|
||||
com.tiedup.remake.entities.AbstractTiedUpNpc npcEntity
|
||||
) {
|
||||
npcEntity.applyBondage(
|
||||
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ropes")),
|
||||
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "cloth_gag")),
|
||||
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_blindfold")),
|
||||
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_earplugs")),
|
||||
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_collar")),
|
||||
DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "ropes")),
|
||||
DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "cloth_gag")),
|
||||
DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "classic_blindfold")),
|
||||
DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "classic_earplugs")),
|
||||
DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "classic_collar")),
|
||||
ItemStack.EMPTY // No clothes
|
||||
);
|
||||
}
|
||||
|
||||
133
src/main/java/com/tiedup/remake/commands/ValidateGlbCommand.java
Normal file
133
src/main/java/com/tiedup/remake/commands/ValidateGlbCommand.java
Normal file
@@ -0,0 +1,133 @@
|
||||
package com.tiedup.remake.commands;
|
||||
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.tiedup.remake.client.gltf.diagnostic.GlbDiagnostic;
|
||||
import com.tiedup.remake.client.gltf.diagnostic.GlbDiagnosticRegistry;
|
||||
import com.tiedup.remake.client.gltf.diagnostic.GlbValidationResult;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
|
||||
/**
|
||||
* Client-only command: /tiedup validate [item_id]
|
||||
*
|
||||
* Displays GLB validation diagnostics in chat.
|
||||
* Registered via {@link net.minecraftforge.client.event.RegisterClientCommandsEvent}
|
||||
* on the FORGE bus (never on the server).
|
||||
*/
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public final class ValidateGlbCommand {
|
||||
|
||||
private ValidateGlbCommand() {}
|
||||
|
||||
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
|
||||
dispatcher.register(
|
||||
Commands.literal("tiedup")
|
||||
.then(Commands.literal("validate")
|
||||
.executes(ctx -> validateAll(ctx.getSource()))
|
||||
.then(Commands.argument("item_id", StringArgumentType.string())
|
||||
.executes(ctx -> validateOne(
|
||||
ctx.getSource(),
|
||||
StringArgumentType.getString(ctx, "item_id")
|
||||
))
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private static int validateAll(CommandSourceStack source) {
|
||||
var all = GlbDiagnosticRegistry.getAll();
|
||||
if (all.isEmpty()) {
|
||||
source.sendSuccess(
|
||||
() -> Component.literal(
|
||||
"[TiedUp] No GLB validation results. "
|
||||
+ "Try reloading resources (F3+T)."
|
||||
).withStyle(ChatFormatting.YELLOW),
|
||||
false
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int totalDiags = 0;
|
||||
for (GlbValidationResult result : all) {
|
||||
if (result.diagnostics().isEmpty()) continue;
|
||||
source.sendSuccess(
|
||||
() -> Component.literal("--- " + result.source() + " ---")
|
||||
.withStyle(
|
||||
result.passed()
|
||||
? ChatFormatting.GREEN
|
||||
: ChatFormatting.RED
|
||||
),
|
||||
false
|
||||
);
|
||||
for (GlbDiagnostic d : result.diagnostics()) {
|
||||
source.sendSuccess(() -> formatDiagnostic(d), false);
|
||||
totalDiags++;
|
||||
}
|
||||
}
|
||||
|
||||
int count = totalDiags;
|
||||
source.sendSuccess(
|
||||
() -> Component.literal(
|
||||
"[TiedUp] " + count + " diagnostic(s) across "
|
||||
+ GlbDiagnosticRegistry.size() + " GLBs"
|
||||
).withStyle(ChatFormatting.GRAY),
|
||||
false
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int validateOne(CommandSourceStack source, String itemId) {
|
||||
ResourceLocation loc = ResourceLocation.tryParse(itemId);
|
||||
if (loc == null) {
|
||||
source.sendFailure(
|
||||
Component.literal("Invalid resource location: " + itemId)
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (GlbValidationResult result : GlbDiagnosticRegistry.getAll()) {
|
||||
boolean match = result.source().equals(loc);
|
||||
if (!match) {
|
||||
match = result.diagnostics().stream()
|
||||
.anyMatch(d -> loc.equals(d.itemDef()));
|
||||
}
|
||||
if (match) {
|
||||
source.sendSuccess(
|
||||
() -> Component.literal("--- " + result.source() + " ---")
|
||||
.withStyle(
|
||||
result.passed()
|
||||
? ChatFormatting.GREEN
|
||||
: ChatFormatting.RED
|
||||
),
|
||||
false
|
||||
);
|
||||
for (GlbDiagnostic d : result.diagnostics()) {
|
||||
source.sendSuccess(() -> formatDiagnostic(d), false);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
source.sendFailure(
|
||||
Component.literal("No validation results for: " + itemId)
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static Component formatDiagnostic(GlbDiagnostic d) {
|
||||
ChatFormatting color = switch (d.severity()) {
|
||||
case ERROR -> ChatFormatting.RED;
|
||||
case WARNING -> ChatFormatting.YELLOW;
|
||||
case INFO -> ChatFormatting.GRAY;
|
||||
};
|
||||
return Component.literal(
|
||||
" [" + d.severity() + "] " + d.code() + ": " + d.message()
|
||||
).withStyle(color);
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,7 @@ public class AccessoryCommands {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ItemStack earplugs = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_earplugs"));
|
||||
ItemStack earplugs = DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "classic_earplugs"));
|
||||
state.putEarplugsOn(earplugs);
|
||||
CommandHelper.syncPlayerState(targetPlayer, state);
|
||||
|
||||
@@ -298,25 +298,25 @@ public class AccessoryCommands {
|
||||
int applied = 0;
|
||||
|
||||
if (!state.isTiedUp()) {
|
||||
ItemStack ropes = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ropes"));
|
||||
ItemStack ropes = DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "ropes"));
|
||||
state.putBindOn(ropes);
|
||||
applied++;
|
||||
}
|
||||
|
||||
if (!state.isGagged()) {
|
||||
ItemStack gag = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "cloth_gag"));
|
||||
ItemStack gag = DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "cloth_gag"));
|
||||
state.putGagOn(gag);
|
||||
applied++;
|
||||
}
|
||||
|
||||
if (!state.isBlindfolded()) {
|
||||
ItemStack blindfold = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_blindfold"));
|
||||
ItemStack blindfold = DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "classic_blindfold"));
|
||||
state.putBlindfoldOn(blindfold);
|
||||
applied++;
|
||||
}
|
||||
|
||||
if (!state.hasCollar()) {
|
||||
ItemStack collar = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(new net.minecraft.resources.ResourceLocation("tiedup", "classic_collar"));
|
||||
ItemStack collar = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("tiedup", "classic_collar"));
|
||||
if (
|
||||
context.getSource().getEntity() instanceof ServerPlayer executor
|
||||
) {
|
||||
@@ -327,7 +327,7 @@ public class AccessoryCommands {
|
||||
}
|
||||
|
||||
if (!state.hasEarplugs()) {
|
||||
ItemStack earplugs = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_earplugs"));
|
||||
ItemStack earplugs = DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "classic_earplugs"));
|
||||
state.putEarplugsOn(earplugs);
|
||||
applied++;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ public class BindCommands {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ItemStack ropes = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ropes"));
|
||||
ItemStack ropes = DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "ropes"));
|
||||
state.putBindOn(ropes);
|
||||
|
||||
CommandHelper.syncPlayerState(targetPlayer, state);
|
||||
|
||||
@@ -68,7 +68,7 @@ public class BlindfoldCommands {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ItemStack blindfold = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_blindfold"));
|
||||
ItemStack blindfold = DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "classic_blindfold"));
|
||||
state.putBlindfoldOn(blindfold);
|
||||
|
||||
CommandHelper.syncPlayerState(targetPlayer, state);
|
||||
|
||||
@@ -93,7 +93,7 @@ public class CollarCommands {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ItemStack collar = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(new net.minecraft.resources.ResourceLocation("tiedup", "classic_collar"));
|
||||
ItemStack collar = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("tiedup", "classic_collar"));
|
||||
|
||||
if (context.getSource().getEntity() instanceof ServerPlayer executor) {
|
||||
CollarHelper.addOwner(collar, executor);
|
||||
@@ -182,19 +182,19 @@ public class CollarCommands {
|
||||
|
||||
// First fully restrain
|
||||
if (!state.isTiedUp()) {
|
||||
ItemStack ropes = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ropes"));
|
||||
ItemStack ropes = DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "ropes"));
|
||||
state.putBindOn(ropes);
|
||||
}
|
||||
if (!state.isGagged()) {
|
||||
ItemStack gag = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "cloth_gag"));
|
||||
ItemStack gag = DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "cloth_gag"));
|
||||
state.putGagOn(gag);
|
||||
}
|
||||
if (!state.isBlindfolded()) {
|
||||
ItemStack blindfold = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_blindfold"));
|
||||
ItemStack blindfold = DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "classic_blindfold"));
|
||||
state.putBlindfoldOn(blindfold);
|
||||
}
|
||||
if (!state.hasCollar()) {
|
||||
ItemStack collar = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(new net.minecraft.resources.ResourceLocation("tiedup", "classic_collar"));
|
||||
ItemStack collar = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("tiedup", "classic_collar"));
|
||||
if (
|
||||
context.getSource().getEntity() instanceof ServerPlayer executor
|
||||
) {
|
||||
@@ -203,7 +203,7 @@ public class CollarCommands {
|
||||
state.putCollarOn(collar);
|
||||
}
|
||||
if (!state.hasEarplugs()) {
|
||||
ItemStack earplugs = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "classic_earplugs"));
|
||||
ItemStack earplugs = DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "classic_earplugs"));
|
||||
state.putEarplugsOn(earplugs);
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ public class GagCommands {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ItemStack gag = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "cloth_gag"));
|
||||
ItemStack gag = DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "cloth_gag"));
|
||||
state.putGagOn(gag);
|
||||
|
||||
CommandHelper.syncPlayerState(targetPlayer, state);
|
||||
|
||||
@@ -215,7 +215,7 @@ public class MCACompatEvents {
|
||||
IBondageState state = MCACompat.getKidnappedState(livingTarget);
|
||||
if (state != null) {
|
||||
// Priority: Untie > Ungag > Unblindfold > etc.
|
||||
// This mimics EntityDamsel logic or ItemBind.interactLivingEntity logic for untying
|
||||
// This mimics EntityDamsel logic or TyingInteractionHelper logic for untying
|
||||
|
||||
// No collar ownership check — any player can untie MCA villagers by design
|
||||
// (MCA villagers use a separate relationship system, not TiedUp collars)
|
||||
|
||||
@@ -9,16 +9,32 @@ import net.minecraftforge.common.ForgeConfigSpec;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Centralized accessor for mod settings that resolves the GameRules vs ModConfig priority.
|
||||
* Centralized accessor for all TiedUp! mod settings.
|
||||
*
|
||||
* Priority order:
|
||||
* 1. GameRules (if a world is loaded and rules are available) - these are per-world overrides
|
||||
* 2. ModConfig (if config is loaded) - these are the user's intended defaults
|
||||
* 3. Hardcoded fallback - safe defaults if neither system is available yet
|
||||
* <h2>Three-Tier Resolution</h2>
|
||||
* <ul>
|
||||
* <li><b>Tier 1 — GameRules</b> (per-world, overridable via {@code /gamerule}): ~24 settings.
|
||||
* These take priority when a world is loaded and {@code GameRules} is available.</li>
|
||||
* <li><b>Tier 2 — ModConfig</b> (server {@code .toml} defaults): ~78+ settings.
|
||||
* Read via {@link #safeGet} to tolerate early access before the config spec is loaded.</li>
|
||||
* <li><b>Tier 3 — Hardcoded fallbacks</b>: safe defaults returned when neither Tier 1
|
||||
* nor Tier 2 is available (e.g., during mod construction).</li>
|
||||
* </ul>
|
||||
*
|
||||
* This class exists because GameRules defaults (set at registration time before config loads)
|
||||
* can diverge from ModConfig defaults. Without this accessor, ModConfig spawn rate values
|
||||
* were completely ignored (BUG-001).
|
||||
* <h2>Rule</h2>
|
||||
* <p><b>All game code should read settings through this class, never directly from
|
||||
* {@link ModConfig} or {@link ModGameRules}.</b> This ensures a single choke-point
|
||||
* for defaults, null-safety, and future GameRule promotion of any setting.</p>
|
||||
*
|
||||
* <p>Settings that only exist in ModConfig (no GameRule equivalent) use parameterless
|
||||
* getters. Settings that have a GameRule override accept a {@code @Nullable GameRules}
|
||||
* parameter.</p>
|
||||
*
|
||||
* <p>Client-side config ({@code ModConfig.CLIENT.*}) is excluded — it is read-only
|
||||
* and single-source, so no accessor is needed.</p>
|
||||
*
|
||||
* @see ModConfig.ServerConfig
|
||||
* @see ModGameRules
|
||||
*/
|
||||
public class SettingsAccessor {
|
||||
|
||||
@@ -160,7 +176,7 @@ public class SettingsAccessor {
|
||||
* <p><b>BUG-003 fix:</b> Previously, {@code IHasResistance.getBaseResistance()}
|
||||
* called {@code ModGameRules.getResistance()} which only knew 4 types (rope, gag,
|
||||
* blindfold, collar) and returned hardcoded 100 for the other 10 types. Meanwhile
|
||||
* the old BindVariant.getResistance() read from ModConfig which had all 14 types.
|
||||
* the old {@code BindVariant.getResistance()} (now removed) read from ModConfig which had all 14 types.
|
||||
* This caused a display-vs-struggle desync (display: 250, struggle: 100).
|
||||
* Now both paths use this method.
|
||||
*
|
||||
@@ -531,6 +547,377 @@ public class SettingsAccessor {
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== Minigame Toggles ====================
|
||||
|
||||
/**
|
||||
* Check if the struggle mini-game is enabled.
|
||||
* When disabled, struggle actions are rejected or use instant logic.
|
||||
*
|
||||
* @return true if the struggle mini-game is enabled (default true)
|
||||
*/
|
||||
public static boolean isStruggleMiniGameEnabled() {
|
||||
return safeGet(() -> ModConfig.SERVER.struggleMiniGameEnabled.get(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the lockpick mini-game is enabled.
|
||||
* When disabled, lockpick actions are rejected or use instant logic.
|
||||
*
|
||||
* @return true if the lockpick mini-game is enabled (default true)
|
||||
*/
|
||||
public static boolean isLockpickMiniGameEnabled() {
|
||||
return safeGet(() -> ModConfig.SERVER.lockpickMiniGameEnabled.get(), true);
|
||||
}
|
||||
|
||||
// ==================== Combat & Weapons ====================
|
||||
|
||||
/**
|
||||
* Get the whip damage amount.
|
||||
*
|
||||
* @return Damage as float (default 2.0)
|
||||
*/
|
||||
public static float getWhipDamage() {
|
||||
return safeGet(() -> ModConfig.SERVER.whipDamage.get(), 2.0).floatValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resistance decrease per whip hit.
|
||||
*
|
||||
* @return Resistance decrease amount (default 15)
|
||||
*/
|
||||
public static int getWhipResistanceDecrease() {
|
||||
return safeGet(() -> ModConfig.SERVER.whipResistanceDecrease.get(), 15);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the taser stun effect duration in ticks.
|
||||
*
|
||||
* @return Duration in ticks (default 100 = 5 seconds)
|
||||
*/
|
||||
public static int getTaserStunDuration() {
|
||||
return safeGet(() -> ModConfig.SERVER.taserStunDuration.get(), 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the swim speed multiplier applied to tied players.
|
||||
*
|
||||
* @return Multiplier (0.0-1.0, default 0.5)
|
||||
*/
|
||||
public static double getTiedSwimSpeedMultiplier() {
|
||||
return safeGet(() -> ModConfig.SERVER.tiedSwimSpeedMultiplier.get(), 0.5);
|
||||
}
|
||||
|
||||
// ==================== Lockpick ====================
|
||||
|
||||
/**
|
||||
* Get the lockpick success chance (0-100).
|
||||
*
|
||||
* @return Success chance percentage (default 25)
|
||||
*/
|
||||
public static int getLockpickSuccessChance() {
|
||||
return safeGet(() -> ModConfig.SERVER.lockpickSuccessChance.get(), 25);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the lockpick jam chance (0.0-100.0).
|
||||
*
|
||||
* @return Jam chance percentage as double (default 2.5)
|
||||
*/
|
||||
public static double getLockpickJamChance() {
|
||||
return safeGet(() -> ModConfig.SERVER.lockpickJamChance.get(), 2.5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the lockpick break chance (0-100).
|
||||
*
|
||||
* @return Break chance percentage (default 15)
|
||||
*/
|
||||
public static int getLockpickBreakChance() {
|
||||
return safeGet(() -> ModConfig.SERVER.lockpickBreakChance.get(), 15);
|
||||
}
|
||||
|
||||
// ==================== Rag ====================
|
||||
|
||||
/**
|
||||
* Get the default rag wet time in ticks.
|
||||
*
|
||||
* @return Wet time in ticks (default 6000 = 5 minutes)
|
||||
*/
|
||||
public static int getRagWetTime() {
|
||||
return safeGet(() -> ModConfig.SERVER.ragWetTime.get(), 6000);
|
||||
}
|
||||
|
||||
// ==================== Arrows ====================
|
||||
|
||||
/**
|
||||
* Get the rope arrow bind chance for non-archer shooters (0-100).
|
||||
*
|
||||
* @return Bind chance percentage (default 50)
|
||||
*/
|
||||
public static int getRopeArrowBindChance() {
|
||||
return safeGet(() -> ModConfig.SERVER.ropeArrowBindChance.get(), 50);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the archer's base bind chance per arrow hit (0-100).
|
||||
*
|
||||
* @return Base bind chance percentage (default 10)
|
||||
*/
|
||||
public static int getArcherArrowBindChanceBase() {
|
||||
return safeGet(() -> ModConfig.SERVER.archerArrowBindChanceBase.get(), 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the additional bind chance per previous hit for archers (0-100).
|
||||
*
|
||||
* @return Per-hit chance increase percentage (default 10)
|
||||
*/
|
||||
public static int getArcherArrowBindChancePerHit() {
|
||||
return safeGet(() -> ModConfig.SERVER.archerArrowBindChancePerHit.get(), 10);
|
||||
}
|
||||
|
||||
// ==================== Kidnap Bomb ====================
|
||||
|
||||
/**
|
||||
* Get the kidnap bomb fuse duration in ticks.
|
||||
*
|
||||
* @return Fuse in ticks (default 80 = 4 seconds)
|
||||
*/
|
||||
public static int getKidnapBombFuse() {
|
||||
return safeGet(() -> ModConfig.SERVER.kidnapBombFuse.get(), 80);
|
||||
}
|
||||
|
||||
// ==================== Merchant ====================
|
||||
|
||||
/**
|
||||
* Get the merchant hostile cooldown duration in ticks.
|
||||
*
|
||||
* @return Duration in ticks (default 6000 = 5 minutes)
|
||||
*/
|
||||
public static int getMerchantHostileDuration() {
|
||||
return safeGet(() -> ModConfig.SERVER.merchantHostileDuration.get(), 6000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the minimum number of random trades a merchant generates.
|
||||
*
|
||||
* @return Minimum trade count (default 8)
|
||||
*/
|
||||
public static int getMerchantMinTrades() {
|
||||
return safeGet(() -> ModConfig.SERVER.merchantMinTrades.get(), 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum number of random trades a merchant generates.
|
||||
*
|
||||
* @return Maximum trade count (default 12)
|
||||
*/
|
||||
public static int getMerchantMaxTrades() {
|
||||
return safeGet(() -> ModConfig.SERVER.merchantMaxTrades.get(), 12);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tier 1 minimum price in nuggets.
|
||||
* @return Min price (default 9)
|
||||
*/
|
||||
public static int getTier1PriceMin() {
|
||||
return safeGet(() -> ModConfig.SERVER.tier1PriceMin.get(), 9);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tier 1 maximum price in nuggets.
|
||||
* @return Max price (default 18)
|
||||
*/
|
||||
public static int getTier1PriceMax() {
|
||||
return safeGet(() -> ModConfig.SERVER.tier1PriceMax.get(), 18);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tier 2 minimum price in nuggets.
|
||||
* @return Min price (default 27)
|
||||
*/
|
||||
public static int getTier2PriceMin() {
|
||||
return safeGet(() -> ModConfig.SERVER.tier2PriceMin.get(), 27);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tier 2 maximum price in nuggets.
|
||||
* @return Max price (default 45)
|
||||
*/
|
||||
public static int getTier2PriceMax() {
|
||||
return safeGet(() -> ModConfig.SERVER.tier2PriceMax.get(), 45);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tier 3 minimum price in nuggets.
|
||||
* @return Min price (default 54)
|
||||
*/
|
||||
public static int getTier3PriceMin() {
|
||||
return safeGet(() -> ModConfig.SERVER.tier3PriceMin.get(), 54);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tier 3 maximum price in nuggets.
|
||||
* @return Max price (default 90)
|
||||
*/
|
||||
public static int getTier3PriceMax() {
|
||||
return safeGet(() -> ModConfig.SERVER.tier3PriceMax.get(), 90);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tier 4 minimum price in nuggets.
|
||||
* @return Min price (default 90)
|
||||
*/
|
||||
public static int getTier4PriceMin() {
|
||||
return safeGet(() -> ModConfig.SERVER.tier4PriceMin.get(), 90);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tier 4 maximum price in nuggets.
|
||||
* @return Max price (default 180)
|
||||
*/
|
||||
public static int getTier4PriceMax() {
|
||||
return safeGet(() -> ModConfig.SERVER.tier4PriceMax.get(), 180);
|
||||
}
|
||||
|
||||
// ==================== Dialogue ====================
|
||||
|
||||
/**
|
||||
* Get the dialogue broadcast radius in blocks.
|
||||
*
|
||||
* @return Radius in blocks (default 20)
|
||||
*/
|
||||
public static int getDialogueRadius() {
|
||||
return safeGet(() -> ModConfig.SERVER.dialogueRadius.get(), 20);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dialogue cooldown in ticks.
|
||||
*
|
||||
* @return Cooldown in ticks (default 100 = 5 seconds)
|
||||
*/
|
||||
public static int getDialogueCooldown() {
|
||||
return safeGet(() -> ModConfig.SERVER.dialogueCooldown.get(), 100);
|
||||
}
|
||||
|
||||
// ==================== Labor ====================
|
||||
|
||||
/**
|
||||
* Get the labor rest duration in seconds.
|
||||
*
|
||||
* @return Rest duration in seconds (default 120)
|
||||
*/
|
||||
public static int getLaborRestSeconds() {
|
||||
return safeGet(() -> ModConfig.SERVER.laborRestSeconds.get(), 120);
|
||||
}
|
||||
|
||||
// ==================== Solo Mode / Master Spawn ====================
|
||||
|
||||
/**
|
||||
* Check if Master NPC spawning is enabled.
|
||||
*
|
||||
* @return true if Master can spawn (default true)
|
||||
*/
|
||||
public static boolean isEnableMasterSpawn() {
|
||||
return safeGet(() -> ModConfig.SERVER.enableMasterSpawn.get(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if solo mode fallback behavior is enabled.
|
||||
* When enabled, kidnappers in solo worlds use alternative captive handling.
|
||||
*
|
||||
* @return true if solo fallback is enabled (default true)
|
||||
*/
|
||||
public static boolean isEnableSoloFallback() {
|
||||
return safeGet(() -> ModConfig.SERVER.enableSoloFallback.get(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the solo mode timeout in seconds before the kidnapper gives up waiting.
|
||||
*
|
||||
* @return Timeout in seconds (default 120)
|
||||
*/
|
||||
public static int getSoloTimeoutSeconds() {
|
||||
return safeGet(() -> ModConfig.SERVER.soloTimeoutSeconds.get(), 120);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the probability (0.0-1.0) that a kidnapper keeps the captive in solo mode.
|
||||
*
|
||||
* @return Keep chance (default 0.6)
|
||||
*/
|
||||
public static double getSoloKeepChance() {
|
||||
return safeGet(() -> ModConfig.SERVER.soloKeepChance.get(), 0.6);
|
||||
}
|
||||
|
||||
// ==================== Abandon Behavior ====================
|
||||
|
||||
/**
|
||||
* Check if kidnapper abandonment keeps the blindfold on the captive.
|
||||
*
|
||||
* @return true if blindfold is kept (default true)
|
||||
*/
|
||||
public static boolean isAbandonKeepsBlindfold() {
|
||||
return safeGet(() -> ModConfig.SERVER.abandonKeepsBlindfold.get(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if kidnapper abandonment keeps binds on the captive.
|
||||
*
|
||||
* @return true if binds are kept (default true)
|
||||
*/
|
||||
public static boolean isAbandonKeepsBinds() {
|
||||
return safeGet(() -> ModConfig.SERVER.abandonKeepsBinds.get(), true);
|
||||
}
|
||||
|
||||
// ==================== Gag Settings ====================
|
||||
|
||||
/**
|
||||
* Get the configured comprehension factor for a gag material.
|
||||
* Falls back to the material's hardcoded default if the config key is missing.
|
||||
*
|
||||
* @param materialKey Lowercase gag material name (e.g., "cloth", "ball")
|
||||
* @param defaultValue The material's built-in default comprehension
|
||||
* @return Comprehension factor as float
|
||||
*/
|
||||
public static float getGagComprehension(String materialKey, float defaultValue) {
|
||||
return safeGet(
|
||||
() -> {
|
||||
if (ModConfig.SERVER == null) return defaultValue;
|
||||
Map<String, ForgeConfigSpec.DoubleValue> map =
|
||||
ModConfig.SERVER.gagComprehension;
|
||||
if (map != null && map.containsKey(materialKey)) {
|
||||
return map.get(materialKey).get().floatValue();
|
||||
}
|
||||
return defaultValue;
|
||||
},
|
||||
defaultValue
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configured talk range for a gag material.
|
||||
* Falls back to the material's hardcoded default if the config key is missing.
|
||||
*
|
||||
* @param materialKey Lowercase gag material name (e.g., "cloth", "ball")
|
||||
* @param defaultValue The material's built-in default talk range
|
||||
* @return Talk range as double
|
||||
*/
|
||||
public static double getGagTalkRange(String materialKey, double defaultValue) {
|
||||
return safeGet(
|
||||
() -> {
|
||||
if (ModConfig.SERVER == null) return defaultValue;
|
||||
Map<String, ForgeConfigSpec.DoubleValue> map =
|
||||
ModConfig.SERVER.gagRange;
|
||||
if (map != null && map.containsKey(materialKey)) {
|
||||
return map.get(materialKey).get();
|
||||
}
|
||||
return defaultValue;
|
||||
},
|
||||
defaultValue
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== Helpers ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -51,7 +51,7 @@ public class PetRequestManager {
|
||||
double dist = master.distanceTo(pet);
|
||||
if (dist > MAX_DISTANCE) {
|
||||
pet.sendSystemMessage(
|
||||
Component.literal("You are too far from your Master to talk.")
|
||||
Component.translatable("entity.tiedup.pet.too_far_to_talk")
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -95,7 +95,7 @@ public class PetRequestManager {
|
||||
double dist = master.distanceTo(pet);
|
||||
if (dist > MAX_DISTANCE) {
|
||||
pet.sendSystemMessage(
|
||||
Component.literal("You are too far from your Master.")
|
||||
Component.translatable("entity.tiedup.pet.too_far_from_master")
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -109,7 +109,7 @@ public class PetRequestManager {
|
||||
|
||||
// Display what the player "says"
|
||||
pet.sendSystemMessage(
|
||||
Component.literal("You: " + request.getPlayerText())
|
||||
Component.translatable("entity.tiedup.pet.you_say", request.getPlayerText())
|
||||
);
|
||||
|
||||
// Handle specific request
|
||||
@@ -171,7 +171,7 @@ public class PetRequestManager {
|
||||
|
||||
// Put dogbind on player (if not already tied)
|
||||
if (!state.isTiedUp()) {
|
||||
ItemStack dogbind = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "dogbinder"));
|
||||
ItemStack dogbind = DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "dogbinder"));
|
||||
state.equip(BodyRegionV2.ARMS, dogbind);
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PetRequestManager] Equipped dogbind on {} for walk",
|
||||
@@ -226,7 +226,7 @@ public class PetRequestManager {
|
||||
}
|
||||
|
||||
// Master equips armbinder on pet (classic pet play restraint)
|
||||
ItemStack bind = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "armbinder"));
|
||||
ItemStack bind = DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "armbinder"));
|
||||
state.equip(BodyRegionV2.ARMS, bind);
|
||||
|
||||
DialogueBridge.talkTo(master, pet, "petplay.tie_accept");
|
||||
|
||||
@@ -143,11 +143,11 @@ public class BondageServiceHandler {
|
||||
// Apply bondage
|
||||
state.equip(
|
||||
BodyRegionV2.ARMS,
|
||||
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ropes"))
|
||||
DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "ropes"))
|
||||
);
|
||||
state.equip(
|
||||
BodyRegionV2.MOUTH,
|
||||
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ball_gag"))
|
||||
DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "ball_gag"))
|
||||
);
|
||||
|
||||
// Teleport to cell
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.tiedup.remake.entities;
|
||||
|
||||
import com.tiedup.remake.core.ModConfig;
|
||||
import com.tiedup.remake.core.SettingsAccessor;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.dialogue.EntityDialogueManager;
|
||||
import java.util.UUID;
|
||||
@@ -179,7 +179,7 @@ public class DamselRewardTracker {
|
||||
// Thank the savior via dialogue
|
||||
damsel.talkToPlayersInRadius(
|
||||
EntityDialogueManager.DialogueCategory.DAMSEL_FREED,
|
||||
ModConfig.SERVER.dialogueRadius.get()
|
||||
SettingsAccessor.getDialogueRadius()
|
||||
);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
|
||||
@@ -644,8 +644,8 @@ public class EntityDamsel
|
||||
player instanceof net.minecraft.server.level.ServerPlayer sp
|
||||
) {
|
||||
sp.displayClientMessage(
|
||||
Component.literal(
|
||||
"This NPC needs a collar before you can feed them."
|
||||
Component.translatable(
|
||||
"entity.tiedup.damsel.needs_collar_to_feed"
|
||||
).withStyle(ChatFormatting.RED),
|
||||
true
|
||||
);
|
||||
@@ -660,8 +660,8 @@ public class EntityDamsel
|
||||
net.minecraft.server.level.ServerPlayer sp
|
||||
) {
|
||||
sp.displayClientMessage(
|
||||
Component.literal(
|
||||
"You don't own this NPC's collar."
|
||||
Component.translatable(
|
||||
"entity.tiedup.damsel.not_collar_owner"
|
||||
).withStyle(ChatFormatting.RED),
|
||||
true
|
||||
);
|
||||
@@ -675,8 +675,8 @@ public class EntityDamsel
|
||||
player instanceof net.minecraft.server.level.ServerPlayer sp
|
||||
) {
|
||||
sp.displayClientMessage(
|
||||
Component.literal(
|
||||
"This NPC can't eat that right now."
|
||||
Component.translatable(
|
||||
"entity.tiedup.damsel.cant_eat_now"
|
||||
).withStyle(ChatFormatting.RED),
|
||||
true
|
||||
);
|
||||
@@ -772,7 +772,7 @@ public class EntityDamsel
|
||||
state == com.tiedup.remake.prison.PrisonerState.IMPRISONED ||
|
||||
state == com.tiedup.remake.prison.PrisonerState.WORKING
|
||||
) {
|
||||
com.tiedup.remake.prison.service.PrisonerService.get().escape(
|
||||
com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
|
||||
serverLevel,
|
||||
uuid,
|
||||
"player_death"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.tiedup.remake.entities;
|
||||
|
||||
import com.tiedup.remake.blocks.entity.KidnapBombBlockEntity;
|
||||
import com.tiedup.remake.core.ModConfig;
|
||||
import com.tiedup.remake.core.SettingsAccessor;
|
||||
import com.tiedup.remake.util.KidnapExplosion;
|
||||
import javax.annotation.Nullable;
|
||||
@@ -56,7 +55,7 @@ public class EntityKidnapBomb extends PrimedTnt {
|
||||
this.setPos(x, y, z);
|
||||
double d0 = level.random.nextDouble() * (Math.PI * 2);
|
||||
this.setDeltaMovement(-Math.sin(d0) * 0.02, 0.2, -Math.cos(d0) * 0.02);
|
||||
this.setFuse(ModConfig.SERVER.kidnapBombFuse.get());
|
||||
this.setFuse(SettingsAccessor.getKidnapBombFuse());
|
||||
|
||||
this.xo = x;
|
||||
this.yo = y;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.tiedup.remake.entities;
|
||||
|
||||
import com.tiedup.remake.cells.CellDataV2;
|
||||
import com.tiedup.remake.cells.CellRegistryV2;
|
||||
import com.tiedup.remake.core.SettingsAccessor;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.dialogue.IDialogueSpeaker;
|
||||
@@ -11,31 +10,23 @@ import com.tiedup.remake.entities.ai.kidnapper.*;
|
||||
import com.tiedup.remake.entities.kidnapper.components.KidnapperAggressionSystem;
|
||||
import com.tiedup.remake.entities.skins.Gender;
|
||||
import com.tiedup.remake.entities.skins.KidnapperSkinManager;
|
||||
import com.tiedup.remake.items.ModItems;
|
||||
import com.tiedup.remake.v2.bondage.CollarHelper;
|
||||
import com.tiedup.remake.personality.PersonalityType;
|
||||
import com.tiedup.remake.state.IBondageState;
|
||||
import com.tiedup.remake.state.ICaptor;
|
||||
import com.tiedup.remake.state.IRestrainable;
|
||||
import com.tiedup.remake.util.KidnappedHelper;
|
||||
import com.tiedup.remake.util.tasks.ItemTask;
|
||||
import com.tiedup.remake.util.tasks.SaleLoader;
|
||||
import com.tiedup.remake.util.teleport.Position;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import javax.annotation.Nullable;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.syncher.EntityDataAccessor;
|
||||
import net.minecraft.network.syncher.EntityDataSerializers;
|
||||
import net.minecraft.network.syncher.SynchedEntityData;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.AttributeSupplier;
|
||||
@@ -44,7 +35,6 @@ import net.minecraft.world.entity.ai.goal.*;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
|
||||
/**
|
||||
* EntityKidnapper - Aggressive NPC that captures and enslaves players.
|
||||
@@ -87,48 +77,6 @@ public class EntityKidnapper
|
||||
implements ICaptor, IDialogueSpeaker
|
||||
{
|
||||
|
||||
// CAPTIVE PRIORITY (for prisoner replacement)
|
||||
|
||||
/**
|
||||
* Priority levels for captives when replacing prisoners in cells.
|
||||
* Higher priority captives will cause lower priority prisoners to be released.
|
||||
*/
|
||||
public enum CaptivePriority {
|
||||
DAMSEL(1),
|
||||
DAMSEL_SHINY(2),
|
||||
PLAYER(3);
|
||||
|
||||
private final int priority;
|
||||
|
||||
CaptivePriority(int priority) {
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
public int getPriority() {
|
||||
return priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the priority for an entity.
|
||||
*
|
||||
* @param entity The entity to check
|
||||
* @return The captive priority
|
||||
*/
|
||||
public static CaptivePriority fromEntity(LivingEntity entity) {
|
||||
if (entity instanceof Player) return PLAYER;
|
||||
if (entity instanceof EntityDamselShiny) return DAMSEL_SHINY;
|
||||
if (entity instanceof EntityDamsel) return DAMSEL;
|
||||
return DAMSEL; // Default for unknown entities
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this priority is higher than another.
|
||||
*/
|
||||
public boolean isHigherThan(CaptivePriority other) {
|
||||
return this.priority > other.priority;
|
||||
}
|
||||
}
|
||||
|
||||
// DATA SYNC (Client-Server)
|
||||
|
||||
/**
|
||||
@@ -194,11 +142,11 @@ public class EntityKidnapper
|
||||
/** Whether this kidnapper is currently dogwalking a prisoner. */
|
||||
private boolean dogwalking = false;
|
||||
|
||||
/** Items stolen from players via KidnapperThiefGoal. Dropped at 100% on death. */
|
||||
private final List<ItemStack> stolenItems = new ArrayList<>();
|
||||
/** Loot manager for stolen items, collar keys, and death drops. */
|
||||
private final com.tiedup.remake.entities.kidnapper.components.KidnapperLootManager lootManager;
|
||||
|
||||
/** Collar keys generated when collaring captives. Dropped at 20% on death. */
|
||||
private final List<ItemStack> collarKeys = new ArrayList<>();
|
||||
/** Dialogue speaker implementation for IDialogueSpeaker delegation. */
|
||||
private final com.tiedup.remake.entities.kidnapper.components.KidnapperDialogue dialogue;
|
||||
|
||||
/** Job manager handles job assignment and tracking. */
|
||||
private final KidnapperJobManager jobManager = new KidnapperJobManager(
|
||||
@@ -275,6 +223,18 @@ public class EntityKidnapper
|
||||
DATA_THEME_COLOR
|
||||
);
|
||||
|
||||
// Initialize loot manager
|
||||
this.lootManager =
|
||||
new com.tiedup.remake.entities.kidnapper.components.KidnapperLootManager(
|
||||
new com.tiedup.remake.entities.kidnapper.hosts.LootHost(this)
|
||||
);
|
||||
|
||||
// Initialize dialogue component
|
||||
this.dialogue =
|
||||
new com.tiedup.remake.entities.kidnapper.components.KidnapperDialogue(
|
||||
new com.tiedup.remake.entities.kidnapper.hosts.DialogueHost(this)
|
||||
);
|
||||
|
||||
// Initialize state manager
|
||||
this.stateManager =
|
||||
new com.tiedup.remake.entities.kidnapper.components.KidnapperStateManager(
|
||||
@@ -585,40 +545,35 @@ public class EntityKidnapper
|
||||
// All real IBondageState instances are IRestrainable, so the cast is safe.
|
||||
// Log a warning if the invariant is ever broken (future-proofing).
|
||||
|
||||
private void withRestrainable(String method, IBondageState captive, java.util.function.Consumer<IRestrainable> action) {
|
||||
if (captive instanceof IRestrainable r) action.accept(r);
|
||||
else logBridgeWarning(method, captive);
|
||||
}
|
||||
|
||||
private boolean testRestrainable(String method, IBondageState captive, java.util.function.Predicate<IRestrainable> test) {
|
||||
if (captive instanceof IRestrainable r) return test.test(r);
|
||||
logBridgeWarning(method, captive);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCaptive(IBondageState captive) {
|
||||
if (captive instanceof IRestrainable r) {
|
||||
captiveManager.addCaptive(r);
|
||||
} else {
|
||||
logBridgeWarning("addCaptive", captive);
|
||||
}
|
||||
withRestrainable("addCaptive", captive, captiveManager::addCaptive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeCaptive(IBondageState captive, boolean transportState) {
|
||||
if (captive instanceof IRestrainable r) {
|
||||
captiveManager.removeCaptive(r, transportState);
|
||||
} else {
|
||||
logBridgeWarning("removeCaptive", captive);
|
||||
}
|
||||
withRestrainable("removeCaptive", captive, r -> captiveManager.removeCaptive(r, transportState));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canCapture(IBondageState captive) {
|
||||
if (
|
||||
captive instanceof IRestrainable r
|
||||
) return captiveManager.canCapture(r);
|
||||
logBridgeWarning("canCapture", captive);
|
||||
return false;
|
||||
return testRestrainable("canCapture", captive, captiveManager::canCapture);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRelease(IBondageState captive) {
|
||||
if (
|
||||
captive instanceof IRestrainable r
|
||||
) return captiveManager.canRelease(r);
|
||||
logBridgeWarning("canRelease", captive);
|
||||
return false;
|
||||
return testRestrainable("canRelease", captive, captiveManager::canRelease);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -633,29 +588,17 @@ public class EntityKidnapper
|
||||
|
||||
@Override
|
||||
public void onCaptiveLogout(IBondageState captive) {
|
||||
if (captive instanceof IRestrainable r) {
|
||||
captiveManager.onCaptiveLogout(r);
|
||||
} else {
|
||||
logBridgeWarning("onCaptiveLogout", captive);
|
||||
}
|
||||
withRestrainable("onCaptiveLogout", captive, captiveManager::onCaptiveLogout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCaptiveReleased(IBondageState captive) {
|
||||
if (captive instanceof IRestrainable r) {
|
||||
captiveManager.onCaptiveReleased(r);
|
||||
} else {
|
||||
logBridgeWarning("onCaptiveReleased", captive);
|
||||
}
|
||||
withRestrainable("onCaptiveReleased", captive, captiveManager::onCaptiveReleased);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCaptiveStruggle(IBondageState captive) {
|
||||
if (captive instanceof IRestrainable r) {
|
||||
captiveManager.onCaptiveStruggle(r);
|
||||
} else {
|
||||
logBridgeWarning("onCaptiveStruggle", captive);
|
||||
}
|
||||
withRestrainable("onCaptiveStruggle", captive, captiveManager::onCaptiveStruggle);
|
||||
}
|
||||
|
||||
private void logBridgeWarning(String method, IBondageState captive) {
|
||||
@@ -936,130 +879,15 @@ public class EntityKidnapper
|
||||
super.die(damageSource);
|
||||
}
|
||||
|
||||
/** Token drop chance (5%) */
|
||||
private static final float TOKEN_DROP_CHANCE = 0.05f;
|
||||
|
||||
/**
|
||||
* Prevent taser from dropping when kidnapper dies.
|
||||
* Taser is unique to kidnappers and should not be obtainable by players.
|
||||
* Also handles token drop (5% chance).
|
||||
* Delegates loot logic to KidnapperLootManager.
|
||||
*/
|
||||
@Override
|
||||
protected void dropEquipment() {
|
||||
// Check main hand for taser - don't drop it
|
||||
ItemStack mainHand = this.getItemBySlot(
|
||||
net.minecraft.world.entity.EquipmentSlot.MAINHAND
|
||||
);
|
||||
if (
|
||||
!mainHand.isEmpty() &&
|
||||
mainHand.getItem() instanceof com.tiedup.remake.items.ItemTaser
|
||||
) {
|
||||
this.setItemSlot(
|
||||
net.minecraft.world.entity.EquipmentSlot.MAINHAND,
|
||||
ItemStack.EMPTY
|
||||
);
|
||||
}
|
||||
|
||||
// Check off hand too
|
||||
ItemStack offHand = this.getItemBySlot(
|
||||
net.minecraft.world.entity.EquipmentSlot.OFFHAND
|
||||
);
|
||||
if (
|
||||
!offHand.isEmpty() &&
|
||||
offHand.getItem() instanceof com.tiedup.remake.items.ItemTaser
|
||||
) {
|
||||
this.setItemSlot(
|
||||
net.minecraft.world.entity.EquipmentSlot.OFFHAND,
|
||||
ItemStack.EMPTY
|
||||
);
|
||||
}
|
||||
|
||||
lootManager.dropEquipment();
|
||||
super.dropEquipment();
|
||||
|
||||
// Token drop: 5% chance when killed
|
||||
if (
|
||||
!this.level().isClientSide &&
|
||||
this.getRandom().nextFloat() < TOKEN_DROP_CHANCE
|
||||
) {
|
||||
ItemStack token = new ItemStack(
|
||||
com.tiedup.remake.items.ModItems.TOKEN.get()
|
||||
);
|
||||
this.spawnAtLocation(token);
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[EntityKidnapper] {} dropped a token on death!",
|
||||
this.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
// Bind item drops from kidnapper inventory
|
||||
if (!this.level().isClientSide) {
|
||||
KidnapperItemSelector.SelectionResult selection =
|
||||
getItemSelection();
|
||||
if (selection != null) {
|
||||
float dropChance = 0.15f;
|
||||
if (
|
||||
!selection.bind.isEmpty() &&
|
||||
this.getRandom().nextFloat() < dropChance
|
||||
) {
|
||||
this.spawnAtLocation(selection.bind.copy());
|
||||
}
|
||||
if (
|
||||
selection.hasGag() &&
|
||||
this.getRandom().nextFloat() < dropChance
|
||||
) {
|
||||
this.spawnAtLocation(selection.gag.copy());
|
||||
}
|
||||
if (
|
||||
selection.hasMittens() &&
|
||||
this.getRandom().nextFloat() < dropChance
|
||||
) {
|
||||
this.spawnAtLocation(selection.mittens.copy());
|
||||
}
|
||||
if (
|
||||
selection.hasEarplugs() &&
|
||||
this.getRandom().nextFloat() < dropChance
|
||||
) {
|
||||
this.spawnAtLocation(selection.earplugs.copy());
|
||||
}
|
||||
if (
|
||||
selection.hasBlindfold() &&
|
||||
this.getRandom().nextFloat() < dropChance
|
||||
) {
|
||||
this.spawnAtLocation(selection.blindfold.copy());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Drop stolen items at 100% rate (player's property)
|
||||
if (!this.level().isClientSide) {
|
||||
for (ItemStack stolen : this.stolenItems) {
|
||||
if (!stolen.isEmpty()) {
|
||||
this.spawnAtLocation(stolen);
|
||||
}
|
||||
}
|
||||
if (!this.stolenItems.isEmpty()) {
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[EntityKidnapper] {} dropped {} stolen item(s) on death",
|
||||
this.getNpcName(),
|
||||
this.stolenItems.size()
|
||||
);
|
||||
}
|
||||
this.stolenItems.clear();
|
||||
}
|
||||
|
||||
// Drop collar keys at 20% rate
|
||||
if (!this.level().isClientSide) {
|
||||
for (ItemStack key : this.collarKeys) {
|
||||
if (!key.isEmpty() && this.getRandom().nextFloat() < 0.20f) {
|
||||
this.spawnAtLocation(key);
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[EntityKidnapper] {} dropped a collar key on death",
|
||||
this.getNpcName()
|
||||
);
|
||||
}
|
||||
}
|
||||
this.collarKeys.clear();
|
||||
}
|
||||
lootManager.dropPostEquipment();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1255,27 +1083,8 @@ public class EntityKidnapper
|
||||
// Delegate to data serializer
|
||||
dataSerializer.saveToNBT(tag);
|
||||
|
||||
// Save stolen items
|
||||
if (!this.stolenItems.isEmpty()) {
|
||||
ListTag stolenTag = new ListTag();
|
||||
for (ItemStack stack : this.stolenItems) {
|
||||
if (!stack.isEmpty()) {
|
||||
stolenTag.add(stack.save(new CompoundTag()));
|
||||
}
|
||||
}
|
||||
tag.put("StolenItems", stolenTag);
|
||||
}
|
||||
|
||||
// Save collar keys
|
||||
if (!this.collarKeys.isEmpty()) {
|
||||
ListTag keysTag = new ListTag();
|
||||
for (ItemStack key : this.collarKeys) {
|
||||
if (!key.isEmpty()) {
|
||||
keysTag.add(key.save(new CompoundTag()));
|
||||
}
|
||||
}
|
||||
tag.put("CollarKeys", keysTag);
|
||||
}
|
||||
// Delegate stolen items and collar keys to loot manager
|
||||
lootManager.saveToNBT(tag);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1285,29 +1094,8 @@ public class EntityKidnapper
|
||||
// Delegate to data serializer
|
||||
dataSerializer.loadFromNBT(tag);
|
||||
|
||||
// Load stolen items
|
||||
this.stolenItems.clear();
|
||||
if (tag.contains("StolenItems", Tag.TAG_LIST)) {
|
||||
ListTag stolenTag = tag.getList("StolenItems", Tag.TAG_COMPOUND);
|
||||
for (int i = 0; i < stolenTag.size(); i++) {
|
||||
ItemStack stack = ItemStack.of(stolenTag.getCompound(i));
|
||||
if (!stack.isEmpty()) {
|
||||
this.stolenItems.add(stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load collar keys
|
||||
this.collarKeys.clear();
|
||||
if (tag.contains("CollarKeys", Tag.TAG_LIST)) {
|
||||
ListTag keysTag = tag.getList("CollarKeys", Tag.TAG_COMPOUND);
|
||||
for (int i = 0; i < keysTag.size(); i++) {
|
||||
ItemStack key = ItemStack.of(keysTag.getCompound(i));
|
||||
if (!key.isEmpty()) {
|
||||
this.collarKeys.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Delegate stolen items and collar keys to loot manager
|
||||
lootManager.loadFromNBT(tag);
|
||||
}
|
||||
|
||||
// ESCAPE TRACKING METHODS
|
||||
@@ -1370,59 +1158,14 @@ public class EntityKidnapper
|
||||
return CollarHelper.isOwner(collar, player);
|
||||
}
|
||||
|
||||
/** Damage reduction multiplier against monsters (50% damage taken) */
|
||||
private static final float MONSTER_DAMAGE_REDUCTION = 0.5f;
|
||||
|
||||
@Override
|
||||
public boolean hurt(
|
||||
net.minecraft.world.damagesource.DamageSource source,
|
||||
float amount
|
||||
) {
|
||||
float finalAmount = amount;
|
||||
|
||||
// Track the attacker for fight back system
|
||||
if (source.getEntity() instanceof LivingEntity attacker) {
|
||||
aggressionSystem.setLastAttacker(attacker);
|
||||
|
||||
// Punish prisoners who dare to attack us
|
||||
if (
|
||||
!this.level().isClientSide &&
|
||||
attacker instanceof ServerPlayer player
|
||||
) {
|
||||
punishAttackingPrisoner(player);
|
||||
|
||||
// Expire PROTECTED status if player attacks a kidnapper
|
||||
// Attacking a kidnapper voids your safe-exit window
|
||||
if (this.level() instanceof ServerLevel serverLevel) {
|
||||
com.tiedup.remake.prison.PrisonerManager pm =
|
||||
com.tiedup.remake.prison.PrisonerManager.get(
|
||||
serverLevel
|
||||
);
|
||||
if (
|
||||
pm.getState(player.getUUID()) ==
|
||||
com.tiedup.remake.prison.PrisonerState.PROTECTED
|
||||
) {
|
||||
pm.expireProtection(
|
||||
player.getUUID(),
|
||||
serverLevel.getGameTime()
|
||||
);
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[EntityKidnapper] Expired PROTECTED status for {} (attacked kidnapper)",
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Camp kidnappers take reduced damage from monsters (trained fighters)
|
||||
if (
|
||||
this.getAssociatedStructure() != null &&
|
||||
attacker instanceof net.minecraft.world.entity.monster.Monster
|
||||
) {
|
||||
finalAmount = amount * MONSTER_DAMAGE_REDUCTION;
|
||||
}
|
||||
}
|
||||
return super.hurt(source, finalAmount);
|
||||
float modified = aggressionSystem.processIncomingDamage(source, amount);
|
||||
if (modified <= 0) return false;
|
||||
return super.hurt(source, modified);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1432,7 +1175,7 @@ public class EntityKidnapper
|
||||
* @param player The player who attacked
|
||||
* @return true if punishment was applied
|
||||
*/
|
||||
protected boolean punishAttackingPrisoner(ServerPlayer player) {
|
||||
public boolean punishAttackingPrisoner(ServerPlayer player) {
|
||||
return captiveManager.punishAttackingPrisoner(player);
|
||||
}
|
||||
|
||||
@@ -1912,76 +1655,31 @@ public class EntityKidnapper
|
||||
com.tiedup.remake.util.MessageDispatcher.actionTo(this, player, action);
|
||||
}
|
||||
|
||||
/** Dialogue cooldown timer (ticks remaining before next dialogue) */
|
||||
private int dialogueCooldown = 0;
|
||||
|
||||
@Override
|
||||
public String getDialogueName() {
|
||||
return this.getNpcName();
|
||||
return dialogue.getDialogueName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpeakerType getSpeakerType() {
|
||||
return SpeakerType.KIDNAPPER;
|
||||
return dialogue.getSpeakerType();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public PersonalityType getSpeakerPersonality() {
|
||||
// Map kidnapper theme to a personality-like behavior
|
||||
KidnapperTheme theme = this.getTheme();
|
||||
if (theme == null) {
|
||||
return PersonalityType.CALM;
|
||||
}
|
||||
|
||||
return switch (theme) {
|
||||
case ROPE, SHIBARI -> PersonalityType.CALM; // Traditional, methodical
|
||||
case TAPE, LEATHER, CHAIN -> PersonalityType.FIERCE; // Rough, aggressive
|
||||
case MEDICAL, ASYLUM -> PersonalityType.PROUD; // Clinical, professional
|
||||
case LATEX, RIBBON -> PersonalityType.PLAYFUL; // Playful, teasing
|
||||
case BEAM, WRAP -> PersonalityType.CURIOUS; // Experimental, modern
|
||||
};
|
||||
return dialogue.getSpeakerPersonality();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSpeakerMood() {
|
||||
// Kidnappers mood is based on:
|
||||
// - Having a captive (+20)
|
||||
// - Current state (varies)
|
||||
int mood = 50;
|
||||
|
||||
if (this.hasCaptives()) {
|
||||
mood += 20;
|
||||
}
|
||||
|
||||
// State-based adjustment
|
||||
KidnapperState state = this.getCurrentState();
|
||||
if (state != null) {
|
||||
mood += switch (state) {
|
||||
case SELLING -> 10; // Excited about sale
|
||||
case JOB_WATCH -> 5;
|
||||
case GUARD -> 0;
|
||||
case CAPTURE -> 15; // Hunting excitement
|
||||
case PUNISH -> -10; // Stern
|
||||
case PATROL, IDLE, HUNT -> 0; // Neutral for patrolling/hunting
|
||||
case ALERT -> -5; // Concerned
|
||||
case TRANSPORT -> 5;
|
||||
};
|
||||
}
|
||||
|
||||
return Math.max(0, Math.min(100, mood));
|
||||
return dialogue.getSpeakerMood();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getTargetRelation(Player player) {
|
||||
// Check if this kidnapper is holding the player captive
|
||||
IRestrainable captive = this.getCaptive();
|
||||
if (captive != null && captive.asLivingEntity() == player) {
|
||||
return "captor";
|
||||
}
|
||||
|
||||
return null;
|
||||
return dialogue.getTargetRelation(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1991,12 +1689,12 @@ public class EntityKidnapper
|
||||
|
||||
@Override
|
||||
public int getDialogueCooldown() {
|
||||
return this.dialogueCooldown;
|
||||
return dialogue.getDialogueCooldown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDialogueCooldown(int ticks) {
|
||||
this.dialogueCooldown = ticks;
|
||||
dialogue.setDialogueCooldown(ticks);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2004,9 +1702,7 @@ public class EntityKidnapper
|
||||
* Called from the main tick method.
|
||||
*/
|
||||
protected void tickDialogueCooldown() {
|
||||
if (this.dialogueCooldown > 0) {
|
||||
this.dialogueCooldown--;
|
||||
}
|
||||
dialogue.tickDialogueCooldown();
|
||||
}
|
||||
|
||||
// STOLEN ITEMS (Thief Goal)
|
||||
@@ -2016,9 +1712,7 @@ public class EntityKidnapper
|
||||
* Called by KidnapperThiefGoal when stealing from a player.
|
||||
*/
|
||||
public void addStolenItem(ItemStack stack) {
|
||||
if (!stack.isEmpty()) {
|
||||
this.stolenItems.add(stack.copy());
|
||||
}
|
||||
lootManager.addStolenItem(stack);
|
||||
}
|
||||
|
||||
// COLLAR KEYS (Capture Goal)
|
||||
@@ -2028,8 +1722,6 @@ public class EntityKidnapper
|
||||
* Called by KidnapperCaptureGoal when collaring a captive.
|
||||
*/
|
||||
public void addCollarKey(ItemStack keyStack) {
|
||||
if (!keyStack.isEmpty()) {
|
||||
this.collarKeys.add(keyStack.copy());
|
||||
}
|
||||
lootManager.addCollarKey(keyStack);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,9 +335,9 @@ public class EntityKidnapperArcher
|
||||
public int getBindChanceForTarget(UUID targetUUID) {
|
||||
int hitCount = targetHitCounts.getOrDefault(targetUUID, 0);
|
||||
int baseChance =
|
||||
com.tiedup.remake.core.ModConfig.SERVER.archerArrowBindChanceBase.get();
|
||||
SettingsAccessor.getArcherArrowBindChanceBase();
|
||||
int perHitChance =
|
||||
com.tiedup.remake.core.ModConfig.SERVER.archerArrowBindChancePerHit.get();
|
||||
SettingsAccessor.getArcherArrowBindChancePerHit();
|
||||
|
||||
int chance = baseChance + (hitCount * perHitChance);
|
||||
return Math.min(chance, ARCHER_MAX_BIND_CHANCE);
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.tiedup.remake.entities;
|
||||
|
||||
import static com.tiedup.remake.util.GameConstants.*;
|
||||
|
||||
import com.tiedup.remake.core.ModConfig;
|
||||
import com.tiedup.remake.core.SettingsAccessor;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.dialogue.SpeakerType;
|
||||
@@ -356,7 +355,7 @@ public class EntityKidnapperMerchant extends EntityKidnapperElite {
|
||||
private void transitionToHostile(LivingEntity attacker) {
|
||||
currentState = MerchantState.HOSTILE;
|
||||
attackerUUID = attacker.getUUID();
|
||||
hostileCooldownTicks = ModConfig.SERVER.merchantHostileDuration.get();
|
||||
hostileCooldownTicks = SettingsAccessor.getMerchantHostileDuration();
|
||||
entityData.set(DATA_MERCHANT_STATE, MerchantState.HOSTILE.getId());
|
||||
|
||||
// Equip kidnapper items
|
||||
@@ -536,8 +535,8 @@ public class EntityKidnapperMerchant extends EntityKidnapperElite {
|
||||
addGuaranteedUtilities();
|
||||
|
||||
// RANDOM TRADES
|
||||
int min = ModConfig.SERVER.merchantMinTrades.get();
|
||||
int max = ModConfig.SERVER.merchantMaxTrades.get();
|
||||
int min = SettingsAccessor.getMerchantMinTrades();
|
||||
int max = SettingsAccessor.getMerchantMaxTrades();
|
||||
int count = min + this.random.nextInt(Math.max(1, max - min + 1));
|
||||
|
||||
// Collect all mod items
|
||||
@@ -644,21 +643,21 @@ public class EntityKidnapperMerchant extends EntityKidnapperElite {
|
||||
int minPrice, maxPrice;
|
||||
switch (tier) {
|
||||
case 4:
|
||||
minPrice = ModConfig.SERVER.tier4PriceMin.get();
|
||||
maxPrice = ModConfig.SERVER.tier4PriceMax.get();
|
||||
minPrice = SettingsAccessor.getTier4PriceMin();
|
||||
maxPrice = SettingsAccessor.getTier4PriceMax();
|
||||
break;
|
||||
case 3:
|
||||
minPrice = ModConfig.SERVER.tier3PriceMin.get();
|
||||
maxPrice = ModConfig.SERVER.tier3PriceMax.get();
|
||||
minPrice = SettingsAccessor.getTier3PriceMin();
|
||||
maxPrice = SettingsAccessor.getTier3PriceMax();
|
||||
break;
|
||||
case 2:
|
||||
minPrice = ModConfig.SERVER.tier2PriceMin.get();
|
||||
maxPrice = ModConfig.SERVER.tier2PriceMax.get();
|
||||
minPrice = SettingsAccessor.getTier2PriceMin();
|
||||
maxPrice = SettingsAccessor.getTier2PriceMax();
|
||||
break;
|
||||
case 1:
|
||||
default:
|
||||
minPrice = ModConfig.SERVER.tier1PriceMin.get();
|
||||
maxPrice = ModConfig.SERVER.tier1PriceMax.get();
|
||||
minPrice = SettingsAccessor.getTier1PriceMin();
|
||||
maxPrice = SettingsAccessor.getTier1PriceMax();
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import com.tiedup.remake.entities.skins.LaborGuardSkinManager;
|
||||
import com.tiedup.remake.labor.LaborTask;
|
||||
import com.tiedup.remake.prison.LaborRecord;
|
||||
import com.tiedup.remake.prison.PrisonerManager;
|
||||
import com.tiedup.remake.prison.service.PrisonerService;
|
||||
import com.tiedup.remake.prison.service.EscapeMonitorService;
|
||||
import com.tiedup.remake.state.IRestrainable;
|
||||
import com.tiedup.remake.util.KidnappedHelper;
|
||||
import com.tiedup.remake.util.MessageDispatcher;
|
||||
@@ -341,7 +341,7 @@ public class EntityLaborGuard extends EntityDamsel {
|
||||
}
|
||||
|
||||
prisoner.sendSystemMessage(
|
||||
Component.literal("Attacking your guard is punished!").withStyle(
|
||||
Component.translatable("entity.tiedup.guard.attack_punished").withStyle(
|
||||
ChatFormatting.DARK_RED
|
||||
)
|
||||
);
|
||||
@@ -520,7 +520,7 @@ public class EntityLaborGuard extends EntityDamsel {
|
||||
labor.setGuardId(null);
|
||||
|
||||
// Trigger escape
|
||||
PrisonerService.get().escape(level, prisonerUUID, reason);
|
||||
EscapeMonitorService.get().escape(level, prisonerUUID, reason);
|
||||
|
||||
// Notify prisoner
|
||||
ServerPlayer prisoner = level
|
||||
@@ -529,8 +529,8 @@ public class EntityLaborGuard extends EntityDamsel {
|
||||
.getPlayer(prisonerUUID);
|
||||
if (prisoner != null) {
|
||||
prisoner.sendSystemMessage(
|
||||
Component.literal(
|
||||
"Your guard has been eliminated! You are free!"
|
||||
Component.translatable(
|
||||
"entity.tiedup.guard.eliminated_free"
|
||||
).withStyle(ChatFormatting.GREEN, ChatFormatting.BOLD)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -447,8 +447,8 @@ public class EntityMaid extends EntityKidnapperElite {
|
||||
player.position().distanceTo(this.position()) <= 50
|
||||
) {
|
||||
player.sendSystemMessage(
|
||||
net.minecraft.network.chat.Component.literal(
|
||||
"The maid has died. Work is paused. A replacement will arrive in 5 minutes."
|
||||
net.minecraft.network.chat.Component.translatable(
|
||||
"entity.tiedup.maid.died_work_paused"
|
||||
).withStyle(net.minecraft.ChatFormatting.GOLD)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -606,8 +606,8 @@ public class EntityMaster extends EntityKidnapperElite {
|
||||
|
||||
// Send warning message to pet
|
||||
pet.sendSystemMessage(
|
||||
Component.literal(
|
||||
this.getNpcName() + " caught you trying to escape!"
|
||||
Component.translatable(
|
||||
"entity.tiedup.master.caught_escaping", this.getNpcName()
|
||||
).withStyle(Style.EMPTY.withColor(MASTER_NAME_COLOR))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.tiedup.remake.entities;
|
||||
|
||||
import com.tiedup.remake.core.ModConfig;
|
||||
import com.tiedup.remake.core.SettingsAccessor;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.items.ModItems;
|
||||
import com.tiedup.remake.state.IBondageState;
|
||||
@@ -87,7 +87,7 @@ public class EntityRopeArrow extends AbstractArrow {
|
||||
bindChance = archer.getBindChanceForTarget(target.getUUID());
|
||||
} else {
|
||||
// Other shooters: default 50% chance
|
||||
bindChance = ModConfig.SERVER.ropeArrowBindChance.get();
|
||||
bindChance = SettingsAccessor.getRopeArrowBindChance();
|
||||
}
|
||||
|
||||
// Roll for bind chance
|
||||
@@ -95,7 +95,7 @@ public class EntityRopeArrow extends AbstractArrow {
|
||||
if (roll <= bindChance) {
|
||||
// Success! Bind the target
|
||||
ItemStack ropeItem = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(
|
||||
new net.minecraft.resources.ResourceLocation("tiedup", "ropes")
|
||||
net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("tiedup", "ropes")
|
||||
);
|
||||
targetState.equip(BodyRegionV2.ARMS, ropeItem);
|
||||
|
||||
|
||||
@@ -200,8 +200,8 @@ public class EntitySlaveTrader extends EntityKidnapperElite {
|
||||
Component.literal("[" + this.getNpcName() + "] ")
|
||||
.withStyle(Style.EMPTY.withColor(TRADER_NAME_COLOR))
|
||||
.append(
|
||||
Component.literal(
|
||||
"You don't have a trader token. Leave now, or I'll make you leave."
|
||||
Component.translatable(
|
||||
"entity.tiedup.trader.no_token_warning"
|
||||
).withStyle(ChatFormatting.RED)
|
||||
)
|
||||
);
|
||||
@@ -711,8 +711,8 @@ public class EntitySlaveTrader extends EntityKidnapperElite {
|
||||
ServerLevel level,
|
||||
BlockPos campCenter
|
||||
) {
|
||||
Component message = Component.literal(
|
||||
"A slave trader camp has been destroyed!"
|
||||
Component message = Component.translatable(
|
||||
"entity.tiedup.trader.camp_destroyed"
|
||||
).withStyle(ChatFormatting.GOLD, ChatFormatting.BOLD);
|
||||
|
||||
// Send to all players within 200 blocks
|
||||
|
||||
@@ -47,7 +47,7 @@ public class KidnapperCaptureEquipment {
|
||||
) {
|
||||
return mainHand;
|
||||
}
|
||||
return DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "ropes"));
|
||||
return DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "ropes"));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,7 +116,7 @@ public class KidnapperCaptureEquipment {
|
||||
public ItemStack getCollarItem() {
|
||||
// Kidnappers always have a shock collar to mark their captives
|
||||
return com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(
|
||||
new net.minecraft.resources.ResourceLocation("tiedup", "shock_collar"));
|
||||
net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("tiedup", "shock_collar"));
|
||||
}
|
||||
|
||||
// HELD ITEM MANAGEMENT
|
||||
|
||||
@@ -184,7 +184,7 @@ public class KidnapperItemSelector {
|
||||
*/
|
||||
public static ItemStack createItemById(String id, @Nullable ItemColor color) {
|
||||
ItemStack stack = DataDrivenBondageItem.createStack(
|
||||
new ResourceLocation("tiedup", id)
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", id)
|
||||
);
|
||||
if (color != null) {
|
||||
applyColor(stack, color);
|
||||
@@ -197,7 +197,7 @@ public class KidnapperItemSelector {
|
||||
*/
|
||||
public static ItemStack createMittens() {
|
||||
return DataDrivenBondageItem.createStack(
|
||||
new ResourceLocation("tiedup", "leather_mittens")
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", "leather_mittens")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ public class KidnapperItemSelector {
|
||||
*/
|
||||
public static ItemStack createEarplugs() {
|
||||
return DataDrivenBondageItem.createStack(
|
||||
new ResourceLocation("tiedup", "classic_earplugs")
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", "classic_earplugs")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ public class KidnapperJobManager {
|
||||
|
||||
// Put a shock collar on the worker AFTER untie/free
|
||||
ItemStack shockCollar = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(
|
||||
new net.minecraft.resources.ResourceLocation("tiedup", "shock_collar_auto"));
|
||||
net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("tiedup", "shock_collar_auto"));
|
||||
// Add kidnapper as owner so the collar is linked
|
||||
CollarHelper.addOwner(
|
||||
shockCollar,
|
||||
|
||||
@@ -93,15 +93,15 @@ public class MerchantTrade {
|
||||
*/
|
||||
public Component getPriceDisplay() {
|
||||
if (ingotPrice > 0 && nuggetPrice > 0) {
|
||||
return Component.literal(
|
||||
ingotPrice + " gold + " + nuggetPrice + " nuggets"
|
||||
return Component.translatable(
|
||||
"entity.tiedup.trade.price_both", ingotPrice, nuggetPrice
|
||||
);
|
||||
} else if (ingotPrice > 0) {
|
||||
return Component.literal(ingotPrice + " gold");
|
||||
return Component.translatable("entity.tiedup.trade.price_gold", ingotPrice);
|
||||
} else if (nuggetPrice > 0) {
|
||||
return Component.literal(nuggetPrice + " nuggets");
|
||||
return Component.translatable("entity.tiedup.trade.price_nuggets", nuggetPrice);
|
||||
} else {
|
||||
return Component.literal("Free");
|
||||
return Component.translatable("entity.tiedup.trade.price_free");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.tiedup.remake.labor.LaborTask;
|
||||
import com.tiedup.remake.prison.LaborRecord;
|
||||
import com.tiedup.remake.prison.PrisonerManager;
|
||||
import com.tiedup.remake.prison.PrisonerRecord;
|
||||
import com.tiedup.remake.prison.service.PrisonerService;
|
||||
import com.tiedup.remake.prison.service.EscapeMonitorService;
|
||||
import com.tiedup.remake.state.IRestrainable;
|
||||
import com.tiedup.remake.util.KidnappedHelper;
|
||||
import com.tiedup.remake.util.MessageDispatcher;
|
||||
@@ -202,8 +202,8 @@ public class GuardMonitorGoal extends Goal {
|
||||
);
|
||||
|
||||
prisoner.sendSystemMessage(
|
||||
Component.literal(
|
||||
"You are too far from your guard! Return within 15 seconds or escape will be triggered!"
|
||||
Component.translatable(
|
||||
"goal.tiedup.guard_monitor.too_far"
|
||||
).withStyle(ChatFormatting.RED, ChatFormatting.BOLD)
|
||||
);
|
||||
|
||||
@@ -227,8 +227,8 @@ public class GuardMonitorGoal extends Goal {
|
||||
);
|
||||
|
||||
prisoner.sendSystemMessage(
|
||||
Component.literal(
|
||||
"You returned to your guard. Stay close!"
|
||||
Component.translatable(
|
||||
"goal.tiedup.guard_monitor.returned"
|
||||
).withStyle(ChatFormatting.YELLOW)
|
||||
);
|
||||
}
|
||||
@@ -562,7 +562,7 @@ public class GuardMonitorGoal extends Goal {
|
||||
LaborRecord labor = manager.getLaborRecord(prisoner.getUUID());
|
||||
labor.setGuardId(null);
|
||||
|
||||
PrisonerService.get().escape(level, prisoner.getUUID(), reason);
|
||||
EscapeMonitorService.get().escape(level, prisoner.getUUID(), reason);
|
||||
|
||||
// Discard the guard entity itself
|
||||
guard.discard();
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
|
||||
import com.tiedup.remake.entities.AbstractTiedUpNpc;
|
||||
import com.tiedup.remake.entities.EntityKidnapper;
|
||||
import com.tiedup.remake.entities.EntityKidnapper.CaptivePriority;
|
||||
import com.tiedup.remake.entities.kidnapper.CaptivePriority;
|
||||
import com.tiedup.remake.entities.ai.StuckDetector;
|
||||
import com.tiedup.remake.entities.ai.WaypointNavigator;
|
||||
import com.tiedup.remake.v2.bondage.CollarHelper;
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
|
||||
import com.tiedup.remake.entities.EntityKidnapper;
|
||||
import com.tiedup.remake.prison.service.PrisonerService;
|
||||
import com.tiedup.remake.prison.service.EscapeMonitorService;
|
||||
import com.tiedup.remake.state.IBondageState;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import java.util.ArrayList;
|
||||
@@ -574,7 +574,7 @@ public class KidnapperDecideNextActionGoal extends Goal {
|
||||
captive.free(false); // false = don't drop leash (we take it)
|
||||
|
||||
// 3b. Clean up PrisonerManager state (CAPTURED -> FREE)
|
||||
PrisonerService.get().escape(
|
||||
EscapeMonitorService.get().escape(
|
||||
(ServerLevel) player.level(),
|
||||
player.getUUID(),
|
||||
"theft_release"
|
||||
@@ -604,8 +604,8 @@ public class KidnapperDecideNextActionGoal extends Goal {
|
||||
);
|
||||
|
||||
player.sendSystemMessage(
|
||||
Component.literal(
|
||||
"You're still tied up - struggle to break free!"
|
||||
Component.translatable(
|
||||
"goal.tiedup.kidnapper_decide.still_tied"
|
||||
).withStyle(ChatFormatting.YELLOW)
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.tiedup.remake.entities.ai.kidnapper;
|
||||
|
||||
import com.tiedup.remake.core.ModConfig;
|
||||
import com.tiedup.remake.core.SettingsAccessor;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.dialogue.EntityDialogueManager;
|
||||
import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
|
||||
@@ -155,7 +155,7 @@ public class KidnapperWaitForBuyerGoal extends Goal {
|
||||
|
||||
// Check if Master should spawn immediately for solo player
|
||||
IRestrainable captive = this.kidnapper.getCaptive();
|
||||
boolean isMasterEnabled = ModConfig.SERVER.enableMasterSpawn.get();
|
||||
boolean isMasterEnabled = SettingsAccessor.isEnableMasterSpawn();
|
||||
boolean isPlayer =
|
||||
captive != null && captive.asLivingEntity() instanceof Player;
|
||||
|
||||
@@ -171,12 +171,12 @@ public class KidnapperWaitForBuyerGoal extends Goal {
|
||||
// Don't return - let the goal continue so kidnapper waits for Master
|
||||
// The wait duration is set to allow Master to arrive
|
||||
this.waitDuration = 6000; // 5 minutes max wait for Master to arrive
|
||||
} else if (this.soloMode && ModConfig.SERVER.enableSoloFallback.get()) {
|
||||
} else if (this.soloMode && SettingsAccessor.isEnableSoloFallback()) {
|
||||
// Solo mode (non-player captive): use config timeout
|
||||
this.waitDuration = ModConfig.SERVER.soloTimeoutSeconds.get() * 20;
|
||||
this.waitDuration = SettingsAccessor.getSoloTimeoutSeconds() * 20;
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[KidnapperWaitForBuyerGoal] Solo mode detected, using {}s timeout",
|
||||
ModConfig.SERVER.soloTimeoutSeconds.get()
|
||||
SettingsAccessor.getSoloTimeoutSeconds()
|
||||
);
|
||||
} else {
|
||||
// Normal mode: random 5-8 minutes
|
||||
@@ -351,14 +351,14 @@ public class KidnapperWaitForBuyerGoal extends Goal {
|
||||
);
|
||||
|
||||
Component announcement = Component.literal("")
|
||||
.append(Component.literal("[SALE] ").withStyle(ChatFormatting.GOLD))
|
||||
.append(Component.translatable("goal.tiedup.kidnapper_sale.tag").withStyle(ChatFormatting.GOLD))
|
||||
.append(
|
||||
Component.literal(this.kidnapper.getNpcName()).withStyle(
|
||||
ChatFormatting.RED
|
||||
)
|
||||
)
|
||||
.append(
|
||||
Component.literal(" is selling ").withStyle(
|
||||
Component.translatable("goal.tiedup.kidnapper_sale.is_selling").withStyle(
|
||||
ChatFormatting.YELLOW
|
||||
)
|
||||
)
|
||||
@@ -367,13 +367,13 @@ public class KidnapperWaitForBuyerGoal extends Goal {
|
||||
ChatFormatting.AQUA
|
||||
)
|
||||
)
|
||||
.append(Component.literal(" for ").withStyle(ChatFormatting.YELLOW))
|
||||
.append(Component.translatable("goal.tiedup.kidnapper_sale.for").withStyle(ChatFormatting.YELLOW))
|
||||
.append(
|
||||
Component.literal(price.toDisplayString()).withStyle(
|
||||
ChatFormatting.GREEN
|
||||
)
|
||||
)
|
||||
.append(Component.literal(" at ").withStyle(ChatFormatting.YELLOW))
|
||||
.append(Component.translatable("goal.tiedup.kidnapper_sale.at").withStyle(ChatFormatting.YELLOW))
|
||||
.append(
|
||||
Component.literal(location).withStyle(ChatFormatting.WHITE)
|
||||
);
|
||||
@@ -426,7 +426,7 @@ public class KidnapperWaitForBuyerGoal extends Goal {
|
||||
}
|
||||
|
||||
// Handle solo mode fallback
|
||||
if (this.soloMode && ModConfig.SERVER.enableSoloFallback.get()) {
|
||||
if (this.soloMode && SettingsAccessor.isEnableSoloFallback()) {
|
||||
handleSoloModeFallback(captive);
|
||||
} else {
|
||||
// Normal multiplayer mode: just flee
|
||||
@@ -450,7 +450,7 @@ public class KidnapperWaitForBuyerGoal extends Goal {
|
||||
}
|
||||
|
||||
// Check if Master spawn is enabled and captive is a player
|
||||
boolean isMasterEnabled = ModConfig.SERVER.enableMasterSpawn.get();
|
||||
boolean isMasterEnabled = SettingsAccessor.isEnableMasterSpawn();
|
||||
boolean isPlayer = captive.asLivingEntity() instanceof Player;
|
||||
|
||||
if (isMasterEnabled && isPlayer) {
|
||||
@@ -460,7 +460,7 @@ public class KidnapperWaitForBuyerGoal extends Goal {
|
||||
}
|
||||
|
||||
// Fallback to original behavior
|
||||
double keepChance = ModConfig.SERVER.soloKeepChance.get();
|
||||
double keepChance = SettingsAccessor.getSoloKeepChance();
|
||||
double roll = this.kidnapper.getRandom().nextDouble();
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
|
||||
@@ -547,7 +547,7 @@ public class KidnapperWalkPrisonerGoal extends Goal {
|
||||
);
|
||||
|
||||
// 3. Change bind to DOGBINDER
|
||||
ItemStack dogBinder = DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "dogbinder"));
|
||||
ItemStack dogBinder = DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "dogbinder"));
|
||||
this.walkedPrisoner.equip(BodyRegionV2.ARMS, dogBinder);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
@@ -736,7 +736,7 @@ public class KidnapperWalkPrisonerGoal extends Goal {
|
||||
if (currentBind.isEmpty() || !prisoner.isTiedUp()) {
|
||||
// They freed themselves - put dogbinder back on
|
||||
ItemStack dogBinder = new ItemStack(
|
||||
DataDrivenBondageItem.createStack(new ResourceLocation("tiedup", "dogbinder")).getItem()
|
||||
DataDrivenBondageItem.createStack(ResourceLocation.fromNamespaceAndPath("tiedup", "dogbinder")).getItem()
|
||||
);
|
||||
prisoner.equip(BodyRegionV2.ARMS, dogBinder);
|
||||
}
|
||||
|
||||
@@ -331,9 +331,9 @@ public class MaidDeliverCaptiveGoal extends Goal {
|
||||
buyerEntity.getName().getString()
|
||||
);
|
||||
buyerEntity.sendSystemMessage(
|
||||
net.minecraft.network.chat.Component.literal(
|
||||
captiveEntity.getName().getString() +
|
||||
" is now on your leash."
|
||||
net.minecraft.network.chat.Component.translatable(
|
||||
"goal.tiedup.maid_deliver.on_leash",
|
||||
captiveEntity.getName().getString()
|
||||
).withStyle(net.minecraft.ChatFormatting.GREEN)
|
||||
);
|
||||
} else {
|
||||
@@ -342,9 +342,9 @@ public class MaidDeliverCaptiveGoal extends Goal {
|
||||
);
|
||||
kidnappedState.free(true);
|
||||
buyerEntity.sendSystemMessage(
|
||||
net.minecraft.network.chat.Component.literal(
|
||||
captiveEntity.getName().getString() +
|
||||
" has been delivered to you."
|
||||
net.minecraft.network.chat.Component.translatable(
|
||||
"goal.tiedup.maid_deliver.delivered",
|
||||
captiveEntity.getName().getString()
|
||||
).withStyle(net.minecraft.ChatFormatting.GREEN)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.tiedup.remake.entities.ai.maid.goals;
|
||||
|
||||
import com.tiedup.remake.cells.CellDataV2;
|
||||
import com.tiedup.remake.cells.CellRegistryV2;
|
||||
import com.tiedup.remake.core.ModConfig;
|
||||
import com.tiedup.remake.core.SettingsAccessor;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.EntityMaid;
|
||||
import com.tiedup.remake.labor.LaborTask;
|
||||
@@ -87,7 +87,7 @@ public class MaidAssignTaskGoal extends Goal {
|
||||
|
||||
if (phase == LaborRecord.WorkPhase.RESTING) {
|
||||
// Check if rest is complete (config: laborRestSeconds, default 120s)
|
||||
long restTicks = ModConfig.SERVER.laborRestSeconds.get() * 20L;
|
||||
long restTicks = SettingsAccessor.getLaborRestSeconds() * 20L;
|
||||
long elapsed = labor.getTimeInPhase(currentTime);
|
||||
if (elapsed >= restTicks) {
|
||||
labor.finishRest(currentTime);
|
||||
@@ -200,13 +200,13 @@ public class MaidAssignTaskGoal extends Goal {
|
||||
|
||||
// Notify prisoner
|
||||
prisoner.sendSystemMessage(
|
||||
net.minecraft.network.chat.Component.literal(
|
||||
"Task assigned: " + task.getDescription()
|
||||
net.minecraft.network.chat.Component.translatable(
|
||||
"goal.tiedup.maid_assign.task_assigned", task.getDescription()
|
||||
).withStyle(net.minecraft.ChatFormatting.YELLOW)
|
||||
);
|
||||
prisoner.sendSystemMessage(
|
||||
net.minecraft.network.chat.Component.literal(
|
||||
"Reward: " + task.getValue() + " emeralds toward your debt."
|
||||
net.minecraft.network.chat.Component.translatable(
|
||||
"goal.tiedup.maid_assign.task_reward", task.getValue()
|
||||
).withStyle(net.minecraft.ChatFormatting.GRAY)
|
||||
);
|
||||
|
||||
@@ -289,8 +289,8 @@ public class MaidAssignTaskGoal extends Goal {
|
||||
}
|
||||
|
||||
prisoner.sendSystemMessage(
|
||||
net.minecraft.network.chat.Component.literal(
|
||||
"Your debt is paid. You are FREE!"
|
||||
net.minecraft.network.chat.Component.translatable(
|
||||
"goal.tiedup.maid.debt_paid_free"
|
||||
).withStyle(
|
||||
net.minecraft.ChatFormatting.GREEN,
|
||||
net.minecraft.ChatFormatting.BOLD
|
||||
|
||||
@@ -346,9 +346,9 @@ public class MaidExtractGoal extends Goal {
|
||||
|
||||
// 7. Notify prisoner
|
||||
prisoner.sendSystemMessage(
|
||||
net.minecraft.network.chat.Component.literal(
|
||||
"You have been extracted for labor. Complete your task: " +
|
||||
task.getDescription()
|
||||
net.minecraft.network.chat.Component.translatable(
|
||||
"goal.tiedup.maid_extract.extracted",
|
||||
task.getDescription()
|
||||
).withStyle(net.minecraft.ChatFormatting.YELLOW)
|
||||
);
|
||||
|
||||
|
||||
@@ -146,8 +146,8 @@ public class MaidIdleGoal extends Goal {
|
||||
labor.completeTask(currentTime);
|
||||
|
||||
prisoner.sendSystemMessage(
|
||||
net.minecraft.network.chat.Component.literal(
|
||||
"Task complete! Walk back to camp."
|
||||
net.minecraft.network.chat.Component.translatable(
|
||||
"goal.tiedup.maid_idle.task_complete"
|
||||
).withStyle(net.minecraft.ChatFormatting.GREEN)
|
||||
);
|
||||
|
||||
@@ -207,17 +207,16 @@ public class MaidIdleGoal extends Goal {
|
||||
// Max punishment - fail task
|
||||
labor.failTask(currentTime);
|
||||
prisoner.sendSystemMessage(
|
||||
net.minecraft.network.chat.Component.literal(
|
||||
"Task failed due to inactivity! You will be returned to your cell."
|
||||
net.minecraft.network.chat.Component.translatable(
|
||||
"goal.tiedup.maid_idle.task_failed"
|
||||
).withStyle(net.minecraft.ChatFormatting.RED)
|
||||
);
|
||||
} else {
|
||||
// Warning
|
||||
prisoner.sendSystemMessage(
|
||||
net.minecraft.network.chat.Component.literal(
|
||||
"Warning: Work or face punishment! (" +
|
||||
shockLevel +
|
||||
"/3)"
|
||||
net.minecraft.network.chat.Component.translatable(
|
||||
"goal.tiedup.maid_idle.inactivity_warning",
|
||||
shockLevel
|
||||
).withStyle(net.minecraft.ChatFormatting.YELLOW)
|
||||
);
|
||||
|
||||
|
||||
@@ -165,18 +165,16 @@ public class MaidInitPrisonerGoal extends Goal {
|
||||
|
||||
// 6. Notify prisoner
|
||||
prisoner.sendSystemMessage(
|
||||
net.minecraft.network.chat.Component.literal(
|
||||
String.format(
|
||||
"You have been imprisoned. Your debt: %d emeralds.",
|
||||
totalRansom
|
||||
)
|
||||
net.minecraft.network.chat.Component.translatable(
|
||||
"goal.tiedup.maid_init.imprisoned",
|
||||
totalRansom
|
||||
).withStyle(net.minecraft.ChatFormatting.RED)
|
||||
);
|
||||
|
||||
if (!confiscated.isEmpty()) {
|
||||
prisoner.sendSystemMessage(
|
||||
net.minecraft.network.chat.Component.literal(
|
||||
"Your valuables have been confiscated."
|
||||
net.minecraft.network.chat.Component.translatable(
|
||||
"goal.tiedup.maid_init.confiscated"
|
||||
).withStyle(net.minecraft.ChatFormatting.GRAY)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -339,7 +339,7 @@ public class MaidReturnGoal extends Goal {
|
||||
cap.equip(
|
||||
BodyRegionV2.ARMS,
|
||||
com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(
|
||||
new net.minecraft.resources.ResourceLocation("tiedup", "ropes")
|
||||
net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("tiedup", "ropes")
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -671,8 +671,8 @@ public class MaidReturnGoal extends Goal {
|
||||
}
|
||||
|
||||
prisoner.sendSystemMessage(
|
||||
net.minecraft.network.chat.Component.literal(
|
||||
"Your debt is paid. You are FREE!"
|
||||
net.minecraft.network.chat.Component.translatable(
|
||||
"goal.tiedup.maid.debt_paid_free"
|
||||
).withStyle(
|
||||
net.minecraft.ChatFormatting.GREEN,
|
||||
net.minecraft.ChatFormatting.BOLD
|
||||
|
||||
@@ -395,7 +395,7 @@ public class MasterHumanChairGoal extends Goal {
|
||||
// Apply invisible dogbind for the pose animation
|
||||
if (!bindState.isTiedUp()) {
|
||||
ItemStack dogbind = DataDrivenBondageItem.createStack(
|
||||
new ResourceLocation("tiedup", "dogbinder")
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", "dogbinder")
|
||||
);
|
||||
CompoundTag tag = dogbind.getOrCreateTag();
|
||||
tag.putBoolean(NBT_HUMAN_CHAIR_BIND, true);
|
||||
|
||||
@@ -124,11 +124,10 @@ public class MasterInventoryInspectGoal extends Goal {
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet != null && !confiscatedItems.isEmpty()) {
|
||||
pet.sendSystemMessage(
|
||||
Component.literal(
|
||||
master.getNpcName() +
|
||||
" confiscated " +
|
||||
confiscatedItems.size() +
|
||||
" contraband item(s) from you!"
|
||||
Component.translatable(
|
||||
"goal.tiedup.master_inspect.confiscated",
|
||||
master.getNpcName(),
|
||||
confiscatedItems.size()
|
||||
).withStyle(
|
||||
Style.EMPTY.withColor(EntityMaster.MASTER_NAME_COLOR)
|
||||
)
|
||||
@@ -180,8 +179,8 @@ public class MasterInventoryInspectGoal extends Goal {
|
||||
*/
|
||||
private void performInspection(ServerPlayer pet) {
|
||||
pet.sendSystemMessage(
|
||||
Component.literal(
|
||||
master.getNpcName() + " is inspecting your inventory..."
|
||||
Component.translatable(
|
||||
"goal.tiedup.master_inspect.inspecting", master.getNpcName()
|
||||
).withStyle(Style.EMPTY.withColor(0xFFFF00))
|
||||
);
|
||||
|
||||
|
||||
@@ -455,13 +455,13 @@ public class MasterPunishGoal extends Goal {
|
||||
private ItemStack createAccessory(BodyRegionV2 region) {
|
||||
return switch (region) {
|
||||
case EYES -> DataDrivenBondageItem.createStack(
|
||||
new ResourceLocation("tiedup", "classic_blindfold")
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", "classic_blindfold")
|
||||
);
|
||||
case MOUTH -> DataDrivenBondageItem.createStack(
|
||||
new ResourceLocation("tiedup", "ball_gag")
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", "ball_gag")
|
||||
);
|
||||
case HANDS -> DataDrivenBondageItem.createStack(
|
||||
new ResourceLocation("tiedup", "leather_mittens")
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", "leather_mittens")
|
||||
);
|
||||
default -> ItemStack.EMPTY;
|
||||
};
|
||||
@@ -472,7 +472,7 @@ public class MasterPunishGoal extends Goal {
|
||||
*/
|
||||
private void applyTighten(ServerPlayer pet) {
|
||||
ItemStack armbinder = DataDrivenBondageItem.createStack(
|
||||
new ResourceLocation("tiedup", "armbinder")
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", "armbinder")
|
||||
);
|
||||
|
||||
// Mark as temporary
|
||||
|
||||
@@ -247,13 +247,13 @@ public class MasterRandomEventGoal extends Goal {
|
||||
private ItemStack createRandomAccessory(BodyRegionV2 region) {
|
||||
return switch (region) {
|
||||
case EYES -> DataDrivenBondageItem.createStack(
|
||||
new ResourceLocation("tiedup", "classic_blindfold")
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", "classic_blindfold")
|
||||
);
|
||||
case MOUTH -> DataDrivenBondageItem.createStack(
|
||||
new ResourceLocation("tiedup", "ball_gag")
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", "ball_gag")
|
||||
);
|
||||
case HANDS -> DataDrivenBondageItem.createStack(
|
||||
new ResourceLocation("tiedup", "leather_mittens")
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", "leather_mittens")
|
||||
);
|
||||
default -> ItemStack.EMPTY;
|
||||
};
|
||||
@@ -304,7 +304,7 @@ public class MasterRandomEventGoal extends Goal {
|
||||
PlayerBindState bindState = PlayerBindState.getInstance(pet);
|
||||
if (bindState != null && !bindState.isTiedUp()) {
|
||||
ItemStack dogbind = DataDrivenBondageItem.createStack(
|
||||
new ResourceLocation("tiedup", "dogbinder")
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", "dogbinder")
|
||||
);
|
||||
bindState.equip(BodyRegionV2.ARMS, dogbind);
|
||||
}
|
||||
|
||||
@@ -331,8 +331,8 @@ public class MasterTaskAssignGoal extends Goal {
|
||||
// FIX: Use MessageDispatcher for consistency with earplug system
|
||||
MessageDispatcher.sendChat(
|
||||
pet,
|
||||
Component.literal(
|
||||
master.getNpcName() + ": \"" + message + "\""
|
||||
Component.translatable(
|
||||
"goal.tiedup.master_assign.task_speech", master.getNpcName(), message
|
||||
).withStyle(Style.EMPTY.withColor(EntityMaster.MASTER_NAME_COLOR))
|
||||
);
|
||||
|
||||
|
||||
@@ -269,8 +269,8 @@ public class MasterTaskWatchGoal extends Goal {
|
||||
if (remainingSec > 0) {
|
||||
MessageDispatcher.sendActionBar(
|
||||
pet,
|
||||
Component.literal(
|
||||
task.name() + " - " + remainingSec + "s remaining"
|
||||
Component.translatable(
|
||||
"goal.tiedup.master_watch.time_remaining", task.name(), remainingSec
|
||||
).withStyle(Style.EMPTY.withColor(0xFFAA00))
|
||||
);
|
||||
}
|
||||
@@ -357,8 +357,8 @@ public class MasterTaskWatchGoal extends Goal {
|
||||
) {
|
||||
MessageDispatcher.sendChat(
|
||||
pet,
|
||||
Component.literal(
|
||||
master.getNpcName() + ": \"I'm waiting...\""
|
||||
Component.translatable(
|
||||
"goal.tiedup.master_watch.speak_waiting", master.getNpcName()
|
||||
).withStyle(
|
||||
Style.EMPTY.withColor(
|
||||
EntityMaster.MASTER_NAME_COLOR
|
||||
@@ -384,8 +384,8 @@ public class MasterTaskWatchGoal extends Goal {
|
||||
.nextInt(demandWarnings.length)];
|
||||
MessageDispatcher.sendChat(
|
||||
pet,
|
||||
Component.literal(
|
||||
master.getNpcName() + ": \"" + warning + "\""
|
||||
Component.translatable(
|
||||
"goal.tiedup.master_watch.demand_warning", master.getNpcName(), warning
|
||||
).withStyle(
|
||||
Style.EMPTY.withColor(
|
||||
EntityMaster.MASTER_NAME_COLOR
|
||||
@@ -431,8 +431,8 @@ public class MasterTaskWatchGoal extends Goal {
|
||||
String message = messages[master.getRandom().nextInt(messages.length)];
|
||||
MessageDispatcher.sendChat(
|
||||
pet,
|
||||
Component.literal(
|
||||
master.getNpcName() + ": \"" + message + "\""
|
||||
Component.translatable(
|
||||
"goal.tiedup.master_watch.punishment", master.getNpcName(), message
|
||||
).withStyle(Style.EMPTY.withColor(EntityMaster.MASTER_NAME_COLOR))
|
||||
);
|
||||
|
||||
|
||||
@@ -450,8 +450,8 @@ public class NpcGuardCommandGoal extends Goal {
|
||||
if (player.getUUID().equals(commanderUUID)) {
|
||||
String slaveName = slave.getNpcName();
|
||||
player.displayClientMessage(
|
||||
net.minecraft.network.chat.Component.literal(
|
||||
npc.getNpcName() + " is chasing " + slaveName + "!"
|
||||
net.minecraft.network.chat.Component.translatable(
|
||||
"goal.tiedup.guard_command.chasing", npc.getNpcName(), slaveName
|
||||
).withStyle(net.minecraft.ChatFormatting.RED),
|
||||
true // Action bar
|
||||
);
|
||||
@@ -545,8 +545,8 @@ public class NpcGuardCommandGoal extends Goal {
|
||||
// Send alert message (could be enhanced with dialogue system)
|
||||
String threatName = threat.getName().getString();
|
||||
player.displayClientMessage(
|
||||
net.minecraft.network.chat.Component.literal(
|
||||
npc.getNpcName() + " spotted: " + threatName + "!"
|
||||
net.minecraft.network.chat.Component.translatable(
|
||||
"goal.tiedup.guard_command.spotted", npc.getNpcName(), threatName
|
||||
).withStyle(net.minecraft.ChatFormatting.YELLOW),
|
||||
true // Action bar
|
||||
);
|
||||
|
||||
@@ -185,13 +185,13 @@ public class TraderSellGoal extends Goal {
|
||||
);
|
||||
|
||||
potentialBuyer.sendSystemMessage(
|
||||
Component.literal(
|
||||
"[" + trader.getNpcName() + "] " + greeting
|
||||
Component.translatable(
|
||||
"goal.tiedup.trader_sell.greeting", trader.getNpcName(), greeting
|
||||
).withStyle(net.minecraft.ChatFormatting.GOLD)
|
||||
);
|
||||
|
||||
potentialBuyer.sendSystemMessage(
|
||||
Component.literal("Right-click me to browse my stock.").withStyle(
|
||||
Component.translatable("goal.tiedup.trader_sell.browse_hint").withStyle(
|
||||
net.minecraft.ChatFormatting.GRAY
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.tiedup.remake.entities.damsel.components;
|
||||
|
||||
import com.tiedup.remake.core.ModConfig;
|
||||
import com.tiedup.remake.core.SettingsAccessor;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.EntityDamsel;
|
||||
import com.tiedup.remake.entities.ai.damsel.*;
|
||||
@@ -223,7 +223,7 @@ public class DamselAIController {
|
||||
Player.class,
|
||||
host
|
||||
.getBoundingBox()
|
||||
.inflate(ModConfig.SERVER.dialogueRadius.get())
|
||||
.inflate(SettingsAccessor.getDialogueRadius())
|
||||
);
|
||||
|
||||
// Get captor entity for comparison
|
||||
@@ -258,7 +258,7 @@ public class DamselAIController {
|
||||
}
|
||||
|
||||
// Reset cooldown
|
||||
int baseCooldown = ModConfig.SERVER.dialogueCooldown.get();
|
||||
int baseCooldown = SettingsAccessor.getDialogueCooldown();
|
||||
if (foundTarget) {
|
||||
// Full cooldown if we talked
|
||||
this.callForHelpCooldown =
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.tiedup.remake.entities.damsel.components;
|
||||
|
||||
import com.tiedup.remake.core.ModConfig;
|
||||
import com.tiedup.remake.core.SettingsAccessor;
|
||||
import com.tiedup.remake.dialogue.EntityDialogueManager;
|
||||
import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
|
||||
import java.util.HashMap;
|
||||
@@ -101,7 +101,7 @@ public class DamselDialogueHandler {
|
||||
|
||||
if (
|
||||
lastUsed != null &&
|
||||
(currentTick - lastUsed) < ModConfig.SERVER.dialogueCooldown.get()
|
||||
(currentTick - lastUsed) < SettingsAccessor.getDialogueCooldown()
|
||||
) {
|
||||
return false; // Still on cooldown
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.tiedup.remake.entities.damsel.components;
|
||||
|
||||
import com.tiedup.remake.core.ModConfig;
|
||||
import com.tiedup.remake.core.SettingsAccessor;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.AbstractTiedUpNpc;
|
||||
import com.tiedup.remake.state.ICaptor;
|
||||
@@ -137,7 +137,7 @@ public class NpcCaptivityManager {
|
||||
if (!entity.level().isClientSide()) {
|
||||
host.talkToPlayersInRadius(
|
||||
com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory.DAMSEL_FREED,
|
||||
ModConfig.SERVER.dialogueRadius.get()
|
||||
SettingsAccessor.getDialogueRadius()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.tiedup.remake.entities.damsel.components;
|
||||
|
||||
import com.tiedup.remake.core.ModConfig;
|
||||
import com.tiedup.remake.core.SettingsAccessor;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.AbstractTiedUpNpc;
|
||||
import com.tiedup.remake.items.base.ILockable;
|
||||
@@ -206,7 +206,7 @@ public class NpcEquipmentManager {
|
||||
if (!wasAlreadyTied && !entity.level().isClientSide()) {
|
||||
host.talkToPlayersInRadius(
|
||||
com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory.DAMSEL_CAPTURED,
|
||||
ModConfig.SERVER.dialogueRadius.get()
|
||||
SettingsAccessor.getDialogueRadius()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.tiedup.remake.entities.kidnapper;
|
||||
|
||||
import com.tiedup.remake.entities.EntityDamsel;
|
||||
import com.tiedup.remake.entities.EntityDamselShiny;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
|
||||
/**
|
||||
* Priority levels for captives when replacing prisoners in cells.
|
||||
* Higher priority captives will cause lower priority prisoners to be released.
|
||||
*/
|
||||
public enum CaptivePriority {
|
||||
DAMSEL(1),
|
||||
DAMSEL_SHINY(2),
|
||||
PLAYER(3);
|
||||
|
||||
private final int priority;
|
||||
|
||||
CaptivePriority(int priority) {
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
public int getPriority() {
|
||||
return priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the priority for an entity.
|
||||
*
|
||||
* @param entity The entity to check
|
||||
* @return The captive priority
|
||||
*/
|
||||
public static CaptivePriority fromEntity(LivingEntity entity) {
|
||||
if (entity instanceof Player) return PLAYER;
|
||||
if (entity instanceof EntityDamselShiny) return DAMSEL_SHINY;
|
||||
if (entity instanceof EntityDamsel) return DAMSEL;
|
||||
return DAMSEL; // Default for unknown entities
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this priority is higher than another.
|
||||
*/
|
||||
public boolean isHigherThan(CaptivePriority other) {
|
||||
return this.priority > other.priority;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.tiedup.remake.entities.kidnapper.components;
|
||||
|
||||
import java.util.UUID;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -26,4 +28,18 @@ public interface IAggressionHost {
|
||||
com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory category,
|
||||
int radius
|
||||
);
|
||||
|
||||
/**
|
||||
* Get the associated camp structure UUID.
|
||||
*/
|
||||
@Nullable
|
||||
UUID getAssociatedStructure();
|
||||
|
||||
/**
|
||||
* Punish a prisoner who attacks this kidnapper.
|
||||
*
|
||||
* @param player The player who attacked
|
||||
* @return true if punishment was applied
|
||||
*/
|
||||
boolean punishAttackingPrisoner(ServerPlayer player);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.tiedup.remake.entities.kidnapper.components;
|
||||
|
||||
import com.tiedup.remake.entities.KidnapperTheme;
|
||||
import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
|
||||
import com.tiedup.remake.personality.PersonalityType;
|
||||
import com.tiedup.remake.state.IRestrainable;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Host interface for KidnapperDialogue callbacks.
|
||||
* Provides access to entity state needed for dialogue speaker implementation.
|
||||
*/
|
||||
public interface IDialogueHost {
|
||||
/**
|
||||
* Get the NPC's display name.
|
||||
*/
|
||||
String getNpcName();
|
||||
|
||||
/**
|
||||
* Get the current theme.
|
||||
*/
|
||||
@Nullable
|
||||
KidnapperTheme getTheme();
|
||||
|
||||
/**
|
||||
* Check if kidnapper has any captives.
|
||||
*/
|
||||
boolean hasCaptives();
|
||||
|
||||
/**
|
||||
* Get the current AI state.
|
||||
*/
|
||||
KidnapperState getCurrentState();
|
||||
|
||||
/**
|
||||
* Get the current captive.
|
||||
*/
|
||||
@Nullable
|
||||
IRestrainable getCaptive();
|
||||
|
||||
/**
|
||||
* Get the entity as LivingEntity.
|
||||
*/
|
||||
LivingEntity asEntity();
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.tiedup.remake.entities.kidnapper.components;
|
||||
|
||||
import com.tiedup.remake.entities.KidnapperItemSelector;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.entity.EquipmentSlot;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Host interface for KidnapperLootManager callbacks.
|
||||
* Provides access to entity methods needed for loot drop and equipment management.
|
||||
*/
|
||||
public interface ILootHost {
|
||||
/**
|
||||
* Get the entity's level/world.
|
||||
*/
|
||||
Level level();
|
||||
|
||||
/**
|
||||
* Get item in the given equipment slot.
|
||||
*/
|
||||
ItemStack getItemBySlot(EquipmentSlot slot);
|
||||
|
||||
/**
|
||||
* Set item in the given equipment slot.
|
||||
*/
|
||||
void setItemSlot(EquipmentSlot slot, ItemStack stack);
|
||||
|
||||
/**
|
||||
* Get the entity's random source.
|
||||
*/
|
||||
RandomSource getRandom();
|
||||
|
||||
/**
|
||||
* Spawn an item at the entity's location.
|
||||
*/
|
||||
@Nullable
|
||||
ItemEntity spawnAtLocation(ItemStack stack);
|
||||
|
||||
/**
|
||||
* Get the NPC's display name for logging.
|
||||
*/
|
||||
String getNpcName();
|
||||
|
||||
/**
|
||||
* Get the item selection for this kidnapper (themed items).
|
||||
*/
|
||||
@Nullable
|
||||
KidnapperItemSelector.SelectionResult getItemSelection();
|
||||
}
|
||||
@@ -6,6 +6,9 @@ import com.tiedup.remake.state.IBondageState;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.damagesource.DamageSource;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -232,6 +235,69 @@ public class KidnapperAggressionSystem {
|
||||
this.robbedImmunity.clear();
|
||||
}
|
||||
|
||||
// DAMAGE PROCESSING
|
||||
|
||||
/** Damage reduction multiplier against monsters (50% damage taken) */
|
||||
private static final float MONSTER_DAMAGE_REDUCTION = 0.5f;
|
||||
|
||||
/**
|
||||
* Process incoming damage: track attacker, punish prisoners, expire protection,
|
||||
* apply monster damage reduction for camp kidnappers.
|
||||
*
|
||||
* @param source The damage source
|
||||
* @param amount The raw damage amount
|
||||
* @return The modified damage amount (0 or less means cancel the hurt)
|
||||
*/
|
||||
public float processIncomingDamage(DamageSource source, float amount) {
|
||||
float finalAmount = amount;
|
||||
|
||||
// Track the attacker for fight back system
|
||||
if (source.getEntity() instanceof LivingEntity attacker) {
|
||||
this.setLastAttacker(attacker);
|
||||
|
||||
// Punish prisoners who dare to attack us
|
||||
if (
|
||||
host.level() != null &&
|
||||
!host.level().isClientSide &&
|
||||
attacker instanceof ServerPlayer player
|
||||
) {
|
||||
host.punishAttackingPrisoner(player);
|
||||
|
||||
// Expire PROTECTED status if player attacks a kidnapper
|
||||
// Attacking a kidnapper voids your safe-exit window
|
||||
if (host.level() instanceof ServerLevel serverLevel) {
|
||||
com.tiedup.remake.prison.PrisonerManager pm =
|
||||
com.tiedup.remake.prison.PrisonerManager.get(
|
||||
serverLevel
|
||||
);
|
||||
if (
|
||||
pm.getState(player.getUUID()) ==
|
||||
com.tiedup.remake.prison.PrisonerState.PROTECTED
|
||||
) {
|
||||
pm.expireProtection(
|
||||
player.getUUID(),
|
||||
serverLevel.getGameTime()
|
||||
);
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[KidnapperAggressionSystem] Expired PROTECTED status for {} (attacked kidnapper)",
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Camp kidnappers take reduced damage from monsters (trained fighters)
|
||||
if (
|
||||
host.getAssociatedStructure() != null &&
|
||||
attacker instanceof net.minecraft.world.entity.monster.Monster
|
||||
) {
|
||||
finalAmount = amount * MONSTER_DAMAGE_REDUCTION;
|
||||
}
|
||||
}
|
||||
|
||||
return finalAmount;
|
||||
}
|
||||
|
||||
// HELPER METHODS
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.tiedup.remake.entities.kidnapper.components;
|
||||
|
||||
import com.tiedup.remake.cells.CellDataV2;
|
||||
import com.tiedup.remake.core.ModConfig;
|
||||
import com.tiedup.remake.core.SettingsAccessor;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.dialogue.EntityDialogueManager;
|
||||
import com.tiedup.remake.entities.EntityKidnapper;
|
||||
@@ -508,7 +508,7 @@ public class KidnapperCaptiveManager {
|
||||
if (record != null && record.isImprisoned()) {
|
||||
// Clear captivity state - prisoner escaped
|
||||
// Use centralized escape service for complete cleanup
|
||||
com.tiedup.remake.prison.service.PrisonerService.get().escape(
|
||||
com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
|
||||
serverLevel,
|
||||
captiveUUID,
|
||||
"kidnapper tied up"
|
||||
@@ -545,7 +545,7 @@ public class KidnapperCaptiveManager {
|
||||
if (record != null && record.isImprisoned()) {
|
||||
// Clear captivity state - captor died
|
||||
// Use centralized escape service for complete cleanup
|
||||
com.tiedup.remake.prison.service.PrisonerService.get().escape(
|
||||
com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
|
||||
serverLevel,
|
||||
captiveUUID,
|
||||
"kidnapper died"
|
||||
@@ -628,7 +628,7 @@ public class KidnapperCaptiveManager {
|
||||
|
||||
// Apply blindfold if enabled in config
|
||||
if (
|
||||
com.tiedup.remake.core.ModConfig.SERVER.abandonKeepsBlindfold.get()
|
||||
SettingsAccessor.isAbandonKeepsBlindfold()
|
||||
) {
|
||||
net.minecraft.world.item.ItemStack blindfold = getBlindfoldItem();
|
||||
if (
|
||||
@@ -652,7 +652,7 @@ public class KidnapperCaptiveManager {
|
||||
|
||||
// Remove restraints if NOT configured to keep them
|
||||
boolean keepBinds =
|
||||
com.tiedup.remake.core.ModConfig.SERVER.abandonKeepsBinds.get();
|
||||
SettingsAccessor.isAbandonKeepsBinds();
|
||||
if (!keepBinds) {
|
||||
// Full release including binds
|
||||
this.currentCaptive.untie(true);
|
||||
@@ -670,7 +670,7 @@ public class KidnapperCaptiveManager {
|
||||
if (record != null && record.isImprisoned()) {
|
||||
// Clear captivity state - prisoner freed
|
||||
// Use centralized escape service for complete cleanup
|
||||
com.tiedup.remake.prison.service.PrisonerService.get().escape(
|
||||
com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
|
||||
serverLevel,
|
||||
captiveUUID,
|
||||
"abandoned by kidnapper"
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.tiedup.remake.entities.kidnapper.components;
|
||||
|
||||
import com.tiedup.remake.dialogue.IDialogueSpeaker;
|
||||
import com.tiedup.remake.dialogue.SpeakerType;
|
||||
import com.tiedup.remake.entities.KidnapperTheme;
|
||||
import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
|
||||
import com.tiedup.remake.personality.PersonalityType;
|
||||
import com.tiedup.remake.state.IRestrainable;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* KidnapperDialogue - Implements IDialogueSpeaker methods for kidnapper entities.
|
||||
*
|
||||
* Handles:
|
||||
* 1. **Speaker Identity** - Name, type, personality mapping from theme
|
||||
* 2. **Mood Calculation** - State-based mood for dialogue selection
|
||||
* 3. **Target Relation** - Captor/captive relationship detection
|
||||
* 4. **Cooldown Tracking** - Dialogue spam prevention
|
||||
*
|
||||
* <p><b>Low complexity</b> - Pure state queries with no side effects.</p>
|
||||
*/
|
||||
public class KidnapperDialogue {
|
||||
|
||||
// FIELDS
|
||||
|
||||
/** Host callbacks */
|
||||
private final IDialogueHost host;
|
||||
|
||||
/** Dialogue cooldown timer (ticks remaining before next dialogue) */
|
||||
private int dialogueCooldown = 0;
|
||||
|
||||
// CONSTRUCTOR
|
||||
|
||||
public KidnapperDialogue(IDialogueHost host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
// IDIALOGUE SPEAKER METHODS
|
||||
|
||||
public String getDialogueName() {
|
||||
return host.getNpcName();
|
||||
}
|
||||
|
||||
public SpeakerType getSpeakerType() {
|
||||
return SpeakerType.KIDNAPPER;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PersonalityType getSpeakerPersonality() {
|
||||
// Map kidnapper theme to a personality-like behavior
|
||||
KidnapperTheme theme = host.getTheme();
|
||||
if (theme == null) {
|
||||
return PersonalityType.CALM;
|
||||
}
|
||||
|
||||
return switch (theme) {
|
||||
case ROPE, SHIBARI -> PersonalityType.CALM; // Traditional, methodical
|
||||
case TAPE, LEATHER, CHAIN -> PersonalityType.FIERCE; // Rough, aggressive
|
||||
case MEDICAL, ASYLUM -> PersonalityType.PROUD; // Clinical, professional
|
||||
case LATEX, RIBBON -> PersonalityType.PLAYFUL; // Playful, teasing
|
||||
case BEAM, WRAP -> PersonalityType.CURIOUS; // Experimental, modern
|
||||
};
|
||||
}
|
||||
|
||||
public int getSpeakerMood() {
|
||||
// Kidnappers mood is based on:
|
||||
// - Having a captive (+20)
|
||||
// - Current state (varies)
|
||||
int mood = 50;
|
||||
|
||||
if (host.hasCaptives()) {
|
||||
mood += 20;
|
||||
}
|
||||
|
||||
// State-based adjustment
|
||||
KidnapperState state = host.getCurrentState();
|
||||
if (state != null) {
|
||||
mood += switch (state) {
|
||||
case SELLING -> 10; // Excited about sale
|
||||
case JOB_WATCH -> 5;
|
||||
case GUARD -> 0;
|
||||
case CAPTURE -> 15; // Hunting excitement
|
||||
case PUNISH -> -10; // Stern
|
||||
case PATROL, IDLE, HUNT -> 0; // Neutral for patrolling/hunting
|
||||
case ALERT -> -5; // Concerned
|
||||
case TRANSPORT -> 5;
|
||||
};
|
||||
}
|
||||
|
||||
return Math.max(0, Math.min(100, mood));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getTargetRelation(Player player) {
|
||||
// Check if this kidnapper is holding the player captive
|
||||
IRestrainable captive = host.getCaptive();
|
||||
if (captive != null && captive.asLivingEntity() == player) {
|
||||
return "captor";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public LivingEntity asEntity() {
|
||||
return host.asEntity();
|
||||
}
|
||||
|
||||
// COOLDOWN
|
||||
|
||||
public int getDialogueCooldown() {
|
||||
return this.dialogueCooldown;
|
||||
}
|
||||
|
||||
public void setDialogueCooldown(int ticks) {
|
||||
this.dialogueCooldown = ticks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tick the dialogue cooldown.
|
||||
* Called from the main tick method.
|
||||
*/
|
||||
public void tickDialogueCooldown() {
|
||||
if (this.dialogueCooldown > 0) {
|
||||
this.dialogueCooldown--;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
package com.tiedup.remake.entities.kidnapper.components;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.KidnapperItemSelector;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.world.entity.EquipmentSlot;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/**
|
||||
* KidnapperLootManager - Manages stolen items, collar keys, and death drops.
|
||||
*
|
||||
* Handles:
|
||||
* 1. **Stolen Items** - Items taken from players via KidnapperThiefGoal (100% drop on death)
|
||||
* 2. **Collar Keys** - Keys generated when collaring captives (20% drop on death)
|
||||
* 3. **Equipment Drops** - Taser removal, token drop (5%), themed item drops (15%)
|
||||
*
|
||||
* <p><b>Low complexity</b> - List management with NBT persistence.</p>
|
||||
*/
|
||||
public class KidnapperLootManager {
|
||||
|
||||
// CONSTANTS
|
||||
|
||||
/** Token drop chance (5%) */
|
||||
private static final float TOKEN_DROP_CHANCE = 0.05f;
|
||||
|
||||
/** Themed item drop chance (15%) */
|
||||
private static final float THEMED_ITEM_DROP_CHANCE = 0.15f;
|
||||
|
||||
/** Collar key drop chance (20%) */
|
||||
private static final float COLLAR_KEY_DROP_CHANCE = 0.20f;
|
||||
|
||||
// FIELDS
|
||||
|
||||
/** Host callbacks */
|
||||
private final ILootHost host;
|
||||
|
||||
/** Items stolen from players via KidnapperThiefGoal. Dropped at 100% on death. */
|
||||
private final List<ItemStack> stolenItems = new ArrayList<>();
|
||||
|
||||
/** Collar keys generated when collaring captives. Dropped at 20% on death. */
|
||||
private final List<ItemStack> collarKeys = new ArrayList<>();
|
||||
|
||||
// CONSTRUCTOR
|
||||
|
||||
public KidnapperLootManager(ILootHost host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
// STOLEN ITEMS
|
||||
|
||||
/**
|
||||
* Add an item to the stolen items list.
|
||||
* Called by KidnapperThiefGoal when stealing from a player.
|
||||
*/
|
||||
public void addStolenItem(ItemStack stack) {
|
||||
if (!stack.isEmpty()) {
|
||||
this.stolenItems.add(stack.copy());
|
||||
}
|
||||
}
|
||||
|
||||
// COLLAR KEYS
|
||||
|
||||
/**
|
||||
* Add a collar key to be stored on this kidnapper.
|
||||
* Called by KidnapperCaptureGoal when collaring a captive.
|
||||
*/
|
||||
public void addCollarKey(ItemStack keyStack) {
|
||||
if (!keyStack.isEmpty()) {
|
||||
this.collarKeys.add(keyStack.copy());
|
||||
}
|
||||
}
|
||||
|
||||
// EQUIPMENT DROP LOGIC
|
||||
|
||||
/**
|
||||
* Pre-super drop: removes taser from hand to prevent vanilla drop.
|
||||
* Must be called BEFORE super.dropEquipment().
|
||||
* @see #dropPostEquipment() for the actual item drops
|
||||
*/
|
||||
public void dropEquipment() {
|
||||
// Check main hand for taser - don't drop it
|
||||
ItemStack mainHand = host.getItemBySlot(EquipmentSlot.MAINHAND);
|
||||
if (
|
||||
!mainHand.isEmpty() &&
|
||||
mainHand.getItem() instanceof com.tiedup.remake.items.ItemTaser
|
||||
) {
|
||||
host.setItemSlot(EquipmentSlot.MAINHAND, ItemStack.EMPTY);
|
||||
}
|
||||
|
||||
// Check off hand too
|
||||
ItemStack offHand = host.getItemBySlot(EquipmentSlot.OFFHAND);
|
||||
if (
|
||||
!offHand.isEmpty() &&
|
||||
offHand.getItem() instanceof com.tiedup.remake.items.ItemTaser
|
||||
) {
|
||||
host.setItemSlot(EquipmentSlot.OFFHAND, ItemStack.EMPTY);
|
||||
}
|
||||
|
||||
// Note: super.dropEquipment() is called by EntityKidnapper between
|
||||
// taser removal and the rest of the drops.
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle post-super drops: token, themed items, stolen items, collar keys.
|
||||
* Called from EntityKidnapper.dropEquipment() AFTER super.dropEquipment().
|
||||
*/
|
||||
public void dropPostEquipment() {
|
||||
if (host.level().isClientSide) return;
|
||||
|
||||
// Token drop: 5% chance when killed
|
||||
if (host.getRandom().nextFloat() < TOKEN_DROP_CHANCE) {
|
||||
ItemStack token = new ItemStack(
|
||||
com.tiedup.remake.items.ModItems.TOKEN.get()
|
||||
);
|
||||
host.spawnAtLocation(token);
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[KidnapperLootManager] {} dropped a token on death!",
|
||||
host.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
// Themed item drops (15% per item)
|
||||
KidnapperItemSelector.SelectionResult selection =
|
||||
host.getItemSelection();
|
||||
if (selection != null) {
|
||||
if (
|
||||
!selection.bind.isEmpty() &&
|
||||
host.getRandom().nextFloat() < THEMED_ITEM_DROP_CHANCE
|
||||
) {
|
||||
host.spawnAtLocation(selection.bind.copy());
|
||||
}
|
||||
if (
|
||||
selection.hasGag() &&
|
||||
host.getRandom().nextFloat() < THEMED_ITEM_DROP_CHANCE
|
||||
) {
|
||||
host.spawnAtLocation(selection.gag.copy());
|
||||
}
|
||||
if (
|
||||
selection.hasMittens() &&
|
||||
host.getRandom().nextFloat() < THEMED_ITEM_DROP_CHANCE
|
||||
) {
|
||||
host.spawnAtLocation(selection.mittens.copy());
|
||||
}
|
||||
if (
|
||||
selection.hasEarplugs() &&
|
||||
host.getRandom().nextFloat() < THEMED_ITEM_DROP_CHANCE
|
||||
) {
|
||||
host.spawnAtLocation(selection.earplugs.copy());
|
||||
}
|
||||
if (
|
||||
selection.hasBlindfold() &&
|
||||
host.getRandom().nextFloat() < THEMED_ITEM_DROP_CHANCE
|
||||
) {
|
||||
host.spawnAtLocation(selection.blindfold.copy());
|
||||
}
|
||||
}
|
||||
|
||||
// Drop stolen items at 100% rate (player's property)
|
||||
for (ItemStack stolen : this.stolenItems) {
|
||||
if (!stolen.isEmpty()) {
|
||||
host.spawnAtLocation(stolen);
|
||||
}
|
||||
}
|
||||
if (!this.stolenItems.isEmpty()) {
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[KidnapperLootManager] {} dropped {} stolen item(s) on death",
|
||||
host.getNpcName(),
|
||||
this.stolenItems.size()
|
||||
);
|
||||
}
|
||||
this.stolenItems.clear();
|
||||
|
||||
// Drop collar keys at 20% rate
|
||||
for (ItemStack key : this.collarKeys) {
|
||||
if (!key.isEmpty() && host.getRandom().nextFloat() < COLLAR_KEY_DROP_CHANCE) {
|
||||
host.spawnAtLocation(key);
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[KidnapperLootManager] {} dropped a collar key on death",
|
||||
host.getNpcName()
|
||||
);
|
||||
}
|
||||
}
|
||||
this.collarKeys.clear();
|
||||
}
|
||||
|
||||
// NBT SERIALIZATION
|
||||
|
||||
/**
|
||||
* Save loot data to NBT.
|
||||
*/
|
||||
public void saveToNBT(CompoundTag tag) {
|
||||
// Save stolen items
|
||||
if (!this.stolenItems.isEmpty()) {
|
||||
ListTag stolenTag = new ListTag();
|
||||
for (ItemStack stack : this.stolenItems) {
|
||||
if (!stack.isEmpty()) {
|
||||
stolenTag.add(stack.save(new CompoundTag()));
|
||||
}
|
||||
}
|
||||
tag.put("StolenItems", stolenTag);
|
||||
}
|
||||
|
||||
// Save collar keys
|
||||
if (!this.collarKeys.isEmpty()) {
|
||||
ListTag keysTag = new ListTag();
|
||||
for (ItemStack key : this.collarKeys) {
|
||||
if (!key.isEmpty()) {
|
||||
keysTag.add(key.save(new CompoundTag()));
|
||||
}
|
||||
}
|
||||
tag.put("CollarKeys", keysTag);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load loot data from NBT.
|
||||
*/
|
||||
public void loadFromNBT(CompoundTag tag) {
|
||||
// Load stolen items
|
||||
this.stolenItems.clear();
|
||||
if (tag.contains("StolenItems", Tag.TAG_LIST)) {
|
||||
ListTag stolenTag = tag.getList("StolenItems", Tag.TAG_COMPOUND);
|
||||
for (int i = 0; i < stolenTag.size(); i++) {
|
||||
ItemStack stack = ItemStack.of(stolenTag.getCompound(i));
|
||||
if (!stack.isEmpty()) {
|
||||
this.stolenItems.add(stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load collar keys
|
||||
this.collarKeys.clear();
|
||||
if (tag.contains("CollarKeys", Tag.TAG_LIST)) {
|
||||
ListTag keysTag = tag.getList("CollarKeys", Tag.TAG_COMPOUND);
|
||||
for (int i = 0; i < keysTag.size(); i++) {
|
||||
ItemStack key = ItemStack.of(keysTag.getCompound(i));
|
||||
if (!key.isEmpty()) {
|
||||
this.collarKeys.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package com.tiedup.remake.entities.kidnapper.hosts;
|
||||
import com.tiedup.remake.dialogue.EntityDialogueManager;
|
||||
import com.tiedup.remake.entities.EntityKidnapper;
|
||||
import com.tiedup.remake.entities.kidnapper.components.IAggressionHost;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -35,4 +37,15 @@ public class AggressionHost implements IAggressionHost {
|
||||
) {
|
||||
entity.talkToPlayersInRadius(category, radius);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public UUID getAssociatedStructure() {
|
||||
return entity.getAssociatedStructure();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean punishAttackingPrisoner(ServerPlayer player) {
|
||||
return entity.punishAttackingPrisoner(player);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.tiedup.remake.entities.kidnapper.hosts;
|
||||
|
||||
import com.tiedup.remake.entities.EntityKidnapper;
|
||||
import com.tiedup.remake.entities.KidnapperTheme;
|
||||
import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
|
||||
import com.tiedup.remake.entities.kidnapper.components.IDialogueHost;
|
||||
import com.tiedup.remake.state.IRestrainable;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Host implementation for KidnapperDialogue callbacks.
|
||||
*/
|
||||
public class DialogueHost implements IDialogueHost {
|
||||
|
||||
private final EntityKidnapper entity;
|
||||
|
||||
public DialogueHost(EntityKidnapper entity) {
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNpcName() {
|
||||
return entity.getNpcName();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public KidnapperTheme getTheme() {
|
||||
return entity.getTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasCaptives() {
|
||||
return entity.hasCaptives();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KidnapperState getCurrentState() {
|
||||
return entity.getCurrentState();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public IRestrainable getCaptive() {
|
||||
return entity.getCaptive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LivingEntity asEntity() {
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.tiedup.remake.entities.kidnapper.hosts;
|
||||
|
||||
import com.tiedup.remake.entities.EntityKidnapper;
|
||||
import com.tiedup.remake.entities.KidnapperItemSelector;
|
||||
import com.tiedup.remake.entities.kidnapper.components.ILootHost;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.entity.EquipmentSlot;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Host implementation for KidnapperLootManager callbacks.
|
||||
*/
|
||||
public class LootHost implements ILootHost {
|
||||
|
||||
private final EntityKidnapper entity;
|
||||
|
||||
public LootHost(EntityKidnapper entity) {
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Level level() {
|
||||
return entity.level();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack getItemBySlot(EquipmentSlot slot) {
|
||||
return entity.getItemBySlot(slot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItemSlot(EquipmentSlot slot, ItemStack stack) {
|
||||
entity.setItemSlot(slot, stack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RandomSource getRandom() {
|
||||
return entity.getRandom();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ItemEntity spawnAtLocation(ItemStack stack) {
|
||||
return entity.spawnAtLocation(stack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNpcName() {
|
||||
return entity.getNpcName();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public KidnapperItemSelector.SelectionResult getItemSelection() {
|
||||
return entity.getItemSelection();
|
||||
}
|
||||
}
|
||||
@@ -165,8 +165,8 @@ public class MaidPrisonInteraction {
|
||||
|
||||
sayToPlayer(player, "Very well. Here are your tools.");
|
||||
player.sendSystemMessage(
|
||||
Component.literal(
|
||||
"The maid manually assigns you to: " + task.getDescription()
|
||||
Component.translatable(
|
||||
"entity.tiedup.maid.manual_assign", task.getDescription()
|
||||
).withStyle(ChatFormatting.YELLOW)
|
||||
);
|
||||
|
||||
@@ -206,8 +206,8 @@ public class MaidPrisonInteraction {
|
||||
|
||||
sayToPlayer(worker, "Good. You've completed your task.");
|
||||
worker.sendSystemMessage(
|
||||
Component.literal(
|
||||
"A Maid will come to collect you shortly."
|
||||
Component.translatable(
|
||||
"entity.tiedup.maid.collect_shortly"
|
||||
).withStyle(ChatFormatting.YELLOW)
|
||||
);
|
||||
|
||||
@@ -229,13 +229,13 @@ public class MaidPrisonInteraction {
|
||||
@Nullable LaborRecord laborRecord
|
||||
) {
|
||||
player.sendSystemMessage(
|
||||
Component.literal("=== Labor Status ===").withStyle(
|
||||
Component.translatable("entity.tiedup.maid.labor_status_header").withStyle(
|
||||
ChatFormatting.GOLD
|
||||
)
|
||||
);
|
||||
|
||||
player.sendSystemMessage(
|
||||
Component.literal("State: " + record.getState().name()).withStyle(
|
||||
Component.translatable("entity.tiedup.maid.labor_status_state", record.getState().name()).withStyle(
|
||||
ChatFormatting.GRAY
|
||||
)
|
||||
);
|
||||
@@ -243,13 +243,13 @@ public class MaidPrisonInteraction {
|
||||
if (laborRecord != null && laborRecord.getTask() != null) {
|
||||
LaborTask task = laborRecord.getTask();
|
||||
player.sendSystemMessage(
|
||||
Component.literal("Task: " + task.getDescription()).withStyle(
|
||||
Component.translatable("entity.tiedup.maid.labor_status_task", task.getDescription()).withStyle(
|
||||
ChatFormatting.GRAY
|
||||
)
|
||||
);
|
||||
player.sendSystemMessage(
|
||||
Component.literal(
|
||||
"Progress: " + task.getProgress() + "/" + task.getQuota()
|
||||
Component.translatable(
|
||||
"entity.tiedup.maid.labor_status_progress", task.getProgress(), task.getQuota()
|
||||
).withStyle(ChatFormatting.GRAY)
|
||||
);
|
||||
}
|
||||
@@ -262,8 +262,8 @@ public class MaidPrisonInteraction {
|
||||
RansomRecord ransom = manager.getRansomRecord(player.getUUID());
|
||||
int remaining = ransom != null ? ransom.getRemainingDebt() : 0;
|
||||
player.sendSystemMessage(
|
||||
Component.literal(
|
||||
"Remaining debt: " + remaining + " emeralds"
|
||||
Component.translatable(
|
||||
"entity.tiedup.maid.labor_status_debt", remaining
|
||||
).withStyle(ChatFormatting.GRAY)
|
||||
);
|
||||
|
||||
@@ -273,8 +273,8 @@ public class MaidPrisonInteraction {
|
||||
laborRecord.getPhase() == LaborRecord.WorkPhase.PENDING_EXTRACTION
|
||||
) {
|
||||
player.sendSystemMessage(
|
||||
Component.literal(
|
||||
"Shift+Right-Click to manually start task"
|
||||
Component.translatable(
|
||||
"entity.tiedup.maid.labor_hint_start_task"
|
||||
).withStyle(ChatFormatting.YELLOW)
|
||||
);
|
||||
} else if (
|
||||
@@ -283,8 +283,8 @@ public class MaidPrisonInteraction {
|
||||
laborRecord.getTask().isComplete()
|
||||
) {
|
||||
player.sendSystemMessage(
|
||||
Component.literal(
|
||||
"Shift+Right-Click to manually turn in task"
|
||||
Component.translatable(
|
||||
"entity.tiedup.maid.labor_hint_turn_in"
|
||||
).withStyle(ChatFormatting.GREEN)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ public class MasterPetManager {
|
||||
|
||||
// Create a choke collar for pet play
|
||||
ItemStack chokeCollar = com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(
|
||||
new net.minecraft.resources.ResourceLocation("tiedup", "choke_collar")
|
||||
net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("tiedup", "choke_collar")
|
||||
);
|
||||
|
||||
// Configure for pet play BEFORE equipping
|
||||
@@ -199,8 +199,8 @@ public class MasterPetManager {
|
||||
|
||||
// Send message to player
|
||||
player.sendSystemMessage(
|
||||
Component.literal(
|
||||
"You are free! Your master " + master.getNpcName() + " is gone."
|
||||
Component.translatable(
|
||||
"entity.tiedup.master.pet_freed", master.getNpcName()
|
||||
).withStyle(Style.EMPTY.withColor(0x00FF00))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -89,8 +89,8 @@ public class CampChestHandler {
|
||||
|
||||
if (entity instanceof Player player) {
|
||||
player.displayClientMessage(
|
||||
Component.literal(
|
||||
"This chest is locked by the camp!"
|
||||
Component.translatable(
|
||||
"msg.tiedup.event.chest_locked_by_camp"
|
||||
).withStyle(ChatFormatting.RED),
|
||||
true
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.tiedup.remake.entities.EntitySlaveTrader;
|
||||
import com.tiedup.remake.entities.ModEntities;
|
||||
// Prison system v2
|
||||
import com.tiedup.remake.prison.PrisonerManager;
|
||||
import com.tiedup.remake.prison.service.PrisonerService;
|
||||
import com.tiedup.remake.prison.service.EscapeMonitorService;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.ChatFormatting;
|
||||
@@ -75,7 +75,7 @@ public class CampManagementHandler {
|
||||
}
|
||||
|
||||
// Prison system v2 - tick escape service (handles escape detection)
|
||||
PrisonerService.get().tick(level.getServer(), currentTime);
|
||||
EscapeMonitorService.get().tick(level.getServer(), currentTime);
|
||||
|
||||
// Prison system v2 - tick protection expiry
|
||||
PrisonerManager.get(level).tickProtectionExpiry(currentTime);
|
||||
@@ -286,10 +286,9 @@ public class CampManagementHandler {
|
||||
.getPlayer(prisonerId);
|
||||
if (player != null) {
|
||||
player.sendSystemMessage(
|
||||
Component.literal(
|
||||
"A new maid, " +
|
||||
maidName +
|
||||
", has arrived. Work resumes."
|
||||
Component.translatable(
|
||||
"msg.tiedup.event.new_maid_arrived",
|
||||
maidName
|
||||
).withStyle(ChatFormatting.GOLD)
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user