Compare commits
2 Commits
main
...
gem-placem
| Author | SHA1 | Date |
|---|---|---|
|
|
1450101edb | |
|
|
c711ae2831 |
54
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
|
the shoulder; a knee gem faces forward already. Right-hand pieces are the same
|
||||||
numbers with x and the turn negated.
|
numbers with x and the turn negated.
|
||||||
|
|
||||||
They are not right yet, and two attempts to make them right are worth recording
|
What made them hard is now known, and it was an arithmetic mistake rather than
|
||||||
so that they are not repeated. Naming the limb as the gem slot's `origin` - so
|
a matter of taste. A slot transform does not act in the model's own space. MIAPI
|
||||||
the offset lands in the entry the limb pass reads and the gem follows the arm -
|
accumulates transforms per origin with `TransformMap.add`, which calls
|
||||||
composes correctly on paper and in game puts the gems several blocks away from
|
`Transform.merge(what is there, what is being added)`, and that merge computes
|
||||||
the wearer, floating. And `tools/preview_armour.py`, which applies the same chain
|
`new.toMatrix().mul(accumulated.toMatrix())` - so the plate's transform applies
|
||||||
MIAPI does and draws the result from any angle, agrees with the paper rather than
|
to the geometry first and the slot's afterwards, in the body part's frame. An
|
||||||
with the game: given the configuration that in practice left gems at the neck, it
|
offset written as though it were measured on the model lands somewhere else
|
||||||
puts them on the pauldron. The tool is worth keeping for reading Armory's own
|
entirely, which is why the gems sat at the collar with their sides swapped and
|
||||||
geometry - where the plate bands are, which face is which, how far the chain
|
the knee gems ended up at the belly.
|
||||||
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.
|
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
|
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
|
transform that names an origin has nothing left for the inventory icon, which
|
||||||
|
|
@ -212,10 +224,19 @@ nothing asks for costs nothing.
|
||||||
Nothing else changes. Sockets are counted per case rather than per item, so a
|
Nothing else changes. Sockets are counted per case rather than per item, so a
|
||||||
sword with a case in its pommel reports one the same way a chestplate does and
|
sword with a case in its pommel reports one the same way a chestplate does and
|
||||||
a guard holding two small cases reports two; Apotheosis sockets gems into
|
a guard holding two small cases reports two; Apotheosis sockets gems into
|
||||||
weapons already. All three are filed under `packs/apotheosis` rather than with
|
weapons already. They are all filed under `packs/apotheosis` rather than with
|
||||||
the pommels, because a case without Apotheosis is an item that grants nothing,
|
the pommels, because a case without Apotheosis is an item that grants nothing,
|
||||||
while a case without Arsenal is one that simply never fits anywhere.
|
while a case without Arsenal is one that simply never fits anywhere.
|
||||||
|
|
||||||
|
Every socket that names a body part needs a case of its own, because a slot
|
||||||
|
takes only what its `allowed` list names and each of those lists names one key:
|
||||||
|
the four limbs, the space helmet's visor and the backtank. That is the cost of
|
||||||
|
placing a gem on the limb it was cut into - the generic case answers for
|
||||||
|
`gem_armor_medium` and cannot answer for a key nothing else uses. The counting
|
||||||
|
is by module id, so a new case has to be added to `CASES` in
|
||||||
|
`ApothicSockets.java` as well as written; one that is missing from that list
|
||||||
|
still fits its slot and still wears its gemstone, and grants nothing.
|
||||||
|
|
||||||
One socket a case, and none of it is in the module JSON, because every route
|
One socket a case, and none of it is in the module JSON, because every route
|
||||||
through data is closed. MIAPI's `components` property sets a component rather
|
through data is closed. MIAPI's `components` property sets a component rather
|
||||||
than adding to it, so two cases both writing `1` leave the piece with one socket
|
than adding to it, so two cases both writing `1` leave the piece with one socket
|
||||||
|
|
@ -386,6 +407,15 @@ of them lands somewhere interpolated and muddy.
|
||||||
`textures/*/templates/` holds blanks to draw over - the same size as the real
|
`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
|
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.
|
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,
|
`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
|
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.
|
new piece is wanted rather than a repaint of an existing one.
|
||||||
|
|
|
||||||
|
|
@ -46,11 +46,14 @@ public final class ApothicSockets {
|
||||||
private static final String MIAPI_MODULE = "smartin.miapi.modules.ItemModule";
|
private static final String MIAPI_MODULE = "smartin.miapi.modules.ItemModule";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Module ids that are a gem case: one per gem size, one per limb socket.
|
* Module ids that are a gem case: one per gem size, and one per socket that
|
||||||
* A case is not listed twice for being allowed in a pommel as well as in
|
* names a body part - the four limbs, the space helmet's visor and the
|
||||||
* armour - it is one module either way, and this counts modules, so a
|
* backtank. A case is not listed twice for being allowed in a pommel as
|
||||||
* guard holding two small cases is worth two sockets by the same rule
|
* well as in armour - it is one module either way, and this counts modules,
|
||||||
* that makes a chestplate holding one worth one.
|
* so a guard holding two small cases is worth two sockets by the same rule
|
||||||
|
* that makes a chestplate holding one worth one. A case missing from this
|
||||||
|
* list still fits its slot and still wears its gemstone; it simply grants
|
||||||
|
* nothing, which is the failure that brought the space cases here.
|
||||||
*/
|
*/
|
||||||
private static final List<String> CASES = List.of(
|
private static final List<String> CASES = List.of(
|
||||||
"cmmodular:gem/apothic_case",
|
"cmmodular:gem/apothic_case",
|
||||||
|
|
@ -59,7 +62,9 @@ public final class ApothicSockets {
|
||||||
"cmmodular:gem/limb/case_arm_left",
|
"cmmodular:gem/limb/case_arm_left",
|
||||||
"cmmodular:gem/limb/case_arm_right",
|
"cmmodular:gem/limb/case_arm_right",
|
||||||
"cmmodular:gem/limb/case_leg_left",
|
"cmmodular:gem/limb/case_leg_left",
|
||||||
"cmmodular:gem/limb/case_leg_right");
|
"cmmodular:gem/limb/case_leg_right",
|
||||||
|
"cmmodular:gem/space/case_helmet",
|
||||||
|
"cmmodular:gem/space/case_backtank");
|
||||||
|
|
||||||
/** MIAPI's module tree, reached without naming its types at compile time. */
|
/** MIAPI's module tree, reached without naming its types at compile time. */
|
||||||
private static Method getModules;
|
private static Method getModules;
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,10 @@
|
||||||
"miapi.module.cmmodular.gem.limb.case_leg_left.name": "Left Knee Apothic Gem Case",
|
"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.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.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.dragonbone": "Dragonbone",
|
||||||
"miapi.material.cmmodular.bone.hippogryph_talon": "Talon",
|
"miapi.material.cmmodular.bone.hippogryph_talon": "Talon",
|
||||||
"miapi.material.cmmodular.bone.sea_serpent_fang": "Sea Serpent Fang",
|
"miapi.material.cmmodular.bone.sea_serpent_fang": "Sea Serpent Fang",
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
"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": {
|
"textures": {
|
||||||
"1": "cmmodular:equipment/life_support_pack",
|
"1": "cmmodular:equipment/life_support_pack",
|
||||||
"particle": "cmmodular:equipment/life_support_pack"
|
"particle": "cmmodular:equipment/life_support_pack"
|
||||||
|
|
@ -8,24 +7,86 @@
|
||||||
"elements": [
|
"elements": [
|
||||||
{
|
{
|
||||||
"name": "tanks",
|
"name": "tanks",
|
||||||
"from": [-3.5, -6.5, 2],
|
"from": [
|
||||||
"to": [3.5, 1.5, 5],
|
-3.5,
|
||||||
|
-6.5,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"to": [
|
||||||
|
3.5,
|
||||||
|
1.5,
|
||||||
|
5
|
||||||
|
],
|
||||||
"faces": {
|
"faces": {
|
||||||
"north": {"uv": [8, 8, 16, 16], "texture": "#1"},
|
"north": {
|
||||||
"east": {"uv": [16, 8, 24, 16], "texture": "#1"},
|
"uv": [
|
||||||
"south": {"uv": [24, 8, 32, 16], "texture": "#1"},
|
1.5,
|
||||||
"west": {"uv": [0, 8, 8, 16], "texture": "#1"},
|
3.0,
|
||||||
"up": {"uv": [8, 0, 16, 8], "texture": "#1"},
|
5.0,
|
||||||
"down": {"uv": [16, 0, 24, 8], "texture": "#1"}
|
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": [
|
"groups": [
|
||||||
{
|
{
|
||||||
"name": "Life Support Pack",
|
"name": "Life Support Pack",
|
||||||
"origin": [0, 0, 0],
|
"origin": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
"color": 0,
|
"color": 0,
|
||||||
"children": [0]
|
"children": [
|
||||||
|
0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
"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": {
|
"textures": {
|
||||||
"1": "cmmodular:equipment/life_support_pack_socket",
|
"1": "cmmodular:equipment/life_support_pack_socket",
|
||||||
"particle": "cmmodular:equipment/life_support_pack_socket"
|
"particle": "cmmodular:equipment/life_support_pack_socket"
|
||||||
|
|
@ -24,55 +20,55 @@
|
||||||
"faces": {
|
"faces": {
|
||||||
"north": {
|
"north": {
|
||||||
"uv": [
|
"uv": [
|
||||||
8,
|
1.5,
|
||||||
8,
|
3.0,
|
||||||
16,
|
5.0,
|
||||||
16
|
11.0
|
||||||
],
|
],
|
||||||
"texture": "#1"
|
"texture": "#1"
|
||||||
},
|
},
|
||||||
"east": {
|
"east": {
|
||||||
"uv": [
|
"uv": [
|
||||||
16,
|
5.0,
|
||||||
8,
|
3.0,
|
||||||
24,
|
6.5,
|
||||||
16
|
11.0
|
||||||
],
|
],
|
||||||
"texture": "#1"
|
"texture": "#1"
|
||||||
},
|
},
|
||||||
"south": {
|
"south": {
|
||||||
"uv": [
|
"uv": [
|
||||||
24,
|
6.5,
|
||||||
8,
|
3.0,
|
||||||
32,
|
10.0,
|
||||||
16
|
11.0
|
||||||
],
|
],
|
||||||
"texture": "#1"
|
"texture": "#1"
|
||||||
},
|
},
|
||||||
"west": {
|
"west": {
|
||||||
"uv": [
|
"uv": [
|
||||||
0,
|
0.0,
|
||||||
8,
|
3.0,
|
||||||
8,
|
1.5,
|
||||||
16
|
11.0
|
||||||
],
|
],
|
||||||
"texture": "#1"
|
"texture": "#1"
|
||||||
},
|
},
|
||||||
"up": {
|
"up": {
|
||||||
"uv": [
|
"uv": [
|
||||||
8,
|
1.5,
|
||||||
0,
|
0.0,
|
||||||
16,
|
5.0,
|
||||||
8
|
3.0
|
||||||
],
|
],
|
||||||
"texture": "#1"
|
"texture": "#1"
|
||||||
},
|
},
|
||||||
"down": {
|
"down": {
|
||||||
"uv": [
|
"uv": [
|
||||||
16,
|
5.0,
|
||||||
0,
|
0.0,
|
||||||
24,
|
8.5,
|
||||||
8
|
3.0
|
||||||
],
|
],
|
||||||
"texture": "#1"
|
"texture": "#1"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
"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": {
|
"textures": {
|
||||||
"1": "cmmodular:equipment/space_helmet",
|
"1": "cmmodular:equipment/space_helmet",
|
||||||
"particle": "cmmodular:equipment/space_helmet"
|
"particle": "cmmodular:equipment/space_helmet"
|
||||||
|
|
@ -8,24 +7,86 @@
|
||||||
"elements": [
|
"elements": [
|
||||||
{
|
{
|
||||||
"name": "dome",
|
"name": "dome",
|
||||||
"from": [-4.25, -4.25, -4.25],
|
"from": [
|
||||||
"to": [4.25, 4.25, 4.25],
|
-4.25,
|
||||||
|
-4.25,
|
||||||
|
-4.25
|
||||||
|
],
|
||||||
|
"to": [
|
||||||
|
4.25,
|
||||||
|
4.25,
|
||||||
|
4.25
|
||||||
|
],
|
||||||
"faces": {
|
"faces": {
|
||||||
"north": {"uv": [8, 8, 16, 16], "texture": "#1"},
|
"north": {
|
||||||
"east": {"uv": [16, 8, 24, 16], "texture": "#1"},
|
"uv": [
|
||||||
"south": {"uv": [24, 8, 32, 16], "texture": "#1"},
|
8.0,
|
||||||
"west": {"uv": [0, 8, 8, 16], "texture": "#1"},
|
8.0,
|
||||||
"up": {"uv": [8, 0, 16, 8], "texture": "#1"},
|
4.0,
|
||||||
"down": {"uv": [16, 0, 24, 8], "texture": "#1"}
|
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": [
|
"groups": [
|
||||||
{
|
{
|
||||||
"name": "Space Helmet",
|
"name": "Space Helmet",
|
||||||
"origin": [0, 0, 0],
|
"origin": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
"color": 0,
|
"color": 0,
|
||||||
"children": [0]
|
"children": [
|
||||||
|
0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
"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": {
|
"textures": {
|
||||||
"1": "cmmodular:equipment/space_helmet_socket",
|
"1": "cmmodular:equipment/space_helmet_socket",
|
||||||
"particle": "cmmodular:equipment/space_helmet_socket"
|
"particle": "cmmodular:equipment/space_helmet_socket"
|
||||||
|
|
@ -24,55 +20,55 @@
|
||||||
"faces": {
|
"faces": {
|
||||||
"north": {
|
"north": {
|
||||||
"uv": [
|
"uv": [
|
||||||
8,
|
8.0,
|
||||||
8,
|
8.0,
|
||||||
16,
|
4.0,
|
||||||
16
|
4.0
|
||||||
],
|
],
|
||||||
"texture": "#1"
|
"texture": "#1"
|
||||||
},
|
},
|
||||||
"east": {
|
"east": {
|
||||||
"uv": [
|
"uv": [
|
||||||
16,
|
0.0,
|
||||||
8,
|
8.0,
|
||||||
24,
|
4.0,
|
||||||
16
|
4.0
|
||||||
],
|
],
|
||||||
"texture": "#1"
|
"texture": "#1"
|
||||||
},
|
},
|
||||||
"south": {
|
"south": {
|
||||||
"uv": [
|
"uv": [
|
||||||
24,
|
16.0,
|
||||||
8,
|
8.0,
|
||||||
32,
|
12.0,
|
||||||
16
|
4.0
|
||||||
],
|
],
|
||||||
"texture": "#1"
|
"texture": "#1"
|
||||||
},
|
},
|
||||||
"west": {
|
"west": {
|
||||||
"uv": [
|
"uv": [
|
||||||
0,
|
8.0,
|
||||||
8,
|
8.0,
|
||||||
8,
|
12.0,
|
||||||
16
|
4.0
|
||||||
],
|
],
|
||||||
"texture": "#1"
|
"texture": "#1"
|
||||||
},
|
},
|
||||||
"up": {
|
"up": {
|
||||||
"uv": [
|
"uv": [
|
||||||
8,
|
12.0,
|
||||||
0,
|
0.0,
|
||||||
16,
|
8.0,
|
||||||
8
|
4.0
|
||||||
],
|
],
|
||||||
"texture": "#1"
|
"texture": "#1"
|
||||||
},
|
},
|
||||||
"down": {
|
"down": {
|
||||||
"uv": [
|
"uv": [
|
||||||
16,
|
8.0,
|
||||||
0,
|
4.0,
|
||||||
24,
|
4.0,
|
||||||
8
|
0.0
|
||||||
],
|
],
|
||||||
"texture": "#1"
|
"texture": "#1"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 141 B After Width: | Height: | Size: 275 B |
|
Before Width: | Height: | Size: 141 B After Width: | Height: | Size: 281 B |
|
Before Width: | Height: | Size: 126 B After Width: | Height: | Size: 264 B |
|
Before Width: | Height: | Size: 126 B After Width: | Height: | Size: 288 B |
|
Before Width: | Height: | Size: 175 B After Width: | Height: | Size: 199 B |
|
Before Width: | Height: | Size: 235 B After Width: | Height: | Size: 291 B |
|
Before Width: | Height: | Size: 217 B After Width: | Height: | Size: 501 B |
|
Before Width: | Height: | Size: 272 B After Width: | Height: | Size: 553 B |
|
Before Width: | Height: | Size: 356 B After Width: | Height: | Size: 636 B |
|
Before Width: | Height: | Size: 219 B After Width: | Height: | Size: 376 B |
|
Before Width: | Height: | Size: 257 B After Width: | Height: | Size: 382 B |
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"type": "directory",
|
||||||
|
"source": "equipment",
|
||||||
|
"prefix": "equipment/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "directory",
|
||||||
|
"source": "items",
|
||||||
|
"prefix": "items/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -4,19 +4,23 @@
|
||||||
{
|
{
|
||||||
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {"x": 0, "y": 0, "z": 0},
|
"rotation": {
|
||||||
"translation": {"x": 0, "y": 0, "z": 0},
|
"x": 0,
|
||||||
"scale": {"x": 1, "y": 1, "z": 1},
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"translation": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"x": 1,
|
||||||
|
"y": 1,
|
||||||
|
"z": 1
|
||||||
|
},
|
||||||
"origin": "left_arm"
|
"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": [
|
"allowed_in_slots": [
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,23 @@
|
||||||
{
|
{
|
||||||
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {"x": 0, "y": 0, "z": 0},
|
"rotation": {
|
||||||
"translation": {"x": 0, "y": 0, "z": 0},
|
"x": 0,
|
||||||
"scale": {"x": 1, "y": 1, "z": 1},
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"translation": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"x": 1,
|
||||||
|
"y": 1,
|
||||||
|
"z": 1
|
||||||
|
},
|
||||||
"origin": "right_arm"
|
"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": [
|
"allowed_in_slots": [
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,23 @@
|
||||||
{
|
{
|
||||||
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {"x": 0, "y": 0, "z": 0},
|
"rotation": {
|
||||||
"translation": {"x": 0, "y": 0, "z": 0},
|
"x": 0,
|
||||||
"scale": {"x": 1, "y": 1, "z": 1},
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"translation": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"x": 1,
|
||||||
|
"y": 1,
|
||||||
|
"z": 1
|
||||||
|
},
|
||||||
"origin": "left_leg"
|
"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": [
|
"allowed_in_slots": [
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,23 @@
|
||||||
{
|
{
|
||||||
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {"x": 0, "y": 0, "z": 0},
|
"rotation": {
|
||||||
"translation": {"x": 0, "y": 0, "z": 0},
|
"x": 0,
|
||||||
"scale": {"x": 1, "y": 1, "z": 1},
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"translation": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"x": 1,
|
||||||
|
"y": 1,
|
||||||
|
"z": 1
|
||||||
|
},
|
||||||
"origin": "right_leg"
|
"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": [
|
"allowed_in_slots": [
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,18 +12,6 @@
|
||||||
"operation": "*",
|
"operation": "*",
|
||||||
"slot": "chest"
|
"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": {
|
"replace": {
|
||||||
|
|
@ -36,9 +24,21 @@
|
||||||
"slotType": "cosmetic",
|
"slotType": "cosmetic",
|
||||||
"translationKey": "miapi.slot.arm_patch.name",
|
"translationKey": "miapi.slot.arm_patch.name",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {"x": 0, "y": 90, "z": 0},
|
"rotation": {
|
||||||
"translation": {"x": -2.15, "y": -1, "z": 0},
|
"x": 0,
|
||||||
"scale": {"x": 1.1, "y": 1, "z": 1},
|
"y": 90,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"translation": {
|
||||||
|
"x": -2.15,
|
||||||
|
"y": -1,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"x": 1.1,
|
||||||
|
"y": 1,
|
||||||
|
"z": 1
|
||||||
|
},
|
||||||
"origin": "left_arm"
|
"origin": "left_arm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -48,9 +48,21 @@
|
||||||
],
|
],
|
||||||
"translationKey": "miapi.slot.gemstone.medium",
|
"translationKey": "miapi.slot.gemstone.medium",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {"x": 90.0, "y": 55.7523, "z": -90.0},
|
"rotation": {
|
||||||
"translation": {"x": -1.7667, "y": 1.5983, "z": 0.0},
|
"x": 90.0,
|
||||||
"scale": {"x": 0.6667, "y": 0.8171, "z": 0.7123},
|
"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"
|
"origin": "left_arm"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,18 +12,6 @@
|
||||||
"operation": "*",
|
"operation": "*",
|
||||||
"slot": "chest"
|
"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": {
|
"replace": {
|
||||||
|
|
@ -36,9 +24,21 @@
|
||||||
"slotType": "cosmetic",
|
"slotType": "cosmetic",
|
||||||
"translationKey": "miapi.slot.arm_patch.name",
|
"translationKey": "miapi.slot.arm_patch.name",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {"x": 0, "y": -90, "z": 0},
|
"rotation": {
|
||||||
"translation": {"x": 2.15, "y": -1, "z": 0},
|
"x": 0,
|
||||||
"scale": {"x": 1.1, "y": 1, "z": 1},
|
"y": -90,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"translation": {
|
||||||
|
"x": 2.15,
|
||||||
|
"y": -1,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"x": 1.1,
|
||||||
|
"y": 1,
|
||||||
|
"z": 1
|
||||||
|
},
|
||||||
"origin": "right_arm"
|
"origin": "right_arm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -48,9 +48,21 @@
|
||||||
],
|
],
|
||||||
"translationKey": "miapi.slot.gemstone.medium",
|
"translationKey": "miapi.slot.gemstone.medium",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {"x": 90.0, "y": -55.7523, "z": 90.0},
|
"rotation": {
|
||||||
"translation": {"x": 1.7667, "y": 1.5983, "z": 0.0},
|
"x": 90.0,
|
||||||
"scale": {"x": 0.6667, "y": 0.8171, "z": 0.7123},
|
"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"
|
"origin": "right_arm"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,18 +12,6 @@
|
||||||
"operation": "*",
|
"operation": "*",
|
||||||
"slot": "legs"
|
"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": {
|
"replace": {
|
||||||
|
|
@ -34,9 +22,21 @@
|
||||||
],
|
],
|
||||||
"translationKey": "miapi.slot.gemstone.medium",
|
"translationKey": "miapi.slot.gemstone.medium",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {"x": 0.0, "y": 0.0, "z": -180.0},
|
"rotation": {
|
||||||
"translation": {"x": 0.0, "y": -1.95, "z": -2.4917},
|
"x": 0.0,
|
||||||
"scale": {"x": 0.8333, "y": 1.0, "z": 0.8333},
|
"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"
|
"origin": "left_leg"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,18 +12,6 @@
|
||||||
"operation": "*",
|
"operation": "*",
|
||||||
"slot": "legs"
|
"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": {
|
"replace": {
|
||||||
|
|
@ -34,9 +22,21 @@
|
||||||
],
|
],
|
||||||
"translationKey": "miapi.slot.gemstone.medium",
|
"translationKey": "miapi.slot.gemstone.medium",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {"x": 0.0, "y": 0.0, "z": -180.0},
|
"rotation": {
|
||||||
"translation": {"x": 0.0, "y": -1.95, "z": -2.4917},
|
"x": 0.0,
|
||||||
"scale": {"x": 0.8333, "y": 1.0, "z": 0.8333},
|
"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"
|
"origin": "right_leg"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,19 +21,42 @@
|
||||||
"path": "cmmodular:models/item/armor/model/helmet/space/[material.texture].json",
|
"path": "cmmodular:models/item/armor/model/helmet/space/[material.texture].json",
|
||||||
"trim_mode": "ARMOR_LAYER_ONE",
|
"trim_mode": "ARMOR_LAYER_ONE",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {"x": 0, "y": 0, "z": 0},
|
"rotation": {
|
||||||
"translation": {"x": 0, "y": 0, "z": 0},
|
"x": 0,
|
||||||
"scale": {"x": 1, "y": 1, "z": 1},
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"translation": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"x": 1,
|
||||||
|
"y": 1,
|
||||||
|
"z": 1
|
||||||
|
},
|
||||||
"origin": "head"
|
"origin": "head"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "cmmodular:models/item/armor/gui/space/helmet/[material.texture].json",
|
"path": "cmmodular:models/item/armor/gui/space/helmet/[material.texture].json",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {"x": 0, "y": 0, "z": 0},
|
"rotation": {
|
||||||
"translation": {"x": 0, "y": 0, "z": 0},
|
"x": 0,
|
||||||
"scale": {"x": 1, "y": 1, "z": 1},
|
"y": 0,
|
||||||
"origin": "item"
|
"z": 0
|
||||||
|
},
|
||||||
|
"translation": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"x": 1,
|
||||||
|
"y": 1,
|
||||||
|
"z": 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -20,19 +20,42 @@
|
||||||
"path": "cmmodular:models/item/armor/model/helmet/space_socket/[material.texture].json",
|
"path": "cmmodular:models/item/armor/model/helmet/space_socket/[material.texture].json",
|
||||||
"trim_mode": "ARMOR_LAYER_ONE",
|
"trim_mode": "ARMOR_LAYER_ONE",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {"x": 0, "y": 0, "z": 0},
|
"rotation": {
|
||||||
"translation": {"x": 0, "y": 4.1387, "z": 0},
|
"x": 0,
|
||||||
"scale": {"x": 1, "y": 1, "z": 1},
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"translation": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"x": 1,
|
||||||
|
"y": 1,
|
||||||
|
"z": 1
|
||||||
|
},
|
||||||
"origin": "head"
|
"origin": "head"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "cmmodular:models/item/armor/gui/space/helmet_socket/[material.texture].json",
|
"path": "cmmodular:models/item/armor/gui/space/helmet_socket/[material.texture].json",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {"x": 0, "y": 0, "z": 0},
|
"rotation": {
|
||||||
"translation": {"x": 0, "y": 0, "z": 0},
|
"x": 0,
|
||||||
"scale": {"x": 1, "y": 1, "z": 1},
|
"y": 0,
|
||||||
"origin": "item"
|
"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",
|
"translationKey": "miapi.slot.hat.name",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {"x": 0, "y": 0, "z": 0},
|
"rotation": {
|
||||||
"translation": {"x": 0, "y": -1, "z": 0},
|
"x": 0,
|
||||||
"scale": {"x": 1.1, "y": 1.1, "z": 1.1},
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"translation": {
|
||||||
|
"x": 0,
|
||||||
|
"y": -1,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"x": 1.1,
|
||||||
|
"y": 1.1,
|
||||||
|
"z": 1.1
|
||||||
|
},
|
||||||
"origin": "head"
|
"origin": "head"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"gem": {
|
"gem": {
|
||||||
"allowed": [
|
"allowed": [
|
||||||
"gem_armor_medium"
|
"gem_armor_medium_helmet"
|
||||||
],
|
],
|
||||||
"translationKey": "miapi.slot.gemstone.medium",
|
"translationKey": "miapi.slot.gemstone.medium",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {"x": 0, "y": 0, "z": 0},
|
"rotation": {
|
||||||
"translation": {"x": 0, "y": 3, "z": 0},
|
"x": 0,
|
||||||
"scale": {"x": 1, "y": 1, "z": 1}
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"translation": {
|
||||||
|
"x": -8.0,
|
||||||
|
"y": -12.06,
|
||||||
|
"z": -12.64
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"x": 1,
|
||||||
|
"y": 1,
|
||||||
|
"z": 1
|
||||||
|
},
|
||||||
|
"origin": "head"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,21 @@
|
||||||
"path": "cmmodular:models/item/armor/model/chest_back/space/[material.texture].json",
|
"path": "cmmodular:models/item/armor/model/chest_back/space/[material.texture].json",
|
||||||
"trim_mode": "ARMOR_LAYER_ONE",
|
"trim_mode": "ARMOR_LAYER_ONE",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {"x": 0, "y": 0, "z": 0},
|
"rotation": {
|
||||||
"translation": {"x": 0, "y": 0, "z": 0},
|
"x": 0,
|
||||||
"scale": {"x": 1, "y": 1, "z": 1},
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"translation": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0.6667
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"x": 0.8,
|
||||||
|
"y": 0.9091,
|
||||||
|
"z": 0.6667
|
||||||
|
},
|
||||||
"origin": "body"
|
"origin": "body"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,21 @@
|
||||||
"path": "cmmodular:models/item/armor/model/chest_back/space_socket/[material.texture].json",
|
"path": "cmmodular:models/item/armor/model/chest_back/space_socket/[material.texture].json",
|
||||||
"trim_mode": "ARMOR_LAYER_ONE",
|
"trim_mode": "ARMOR_LAYER_ONE",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {"x": 0, "y": 0, "z": 0},
|
"rotation": {
|
||||||
"translation": {"x": 0, "y": -2.1901, "z": 0},
|
"x": 0,
|
||||||
"scale": {"x": 1, "y": 1, "z": 1},
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"translation": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0.6667
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"x": 0.8,
|
||||||
|
"y": 0.9091,
|
||||||
|
"z": 0.6667
|
||||||
|
},
|
||||||
"origin": "body"
|
"origin": "body"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -36,21 +48,45 @@
|
||||||
"slotType": "cosmetic",
|
"slotType": "cosmetic",
|
||||||
"translationKey": "miapi.slot.chest_back_center.name",
|
"translationKey": "miapi.slot.chest_back_center.name",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {"x": 180, "y": 0, "z": 180},
|
"rotation": {
|
||||||
"translation": {"x": 0, "y": 1.7, "z": 2.35},
|
"x": 180,
|
||||||
"scale": {"x": 1, "y": 0.775, "z": 1.1},
|
"y": 0,
|
||||||
|
"z": 180
|
||||||
|
},
|
||||||
|
"translation": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 1.7,
|
||||||
|
"z": 2.35
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"x": 1,
|
||||||
|
"y": 0.775,
|
||||||
|
"z": 1.1
|
||||||
|
},
|
||||||
"origin": "body"
|
"origin": "body"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"gem": {
|
"gem": {
|
||||||
"allowed": [
|
"allowed": [
|
||||||
"gem_armor_medium"
|
"gem_armor_medium_backtank"
|
||||||
],
|
],
|
||||||
"translationKey": "miapi.slot.gemstone.medium",
|
"translationKey": "miapi.slot.gemstone.medium",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {"x": 0, "y": 0, "z": 0},
|
"rotation": {
|
||||||
"translation": {"x": 0, "y": -1, "z": 0},
|
"x": 0,
|
||||||
"scale": {"x": 1.1, "y": 1.1, "z": 1.1},
|
"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"
|
"origin": "body"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,23 @@
|
||||||
{
|
{
|
||||||
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {"x": 0, "y": 0, "z": 0},
|
"rotation": {
|
||||||
"translation": {"x": 0, "y": 0, "z": 0},
|
"x": 0,
|
||||||
"scale": {"x": 1, "y": 1, "z": 1},
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"translation": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"x": 1,
|
||||||
|
"y": 1,
|
||||||
|
"z": 1
|
||||||
|
},
|
||||||
"origin": "left_arm"
|
"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": [
|
"allowed_in_slots": [
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,23 @@
|
||||||
{
|
{
|
||||||
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {"x": 0, "y": 0, "z": 0},
|
"rotation": {
|
||||||
"translation": {"x": 0, "y": 0, "z": 0},
|
"x": 0,
|
||||||
"scale": {"x": 1, "y": 1, "z": 1},
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"translation": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"x": 1,
|
||||||
|
"y": 1,
|
||||||
|
"z": 1
|
||||||
|
},
|
||||||
"origin": "right_arm"
|
"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": [
|
"allowed_in_slots": [
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,23 @@
|
||||||
{
|
{
|
||||||
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {"x": 0, "y": 0, "z": 0},
|
"rotation": {
|
||||||
"translation": {"x": 0, "y": 0, "z": 0},
|
"x": 0,
|
||||||
"scale": {"x": 1, "y": 1, "z": 1},
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"translation": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"x": 1,
|
||||||
|
"y": 1,
|
||||||
|
"z": 1
|
||||||
|
},
|
||||||
"origin": "left_leg"
|
"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": [
|
"allowed_in_slots": [
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,23 @@
|
||||||
{
|
{
|
||||||
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {"x": 0, "y": 0, "z": 0},
|
"rotation": {
|
||||||
"translation": {"x": 0, "y": 0, "z": 0},
|
"x": 0,
|
||||||
"scale": {"x": 1, "y": 1, "z": 1},
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"translation": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"x": 1,
|
||||||
|
"y": 1,
|
||||||
|
"z": 1
|
||||||
|
},
|
||||||
"origin": "right_leg"
|
"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": [
|
"allowed_in_slots": [
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
*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
|
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
|
`left_arm` transform above it. A merge multiplies the two matrices and then
|
||||||
1.21; `--lossy-merge` reproduces the Euler round trip older versions did, which
|
decomposes the product back into translation, Euler angles and scale - 2.3.8
|
||||||
drops shear wherever a rotation meets a non-uniform scale.
|
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
|
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.
|
forward, and the origin is the base of the neck.
|
||||||
|
|
@ -82,8 +85,20 @@ class Placement:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def origin(self):
|
def origin(self):
|
||||||
"""The body part this module is drawn under. Absent means the torso."""
|
"""The body part this module is drawn under, or None for none of them.
|
||||||
return self.transform.get('origin', 'body')
|
|
||||||
|
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
|
@property
|
||||||
def auto_chain(self):
|
def auto_chain(self):
|
||||||
|
|
@ -195,6 +210,7 @@ PLATES = (
|
||||||
)
|
)
|
||||||
|
|
||||||
SOCKETS = 'src/main/resources/packs/armory/data/cmmodular/miapi/modules/armor/socket'
|
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'
|
LIMB_GEMS = 'src/main/resources/packs/armory/data/cmmodular/miapi/modules/gem/limb'
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -278,6 +294,74 @@ def scene_armory(res, args):
|
||||||
return out
|
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):
|
def scene_icons(res, args):
|
||||||
"""The four inventory icons, side by side, each with its gem on it.
|
"""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,
|
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
|
# 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
|
# 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.res, self.placements, self.args = res, placements, args
|
||||||
self.force_chain = args.chain
|
self.force_chain = args.chain
|
||||||
self.textured = not args.no_textures
|
self.textured = not args.no_textures
|
||||||
self.lossy = args.lossy_merge
|
self.lossy = not args.exact_merge
|
||||||
self.step = args.step
|
self.step = args.step
|
||||||
self.turn_step = 7.5
|
self.turn_step = 7.5
|
||||||
self.show_body = args.body
|
self.show_body = args.body
|
||||||
|
|
@ -541,7 +625,7 @@ class Viewport:
|
||||||
f' {p.summary()}',
|
f' {p.summary()}',
|
||||||
f' step {self.step} chain {"forced" if self.force_chain else "auto"}'
|
f' step {self.step} chain {"forced" if self.force_chain else "auto"}'
|
||||||
f' textures {"on" if self.textured else "off"}'
|
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,
|
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('--body', action='store_true', help='draw the wearer as a reference')
|
||||||
ap.add_argument('--chain', action='store_true',
|
ap.add_argument('--chain', action='store_true',
|
||||||
help='force the slot chain on even where MIAPI would file it elsewhere')
|
help='force the slot chain on even where MIAPI would file it elsewhere')
|
||||||
ap.add_argument('--lossy-merge', action='store_true',
|
ap.add_argument('--exact-merge', action='store_true',
|
||||||
help='decompose each merge to Euler angles, as MIAPI did before 1.21')
|
help='keep the whole matrix product instead of the Euler round trip'
|
||||||
|
' MIAPI does on every merge')
|
||||||
args = ap.parse_args(argv)
|
args = ap.parse_args(argv)
|
||||||
|
|
||||||
if args.scene is None:
|
if args.scene is None:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Write the armour placement out as a Blockbench project.
|
||||||
|
|
||||||
|
bb_export.py <armory.jar> [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')
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Read a Blockbench project back into the module JSON.
|
||||||
|
|
||||||
|
bb_import.py <armory.jar> <placement.bbmodel> [--write]
|
||||||
|
|
||||||
|
Only cubes tagged `[edit:<file>#<target>]` 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))
|
||||||
|
|
@ -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 [],
|
||||||
|
}
|
||||||
|
|
@ -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, {})
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Push a placement change into the running game.
|
||||||
|
#
|
||||||
|
# live-reload.sh <saved-file>
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
|
@ -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 /<command> 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
|
||||||
|
|
@ -213,25 +213,42 @@ def decompose(m):
|
||||||
'scale': {'x': s[0], 'y': s[1], 'z': s[2]}}
|
'scale': {'x': s[0], 'y': s[1], 'z': s[2]}}
|
||||||
|
|
||||||
|
|
||||||
def merge(parent, child, lossy=False):
|
def merge(parent, child, lossy=True):
|
||||||
"""MIAPI's `Transform.merge`.
|
"""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`
|
Read out of the jar rather than assumed, because the assumption was wrong
|
||||||
and `merge` returns `new Transform(parent.matrix.mul(child.matrix))`, with
|
and cost a set of gem placements. `Transform.merge(a, b)` builds both
|
||||||
no round trip through Euler angles. The parent is the transform already
|
matrices, computes `b.mul(a)` - so the accumulated transform applies first
|
||||||
accumulated and the child the one being added, so the child 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
|
`TransformMap.add` calls it as `merge(what is there, what is being added)`,
|
||||||
back into translation, Euler angles and scale before being stored - which
|
which is what makes a slot transform act *after* the plate's rather than
|
||||||
silently drops the shear that appears the moment a rotation meets a
|
inside it. A number written as an offset in the model's own space lands
|
||||||
non-uniform scale. Armory's limb slots are exactly that, so the two answers
|
somewhere else entirely; `packs/armory/.../socket/*.json` are solved in the
|
||||||
differ there and it is worth being able to see both.
|
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
|
product = child @ parent
|
||||||
return transform_matrix(decompose(product)) if lossy else product
|
return transform_matrix(decompose(product)) if lossy else product
|
||||||
|
|
||||||
|
|
||||||
# `HumanoidModel.createMesh` pivots - the frame each `origin` resolves against.
|
# `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 = {
|
PIVOTS = {
|
||||||
'head': (0.0, 0.0, 0.0),
|
'head': (0.0, 0.0, 0.0),
|
||||||
'hat': (0.0, 0.0, 0.0),
|
'hat': (0.0, 0.0, 0.0),
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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()
|
||||||