create-mekanism-modular/README.md

654 lines
38 KiB
Markdown

# Create/Mekanism Modular
A [Truly Modular](https://modrinth.com/mod/modular-item-api) addon for Minecraft
1.21.1 / NeoForge. It adds 128 materials drawn from 22 mods - Create, Mekanism, Ice
and Fire, Immersive Engineering, Ender IO, Applied Energistics, Occultism,
Ars Nouveau, Iron's Spells, Pastel, Crystal Chronicles, NauTec, Superb Warfare,
Industrial Foregoing, RFTools, Create Deco, Steampunk Dimension and the Create
aerospace addons - so their metals, gems, scales and essences can all be
used to build modular tools and armour.
On top of the materials it adds a few modules of its own: a khopesh blade for
Arsenal, a sealed helmet and life support pack for Armory that between them
let you breathe in the vacuum Create: Cosmonautics puts above the atmosphere,
socketed arms and legs for the armour pieces Armory leaves without a gem slot,
and a gem case that turns one of Armory's gem slots into an Apotheosis one.
It adds one item as well, the arcana core, which is an endgame gem that ends
every limit Iron's Spells puts on a caster.
Every source mod is optional. Install any subset - the materials for mods you do
not have are never registered.
## How it is put together
Almost all of this mod is data: MIAPI material JSON under
`src/main/resources/packs/<mod>/data/cmmodular/miapi/materials/`.
The small amount of Java exists for the handful of things data cannot do.
**Conditional loading.** MIAPI reads materials with its own loader, which does
not honour `neoforge:conditions` or anything else that could switch a file off,
and a material whose ingredient item is missing fails to parse. So each source
mod's materials are a separate built-in datapack, registered in
`CMModular.java` only when that mod is present.
**Identical materials are one material.** Several metals and gems come from
more than one mod - silver from Ice and Fire, Immersive Engineering, Occultism
and Superb Warfare, steel from Mekanism, Immersive Engineering and Superb
Warfare, and so on. There is one entry per metal, and every provider's version
feeds it: alongside the primary provider's own items each material also accepts
the `c:` common tag that all of them register into. Any mod's silver ingot is
just silver.
Nothing records where a stack came from, because there is nothing to record -
MIAPI stores only the material id on the item, so a tool built from Immersive
Engineering silver is indistinguishable from one built with Ice and Fire silver.
The primary provider's item is listed first, so anywhere a single representative
item is read back out - repair and deconstruct previews - it resolves to the
default rather than to whichever variant was used. Primaries are Mekanism for
steel/lead/uranium, Ice and Fire for silver, Cosmonautics for titanium; each is
a one-line change in `tools/materials.py`.
The material still lives in its primary provider's pack, so its icon and its
first ingredient always point at an item that exists. Create: The Air War is the
one provider that ships no common tags, so `data/c/` adds its titanium to
`c:ingots/titanium` with optional entries.
**Fire immunity.** Cinder essence grants immunity to fire, which is not an
attribute the game has and not a property MIAPI offers. Instead of coupling to
MIAPI's internals, `FireImmunity.java` registers a `cmmodular:fire_immunity`
attribute, the material grants it like any other attribute, and a damage handler
watches for it. Neither side knows about the other.
**Breathing in a vacuum.** The same trick, for the space set - see below.
**Breathing underwater.** And again, for sea serpent fang armour:
`WaterBreathing.java` answers the same `LivingBreatheEvent`, but only when the
wearer's eyes are in water, so a fang is a fang rather than half a spacesuit.
Half a lung per module, the same trade the space set makes, so it takes two
worn fang modules and a fang weapon does nothing at all - see below.
**Fighting underwater.** The other half of what a fang is for.
`WaterCombat.java` registers a `cmmodular:water_combat` attribute that a fang
grants in the main hand, and while the bearer is in water it adds that fraction
to the damage they deal and to how fast they swing. Damage is answered on the
blow. Attack speed cannot be - the game reads it off the attribute map whenever
it wants to know - so a transient modifier under one id goes on and comes off as
the bearer enters and leaves the water. Mining needs none of this: vanilla
already models underwater digging as an attribute,
`minecraft:player.submerged_mining_speed`, and the material simply adds to it.
**Moving through sand.** Deathworm chitin does what the worm does, and this is
the one thing an attribute and an event cannot carry on their own: whether a
block collides is asked of the block, with the entity along for the ride in the
collision context and nothing in between. So there is one mixin,
`BlockStateBaseMixin`, and it is two lines - `SandPhasing.java` owns the
decision. A block in `cmmodular:sand_like` returns no collision to a bearer of
`cmmodular:sand_phasing`, and sand you are standing inside of cannot suffocate
you.
It does not drop you through the desert. A block wholly below the feet still
holds the bearer up, so sand stays something to walk on and a dune becomes
something to walk into: step in and you keep your level, jump and the sand under
you turns solid and puts you back on top. Without that rule the first sand block
you stood on would swallow you down to the sandstone and leave you there, which
is a trap rather than an ability.
**Attributes that go missing.** Modifiers granted by worn and held items are
transient - vanilla reads them when equipment changes and the entity's attribute
map is the only place they live. Go through a portal and they can be gone, with
no equipment change to follow, so nothing puts them back until the piece is
taken off and put on again; cinder essence's fire immunity is the one that gets
noticed, because the Nether is exactly where it was wanted.
`EquipmentAttributes.java` rebuilds the map from the equipment after a dimension
change or a respawn, which is what re-equipping would have done and which fixes
every attribute rather than only this mod's, and reads this mod's own attributes
back off the equipment as well, so the answer is the same whether or not the map
ever lost them.
**Counting gem cases.** `ApothicSockets.java` makes each Apothic gem case worth
an Apotheosis socket, which data cannot do at all - every route through MIAPI's
component properties is either dead or crashes the crafting screen, see below.
It is the one place this mod reaches into another's internals, and it does it
reflectively so that neither Apotheosis nor Truly Modular becomes something to
build against.
**One item.** The arcana core, which is the only thing here that is not a
material or a module - see below.
## Modules
Four of the packs hold modules rather than materials. They are hand-written
rather than generated, and gated on the Truly Modular addon whose slots they
use - and, for the gem case, on Apotheosis as well.
**Khopesh blade** (`packs/arsenal`) fits any `sword_blade` slot, so it is an
option wherever the plain sword blade is. It is the chopping sword it looks
like: axe damage off hardness *and* density, a full point of knockback, an axe's
mining level so it fells trees and strips logs, and a fast swing. The cost is
everything a hook gives up - shorter reach than a sword, no sweep, and a quarter
less durability than the blade it replaces.
There was no khopesh to borrow. Nothing in the Truly Modular ecosystem ships
one, and the khopesh art that exists elsewhere is either another mod's item or a
retexture of a vanilla sword - neither is a MIAPI module and neither is ours to
ship, so the blade is drawn here.
**Socketed pommels** (`packs/arsenal`) give the bigger gems somewhere to sit.
Arsenal does ship a socket pommel, but it takes a small gem, and a sweep of
Arsenal, Armory, Archery and MIAPI turns up no module anywhere that offers a
`gem_melee_large` slot - the large gemstone is defined and then has nowhere to
go. So there are two here: a medium socket, and a great socket for the large
gem. Each is a heavier counterweight than the last, paid for in swing speed.
Either socket takes an Apothic gem case as readily as a gemstone - see below.
**Socketed arms and legs** (`packs/armory`) finish a set Armory started.
Armory sockets four pieces - helmet, chestplate, backplate and belt - and stops
there, so an arm or a leg is a piece of armour that can never hold a gem. These
four are the missing ones: the heavy pauldron and the heavy pants, left and
right, each with the same `gem_armor_medium` slot the rest of the set uses and
the same price, 5% of the piece's armour.
They inherit Armory's heavy pieces whole and change two things: the slot, and
where the gem sits in it.
Where the gem sits is the unfinished part of these four, and the honest summary
is that it is placed by trial rather than by arithmetic. MIAPI draws a worn piece
once per body part; which part a module is drawn under comes from its own model's
`origin`, and where it sits comes from the matching entry of its slot's transform
stack. Armory's gemstone declares no origin, so a gem is drawn once, in the
`body` pass, from the `body` entry - which means a gem slot is placed in body
space no matter which limb it is cut into, and is pinned to the torso rather than
to the limb it belongs to.
The numbers are body-space pixels from the neck: +x to the wearer's left, +y
down, -z forward. A pauldron gem is turned a quarter turn about y to face out of
the shoulder; a knee gem faces forward already. Right-hand pieces are the same
numbers with x and the turn negated.
What made them hard is now known, and it was an arithmetic mistake rather than
a matter of taste. A slot transform does not act in the model's own space. MIAPI
accumulates transforms per origin with `TransformMap.add`, which calls
`Transform.merge(what is there, what is being added)`, and that merge computes
`new.toMatrix().mul(accumulated.toMatrix())` - so the plate's transform applies
to the geometry first and the slot's afterwards, in the body part's frame. An
offset written as though it were measured on the model lands somewhere else
entirely, which is why the gems sat at the collar with their sides swapped and
the knee gems ended up at the belly.
So they are solved in that frame instead: take the middle of the outward face of
the socket this repo draws, carry it through Armory's own plate transform into
the limb's frame, turn the gem's sprite onto that face, and undo the plate's
non-uniform stretch so the stone comes out the size it was drawn. The numbers in
`packs/armory/.../socket/*.json` are that solution, and each one is checked by
composing it the way MIAPI does - including the `fromMatrix` round trip, which
2.3.8 performs on *every* merge and which drops shear wherever a rotation meets
a non-uniform scale. Left and right are exact mirrors of one another, and each
gem lands on its socket.
`tools/preview_armour.py` and `ARMOUR_EDITOR.py` read Armory's own geometry for
the same purpose - where the plate bands are, which face is which, how far the
chain scales a limb.
One thing does not survive any of this and is not fixable from here. A slot
transform that names an origin has nothing left for the inventory icon, which
reads a different entry of the same stack, so the gem sits at the centre of the
icon - the collar of a chestplate, which is where Armory leaves its own, and the
crotch of a pair of pants, which is where this one leaves ours. The two
placements come from one transform and cannot both be aimed.
The pieces take any medium armour gemstone, which includes the Apothic gem case
below - so a pauldron can hold an Apotheosis gem as readily as a Truly Modular
one.
**The Apothic gem case** (`packs/apotheosis`) trades a Truly Modular gem slot
for an Apotheosis one. Apotheosis gems are not modules and never can be - what
makes one worth having is its own rarity and purity data, which a MIAPI
material cannot carry - so the two systems cannot share a slot. What they can
share is the item. Apotheosis stores how many gems a thing holds in a plain
data component, `apotheosis:sockets`, and MIAPI can set any component a module
likes: put a case in an armour piece's `gem_armor_medium` slot instead of a
gemstone, and the piece reports an Apotheosis socket. The gem itself goes in at
a smithing table, through Apotheosis' own socketing recipe, not at the workbench.
A weapon takes one too, and there is a case per gem size to say so. A slot
accepts what its `allowed` list names: the socketed pommels above ask for
`gem_melee_medium` and `gem_melee_large`, and the baseline sockets - Arsenal's
own socket pommel, and the dual socket guard that carries two of them - ask for
`gem_melee_small`. So the case is allowed in the medium slots, a **great case**
answers for the large, and a **small case** for the small, which is the same
trade in a sword that it is in a pauldron. The small one names the armour size
key as well, because a small slot is a small slot wherever it was cut and a key
nothing asks for costs nothing.
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
a guard holding two small cases reports two; Apotheosis sockets gems into
weapons already. They are all filed under `packs/apotheosis` rather than with
the pommels, because a case without Apotheosis is an item that grants nothing,
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
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
between them. The property that could resolve a count instead of a constant,
`advanced_components`, is written but never registered in MIAPI 2.3.8 -
`PropertyRegistry` wires up `components` and `material_component_property` and
never mentions it - so that key resolves to nothing. And the one that does work
turned out to be worse than useless: `ComponentProperty.preview` dereferences the
workbench without a null check, MIAPI passes it null while hovering an option in
the replace list, and `shouldExecuteOnCraft` only calls it for a module that
carries the property - so a gem case with a `components` line crashed the
crafting screen, and only a gem case did.
So `ApothicSockets.java` answers it instead, and Apotheosis makes that easy by
asking the question out loud: `SocketHelper` reads the component and then fires
`GetItemSocketsEvent` so anything may revise the answer. This counts the cases on
the stack and adds one for each. Nothing writes the component now, which is
better than the data version managed: a socket cut by a Sigil of Socketing
survives and the cases stack on top, where the component used to overwrite it.
It reaches both mods reflectively rather than compiling against them. Neither is
a build dependency here and one socket is not worth making them one, so the seven
members it needs - three off MIAPI's module tree, three off the event, and the
event class itself - are resolved once at startup and cached. Missing classes
mean the mods are not installed, which is ordinary and silent; a missing method
means something was renamed, which is logged, because it is a thing to fix rather
than a thing to expect. Either way the count falls back to what the component
says on its own.
That leaves the two systems where they each work best. Apotheosis reads the
socket count through `SocketHelper`, which classifies the item with
`LootCategory` - and modular armour extends vanilla `ArmorItem`, so it is a
helmet or a chestplate to Apotheosis rather than nothing at all. Gem bonuses
arrive as attribute modifiers on the stack, through `ItemAttributeModifierEvent`,
which is where MIAPI is not looking; MIAPI writes its own numbers into the
`ATTRIBUTE_MODIFIERS` component, and the two add up rather than overwrite.
It is drawn here rather than borrowed. It used to wear Armory's medium gemstone
model, which was the least work and the wrong picture: a case is the setting,
not the stone, and one that looks like a gemstone is indistinguishable from the
gemstone it is an alternative to. So `items/gem_case.png` is a four-by-four
bezel around an empty two-by-two socket - a gemstone's own footprint, with the
mount drawn around it - lit from the top left like the pommel sockets, and on
the same greys the materials' palettes map, so it comes out in whatever it was
built from. The socket stays empty-looking whatever is in it, which is honest:
an Apotheosis gem lives in a component and MIAPI has nothing to render for it.
The bezel is twice a gemstone across, which would put a case at twice a
gemstone's size in a slot sized for one, so the module's own model transform
carries a scale of 0.5 and hands the slot back a gemstone's footprint. That is
the module's business rather than the slot's: a socket that holds either should
not have to be scaled differently depending on which went in.
One thing worth knowing: a gem sitting in a case stops counting if the case is
removed - it is not destroyed, it is just in a socket that no longer exists.
**The space set** (`packs/armory`) is Create: Cosmonautics' answer to vacuum,
rebuilt as modules. Cosmonautics suffocates you above the atmosphere by
answering NeoForge's `LivingBreatheEvent` with "no", and exempts its own space
helmet through Create's diving code, which asks whether the worn helmet *is a*
`DivingHelmetItem`. A modular helmet cannot be one, so the exemption is
unreachable and the set brings its own: `VacuumSeal.java` registers a
`cmmodular:vacuum_seal` attribute and answers the same event last.
It takes two modules to reach the seal. The sealed helmet grants 0.5 and the
life support pack on the chest grants 0.5, and only at 1 does the wearer
breathe - so it is a suit, not a hat, which is the same trade Cosmonautics makes
by pairing its helmet with a jetpack. Both come in a socketed version that
trades 5% armour for a medium gem slot, exactly as Armory's own socket pieces
do, and each socketed version has its own model and texture with the setting
drawn in, rather than wearing the plain piece's art.
All of the art here is placeholder-grade and meant to be repainted - see
Textures below.
## The arcana core
The one item this mod adds, and the only late-game thing in it that is not
simply a good material. One trophy from each corner of the game in the corners
of the grid - a nether star, a dragon's breath, a heart of the sea and Iron's
Spells' ancient knowledge fragment - with two cinder essence and two arcane
essence on the edges and nothing in the middle. All of it is already endgame,
and what comes out is a gem that fits any socket and ends Iron's Spells'
limits: a mana pool of a million refilled a hundred times over, cooldowns and
cast times at zero, and ten times the spell power. It is deliberately absurd,
which is the point of the recipe.
Cooldown and cast time are read by Iron's Spells as a factor on a base of 1 and
spent as `time * (2 - value)`, so a `*` modifier of 1 puts the value at 2 and
the time at nothing; spell power is the same reading, where `1 + 9` is ten
times. There is no "infinite mana" to ask for, so a pool nothing can empty and a
regeneration that refills it instantly is what that means here.
The item is always registered, because an item cannot be added conditionally the
way a datapack can. Nothing that makes it worth having is: the material and the
recipe both live in the Iron's Spells pack, so without that mod the core cannot
be crafted, is not a material, and grants nothing.
Its texture is Minecraft's own heart of the sea with its hue rotated into
violet - the only art here that is not drawn for this mod, and Mojang's rather
than ours, which is worth knowing given what the khopesh section above says
about shipping other people's. Nothing but the hue is touched, so the shading
is the vanilla item's, including how dark its underside is: the palette the
material hands out is therefore a deep one with little highlight, which is what
the item actually looks like rather than what a gem usually does.
## Stats
Tinkers' Construct 3 is the reference, re-scaled onto MIAPI's vanilla-derived
numbers (iron 235 durability, diamond 1546, netherite 2016). Materials with no
Tinkers counterpart are built from what the material actually is, then fitted to
the nearest tier band:
| Tinkers | here | shape |
| --- | --- | --- |
| cobalt | titanium | light, fast, durable, unremarkable damage |
| steel | steel | balanced workhorse, poor enchantability |
| hepatizon | brass, bronze | copper alloys - quick and enchantable, not durable |
| manyullyn | dragonsteel | top damage, endgame gate |
| rose gold | refined glowstone, pixie dust | fragile, huge enchantability |
| ancient hide | dragon scales, chitin | armour-first, high toughness |
Tiers 6 and 7 sit above netherite: 6 is refined obsidian, shadow steel, refined
radiance, atomic alloy and the two Iron's Spells essences; 7 is the three
dragonsteels and antimatter.
Real-material character does the rest of the work - lead is the heaviest thing
here and swings slowest, osmium is dense and tough, HDPE weighs nothing and
cannot cut rock, silver smites the undead.
All of it lives in `tools/materials.py`, one entry per material with the
reasoning in comments next to the numbers.
## Palettes
Colours are not hand-picked. `tools/generate_materials.py` opens the source
mods' jars, resolves each material's item model to its texture, and samples that
texture to build MIAPI's seven-stop `grayscale_map`.
The sampling deliberately does *not* normalise a stop's colour to its brightness
number. A stop key is the brightness of MIAPI's grayscale template; its value is
whatever the material looks like at that point in its own shading, which is why
MIAPI's own netherite tops out at a murky `847a84` while iron runs to white.
Samples are taken at percentiles of the texture's brightness order, so a texture
with four shades still yields a full ramp and any hue drift between shadow and
highlight survives. Pure-black outline pixels are excluded - they are not a
shade of the material - and the darkest stop is extrapolated below the texture,
which is what MIAPI's own palettes do.
The sample points were tuned against MIAPI's hand-authored copper palette and
reproduce it to within about 2% per channel.
## Textures
The modules' own art is hand-drawn and lives where it can be found and changed:
```
assets/cmmodular/textures/items/ flat 16x16 art - gui icons, the khopesh blade
assets/cmmodular/textures/equipment/ 32x32 cube nets for the worn armour models
assets/cmmodular/textures/templates_guide.png
```
Everything is greyscale, because MIAPI recolours a module's texture through the
material's palette: the seven shades a texture may use are the brightness stops
of that palette (24, 68, 107, 150, 190, 216, 255), and a pixel drawn between two
of them lands somewhere interpolated and muddy.
`textures/*/templates/` holds blanks to draw over - the same size as the real
thing, with every region a model samples outlined and the seven shades parked in
a corner no model reads, so they can be picked straight out of the file.
`atlases/blocks.json` is what puts these two directories on the block atlas, and
without it a worn module is drawn untextured. Minecraft stitches
`textures/block` and `textures/item` and nothing else - everything else reaches
the atlas only by being referenced from a model the game itself loads, and a
module model is loaded by MIAPI rather than by Minecraft, so nothing of ours is
referenced by the time the atlas is built. Armory sidesteps it by filing its
module art under `miapi:item/...`, inside the directory vanilla already takes;
naming our own two directories is the same fix without moving the art.
`templates_guide.png` is a single sheet showing the shades with their values,
the cube net with each face labelled, and the vanilla armour layout, for when a
new piece is wanted rather than a repaint of an existing one.
## Building
```sh
./build.sh
```
The jar lands in `build/libs/`. Needs nothing but NeoForge - Truly Modular is a
runtime dependency only, since this mod talks to it purely through datapack
JSON.
`./gradlew build` does the same thing, and is fine if the java on your path is
one Gradle can run on. Gradle 8.12 cannot run on a JDK newer than 23, which a
distro's default java may well be by now, and the failure when it is says
`Could not create task ':test'` and `Type T not present` without ever mentioning
Java. `build.sh` exists to pick a JDK between 17 and 23 - honouring `JAVA_HOME`
if it already points at one - and to say what is wrong in as many words when
there is none to pick. Arguments are passed through, with any task named
replacing the default `build` rather than adding to it, so `./build.sh clean
build` cleans and then builds; a bare flag such as `./build.sh --info` still
gets the default task.
None of this touches what the mod is compiled against: the toolchain in
`build.gradle` is Java 21 either way, and Gradle provisions it if it is missing.
## Regenerating materials
Only needed after editing `tools/materials.py` or when a source mod changes its
textures. The generated JSON is committed, so a plain build does not need this.
```sh
python3 tools/generate_materials.py --jars ~/path/to/mod/jars
```
Point it at the pack's `mods` folder. This mod's own namespace is served out of
`src/main/resources/assets/cmmodular/` rather than a jar, so the arcana core is
checked and sampled like everything else. Every item id in `materials.py` is
checked against those jars and the run fails on a typo, rather than shipping a material
that silently refuses to load.
A mod whose jar is not there is skipped instead, with a line saying so, and its
committed files are left untouched - the pack this follows adds and drops mods,
and neither losing the materials of a mod you no longer have nor being unable to
regenerate anything without a complete set of jars is useful. Nothing else is
relaxed: a typo in a mod you *do* have still fails the run. Where several jars
claim the same asset namespace - Applied Mekanistics writes into `ae2`, half the
Create addons into `create` - the one with the most files in it wins, which is
the mod that owns the namespace rather than whichever sorted first.
Requires `pillow` and `numpy`.
Module JSON and textures are not generated. A run only clears the material
folders, so the hand-written packs survive it.
## Looking at the armour
```sh
python3 tools/ARMOUR_QUICKSTART.py # every armour model, one at a time
python3 tools/ARMOUR_GUI.py --jar <armory> # draw and unwrap socket geometry
python3 tools/ARMOUR_EDITOR.py --jar <armory> # pose a scene and nudge placements
```
The quickstart is the way in and needs no arguments: it reads every model in
`src/main/resources` - worn armour, icons, sword parts, the loose item models,
the files in the tree rather than a built jar - and lists them with the first
one showing. A jar passed with `--jar` adds its models to the list too, after
yours and marked with their namespace, which is the only way to see a socket
against the plate it is cut into: the geometry in this repo is the socket, and
`arm_left/heavy` is Armory's. Click a row to show or hide it and to point the camera at it;
`Reload from disk` picks up a model saved in another window.
The left panel is the selected model's MIAPI transform. Three arrows on the
model's origin move it along an axis and the rotate toggle swaps them for three
rings; the position and rotation boxes read what the drag did, in the units a
module writes, and take typed numbers back. `Copy as JSON` puts the `transform`
block on the clipboard, and `Write to source` puts it back in the module entry
it was read from - scale and origin untouched. It writes every model that has
moved rather than just the selected one, and marks a moved row with a `*` until
it has; a model no module names has nowhere to write to and is named in the
report instead. `Unwrap UVs` re-cuts the selected model's texture so every
face has a patch of its own, writes the atlas size beside the boxes, and paints
a template to match where there is no texture yet - never over one there is.
Where a model goes comes from the mod's own module data: MIAPI draws a model
under the body part its `origin` names, and those are declared in
`packs/*/data/*/miapi/modules/`. What is *not* in this tree is the offset
Armory's slot transforms carry, so without `--jar` a worn model is flipped onto
its part but not moved along it - right limb, roughly right place. Pass the
Armory jar for exact placement, and for its own plates to line new geometry up
against.
These need pyvista and pyvistaqt, which hardly any distribution packages, so
the tools keep a virtualenv at `tools/.venv` and re-run themselves inside it.
Build it once with
```sh
python3 -m venv --system-site-packages tools/.venv
tools/.venv/bin/pip install pyvista pyvistaqt
```
`--system-site-packages` is what keeps the distribution's Qt in charge; a
PySide6 from pip alongside the system Qt is a partial upgrade waiting to
happen. On a Wayland session the viewport runs through XWayland, because VTK's
Python wheels have no Wayland window backend - the tools say which display they
picked, and why, when they start.
## Notes
- Titanium's palette comes from Cosmonautics' ingot, which is violet. Air War's
is blue-white; swap `palette_from` on the titanium entry to prefer it.
- Cinder essence is nearly black, because its texture is a charred ember. It is
emissive, so it still reads as fire in game.
- Dragon scales are one material per colour (12 dragon, 7 sea serpent, 3 death
worm chitin). Colours within an element share stats and differ only in palette.
- Sea serpent scales add `miapi:generic.swim_speed` per worn slot, 0.10 a
module. It stacks per module like everything else here, so a full suit of
them lands near +1 - about what Armory's own scuba set is worth, which is the
company a sea serpent belongs in.
- Deathworm chitin's sand phasing is gated on a tag, `cmmodular:sand_like`, so
a pack can widen it: the common `#c:sands`, `#c:gravels` and
`#c:concrete_powders` (the `c:` namespace being where Forge's tags went),
each optional in case a set is not registered, plus the vanilla loose blocks
by name so it works with none of them.
- Dragon bone burns what it hits: a fake Fire Aspect I on handheld modules, the
way Ice and Fire's own flamed dragonbone sword does. MIAPI's `immolate` is
not that - despite the name it is bonus damage against something that is
already alight, and MIAPI's own wiki marks it unimplemented. Ice and Fire
ships one dragon bone item for all three kinds of dragon, so this cannot be
the fire dragon's alone; the fire dragonscales are the only element-specific
dragon material there is.
- Bones are the other half of that. Dragon bone already built grips, handles
and hafts - `bone` is on every tool and bow whitelist - and now carries
`gem_armor` as a hidden group as well, so a piece can be set into an armour
socket instead of only shaped into the armour. Sea serpent fang is new and
does both: Ice and Fire has no sea serpent bone, and the fang is what a
serpent leaves behind besides its scales.
- What a fang is worth depends on where it sits, and all of it is filed under
`default` so a fang set into an armour socket counts as much as one shaped
into the piece. Worn, it grants 0.5 water breathing in each armour slot -
which is what Ice and Fire's own Tide Guardian armour does - and the handler
only reads 1 as breathing, so it takes two worn fang modules: two sockets,
two pieces built of it, or a socketed piece that is both. Held, it breathes
not at all and instead does what a serpent does in its element: `+0.6`
submerged mining speed on vanilla's base of `0.2`, so a single fang module
digs four times faster underwater than a plain tool and two of them beat
dry-land speed, and `+0.10` `cmmodular:water_combat`, so a weapon built of it
hits that much harder and swings that much faster while its bearer is in
water. Both stack per module; the combat half caps at `+0.5`. That half is
the bearer's rather than the weapon's, so a fang bow's arrows count as well,
as long as the fang is in hand and its bearer in water when they land.
- Scales are in the `scute` group, and no module in Truly Modular accepts that
group - not Armory's armour, not Arsenal's or Archery's parts, and MIAPI's own
turtle and armadillo scutes are stuck the same way. So each scale material
carries `bone` as a hidden group: matched when a module decides what it will
accept, ignored when the workbench decides what heading to file the material
under, which is how Armory hands its own gem slots vanilla materials. Bone is
on every armour whitelist except the scuba set - that one takes
fabric/metal/glass, being a sealed suit rather than a plated one - so a scale
builds every other piece of armour, and tool and bow parts besides.
- Interstellar Expansion is mostly machinery. Only three of its items are a
material rather than a component; solid E-710 is left out because it is rocket
fuel.
- Gem sockets do not check the `gem` group. Arsenal gates melee gems on
`gem_melee` and Armory gates armour gems on `gem_armor`, so every gem-like
material carries both and fits any gem slot.
- Immersive Engineering contributes aluminium, nickel, constantan, electrum and
graphite. Its steel, lead, uranium and silver are not separate materials - see
overlapping metals above. Superb Warfare's steel/lead/silver and Create Crafts
& Additions' electrum are skipped for the same reason.
- Crystal Chronicles and Pastel add whole families of gems that differ by colour
and lore rather than by kind, so those are generated from a table with shared
stat bands per tier. What actually tells them apart in game is the palette,
and that still comes from each gem's own texture.
- Arcane essence gives max mana per slot: 1300 on a helmet, 900 chest, 800
legs, 700 boots, 900 held in the main hand, 400 offhand. Separate per-slot
modifiers rather than one `armor`-wide one, so a weapon reads as a held bonus
instead of claiming something "when worn". They stack per module - a piece
built out of arcane essence that also carries an arcane gem counts twice.
- Titanium adds `miapi:generic.elytra_glide_efficiency` on the chest. Heavy
armour subtracts `-30 - density*2` from that stat, so this hands a little
back rather than cancelling it; titanium's flexibility is nudged up too,
since Armory's wing modules read flexibility and density for glide.
- Cinder essence uses slot `any`, so one gem grants fire immunity wherever it
sits, worn or held. The attribute caps at 1 and the handler treats anything at
or above 1 as immune, so extra pieces neither add nor dilute.
- A material's name is a lang key, not a field on the material. MIAPI reads
`miapi.material.<namespace>.<group>.<name>` - its own id with the separators
turned into dots - so `cmmodular:metal/arcane_metal` is named by
`miapi.material.cmmodular.metal.arcane_metal` in `assets/cmmodular/lang/`,
which the generator writes. The value is the bare name: MIAPI puts in the
space before the module's own name. There is a `translation` field, but it is
only read alongside `fake_translation`, as a pair registering a name for a key
at runtime; on its own it names nothing, and the key shows raw in game.
- Material properties belong under the `default` key, not `armor` or `handheld`.
A gem socketed into armour is not tagged `armor`: gem modules declare which
material properties they accept and list only `default` plus their own gem
tags. Anything filed under `armor`/`handheld` silently never applies to a
socketed gem. The attribute's own slot is what decides where it counts.
- Arcane metal shortens Iron's Spells cooldowns, and that is the point of it:
0.10 held, 0.05 on a helmet, 0.03 on the lower armour, filed under `default`
so a suit plated in it counts as well as a staff made of it. Iron's Spells
reads the attribute as a factor on a base of 1 and spends it as
`cooldown * (2 - value)`, so the operation is `*` - a flat `+` would be added
to a base of zero and do nothing - and 0.10 means 10% off the wait. It stacks
per module, so a weapon built entirely of arcane metal is around 40%, against
25% for Iron's Spells' own best staff; a full set adds roughly another 45%,
which keeps even an all-arcane player under the 100% where the formula
bottoms out. The material is called Arcane Metal because the item is an Arcane
Ingot and a material is named for the substance, not the shape it is traded
in; plain Arcane, which is what Iron's Spells calls the material itself, is
taken here by the essence. Create: Wizardry's arcane sheet and block are the
same metal - they arrive as a material extension in that mod's own pack, since
naming them directly would break the material for anyone without it.
- Platinum comes from Create Propulsion: Simulated, the only platinum in the
pack. It is soft for its density, so it swings slowly and hits like steel; it
is worth mining for gold-grade enchantability that does not fall apart, and it
is heat proof because platinum melts at 1768C.
- Aviation alloy is AeroEngine's, iron superheated with netherite powder. A
turbine superalloy: light and heat proof like titanium, harder, worse to
enchant. It sits between titanium and titanium alloy, which is where its
recipe sits too.
- Extensions are material files with a different shape - `parent` naming the
material to edit and `data` merged onto a copy of it, the same form MIAPI's
stained glass uses to vary plain glass. The merge replaces a field rather than
appending to it, so an extension that adds an ingredient repeats the ones it
keeps; `_extend_with_extra_forms` builds those lists from the material itself.
- Ice and Fire's amethyst gem is spelled `amythest_gem` in Ice and Fire. The
extension named the correct spelling, which is an item that does not exist, so
it did nothing. Extensions are item-checked like materials now.
- Pastel's paltaeria and stratine gems break into eight fragments on an anvil,
so a fragment counts as an eighth of a gem, the way a nugget counts as a ninth
of an ingot.