624 lines
37 KiB
Markdown
624 lines
37 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.
|
|
|
|
They are not right yet, and two attempts to make them right are worth recording
|
|
so that they are not repeated. Naming the limb as the gem slot's `origin` - so
|
|
the offset lands in the entry the limb pass reads and the gem follows the arm -
|
|
composes correctly on paper and in game puts the gems several blocks away from
|
|
the wearer, floating. And `tools/preview_armour.py`, which applies the same chain
|
|
MIAPI does and draws the result from any angle, agrees with the paper rather than
|
|
with the game: given the configuration that in practice left gems at the neck, it
|
|
puts them on the pauldron. The tool is worth keeping for reading Armory's own
|
|
geometry - where the plate bands are, which face is which, how far the chain
|
|
scales a limb - and it should not be trusted to predict a gem's final position
|
|
until it can reproduce a symptom somebody has actually seen.
|
|
|
|
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. All three are 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.
|
|
|
|
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.
|
|
`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.
|