Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/main/java/com/ruling_0/materiallib/CommandMatInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,21 @@ private static String formatValue(Material material, Property<?> property) {
if (TINT_PROPERTIES.contains(property)) {
return String.format("0x%08X", value);
}
if (property == StandardProperties.LAYER_TINTS) {
return hexList((List<?>) value);
}
return String.valueOf(value);
}

/// The layer tints as AARRGGBB hex, in layer order.
private static String hexList(List<?> tints) {
List<String> hex = new ArrayList<>(tints.size());
for (Object tint : tints) {
hex.add(String.format("0x%08X", tint));
}
return hex.toString();
}

private static String type(Shape shape) {
if (shape instanceof ShapeFluidInContainer) return "fluid container";
if (shape instanceof ShapeBlock) return "block";
Expand Down
32 changes: 23 additions & 9 deletions src/main/java/com/ruling_0/materiallib/api/IconSet.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
import cpw.mods.fml.relauncher.SideOnly;

/// A named per-material icon binding on one texture atlas, with no backing item or block: the icon resolution of a
/// shape (resource-pack overrides, texture-set chain, fallbacks, unification alternatives, `_OVERLAY` convention) for
/// art a mod composites in its own renderers, e.g. tool parts drawn by the material read from a stack's NBT, or an
/// extra face icon of a block the mod already owns. The set's name is the `<shape>` its art is filed under inside
/// each texture set, i.e. the `<shape>` of `materials/<set>/<shape>.png`.
/// shape (resource-pack overrides, texture-set chain, fallbacks, unification alternatives, layer stack) for art a
/// mod composites in its own renderers, e.g. tool parts drawn by the material read from a stack's NBT, or an extra
/// face icon of a block the mod already owns. The set's name is the `<shape>` its art is filed under inside each
/// texture set, i.e. the `<shape>` of `materials/<set>/<shape>.png`.
///
/// Created through [MaterialLibClient#newIconSet] before the first texture stitch. Icons bind for every registered
/// material -- an icon set has no served set, since no material generates one -- when the chosen atlas stitches, and
Expand Down Expand Up @@ -47,15 +47,25 @@ public enum Atlas {
this.icons = new ShapeIcons(atlas == Atlas.ITEMS);
}

/// The icon bound for `material`, or the transparent placeholder when none resolved. Valid only once the
/// atlas has stitched.
/// The first layer of `material`'s icon stack, or the transparent placeholder when none resolved. Valid only
/// once the atlas has stitched.
public IIcon getIcon(Material material) {
return icons.get(material.getIndex());
}

/// The `_OVERLAY` icon bound for `material`, or null when its resolved texture set has none.
public IIcon getOverlayIcon(Material material) {
return icons.getOverlayOrNull(material.getIndex());
/// The number of icon layers bound for `material`; see [TextureSet].
public int getLayerCount(Material material) {
return icons.layerCount(material.getIndex());
}

/// The icon at `layer` of `material`'s stack, or the transparent placeholder outside the stack's bounds.
public IIcon getLayerIcon(Material material, int layer) {
return icons.layer(material.getIndex(), layer);
}

/// The ARGB tint `material`'s layer `layer` takes; see [ShapeItem#getMaterialLayerColor].
public int getLayerColor(Material material, int layer) {
return icons.layerColor(material, layer);
}

/// Whether `material`'s icon bound from the resource-pack override location; see [ShapeItem#hasOverrideIcon].
Expand All @@ -67,6 +77,10 @@ void bind(IIconRegister register, Material[] materials) {
icons.bind(register, materials, name);
}

void bindPlaceholder(IIconRegister register) {
icons.bindPlaceholder(register);
}

int atlasType() {
return atlas.textureType;
}
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/ruling_0/materiallib/api/IconSetBinder.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,14 @@ public final class IconSetBinder {
@SubscribeEvent
public void onTextureStitch(TextureStitchEvent.Pre event) {
int textureType = event.map.getTextureType();
boolean defer = MaterialLibClient.deferIconBinding();
Material[] materials = null;
for (IconSet set : MaterialLibClient.getIconSets()) {
if (set.atlasType() != textureType) continue;
if (defer) {
set.bindPlaceholder(event.map);
continue;
}
if (materials == null) materials = registeredMaterials();
set.bind(event.map, materials);
}
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/ruling_0/materiallib/api/MaterialBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ public MaterialBuilder setTint(int tint) {
return setProperty(StandardProperties.TINT, tint);
}

/// Sets [StandardProperties#LAYER_TINTS], the ARGB tints of the shape textures' numbered icon layers, the
/// first tinting `_LAYER1`.
public MaterialBuilder setLayerTints(int... tints) {
Integer[] boxed = new Integer[tints.length];
for (int i = 0; i < tints.length; i++) {
boxed[i] = tints[i];
}
return setProperty(StandardProperties.LAYER_TINTS, List.of(boxed));
}

/// Sets [StandardProperties#FLUID_TINT], the ARGB tint applied to a fluid shape's fill icon in place of
/// [StandardProperties#TINT].
public MaterialBuilder setFluidTint(int fluidTint) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import net.minecraftforge.client.IItemRenderer;

import cpw.mods.fml.common.Loader;
import cpw.mods.fml.common.LoaderState;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
Expand Down Expand Up @@ -36,6 +38,12 @@ public static IconSet newIconSet(String modid, String name, IconSet.Atlas atlas)

static List<IconSet> getIconSets() { return iconSets; }

/// Whether atlas stitches bind only the placeholder icon: true until FML finishes mod loading, since the
/// mid-startup resource refresh stitches a temporary atlas that the load-complete reload replaces.
static boolean deferIconBinding() {
return !Loader.instance().hasReachedState(LoaderState.AVAILABLE);
}

/// Renders every item shape of `material` through `renderer`. Call from a mod's client proxy.
public static void setItemRenderer(Material material, IItemRenderer renderer) {
Objects.requireNonNull(material, "material must not be null");
Expand Down
41 changes: 35 additions & 6 deletions src/main/java/com/ruling_0/materiallib/api/MaterialTints.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.ruling_0.materiallib.api;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

Expand All @@ -9,13 +10,15 @@
/// Reads the standard ARGB tint properties through [ColorResource], so a resource pack retints one material with a
/// lang entry; see [StandardProperties] for the pack-facing key and value format.
///
/// One [ColorResource] per unified material and property is cached here for the life of the game. GTNHLib tracks its
/// instances weakly and refreshes only the reachable ones when resource packs reload. Angelica meshes chunks on
/// several threads at once (see [ShapeBlockRenderingHandler]), so the cache is concurrent. A dedicated server carries
/// no lang files, so every read there returns the value the material declares.
/// One [ColorResource] per unified material and property, and one per unified material and numbered icon layer, is
/// cached here for the life of the game. GTNHLib tracks its instances weakly and refreshes only the reachable ones
/// when resource packs reload. Angelica meshes chunks on several threads at once (see [ShapeBlockRenderingHandler]),
/// so the cache is concurrent. A dedicated server carries no lang files, so every read there returns the value the
/// material declares.
final class MaterialTints {

private static final Map<Property<Integer>, Map<Material, ColorResource>> RESOURCES = new ConcurrentHashMap<>();
private static final Map<Material, Map<Integer, ColorResource>> LAYER_RESOURCES = new ConcurrentHashMap<>();

private MaterialTints() {}

Expand All @@ -25,13 +28,39 @@ static int color(Material material, Property<Integer> property) {
return resourceFor(material, property).getColor();
}

/// The [ColorResource] backing `property` for `material`, created on first read and shared by every declaration
/// unified onto the same material.
/// The [ColorResource] backing `property` for `material`.
static ColorResource resourceFor(Material material, Property<Integer> property) {
Map<Material, ColorResource> byMaterial = RESOURCES.computeIfAbsent(property,
ignored -> new ConcurrentHashMap<>());
return byMaterial.computeIfAbsent(material.canonical(),
canonical -> new ColorResource(MaterialLib.MODID, canonical.getName() + "." + property.getName(),
String.format("0x%08X", canonical.getProperty(property)), true));
}

/// The ARGB tint `material` resolves for its `_LAYER<layer>` icon, or opaque white where
/// [StandardProperties#LAYER_TINTS] codes no element for that layer.
static int layerColor(Material material, int layer) {
ColorResource resource = layerResourceFor(material, layer);
return resource != null ? resource.getColor() : 0xFFFFFFFF;
}

/// The [ColorResource] backing `material`'s `_LAYER<layer>` tint, or null where
/// [StandardProperties#LAYER_TINTS] codes no element for that layer.
static ColorResource layerResourceFor(Material material, int layer) {
Material canonical = material.canonical();
Integer coded = codedLayerTint(canonical, layer);
if (coded == null) return null;
Map<Integer, ColorResource> byLayer = LAYER_RESOURCES.computeIfAbsent(canonical,
ignored -> new ConcurrentHashMap<>());
return byLayer.computeIfAbsent(layer,
number -> new ColorResource(MaterialLib.MODID, canonical.getName() + ".tint." + number,
String.format("0x%08X", coded), true));
}

/// The tint `canonical` codes for its `_LAYER<layer>` icon, or null where the list stops short of it.
private static Integer codedLayerTint(Material canonical, int layer) {
List<Integer> tints = canonical.getProperty(StandardProperties.LAYER_TINTS);
if (tints == null || layer < 1 || layer > tints.size()) return null;
return tints.get(layer - 1);
}
}
82 changes: 69 additions & 13 deletions src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
/// Drops, hardness, resistance, and harvest level may be overridden per material and variant, and the harvest tool
/// class per shape, through [BlockShapeBuilder]'s behavior hooks; a hook left unset preserves the vanilla default
/// it replaces.
///
/// [ShapeBlockRenderingHandler] also composites a material whose art binds more than one icon layer (see
/// [TextureSet]). A stack's depth is known only once the atlas stitches, so [#registerBlockIcons] switches such a
/// block to that handler's render type there and never switches back.
public class ShapeBlock extends Block implements BackedShape {

private final String modid;
Expand Down Expand Up @@ -182,13 +186,30 @@ protected String iconName() {
@Override
@SideOnly(Side.CLIENT)
public void registerBlockIcons(IIconRegister register) {
if (MaterialLibClient.deferIconBinding()) {
icons.bindPlaceholder(register);
if (baseTexture != null) baseIcon = register.registerIcon(ShapeIcons.EMPTY_ICON);
return;
}
String iconName = iconName();
List<String> candidates = groupName != null ?
List.of(ShapeNaming.variantBlockName(iconName, variant), iconName) : List.of(iconName);
icons.bind(register, served.get(), candidates, this::iconPathFor);
if (baseTexture != null) {
baseIcon = registerBaseIcon(register);
}
if (renderType == 0 && hasLayeredMaterial()) {
setRenderType(ShapeBlockRenderingHandler.RENDER_ID);
}
}

/// Whether any served material bound more than one icon layer.
@SideOnly(Side.CLIENT)
private boolean hasLayeredMaterial() {
for (Material material : served.get()) {
if (icons.layerCount(material.getIndex()) > 1) return true;
}
return false;
}

/// Registers [#baseTexture] if it names an existing file, or the [ShapeIcons#EMPTY_ICON] placeholder if it
Expand All @@ -213,8 +234,8 @@ public boolean hasBaseTexture() {
return baseTexture != null;
}

/// Sets the render type [#getRenderType] reports: [ShapeBlockRenderingHandler]'s render ID for a
/// [#hasBaseTexture] composite, or the vanilla full-cube default (0).
/// Sets the render type [#getRenderType] reports: [ShapeBlockRenderingHandler]'s render ID for a composite,
/// or the vanilla full-cube default (0).
@SideOnly(Side.CLIENT)
public void setRenderType(int renderType) { this.renderType = renderType; }

Expand All @@ -227,10 +248,16 @@ IIcon baseIcon() {
return baseIcon;
}

/// The material icon bound at the given metadata; see [ShapeIcons#get].
/// The number of material icon layers bound at the given metadata; see [ShapeIcons#layerCount].
@SideOnly(Side.CLIENT)
IIcon materialIcon(int meta) {
return icons.get(meta);
int materialLayerCount(int meta) {
return icons.layerCount(meta);
}

/// The material icon layer bound at the given metadata; see [ShapeIcons#layer].
@SideOnly(Side.CLIENT)
IIcon materialLayer(int meta, int layer) {
return icons.layer(meta, layer);
}

/// The icon path to try for `material` before this shape's texture-set candidates, or null to skip straight
Expand All @@ -249,18 +276,31 @@ public IIcon getIcon(int side, int meta) {
return icons.get(meta);
}

/// The icon bound for `material` on this shape, or the transparent placeholder when none resolved. Valid only
/// after the block atlas has stitched; the caching contract of [ShapeItem#getMaterialIcon] applies.
/// The first layer of `material`'s icon stack on this shape, or the transparent placeholder when none resolved.
/// Valid only after the block atlas has stitched; the caching contract of [ShapeItem#getMaterialIcon] applies.
@SideOnly(Side.CLIENT)
public IIcon getMaterialIcon(Material material) {
return icons.get(material.getIndex());
}

/// The `_OVERLAY` icon bound for `material` on this shape, or null when its resolved texture set has none;
/// see [#getMaterialIcon] for the caching contract.
/// The number of icon layers bound for `material` on this shape; see [TextureSet].
@SideOnly(Side.CLIENT)
public int getMaterialLayerCount(Material material) {
return materialLayerCount(material.getIndex());
}

/// The icon at `layer` of `material`'s stack on this shape, or the transparent placeholder outside the stack's
/// bounds; see [#getMaterialIcon] for the caching contract.
@SideOnly(Side.CLIENT)
public IIcon getMaterialLayerIcon(Material material, int layer) {
return materialLayer(material.getIndex(), layer);
}

/// The ARGB tint `material`'s layer `layer` takes on this shape; see [ShapeItem#getMaterialLayerColor]. Layer 0
/// reports the material's [StandardProperties#TINT], not [StandardProperties#BLOCK_TINT].
@SideOnly(Side.CLIENT)
public IIcon getMaterialOverlayIcon(Material material) {
return icons.getOverlayOrNull(material.getIndex());
public int getMaterialLayerColor(Material material, int layer) {
return icons.layerColor(material, layer);
}

/// Whether `material`'s icon bound from the resource-pack override location; see [ShapeItem#hasOverrideIcon].
Expand All @@ -272,7 +312,7 @@ public boolean hasOverrideIcon(Material material) {
@Override
@SideOnly(Side.CLIENT)
public int getRenderColor(int meta) {
if (baseTexture != null) {
if (rendersComposite()) {
return 0xFFFFFF;
}
return tintFor(meta);
Expand All @@ -281,12 +321,18 @@ public int getRenderColor(int meta) {
@Override
@SideOnly(Side.CLIENT)
public int colorMultiplier(IBlockAccess world, int x, int y, int z) {
if (baseTexture != null) {
if (rendersComposite()) {
return 0xFFFFFF;
}
return tintFor(world.getBlockMetadata(x, y, z));
}

/// Whether [ShapeBlockRenderingHandler] draws this block and supplies each layer's color itself.
@SideOnly(Side.CLIENT)
private boolean rendersComposite() {
return baseTexture != null || renderType != 0;
}

/// The RGB tint of the material at the given metadata, or white when the metadata maps to no live material:
/// [StandardProperties#BLOCK_OVERLAY_TINT] for a [#hasBaseTexture] composite's overlay layer,
/// [StandardProperties#BLOCK_TINT] for a plain block, [StandardProperties#TINT] when the specific property is
Expand All @@ -303,6 +349,16 @@ int tintFor(int meta) {
return MaterialTints.color(material, tint) & 0xFFFFFF;
}

/// The RGB tint of a material icon layer above the first at the given metadata, or white when the metadata maps
/// to no live material; see [ShapeItem#getMaterialLayerColor] for the layer colors. Masked to 24 bits like
/// [#tintFor].
@SideOnly(Side.CLIENT)
int layerTint(int meta, int layer) {
Material material = MaterialRegistry.instance().getMaterialByIndex(meta);
if (material == null) return 0xFFFFFF;
return icons.layerColor(material, layer) & 0xFFFFFF;
}

@Override
public float getBlockHardness(World world, int x, int y, int z) {
Material material = behavior.hardness() != null ? materialAt(world, x, y, z) : null;
Expand Down
Loading