Compare commits
No commits in common. "gem-placement" and "main" have entirely different histories.
gem-placem
...
main
54
README.md
|
|
@ -166,29 +166,17 @@ 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.
|
||||||
|
|
||||||
What made them hard is now known, and it was an arithmetic mistake rather than
|
They are not right yet, and two attempts to make them right are worth recording
|
||||||
a matter of taste. A slot transform does not act in the model's own space. MIAPI
|
so that they are not repeated. Naming the limb as the gem slot's `origin` - so
|
||||||
accumulates transforms per origin with `TransformMap.add`, which calls
|
the offset lands in the entry the limb pass reads and the gem follows the arm -
|
||||||
`Transform.merge(what is there, what is being added)`, and that merge computes
|
composes correctly on paper and in game puts the gems several blocks away from
|
||||||
`new.toMatrix().mul(accumulated.toMatrix())` - so the plate's transform applies
|
the wearer, floating. And `tools/preview_armour.py`, which applies the same chain
|
||||||
to the geometry first and the slot's afterwards, in the body part's frame. An
|
MIAPI does and draws the result from any angle, agrees with the paper rather than
|
||||||
offset written as though it were measured on the model lands somewhere else
|
with the game: given the configuration that in practice left gems at the neck, it
|
||||||
entirely, which is why the gems sat at the collar with their sides swapped and
|
puts them on the pauldron. The tool is worth keeping for reading Armory's own
|
||||||
the knee gems ended up at the belly.
|
geometry - where the plate bands are, which face is which, how far the chain
|
||||||
|
scales a limb - and it should not be trusted to predict a gem's final position
|
||||||
So they are solved in that frame instead: take the middle of the outward face of
|
until it can reproduce a symptom somebody has actually seen.
|
||||||
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
|
||||||
|
|
@ -224,19 +212,10 @@ 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. They are all filed under `packs/apotheosis` rather than with
|
weapons already. All three are 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
|
||||||
|
|
@ -407,15 +386,6 @@ 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.
|
||||||
|
|
|
||||||
2211
placement.bbmodel
|
|
@ -46,14 +46,11 @@ 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, and one per socket that
|
* Module ids that are a gem case: one per gem size, one per limb socket.
|
||||||
* names a body part - the four limbs, the space helmet's visor and the
|
* A case is not listed twice for being allowed in a pommel as well as in
|
||||||
* backtank. A case is not listed twice for being allowed in a pommel as
|
* armour - it is one module either way, and this counts modules, so a
|
||||||
* well as in armour - it is one module either way, and this counts modules,
|
* guard holding two small cases is worth two sockets by the same rule
|
||||||
* so a guard holding two small cases is worth two sockets by the same rule
|
* that makes a chestplate holding one worth one.
|
||||||
* 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",
|
||||||
|
|
@ -62,9 +59,7 @@ 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,10 +35,6 @@
|
||||||
"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,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"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"
|
||||||
|
|
@ -7,86 +8,24 @@
|
||||||
"elements": [
|
"elements": [
|
||||||
{
|
{
|
||||||
"name": "tanks",
|
"name": "tanks",
|
||||||
"from": [
|
"from": [-3.5, -6.5, 2],
|
||||||
-3.5,
|
"to": [3.5, 1.5, 5],
|
||||||
-6.5,
|
|
||||||
2
|
|
||||||
],
|
|
||||||
"to": [
|
|
||||||
3.5,
|
|
||||||
1.5,
|
|
||||||
5
|
|
||||||
],
|
|
||||||
"faces": {
|
"faces": {
|
||||||
"north": {
|
"north": {"uv": [8, 8, 16, 16], "texture": "#1"},
|
||||||
"uv": [
|
"east": {"uv": [16, 8, 24, 16], "texture": "#1"},
|
||||||
1.5,
|
"south": {"uv": [24, 8, 32, 16], "texture": "#1"},
|
||||||
3.0,
|
"west": {"uv": [0, 8, 8, 16], "texture": "#1"},
|
||||||
5.0,
|
"up": {"uv": [8, 0, 16, 8], "texture": "#1"},
|
||||||
11.0
|
"down": {"uv": [16, 0, 24, 8], "texture": "#1"}
|
||||||
],
|
|
||||||
"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": [
|
"origin": [0, 0, 0],
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
"color": 0,
|
"color": 0,
|
||||||
"children": [
|
"children": [0]
|
||||||
0
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
{
|
{
|
||||||
"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"
|
||||||
|
|
@ -20,55 +24,55 @@
|
||||||
"faces": {
|
"faces": {
|
||||||
"north": {
|
"north": {
|
||||||
"uv": [
|
"uv": [
|
||||||
1.5,
|
8,
|
||||||
3.0,
|
8,
|
||||||
5.0,
|
16,
|
||||||
11.0
|
16
|
||||||
],
|
],
|
||||||
"texture": "#1"
|
"texture": "#1"
|
||||||
},
|
},
|
||||||
"east": {
|
"east": {
|
||||||
"uv": [
|
"uv": [
|
||||||
5.0,
|
16,
|
||||||
3.0,
|
8,
|
||||||
6.5,
|
24,
|
||||||
11.0
|
16
|
||||||
],
|
],
|
||||||
"texture": "#1"
|
"texture": "#1"
|
||||||
},
|
},
|
||||||
"south": {
|
"south": {
|
||||||
"uv": [
|
"uv": [
|
||||||
6.5,
|
24,
|
||||||
3.0,
|
8,
|
||||||
10.0,
|
32,
|
||||||
11.0
|
16
|
||||||
],
|
],
|
||||||
"texture": "#1"
|
"texture": "#1"
|
||||||
},
|
},
|
||||||
"west": {
|
"west": {
|
||||||
"uv": [
|
"uv": [
|
||||||
0.0,
|
0,
|
||||||
3.0,
|
8,
|
||||||
1.5,
|
8,
|
||||||
11.0
|
16
|
||||||
],
|
],
|
||||||
"texture": "#1"
|
"texture": "#1"
|
||||||
},
|
},
|
||||||
"up": {
|
"up": {
|
||||||
"uv": [
|
"uv": [
|
||||||
1.5,
|
8,
|
||||||
0.0,
|
0,
|
||||||
5.0,
|
16,
|
||||||
3.0
|
8
|
||||||
],
|
],
|
||||||
"texture": "#1"
|
"texture": "#1"
|
||||||
},
|
},
|
||||||
"down": {
|
"down": {
|
||||||
"uv": [
|
"uv": [
|
||||||
5.0,
|
16,
|
||||||
0.0,
|
0,
|
||||||
8.5,
|
24,
|
||||||
3.0
|
8
|
||||||
],
|
],
|
||||||
"texture": "#1"
|
"texture": "#1"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"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"
|
||||||
|
|
@ -7,86 +8,24 @@
|
||||||
"elements": [
|
"elements": [
|
||||||
{
|
{
|
||||||
"name": "dome",
|
"name": "dome",
|
||||||
"from": [
|
"from": [-4.25, -4.25, -4.25],
|
||||||
-4.25,
|
"to": [4.25, 4.25, 4.25],
|
||||||
-4.25,
|
|
||||||
-4.25
|
|
||||||
],
|
|
||||||
"to": [
|
|
||||||
4.25,
|
|
||||||
4.25,
|
|
||||||
4.25
|
|
||||||
],
|
|
||||||
"faces": {
|
"faces": {
|
||||||
"north": {
|
"north": {"uv": [8, 8, 16, 16], "texture": "#1"},
|
||||||
"uv": [
|
"east": {"uv": [16, 8, 24, 16], "texture": "#1"},
|
||||||
8.0,
|
"south": {"uv": [24, 8, 32, 16], "texture": "#1"},
|
||||||
8.0,
|
"west": {"uv": [0, 8, 8, 16], "texture": "#1"},
|
||||||
4.0,
|
"up": {"uv": [8, 0, 16, 8], "texture": "#1"},
|
||||||
4.0
|
"down": {"uv": [16, 0, 24, 8], "texture": "#1"}
|
||||||
],
|
|
||||||
"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": [
|
"origin": [0, 0, 0],
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
"color": 0,
|
"color": 0,
|
||||||
"children": [
|
"children": [0]
|
||||||
0
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
{
|
{
|
||||||
"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"
|
||||||
|
|
@ -20,55 +24,55 @@
|
||||||
"faces": {
|
"faces": {
|
||||||
"north": {
|
"north": {
|
||||||
"uv": [
|
"uv": [
|
||||||
8.0,
|
8,
|
||||||
8.0,
|
8,
|
||||||
4.0,
|
16,
|
||||||
4.0
|
16
|
||||||
],
|
],
|
||||||
"texture": "#1"
|
"texture": "#1"
|
||||||
},
|
},
|
||||||
"east": {
|
"east": {
|
||||||
"uv": [
|
"uv": [
|
||||||
0.0,
|
16,
|
||||||
8.0,
|
8,
|
||||||
4.0,
|
24,
|
||||||
4.0
|
16
|
||||||
],
|
],
|
||||||
"texture": "#1"
|
"texture": "#1"
|
||||||
},
|
},
|
||||||
"south": {
|
"south": {
|
||||||
"uv": [
|
"uv": [
|
||||||
16.0,
|
24,
|
||||||
8.0,
|
8,
|
||||||
12.0,
|
32,
|
||||||
4.0
|
16
|
||||||
],
|
],
|
||||||
"texture": "#1"
|
"texture": "#1"
|
||||||
},
|
},
|
||||||
"west": {
|
"west": {
|
||||||
"uv": [
|
"uv": [
|
||||||
8.0,
|
0,
|
||||||
8.0,
|
8,
|
||||||
12.0,
|
8,
|
||||||
4.0
|
16
|
||||||
],
|
],
|
||||||
"texture": "#1"
|
"texture": "#1"
|
||||||
},
|
},
|
||||||
"up": {
|
"up": {
|
||||||
"uv": [
|
"uv": [
|
||||||
12.0,
|
8,
|
||||||
0.0,
|
0,
|
||||||
8.0,
|
16,
|
||||||
4.0
|
8
|
||||||
],
|
],
|
||||||
"texture": "#1"
|
"texture": "#1"
|
||||||
},
|
},
|
||||||
"down": {
|
"down": {
|
||||||
"uv": [
|
"uv": [
|
||||||
8.0,
|
16,
|
||||||
4.0,
|
0,
|
||||||
4.0,
|
24,
|
||||||
0.0
|
8
|
||||||
],
|
],
|
||||||
"texture": "#1"
|
"texture": "#1"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 275 B After Width: | Height: | Size: 141 B |
|
Before Width: | Height: | Size: 281 B After Width: | Height: | Size: 141 B |
|
Before Width: | Height: | Size: 264 B After Width: | Height: | Size: 126 B |
|
Before Width: | Height: | Size: 288 B After Width: | Height: | Size: 126 B |
|
Before Width: | Height: | Size: 199 B After Width: | Height: | Size: 175 B |
|
Before Width: | Height: | Size: 291 B After Width: | Height: | Size: 235 B |
|
Before Width: | Height: | Size: 501 B After Width: | Height: | Size: 217 B |
|
Before Width: | Height: | Size: 553 B After Width: | Height: | Size: 272 B |
|
Before Width: | Height: | Size: 636 B After Width: | Height: | Size: 356 B |
|
Before Width: | Height: | Size: 376 B After Width: | Height: | Size: 219 B |
|
Before Width: | Height: | Size: 382 B After Width: | Height: | Size: 257 B |
|
|
@ -1,14 +0,0 @@
|
||||||
{
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"type": "directory",
|
|
||||||
"source": "equipment",
|
|
||||||
"prefix": "equipment/"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "directory",
|
|
||||||
"source": "items",
|
|
||||||
"prefix": "items/"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -4,23 +4,19 @@
|
||||||
{
|
{
|
||||||
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {
|
"rotation": {"x": 0, "y": 0, "z": 0},
|
||||||
"x": 0,
|
"translation": {"x": 0, "y": 0, "z": 0},
|
||||||
"y": 0,
|
"scale": {"x": 1, "y": 1, "z": 1},
|
||||||
"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,23 +4,19 @@
|
||||||
{
|
{
|
||||||
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {
|
"rotation": {"x": 0, "y": 0, "z": 0},
|
||||||
"x": 0,
|
"translation": {"x": 0, "y": 0, "z": 0},
|
||||||
"y": 0,
|
"scale": {"x": 1, "y": 1, "z": 1},
|
||||||
"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,23 +4,19 @@
|
||||||
{
|
{
|
||||||
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {
|
"rotation": {"x": 0, "y": 0, "z": 0},
|
||||||
"x": 0,
|
"translation": {"x": 0, "y": 0, "z": 0},
|
||||||
"y": 0,
|
"scale": {"x": 1, "y": 1, "z": 1},
|
||||||
"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,23 +4,19 @@
|
||||||
{
|
{
|
||||||
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {
|
"rotation": {"x": 0, "y": 0, "z": 0},
|
||||||
"x": 0,
|
"translation": {"x": 0, "y": 0, "z": 0},
|
||||||
"y": 0,
|
"scale": {"x": 1, "y": 1, "z": 1},
|
||||||
"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": [
|
||||||
|
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
{
|
|
||||||
"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,6 +12,18 @@
|
||||||
"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": {
|
||||||
|
|
@ -24,21 +36,9 @@
|
||||||
"slotType": "cosmetic",
|
"slotType": "cosmetic",
|
||||||
"translationKey": "miapi.slot.arm_patch.name",
|
"translationKey": "miapi.slot.arm_patch.name",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {
|
"rotation": {"x": 0, "y": 90, "z": 0},
|
||||||
"x": 0,
|
"translation": {"x": -2.15, "y": -1, "z": 0},
|
||||||
"y": 90,
|
"scale": {"x": 1.1, "y": 1, "z": 1},
|
||||||
"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,21 +48,9 @@
|
||||||
],
|
],
|
||||||
"translationKey": "miapi.slot.gemstone.medium",
|
"translationKey": "miapi.slot.gemstone.medium",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {
|
"rotation": {"x": 90.0, "y": 55.7523, "z": -90.0},
|
||||||
"x": 90.0,
|
"translation": {"x": -1.7667, "y": 1.5983, "z": 0.0},
|
||||||
"y": -55.7523,
|
"scale": {"x": 0.6667, "y": 0.8171, "z": 0.7123},
|
||||||
"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,6 +12,18 @@
|
||||||
"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": {
|
||||||
|
|
@ -24,21 +36,9 @@
|
||||||
"slotType": "cosmetic",
|
"slotType": "cosmetic",
|
||||||
"translationKey": "miapi.slot.arm_patch.name",
|
"translationKey": "miapi.slot.arm_patch.name",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {
|
"rotation": {"x": 0, "y": -90, "z": 0},
|
||||||
"x": 0,
|
"translation": {"x": 2.15, "y": -1, "z": 0},
|
||||||
"y": -90,
|
"scale": {"x": 1.1, "y": 1, "z": 1},
|
||||||
"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,21 +48,9 @@
|
||||||
],
|
],
|
||||||
"translationKey": "miapi.slot.gemstone.medium",
|
"translationKey": "miapi.slot.gemstone.medium",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {
|
"rotation": {"x": 90.0, "y": -55.7523, "z": 90.0},
|
||||||
"x": 90.0,
|
"translation": {"x": 1.7667, "y": 1.5983, "z": 0.0},
|
||||||
"y": 55.7523,
|
"scale": {"x": 0.6667, "y": 0.8171, "z": 0.7123},
|
||||||
"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,6 +12,18 @@
|
||||||
"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": {
|
||||||
|
|
@ -22,21 +34,9 @@
|
||||||
],
|
],
|
||||||
"translationKey": "miapi.slot.gemstone.medium",
|
"translationKey": "miapi.slot.gemstone.medium",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {
|
"rotation": {"x": 0.0, "y": 0.0, "z": -180.0},
|
||||||
"x": 0.0,
|
"translation": {"x": 0.0, "y": -1.95, "z": -2.4917},
|
||||||
"y": 0.0,
|
"scale": {"x": 0.8333, "y": 1.0, "z": 0.8333},
|
||||||
"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,6 +12,18 @@
|
||||||
"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": {
|
||||||
|
|
@ -22,21 +34,9 @@
|
||||||
],
|
],
|
||||||
"translationKey": "miapi.slot.gemstone.medium",
|
"translationKey": "miapi.slot.gemstone.medium",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {
|
"rotation": {"x": 0.0, "y": 0.0, "z": -180.0},
|
||||||
"x": 0.0,
|
"translation": {"x": 0.0, "y": -1.95, "z": -2.4917},
|
||||||
"y": 0.0,
|
"scale": {"x": 0.8333, "y": 1.0, "z": 0.8333},
|
||||||
"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,42 +21,19 @@
|
||||||
"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": {
|
"rotation": {"x": 0, "y": 0, "z": 0},
|
||||||
"x": 0,
|
"translation": {"x": 0, "y": 0, "z": 0},
|
||||||
"y": 0,
|
"scale": {"x": 1, "y": 1, "z": 1},
|
||||||
"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": {
|
"rotation": {"x": 0, "y": 0, "z": 0},
|
||||||
"x": 0,
|
"translation": {"x": 0, "y": 0, "z": 0},
|
||||||
"y": 0,
|
"scale": {"x": 1, "y": 1, "z": 1},
|
||||||
"z": 0
|
"origin": "item"
|
||||||
},
|
|
||||||
"translation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"scale": {
|
|
||||||
"x": 1,
|
|
||||||
"y": 1,
|
|
||||||
"z": 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -20,42 +20,19 @@
|
||||||
"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": {
|
"rotation": {"x": 0, "y": 0, "z": 0},
|
||||||
"x": 0,
|
"translation": {"x": 0, "y": 4.1387, "z": 0},
|
||||||
"y": 0,
|
"scale": {"x": 1, "y": 1, "z": 1},
|
||||||
"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": {
|
"rotation": {"x": 0, "y": 0, "z": 0},
|
||||||
"x": 0,
|
"translation": {"x": 0, "y": 0, "z": 0},
|
||||||
"y": 0,
|
"scale": {"x": 1, "y": 1, "z": 1},
|
||||||
"z": 0
|
"origin": "item"
|
||||||
},
|
|
||||||
"translation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"scale": {
|
|
||||||
"x": 1,
|
|
||||||
"y": 1,
|
|
||||||
"z": 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
@ -67,46 +44,21 @@
|
||||||
],
|
],
|
||||||
"translationKey": "miapi.slot.hat.name",
|
"translationKey": "miapi.slot.hat.name",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {
|
"rotation": {"x": 0, "y": 0, "z": 0},
|
||||||
"x": 0,
|
"translation": {"x": 0, "y": -1, "z": 0},
|
||||||
"y": 0,
|
"scale": {"x": 1.1, "y": 1.1, "z": 1.1},
|
||||||
"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_helmet"
|
"gem_armor_medium"
|
||||||
],
|
],
|
||||||
"translationKey": "miapi.slot.gemstone.medium",
|
"translationKey": "miapi.slot.gemstone.medium",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {
|
"rotation": {"x": 0, "y": 0, "z": 0},
|
||||||
"x": 0,
|
"translation": {"x": 0, "y": 3, "z": 0},
|
||||||
"y": 0,
|
"scale": {"x": 1, "y": 1, "z": 1}
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"translation": {
|
|
||||||
"x": -8.0,
|
|
||||||
"y": -12.06,
|
|
||||||
"z": -12.64
|
|
||||||
},
|
|
||||||
"scale": {
|
|
||||||
"x": 1,
|
|
||||||
"y": 1,
|
|
||||||
"z": 1
|
|
||||||
},
|
|
||||||
"origin": "head"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -21,21 +21,9 @@
|
||||||
"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": {
|
"rotation": {"x": 0, "y": 0, "z": 0},
|
||||||
"x": 0,
|
"translation": {"x": 0, "y": 0, "z": 0},
|
||||||
"y": 0,
|
"scale": {"x": 1, "y": 1, "z": 1},
|
||||||
"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,21 +20,9 @@
|
||||||
"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": {
|
"rotation": {"x": 0, "y": 0, "z": 0},
|
||||||
"x": 0,
|
"translation": {"x": 0, "y": -2.1901, "z": 0},
|
||||||
"y": 0,
|
"scale": {"x": 1, "y": 1, "z": 1},
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"translation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0.6667
|
|
||||||
},
|
|
||||||
"scale": {
|
|
||||||
"x": 0.8,
|
|
||||||
"y": 0.9091,
|
|
||||||
"z": 0.6667
|
|
||||||
},
|
|
||||||
"origin": "body"
|
"origin": "body"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -48,45 +36,21 @@
|
||||||
"slotType": "cosmetic",
|
"slotType": "cosmetic",
|
||||||
"translationKey": "miapi.slot.chest_back_center.name",
|
"translationKey": "miapi.slot.chest_back_center.name",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {
|
"rotation": {"x": 180, "y": 0, "z": 180},
|
||||||
"x": 180,
|
"translation": {"x": 0, "y": 1.7, "z": 2.35},
|
||||||
"y": 0,
|
"scale": {"x": 1, "y": 0.775, "z": 1.1},
|
||||||
"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_backtank"
|
"gem_armor_medium"
|
||||||
],
|
],
|
||||||
"translationKey": "miapi.slot.gemstone.medium",
|
"translationKey": "miapi.slot.gemstone.medium",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {
|
"rotation": {"x": 0, "y": 0, "z": 0},
|
||||||
"x": 0,
|
"translation": {"x": 0, "y": -1, "z": 0},
|
||||||
"y": 0,
|
"scale": {"x": 1.1, "y": 1.1, "z": 1.1},
|
||||||
"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,23 +4,19 @@
|
||||||
{
|
{
|
||||||
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {
|
"rotation": {"x": 0, "y": 0, "z": 0},
|
||||||
"x": 0,
|
"translation": {"x": 0, "y": 0, "z": 0},
|
||||||
"y": 0,
|
"scale": {"x": 1, "y": 1, "z": 1},
|
||||||
"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,23 +4,19 @@
|
||||||
{
|
{
|
||||||
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {
|
"rotation": {"x": 0, "y": 0, "z": 0},
|
||||||
"x": 0,
|
"translation": {"x": 0, "y": 0, "z": 0},
|
||||||
"y": 0,
|
"scale": {"x": 1, "y": 1, "z": 1},
|
||||||
"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,23 +4,19 @@
|
||||||
{
|
{
|
||||||
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {
|
"rotation": {"x": 0, "y": 0, "z": 0},
|
||||||
"x": 0,
|
"translation": {"x": 0, "y": 0, "z": 0},
|
||||||
"y": 0,
|
"scale": {"x": 1, "y": 1, "z": 1},
|
||||||
"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,23 +4,19 @@
|
||||||
{
|
{
|
||||||
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
"path": "miapi:models/item/armor/gems/medium/[material.texture].json",
|
||||||
"transform": {
|
"transform": {
|
||||||
"rotation": {
|
"rotation": {"x": 0, "y": 0, "z": 0},
|
||||||
"x": 0,
|
"translation": {"x": 0, "y": 0, "z": 0},
|
||||||
"y": 0,
|
"scale": {"x": 1, "y": 1, "z": 1},
|
||||||
"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": [
|
||||||
|
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
{
|
|
||||||
"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,12 +22,9 @@ 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. A merge multiplies the two matrices and then
|
`left_arm` transform above it. Merging itself is an exact matrix multiply in
|
||||||
decomposes the product back into translation, Euler angles and scale - 2.3.8
|
1.21; `--lossy-merge` reproduces the Euler round trip older versions did, which
|
||||||
does that on every merge, not only the old versions, so shear is dropped
|
drops shear wherever a rotation meets a non-uniform scale.
|
||||||
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.
|
||||||
|
|
@ -85,20 +82,8 @@ class Placement:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def origin(self):
|
def origin(self):
|
||||||
"""The body part this module is drawn under, or None for none of them.
|
"""The body part this module is drawn under. Absent means the torso."""
|
||||||
|
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):
|
||||||
|
|
@ -210,7 +195,6 @@ 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'
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -294,74 +278,6 @@ 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.
|
||||||
|
|
||||||
|
|
@ -403,7 +319,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, 'space': scene_space}
|
'armory': scene_armory, 'icons': scene_icons}
|
||||||
|
|
||||||
# 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
|
||||||
|
|
@ -527,7 +443,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 = not args.exact_merge
|
self.lossy = args.lossy_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
|
||||||
|
|
@ -625,7 +541,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 {"as MIAPI does" if self.lossy else "exact, shear kept"}',
|
f' merge {"lossy (1.20)" if self.lossy else "exact (1.21)"}',
|
||||||
'',
|
'',
|
||||||
HELP,
|
HELP,
|
||||||
]
|
]
|
||||||
|
|
@ -1020,9 +936,8 @@ 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('--exact-merge', action='store_true',
|
ap.add_argument('--lossy-merge', action='store_true',
|
||||||
help='keep the whole matrix product instead of the Euler round trip'
|
help='decompose each merge to Euler angles, as MIAPI did before 1.21')
|
||||||
' 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:
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
#!/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
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
#!/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"
|
|
||||||
|
|
@ -1,168 +0,0 @@
|
||||||
#!/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')
|
|
||||||
|
|
@ -1,146 +0,0 @@
|
||||||
#!/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))
|
|
||||||
|
|
@ -1,263 +0,0 @@
|
||||||
"""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 [],
|
|
||||||
}
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
-- 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, {})
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
#!/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
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
#!/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,42 +213,25 @@ 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=True):
|
def merge(parent, child, lossy=False):
|
||||||
"""MIAPI's `Transform.merge`, as MIAPI 2.3.8 actually has it.
|
"""MIAPI's `Transform.merge`.
|
||||||
|
|
||||||
Read out of the jar rather than assumed, because the assumption was wrong
|
In 1.21 this is a plain matrix multiply - `Transform` holds a `Matrix4f`
|
||||||
and cost a set of gem placements. `Transform.merge(a, b)` builds both
|
and `merge` returns `new Transform(parent.matrix.mul(child.matrix))`, with
|
||||||
matrices, computes `b.mul(a)` - so the accumulated transform applies first
|
no round trip through Euler angles. The parent is the transform already
|
||||||
and the new one after it, in the frame the accumulation left off in - and
|
accumulated and the child the one being added, so the child applies first.
|
||||||
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.
|
|
||||||
|
|
||||||
`TransformMap.add` calls it as `merge(what is there, what is being added)`,
|
`lossy` reproduces the older behaviour, where the product was decomposed
|
||||||
which is what makes a slot transform act *after* the plate's rather than
|
back into translation, Euler angles and scale before being stored - which
|
||||||
inside it. A number written as an offset in the model's own space lands
|
silently drops the shear that appears the moment a rotation meets a
|
||||||
somewhere else entirely; `packs/armory/.../socket/*.json` are solved in the
|
non-uniform scale. Armory's limb slots are exactly that, so the two answers
|
||||||
frame this describes.
|
differ there and it is worth being able to see both.
|
||||||
|
|
||||||
`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),
|
||||||
|
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
#!/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"
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
#!/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()
|
|
||||||