Add the modules Truly Modular is missing, and two more materials

Platinum (Create Propulsion: Simulated) and aviation alloy (AeroEngine) were
the only materials left in the pack; Pastel's gem fragments now count as an
eighth of a gem, and Create: Wizardry's arcane sheet, arcane block and mithril
nugget feed the metals they are made of, through extensions in its own pack so
they cost nothing to anyone without it.

Four modules of our own, in packs gated on the addon whose slots they use:

- A khopesh blade for any sword blade slot - axe damage, a point of knockback,
  an axe's mining level, fast, and short-reached and fragile to pay for it.
  Nothing in the ecosystem ships one, so it is drawn here.
- Socketed pommels. Arsenal's own socket pommel takes a small gem, and no
  module anywhere in Arsenal, Armory, Archery or MIAPI offers a gem_melee_large
  slot, so the large gemstone had nowhere to go. Now it does.
- A sealed helmet and a life support pack. Cosmonautics suffocates you in space
  through LivingBreatheEvent and exempts only items that *are* a Create diving
  helmet, which a modular helmet cannot be, so VacuumSeal.java answers the same
  event last. Half a seal each: it takes the suit, not the hat.

Arcane alloy is what it is called in game - Create: Wizardry renames the ingot -
and cutting spell cooldowns is now the reason to build out of it: 10% held per
module, less on armour, on Iron's Spells' cooldown * (2 - value), so a weapon
made of it beats the mod's best staff and a full kit still stays inside the
formula.

Two bugs found on the way: the Ice and Fire amethyst extension named an item
that does not exist (the mod spells it amythest_gem) in a shape MIAPI's codec
does not read, so it had never done anything - extensions are item-checked like
materials now. And the generator handed a namespace to whichever jar sorted
first, which is an addon as often as the mod that owns it; it goes to the jar
with the most of it now. A mod whose jar is missing is skipped with its files
left alone rather than failing the run, and only material folders are cleared,
since the module packs are hand-written.

Module art is placeholder-grade and meant to be repainted: it lives in
textures/items and textures/equipment, with blank templates and a guide sheet
giving the palette stops and every region a model samples.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
modules-and-missing-materials
Amir Alexander Abdelbaki 2026-08-14 10:04:46 +02:00
parent d0ab532683
commit 4e470110c2
50 changed files with 1557 additions and 52 deletions

128
README.md
View File

@ -1,13 +1,17 @@
# Create/Mekanism Modular
A [Truly Modular](https://modrinth.com/mod/modular-item-api) addon for Minecraft
1.21.1 / NeoForge. It adds 124 materials drawn from 20 mods - Create, Mekanism, Ice
1.21.1 / NeoForge. It adds 126 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 three
Create aerospace addons - so their metals, gems, scales and essences can all be
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, and a sealed helmet and life support pack for Armory that between them
let you breathe in the vacuum Create: Cosmonautics puts above the atmosphere.
Every source mod is optional. Install any subset - the materials for mods you do
not have are never registered.
@ -52,6 +56,51 @@ 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.
## Modules
Three 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.
**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.
**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.
## Stats
Tinkers' Construct 3 is the reference, re-scaled onto MIAPI's vanilla-derived
@ -98,6 +147,28 @@ 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
@ -116,12 +187,24 @@ textures. The generated JSON is committed, so a plain build does not need this.
python3 tools/generate_materials.py --jars ~/path/to/mod/jars
```
The folder needs one jar per source mod. 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.
Point it at the pack's `mods` folder. 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.
## Notes
- Titanium's palette comes from Cosmonautics' ingot, which is violet. Air War's
@ -161,3 +244,36 @@ Requires `pillow` and `numpy`.
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 alloy 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 alloy 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 Alloy rather than Arcane Steel because Create: Wizardry
renames the ingot that way, and 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.

View File

@ -7,7 +7,7 @@ neo_version=21.1.248
mod_id=cmmodular
mod_name=Create/Mekanism Modular
mod_version=1.1.0
mod_version=1.2.0
mod_group_id=eu.abdelbaki.cmmodular
mod_authors=themiro
mod_description=Materials from Create, Mekanism, Ice and Fire, Iron's Spells and the Create aerospace addons, for Truly Modular.
mod_description=Materials from Create, Mekanism, Ice and Fire, Iron's Spells and the Create aerospace addons, for Truly Modular - plus a khopesh blade and a sealed space suit.

View File

@ -33,7 +33,7 @@ import net.neoforged.neoforge.event.AddPackFindersEvent;
public class CMModular {
public static final String MOD_ID = "cmmodular";
/** A set of materials, and the mod it needs in order to load. */
/** A built-in datapack - materials or modules - and the mod it needs. */
private record MaterialPack(String id, String name, String requires) {
boolean present() {
return ModList.get().isLoaded(requires);
@ -61,10 +61,18 @@ public class CMModular {
new MaterialPack("rftoolsbase", "RFTools Materials", "rftoolsbase"),
new MaterialPack("createdeco", "Create Deco Materials", "createdeco"),
new MaterialPack("crystal_chronicles", "Crystal Chronicles Materials", "crystal_chronicles"),
new MaterialPack("pastel", "Pastel Materials", "pastel"));
new MaterialPack("pastel", "Pastel Materials", "pastel"),
new MaterialPack("createpropulsion", "Create Propulsion Materials", "createpropulsion"),
new MaterialPack("aeroengineering", "AeroEngine Materials", "aeroengineering"),
new MaterialPack("create_wizardry", "Create Wizardry Materials", "create_wizardry"),
// Modules, not materials, and gated for the same reason: they name
// slots - and inherit modules - that only exist with these installed.
new MaterialPack("arsenal", "Arsenal Modules", "tm_arsenal"),
new MaterialPack("armory", "Armory Modules", "tm_armory"));
public CMModular(IEventBus modBus) {
FireImmunity.register(modBus);
VacuumSeal.register(modBus);
modBus.addListener(this::addMaterialPacks);
}

View File

@ -0,0 +1,64 @@
package eu.abdelbaki.cmmodular;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
import net.minecraft.world.entity.ai.attributes.RangedAttribute;
import net.neoforged.bus.api.EventPriority;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.event.entity.EntityAttributeModificationEvent;
import net.neoforged.neoforge.event.entity.living.LivingBreatheEvent;
import net.neoforged.neoforge.registries.DeferredHolder;
import net.neoforged.neoforge.registries.DeferredRegister;
/**
* The space set: a sealed helmet and a life support pack that together let the
* wearer breathe where there is nothing to breathe.
*
* <p>Create: Cosmonautics suffocates you in space by answering
* {@link LivingBreatheEvent} with "no", the same event vanilla uses underwater,
* and Create decides who is exempt by asking whether the helmet
* <em>is a</em> {@code DivingHelmetItem}. A modular helmet never can be, so
* neither mod's own answer can be reused - hence an attribute of this mod's own
* and a listener for it, exactly as {@link FireImmunity} does for fire.
*
* <p>Half a seal per piece is the point: one module is never enough, so a
* player wanting to breathe in a vacuum has to build both halves of the suit,
* which is the trade Cosmonautics itself makes with its helmet and jetpack.
*/
public final class VacuumSeal {
private static final DeferredRegister<Attribute> ATTRIBUTES =
DeferredRegister.create(Registries.ATTRIBUTE, CMModular.MOD_ID);
/** At or above 1 the wearer always has air; each piece of the set is 0.5. */
public static final DeferredHolder<Attribute, Attribute> VACUUM_SEAL =
ATTRIBUTES.register("vacuum_seal", id -> new RangedAttribute(
"attribute.cmmodular.vacuum_seal", 0.0D, 0.0D, 1.0D).setSyncable(true));
private VacuumSeal() {
}
static void register(IEventBus modBus) {
ATTRIBUTES.register(modBus);
modBus.addListener(VacuumSeal::attachToEntities);
// Last, so it answers after whatever said there was no air - Cosmonautics
// in space, vanilla underwater.
NeoForge.EVENT_BUS.addListener(EventPriority.LOWEST, VacuumSeal::onBreathe);
}
/** Attributes only exist on an entity if they were added to its type. */
private static void attachToEntities(EntityAttributeModificationEvent event) {
event.getTypes().forEach(type -> event.add(type, VACUUM_SEAL));
}
private static void onBreathe(LivingBreatheEvent event) {
if (event.canBreathe()) {
return;
}
AttributeInstance instance = event.getEntity().getAttribute(VACUUM_SEAL);
if (instance != null && instance.getValue() >= 1.0D) {
event.setCanBreathe(true);
}
}
}

View File

@ -182,3 +182,41 @@ type = "optional"
versionRange = "[0,)"
ordering = "AFTER"
side = "BOTH"
[[dependencies.${mod_id}]]
modId = "createpropulsion"
type = "optional"
versionRange = "[0,)"
ordering = "AFTER"
side = "BOTH"
[[dependencies.${mod_id}]]
modId = "aeroengineering"
type = "optional"
versionRange = "[0,)"
ordering = "AFTER"
side = "BOTH"
[[dependencies.${mod_id}]]
modId = "create_wizardry"
type = "optional"
versionRange = "[0,)"
ordering = "AFTER"
side = "BOTH"
# The two Truly Modular addons that own the slots this mod's modules go in, and
# the modules they inherit from. Optional like everything else - without them
# the module packs are simply never registered.
[[dependencies.${mod_id}]]
modId = "tm_arsenal"
type = "optional"
versionRange = "[0,)"
ordering = "AFTER"
side = "BOTH"
[[dependencies.${mod_id}]]
modId = "tm_armory"
type = "optional"
versionRange = "[0,)"
ordering = "AFTER"
side = "BOTH"

View File

@ -1,3 +1,13 @@
{
"attribute.cmmodular.fire_immunity": "Fire Immunity"
"attribute.cmmodular.fire_immunity": "Fire Immunity",
"attribute.cmmodular.vacuum_seal": "Vacuum Seal",
"miapi.module.cmmodular.blade.khopesh.name": "Khopesh Blade",
"miapi.module.cmmodular.pommel.gem_socket.name": "Socketed Pommel",
"miapi.module.cmmodular.pommel.great_socket.name": "Great Socketed Pommel",
"miapi.module.cmmodular.armor.space.helmet.name": "Sealed Helmet",
"miapi.module.cmmodular.armor.space.helmet_socket.name": "Sealed Helmet with Socket",
"miapi.module.cmmodular.armor.space.life_support.name": "Life Support Pack",
"miapi.module.cmmodular.armor.space.life_support_socket.name": "Life Support Pack with Socket"
}

View File

@ -0,0 +1,6 @@
{
"parent": "item/generated",
"textures": {
"layer0": "cmmodular:items/space_helmet"
}
}

View File

@ -0,0 +1,6 @@
{
"parent": "item/generated",
"textures": {
"layer0": "cmmodular:items/space_helmet_socket"
}
}

View File

@ -0,0 +1,31 @@
{
"comment": "Life support pack, worn. Twin tanks sitting on the back plate; the same 32x32 cube net as the helmet, so both are edited the same way.",
"texture_size": [32, 32],
"textures": {
"1": "cmmodular:equipment/life_support_pack",
"particle": "cmmodular:equipment/life_support_pack"
},
"elements": [
{
"name": "tanks",
"from": [-3.5, -6.5, 2],
"to": [3.5, 1.5, 5],
"faces": {
"north": {"uv": [8, 8, 16, 16], "texture": "#1"},
"east": {"uv": [16, 8, 24, 16], "texture": "#1"},
"south": {"uv": [24, 8, 32, 16], "texture": "#1"},
"west": {"uv": [0, 8, 8, 16], "texture": "#1"},
"up": {"uv": [8, 0, 16, 8], "texture": "#1"},
"down": {"uv": [16, 0, 24, 8], "texture": "#1"}
}
}
],
"groups": [
{
"name": "Life Support Pack",
"origin": [0, 0, 0],
"color": 0,
"children": [0]
}
]
}

View File

@ -0,0 +1,96 @@
{
"comment": "Life support pack with a gem setting between the tanks, worn. Same box as the plain pack, socket stamped into the back face of the net.",
"texture_size": [
32,
32
],
"textures": {
"1": "cmmodular:equipment/life_support_pack_socket",
"particle": "cmmodular:equipment/life_support_pack_socket"
},
"elements": [
{
"name": "tanks",
"from": [
-3.5,
-6.5,
2
],
"to": [
3.5,
1.5,
5
],
"faces": {
"north": {
"uv": [
8,
8,
16,
16
],
"texture": "#1"
},
"east": {
"uv": [
16,
8,
24,
16
],
"texture": "#1"
},
"south": {
"uv": [
24,
8,
32,
16
],
"texture": "#1"
},
"west": {
"uv": [
0,
8,
8,
16
],
"texture": "#1"
},
"up": {
"uv": [
8,
0,
16,
8
],
"texture": "#1"
},
"down": {
"uv": [
16,
0,
24,
8
],
"texture": "#1"
}
}
}
],
"groups": [
{
"name": "Life Support Pack, socketed",
"origin": [
0,
0,
0
],
"color": 0,
"children": [
0
]
}
]
}

View File

@ -0,0 +1,31 @@
{
"comment": "Sealed helmet, worn. One cube around the head, textured from a 32x32 cube net so the faces can be edited without touching a vanilla armour atlas.",
"texture_size": [32, 32],
"textures": {
"1": "cmmodular:equipment/space_helmet",
"particle": "cmmodular:equipment/space_helmet"
},
"elements": [
{
"name": "dome",
"from": [-4.25, -4.25, -4.25],
"to": [4.25, 4.25, 4.25],
"faces": {
"north": {"uv": [8, 8, 16, 16], "texture": "#1"},
"east": {"uv": [16, 8, 24, 16], "texture": "#1"},
"south": {"uv": [24, 8, 32, 16], "texture": "#1"},
"west": {"uv": [0, 8, 8, 16], "texture": "#1"},
"up": {"uv": [8, 0, 16, 8], "texture": "#1"},
"down": {"uv": [16, 0, 24, 8], "texture": "#1"}
}
}
],
"groups": [
{
"name": "Space Helmet",
"origin": [0, 0, 0],
"color": 0,
"children": [0]
}
]
}

View File

@ -0,0 +1,96 @@
{
"comment": "Sealed helmet with a gem setting on the crown, worn. Same cube as the plain helmet, socket stamped into the top face of the net.",
"texture_size": [
32,
32
],
"textures": {
"1": "cmmodular:equipment/space_helmet_socket",
"particle": "cmmodular:equipment/space_helmet_socket"
},
"elements": [
{
"name": "dome",
"from": [
-4.25,
-4.25,
-4.25
],
"to": [
4.25,
4.25,
4.25
],
"faces": {
"north": {
"uv": [
8,
8,
16,
16
],
"texture": "#1"
},
"east": {
"uv": [
16,
8,
24,
16
],
"texture": "#1"
},
"south": {
"uv": [
24,
8,
32,
16
],
"texture": "#1"
},
"west": {
"uv": [
0,
8,
8,
16
],
"texture": "#1"
},
"up": {
"uv": [
8,
0,
16,
8
],
"texture": "#1"
},
"down": {
"uv": [
16,
0,
24,
8
],
"texture": "#1"
}
}
}
],
"groups": [
{
"name": "Space Helmet, socketed",
"origin": [
0,
0,
0
],
"color": 0,
"children": [
0
]
}
]
}

View File

@ -0,0 +1,6 @@
{
"parent": "item/generated",
"textures": {
"layer0": "cmmodular:items/khopesh_blade"
}
}

View File

@ -0,0 +1,6 @@
{
"parent": "item/generated",
"textures": {
"layer0": "cmmodular:items/pommel_gem_socket"
}
}

View File

@ -0,0 +1,6 @@
{
"parent": "item/generated",
"textures": {
"layer0": "cmmodular:items/pommel_great_socket"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@ -0,0 +1,64 @@
{
"translation": "Aviation Alloy ",
"groups": [
"metal"
],
"icon": {
"type": "item",
"item": "aeroengineering:aviation_alloy_ingot"
},
"hardness": 6.6,
"density": 2.4,
"flexibility": 4,
"durability": 1650,
"enchantability": 14,
"toughness": 3,
"tier": 5,
"mining_speed": 10,
"mining_level": "minecraft:incorrect_for_netherite_tool",
"armor_durability": 35,
"armor_toughness": 2.0,
"color": "889295",
"color_palette": {
"type": "grayscale_map",
"colors": {
"24": "1f2122",
"68": "3b4041",
"107": "4f575a",
"150": "575e60",
"190": "666f72",
"216": "889295",
"255": "a5aeb0"
},
"filler": "interpolate"
},
"textures": [
"metallic",
"bright"
],
"properties": {
"default": {
"fire_proof": true
},
"handheld": {
"attributes": [
{
"attribute": "generic.attack_speed",
"value": "0.08",
"operation": "+",
"slot": "mainhand"
}
]
}
},
"items": [
{
"item": "aeroengineering:aviation_alloy_ingot",
"value": 1.0
},
{
"item": "aeroengineering:aviation_alloy_sheet",
"value": 1.0
}
]
}

View File

@ -0,0 +1,6 @@
{
"pack": {
"description": "AeroEngine Materials for Truly Modular",
"pack_format": 48
}
}

View File

@ -0,0 +1,52 @@
{
"parent": "tm_armory:armor/default/helmet",
"data": {
"merge": {
"module_tag": [
"space",
"sealed"
],
"attributes": [
{
"attribute": "cmmodular:vacuum_seal",
"value": "0.5",
"operation": "+",
"slot": "head"
}
]
},
"replace": {
"model": [
{
"path": "cmmodular:models/item/armor/model/helmet/space/[material.texture].json",
"trim_mode": "ARMOR_LAYER_ONE",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": 0, "y": 0, "z": 0},
"scale": {"x": 1, "y": 1, "z": 1},
"origin": "head"
}
},
{
"path": "cmmodular:models/item/armor/gui/space/helmet/[material.texture].json",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": 0, "y": 0, "z": 0},
"scale": {"x": 1, "y": 1, "z": 1},
"origin": "item"
}
}
],
"priority": 4,
"display_name": "miapi.module.cmmodular.armor.space.helmet.name",
"allowed_material": {
"allowedMaterials": [
"metal",
"crystal",
"glass"
],
"cost": 6
}
}
}
}

View File

@ -0,0 +1,68 @@
{
"parent": "cmmodular:armor/space/helmet",
"data": {
"merge": {
"module_tag": [
"socket_armor"
],
"attributes": [
{
"attribute": "minecraft:generic.armor",
"value": "-0.05",
"operation": "*",
"slot": "head"
}
]
},
"replace": {
"model": [
{
"path": "cmmodular:models/item/armor/model/helmet/space_socket/[material.texture].json",
"trim_mode": "ARMOR_LAYER_ONE",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": 0, "y": 0, "z": 0},
"scale": {"x": 1, "y": 1, "z": 1},
"origin": "head"
}
},
{
"path": "cmmodular:models/item/armor/gui/space/helmet_socket/[material.texture].json",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": 0, "y": 0, "z": 0},
"scale": {"x": 1, "y": 1, "z": 1},
"origin": "item"
}
}
],
"slots": {
"hat": {
"allowed": [
"hat",
"sub_hat"
],
"translationKey": "miapi.slot.hat.name",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": 0, "y": -1, "z": 0},
"scale": {"x": 1.1, "y": 1.1, "z": 1.1},
"origin": "head"
}
},
"gem": {
"allowed": [
"gem_armor_medium"
],
"translationKey": "miapi.slot.gemstone.medium",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": 0, "y": 3, "z": 0},
"scale": {"x": 1, "y": 1, "z": 1}
}
}
},
"display_name": "miapi.module.cmmodular.armor.space.helmet_socket.name"
}
}
}

View File

@ -0,0 +1,42 @@
{
"parent": "tm_armory:armor/default/back_chest",
"data": {
"merge": {
"module_tag": [
"space",
"sealed"
],
"attributes": [
{
"attribute": "cmmodular:vacuum_seal",
"value": "0.5",
"operation": "+",
"slot": "chest"
}
]
},
"replace": {
"model": [
{
"path": "cmmodular:models/item/armor/model/chest_back/space/[material.texture].json",
"trim_mode": "ARMOR_LAYER_ONE",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": 0, "y": 0, "z": 0},
"scale": {"x": 1, "y": 1, "z": 1},
"origin": "body"
}
}
],
"display_name": "miapi.module.cmmodular.armor.space.life_support.name",
"allowed_material": {
"allowedMaterials": [
"metal",
"crystal",
"glass"
],
"cost": 4
}
}
}
}

View File

@ -0,0 +1,61 @@
{
"parent": "cmmodular:armor/space/life_support",
"data": {
"merge": {
"module_tag": [
"socket_armor"
],
"attributes": [
{
"attribute": "minecraft:generic.armor",
"value": "-0.05",
"operation": "*",
"slot": "chest"
}
]
},
"replace": {
"model": [
{
"path": "cmmodular:models/item/armor/model/chest_back/space_socket/[material.texture].json",
"trim_mode": "ARMOR_LAYER_ONE",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": 0, "y": 0, "z": 0},
"scale": {"x": 1, "y": 1, "z": 1},
"origin": "body"
}
}
],
"slots": {
"banner-back": {
"allowed": [
"cosmetic_4x8",
"banner_4x8"
],
"slotType": "cosmetic",
"translationKey": "miapi.slot.chest_back_center.name",
"transform": {
"rotation": {"x": 180, "y": 0, "z": 180},
"translation": {"x": 0, "y": 1.7, "z": 2.35},
"scale": {"x": 1, "y": 0.775, "z": 1.1},
"origin": "body"
}
},
"gem": {
"allowed": [
"gem_armor_medium"
],
"translationKey": "miapi.slot.gemstone.medium",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": 0, "y": -1, "z": 0},
"scale": {"x": 1.1, "y": 1.1, "z": 1.1},
"origin": "body"
}
}
},
"display_name": "miapi.module.cmmodular.armor.space.life_support_socket.name"
}
}
}

View File

@ -0,0 +1,6 @@
{
"pack": {
"description": "Armory Modules for Truly Modular",
"pack_format": 48
}
}

View File

@ -0,0 +1,155 @@
{
"display_name": "miapi.module.cmmodular.blade.khopesh.name",
"model": [
{
"path": "cmmodular:models/item/sword/blade/khopesh/[material.texture].json",
"color": "[material.color]",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": -1, "y": -1, "z": -0.001},
"scale": {"x": 1, "y": 1, "z": 1}
}
}
],
"is_weapon": "true",
"priority": 3,
"repair_priority": -1,
"allowed_in_slots": [
"sword_blade"
],
"module_stats": {
"blade_size": 1
},
"allowed_material": {
"allowedMaterials": [
"wood",
"metal",
"flint",
"bone",
"stone",
"crystal",
"glass"
],
"cost": 2
},
"material_indication": {
"durability": 1,
"hardness": {
"strength": 3,
"info": {
"type": "translatable",
"translate": "tm_arsenal.material_indication.blade.damage",
"color": "dark_gray"
}
},
"density": {
"strength": 2,
"info": {
"type": "translatable",
"translate": "tm_arsenal.material_indication.blade.damage",
"color": "dark_gray"
}
},
"mining_speed": {
"strength": 1,
"info": {
"type": "translatable",
"translate": "tm_arsenal.material_indication.default.mining_speed",
"color": "dark_gray"
}
}
},
"enchantments": {
"allowed": [
"minecraft:enchantable/sword",
"minecraft:enchantable/fire_aspect",
"minecraft:enchantable/sharp_weapon",
"minecraft:enchantable/weapon",
"minecraft:enchantable/mining",
"minecraft:enchantable/mining_loot"
]
},
"mining_level": {
"axe": {
"allowed": [
"#minecraft:mineable/axe"
],
"use_material": true,
"speed": "[material.mining_speed] * 0.75"
},
"sword": {
"allowed": [
"#minecraft:sword_efficient"
],
"use_material": false,
"speed": "1.5"
},
"cobweb": {
"allowed": [
[
"minecraft:cobweb"
]
],
"use_material": false,
"correct_for_drops": true,
"speed": "15"
}
},
"ability_context": {
"axe_ability": {}
},
"attributes": [
{
"attribute": "generic.attack_damage",
"value": "[material.hardness]*0.9 + [material.density]*0.5",
"operation": "+",
"slot": "mainhand"
},
{
"attribute": "generic.attack_speed",
"value": "0.15",
"operation": "+",
"slot": "mainhand"
},
{
"attribute": "generic.attack_knockback",
"value": "1",
"operation": "+",
"slot": "mainhand"
},
{
"attribute": "miapi:generic.attack_range",
"value": "-0.4",
"operation": "+",
"slot": "mainhand"
},
{
"attribute": "miapi:generic.shield_break",
"value": "4",
"operation": "+",
"slot": "mainhand"
}
],
"durability": "[material.durability] * 0.2",
"better_combat_config": "bettercombat:axe",
"item_id": "miapi:modular_sword",
"fake_item_tag": [
"minecraft:swords",
"c:swords",
"minecraft:axes",
"c:axes",
"c:tools",
"c:tool/axe"
],
"module_tag": [
"default",
"handheld",
"blade",
"head",
"blade",
"blade_medium",
"axe",
"polearm_glaive",
"sharp"
]
}

View File

@ -0,0 +1,59 @@
{
"display_name": "miapi.module.cmmodular.pommel.gem_socket.name",
"model": {
"path": "cmmodular:models/item/sword/pommel/gem_socket/[material.texture].json",
"color": "[material.color]",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": -4, "y": -5, "z": 0.001},
"scale": {"x": 1, "y": 1, "z": 1}
}
},
"allowed_in_slots": [
"sword_pommel"
],
"slots": {
"socket_medium": {
"allowed": [
"gem_melee_medium"
],
"translationKey": "miapi.slot.gem.name",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": -5, "y": -5, "z": 0},
"scale": {"x": 1, "y": 1, "z": 1}
}
}
},
"attributes": [
{
"attribute": "generic.attack_speed",
"value": "-0.06",
"operation": "*",
"slot": "mainhand"
}
],
"priority": 0,
"repair_priority": 0,
"durability": "[material.durability]* 0.25",
"material_indication": {
"durability": 1
},
"allowed_material": {
"allowedMaterials": [
"wood",
"metal",
"flint",
"bone",
"stone",
"rod",
"crystal",
"glass"
],
"cost": 2
},
"module_tag": [
"default",
"handheld"
]
}

View File

@ -0,0 +1,57 @@
{
"display_name": "miapi.module.cmmodular.pommel.great_socket.name",
"model": {
"path": "cmmodular:models/item/sword/pommel/great_socket/[material.texture].json",
"color": "[material.color]",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": -4, "y": -5, "z": 0.001},
"scale": {"x": 1, "y": 1, "z": 1}
}
},
"allowed_in_slots": [
"sword_pommel"
],
"slots": {
"socket_large": {
"allowed": [
"gem_melee_large"
],
"translationKey": "miapi.slot.gem.name",
"transform": {
"rotation": {"x": 0, "y": 0, "z": 0},
"translation": {"x": -5, "y": -5, "z": 0},
"scale": {"x": 1, "y": 1, "z": 1}
}
}
},
"attributes": [
{
"attribute": "generic.attack_speed",
"value": "-0.12",
"operation": "*",
"slot": "mainhand"
}
],
"priority": 0,
"repair_priority": 0,
"durability": "[material.durability]* 0.3",
"material_indication": {
"durability": 1
},
"allowed_material": {
"allowedMaterials": [
"metal",
"flint",
"bone",
"stone",
"crystal",
"glass"
],
"cost": 3
},
"module_tag": [
"default",
"handheld"
]
}

View File

@ -0,0 +1,6 @@
{
"pack": {
"description": "Arsenal Modules for Truly Modular",
"pack_format": 48
}
}

View File

@ -0,0 +1,19 @@
{
"parent": "cmmodular:metal/arcane_metal",
"data": {
"items": [
{
"item": "irons_spellbooks:arcane_ingot",
"value": 1.0
},
{
"item": "create_wizardry:arcane_sheet",
"value": 1.0
},
{
"item": "create_wizardry:arcane_block",
"value": 9.0
}
]
}
}

View File

@ -0,0 +1,27 @@
{
"parent": "cmmodular:metal/mithril",
"data": {
"items": [
{
"item": "irons_spellbooks:mithril_ingot",
"value": 1.0
},
{
"tag": "c:ingots/mithril",
"value": 1.0
},
{
"tag": "c:nuggets/mithril",
"value": 0.11112
},
{
"tag": "c:storage_blocks/mithril",
"value": 9.0
},
{
"item": "create_wizardry:mithril_nugget",
"value": 0.11112
}
]
}
}

View File

@ -0,0 +1,6 @@
{
"pack": {
"description": "Create Wizardry Materials for Truly Modular",
"pack_format": 48
}
}

View File

@ -0,0 +1,84 @@
{
"translation": "Platinum ",
"groups": [
"metal"
],
"icon": {
"type": "item",
"item": "createpropulsion:platinum_ingot"
},
"hardness": 5.0,
"density": 5.7,
"flexibility": 3,
"durability": 680,
"enchantability": 24,
"toughness": 2,
"tier": 4,
"mining_speed": 7,
"mining_level": "minecraft:incorrect_for_diamond_tool",
"armor_durability": 24,
"armor_toughness": 1.0,
"color": "ADCCDE",
"color_palette": {
"type": "grayscale_map",
"colors": {
"24": "1b2731",
"68": "334b5e",
"107": "547086",
"150": "6896a4",
"190": "8dabc2",
"216": "adccde",
"255": "dbf5e9"
},
"filler": "interpolate"
},
"textures": [
"metallic",
"shiny"
],
"properties": {
"default": {
"fire_proof": true
},
"handheld": {
"attributes": [
{
"attribute": "generic.attack_speed",
"value": "-0.08",
"operation": "+",
"slot": "mainhand"
}
]
}
},
"items": [
{
"item": "createpropulsion:platinum_ingot",
"value": 1.0
},
{
"item": "createpropulsion:platinum_nugget",
"value": 0.11112
},
{
"item": "createpropulsion:platinum_sheet",
"value": 1.0
},
{
"item": "createpropulsion:platinum_block",
"value": 9.0
},
{
"tag": "c:ingots/platinum",
"value": 1.0
},
{
"tag": "c:nuggets/platinum",
"value": 0.11112
},
{
"tag": "c:storage_blocks/platinum",
"value": 9.0
}
]
}

View File

@ -0,0 +1,6 @@
{
"pack": {
"description": "Create Propulsion Materials for Truly Modular",
"pack_format": 48
}
}

View File

@ -1,9 +1,23 @@
{
"key": "miapi:crystal/amethyst",
"parent": "miapi:crystal/amethyst",
"data": {
"items": [
{
"item": "iceandfire:amethyst_gem",
"item": "minecraft:amethyst_shard",
"value": 0.25
},
{
"item": "minecraft:amethyst_cluster",
"value": 0.25
},
{
"item": "minecraft:amethyst_block",
"value": 1.0
},
{
"item": "iceandfire:amythest_gem",
"value": 0.25
}
]
}
}

View File

@ -1,5 +1,5 @@
{
"translation": "Arcane Steel ",
"translation": "Arcane Alloy ",
"groups": [
"metal"
],
@ -36,8 +36,44 @@
"metallic"
],
"properties": {
"handheld": {
"default": {
"attributes": [
{
"attribute": "irons_spellbooks:cooldown_reduction",
"value": "0.1",
"operation": "*",
"slot": "mainhand"
},
{
"attribute": "irons_spellbooks:cooldown_reduction",
"value": "0.05",
"operation": "*",
"slot": "head"
},
{
"attribute": "irons_spellbooks:cooldown_reduction",
"value": "0.04",
"operation": "*",
"slot": "chest"
},
{
"attribute": "irons_spellbooks:cooldown_reduction",
"value": "0.03",
"operation": "*",
"slot": "legs"
},
{
"attribute": "irons_spellbooks:cooldown_reduction",
"value": "0.03",
"operation": "*",
"slot": "feet"
},
{
"attribute": "irons_spellbooks:cooldown_reduction",
"value": "0.03",
"operation": "*",
"slot": "offhand"
},
{
"attribute": "irons_spellbooks:spell_power",
"value": "0.05",

View File

@ -43,6 +43,10 @@
{
"item": "pastel:paltaeria_gem",
"value": 1.0
},
{
"item": "pastel:paltaeria_fragments",
"value": 0.125
}
]
}

View File

@ -43,6 +43,10 @@
{
"item": "pastel:stratine_gem",
"value": 1.0
},
{
"item": "pastel:stratine_fragments",
"value": 0.125
}
]
}

View File

@ -6,19 +6,21 @@ the mod that adds it, so a material always looks like the thing it is made of.
python3 tools/generate_materials.py --jars ~/mods
The jar directory needs one jar per source mod (Mekanism, Create, Create: The
Air War, Create: Cosmonautics, Ice and Fire). Generated files are committed, so
this only needs re-running when a material definition or a source texture
changes.
Point --jars at the pack's mods folder. Generated files are committed, so this
only needs re-running when a material definition or a source texture changes.
Every item id in materials.py is checked against the jars; a typo fails the run
rather than shipping a material that silently refuses to load.
rather than shipping a material that silently refuses to load. A mod whose jar
is not in the folder is skipped instead, keeping its committed files, so the set
of jars does not have to be complete to regenerate the rest.
Only the material folders are rewritten. The hand-written module JSON that lives
in the same packs is left alone.
"""
import argparse
import json
import re
import shutil
import sys
import zipfile
@ -31,6 +33,7 @@ sys.path.insert(0, str(Path(__file__).parent))
from materials import EXTENSIONS, MATERIALS, PACKS # noqa: E402
NAMESPACE = "cmmodular"
VANILLA = "minecraft:"
PACK_FORMAT = 48 # 1.21.1
# Brightness levels of MIAPI's grayscale templates. Each gets a colour; values
@ -56,14 +59,24 @@ class Jars:
def __init__(self, folder: Path):
self.zips = []
self.by_namespace = {}
# Addons ship a handful of files under the namespace of the mod they add
# to - Applied Mekanistics into ae2, half the Create addons into create -
# so a namespace goes to whichever jar has the most of it, which is the
# mod that actually owns it rather than whichever sorted first.
counts = {}
for path in sorted(folder.glob("*.jar")):
zf = zipfile.ZipFile(path)
names = zf.namelist()
self.zips.append((path.name, zf, set(names)))
here = {}
for name in names:
m = re.match(r"assets/([^/]+)/", name)
if m:
self.by_namespace.setdefault(m.group(1), (path.name, zf, set(names)))
here[m.group(1)] = here.get(m.group(1), 0) + 1
for namespace, count in here.items():
if count > counts.get(namespace, 0):
counts[namespace] = count
self.by_namespace[namespace] = (path.name, zf, set(names))
def _for(self, namespace):
hit = self.by_namespace.get(namespace)
@ -199,6 +212,38 @@ def check_items(spec, jars: Jars):
return problems
def check_extension_items(ext, jars: Jars):
"""Same check for extensions - they name items too, and used to escape it."""
problems = []
for entry in ext["data"].get("data", {}).get("items", []):
item = entry.get("item", "")
if item.startswith(VANILLA):
continue
if item and not jars.has_item(item):
problems.append(f"{ext['path']}: no such item {item}")
return problems
def materials_path(out: Path, spec) -> Path:
"""Where a material or extension is written."""
path = spec["path"] if "path" in spec else f"{spec['group']}/{spec['name']}"
return out / spec["pack"] / "data" / NAMESPACE / "miapi" / "materials" / f"{path}.json"
def namespaces_of(spec):
"""Every namespace a material or extension needs a jar for.
Vanilla is not one of them - there is no Minecraft jar in the folder, and
the `minecraft` namespace a mod's resource overrides create is not it.
"""
entries = (spec["items"] if "items" in spec
else spec["data"].get("data", {}).get("items", []))
names = {entry["item"].split(":", 1)[0] for entry in entries if "item" in entry}
if "palette_from" in spec:
names.add(spec["palette_from"].split(":", 1)[0])
return names - {VANILLA.rstrip(":")}
def main():
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
@ -212,20 +257,45 @@ def main():
jars = Jars(args.jars)
print(f"namespaces found: {', '.join(sorted(jars.by_namespace))}\n")
# A mod whose jar is not in the folder is skipped rather than fatal, and its
# committed file is left alone: the pack this addon follows drops and adds
# mods, and losing every material of a mod you no longer have - or being
# unable to regenerate anything without a complete set of jars - is worse
# than regenerating what you can. Anything the jars *can* be checked against
# is still checked, so a typo in a mod you do have still fails the run.
def present(spec):
return namespaces_of(spec) <= set(jars.by_namespace)
materials = [spec for spec in MATERIALS if present(spec)]
extensions = [ext for ext in EXTENSIONS if present(ext)]
skipped = [spec for spec in MATERIALS if not present(spec)]
skipped += [ext for ext in EXTENSIONS if not present(ext)]
if skipped:
missing = sorted({ns for spec in skipped for ns in namespaces_of(spec)
if ns not in jars.by_namespace})
print(f"no jar for {', '.join(missing)} - leaving "
f"{len(skipped)} committed file(s) as they are\n")
problems = []
for spec in MATERIALS:
for spec in materials:
problems += check_items(spec, jars)
for ext in extensions:
problems += check_extension_items(ext, jars)
if problems:
print("aborting, item ids do not check out:", file=sys.stderr)
for p in problems:
print(" " + p, file=sys.stderr)
return 1
# Wipe first. A material that moves between packs would otherwise leave a
# stale copy behind, and two files claiming the same material id is a silent
# mess to debug.
if args.out.exists():
shutil.rmtree(args.out)
# Clear out the material files first. A material that moves between packs
# would otherwise leave a stale copy behind, and two files claiming the same
# material id is a silent mess to debug. Only the generated material folders
# go: the packs also hold hand-written module JSON, and the files of the
# skipped mods above have to survive a run that cannot rebuild them.
keep = {materials_path(args.out, spec) for spec in skipped}
for stale in sorted(args.out.glob(f"*/data/{NAMESPACE}/miapi/materials/**/*.json")):
if stale not in keep:
stale.unlink()
written = 0
for pack_id, pack in PACKS.items():
@ -239,23 +309,21 @@ def main():
root.mkdir(parents=True, exist_ok=True)
(root / "pack.mcmeta").write_text(json.dumps(mcmeta, indent=2) + "\n")
for spec in MATERIALS:
for spec in materials:
material = build_material(spec, jars)
path = (args.out / spec["pack"] / "data" / NAMESPACE / "miapi" / "materials"
/ spec["group"] / f"{spec['name']}.json")
path = materials_path(args.out, spec)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(material, indent=2) + "\n")
written += 1
swatch = " ".join(material["color_palette"]["colors"][str(s)] for s in STOPS)
print(f" {spec['pack']:<13} {NAMESPACE}:{spec['group']}/{spec['name']:<28} {swatch}")
print(f" {spec['pack']:<16} {NAMESPACE}:{spec['group']}/{spec['name']:<28} {swatch}")
for ext in EXTENSIONS:
path = (args.out / ext["pack"] / "data" / NAMESPACE / "miapi" / "materials"
/ f"{ext['path']}.json")
for ext in extensions:
path = materials_path(args.out, ext)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(ext["data"], indent=2) + "\n")
written += 1
print(f" {ext['pack']:<13} extends {ext['data']['key']}")
print(f" {ext['pack']:<16} extends {ext['data']['parent']}")
print(f"\n{written} files written to {args.out}")
return 0

View File

@ -67,6 +67,17 @@ PACKS = {
"crystal_chronicles": {"mod": "crystal_chronicles",
"name": "Crystal Chronicles Materials"},
"pastel": {"mod": "pastel", "name": "Pastel Materials"},
"createpropulsion": {"mod": "createpropulsion",
"name": "Create Propulsion Materials"},
"aeroengineering": {"mod": "aeroengineering", "name": "AeroEngine Materials"},
"create_wizardry": {"mod": "create_wizardry", "name": "Create Wizardry Materials"},
# Modules rather than materials, but the same problem: a module that names
# Arsenal's or Armory's slots - or inherits one of their modules - is only
# useful when that mod is installed. Nothing is generated into these two;
# they hold the hand-written module JSON under packs/arsenal and
# packs/armory, and are listed here so their pack.mcmeta is written.
"arsenal": {"mod": "tm_arsenal", "name": "Arsenal Modules"},
"armory": {"mod": "tm_armory", "name": "Armory Modules"},
}
# Attribute this mod registers itself, so a material can grant something the
@ -341,6 +352,38 @@ MATERIALS = [
enchantability=12, mining_speed=10, toughness=3, armor_durability=36,
armor_toughness=2.0, properties={"default": {"fire_proof": True}}),
# Create Propulsion: Simulated's own ore, and the only platinum in the pack.
M("platinum", "createpropulsion", "metal", "Platinum",
"createpropulsion:platinum_ingot",
[{"item": "createpropulsion:platinum_ingot", "value": 1.0},
{"item": "createpropulsion:platinum_nugget", "value": NUGGET},
{"item": "createpropulsion:platinum_sheet", "value": 1.0},
{"item": "createpropulsion:platinum_block", "value": BLOCK}],
tier=4, hardness=5.0, density=5.7, flexibility=3, durability=680,
enchantability=24, mining_speed=7, toughness=2, armor_durability=24,
armor_toughness=1.0, textures=("metallic", "shiny"),
properties={"default": {"fire_proof": True},
"handheld": speed(-0.08)}),
# Real platinum: nearly as dense as osmium, but soft, so it swings slow
# and hits no harder than steel. What it is actually for is the precious
# metal's bargain - gold-grade enchantability without gold's fragility -
# and it melts at 1768C, hence the heat proofing.
# -------------------------------------------------------------- AeroEngine
M("aviation_alloy", "aeroengineering", "metal", "Aviation Alloy",
"aeroengineering:aviation_alloy_ingot",
[{"item": "aeroengineering:aviation_alloy_ingot", "value": 1.0},
{"item": "aeroengineering:aviation_alloy_sheet", "value": 1.0}],
tier=5, hardness=6.6, density=2.4, flexibility=4, durability=1650,
enchantability=14, mining_speed=10, toughness=3, armor_durability=35,
armor_toughness=2.0, textures=("metallic", "bright"),
properties={"default": {"fire_proof": True},
"handheld": speed(0.08)}),
# Iron superheated with netherite powder: a turbine superalloy. Light and
# heat-proof like titanium, harder and more flexible, worse to enchant -
# it sits between titanium and titanium alloy, which is where its recipe
# sits too.
# ------------------------------------------------------- Create: The Air War
M("sulfur", "air_war", "crystal", "Sulfur", "create_the_air_wars:raw_sulfur",
[{"item": "create_the_air_wars:raw_sulfur", "value": 1.0},
@ -589,14 +632,44 @@ MATERIALS = [
properties={"handheld": speed(0.12)}),
# Light enough to swing fast and enchant well, in the Tolkien sense.
M("arcane_metal", "irons_spellbooks", "metal", "Arcane Steel",
# "Arcane Alloy" rather than Arcane Steel: Create: Wizardry ships a resource
# pack that renames the ingot, and that is what the item is called in game.
M("arcane_metal", "irons_spellbooks", "metal", "Arcane Alloy",
"irons_spellbooks:arcane_ingot",
[{"item": "irons_spellbooks:arcane_ingot", "value": 1.0}],
tier=5, hardness=6.2, density=2.8, flexibility=3, durability=1700,
enchantability=26, mining_speed=9, toughness=3, armor_durability=34,
armor_toughness=2.0,
properties={"handheld": {"attributes": [
attribute("irons_spellbooks:spell_power", 0.05, "mainhand", "*")]}}),
# The one thing arcane alloy is for: spells come back faster. Cooldown
# reduction is the whole reason to build out of it rather than out of
# mithril, so it is deliberately the strongest effect on any metal here.
#
# Under `default`, not `handheld`, so armour plated in it counts too - a
# caster wants the set, not just the sword. Per-slot rather than one
# `armor`-wide modifier, for the same reasons as arcane essence: a helmet
# can be worth more than boots, and a weapon reads as a held bonus
# instead of claiming something "when worn".
#
# Iron's Spells treats the attribute as a factor on a base of 1, hence the
# `*` operation - a flat `+` would be added to a base of zero and do
# nothing - and spends it as `cooldown * (2 - value)`. So +0.10 is 10% off
# the wait, and +1.00 in total would be instant casting.
#
# These stack per module, and that is what sets the numbers. A weapon is
# about four modules, so one built entirely of arcane alloy is 40% - well
# past the 25% of Iron's Spells' own best staff, which is the point. The
# armour values are smaller because a full set is a dozen modules: all of
# it in arcane alloy lands near 45%, so even a player wearing the set and
# holding the staff stays under the 100% the formula bottoms out at.
properties={"default": {"attributes": [
attribute("irons_spellbooks:cooldown_reduction", 0.10, "mainhand", "*"),
attribute("irons_spellbooks:cooldown_reduction", 0.05, "head", "*"),
attribute("irons_spellbooks:cooldown_reduction", 0.04, "chest", "*"),
attribute("irons_spellbooks:cooldown_reduction", 0.03, "legs", "*"),
attribute("irons_spellbooks:cooldown_reduction", 0.03, "feet", "*"),
attribute("irons_spellbooks:cooldown_reduction", 0.03, "offhand", "*"),
attribute("irons_spellbooks:spell_power", 0.05, "mainhand", "*"),
]}}),
# ------------------------------------------------- Immersive Engineering
# Steel, lead, uranium and silver are not here - Immersive Engineering adds
@ -1064,14 +1137,26 @@ _GEM_BAND = {
}
# Nuggets, in effect: Pastel's two float-gems break into eight fragments on an
# anvil and nine come back out of a floatblock, so a fragment is an eighth of a
# gem and worth accepting as one.
GEM_FRAGMENTS = {
"paltaeria_gem": ("pastel:paltaeria_fragments", 0.125),
"stratine_gem": ("pastel:stratine_fragments", 0.125),
}
def _gem_family(pack, namespace, table):
out = []
for item, (tier, props) in table.items():
label = item.replace("_shard", "").replace("_crystal", "").replace("_gem", "")
items = [{"item": f"{namespace}:{item}", "value": 1.0}]
if item in GEM_FRAGMENTS:
fragment, value = GEM_FRAGMENTS[item]
items.append({"item": fragment, "value": value})
out.append(M(
item, pack, "crystal", label.replace("_", " ").title(),
f"{namespace}:{item}",
[{"item": f"{namespace}:{item}", "value": 1.0}],
f"{namespace}:{item}", items,
tier=tier, groups=("crystal", "gem"), textures=("crystal", "bright"),
properties={"default": props} if props else None,
**_GEM_BAND[tier]))
@ -1100,21 +1185,50 @@ for _material in MATERIALS:
g for g in GEM_SOCKET_GROUPS if g not in _material["groups"]]
# Extensions add data to a material that already exists rather than replacing it.
# Ice and Fire's amethyst is the same stone MIAPI already ships, so it just
# becomes another way to craft it.
# Extensions edit a material that already exists instead of adding a new one.
# The file goes in the materials folder like any other and is told apart by its
# shape: `parent` names the material it edits and `data` is merged onto a copy
# of it - the same form MIAPI's own stained glass uses to vary plain glass.
#
# The merge overwrites whole fields rather than appending to them, so an
# extension that adds an ingredient has to repeat the ones it wants to keep.
EXTENSIONS = [
# Ice and Fire's amethyst is the same stone MIAPI already ships, so it just
# becomes another way to craft it. The item id really is spelled
# "amythest_gem" - that typo is in Ice and Fire itself.
{
"pack": "iceandfire",
"path": "crystal/amethyst",
"data": {
"key": "miapi:crystal/amethyst",
"items": [{"item": "iceandfire:amethyst_gem", "value": 1.0}],
"parent": "miapi:crystal/amethyst",
"data": {
"items": [
{"item": "minecraft:amethyst_shard", "value": 0.25},
{"item": "minecraft:amethyst_cluster", "value": 0.25},
{"item": "minecraft:amethyst_block", "value": 1.0},
{"item": "iceandfire:amythest_gem", "value": 0.25},
],
},
},
},
]
# Forms of a material that a *different* mod adds. Create: Wizardry presses
# Iron's Spells' arcane ingots into sheets and stacks them into blocks, and adds
# the mithril nugget the base mod never had, so all of those are the same metal.
#
# They cannot simply be listed on the material: it lives in the Iron's Spells
# pack, and an item id belonging to a mod that is not installed stops the whole
# material from loading. An extension in Create: Wizardry's own pack is only
# read when that mod is there.
EXTRA_ITEM_FORMS = {
"arcane_metal": [("create_wizardry:arcane_sheet", 1.0),
("create_wizardry:arcane_block", BLOCK)],
"mithril": [("create_wizardry:mithril_nugget", NUGGET)],
}
# ------------------------------------------------- identical materials, one entry
# Several of these metals and gems are added by more than one mod. Every
# variant should behave as the same material rather than as a near-duplicate,
@ -1143,7 +1257,7 @@ COMMON_TAGS = {
"energetic_alloy": "energetic_alloy", "vibrant_alloy": "vibrant_alloy",
"conductive_alloy": "conductive_alloy", "pulsating_alloy": "pulsating_alloy",
"redstone_alloy": "redstone_alloy", "soularium": "soularium",
"mithril": "mithril",
"mithril": "mithril", "platinum": "platinum",
}
COMMON_GEM_TAGS = {
@ -1178,3 +1292,30 @@ def _accept_every_provider():
_accept_every_provider()
def _extend_with_extra_forms(pack="create_wizardry"):
"""Turn EXTRA_ITEM_FORMS into material extensions.
Built after the common tags are appended, and out of the material's own item
list, because the merge replaces the ingredient list rather than adding to
it - everything the material already accepted has to be carried over.
"""
by_name = {material["name"]: material for material in MATERIALS}
for name, extras in EXTRA_ITEM_FORMS.items():
base = by_name[name]
path = f"{base['group']}/{name}"
EXTENSIONS.append({
"pack": pack,
"path": path,
"data": {
"parent": f"cmmodular:{path}",
"data": {
"items": base["items"] + [{"item": item, "value": value}
for item, value in extras],
},
},
})
_extend_with_extra_forms()