From c711ae2831aa730a03c9972569a841cac1b80078 Mon Sep 17 00:00:00 2001 From: The_miro Date: Wed, 26 Aug 2026 20:28:07 +0200 Subject: [PATCH] Solve gem placement instead of dialling it in Gem slots could not be positioned by hand because translation is not the gem's position. The gem model is `item/generated`: a 2x2 sprite block at the dead centre (8,8) of a 16x16 sheet, so its geometry sits 8px from the model origin on every axis. MIAPI's Transform.toMatrix is T * Rx * Ry * Rz * S with no centring offset, so rotation pivots on that origin and throws the gem up to 12px away. Translation is whatever cancels the throw. world = pivot + flip(T + R*S*c), flip = negate x,y, c = (8,8,8) `flip` is the block-model(+y up) -> ModelPart(+y down) conversion. Inverting it gives T from a desired anchor, so tools/solve_gems.py takes targets in plain world pixels and asserts the result forward-evaluates back. The helmet gem is the exception: it chains through the hat slot, which scales by 1.25 and lifts by -3.925, and does NOT go through the flip. Solving it with the flip inverts y and drops the gem past the chin to the floor. The chain is recorded against two in-game sightings that pin it. Also: - Drop the stray no-origin model entry from all eight limb gem/case modules. Without an origin a model falls through the slot's z180 and lands mirrored in x and y - which is why a right-arm gem showed up above the left shoulder. - Give the helmet and backtank gems origin-carrying modules. Their slots took `gem_armor_medium`, whose only members declare no origin, so the gem was drawn in the body pass and never appeared. Apothic cases included so repointing the slots loses nothing. - Compensate the z180 in the helmet model UVs. Geometry in these models is already authored around the flip; UVs cannot be, so every face rendered rotated 180. The face that shows on top is the one authored `down`. - Remove the misaligned placeholder bezels from the four limb sockets. Co-Authored-By: Claude Opus 5 --- README.md | 43 +- placement.bbmodel | 2211 +++++++++++++++++ .../assets/cmmodular/lang/en_us.json | 4 + .../armor/model/chest_back/space/default.json | 83 +- .../chest_back/space_socket/default.json | 52 +- .../armor/model/helmet/space/default.json | 83 +- .../model/helmet/space_socket/default.json | 52 +- .../textures/equipment/arm_left_socket.png | Bin 141 -> 275 bytes .../textures/equipment/arm_right_socket.png | Bin 141 -> 281 bytes .../textures/equipment/leg_left_socket.png | Bin 126 -> 264 bytes .../textures/equipment/leg_right_socket.png | Bin 126 -> 288 bytes .../textures/equipment/life_support_pack.png | Bin 175 -> 199 bytes .../equipment/life_support_pack_socket.png | Bin 235 -> 291 bytes .../textures/equipment/space_helmet.png | Bin 217 -> 501 bytes .../equipment/space_helmet_socket.png | Bin 272 -> 553 bytes .../cmmodular/textures/items/arcana_core.png | Bin 356 -> 636 bytes .../cmmodular/textures/items/space_helmet.png | Bin 219 -> 376 bytes .../textures/items/space_helmet_socket.png | Bin 257 -> 382 bytes .../assets/minecraft/atlases/blocks.json | 14 + .../miapi/modules/gem/limb/case_arm_left.json | 26 +- .../modules/gem/limb/case_arm_right.json | 26 +- .../miapi/modules/gem/limb/case_leg_left.json | 26 +- .../modules/gem/limb/case_leg_right.json | 26 +- .../modules/gem/space/case_backtank.json | 51 + .../miapi/modules/gem/space/case_helmet.json | 51 + .../miapi/modules/armor/socket/arm_left.json | 48 +- .../miapi/modules/armor/socket/arm_right.json | 48 +- .../miapi/modules/armor/socket/leg_left.json | 30 +- .../miapi/modules/armor/socket/leg_right.json | 30 +- .../miapi/modules/armor/space/helmet.json | 37 +- .../modules/armor/space/helmet_socket.json | 76 +- .../modules/armor/space/life_support.json | 18 +- .../armor/space/life_support_socket.json | 56 +- .../miapi/modules/gem/limb/gem_arm_left.json | 26 +- .../miapi/modules/gem/limb/gem_arm_right.json | 26 +- .../miapi/modules/gem/limb/gem_leg_left.json | 26 +- .../miapi/modules/gem/limb/gem_leg_right.json | 26 +- .../miapi/modules/gem/space/gem_backtank.json | 52 + .../miapi/modules/gem/space/gem_helmet.json | 52 + tools/ARMOUR_EDITOR.py | 105 +- tools/bb-apply.sh | 12 + tools/bb-open.sh | 16 + tools/bb_export.py | 168 ++ tools/bb_import.py | 146 ++ tools/blockbench.py | 263 ++ tools/live-edit.lua | 62 + tools/live-reload.sh | 125 + tools/mc-type.sh | 38 + tools/mcmodel.py | 39 +- tools/redeploy.sh | 45 + tools/solve_gems.py | 106 + 51 files changed, 4126 insertions(+), 298 deletions(-) create mode 100644 placement.bbmodel create mode 100644 src/main/resources/assets/minecraft/atlases/blocks.json create mode 100644 src/main/resources/packs/apotheosis/data/cmmodular/miapi/modules/gem/space/case_backtank.json create mode 100644 src/main/resources/packs/apotheosis/data/cmmodular/miapi/modules/gem/space/case_helmet.json create mode 100644 src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/space/gem_backtank.json create mode 100644 src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/space/gem_helmet.json create mode 100755 tools/bb-apply.sh create mode 100755 tools/bb-open.sh create mode 100644 tools/bb_export.py create mode 100644 tools/bb_import.py create mode 100644 tools/blockbench.py create mode 100644 tools/live-edit.lua create mode 100755 tools/live-reload.sh create mode 100755 tools/mc-type.sh create mode 100755 tools/redeploy.sh create mode 100755 tools/solve_gems.py diff --git a/README.md b/README.md index e852718..b1e0b2b 100644 --- a/README.md +++ b/README.md @@ -166,17 +166,29 @@ down, -z forward. A pauldron gem is turned a quarter turn about y to face out of the shoulder; a knee gem faces forward already. Right-hand pieces are the same numbers with x and the turn negated. -They are not right yet, and two attempts to make them right are worth recording -so that they are not repeated. Naming the limb as the gem slot's `origin` - so -the offset lands in the entry the limb pass reads and the gem follows the arm - -composes correctly on paper and in game puts the gems several blocks away from -the wearer, floating. And `tools/preview_armour.py`, which applies the same chain -MIAPI does and draws the result from any angle, agrees with the paper rather than -with the game: given the configuration that in practice left gems at the neck, it -puts them on the pauldron. The tool is worth keeping for reading Armory's own -geometry - where the plate bands are, which face is which, how far the chain -scales a limb - and it should not be trusted to predict a gem's final position -until it can reproduce a symptom somebody has actually seen. +What made them hard is now known, and it was an arithmetic mistake rather than +a matter of taste. A slot transform does not act in the model's own space. MIAPI +accumulates transforms per origin with `TransformMap.add`, which calls +`Transform.merge(what is there, what is being added)`, and that merge computes +`new.toMatrix().mul(accumulated.toMatrix())` - so the plate's transform applies +to the geometry first and the slot's afterwards, in the body part's frame. An +offset written as though it were measured on the model lands somewhere else +entirely, which is why the gems sat at the collar with their sides swapped and +the knee gems ended up at the belly. + +So they are solved in that frame instead: take the middle of the outward face of +the socket this repo draws, carry it through Armory's own plate transform into +the limb's frame, turn the gem's sprite onto that face, and undo the plate's +non-uniform stretch so the stone comes out the size it was drawn. The numbers in +`packs/armory/.../socket/*.json` are that solution, and each one is checked by +composing it the way MIAPI does - including the `fromMatrix` round trip, which +2.3.8 performs on *every* merge and which drops shear wherever a rotation meets +a non-uniform scale. Left and right are exact mirrors of one another, and each +gem lands on its socket. + +`tools/preview_armour.py` and `ARMOUR_EDITOR.py` read Armory's own geometry for +the same purpose - where the plate bands are, which face is which, how far the +chain scales a limb. One thing does not survive any of this and is not fixable from here. A slot transform that names an origin has nothing left for the inventory icon, which @@ -386,6 +398,15 @@ of them lands somewhere interpolated and muddy. `textures/*/templates/` holds blanks to draw over - the same size as the real thing, with every region a model samples outlined and the seven shades parked in a corner no model reads, so they can be picked straight out of the file. + +`atlases/blocks.json` is what puts these two directories on the block atlas, and +without it a worn module is drawn untextured. Minecraft stitches +`textures/block` and `textures/item` and nothing else - everything else reaches +the atlas only by being referenced from a model the game itself loads, and a +module model is loaded by MIAPI rather than by Minecraft, so nothing of ours is +referenced by the time the atlas is built. Armory sidesteps it by filing its +module art under `miapi:item/...`, inside the directory vanilla already takes; +naming our own two directories is the same fix without moving the art. `templates_guide.png` is a single sheet showing the shades with their values, the cube net with each face labelled, and the vanilla armour layout, for when a new piece is wanted rather than a repaint of an existing one. diff --git a/placement.bbmodel b/placement.bbmodel new file mode 100644 index 0000000..a4e48b6 --- /dev/null +++ b/placement.bbmodel @@ -0,0 +1,2211 @@ +{ + "meta": { + "format_version": "4.10", + "model_format": "free", + "box_uv": false + }, + "name": "cmmodular placement", + "model_identifier": "", + "visible_box": [ + 4, + 4, + 0 + ], + "variable_placeholders": "", + "variable_placeholder_buttons": [], + "unhandled_root_fields": {}, + "resolution": { + "width": 16, + "height": 16 + }, + "elements": [ + { + "name": "head (wearer)", + "box_uv": false, + "rescale": false, + "locked": true, + "render_order": "default", + "allow_mirror_modeling": true, + "from": [ + -4.0, + 24.0, + -4.0 + ], + "to": [ + 4.0, + 32.0, + 4.0 + ], + "autouv": 0, + "color": 8, + "origin": [ + 0.0, + 28.0, + 0.0 + ], + "rotation": [ + 0.0, + -0.0, + 0.0 + ], + "faces": { + "east": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "west": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "up": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "down": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "south": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "north": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + } + }, + "type": "cube", + "uuid": "dcebf35d-2a7c-420e-bea1-53538457267b" + }, + { + "name": "body (wearer)", + "box_uv": false, + "rescale": false, + "locked": true, + "render_order": "default", + "allow_mirror_modeling": true, + "from": [ + -4.0, + 12.0, + -2.0 + ], + "to": [ + 4.0, + 24.0, + 2.0 + ], + "autouv": 0, + "color": 8, + "origin": [ + 0.0, + 18.0, + 0.0 + ], + "rotation": [ + 0.0, + -0.0, + 0.0 + ], + "faces": { + "east": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "west": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "up": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "down": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "south": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "north": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + } + }, + "type": "cube", + "uuid": "f9c76f4a-bcf9-472a-b480-03e4a61242ef" + }, + { + "name": "right_arm (wearer)", + "box_uv": false, + "rescale": false, + "locked": true, + "render_order": "default", + "allow_mirror_modeling": true, + "from": [ + 4.0, + 12.0, + -2.0 + ], + "to": [ + 8.0, + 24.0, + 2.0 + ], + "autouv": 0, + "color": 8, + "origin": [ + 6.0, + 18.0, + 0.0 + ], + "rotation": [ + 0.0, + -0.0, + 0.0 + ], + "faces": { + "east": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "west": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "up": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "down": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "south": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "north": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + } + }, + "type": "cube", + "uuid": "47dd1a7d-4890-4b84-919e-89eae5e618cd" + }, + { + "name": "left_arm (wearer)", + "box_uv": false, + "rescale": false, + "locked": true, + "render_order": "default", + "allow_mirror_modeling": true, + "from": [ + -8.0, + 12.0, + -2.0 + ], + "to": [ + -4.0, + 24.0, + 2.0 + ], + "autouv": 0, + "color": 8, + "origin": [ + -6.0, + 18.0, + 0.0 + ], + "rotation": [ + 0.0, + -0.0, + 0.0 + ], + "faces": { + "east": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "west": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "up": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "down": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "south": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "north": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + } + }, + "type": "cube", + "uuid": "a3614afb-2457-496c-a149-eca64f411925" + }, + { + "name": "right_leg (wearer)", + "box_uv": false, + "rescale": false, + "locked": true, + "render_order": "default", + "allow_mirror_modeling": true, + "from": [ + -0.1, + 0.0, + -2.0 + ], + "to": [ + 3.9, + 12.0, + 2.0 + ], + "autouv": 0, + "color": 8, + "origin": [ + 1.9, + 6.0, + 0.0 + ], + "rotation": [ + 0.0, + -0.0, + 0.0 + ], + "faces": { + "east": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "west": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "up": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "down": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "south": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "north": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + } + }, + "type": "cube", + "uuid": "26d74e48-22e5-4106-9242-e2ff862a9534" + }, + { + "name": "left_leg (wearer)", + "box_uv": false, + "rescale": false, + "locked": true, + "render_order": "default", + "allow_mirror_modeling": true, + "from": [ + -3.9, + 0.0, + -2.0 + ], + "to": [ + 0.1, + 12.0, + 2.0 + ], + "autouv": 0, + "color": 8, + "origin": [ + -1.9, + 6.0, + 0.0 + ], + "rotation": [ + 0.0, + -0.0, + 0.0 + ], + "faces": { + "east": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "west": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "up": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "down": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "south": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "north": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + } + }, + "type": "cube", + "uuid": "13620e0a-8a98-47cf-9e73-f7826fc2488f" + }, + { + "name": "arm_left plate", + "box_uv": false, + "rescale": false, + "locked": true, + "render_order": "default", + "allow_mirror_modeling": true, + "from": [ + -9.0587, + 13.34, + -3.375 + ], + "to": [ + -2.0803, + 25.7574, + 3.375 + ], + "autouv": 0, + "color": 1, + "origin": [ + -5.5695, + 19.5487, + 0.0 + ], + "rotation": [ + 0.0, + -0.0, + 180.0 + ], + "faces": { + "east": { + "uv": [ + 11.25, + 5.5, + 12.5, + 7.0 + ], + "texture": 0 + }, + "west": { + "uv": [ + 13.75, + 5.5, + 15.0, + 7.0 + ], + "texture": 0 + }, + "up": { + "uv": [ + 13.75, + 3.0, + 15.0, + 5.5 + ], + "texture": 0 + }, + "down": { + "uv": [ + 11.25, + 3.0, + 12.5, + 5.5 + ], + "texture": 0 + }, + "south": { + "uv": [ + 12.5, + 5.5, + 13.75, + 7.0 + ], + "texture": 0 + }, + "north": { + "uv": [ + 10.0, + 5.5, + 11.25, + 7.0 + ], + "texture": 0 + } + }, + "type": "cube", + "uuid": "febb3b78-4527-4512-9e1c-511c06bd2efd" + }, + { + "name": "arm_left bezel", + "box_uv": false, + "rescale": false, + "locked": true, + "render_order": "default", + "allow_mirror_modeling": true, + "from": [ + -9.6081, + 23.3252, + -2.25 + ], + "to": [ + -7.7671, + 25.711, + 2.25 + ], + "autouv": 0, + "color": 1, + "origin": [ + -8.6876, + 24.5181, + 0.0 + ], + "rotation": [ + 0.0, + -0.0, + 180.0 + ], + "faces": { + "east": { + "uv": [ + 0.0, + 6.0, + 3.0, + 10.0 + ], + "texture": 1 + }, + "west": { + "uv": [ + 4.0, + 6.0, + 7.0, + 10.0 + ], + "texture": 1 + }, + "up": { + "uv": [ + 4.0, + 0.0, + 5.0, + 6.0 + ], + "texture": 1 + }, + "down": { + "uv": [ + 3.0, + 0.0, + 4.0, + 6.0 + ], + "texture": 1 + }, + "south": { + "uv": [ + 7.0, + 6.0, + 8.0, + 10.0 + ], + "texture": 1 + }, + "north": { + "uv": [ + 3.0, + 6.0, + 4.0, + 10.0 + ], + "texture": 1 + } + }, + "type": "cube", + "uuid": "e5699921-c398-48fb-b03c-17101e3228f8" + }, + { + "name": "arm_left GEM [edit:socket/arm_left#gem]", + "box_uv": false, + "rescale": false, + "locked": false, + "render_order": "default", + "allow_mirror_modeling": true, + "from": [ + -9.8419, + 23.623, + -0.5 + ], + "to": [ + -7.8418, + 25.7209, + 0.5 + ], + "autouv": 0, + "color": 1, + "origin": [ + -8.8419, + 24.6719, + 0.0 + ], + "rotation": [ + 0.0, + -90.0, + 152.03 + ], + "faces": { + "east": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "west": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "up": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "down": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "south": { + "uv": [ + 7.0, + 7.0, + 9.0, + 9.0 + ], + "texture": 2 + }, + "north": { + "uv": [ + 7.0, + 7.0, + 9.0, + 9.0 + ], + "texture": 2 + } + }, + "type": "cube", + "uuid": "a6e586dd-ebb2-4dc7-b6ed-eb60599b0a22" + }, + { + "name": "arm_right plate", + "box_uv": false, + "rescale": false, + "locked": true, + "render_order": "default", + "allow_mirror_modeling": true, + "from": [ + 2.0803, + 13.34, + -3.375 + ], + "to": [ + 9.0587, + 25.7574, + 3.375 + ], + "autouv": 0, + "color": 2, + "origin": [ + 5.5695, + 19.5487, + 0.0 + ], + "rotation": [ + 0.0, + -0.0, + 180.0 + ], + "faces": { + "east": { + "uv": [ + 11.25, + 5.5, + 12.5, + 7.0 + ], + "texture": 0 + }, + "west": { + "uv": [ + 13.75, + 5.5, + 15.0, + 7.0 + ], + "texture": 0 + }, + "up": { + "uv": [ + 13.75, + 3.0, + 15.0, + 5.5 + ], + "texture": 0 + }, + "down": { + "uv": [ + 11.25, + 3.0, + 12.5, + 5.5 + ], + "texture": 0 + }, + "south": { + "uv": [ + 12.5, + 5.5, + 13.75, + 7.0 + ], + "texture": 0 + }, + "north": { + "uv": [ + 10.0, + 5.5, + 11.25, + 7.0 + ], + "texture": 0 + } + }, + "type": "cube", + "uuid": "8166df39-ee30-4918-aeb5-d380fb309583" + }, + { + "name": "arm_right bezel", + "box_uv": false, + "rescale": false, + "locked": true, + "render_order": "default", + "allow_mirror_modeling": true, + "from": [ + 7.7671, + 23.3252, + -2.25 + ], + "to": [ + 9.6081, + 25.711, + 2.25 + ], + "autouv": 0, + "color": 2, + "origin": [ + 8.6876, + 24.5181, + 0.0 + ], + "rotation": [ + 0.0, + -0.0, + 180.0 + ], + "faces": { + "east": { + "uv": [ + 0.0, + 6.0, + 3.0, + 10.0 + ], + "texture": 3 + }, + "west": { + "uv": [ + 4.0, + 6.0, + 7.0, + 10.0 + ], + "texture": 3 + }, + "up": { + "uv": [ + 4.0, + 0.0, + 5.0, + 6.0 + ], + "texture": 3 + }, + "down": { + "uv": [ + 3.0, + 0.0, + 4.0, + 6.0 + ], + "texture": 3 + }, + "south": { + "uv": [ + 7.0, + 6.0, + 8.0, + 10.0 + ], + "texture": 3 + }, + "north": { + "uv": [ + 3.0, + 6.0, + 4.0, + 10.0 + ], + "texture": 3 + } + }, + "type": "cube", + "uuid": "deb91276-44a7-4cc6-af78-4c9d72bd982c" + }, + { + "name": "arm_right GEM [edit:socket/arm_right#gem]", + "box_uv": false, + "rescale": false, + "locked": false, + "render_order": "default", + "allow_mirror_modeling": true, + "from": [ + 7.8419, + 23.623, + -0.5 + ], + "to": [ + 9.842, + 25.7209, + 0.5 + ], + "autouv": 0, + "color": 2, + "origin": [ + 8.8419, + 24.6719, + 0.0 + ], + "rotation": [ + 0.0, + 90.0, + -152.03 + ], + "faces": { + "east": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "west": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "up": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "down": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "south": { + "uv": [ + 7.0, + 7.0, + 9.0, + 9.0 + ], + "texture": 2 + }, + "north": { + "uv": [ + 7.0, + 7.0, + 9.0, + 9.0 + ], + "texture": 2 + } + }, + "type": "cube", + "uuid": "94a1d9e9-0098-4dc9-965c-8c578f6ae0e6" + }, + { + "name": "leg_left plate", + "box_uv": false, + "rescale": false, + "locked": true, + "render_order": "default", + "allow_mirror_modeling": true, + "from": [ + -4.54, + 3.5, + -2.64 + ], + "to": [ + 0.74, + 12.6, + 2.64 + ], + "autouv": 0, + "color": 3, + "origin": [ + -1.9, + 8.05, + 0.0 + ], + "rotation": [ + 0.0, + -0.0, + 180.0 + ], + "faces": { + "east": { + "uv": [ + 0.0, + 12.5, + 1.0, + 14.5 + ], + "texture": 4 + }, + "west": { + "uv": [ + 2.0, + 12.5, + 3.0, + 14.5 + ], + "texture": 4 + }, + "up": { + "uv": [ + 0.0, + 8.0, + 1.0, + 9.95 + ], + "texture": 4 + }, + "down": { + "uv": [ + 2.0, + 8.05, + 3.0, + 10.0 + ], + "texture": 4 + }, + "south": { + "uv": [ + 1.0, + 12.5, + 2.0, + 14.5 + ], + "texture": 4 + }, + "north": { + "uv": [ + 3.0, + 12.5, + 4.0, + 14.5 + ], + "texture": 4 + } + }, + "type": "cube", + "uuid": "55b28c65-09f0-470e-982c-ae45cddee7a3" + }, + { + "name": "leg_left bezel", + "box_uv": false, + "rescale": false, + "locked": true, + "render_order": "default", + "allow_mirror_modeling": true, + "from": [ + -3.7, + 4.55, + -3.24 + ], + "to": [ + -0.1, + 6.55, + -2.64 + ], + "autouv": 0, + "color": 3, + "origin": [ + -1.9, + 5.55, + -2.94 + ], + "rotation": [ + 0.0, + -0.0, + 180.0 + ], + "faces": { + "east": { + "uv": [ + 0.0, + 4.0, + 2.0, + 12.0 + ], + "texture": 5 + }, + "west": { + "uv": [ + 8.0, + 4.0, + 10.0, + 12.0 + ], + "texture": 5 + }, + "up": { + "uv": [ + 8.0, + 0.0, + 14.0, + 4.0 + ], + "texture": 5 + }, + "down": { + "uv": [ + 2.0, + 0.0, + 8.0, + 4.0 + ], + "texture": 5 + }, + "south": { + "uv": [ + 10.0, + 4.0, + 16.0, + 12.0 + ], + "texture": 5 + }, + "north": { + "uv": [ + 2.0, + 4.0, + 8.0, + 12.0 + ], + "texture": 5 + } + }, + "type": "cube", + "uuid": "84a74754-473b-4e6e-b2db-3f1fd77edefd" + }, + { + "name": "leg_left GEM [edit:socket/leg_left#gem]", + "box_uv": false, + "rescale": false, + "locked": false, + "render_order": "default", + "allow_mirror_modeling": true, + "from": [ + -2.9, + 4.55, + -3.74 + ], + "to": [ + -0.9, + 6.55, + -2.74 + ], + "autouv": 0, + "color": 3, + "origin": [ + -1.9, + 5.55, + -3.24 + ], + "rotation": [ + 0.0, + -0.0, + 0.0 + ], + "faces": { + "east": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "west": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "up": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "down": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "south": { + "uv": [ + 7.0, + 7.0, + 9.0, + 9.0 + ], + "texture": 2 + }, + "north": { + "uv": [ + 7.0, + 7.0, + 9.0, + 9.0 + ], + "texture": 2 + } + }, + "type": "cube", + "uuid": "8dddd53b-c49c-413d-bf4d-da79c8f07475" + }, + { + "name": "leg_right plate", + "box_uv": false, + "rescale": false, + "locked": true, + "render_order": "default", + "allow_mirror_modeling": true, + "from": [ + -0.74, + 3.5, + -2.64 + ], + "to": [ + 4.54, + 12.6, + 2.64 + ], + "autouv": 0, + "color": 4, + "origin": [ + 1.9, + 8.05, + 0.0 + ], + "rotation": [ + 0.0, + -0.0, + 180.0 + ], + "faces": { + "east": { + "uv": [ + 0.0, + 12.5, + 1.0, + 14.5 + ], + "texture": 4 + }, + "west": { + "uv": [ + 2.0, + 12.5, + 3.0, + 14.5 + ], + "texture": 4 + }, + "up": { + "uv": [ + 0.0, + 8.0, + 1.0, + 10.0 + ], + "texture": 4 + }, + "down": { + "uv": [ + 2.0, + 8.0, + 3.0, + 10.0 + ], + "texture": 4 + }, + "south": { + "uv": [ + 1.0, + 12.5, + 2.0, + 14.5 + ], + "texture": 4 + }, + "north": { + "uv": [ + 3.0, + 12.5, + 4.0, + 14.5 + ], + "texture": 4 + } + }, + "type": "cube", + "uuid": "0366f3f1-b5e5-436f-92cb-a68c3af04206" + }, + { + "name": "leg_right bezel", + "box_uv": false, + "rescale": false, + "locked": true, + "render_order": "default", + "allow_mirror_modeling": true, + "from": [ + 0.1, + 4.55, + -3.24 + ], + "to": [ + 3.7, + 6.55, + -2.64 + ], + "autouv": 0, + "color": 4, + "origin": [ + 1.9, + 5.55, + -2.94 + ], + "rotation": [ + 0.0, + -0.0, + 180.0 + ], + "faces": { + "east": { + "uv": [ + 0.0, + 4.0, + 2.0, + 12.0 + ], + "texture": 6 + }, + "west": { + "uv": [ + 8.0, + 4.0, + 10.0, + 12.0 + ], + "texture": 6 + }, + "up": { + "uv": [ + 8.0, + 0.0, + 14.0, + 4.0 + ], + "texture": 6 + }, + "down": { + "uv": [ + 2.0, + 0.0, + 8.0, + 4.0 + ], + "texture": 6 + }, + "south": { + "uv": [ + 10.0, + 4.0, + 16.0, + 12.0 + ], + "texture": 6 + }, + "north": { + "uv": [ + 2.0, + 4.0, + 8.0, + 12.0 + ], + "texture": 6 + } + }, + "type": "cube", + "uuid": "282461b3-777b-4d06-b7fc-257077584d82" + }, + { + "name": "leg_right GEM [edit:socket/leg_right#gem]", + "box_uv": false, + "rescale": false, + "locked": false, + "render_order": "default", + "allow_mirror_modeling": true, + "from": [ + 0.9, + 4.55, + -3.74 + ], + "to": [ + 2.9, + 6.55, + -2.74 + ], + "autouv": 0, + "color": 4, + "origin": [ + 1.9, + 5.55, + -3.24 + ], + "rotation": [ + 0.0, + -0.0, + 0.0 + ], + "faces": { + "east": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "west": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "up": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "down": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": null + }, + "south": { + "uv": [ + 7.0, + 7.0, + 9.0, + 9.0 + ], + "texture": 2 + }, + "north": { + "uv": [ + 7.0, + 7.0, + 9.0, + 9.0 + ], + "texture": 2 + } + }, + "type": "cube", + "uuid": "e649c7cb-eb32-4e52-b6ff-842b9f512d42" + }, + { + "name": "life_support_socket [edit:space/life_support_socket#model0]", + "box_uv": false, + "rescale": false, + "locked": false, + "render_order": "default", + "allow_mirror_modeling": true, + "from": [ + -3.5, + 14.2299, + 3.0001 + ], + "to": [ + 3.5, + 22.23, + 6.0003 + ], + "autouv": 0, + "color": 5, + "origin": [ + -0.0, + 18.23, + 4.5002 + ], + "rotation": [ + 0.0, + -0.0, + 180.0 + ], + "faces": { + "east": { + "uv": [ + 0.0, + 3.0, + 1.5, + 11.0 + ], + "texture": 7 + }, + "west": { + "uv": [ + 5.0, + 3.0, + 6.5, + 11.0 + ], + "texture": 7 + }, + "up": { + "uv": [ + 5.0, + 0.0, + 8.5, + 3.0 + ], + "texture": 7 + }, + "down": { + "uv": [ + 1.5, + 0.0, + 5.0, + 3.0 + ], + "texture": 7 + }, + "south": { + "uv": [ + 6.5, + 3.0, + 10.0, + 11.0 + ], + "texture": 7 + }, + "north": { + "uv": [ + 1.5, + 3.0, + 5.0, + 11.0 + ], + "texture": 7 + } + }, + "type": "cube", + "uuid": "f3ff891a-c853-4c74-b380-9cfcc91c265e" + }, + { + "name": "helmet_socket [edit:space/helmet_socket#model0]", + "box_uv": false, + "rescale": false, + "locked": false, + "render_order": "default", + "allow_mirror_modeling": true, + "from": [ + -5.3125, + 22.6125, + -5.3125 + ], + "to": [ + 5.3125, + 33.2375, + 5.3125 + ], + "autouv": 0, + "color": 6, + "origin": [ + 0.0, + 27.925, + 0.0 + ], + "rotation": [ + 0.0, + -0.0, + 0.0 + ], + "faces": { + "east": { + "uv": [ + 0.0, + 4.0, + 4.0, + 8.0 + ], + "texture": 8 + }, + "west": { + "uv": [ + 8.0, + 4.0, + 12.0, + 8.0 + ], + "texture": 8 + }, + "up": { + "uv": [ + 8.0, + 0.0, + 12.0, + 4.0 + ], + "texture": 8 + }, + "down": { + "uv": [ + 4.0, + 0.0, + 8.0, + 4.0 + ], + "texture": 8 + }, + "south": { + "uv": [ + 12.0, + 4.0, + 16.0, + 8.0 + ], + "texture": 8 + }, + "north": { + "uv": [ + 4.0, + 4.0, + 8.0, + 8.0 + ], + "texture": 8 + } + }, + "type": "cube", + "uuid": "8e240300-69c2-453b-93fe-de7ac57b4fbe" + } + ], + "outliner": [ + { + "name": "WEARER (locked reference)", + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "color": 0, + "uuid": "965cef4f-e908-4200-9741-7f28471490fa", + "export": true, + "mirror_uv": false, + "isOpen": true, + "locked": false, + "visibility": true, + "autouv": 0, + "children": [ + "dcebf35d-2a7c-420e-bea1-53538457267b", + "f9c76f4a-bcf9-472a-b480-03e4a61242ef", + "47dd1a7d-4890-4b84-919e-89eae5e618cd", + "a3614afb-2457-496c-a149-eca64f411925", + "26d74e48-22e5-4106-9242-e2ff862a9534", + "13620e0a-8a98-47cf-9e73-f7826fc2488f" + ] + }, + { + "name": "left_arm", + "origin": [ + -5.0, + 22.0, + 0.0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "color": 0, + "uuid": "0f29451f-edef-49cf-9f93-16d9f3943e04", + "export": true, + "mirror_uv": false, + "isOpen": true, + "locked": false, + "visibility": true, + "autouv": 0, + "children": [ + "febb3b78-4527-4512-9e1c-511c06bd2efd", + "e5699921-c398-48fb-b03c-17101e3228f8", + "a6e586dd-ebb2-4dc7-b6ed-eb60599b0a22" + ] + }, + { + "name": "right_arm", + "origin": [ + 5.0, + 22.0, + 0.0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "color": 0, + "uuid": "82be414b-5718-44e9-9edd-b00b533e67a0", + "export": true, + "mirror_uv": false, + "isOpen": true, + "locked": false, + "visibility": true, + "autouv": 0, + "children": [ + "8166df39-ee30-4918-aeb5-d380fb309583", + "deb91276-44a7-4cc6-af78-4c9d72bd982c", + "94a1d9e9-0098-4dc9-965c-8c578f6ae0e6" + ] + }, + { + "name": "left_leg", + "origin": [ + -1.9, + 12.0, + 0.0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "color": 0, + "uuid": "a412e18e-7766-43ab-b5cc-4f1d253062f1", + "export": true, + "mirror_uv": false, + "isOpen": true, + "locked": false, + "visibility": true, + "autouv": 0, + "children": [ + "55b28c65-09f0-470e-982c-ae45cddee7a3", + "84a74754-473b-4e6e-b2db-3f1fd77edefd", + "8dddd53b-c49c-413d-bf4d-da79c8f07475" + ] + }, + { + "name": "right_leg", + "origin": [ + 1.9, + 12.0, + 0.0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "color": 0, + "uuid": "c3a77481-46a6-4421-99d1-d63ac90c2480", + "export": true, + "mirror_uv": false, + "isOpen": true, + "locked": false, + "visibility": true, + "autouv": 0, + "children": [ + "0366f3f1-b5e5-436f-92cb-a68c3af04206", + "282461b3-777b-4d06-b7fc-257077584d82", + "e649c7cb-eb32-4e52-b6ff-842b9f512d42" + ] + }, + { + "name": "body - life_support_socket", + "origin": [ + 0.0, + 24.0, + 0.0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "color": 0, + "uuid": "97f5eebf-6aa3-468d-91fd-30fc7c7de9e2", + "export": true, + "mirror_uv": false, + "isOpen": true, + "locked": false, + "visibility": true, + "autouv": 0, + "children": [ + "f3ff891a-c853-4c74-b380-9cfcc91c265e" + ] + }, + { + "name": "head - helmet_socket", + "origin": [ + 0.0, + 24.0, + 0.0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "color": 0, + "uuid": "e4d3c097-f781-4920-8319-b0ed5a76d3f2", + "export": true, + "mirror_uv": false, + "isOpen": true, + "locked": false, + "visibility": true, + "autouv": 0, + "children": [ + "8e240300-69c2-453b-93fe-de7ac57b4fbe" + ] + } + ], + "textures": [ + { + "path": "", + "name": "layer_1.png", + "folder": "", + "namespace": "", + "id": "0", + "width": 64, + "height": 32, + "uv_width": 16, + "uv_height": 16, + "particle": false, + "use_as_default": false, + "layers_enabled": false, + "sync_to_project": "", + "render_mode": "default", + "render_sides": "auto", + "frame_time": 1, + "frame_order_type": "loop", + "frame_order": "", + "frame_interpolate": false, + "visible": true, + "internal": true, + "saved": false, + "uuid": "89ab1844-d2c7-429f-88cc-bf60dd6dbecf", + "relative_path": "", + "source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAAK4UlEQVR4nNWZW2gc5RvGfzO7szPJnrLZQzanxiQlqUka0V54gLbe1PMBsahUwQUt9UqUUK+EKGIvxIIgXohepIiCIggiVdpqBW1z0bSpTZOYsG0Om2yyu5k9ZTd7nvlf9J+RNLEJ1Yo+dxnenXm/9/u+533eJwKboK+vT08mk4iiiMPhMJ6rqgrAwMCAsNk7/s0wbxYgiiJvvfUWgiBw4cIFKpUKCwsLNDY2Mjc390/keEuxaQEEQSCfz5NMJpmYmKBUKgGg6/otT+6fwKYFiEajBINBwuEwwWAQXdcRRZH29nYE4T99+oEtFAAgn88Ti8UQRRGPx4Pb7UaWZYMH/ssQtxIUCoUoFouUy2W8Xi/ZbBaLxUJtbe2tzu+WY9MTUFtbS7FYRJZltm/fjiRJtLW1YTKZiMfj/0SO6xAIBNYR0Ebd6MUXX9R0XS8IgiADAsCxY8fWxG2JBMPhsLFYt9t9s3n/rdi5cyc+n4/x8XHC4fCfhQk9PT2K1WolHA4TCoXy1weY+/r6dIvFQiQSQdM0XC4XgiAYLC+KIqVSCb/fT6lUQhAEzGYzXq8Xs9lMf3+/HolEsFqtwDV9IMsytbW1ZLPZax8x/1HneDxuXJ14PH7TOqKlpYW2tjb8fj9ffvnln4XpjY2NQiKRYOfOnczPzyvrCiCKIo899hjBYJBQKER1dTW7d+/mm2++QZIk9u3bh9frpVgs8vXXX+N0OnnmmWfQNI3jx4+TSCTYv38/i4uLJJNJOjo66OjooK2tjU8++YSlpSVee+01MpkMZ8+eZWZmBp/PRy6Xo1Kp3MzaATh37hzlchmbzYYobkxluq4XRkZGlNUru1HrNsdiMS5fvkxVVRVutxu/34/L5aJSqZBKpRgZGUFRFCRJQhRFisUis7OzWCwW8vk8qVSKZDLJzMwMs7OzaJqGqqpkMhmjc6RSKWZnZ4lEIiQSCZLJJDt27PjTxLeCSCTCiRMnMJlMhja5HoIgmBcWFlhYWADQ2ID0zQAOh4Pdu3dz5coVotEog4OD2O12UqkU4XCYd955RwD49NNPdVmWue+++wSAN954QzeZTCwuLhq7IUkS6XSa8+fPUygUAJiamuLUqVMIgoAoilgsFhwOB8vLy5sudJXwRFFE0zQArFYriqIQj8eNU7QapygK+fwfV72pqYmlpSVKpZK40Ykzu91uLBYLS0tLhsw1m82kUilaW1vX3F+Px0MmkzH+1nWdbDbLtm3buP/++7l8+TI+nw9N0xgdHSWTyZBOp5mZmaG6upo77riDXC5HZ2cnjY2NJBKJTQsAcPvtt9PT00MwGGR4eJhCocD+/fspFApomsZXX30FwPbt2+nq6mJ5eZmRkREAnnrqKWKxGJIk8fnnn68vgKqqhEIhWlpa+P3331lcXKRUKpFMJvF4PHR1dTEwMKDPz88zOjqKIAj09fXpfr+fYrFINpslmUyi6zo1NTWEQiFsNhuVSoVCoUBVVRXZbBa/34/JZGLXrl3U1dVx6dIlxsbGNl28JEn09PRQLpfp6Ojg4sWLVCoVVFUllUrR0NCAIAjY7Xa6u7uJxWJs27aNs2fPArC0tMTY2BidnZ1UV1evLwBcY+7p6WlKpRJWq5VMJoMgCMzNzZFIJMjn83zwwQdr2PqVV17Ra2trKZfLOJ1O8vk8V69eJZfLUSwW13zEZDIhSRLz8/N4vV5cLhflcnlLu18qlRgeHubOO+8kGo0iCAKapqHrOs3NzTidTux2O3CtRSuKgs/nM95vtVq55557MJlMmEymde8XNhIVq0nncjm++OKLG7ap559/XrdYLJjNZiqVCpqmIYoiuq4brGs2m9F1HafTiclkQhAEVFWlUqls2gZX8zObzWiaZvAAgM1mw2azEY1GDW4pl8trNkBRFKxWK5qmkUgk1n1v0x78/vvv62NjY8Tjcbq7u3n33Xdv+Js333xTlySJ/v7+NXH9/f06wNtvv/2vmqA27UOCIODz+ahUKlvq2w6HY93i4drCm5qabjLNW4ctGSI2m40HHnhgTXu5GUxNTf2l398KbHoCZmZmiMfjnDt3blVQ3BCKovDhhx+u45X33ntPlyTpJtO8ddj0BFzP/tejr69Phz88wlQqtaEyi8ViVCoVDh8+rK+qRfhjuDp69Oia76y+9/rnfze2ZIhshkOHDnH69GlDJ+i6zuHDh/VsNmsMT6IoYjKZEEWRQCDA6dOnmZycRNd1du3aRSAQ0FeLoaoqhw4d4siRI39HejfEXy6AqqqMjo6STqeNFvjCCy8QiUT47LPPAHj66afx+XwcP36caDTK6OioIYM1TWN8fJzW1lbcbjeSJOHz+fj222+Ba23wueeeY2VlhZ9//plEIsETTzyB2WxmcHCQZDLJgQMHKJfLfP/996iqyt69e5FlmRMnTmzaZm9+Gvk/3G43V65cATAEFIDdbkeWZWPn4Zqqs9lsRvyqmpNlmdnZWeP6BINBYrEYXq/3r6a3Kf6WK7CqxFwuFxMTE5w8eZKamhrg2qKHh4cRRZHffvuN9vb2NcrN4/Hw6KOP8t1335FMJslkMjgcDh5++GFOnTqFx+MhkUiwtLRENBqltraWiYkJOjs7yeVyWK1WRkZGMJvNqKqK1+s1xne/33/rC6CqKsvLy0xNTSHLMi0tLYyNjVFfX48oiiiKQjQaJZ/Ps2PHDtLptBGvKArLy8vMz8/T2NhITU2NMVVmMhkKhQK6ruP3+2loaGBsbAyHw8Fdd92F3W4nnU5TV1dHU1OTMVpLkoSiKFy9evVPx+Q1BXj22Wd1WZaNB7quI8syNpuNfD5POp2mtbWVfD5PJBLB6XQaro6maeRyOex2O62trYTDYW677Tb27t3L6OgoxWIRSZIoFAr09vZSV1fHL7/8gsPhoL29nfn5ecLhMD/++CPlctmYQk0mEy6Xi1gsxsDAgHD06FEjv5dffllfnSuOHTsmBAIBfXx8HLfbjdvt5siRI0Jra6u+Krc3LYCmaRw4cIChoSEuXbqEKIq8+uqr/PDDDwSDQerr6wkEAoyNjfHxxx9TqVR48sknSSQSnD59mubmZmNHs9ksQ0NDaJrG448/jiAIxv0fHBzkp59+Mqwzp9NJJBJBkiQefPBBUqkUqqpSLBbxeDw0NDRw8uTJdQmXy2X27du3htgmJyfXeJUHDx4UYGPzdF0BSqUS4XAYWZaxWq1UKhXOnDnD3Nwc+XweQRA4f/48U1NTiKJIJpMhHo+TzWZZlbYLCwtks1lqa2vxer0MDg4iyzL19fVYLBbS6TS//vorzc3NqKpKqVTCZDIZ3oGqqsRiMaanpwFIJpNYLJYNLazrWf1GLL8Vv9FcLBYZGhqit7eXdDqN1WolFosRCoXweDxEo1Hm5uaoqanB7XaTyWSYnJzk7rvvJhqN0tDQoGezWTweDwCyLAt79uzhwoULhu+nKIphpsiyjK7rhMNhVlZWKJfLXLx4kXQ6bUjteDzOwsLCX/IMtwqzoij09vay6gxJkkRXVxe6rhMKhchkMlRVVeHz+bDb7dhsNu699156eno4c+YMr7/++ppWevDgQX3Pnj088sgjAIZnGA6HmZ6eXrcrgUBAVxRljeUlSRLlcpl/QjoLDz30kL467cXjcSwWC06nk3Q6zcrKCiaTCYvFgsvlYmVlxWDbxsZG8vk8H3300bpj9tJLL+nd3d3Isszy8jKpVIqFhYV/5b/S/weIezzyoMFS2AAAAABJRU5ErkJggg==" + }, + { + "path": "", + "name": "arm_left_socket.png", + "folder": "", + "namespace": "", + "id": "1", + "width": 16, + "height": 8, + "uv_width": 16, + "uv_height": 16, + "particle": false, + "use_as_default": false, + "layers_enabled": false, + "sync_to_project": "", + "render_mode": "default", + "render_sides": "auto", + "frame_time": 1, + "frame_order_type": "loop", + "frame_order": "", + "frame_interpolate": false, + "visible": true, + "internal": true, + "saved": false, + "uuid": "d75acd2d-c105-4d52-98a1-8d0fb3c52e7b", + "relative_path": "", + "source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAICAYAAADwdn+XAAAARElEQVR4nNWOsREAIAwC0SkYgv0nYYhsEVtj48XOLzmeA9ggmSQTDeYZSOr4daArA8AgmbtouxQiYlwf2C6ipKc3n7IA3XMQngZM1dgAAAAASUVORK5CYII=" + }, + { + "path": "", + "name": "default.png", + "folder": "", + "namespace": "", + "id": "2", + "width": 16, + "height": 16, + "uv_width": 16, + "uv_height": 16, + "particle": false, + "use_as_default": false, + "layers_enabled": false, + "sync_to_project": "", + "render_mode": "default", + "render_sides": "auto", + "frame_time": 1, + "frame_order_type": "loop", + "frame_order": "", + "frame_interpolate": false, + "visible": true, + "internal": true, + "saved": false, + "uuid": "82c3fa82-4ad1-4cf3-a8fe-b316ec3d3bd6", + "relative_path": "", + "source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAKklEQVR4nGNgGAXDHVy+fPn/wYMH/+NTw4JP8smTJwzPnz+nrqtGwWAEANKoDCDpkVG5AAAAAElFTkSuQmCC" + }, + { + "path": "", + "name": "arm_right_socket.png", + "folder": "", + "namespace": "", + "id": "3", + "width": 16, + "height": 8, + "uv_width": 16, + "uv_height": 16, + "particle": false, + "use_as_default": false, + "layers_enabled": false, + "sync_to_project": "", + "render_mode": "default", + "render_sides": "auto", + "frame_time": 1, + "frame_order_type": "loop", + "frame_order": "", + "frame_interpolate": false, + "visible": true, + "internal": true, + "saved": false, + "uuid": "7bc68bdb-a6a7-4591-aa43-2c7c4e8090ae", + "relative_path": "", + "source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAICAYAAADwdn+XAAAATklEQVR4nNWOsQ2AQAwD/UzhDdJ4G8+WMTLjUz16JCQUOq5z4dMBGyQnyYkGxz5sw3bnfxdUVesMAGMlS0JEXAVLlpnjtUDSY8WXoh9yAvIuEiHA/woKAAAAAElFTkSuQmCC" + }, + { + "path": "", + "name": "layer_2.png", + "folder": "", + "namespace": "", + "id": "4", + "width": 64, + "height": 32, + "uv_width": 16, + "uv_height": 16, + "particle": false, + "use_as_default": false, + "layers_enabled": false, + "sync_to_project": "", + "render_mode": "default", + "render_sides": "auto", + "frame_time": 1, + "frame_order_type": "loop", + "frame_order": "", + "frame_interpolate": false, + "visible": true, + "internal": true, + "saved": false, + "uuid": "8296699e-4d51-466c-8588-1c5ef009ff8c", + "relative_path": "", + "source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAAC9ElEQVR4nO2XMUsrTRSGn90kqCQbMROFECwCioLTqUVS2PgjbMVGsM3vsLT3X2ilYmFCJN2CKGrUEBMlsxCDEsNm5xYhi3otPvBT0btPMzNnhuG8hzlnZiAgICAgICAgICAgICAgIOBfw3hryOfzGkAphRCCzc3Nv9b8Jsz3jOvr62SzWZRSX+3Pl/NXAJRS7O/vUygUvsOfL+fdE/CviAcwBjkP+EdeCOGPX/bfzgFsb2//6BphQl9MKpUC+gJTqZQ/BkilUkgpffEv5346plKKUCiEYRgkEgm01liWhWVZJBIJlFL0ej0sy6LZbNJsNrEs61VAfjJmOBxmY2ODXC5Hs9nk5uaGeDxOPB7n4uKCbrfLwsICmUyGp6cnHh4emJubY3FxkVar9d3+fxgTwPM8DMOg3W6TSCQwTZPd3V1GR0fp9Xo8Pz/T7XYxTRMhBJ7n4brud/v+vxB2XZetrS2klMTjcWKxGDs7O+RyOU5PT1FKcXBwQK1WI51OA1AqlbBt+3ekgBACKSW2bZNMJnEch2w2S6FQwHEckskkExMTLC0t4TgOhmFg2zZSyl/xUDLy+bweVPV6vQ68vvIGfSmlPz9Yb9s24+PjxGIxrq+v8TwPIQS9Xo92u00kEiEajfp7DvYbtFprRkZG6Ha7RKNRHh8fSafTVKtV38HPvmbDAyHv3f0veRmcwRohBMPDw6ysrHB0dIRt23iex/T0NKFQiJmZGS4vL/16cXZ2xuTkJMvLy5ycnFAsFjFNEyklruuSyWSYnZ3l8PCQTqdDrVb7TO39AHz0s7O2tqZLpRLFYhHLsnAch0gkQr1ex3VdyuUyWms/ZSKRCFdXVxwfH5NMJlFKEY1GOT8/5/7+nnK5zN3dHVNTUxjG57+xwh/dYGxsjL29Pb+OCCHQWlOpVKhUKkA/faCfUo1Gg0aj8SqlWq0W1WqVTqfD0NAQ8/PzGIbxJTXmwyFeXV3Vb20DwfV6/T+JkFKiteb29tYvtMCXfMf/ACqkZE/uhnD9AAAAAElFTkSuQmCC" + }, + { + "path": "", + "name": "leg_left_socket.png", + "folder": "", + "namespace": "", + "id": "5", + "width": 8, + "height": 4, + "uv_width": 16, + "uv_height": 16, + "particle": false, + "use_as_default": false, + "layers_enabled": false, + "sync_to_project": "", + "render_mode": "default", + "render_sides": "auto", + "frame_time": 1, + "frame_order_type": "loop", + "frame_order": "", + "frame_interpolate": false, + "visible": true, + "internal": true, + "saved": false, + "uuid": "6a6d3c08-0207-420c-b228-6c8c0701bd25", + "relative_path": "", + "source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAECAYAAACzzX7wAAAAOUlEQVR4nHWLsQ0AMRDCnC0YgjHYv2KfS3WvNO8GLAEASUbSbG4HOEmmLbZZXj+SxjZtv8H6e/rlAk9sGM+9qFpUAAAAAElFTkSuQmCC" + }, + { + "path": "", + "name": "leg_right_socket.png", + "folder": "", + "namespace": "", + "id": "6", + "width": 8, + "height": 4, + "uv_width": 16, + "uv_height": 16, + "particle": false, + "use_as_default": false, + "layers_enabled": false, + "sync_to_project": "", + "render_mode": "default", + "render_sides": "auto", + "frame_time": 1, + "frame_order_type": "loop", + "frame_order": "", + "frame_interpolate": false, + "visible": true, + "internal": true, + "saved": false, + "uuid": "ed00b67e-a695-432b-8dd2-f80b33f6e6ba", + "relative_path": "", + "source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAECAYAAACzzX7wAAAAR0lEQVR4nH3JMRHAIBAAwU1UvAMaZODgO6TgCQtvjXSZVLlqZw5U1YmIU1VnjPEarog40HsHay0w55SZ7u9srdl7g8x8/dsDBkgZSKeLXn4AAAAASUVORK5CYII=" + }, + { + "path": "", + "name": "life_support_pack_socket.png", + "folder": "", + "namespace": "", + "id": "7", + "width": 32, + "height": 16, + "uv_width": 16, + "uv_height": 16, + "particle": false, + "use_as_default": false, + "layers_enabled": false, + "sync_to_project": "", + "render_mode": "default", + "render_sides": "auto", + "frame_time": 1, + "frame_order_type": "loop", + "frame_order": "", + "frame_interpolate": false, + "visible": true, + "internal": true, + "saved": false, + "uuid": "8bef7ecb-d3b0-4516-8aa2-0613b2fbd1d8", + "relative_path": "", + "source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAQCAYAAAB3AH1ZAAAA6klEQVR4nO2TwQ2DMAxFfzoGC8CBJYzCNkzAAEzADCyB5CyRg1mANdxToqSlVZMeeum7JLa/8RdyDBJUVcPdORfzRJTKYi3kjTEGlWSNqqrp4JQw7Ko+DEO1gdurQa9yj/UrfRH6JdbaLK4ywMzZR0REmVmZWUUk5lMdM6u1NjtFpNiAYebYRERwzqFpGpznCQDZPdWN4wgA8N6j73sAwLqu6LquaB+eduBT5nkGgDg8xKVUGwh477/qrzawLMvb+GMD4RmlZ9u2mYiInp5b+OXbtmVxKaZmc1OmacK+7ziOAwCKl/DPn59zB3Xw2Ea5uMbPAAAAAElFTkSuQmCC" + }, + { + "path": "", + "name": "space_helmet_socket.png", + "folder": "", + "namespace": "", + "id": "8", + "width": 32, + "height": 32, + "uv_width": 16, + "uv_height": 16, + "particle": false, + "use_as_default": false, + "layers_enabled": false, + "sync_to_project": "", + "render_mode": "default", + "render_sides": "auto", + "frame_time": 1, + "frame_order_type": "loop", + "frame_order": "", + "frame_interpolate": false, + "visible": true, + "internal": true, + "saved": false, + "uuid": "0bba5a9c-5d2e-4185-a738-04e96428db3e", + "relative_path": "", + "source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABaUlEQVR4nO2UMXLCMBBFHySZFAxdCuwm6UShS3iGG6jTFfA5fAKdQZ3P4Eu4wBcwk8kkRdLRKAXG44DBcoBOr/Ks1n//rHYFAzjn3Hq9ds45t1qt3GKxcN3YJYa0AR59kgCKoqAsS5RSFEVxctYlSRKqqvLS9TYAYK3tNdYXi+PYS3M6xkCe5+R5PuaXQbw6YIwhTVOUUq0RY8xNDIzqwD3wMpCmKcaY9goOHYH9wB3TFzvHZChhs9l4rVMfy+VyUH/is69RFJ1sgNaa7Xb7X28t7RB216nbwib+rbXeWWtfmuJfzdk8SZKnPo1jnb4aVVXtZ2DoYQFmwE5rXWut34Ef4Bk4W/w41ldDCOE3hNbaByllDHwAn1LKV2vt7FzhMVx8B7riZVkCzIHn5vsmXOzA4TnNsgwpJcAbEEkpybLsT85dDNR1jdYaACEESqmpUmoqhAD2m1DX9VUGJtfseSAQCAQCgcAt+AXMpLw7q8BL0gAAAABJRU5ErkJggg==" + } + ] +} \ No newline at end of file diff --git a/src/main/resources/assets/cmmodular/lang/en_us.json b/src/main/resources/assets/cmmodular/lang/en_us.json index 0b3ad85..6099058 100644 --- a/src/main/resources/assets/cmmodular/lang/en_us.json +++ b/src/main/resources/assets/cmmodular/lang/en_us.json @@ -35,6 +35,10 @@ "miapi.module.cmmodular.gem.limb.case_leg_left.name": "Left Knee Apothic Gem Case", "miapi.module.cmmodular.gem.limb.gem_leg_right.name": "%s Right Knee Gemstone", "miapi.module.cmmodular.gem.limb.case_leg_right.name": "Right Knee Apothic Gem Case", + "miapi.module.cmmodular.gem.space.gem_helmet.name": "%s Visor Gemstone", + "miapi.module.cmmodular.gem.space.case_helmet.name": "Visor Apothic Gem Case", + "miapi.module.cmmodular.gem.space.gem_backtank.name": "%s Backtank Gemstone", + "miapi.module.cmmodular.gem.space.case_backtank.name": "Backtank Apothic Gem Case", "miapi.material.cmmodular.bone.dragonbone": "Dragonbone", "miapi.material.cmmodular.bone.hippogryph_talon": "Talon", "miapi.material.cmmodular.bone.sea_serpent_fang": "Sea Serpent Fang", diff --git a/src/main/resources/assets/cmmodular/models/item/armor/model/chest_back/space/default.json b/src/main/resources/assets/cmmodular/models/item/armor/model/chest_back/space/default.json index 3907ee4..5d304cd 100644 --- a/src/main/resources/assets/cmmodular/models/item/armor/model/chest_back/space/default.json +++ b/src/main/resources/assets/cmmodular/models/item/armor/model/chest_back/space/default.json @@ -1,6 +1,5 @@ { "comment": "Life support pack, worn. Twin tanks sitting on the back plate; the same 32x32 cube net as the helmet, so both are edited the same way.", - "texture_size": [32, 32], "textures": { "1": "cmmodular:equipment/life_support_pack", "particle": "cmmodular:equipment/life_support_pack" @@ -8,24 +7,86 @@ "elements": [ { "name": "tanks", - "from": [-3.5, -6.5, 2], - "to": [3.5, 1.5, 5], + "from": [ + -3.5, + -6.5, + 2 + ], + "to": [ + 3.5, + 1.5, + 5 + ], "faces": { - "north": {"uv": [8, 8, 16, 16], "texture": "#1"}, - "east": {"uv": [16, 8, 24, 16], "texture": "#1"}, - "south": {"uv": [24, 8, 32, 16], "texture": "#1"}, - "west": {"uv": [0, 8, 8, 16], "texture": "#1"}, - "up": {"uv": [8, 0, 16, 8], "texture": "#1"}, - "down": {"uv": [16, 0, 24, 8], "texture": "#1"} + "north": { + "uv": [ + 1.5, + 3.0, + 5.0, + 11.0 + ], + "texture": "#1" + }, + "east": { + "uv": [ + 5.0, + 3.0, + 6.5, + 11.0 + ], + "texture": "#1" + }, + "south": { + "uv": [ + 6.5, + 3.0, + 10.0, + 11.0 + ], + "texture": "#1" + }, + "west": { + "uv": [ + 0.0, + 3.0, + 1.5, + 11.0 + ], + "texture": "#1" + }, + "up": { + "uv": [ + 1.5, + 0.0, + 5.0, + 3.0 + ], + "texture": "#1" + }, + "down": { + "uv": [ + 5.0, + 0.0, + 8.5, + 3.0 + ], + "texture": "#1" + } } } ], "groups": [ { "name": "Life Support Pack", - "origin": [0, 0, 0], + "origin": [ + 0, + 0, + 0 + ], "color": 0, - "children": [0] + "children": [ + 0 + ] } ] } diff --git a/src/main/resources/assets/cmmodular/models/item/armor/model/chest_back/space_socket/default.json b/src/main/resources/assets/cmmodular/models/item/armor/model/chest_back/space_socket/default.json index 831866c..ce8d23b 100644 --- a/src/main/resources/assets/cmmodular/models/item/armor/model/chest_back/space_socket/default.json +++ b/src/main/resources/assets/cmmodular/models/item/armor/model/chest_back/space_socket/default.json @@ -1,9 +1,5 @@ { "comment": "Life support pack with a gem setting between the tanks, worn. Same box as the plain pack, socket stamped into the back face of the net.", - "texture_size": [ - 32, - 32 - ], "textures": { "1": "cmmodular:equipment/life_support_pack_socket", "particle": "cmmodular:equipment/life_support_pack_socket" @@ -24,55 +20,55 @@ "faces": { "north": { "uv": [ - 8, - 8, - 16, - 16 + 1.5, + 3.0, + 5.0, + 11.0 ], "texture": "#1" }, "east": { "uv": [ - 16, - 8, - 24, - 16 + 5.0, + 3.0, + 6.5, + 11.0 ], "texture": "#1" }, "south": { "uv": [ - 24, - 8, - 32, - 16 + 6.5, + 3.0, + 10.0, + 11.0 ], "texture": "#1" }, "west": { "uv": [ - 0, - 8, - 8, - 16 + 0.0, + 3.0, + 1.5, + 11.0 ], "texture": "#1" }, "up": { "uv": [ - 8, - 0, - 16, - 8 + 1.5, + 0.0, + 5.0, + 3.0 ], "texture": "#1" }, "down": { "uv": [ - 16, - 0, - 24, - 8 + 5.0, + 0.0, + 8.5, + 3.0 ], "texture": "#1" } diff --git a/src/main/resources/assets/cmmodular/models/item/armor/model/helmet/space/default.json b/src/main/resources/assets/cmmodular/models/item/armor/model/helmet/space/default.json index af2b71e..c3b6efc 100644 --- a/src/main/resources/assets/cmmodular/models/item/armor/model/helmet/space/default.json +++ b/src/main/resources/assets/cmmodular/models/item/armor/model/helmet/space/default.json @@ -1,6 +1,5 @@ { "comment": "Sealed helmet, worn. One cube around the head, textured from a 32x32 cube net so the faces can be edited without touching a vanilla armour atlas.", - "texture_size": [32, 32], "textures": { "1": "cmmodular:equipment/space_helmet", "particle": "cmmodular:equipment/space_helmet" @@ -8,24 +7,86 @@ "elements": [ { "name": "dome", - "from": [-4.25, -4.25, -4.25], - "to": [4.25, 4.25, 4.25], + "from": [ + -4.25, + -4.25, + -4.25 + ], + "to": [ + 4.25, + 4.25, + 4.25 + ], "faces": { - "north": {"uv": [8, 8, 16, 16], "texture": "#1"}, - "east": {"uv": [16, 8, 24, 16], "texture": "#1"}, - "south": {"uv": [24, 8, 32, 16], "texture": "#1"}, - "west": {"uv": [0, 8, 8, 16], "texture": "#1"}, - "up": {"uv": [8, 0, 16, 8], "texture": "#1"}, - "down": {"uv": [16, 0, 24, 8], "texture": "#1"} + "north": { + "uv": [ + 8.0, + 8.0, + 4.0, + 4.0 + ], + "texture": "#1" + }, + "east": { + "uv": [ + 0.0, + 8.0, + 4.0, + 4.0 + ], + "texture": "#1" + }, + "south": { + "uv": [ + 16.0, + 8.0, + 12.0, + 4.0 + ], + "texture": "#1" + }, + "west": { + "uv": [ + 8.0, + 8.0, + 12.0, + 4.0 + ], + "texture": "#1" + }, + "up": { + "uv": [ + 12.0, + 0.0, + 8.0, + 4.0 + ], + "texture": "#1" + }, + "down": { + "uv": [ + 8.0, + 4.0, + 4.0, + 0.0 + ], + "texture": "#1" + } } } ], "groups": [ { "name": "Space Helmet", - "origin": [0, 0, 0], + "origin": [ + 0, + 0, + 0 + ], "color": 0, - "children": [0] + "children": [ + 0 + ] } ] } diff --git a/src/main/resources/assets/cmmodular/models/item/armor/model/helmet/space_socket/default.json b/src/main/resources/assets/cmmodular/models/item/armor/model/helmet/space_socket/default.json index 7728666..b16203e 100644 --- a/src/main/resources/assets/cmmodular/models/item/armor/model/helmet/space_socket/default.json +++ b/src/main/resources/assets/cmmodular/models/item/armor/model/helmet/space_socket/default.json @@ -1,9 +1,5 @@ { "comment": "Sealed helmet with a gem setting on the crown, worn. Same cube as the plain helmet, socket stamped into the top face of the net.", - "texture_size": [ - 32, - 32 - ], "textures": { "1": "cmmodular:equipment/space_helmet_socket", "particle": "cmmodular:equipment/space_helmet_socket" @@ -24,55 +20,55 @@ "faces": { "north": { "uv": [ - 8, - 8, - 16, - 16 + 8.0, + 8.0, + 4.0, + 4.0 ], "texture": "#1" }, "east": { "uv": [ - 16, - 8, - 24, - 16 + 0.0, + 8.0, + 4.0, + 4.0 ], "texture": "#1" }, "south": { "uv": [ - 24, - 8, - 32, - 16 + 16.0, + 8.0, + 12.0, + 4.0 ], "texture": "#1" }, "west": { "uv": [ - 0, - 8, - 8, - 16 + 8.0, + 8.0, + 12.0, + 4.0 ], "texture": "#1" }, "up": { "uv": [ - 8, - 0, - 16, - 8 + 12.0, + 0.0, + 8.0, + 4.0 ], "texture": "#1" }, "down": { "uv": [ - 16, - 0, - 24, - 8 + 8.0, + 4.0, + 4.0, + 0.0 ], "texture": "#1" } diff --git a/src/main/resources/assets/cmmodular/textures/equipment/arm_left_socket.png b/src/main/resources/assets/cmmodular/textures/equipment/arm_left_socket.png index 602c54c62b57a69597b397d65180a8317995b0b2..a021842d481a32c974f32a44fce814bc7b353c61 100644 GIT binary patch delta 247 zcmeBWoXj*qrJk`k$lZzY=1HA;1_lO}bVpxD28NCO+1m4(lI{y)yf6Z_5X6w~Y1lmm)O)lJe@RMy@sm%s$?@J6- qVv}`RS4r*XpQN+s?PBxkpP23z3(VW~E%+ADJO)o!KbLh*2~7ZIhfj9^ delta 112 zcmbQt)XO+QB{Rg+#WAE}PI7_+)8^zpk?%nER&Shb;5v z9_5%K=KTzZ_nmC*`N5Nrn2?ZQ@Ws681K)cPGofSCr*?^guL?`~WVjd@V*CZ(%!oR- QhXDvYUHx3vIVCg!0N)@lkpKVy diff --git a/src/main/resources/assets/cmmodular/textures/equipment/arm_right_socket.png b/src/main/resources/assets/cmmodular/textures/equipment/arm_right_socket.png index 602c54c62b57a69597b397d65180a8317995b0b2..e346ce1b3f5df59845c3a0c6a5fba96179d300b3 100644 GIT binary patch delta 253 zcmeBWoXIpnrJk`k$lZzY=1HA;1_lO}bVpxD28NCO+yKM&wNtU=qlsM<-=BDPAFgTYLr6!i7rYMwWmSiZnd-?{1H}Z)CRe5>3 zIEHAPPfl=P5)u;nud1T*&v*jEj0u(p4<6(J;S}i&WpAU{f*6(0oV2pC;(7JzRe|^c sp4nEVTFQ>%FJ8PzC}p+OlWJmM_@pPKSXXu64$xEvPgg&ebxsLQ02Y%=^#A|> delta 112 zcmbQq)XO+QB{Rg+#WAE}PI7_+)8^zpk?%nER&Shb;5v z9_5%K=KTzZ_nmC*`N5Nrn2?ZQ@Ws681K)cPGofSCr*?^guL?`~WVjd@V*CZ(%!oR- QhXDvYUHx3vIVCg!0O7zdmjD0& diff --git a/src/main/resources/assets/cmmodular/textures/equipment/leg_left_socket.png b/src/main/resources/assets/cmmodular/textures/equipment/leg_left_socket.png index 67c7a2b8bd549cbb507764e3faa11440e5032965..bd06fd49b4fe2dfdfa6a96186778947595b2c530 100644 GIT binary patch delta 235 zcmb>HV49#(&sZGf?!Lkk1L zFQ8Dv3kHT#0|tgy2@DKYGZ+}e3+C(!v;j(R7I;J!Gca%qgD@k*tT_@uLG}_)Usv{5 z91oj0~P`mvZvhEj*X$Zi|ewpLEPDcmKg( b)xVxH`R7QS7mWG^GK9g?)z4*}Q$iB}pXEgZ delta 96 zcmeBRs+*t^VaDA@U*|J&@y)%~W+*9^|0XTv%XJ diff --git a/src/main/resources/assets/cmmodular/textures/equipment/leg_right_socket.png b/src/main/resources/assets/cmmodular/textures/equipment/leg_right_socket.png index 67c7a2b8bd549cbb507764e3faa11440e5032965..717c1a74b67a3a31029471508186abae6acd10c7 100644 GIT binary patch delta 259 zcmbE)e-c@Ne1&9>AYTTCDm4a%h86~f zUqGRT7Yq!g1`G_Z5*Qe)W-u^_7tGleXakhsEbxddW?I3Fk^S>hT|;+&tGo0?a`;9Ogjnpl#WqEMb$lA+-4=^GH<$R`d|73k^W z7{YPA^`s%Mv!aN@#mgnG2Z|k>CNwgeEit$%Ahw}V=HHcddoO$H-Kf6bv7`F^?Wo_9 zae{>luW>z_lhCv9`@6Y8ziQIY-L!PqWL>lM&s#>5SCSu+a~sluhBJ7&`njxgN@xNA DMJ!l2 delta 96 zcmZ3$R5w8-%GJ}wF{ENna!Nu%!i>9*zRqXl;+uP|%}`P-|4mxTm-YNV-WU9kjhPU6 zY+@qweug>cnUY~41^@s6cL04^0001vNkl%oew6?I2UI`>tk(ML3>ig4L_~IvCo5hFmgZ>y O0000~41^@s6cL04^0002$NklANJhzwsAx3=&jT!`(R-^F)-&VlCv2F$R@n zjjF2Y;FK`zsy+-uwSn8KL9cR;^(%c5`X$!dEUq%Z%-KgoM1PrCnOQ|7=2IOrtF<;e zMWon(VdmIGL~zcbX&Rj82>{tW)s5o_0Pxg7k?lK1^@s6b9#F800028NklFbM8`Kb_M6PpG~No^Iu~q2!bF8g75@G W206ExokT004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00009a7bBm000XU000XU0RWnu z7ytkO2XskIMF;8#7!(QxPkgal0000PbVXQnLvL+uWo~o;Lw|B*VRU6=Aa`kWXdp*P zO;A^X4i^9b0VYXAK~z}7?Uh|pgfI+%zv4QpKIr;bJV4LE8Mp?=;Xb_vy#P6YI^&Lx z__AeK7ASwd^?M>DZIU)g6#<&2X;_I;N`-6=b2qcrUddUOjq3ZlDU!9>bshGD>&u%1 zShAtVIrqOOuzwe`0tnsdklq-x?+NssOvy`%FxVzlRR!D77}MT)H}g4dsAl&0uhu#N z&cG2sM0Tlxn*e;pcaKqP{Q-Pk0=NSnffo_MT6<+Xw0>$%Br|*a3HSuQfp_4?E7CL_ zUGL?oIgw2Dy0sSP93qkcua_VmfTM`uoTIMmRkOm|CsYe6{~34#8sL7fIdB+@-aGdh ziU?Zk6L9Kg-@SrOML1R@MNzchWm(?*%k#Xo4?RdjdbQ=bjYJ}mNF);3gFp2pmA1}7 RU^4&!002ovPDHLkV1j}nzE}VN delta 189 zcmV;u07C!u1K9zPB!8euL_t(|ob8pp4aFb~gmIK9ya01B104gh25Z0qUV~S?F0l=X zl;*w?{By!SLBU|oIjxvGh={D1(^IImw%nYVU2(^9<(Gi~2rykR#&B=+4&?EPA*u~UB;=gc r0wNM(jN1aJ_rCqz0ssI20KCBub<#6VyDZ!X00000NkvXXu0mjf`omVg diff --git a/src/main/resources/assets/cmmodular/textures/equipment/space_helmet_socket.png b/src/main/resources/assets/cmmodular/textures/equipment/space_helmet_socket.png index 08b6d56759f4b61fbe00da40bdff7227eb2146e1..6fabf2b4a478c5e4033f9ec38477095c7532dea2 100644 GIT binary patch delta 528 zcmV+r0`L8h0;vR$B!2;OQb$4o*~u(_00004XF*Lt006O%3;baP0000WV@Og>004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00009a7bBm000XU000XU0RWnu z7ytkO2XskIMF;8#7!(9M5T1c~0000PbVXQnLvL+uWo~o;Lw|B*VRU6=Aa`kWXdp*P zO;A^X4i^9b0a{5!K~z}7?UXTcgD?<9pGiC^Jnm8$nxvBpU4j|kK%K5&&cO+E26dc4 zm!PmOfG3klillR+G~gjI7!u=2qgOGzV)a?}FAU(l_uK8(d++^bv+=9d%C}>eUb^JqXeYDm-)|bE6xn-KB^Go}n&$7*~h=00X?C|yiIBKmct@R7=2|NJv zp?%Pocg`V%7&nC0+B+#_0=xomQpzW5?V|7E$@W0Icv+SJOW>h?zP$~bb(-fnQc8el zAeK^+=lQoYHxW=(6P#elC znS5*sMX=%kvam*JXBe55bai zO@Vb?yU$OS0X5V8rlHoOr5+KrO{ir)=d4~2GdJHg7ib;6Q`5p6!+)V(fyUE>G-Jrj u>W7D!G0$`N0-==B{kwG>$8j9T8NnM+6-Bv1&tZW80000z@;j|==^1poj5AY({UO#lFT zCIA3{ga82g0001h=l}q9FaQARU;qF*m;eA5aGbhPJOBUy32;bRa{vGf6951U69E94 zoEQKA00(qQO+^Rl2pAM6508TatpET38FWQhbVF}#ZDnqB0DnVrWnpw>WFU8GbZ8() zNlj2!fese{00FH@L_t(I%dL}7NK{c2hrj!tZ{8>amVeNc689}a0-=Q<>Oy6RpoQ&= zC|!s~ZDw4UZX!rfY>~JT#DzrS!i7Y&Xb}WOt9Io=W+67pjJ$bo-hHP~p;eQ{c&I{bTv?QhHSD6V_lR*e3%vKD25rDwVlGg`SjZ`IE3RWsGvwS_= z<(;zq7Cx=tkhMqmQThS^F{8c=@&X4j zj{vwmw~XUz0EQoYRtCVYV~>Hy3!KPa)xJY`G`)!9YJUo@=5^}FX$22kT4X47IfC9k zn*_iz;s6wb0;B0g3V$b@Km?cVL64o3`GriB`6%ZR zkC{;gu3Z<&65iZzam+-R|1}Z2KFkrq2`_g{lPqn|=c;`>mvSw1#H@0000> zc0P`sK7d^cOA#L+O{%n3A%+l^g#ja(WGv)v_Gco3o@SUk^PPL<2mUCvZ{a4nbH(}n z?AE)tRuoW&9fHjf0NXIdY!+T4RfUUSRogISynN>M<00n)AAj7ieHym!H92XYb9Q*` zY6icR*(@+_na?9%^mX*+jW6m218Ct!tDy?^Z7SBP^-i3W6O7XrdclBBoS?&>;{z#q;Rl9USBIyQT8XCPeJlRK8 zN+6O1B^`VZnM0dn+!D>sFu%mnYN(`p=H|5&0(WgQ?+%`?d=XtaiCR349HR((4XQ?> cD)?{u0^*v06S!8pGXMYp07*qoM6N<$g6m6}0ssI2 diff --git a/src/main/resources/assets/cmmodular/textures/items/space_helmet.png b/src/main/resources/assets/cmmodular/textures/items/space_helmet.png index 588cdbb29c04aeaed2b18451fec88d6737aa79c7..fe59276926df8f4035185cb98d87bc6f619a988a 100644 GIT binary patch delta 349 zcmcc3_=9PJNG(&67Iy3=9k`>5jgR3=A9lx&I`x0{IHb9znhg3{`3j3=J&| z48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!CBxDSSURengk}PqJC~?lu%}vcKVQ{W5N=+(t4=hosBvhvk(b;X6uHP4M1;vYp! zbVULPJ~?Z? zm#VPciDBRE$|V&Z{kmXmV4R%RZFo=2_`A$8oFVdQ&MBb@0N-nWI{*Lx delta 191 zcmV;w06_ov0^0$QB!8kwL_t(|oYhjn5yLPDqq#2O30Q;;Sc7pG$AiEG9>aOzOKjXr zudnJUj3h7sKjM{q z;uvCa`fAWY!6OPBcT$2{WN&oHMzBToZb@X0S$X67tNe}Y8PnRn|C{t;FS~KQ`UQ`7a`%NYdV^+vUpDb^ z#BoDEE+>xK!fX0lQnM_1XD@r=xpG(Q|6cxIeDja9mptbLx}Cw()z4*}Q$iB}j3$Eo delta 229 zcmVSa52QC9u!$tC~ zW$iDx;3fp-BLVOiP}L-&jEE$nRMk|~9L?T)A^^BM0AN`b?E8N7%`5?6+qT=#H6k)1 zGBcBia-QcTqHJjW$23zfBqCsD0KmGg003qNGebl!##3`|v0gucyC2UzB@hvq*;AsK zT~eF`YOQd0lu|C_QVQH1wboPpewoDY7T(o{0?nZ(^v(^(KEL@$Gdpf!>KmVshY22& fyb>5*;s3@DwY6%WEK5R700000NkvXXu0mjfiK}Fl diff --git a/src/main/resources/assets/minecraft/atlases/blocks.json b/src/main/resources/assets/minecraft/atlases/blocks.json new file mode 100644 index 0000000..ab049ab --- /dev/null +++ b/src/main/resources/assets/minecraft/atlases/blocks.json @@ -0,0 +1,14 @@ +{ + "sources": [ + { + "type": "directory", + "source": "equipment", + "prefix": "equipment/" + }, + { + "type": "directory", + "source": "items", + "prefix": "items/" + } + ] +} diff --git a/src/main/resources/packs/apotheosis/data/cmmodular/miapi/modules/gem/limb/case_arm_left.json b/src/main/resources/packs/apotheosis/data/cmmodular/miapi/modules/gem/limb/case_arm_left.json index 2c25db5..439cd70 100644 --- a/src/main/resources/packs/apotheosis/data/cmmodular/miapi/modules/gem/limb/case_arm_left.json +++ b/src/main/resources/packs/apotheosis/data/cmmodular/miapi/modules/gem/limb/case_arm_left.json @@ -4,19 +4,23 @@ { "path": "miapi:models/item/armor/gems/medium/[material.texture].json", "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 0, "y": 0, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1}, + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "translation": { + "x": 0, + "y": 0, + "z": 0 + }, + "scale": { + "x": 1, + "y": 1, + "z": 1 + }, "origin": "left_arm" } - }, - { - "path": "miapi:models/item/armor/gems/medium/[material.texture].json", - "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 4.5, "y": 3.5, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1} - } } ], "allowed_in_slots": [ diff --git a/src/main/resources/packs/apotheosis/data/cmmodular/miapi/modules/gem/limb/case_arm_right.json b/src/main/resources/packs/apotheosis/data/cmmodular/miapi/modules/gem/limb/case_arm_right.json index de3489f..2e742cc 100644 --- a/src/main/resources/packs/apotheosis/data/cmmodular/miapi/modules/gem/limb/case_arm_right.json +++ b/src/main/resources/packs/apotheosis/data/cmmodular/miapi/modules/gem/limb/case_arm_right.json @@ -4,19 +4,23 @@ { "path": "miapi:models/item/armor/gems/medium/[material.texture].json", "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 0, "y": 0, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1}, + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "translation": { + "x": 0, + "y": 0, + "z": 0 + }, + "scale": { + "x": 1, + "y": 1, + "z": 1 + }, "origin": "right_arm" } - }, - { - "path": "miapi:models/item/armor/gems/medium/[material.texture].json", - "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": -4.5, "y": 3.5, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1} - } } ], "allowed_in_slots": [ diff --git a/src/main/resources/packs/apotheosis/data/cmmodular/miapi/modules/gem/limb/case_leg_left.json b/src/main/resources/packs/apotheosis/data/cmmodular/miapi/modules/gem/limb/case_leg_left.json index b284ef8..c8b4485 100644 --- a/src/main/resources/packs/apotheosis/data/cmmodular/miapi/modules/gem/limb/case_leg_left.json +++ b/src/main/resources/packs/apotheosis/data/cmmodular/miapi/modules/gem/limb/case_leg_left.json @@ -4,19 +4,23 @@ { "path": "miapi:models/item/armor/gems/medium/[material.texture].json", "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 0, "y": 0, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1}, + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "translation": { + "x": 0, + "y": 0, + "z": 0 + }, + "scale": { + "x": 1, + "y": 1, + "z": 1 + }, "origin": "left_leg" } - }, - { - "path": "miapi:models/item/armor/gems/medium/[material.texture].json", - "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 2.5, "y": -4.5, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1} - } } ], "allowed_in_slots": [ diff --git a/src/main/resources/packs/apotheosis/data/cmmodular/miapi/modules/gem/limb/case_leg_right.json b/src/main/resources/packs/apotheosis/data/cmmodular/miapi/modules/gem/limb/case_leg_right.json index 7959db8..004458d 100644 --- a/src/main/resources/packs/apotheosis/data/cmmodular/miapi/modules/gem/limb/case_leg_right.json +++ b/src/main/resources/packs/apotheosis/data/cmmodular/miapi/modules/gem/limb/case_leg_right.json @@ -4,19 +4,23 @@ { "path": "miapi:models/item/armor/gems/medium/[material.texture].json", "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 0, "y": 0, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1}, + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "translation": { + "x": 0, + "y": 0, + "z": 0 + }, + "scale": { + "x": 1, + "y": 1, + "z": 1 + }, "origin": "right_leg" } - }, - { - "path": "miapi:models/item/armor/gems/medium/[material.texture].json", - "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": -2.5, "y": -4.5, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1} - } } ], "allowed_in_slots": [ diff --git a/src/main/resources/packs/apotheosis/data/cmmodular/miapi/modules/gem/space/case_backtank.json b/src/main/resources/packs/apotheosis/data/cmmodular/miapi/modules/gem/space/case_backtank.json new file mode 100644 index 0000000..4471cce --- /dev/null +++ b/src/main/resources/packs/apotheosis/data/cmmodular/miapi/modules/gem/space/case_backtank.json @@ -0,0 +1,51 @@ +{ + "display_name": "miapi.module.cmmodular.gem.space.case_backtank.name", + "model": [ + { + "path": "miapi:models/item/armor/gems/medium/[material.texture].json", + "transform": { + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "translation": { + "x": 0, + "y": 0, + "z": 0 + }, + "scale": { + "x": 1, + "y": 1, + "z": 1 + }, + "origin": "body" + } + } + ], + "allowed_in_slots": [ + "gem_armor_medium_backtank" + ], + "tag": [ + "gem_armor", + "gem_armor_medium", + "gem_armor_generic", + "gem_armor_medium_generic" + ], + "material_property": [ + "default" + ], + "priority": 0, + "repair_priority": 0, + "rarity": "rare", + "allowed_material": { + "allowedMaterials": [ + "metal", + "crystal", + "glass", + "bone", + "stone" + ], + "cost": 2 + } +} diff --git a/src/main/resources/packs/apotheosis/data/cmmodular/miapi/modules/gem/space/case_helmet.json b/src/main/resources/packs/apotheosis/data/cmmodular/miapi/modules/gem/space/case_helmet.json new file mode 100644 index 0000000..9ac2da6 --- /dev/null +++ b/src/main/resources/packs/apotheosis/data/cmmodular/miapi/modules/gem/space/case_helmet.json @@ -0,0 +1,51 @@ +{ + "display_name": "miapi.module.cmmodular.gem.space.case_helmet.name", + "model": [ + { + "path": "miapi:models/item/armor/gems/medium/[material.texture].json", + "transform": { + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "translation": { + "x": 0, + "y": 0, + "z": 0 + }, + "scale": { + "x": 1, + "y": 1, + "z": 1 + }, + "origin": "head" + } + } + ], + "allowed_in_slots": [ + "gem_armor_medium_helmet" + ], + "tag": [ + "gem_armor", + "gem_armor_medium", + "gem_armor_generic", + "gem_armor_medium_generic" + ], + "material_property": [ + "default" + ], + "priority": 0, + "repair_priority": 0, + "rarity": "rare", + "allowed_material": { + "allowedMaterials": [ + "metal", + "crystal", + "glass", + "bone", + "stone" + ], + "cost": 2 + } +} diff --git a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/socket/arm_left.json b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/socket/arm_left.json index 724153a..b1dc008 100644 --- a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/socket/arm_left.json +++ b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/socket/arm_left.json @@ -12,18 +12,6 @@ "operation": "*", "slot": "chest" } - ], - "model": [ - { - "path": "cmmodular:models/item/armor/model/arm_left/socket/[material.texture].json", - "trim_mode": "ARMOR_LAYER_ONE", - "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 0, "y": 0, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1}, - "origin": "left_arm" - } - } ] }, "replace": { @@ -36,9 +24,21 @@ "slotType": "cosmetic", "translationKey": "miapi.slot.arm_patch.name", "transform": { - "rotation": {"x": 0, "y": 90, "z": 0}, - "translation": {"x": -2.15, "y": -1, "z": 0}, - "scale": {"x": 1.1, "y": 1, "z": 1}, + "rotation": { + "x": 0, + "y": 90, + "z": 0 + }, + "translation": { + "x": -2.15, + "y": -1, + "z": 0 + }, + "scale": { + "x": 1.1, + "y": 1, + "z": 1 + }, "origin": "left_arm" } }, @@ -48,9 +48,21 @@ ], "translationKey": "miapi.slot.gemstone.medium", "transform": { - "rotation": {"x": 90.0, "y": 55.7523, "z": -90.0}, - "translation": {"x": -1.7667, "y": 1.5983, "z": 0.0}, - "scale": {"x": 0.6667, "y": 0.8171, "z": 0.7123}, + "rotation": { + "x": 90.0, + "y": -55.7523, + "z": 90.0 + }, + "translation": { + "x": 5.0891, + "y": -2.1965, + "z": -5.3336 + }, + "scale": { + "x": 0.6667, + "y": 0.8171, + "z": 0.7123 + }, "origin": "left_arm" } } diff --git a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/socket/arm_right.json b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/socket/arm_right.json index 0d7ec19..8807e59 100644 --- a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/socket/arm_right.json +++ b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/socket/arm_right.json @@ -12,18 +12,6 @@ "operation": "*", "slot": "chest" } - ], - "model": [ - { - "path": "cmmodular:models/item/armor/model/arm_right/socket/[material.texture].json", - "trim_mode": "ARMOR_LAYER_ONE", - "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 0, "y": 0, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1}, - "origin": "right_arm" - } - } ] }, "replace": { @@ -36,9 +24,21 @@ "slotType": "cosmetic", "translationKey": "miapi.slot.arm_patch.name", "transform": { - "rotation": {"x": 0, "y": -90, "z": 0}, - "translation": {"x": 2.15, "y": -1, "z": 0}, - "scale": {"x": 1.1, "y": 1, "z": 1}, + "rotation": { + "x": 0, + "y": -90, + "z": 0 + }, + "translation": { + "x": 2.15, + "y": -1, + "z": 0 + }, + "scale": { + "x": 1.1, + "y": 1, + "z": 1 + }, "origin": "right_arm" } }, @@ -48,9 +48,21 @@ ], "translationKey": "miapi.slot.gemstone.medium", "transform": { - "rotation": {"x": 90.0, "y": -55.7523, "z": 90.0}, - "translation": {"x": 1.7667, "y": 1.5983, "z": 0.0}, - "scale": {"x": 0.6667, "y": 0.8171, "z": 0.7123}, + "rotation": { + "x": 90.0, + "y": 55.7523, + "z": -90.0 + }, + "translation": { + "x": -5.0891, + "y": -2.1965, + "z": 5.3336 + }, + "scale": { + "x": 0.6667, + "y": 0.8171, + "z": 0.7123 + }, "origin": "right_arm" } } diff --git a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/socket/leg_left.json b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/socket/leg_left.json index bd3234f..108f27f 100644 --- a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/socket/leg_left.json +++ b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/socket/leg_left.json @@ -12,18 +12,6 @@ "operation": "*", "slot": "legs" } - ], - "model": [ - { - "path": "cmmodular:models/item/armor/model/leg_left/socket/[material.texture].json", - "trim_mode": "ARMOR_LAYER_ONE", - "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 0, "y": 0, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1}, - "origin": "left_leg" - } - } ] }, "replace": { @@ -34,9 +22,21 @@ ], "translationKey": "miapi.slot.gemstone.medium", "transform": { - "rotation": {"x": 0.0, "y": 0.0, "z": -180.0}, - "translation": {"x": 0.0, "y": -1.95, "z": -2.4917}, - "scale": {"x": 0.8333, "y": 1.0, "z": 0.8333}, + "rotation": { + "x": 0.0, + "y": 0.0, + "z": -180.0 + }, + "translation": { + "x": 6.6664, + "y": 7.0, + "z": -9.1664 + }, + "scale": { + "x": 0.8333, + "y": 1.0, + "z": 0.8333 + }, "origin": "left_leg" } } diff --git a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/socket/leg_right.json b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/socket/leg_right.json index 725ea8c..c48a612 100644 --- a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/socket/leg_right.json +++ b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/socket/leg_right.json @@ -12,18 +12,6 @@ "operation": "*", "slot": "legs" } - ], - "model": [ - { - "path": "cmmodular:models/item/armor/model/leg_right/socket/[material.texture].json", - "trim_mode": "ARMOR_LAYER_ONE", - "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 0, "y": -2, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1}, - "origin": "right_leg" - } - } ] }, "replace": { @@ -34,9 +22,21 @@ ], "translationKey": "miapi.slot.gemstone.medium", "transform": { - "rotation": {"x": 0.0, "y": 0.0, "z": -180.0}, - "translation": {"x": 0.0, "y": -1.95, "z": -2.4917}, - "scale": {"x": 0.8333, "y": 1.0, "z": 0.8333}, + "rotation": { + "x": 0.0, + "y": 0.0, + "z": -180.0 + }, + "translation": { + "x": 6.6664, + "y": 7.0, + "z": -9.1664 + }, + "scale": { + "x": 0.8333, + "y": 1.0, + "z": 0.8333 + }, "origin": "right_leg" } } diff --git a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/helmet.json b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/helmet.json index bcacefd..1b1c53f 100644 --- a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/helmet.json +++ b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/helmet.json @@ -21,19 +21,42 @@ "path": "cmmodular:models/item/armor/model/helmet/space/[material.texture].json", "trim_mode": "ARMOR_LAYER_ONE", "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 0, "y": 0, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1}, + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "translation": { + "x": 0, + "y": 0, + "z": 0 + }, + "scale": { + "x": 1, + "y": 1, + "z": 1 + }, "origin": "head" } }, { "path": "cmmodular:models/item/armor/gui/space/helmet/[material.texture].json", "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 0, "y": 0, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1}, - "origin": "item" + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "translation": { + "x": 0, + "y": 0, + "z": 0 + }, + "scale": { + "x": 1, + "y": 1, + "z": 1 + } } } ], diff --git a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/helmet_socket.json b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/helmet_socket.json index 3cc5fe9..42e1bf9 100644 --- a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/helmet_socket.json +++ b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/helmet_socket.json @@ -20,19 +20,42 @@ "path": "cmmodular:models/item/armor/model/helmet/space_socket/[material.texture].json", "trim_mode": "ARMOR_LAYER_ONE", "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 0, "y": 4.1387, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1}, + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "translation": { + "x": 0, + "y": 0, + "z": 0 + }, + "scale": { + "x": 1, + "y": 1, + "z": 1 + }, "origin": "head" } }, { "path": "cmmodular:models/item/armor/gui/space/helmet_socket/[material.texture].json", "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 0, "y": 0, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1}, - "origin": "item" + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "translation": { + "x": 0, + "y": 0, + "z": 0 + }, + "scale": { + "x": 1, + "y": 1, + "z": 1 + } } } ], @@ -44,21 +67,46 @@ ], "translationKey": "miapi.slot.hat.name", "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 0, "y": -1, "z": 0}, - "scale": {"x": 1.1, "y": 1.1, "z": 1.1}, + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "translation": { + "x": 0, + "y": -1, + "z": 0 + }, + "scale": { + "x": 1.1, + "y": 1.1, + "z": 1.1 + }, "origin": "head" } }, "gem": { "allowed": [ - "gem_armor_medium" + "gem_armor_medium_helmet" ], "translationKey": "miapi.slot.gemstone.medium", "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 0, "y": 3, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1} + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "translation": { + "x": -8.0, + "y": -12.06, + "z": -12.64 + }, + "scale": { + "x": 1, + "y": 1, + "z": 1 + }, + "origin": "head" } } }, diff --git a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/life_support.json b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/life_support.json index 4d0ef65..67aa876 100644 --- a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/life_support.json +++ b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/life_support.json @@ -21,9 +21,21 @@ "path": "cmmodular:models/item/armor/model/chest_back/space/[material.texture].json", "trim_mode": "ARMOR_LAYER_ONE", "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 0, "y": 0, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1}, + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "translation": { + "x": 0, + "y": 0, + "z": 0.6667 + }, + "scale": { + "x": 0.8, + "y": 0.9091, + "z": 0.6667 + }, "origin": "body" } } diff --git a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/life_support_socket.json b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/life_support_socket.json index b0f5196..900b002 100644 --- a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/life_support_socket.json +++ b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space/life_support_socket.json @@ -20,9 +20,21 @@ "path": "cmmodular:models/item/armor/model/chest_back/space_socket/[material.texture].json", "trim_mode": "ARMOR_LAYER_ONE", "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 0, "y": -2.1901, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1}, + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "translation": { + "x": 0, + "y": 0, + "z": 0.6667 + }, + "scale": { + "x": 0.8, + "y": 0.9091, + "z": 0.6667 + }, "origin": "body" } } @@ -36,21 +48,45 @@ "slotType": "cosmetic", "translationKey": "miapi.slot.chest_back_center.name", "transform": { - "rotation": {"x": 180, "y": 0, "z": 180}, - "translation": {"x": 0, "y": 1.7, "z": 2.35}, - "scale": {"x": 1, "y": 0.775, "z": 1.1}, + "rotation": { + "x": 180, + "y": 0, + "z": 180 + }, + "translation": { + "x": 0, + "y": 1.7, + "z": 2.35 + }, + "scale": { + "x": 1, + "y": 0.775, + "z": 1.1 + }, "origin": "body" } }, "gem": { "allowed": [ - "gem_armor_medium" + "gem_armor_medium_backtank" ], "translationKey": "miapi.slot.gemstone.medium", "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 0, "y": -1, "z": 0}, - "scale": {"x": 1.1, "y": 1.1, "z": 1.1}, + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "translation": { + "x": -8.8, + "y": -11.07, + "z": -4.3 + }, + "scale": { + "x": 1.1, + "y": 1.1, + "z": 1.1 + }, "origin": "body" } } diff --git a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/limb/gem_arm_left.json b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/limb/gem_arm_left.json index 093fd70..96b67b6 100644 --- a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/limb/gem_arm_left.json +++ b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/limb/gem_arm_left.json @@ -4,19 +4,23 @@ { "path": "miapi:models/item/armor/gems/medium/[material.texture].json", "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 0, "y": 0, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1}, + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "translation": { + "x": 0, + "y": 0, + "z": 0 + }, + "scale": { + "x": 1, + "y": 1, + "z": 1 + }, "origin": "left_arm" } - }, - { - "path": "miapi:models/item/armor/gems/medium/[material.texture].json", - "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 4.5, "y": 3.5, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1} - } } ], "allowed_in_slots": [ diff --git a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/limb/gem_arm_right.json b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/limb/gem_arm_right.json index 1221bbd..e57bc49 100644 --- a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/limb/gem_arm_right.json +++ b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/limb/gem_arm_right.json @@ -4,19 +4,23 @@ { "path": "miapi:models/item/armor/gems/medium/[material.texture].json", "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 0, "y": 0, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1}, + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "translation": { + "x": 0, + "y": 0, + "z": 0 + }, + "scale": { + "x": 1, + "y": 1, + "z": 1 + }, "origin": "right_arm" } - }, - { - "path": "miapi:models/item/armor/gems/medium/[material.texture].json", - "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": -4.5, "y": 3.5, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1} - } } ], "allowed_in_slots": [ diff --git a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/limb/gem_leg_left.json b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/limb/gem_leg_left.json index c720803..c8de66f 100644 --- a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/limb/gem_leg_left.json +++ b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/limb/gem_leg_left.json @@ -4,19 +4,23 @@ { "path": "miapi:models/item/armor/gems/medium/[material.texture].json", "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 0, "y": 0, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1}, + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "translation": { + "x": 0, + "y": 0, + "z": 0 + }, + "scale": { + "x": 1, + "y": 1, + "z": 1 + }, "origin": "left_leg" } - }, - { - "path": "miapi:models/item/armor/gems/medium/[material.texture].json", - "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 2.5, "y": -4.5, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1} - } } ], "allowed_in_slots": [ diff --git a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/limb/gem_leg_right.json b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/limb/gem_leg_right.json index c9d161b..6d6b80e 100644 --- a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/limb/gem_leg_right.json +++ b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/limb/gem_leg_right.json @@ -4,19 +4,23 @@ { "path": "miapi:models/item/armor/gems/medium/[material.texture].json", "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": 0, "y": 0, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1}, + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "translation": { + "x": 0, + "y": 0, + "z": 0 + }, + "scale": { + "x": 1, + "y": 1, + "z": 1 + }, "origin": "right_leg" } - }, - { - "path": "miapi:models/item/armor/gems/medium/[material.texture].json", - "transform": { - "rotation": {"x": 0, "y": 0, "z": 0}, - "translation": {"x": -2.5, "y": -4.5, "z": 0}, - "scale": {"x": 1, "y": 1, "z": 1} - } } ], "allowed_in_slots": [ diff --git a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/space/gem_backtank.json b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/space/gem_backtank.json new file mode 100644 index 0000000..7b729ff --- /dev/null +++ b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/space/gem_backtank.json @@ -0,0 +1,52 @@ +{ + "display_name": "miapi.module.cmmodular.gem.space.gem_backtank.name", + "model": [ + { + "path": "miapi:models/item/armor/gems/medium/[material.texture].json", + "transform": { + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "translation": { + "x": 0, + "y": 0, + "z": 0 + }, + "scale": { + "x": 1, + "y": 1, + "z": 1 + }, + "origin": "body" + } + } + ], + "allowed_in_slots": [ + "gem_armor_medium_backtank" + ], + "material_property": [ + "default", + "gem_armor_handheld", + "gem_armor_generic" + ], + "module_stats": { + "gem_power": 1 + }, + "tag": [ + "gem_armor", + "gem_armor_medium", + "gem_armor_generic", + "gem_armor_medium_generic" + ], + "priority": 0, + "repair_priority": 0, + "rarity": "uncommon", + "allowed_material": { + "allowedMaterials": [ + "gem_armor" + ], + "cost": 2 + } +} diff --git a/src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/space/gem_helmet.json b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/space/gem_helmet.json new file mode 100644 index 0000000..cb39d68 --- /dev/null +++ b/src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/space/gem_helmet.json @@ -0,0 +1,52 @@ +{ + "display_name": "miapi.module.cmmodular.gem.space.gem_helmet.name", + "model": [ + { + "path": "miapi:models/item/armor/gems/medium/[material.texture].json", + "transform": { + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "translation": { + "x": 0, + "y": 0, + "z": 0 + }, + "scale": { + "x": 1, + "y": 1, + "z": 1 + }, + "origin": "head" + } + } + ], + "allowed_in_slots": [ + "gem_armor_medium_helmet" + ], + "material_property": [ + "default", + "gem_armor_handheld", + "gem_armor_generic" + ], + "module_stats": { + "gem_power": 1 + }, + "tag": [ + "gem_armor", + "gem_armor_medium", + "gem_armor_generic", + "gem_armor_medium_generic" + ], + "priority": 0, + "repair_priority": 0, + "rarity": "uncommon", + "allowed_material": { + "allowedMaterials": [ + "gem_armor" + ], + "cost": 2 + } +} diff --git a/tools/ARMOUR_EDITOR.py b/tools/ARMOUR_EDITOR.py index ae49548..7a69354 100644 --- a/tools/ARMOUR_EDITOR.py +++ b/tools/ARMOUR_EDITOR.py @@ -22,9 +22,12 @@ whichever of the two happened to sort in front. *MIAPI's arithmetic.* Transforms are kept one per `origin` and merged only within an entry, which is why a gem whose slot says `body` never picks up the -`left_arm` transform above it. Merging itself is an exact matrix multiply in -1.21; `--lossy-merge` reproduces the Euler round trip older versions did, which -drops shear wherever a rotation meets a non-uniform scale. +`left_arm` transform above it. A merge multiplies the two matrices and then +decomposes the product back into translation, Euler angles and scale - 2.3.8 +does that on every merge, not only the old versions, so shear is dropped +wherever a rotation meets a non-uniform scale, which is what Armory's limb +slots are. That is the default here; `--exact-merge` keeps the product whole, +for seeing what the shear would have done. Everything is in model pixels: +x is the wearer's left, +y is down, -z is forward, and the origin is the base of the neck. @@ -82,8 +85,20 @@ class Placement: @property def origin(self): - """The body part this module is drawn under. Absent means the torso.""" - return self.transform.get('origin', 'body') + """The body part this module is drawn under, or None for none of them. + + Absent does **not** mean the torso. MIAPI renders armour by walking a + fixed list of seven part names and matching models to each, so a + transform that names nothing - or names `item` - matches no part and is + never drawn on the wearer at all. That entry is the inventory model, + which is why `scene_icons` picks it out by its missing origin. + """ + return self.transform.get('origin') + + @property + def worn(self): + """Whether MIAPI would draw this on the body rather than in the slot UI.""" + return self.origin in mc.BODY_PARTS @property def auto_chain(self): @@ -195,6 +210,7 @@ PLATES = ( ) SOCKETS = 'src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/socket' +SPACE = 'src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space' LIMB_GEMS = 'src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/limb' @@ -278,6 +294,74 @@ def scene_armory(res, args): return out +def scene_space(res, args): + """The space set, composed the way MIAPI composes it rather than by slot alone. + + A module's model carries a transform of its own, and the slot above it + carries another; `ModuleModel` looks up `SlotProperty.getTransformStack(key)` + and the model's matrix applies inside it. Drawing the slot transform alone - + which an earlier version of this scene did - shows the model at whatever size + it was drawn rather than the size it renders at, and the chest_back slot + scales by (1.25, 1.1, 1.5), so that is the difference between a backpack and + a slab wider than the torso. Both transforms are read from the module JSON + here, so the picture moves when the files do, and the module transform is the + editable one. + + Entries whose origin is not a body part are skipped rather than drawn at the + torso: MIAPI matches a model to a part by name, so those never appear on a + wearer at all - they are the inventory model. + """ + chest = json.loads(res.read('data/tm_armory/miapi/modules/armor/chestplate.json'))['slots'] + helmet = json.loads(res.read('data/tm_armory/miapi/modules/armor/helmet.json'))['slots'] + # part, module file, and the slot transform carrying it. Nothing here is a + # root module: a helmet plate sits in the helmet item's `hat` slot, whose + # transform lifts it from the neck onto the head (`y -3.925`, the head's own + # centre) and scales it up a quarter. Leaving that out drops every helmet + # four pixels and makes it look like the model is wrong when it is not. + pieces = ( + ('body', 'life_support_socket', chest['chest_back']['transform']), + ('head', 'helmet_socket', helmet['hat']['transform']), + ) + out = [] + for i, (part, name, outer_tr) in enumerate(pieces): + path = os.path.join(REPO, SPACE, f'{name}.json') + with open(path) as fh: + module = json.load(fh) + entries = module['data']['replace']['model'] + for j, entry in enumerate(entries): + tr = entry['transform'] + if tr.get('origin') not in mc.BODY_PARTS: + continue + try: + quads = mc.model_quads(res.model(entry['path'], args.variant), res) + except KeyError: + print(f'missing model {entry["path"]}') + continue + out.append(Placement( + f'{name} ({tr["origin"]})', quads, tr, + outer=mc.transform_matrix(outer_tr) if outer_tr else None, + chain_origin=part, + source=(path, ['data', 'replace', 'model', j, 'transform']), + colour=PALETTE[i % len(PALETTE)])) + + # Armory's own back plate and helmet, unedited, as the controls: both are + # known to sit right, so a piece of ours that lands somewhere else is a + # number to change rather than a chain to doubt. + out.append(Placement( + 'armory back plate (control)', + mc.model_quads(res.model('miapi:models/item/armor/model/chest_back/base/' + '[material.texture].json', args.variant), res), + chest['chest_back']['transform'], colour=(0.55, 0.55, 0.62))) + out.append(Placement( + 'armory helmet (control)', + mc.model_quads(res.model('miapi:models/item/armor/model/helmet/base/' + '[material.texture].json', args.variant), res), + {'rotation': {'x': 0, 'y': 0, 'z': 0}, 'translation': {'x': 0, 'y': 0, 'z': 0}, + 'scale': {'x': 1, 'y': 1, 'z': 1}, 'origin': 'head'}, + colour=(0.62, 0.62, 0.68))) + return out + + def scene_icons(res, args): """The four inventory icons, side by side, each with its gem on it. @@ -319,7 +403,7 @@ def scene_icons(res, args): SCENES = {'vanilla': scene_vanilla, 'sockets': scene_sockets, - 'armory': scene_armory, 'icons': scene_icons} + 'armory': scene_armory, 'icons': scene_icons, 'space': scene_space} # Scenes drawn in item space rather than on a body: +y is up and the camera # has to agree, or every icon renders upside down and every offset written @@ -443,7 +527,7 @@ class Viewport: self.res, self.placements, self.args = res, placements, args self.force_chain = args.chain self.textured = not args.no_textures - self.lossy = args.lossy_merge + self.lossy = not args.exact_merge self.step = args.step self.turn_step = 7.5 self.show_body = args.body @@ -541,7 +625,7 @@ class Viewport: f' {p.summary()}', f' step {self.step} chain {"forced" if self.force_chain else "auto"}' f' textures {"on" if self.textured else "off"}' - f' merge {"lossy (1.20)" if self.lossy else "exact (1.21)"}', + f' merge {"as MIAPI does" if self.lossy else "exact, shear kept"}', '', HELP, ] @@ -936,8 +1020,9 @@ def main(argv=None): ap.add_argument('--body', action='store_true', help='draw the wearer as a reference') ap.add_argument('--chain', action='store_true', help='force the slot chain on even where MIAPI would file it elsewhere') - ap.add_argument('--lossy-merge', action='store_true', - help='decompose each merge to Euler angles, as MIAPI did before 1.21') + ap.add_argument('--exact-merge', action='store_true', + help='keep the whole matrix product instead of the Euler round trip' + ' MIAPI does on every merge') args = ap.parse_args(argv) if args.scene is None: diff --git a/tools/bb-apply.sh b/tools/bb-apply.sh new file mode 100755 index 0000000..5c8ee73 --- /dev/null +++ b/tools/bb-apply.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Read the Blockbench project back into the module JSON, then push it in game. +# +# bb-apply.sh dry run: show what each cube would become +# bb-apply.sh --write write the module files and live-reload the game +set -euo pipefail +REPO=/home/themiro/code.dev/create-mekanism-modular +JAR=$(ls "/home/themiro/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/AbdelPack Delta/minecraft/mods/"Truly-Modular-armory-*.jar | head -1) +"$REPO/tools/.venv/bin/python" "$REPO/tools/bb_import.py" "$JAR" "$REPO/placement.bbmodel" "$@" +if [ "${1:-}" = "--write" ]; then + "$REPO/tools/live-reload.sh" "$REPO/src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/socket/arm_left.json" +fi diff --git a/tools/bb-open.sh b/tools/bb-open.sh new file mode 100755 index 0000000..7d4f87d --- /dev/null +++ b/tools/bb-open.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Regenerate the placement project from the live JSON and open it. +# +# bb-open.sh regenerate from the module files, then open +# bb-open.sh --keep open what is already there, without regenerating +# +# Regenerating is the default because the project is a view of the module JSON, +# not a second copy of it: anything changed in the files since the last export +# should be what you see. +set -euo pipefail +REPO=/home/themiro/code.dev/create-mekanism-modular +JAR=$(ls "/home/themiro/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/AbdelPack Delta/minecraft/mods/"Truly-Modular-armory-*.jar | head -1) +OUT=$REPO/placement.bbmodel +[ "${1:-}" = "--keep" ] || "$REPO/tools/.venv/bin/python" "$REPO/tools/bb_export.py" "$JAR" "$OUT" +setsid flatpak run net.blockbench.Blockbench "$OUT" >/dev/null 2>&1 < /dev/null & +echo "opening $OUT" diff --git a/tools/bb_export.py b/tools/bb_export.py new file mode 100644 index 0000000..66665c6 --- /dev/null +++ b/tools/bb_export.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +"""Write the armour placement out as a Blockbench project. + + bb_export.py [out.bbmodel] + +Everything that positions a piece is drawn: the wearer, Armory's plates, this +mod's bezels, and - as the only unlocked cubes - the pieces whose transforms are +ours to change. Each of those carries its destination in its own name, in an +`[edit:...]` tag, so the file is the only thing `bb_import.py` needs. + +The reference geometry is locked rather than merely present. A gem is aimed at a +bezel, and a bezel that can be nudged by accident is a bezel that stops being an +answer. +""" +import json +import os +import sys + +import numpy as np + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import mcmodel as mc +import ARMOUR_EDITOR as ed +import blockbench as bb + +REPO = ed.REPO +SOCKETS = ed.SOCKETS +SPACE = 'src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space' + +# hue per body part, so a group reads as a unit in the outliner +COLOURS = {'left_arm': 1, 'right_arm': 2, 'left_leg': 3, 'right_leg': 4, + 'body': 5, 'head': 6} + + +def box_cube(name, quads, matrix, pivot, colour, locked): + """One drawn thing, already composed, as a Blockbench cube.""" + pts = np.vstack([q.pts for q in quads]) + pts = (np.column_stack([pts, np.ones(len(pts))]) @ np.asarray(matrix).T)[:, :3] + pts = pts + np.asarray(pivot, float) + centre, lo, hi, r = bb.oriented_box(pts, matrix) + return bb.cube(name, + origin=bb.to_bb(centre), + lo=bb.to_bb(centre)[0] * 0 + bb.to_bb(centre) + lo, # placeholder + hi=bb.to_bb(centre) + hi, + rotation=bb.matrix_to_euler_zyx(bb.rot_to_bb(r)), + colour=colour, locked=locked) + + +def main(jar, out): + class A: + pass + args = A() + args.scene = 'sockets'; args.jar = [jar]; args.variant = 'default' + args.gem = 'medium'; args.chain = False; args.exact_merge = False + args.layer1 = args.layer2 = 'miapi:item/armor/base/iron/layer_1' + res = mc.Resources([jar, os.path.join(REPO, 'src/main/resources')]) + + elements, outliner = [], [] + textures, tex_index = [], {} + + def texture(ref): + """Embed a texture once, and hand back the index a face refers to it by.""" + if ref is None: + return None + if ref not in tex_index: + try: + img = res.image(ref) + except (KeyError, OSError): + tex_index[ref] = None + return None + tex_index[ref] = len(textures) + textures.append(bb.texture_entry(ref.split('/')[-1], img, len(textures))) + return tex_index[ref] + + def add(grp_children, name, quads, matrix, pivot, colour, locked): + pts = np.vstack([q.pts for q in quads]) + pts = (np.column_stack([pts, np.ones(len(pts))]) @ np.asarray(matrix).T)[:, :3] + pts = pts + np.asarray(pivot, float) + centre, lo, hi, r = bb.oriented_box(pts, matrix) + c = bb.to_bb(centre) + for q in quads: + texture(q.texture) + faces = bb.faces_from_quads(quads, matrix, r, tex_index) + cu = bb.cube(name, origin=c, lo=c + lo, hi=c + hi, + rotation=bb.matrix_to_euler_zyx(bb.rot_to_bb(r)), + colour=colour, locked=locked, faces=faces) + elements.append(cu) + grp_children.append(cu['uuid']) + + # ---------------------------------------------------------------- wearer + body = [] + for part, (o, s, off, mirror) in mc.HUMANOID.items(): + if part == 'hat': + continue + q = mc.cube_quads(o, s, off, 0.0, mirror, (64, 64), + 'minecraft:entity/player/wide/steve', 0) + add(body, f'{part} (wearer)', q, np.eye(4), mc.PIVOTS[part], 8, True) + outliner.append(bb.group('WEARER (locked reference)', body)) + + # ------------------------------------------------------------ the limbs + chest = json.loads(res.read('data/tm_armory/miapi/modules/armor/chestplate.json'))['slots'] + pants = json.loads(res.read('data/tm_armory/miapi/modules/armor/pants.json'))['slots'] + slots = {'arm_left': chest['arm_left'], 'arm_right': chest['arm_right'], + 'leg_left': pants['leg_left'], 'leg_right': pants['leg_right']} + gem_q = mc.model_quads(res.model('miapi:models/item/armor/gems/medium/' + '[material.texture].json', 'default'), res) + + for part, slot, model in ed.PLATES: + kids = [] + limb_tr = slots[slot]['transform'] + limb = mc.transform_matrix(limb_tr) + piv = mc.PIVOTS[part] + col = COLOURS[part] + + plate = mc.model_quads(res.model(f'miapi:models/item/armor/model/{model}/' + '[material.texture].json', 'default'), res) + add(kids, f'{slot} plate', plate, limb, piv, col, True) + try: + bez = mc.model_quads(res.model(f'cmmodular:models/item/armor/model/{slot}/' + 'socket/[material.texture].json', 'default'), res) + add(kids, f'{slot} bezel', bez, limb, piv, col, True) + except KeyError: + pass + + path = os.path.join(REPO, SOCKETS, f'{slot}.json') + with open(path) as fh: + gem_tr = json.load(fh)['data']['replace']['slots']['gem']['transform'] + world = mc.merge(mc.transform_matrix(gem_tr), limb, True) + add(kids, f'{slot} GEM [edit:socket/{slot}#gem]', gem_q, world, piv, col, False) + outliner.append(bb.group(part, kids, origin=bb.to_bb(piv))) + + # ------------------------------------------------------------ space gear + helmet = json.loads(res.read('data/tm_armory/miapi/modules/armor/helmet.json'))['slots'] + # The `hat` slot is what puts a helmet on the head rather than the neck. + for part, name, outer_tr, path_in_repo in ( + ('body', 'life_support_socket', chest['chest_back']['transform'], + f'{SPACE}/life_support_socket.json'), + ('head', 'helmet_socket', helmet['hat']['transform'], + f'{SPACE}/helmet_socket.json')): + kids = [] + piv = mc.PIVOTS[part] + outer = mc.transform_matrix(outer_tr) + with open(os.path.join(REPO, path_in_repo)) as fh: + module = json.load(fh) + for i, entry in enumerate(module['data']['replace']['model']): + tr = entry['transform'] + if tr.get('origin') not in mc.BODY_PARTS: + continue # inventory model; MIAPI never draws it on a body + q = mc.model_quads(res.model(entry['path'], 'default'), res) + world = mc.merge(mc.transform_matrix(tr), outer, True) + add(kids, f'{name} [edit:space/{name}#model{i}]', q, world, piv, + COLOURS[part], False) + if kids: + outliner.append(bb.group(f'{part} - {name}', kids, origin=bb.to_bb(piv))) + + doc = bb.project('cmmodular placement', outliner, elements, textures) + with open(out, 'w') as fh: + json.dump(doc, fh, indent=1) + editable = [e['name'] for e in elements if not e['locked']] + print(f'{out}\n {len(elements)} cubes, {len(outliner)} groups, ' + f'{len(textures)} textures') + print(' editable:') + for n in editable: + print(f' {n}') + + +if __name__ == '__main__': + main(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else 'placement.bbmodel') diff --git a/tools/bb_import.py b/tools/bb_import.py new file mode 100644 index 0000000..cdffa5d --- /dev/null +++ b/tools/bb_import.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +"""Read a Blockbench project back into the module JSON. + + bb_import.py [--write] + +Only cubes tagged `[edit:#]` are read; everything else in the file +is reference geometry. For each one the cube's box is turned back into the +transform that would produce it: + + D = T(t) @ R @ S the world matrix MIAPI must end up with + own = inv(slot_chain) @ D the transform the module file actually stores + +`own` is then decomposed to translation/Euler/scale, which is all a MIAPI +transform can hold. That step can lose something: the limb slots are a rotation +against a non-uniform scale, and inverting one puts shear into `own` that the +triple cannot carry - so the result is measured against the cube it came from +and the error is printed in pixels. A tenth of a pixel is nothing; a whole one +means the pose asked for cannot be expressed and wants a different rotation. + +Blockbench composes a cube's rotation as ZYX and MIAPI composes its own as XYZ, +so nothing here passes Euler triples between the two - only matrices. +""" +import json +import os +import sys + +import numpy as np + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import mcmodel as mc +import ARMOUR_EDITOR as ed +import blockbench as bb + +REPO = ed.REPO +SOCKETS = ed.SOCKETS +SPACE = 'src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/space' + + +def local_box(res, path, variant='default'): + """The model's own extents, before anything is applied to it.""" + q = mc.model_quads(res.model(path, variant), res) + pts = np.vstack([x.pts for x in q]) + return (pts.min(0) + pts.max(0)) / 2, (pts.max(0) - pts.min(0)) / 2, q + + +def target_info(res, spec): + """Where a tag points: the module file, its model, and the chain above it.""" + where, key = spec.split('#') + kind, name = where.split('/') + chest = json.loads(res.read('data/tm_armory/miapi/modules/armor/chestplate.json'))['slots'] + pants = json.loads(res.read('data/tm_armory/miapi/modules/armor/pants.json'))['slots'] + if kind == 'socket': + path = os.path.join(REPO, SOCKETS, f'{name}.json') + slot = (chest if name.startswith('arm') else pants)[name]['transform'] + part = {'arm_left': 'left_arm', 'arm_right': 'right_arm', + 'leg_left': 'left_leg', 'leg_right': 'right_leg'}[name] + keys = ['data', 'replace', 'slots', key, 'transform'] + model = 'miapi:models/item/armor/gems/medium/[material.texture].json' + return path, keys, mc.transform_matrix(slot), mc.PIVOTS[part], model + path = os.path.join(REPO, SPACE, f'{name}.json') + i = int(key.replace('model', '')) + with open(path) as fh: + entry = json.load(fh)['data']['replace']['model'][i] + origin = entry['transform'].get('origin') + if origin == 'body': + outer = mc.transform_matrix(chest['chest_back']['transform']) + elif origin == 'head': + helmet = json.loads(res.read('data/tm_armory/miapi/modules/armor/helmet.json'))['slots'] + outer = mc.transform_matrix(helmet['hat']['transform']) + else: + outer = np.eye(4) + keys = ['data', 'replace', 'model', i, 'transform'] + return path, keys, outer, mc.PIVOTS[origin], entry['path'] + + +def main(jar, model_path, write): + res = mc.Resources([jar, os.path.join(REPO, 'src/main/resources')]) + doc = json.load(open(model_path)) + np.set_printoptions(precision=3, suppress=True) + + edits = [e for e in doc['elements'] if '[edit:' in e.get('name', '')] + if not edits: + print('no [edit:...] cubes in that file'); return 1 + print(f'{len(edits)} editable cube(s)\n') + + for cu in edits: + spec = cu['name'].split('[edit:')[1].split(']')[0] + path, keys, outer, pivot, model = target_info(res, spec) + c_local, h_local, quads = local_box(res, model) + + # the cube, back in world coordinates + lo = np.asarray(cu['from'], float); hi = np.asarray(cu['to'], float) + c_bb = (lo + hi) / 2 + h_bb = (hi - lo) / 2 + r_bb = bb.euler_zyx_to_matrix(cu.get('rotation', [0, 0, 0])) + c_world = bb.to_world(c_bb) + r_world = bb.rot_to_bb(r_bb) # conjugation is its own inverse + + scale = np.where(h_local > 1e-9, h_bb / np.where(h_local > 1e-9, h_local, 1), 1.0) + s = np.diag(scale) + t = (c_world - np.asarray(pivot, float)) - r_world @ s @ c_local + + d = np.eye(4); d[:3, :3] = r_world @ s; d[:3, 3] = t + own = np.linalg.inv(outer) @ d + tr = mc.decompose(own) + + # what MIAPI will actually render, after the lossy merge + got = mc.merge(mc.transform_matrix(tr), outer, True) + pts = np.vstack([q.pts for q in quads]) + want = (np.column_stack([pts, np.ones(len(pts))]) @ d.T)[:, :3] + have = (np.column_stack([pts, np.ones(len(pts))]) @ got.T)[:, :3] + err = np.linalg.norm(want - have, axis=1).max() + + flag = '' if err < 0.05 else (' <-- lossy, check it' if err < 0.5 else ' <-- CANNOT BE EXPRESSED') + print(f'{spec}') + print(f' world centre {(c_world).round(3)} size {(h_bb*2).round(3)}') + print(f' -> t {[round(v,4) for v in tr["translation"].values()]}' + f' r {[round(v,4) for v in tr["rotation"].values()]}' + f' s {[round(v,4) for v in tr["scale"].values()]}') + print(f' residual after MIAPI\'s merge: {err:.4f}px{flag}') + + if write: + with open(path) as fh: + doc_j = json.load(fh) + node = doc_j + for k in keys[:-1]: + node = node[k] + keep = node[keys[-1]].get('origin') + node[keys[-1]] = { + 'rotation': {a: round(float(tr['rotation'][a]), 4) for a in 'xyz'}, + 'translation': {a: round(float(tr['translation'][a]), 4) for a in 'xyz'}, + 'scale': {a: round(float(tr['scale'][a]), 4) for a in 'xyz'}, + } + if keep: + node[keys[-1]]['origin'] = keep + with open(path, 'w') as fh: + json.dump(doc_j, fh, indent=2); fh.write('\n') + print(f' wrote {os.path.relpath(path, REPO)}') + print() + if not write: + print('(dry run - pass --write to apply)') + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1], sys.argv[2], '--write' in sys.argv)) diff --git a/tools/blockbench.py b/tools/blockbench.py new file mode 100644 index 0000000..3c8e715 --- /dev/null +++ b/tools/blockbench.py @@ -0,0 +1,263 @@ +"""Take armour placement out to Blockbench and bring it back. + +Three coordinate systems meet here and none of them agree, so each conversion is +written down rather than assumed. + +**MIAPI model pixels** - the units the module JSON is written in. `+x` is the +wearer's left, **`+y` is down**, `-z` is forward, and the origin is the base of +the neck. Those are the raw `ModelPart` coordinates: vanilla builds the torso as +`addBox(-4, 0, -2, 8, 12, 4)`, which is why the body runs y 0..12 downward from +the neck and the head runs y -8..0 above it. A translation is in pixels; +`Transform.toModelTransformation` multiplies by 1/16 at render time, not before. + +**The MIAPI chain** - `Transform.toMatrix` is `T * Rx * Ry * Rz * S`, and +`Transform.merge(parent, child)` computes `child.mul(parent)` and then +`fromMatrix`, which decomposes back to translation/Euler/scale and **drops any +shear**. `TransformMap.add(id, t)` files that per origin and calls +`merge(existing, t)`. So a module's world matrix is + + world = slot_chain(origin) @ own (own applies first, in its own frame) + +and the pivot of the body part named by `origin` is added on top. + +**Blockbench** - `+y` is up, and cube rotations compose as **ZYX** +(`Format.euler_order` defaults to `"ZYX"`), not the XYZ that MIAPI uses. Getting +that pair backwards silently mangles every rotated piece, which is why both +directions here go through matrices and never through raw Euler triples. + +The bridge between the first and the last is a 180 degree turn about z, +`diag(-1, -1, 1)`. Flipping y alone would also stand the model up, but it is a +mirror: it would send a rotation back as its opposite sense, and the wearer's +left arm would come home as the right. The turn is a proper rotation, and it is +its own inverse, so one constant serves both ways. +""" +import json +import math +import uuid as _uuid + +import numpy as np + +import mcmodel as mc + +# World <-> Blockbench. A 180 degree turn about z: head up, handedness intact, +# and self-inverse so export and import share it. +FLIP = np.diag([-1.0, -1.0, 1.0]) + +# ...and then dropped so the wearer stands on the grid rather than hanging under +# it. The world origin is the base of the neck, so a whole body sits 24 pixels +# *below* y=0 and lands outside the view Blockbench opens with - the model is +# there, just off the top of nothing. 24 puts the feet on zero, which is where +# Blockbench expects a humanoid and where its default camera is aimed. +# +# This is a constant rather than something written into the file on purpose: +# both directions read it from here, so they cannot disagree. +ORIGIN = np.array([0.0, 24.0, 0.0]) + + +def to_bb(points): + """World model pixels -> Blockbench coordinates.""" + return np.asarray(points, float) @ FLIP.T + ORIGIN + + +def to_world(points): + """Blockbench coordinates -> world model pixels.""" + return (np.asarray(points, float) - ORIGIN) @ FLIP.T + + +def rot_to_bb(r): + """Conjugate a world rotation into Blockbench's frame.""" + return FLIP @ r @ FLIP + + +def _rx(a): + c, s = math.cos(a), math.sin(a) + return np.array([[1, 0, 0], [0, c, -s], [0, s, c]], float) + + +def _ry(a): + c, s = math.cos(a), math.sin(a) + return np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]], float) + + +def _rz(a): + c, s = math.cos(a), math.sin(a) + return np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]], float) + + +def euler_zyx_to_matrix(deg): + """Blockbench's rotation triple -> a matrix. + + `Format.euler_order` is `"ZYX"`, which in three.js means the intrinsic + sequence z then y then x, and an intrinsic sequence composes by + right-multiplication: `Rz @ Ry @ Rx`. + """ + x, y, z = (math.radians(float(v)) for v in deg) + return _rz(z) @ _ry(y) @ _rx(x) + + +def matrix_to_euler_zyx(m): + """The inverse of `euler_zyx_to_matrix`, in degrees.""" + m = np.asarray(m, float) + sy = -m[2, 0] + sy = max(-1.0, min(1.0, sy)) + if abs(sy) < 0.999999: + x = math.atan2(m[2, 1], m[2, 2]) + y = math.asin(sy) + z = math.atan2(m[1, 0], m[0, 0]) + else: # gimbal lock: fold x into z + x = 0.0 + y = math.asin(sy) + z = math.atan2(-m[0, 1], m[1, 1]) + return [round(math.degrees(v), 4) for v in (x, y, z)] + + +def oriented_box(points, matrix): + """Fit a Blockbench cube to a transformed box. + + A cube there is an axis-aligned box plus a rotation about a pivot, so it can + carry a rotated box and a box scaled unevenly along its own axes - which is + what a limb transform makes of a plate - but not a sheared one. The rotation + is taken from the composed matrix by polar decomposition and the extents are + measured in that frame, so a shear shows up as a box slightly too big rather + than as silence. + """ + pts = np.asarray(points, float) + u, _, vt = np.linalg.svd(np.asarray(matrix, float)[:3, :3]) + r = u @ vt + if np.linalg.det(r) < 0: + u[:, -1] *= -1 + r = u @ vt + centre = (pts.min(0) + pts.max(0)) / 2 + local = (pts - centre) @ r + return centre, local.min(0), local.max(0), r + + +def new_uuid(): + return str(_uuid.uuid4()) + + +# Which Blockbench face a direction points at. The list is in Blockbench's own +# axis order, and the signs are read in *Blockbench* space, so a source face is +# matched by turning its normal into the cube's local frame first. +FACE_AXES = (('east', (1, 0, 0)), ('west', (-1, 0, 0)), + ('up', (0, 1, 0)), ('down', (0, -1, 0)), + ('south', (0, 0, 1)), ('north', (0, 0, -1))) + + +def faces_from_quads(quads, matrix, rot_local, tex_index): + """Per-face uv and texture for a fitted box, matched by normal. + + A cube here is a box fitted to geometry that has already been rotated and + scaled, so the source model's faces are not in the places a cube expects + them. Each source quad is matched to the cube face its normal ends up + pointing at, which is the only correspondence that survives an arbitrary + rotation. + + UVs come across normalised and are written in the project's 0-16 space, so + one number works for a 16x16 gem and a 64x64 wearer alike - Blockbench reads + a face uv as a fraction of whatever texture the face names, and the project + resolution is 16. Nothing has to know how big any png is. + """ + faces = {name: {'uv': [0, 0, 16, 16], 'texture': None} for name, _ in FACE_AXES} + for q in quads: + n = np.cross(q.pts[1] - q.pts[0], q.pts[2] - q.pts[0]) + ln = np.linalg.norm(n) + if ln < 1e-9: + continue + n = (np.asarray(matrix, float)[:3, :3] @ (n / ln)) + ln = np.linalg.norm(n) + if ln < 1e-9: + continue + n = rot_local.T @ (n / ln) # into the cube's own frame + n = FLIP @ n # and into Blockbench's + name = max(FACE_AXES, key=lambda fa: float(np.dot(n, fa[1])))[0] + idx = tex_index.get(q.texture) + if idx is None: + continue + u0, v0 = q.uv.min(axis=0) + u1, v1 = q.uv.max(axis=0) + faces[name] = {'uv': [round(u0 * 16, 4), round(v0 * 16, 4), + round(u1 * 16, 4), round(v1 * 16, 4)], + 'texture': idx} + return faces + + +def texture_entry(name, image, index): + """One embedded texture, as Blockbench stores an internal one.""" + import base64 + import io + buf = io.BytesIO() + image.save(buf, format='PNG') + return { + 'path': '', 'name': f'{name}.png', 'folder': '', + 'namespace': '', 'id': str(index), 'width': image.width, + 'height': image.height, 'uv_width': 16, 'uv_height': 16, + 'particle': False, 'use_as_default': False, 'layers_enabled': False, + 'sync_to_project': '', 'render_mode': 'default', 'render_sides': 'auto', + 'frame_time': 1, 'frame_order_type': 'loop', 'frame_order': '', + 'frame_interpolate': False, 'visible': True, 'internal': True, + 'saved': False, 'uuid': new_uuid(), + 'relative_path': '', + 'source': 'data:image/png;base64,' + base64.b64encode(buf.getvalue()).decode(), + } + + +def cube(name, origin, lo, hi, rotation, colour=0, locked=False, faces=None): + """One Blockbench cube, in Blockbench coordinates.""" + return { + 'name': name, + 'box_uv': False, + 'rescale': False, + 'locked': locked, + 'render_order': 'default', + 'allow_mirror_modeling': True, + 'from': [round(float(v), 4) for v in lo], + 'to': [round(float(v), 4) for v in hi], + 'autouv': 0, + 'color': colour, + 'origin': [round(float(v), 4) for v in origin], + 'rotation': [round(float(v), 4) for v in rotation], + 'faces': faces or {f: {'uv': [0, 0, 16, 16], 'texture': None} + for f in ('north', 'east', 'south', 'west', 'up', 'down')}, + 'type': 'cube', + 'uuid': new_uuid(), + } + + +def group(name, children, origin=(0, 0, 0)): + return { + 'name': name, + 'origin': [round(float(v), 4) for v in origin], + 'rotation': [0, 0, 0], + 'color': 0, + 'uuid': new_uuid(), + 'export': True, + 'mirror_uv': False, + 'isOpen': True, + 'locked': False, + 'visibility': True, + 'autouv': 0, + 'children': children, + } + + +def project(name, outliner, elements, textures=None): + """A .bbmodel in the shape `Codecs.project.parse` reads. + + `free` is the format because it is the only one that lets a cube hold a + rotation on all three axes at once; the java block format snaps to a single + axis in 22.5 degree steps and would quietly round every placement here. + """ + return { + 'meta': {'format_version': '4.10', 'model_format': 'free', 'box_uv': False}, + 'name': name, + 'model_identifier': '', + 'visible_box': [4, 4, 0], + 'variable_placeholders': '', + 'variable_placeholder_buttons': [], + 'unhandled_root_fields': {}, + 'resolution': {'width': 16, 'height': 16}, + 'elements': elements, + 'outliner': outliner, + 'textures': textures or [], + } diff --git a/tools/live-edit.lua b/tools/live-edit.lua new file mode 100644 index 0000000..822835e --- /dev/null +++ b/tools/live-edit.lua @@ -0,0 +1,62 @@ +-- Live placement editing: save a file, watch it change in game. +-- +-- Sourced on top of your own config with `-c luafile`, never with `-u`, so +-- nothing here sets an option or steals a mapping - it only adds the reload. +-- +-- Saving any module JSON rebuilds the mod, copies it into whichever instance is +-- running, and tells that game to re-read the half of the jar the file belongs +-- to: `/reload` for module placement under `packs/`, F3+T for models and +-- textures under `assets/`. The build is async, so a save never blocks. +-- +-- :Reload rebuild and reload now, without saving +-- :ReloadLog open the build log after a failure +-- :ReloadOff stop reloading on save (:ReloadOn to resume) + +local REPO = '/home/themiro/code.dev/create-mekanism-modular' +local SCRIPT = REPO .. '/tools/live-reload.sh' +local running, enabled = false, true + +local function reload(file) + if running then + vim.notify('[live] a reload is already in flight', vim.log.levels.WARN) + return + end + running = true + vim.notify('[live] building ' .. vim.fn.fnamemodify(file, ':t') .. ' ...') + local out = {} + local function grab(_, d) + for _, l in ipairs(d or {}) do if l ~= '' then table.insert(out, l) end end + end + vim.fn.jobstart({ SCRIPT, file }, { + stdout_buffered = true, stderr_buffered = true, + on_stdout = grab, on_stderr = grab, + on_exit = function(_, code) + running = false + local msg = '[live] ' .. table.concat(out, ' | ') + vim.notify(code == 0 and msg or (msg .. ' (:ReloadLog)'), + code == 0 and vim.log.levels.INFO or vim.log.levels.ERROR) + end, + }) +end + +-- Only the files that actually feed placement, so an unrelated save never +-- kicks off a build. +vim.api.nvim_create_autocmd('BufWritePost', { + group = vim.api.nvim_create_augroup('CmmodularLive', { clear = true }), + pattern = { REPO .. '/src/main/resources/packs/*.json', + REPO .. '/src/main/resources/assets/*.json' }, + callback = function(a) if enabled then reload(a.match) end end, +}) + +vim.api.nvim_create_user_command('Reload', function() + reload(vim.api.nvim_buf_get_name(0)) +end, { desc = 'rebuild, deploy, and reload in game' }) +vim.api.nvim_create_user_command('ReloadLog', function() + vim.cmd('split /tmp/live-reload-build.log') +end, { desc = 'build log from the last live reload' }) +vim.api.nvim_create_user_command('ReloadOff', function() + enabled = false; vim.notify('[live] reload-on-save off') +end, {}) +vim.api.nvim_create_user_command('ReloadOn', function() + enabled = true; vim.notify('[live] reload-on-save on') +end, {}) diff --git a/tools/live-reload.sh b/tools/live-reload.sh new file mode 100755 index 0000000..9bffc1d --- /dev/null +++ b/tools/live-reload.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# Push a placement change into the running game. +# +# live-reload.sh +# +# The mod jar is deliberately NOT touched. NeoForge holds every mod jar open for +# the whole session, and `cp` rewrites a file in place - same inode - so +# overwriting the jar under a running game invalidates its view of its own +# archive. The symptom is not a crash but modules quietly going missing: +# +# could not find module cmmodular:armor/socket/arm_left substituting with empty module +# +# ...and every part built on them vanishing off the wearer, with the jar on disk +# still passing `unzip -t`. Only a restart clears it. +# +# So the module JSON goes into the world's own datapack folder instead, which is +# read fresh on every `/reload` and loads after the mod's built-in packs, so the +# copy there wins. Editing is live and nothing is held open. +# +# Models and textures under `assets/` are resourcepack, not datapack, and are +# still read from the jar - those need a restart. The script says so rather than +# pretending F3+T reached them. +set -uo pipefail + +REPO=/home/themiro/code.dev/create-mekanism-modular +INSTANCES=/home/themiro/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances +export YDOTOOL_SOCKET=${YDOTOOL_SOCKET:-/run/user/1000/.ydotool_socket} +SAVED=${1:-} +PY=$REPO/tools/.venv/bin/python +MCWIN="Minecraft NeoForge" +PACK=cmmodular-live + +say() { printf '%s\n' "$*"; } + +# `pgrep -f java.*minecraft` also matches a Gradle daemon - the word turns up in +# a classpath - so the instance is identified by the Prism path in the cmdline +# and nothing else. Guessing wrong here means reloading an instance nobody is +# looking at, or claiming the game is up when it is not. +INSTANCE="" +for p in $(pgrep -x java 2>/dev/null); do + c=$(tr '\0' ' ' < /proc/$p/cmdline 2>/dev/null) + case "$c" in + *PrismLauncher/instances/*) + INSTANCE=${c#*PrismLauncher/instances/} + INSTANCE=${INSTANCE%%/*} + break ;; + esac +done +[ -z "$INSTANCE" ] && { say "game not running (no Prism instance in any java process)"; exit 0; } +MC="$INSTANCES/$INSTANCE/minecraft" +WORLD=$(ls -t "$MC/saves" 2>/dev/null | head -1) +[ -z "$WORLD" ] && { say "no world in $INSTANCE"; exit 1; } + +case "$SAVED" in + *"/assets/"*) + say "NOTE: $(basename "$SAVED") is a resourcepack file (models/textures)." + say " Those load from the jar and cannot be hot-swapped - restart to see it." + exit 0 ;; +esac + +# Validate before shipping: a malformed module is dropped silently by MIAPI and +# looks exactly like the part being mispositioned. +if [ -n "$SAVED" ] && ! "$PY" -c "import json,sys;json.load(open(sys.argv[1]))" "$SAVED" 2>/tmp/live-json.err; then + say "INVALID JSON - not deploying"; sed 's/^/ /' /tmp/live-json.err; exit 1 +fi + +DEST="$MC/saves/$WORLD/datapacks/$PACK" +FIRST=0; [ -d "$DEST" ] || FIRST=1 # a pack added mid-session needs enabling +mkdir -p "$DEST" +cat > "$DEST/pack.mcmeta" <<'META' +{ + "pack": { + "description": "cmmodular live placement edits - overrides the jar's modules while iterating", + "pack_format": 48 + } +} +META +rm -rf "$DEST/data" +for p in "$REPO"/src/main/resources/packs/*/data; do + cp -r "$p/." "$DEST/data/" 2>/dev/null || { mkdir -p "$DEST/data"; cp -r "$p/." "$DEST/data/"; } +done +say "staged -> saves/$WORLD/datapacks/$PACK" + +wmctrl -a "$MCWIN" >/dev/null 2>&1 || { say "no game window; datapack staged"; exit 0; } +sleep 0.6 + +menu_open() { + grim /tmp/live-reload-probe.png 2>/dev/null || return 1 + "$PY" - <<'EOF' +import sys +import numpy as np +from PIL import Image +a=np.array(Image.open('/tmp/live-reload-probe.png').convert('RGB')) +h,w,_=a.shape +b=a[h//6:h//2, w//3:2*w//3] +g=((abs(b[...,0].astype(int)-b[...,1])<12)&(abs(b[...,1].astype(int)-b[...,2])<12) + &(b[...,0]>110)&(b[...,0]<210)).mean() +sys.exit(0 if g>0.15 else 1) +EOF +} +# One Escape closes chat, closes the menu, or opens it from gameplay; after it +# the only ambiguity left is whether the menu is up, which is checked. Without +# this a stray open chat eats the T and the command posts as literal "t/reload". +ydotool key 1:1 1:0; sleep 0.7 +if menu_open; then ydotool key 1:1 1:0; sleep 0.7; fi + +# QWERTZ: ydotool speaks US keycodes, so the two punctuation marks these +# commands need are sent by position - '/' is shift+7, '-' is where US puts '/'. +slash() { ydotool key 42:1 8:1 8:0 42:0; sleep 0.2; } +dash() { ydotool key 53:1 53:0; sleep 0.2; } + +ydotool key 20:1 20:0; sleep 0.7 # T -> chat +slash +if [ "$FIRST" = 1 ]; then + # A datapack that appeared after the world was loaded is present but not + # enabled; `/reload` alone will not pick it up. Enabling reloads too. + ydotool type -d 28 "datapack enable file"; slash + ydotool type -d 28 "cmmodular"; dash; ydotool type -d 28 "live" + sleep 0.4; ydotool key 28:1 28:0; sleep 2.0 + say "enabled datapack $PACK (first run this session)" +else + ydotool type -d 28 "reload"; sleep 0.4 + ydotool key 28:1 28:0; sleep 1.5 + say "/reload sent - $(basename "${SAVED:-all modules}") live" +fi diff --git a/tools/mc-type.sh b/tools/mc-type.sh new file mode 100755 index 0000000..9075c2c --- /dev/null +++ b/tools/mc-type.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Type a chat command into Minecraft on a QWERTZ keyboard. +# +# ydotool speaks evdev keycodes, which are *positional* and named for a US +# layout, so `ydotool type` writes whatever the user's layout has at those +# positions. On QWERTZ that swaps y and z, and '/' is shift+7 rather than a key +# of its own. Sending the wrong thing does not fail loudly - it posts as chat, +# which is how "/reload" once went out as "-reload" and "day" as "daz". +# +# mc-type.sh "time set day" types / and presses enter +set -uo pipefail +export YDOTOOL_SOCKET=${YDOTOOL_SOCKET:-/run/user/1000/.ydotool_socket} + +declare -A K=( + [a]=30 [b]=48 [c]=46 [d]=32 [e]=18 [f]=33 [g]=34 [h]=35 [i]=23 + [j]=36 [k]=37 [l]=38 [m]=50 [n]=49 [o]=24 [p]=25 [q]=16 [r]=19 + [s]=31 [t]=20 [u]=22 [v]=47 [w]=17 [x]=45 + [y]=44 [z]=21 # QWERTZ: y and z sit on each other's keys + [0]=11 [1]=2 [2]=3 [3]=4 [4]=5 [5]=6 [6]=7 [7]=8 [8]=9 [9]=10 + [' ']=57 [.]=52 [_]=12 +) + +ydotool key 42:0 54:0 # make sure no shift is stuck +sleep 0.2 +ydotool key 20:1 20:0 # T -> open chat +sleep 0.9 +ydotool key 42:1 8:1 8:0 42:0 # shift+7 -> '/' +sleep 0.3 +ydotool key 42:0 54:0 # release shift again, explicitly +sleep 0.2 +cmd="$1" +for (( i=0; i<${#cmd}; i++ )); do + ch="${cmd:$i:1}" + code="${K[$ch]:-}" + if [ -n "$code" ]; then ydotool key "$code:1" "$code:0"; sleep 0.04; fi +done +sleep 0.4 +ydotool key 28:1 28:0 # enter diff --git a/tools/mcmodel.py b/tools/mcmodel.py index e852004..1a6d67f 100644 --- a/tools/mcmodel.py +++ b/tools/mcmodel.py @@ -213,25 +213,42 @@ def decompose(m): 'scale': {'x': s[0], 'y': s[1], 'z': s[2]}} -def merge(parent, child, lossy=False): - """MIAPI's `Transform.merge`. +def merge(parent, child, lossy=True): + """MIAPI's `Transform.merge`, as MIAPI 2.3.8 actually has it. - In 1.21 this is a plain matrix multiply - `Transform` holds a `Matrix4f` - and `merge` returns `new Transform(parent.matrix.mul(child.matrix))`, with - no round trip through Euler angles. The parent is the transform already - accumulated and the child the one being added, so the child applies first. + Read out of the jar rather than assumed, because the assumption was wrong + and cost a set of gem placements. `Transform.merge(a, b)` builds both + matrices, computes `b.mul(a)` - so the accumulated transform applies first + and the new one after it, in the frame the accumulation left off in - and + then hands the product to `fromMatrix`. That last step is the important + one: it decomposes back into translation, Euler angles and per-axis scale, + so every merge drops whatever shear the product had. Armory's limb slots + are a rotation with a non-uniform scale, which is exactly where shear + appears, so this is not a corner case here - it is the case. - `lossy` reproduces the older behaviour, where the product was decomposed - back into translation, Euler angles and scale before being stored - which - silently drops the shear that appears the moment a rotation meets a - non-uniform scale. Armory's limb slots are exactly that, so the two answers - differ there and it is worth being able to see both. + `TransformMap.add` calls it as `merge(what is there, what is being added)`, + which is what makes a slot transform act *after* the plate's rather than + inside it. A number written as an offset in the model's own space lands + somewhere else entirely; `packs/armory/.../socket/*.json` are solved in the + frame this describes. + + `lossy=False` gives the exact product instead, for seeing what the shear + would have done had MIAPI kept it. """ product = child @ parent return transform_matrix(decompose(product)) if lossy else product # `HumanoidModel.createMesh` pivots - the frame each `origin` resolves against. +# The only origins MIAPI will draw on a body. Verified against +# `ArmorModelManager$ModelPartProvider.modelParts` in miapi 2.3.8: the renderer +# walks this fixed list and matches models to each name. A transform naming +# anything else - or naming nothing at all - matches no part and is never drawn +# on the wearer; it is the inventory model. `Transform.repair` leaves a missing +# origin missing, it does not substitute one. +BODY_PARTS = ('head', 'hat', 'body', + 'left_arm', 'right_arm', 'left_leg', 'right_leg') + PIVOTS = { 'head': (0.0, 0.0, 0.0), 'hat': (0.0, 0.0, 0.0), diff --git a/tools/redeploy.sh b/tools/redeploy.sh new file mode 100755 index 0000000..9ca9617 --- /dev/null +++ b/tools/redeploy.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Build, swap the jar under a stopped instance, relaunch, wait for the title screen. +# +# The jar is NOT hot-swappable: NeoForge holds every mod jar open for the whole +# session, so overwriting it in place under a running game leaves the archive +# valid on disk but invalid to the running JVM. The symptom is silent - +# "could not find module cmmodular:armor/socket/arm_left substituting with +# empty module" - and every part built on it vanishes off the wearer. Hence the +# kill/copy/relaunch, every time, with no shortcut. +set -uo pipefail +REPO=/home/themiro/code.dev/create-mekanism-modular +INST=/home/themiro/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/Abdelpak-Delta-testing/minecraft +JAR=cmmodular-1.4.0.jar + +cd "$REPO" || exit 1 +# `gradlew build` fails on this toolchain: the :test task cannot be created +# ("Type T not present"). `jar` is the only target we need anyway. +./gradlew jar -q --console=plain 2>&1 | grep -vi 'warning\|restricted' +[ -f "build/libs/$JAR" ] || { echo "build produced no jar"; exit 1; } + +pid=$(for p in $(pgrep -x java 2>/dev/null); do + tr '\0' ' ' < /proc/$p/cmdline 2>/dev/null \ + | grep -q 'PrismLauncher/instances/Abdelpak-Delta-testing' && echo "$p"; done | head -1) +if [ -n "$pid" ]; then + kill "$pid" + for _ in $(seq 1 40); do kill -0 "$pid" 2>/dev/null || break; sleep 0.5; done + kill -0 "$pid" 2>/dev/null && kill -9 "$pid" + sleep 2 +fi + +cp "build/libs/$JAR" "$INST/mods/$JAR" || exit 1 +unzip -t "$INST/mods/$JAR" >/dev/null || { echo "deployed jar is corrupt"; exit 1; } + +: > "$INST/logs/latest.log" 2>/dev/null +cd /tmp && setsid flatpak run org.prismlauncher.PrismLauncher \ + --launch Abdelpak-Delta-testing >/dev/null 2>&1 < /dev/null & + +for _ in $(seq 1 120); do + grep -qi 'Sound engine started\|OpenAL initialized' "$INST/logs/latest.log" 2>/dev/null && { + echo "ready (title screen)" + grep -i 'could not find module\|substituting with empty' "$INST/logs/latest.log" | tail -5 + exit 0; } + sleep 5 +done +echo "timed out waiting for the title screen" diff --git a/tools/solve_gems.py b/tools/solve_gems.py new file mode 100755 index 0000000..d6c7c8c --- /dev/null +++ b/tools/solve_gems.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""Solve gem-slot translations from where you want the gem to actually appear. + +Placement cannot be dialled in by hand because the gem model is `item/generated`: +a 2x2 sprite block sitting at the dead centre (8,8) of a 16x16 sheet, so its +geometry is 8px from the model origin on every axis. MIAPI's toMatrix is +T * Rx * Ry * Rz * S with no centring offset, so the rotation pivots on that +origin and flings the gem up to ~12px away. Translation is therefore NOT the +gem's position - it is whatever cancels that throw. + + world = pivot + flip( T + R*S*c ), flip = negate x,y, c = (8,8,8) + +`flip` is the block-model(+y up) -> ModelPart(+y down) conversion. Inverting: + + T = flip(D) - R*S*c + +with D the desired gem centre as an offset from the body part's pivot. + +Edit TARGET below (in world coordinates, +x = wearer's LEFT, +y = DOWN, +-z = forward) and run. Rotation and scale are read from the JSON and preserved. +""" +import math, json, collections, os + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BASE = os.path.join(REPO, "src/main/resources/packs/armory/data/cmmodular" + "/miapi/modules") + +# vanilla HumanoidModel pivots +PIVOT = {"arm_left": (5, 2, 0), "arm_right": (-5, 2, 0), + "leg_left": (1.9, 12, 0), "leg_right": (-1.9, 12, 0), + "life_support_socket": (0, 0, 0), "helmet_socket": (0, 0, 0)} + +# Which datapack file holds each slot. +FILE = {n: f"armor/socket/{n}.json" for n in + ("arm_left", "arm_right", "leg_left", "leg_right")} +FILE["life_support_socket"] = "armor/space/life_support_socket.json" +FILE["helmet_socket"] = "armor/space/helmet_socket.json" + +# The helmet gem is the one slot that does NOT go through the z180 flip. It +# chains through the hat slot instead, which scales by 1.25 and lifts by +# -3.925 to sit the helmet on the skull rather than the neck: +# world = (T + R*S*c) * 1.25 + (0, -3.925, 0) +# Solving it with the flip inverts y and drops the gem past the chin to the +# floor. Verified against two in-game observations: T_y -2.0 rendered just +# under the chin (predicted +3.58) and T_y +11.5 at floor height (+20.45). +HAT_SCALE, HAT_LIFT = 1.25, -3.925 +NO_FLIP = {"helmet_socket"} + +# Desired gem centre, in WORLD model pixels. Reference geometry: +# arms x +-3..7 (surface at +-7), shoulder y 2, hand y 14 +# legs x 0..+-4 (surface at +-2), hip y 12, knee y 18, foot y 24 +# front face z -2; -z is forward +TARGET = { + "arm_left": ( 8.30, 2.00, 0.00), # outer pauldron face, jutting ~1.3px proud + "arm_right": (-8.30, 2.00, 0.00), + "leg_left": ( 1.90, 13.00, -2.50), # just under the knee + "leg_right": (-1.90, 13.00, -2.50), + # pack occupies x +-2.80, y -1.36..5.91, z 2.00..4.00 + "life_support_socket": ( 0.00, 2.27, 4.50), # centre of the rear face + # dome is +-4.25*1.25 about the lift: y -9.24..+1.39, front face z -5.31 + "helmet_socket": ( 0.00, -9.00, -5.80), # upper edge of the front face +} + +d = math.radians +def rx(a): c, s = math.cos(a), math.sin(a); return [[1,0,0],[0,c,-s],[0,s,c]] +def ry(a): c, s = math.cos(a), math.sin(a); return [[c,0,s],[0,1,0],[-s,0,c]] +def rz(a): c, s = math.cos(a), math.sin(a); return [[c,-s,0],[s,c,0],[0,0,1]] +def mul(A, B): return [[sum(A[i][k]*B[k][j] for k in range(3)) for j in range(3)] for i in range(3)] +def ap(M, v): return [sum(M[i][k]*v[k] for k in range(3)) for i in range(3)] +def rot(r): return mul(rx(d(r["x"])), mul(ry(d(r["y"])), rz(d(r["z"])))) +def flip(v): return [-v[0], -v[1], v[2]] + +C = [8.0, 8.0, 8.0] + +def main(): + for name, world in TARGET.items(): + path = os.path.join(BASE, FILE[name]) + doc = json.load(open(path), object_pairs_hook=collections.OrderedDict) + t = doc["data"]["replace"]["slots"]["gem"]["transform"] + piv = PIVOT[name] + D = [world[i] - piv[i] for i in range(3)] + S = [t["scale"][k] for k in "xyz"] + throw = ap(rot(t["rotation"]), [C[i]*S[i] for i in range(3)]) + if name in NO_FLIP: + lift = [0.0, HAT_LIFT, 0.0] + T = [round((D[i] - lift[i]) / HAT_SCALE - throw[i], 4) + 0.0 for i in range(3)] + else: + T = [round(flip(D)[i] - throw[i], 4) + 0.0 for i in range(3)] + + old = dict(t["translation"]) + for i, k in enumerate("xyz"): + t["translation"][k] = T[i] + open(path, "w").write(json.dumps(doc, indent=2) + "\n") + + if name in NO_FLIP: + landed = [piv[i] + (T[j] + throw[j]) * HAT_SCALE + [0.0, HAT_LIFT, 0.0][j] + for i, j in enumerate(range(3))] + else: + landed = [piv[i] + flip([T[j] + throw[j] for j in range(3)])[i] for i in range(3)] + assert all(abs(landed[i] - world[i]) < 1e-3 for i in range(3)), f"{name} did not solve" + print(f"{name:9} T {old['x']:+8.3f},{old['y']:+8.3f},{old['z']:+8.3f}" + f" -> {T[0]:+8.3f},{T[1]:+8.3f},{T[2]:+8.3f}" + f" world ({landed[0]:+6.2f},{landed[1]:+6.2f},{landed[2]:+6.2f})") + +if __name__ == "__main__": + main()