package codechicken.microblock; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import net.minecraft.block.Block.SoundType; import net.minecraft.entity.Entity; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.util.IIcon; import net.minecraft.util.MovingObjectPosition; import codechicken.lib.data.MCDataInput; import codechicken.lib.data.MCDataOutput; import codechicken.lib.packet.PacketCustom; import codechicken.lib.vec.Cuboid6; import codechicken.lib.vec.Vector3; import codechicken.microblock.handler.MicroblockProxy; import codechicken.multipart.IDWriter; import codechicken.multipart.MultiPartRegistry; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import scala.Tuple2; public final class MicroMaterialRegistry { /** Interface for defining a micro material. */ public interface IMicroMaterial { /** The icon to be used for breaking particles on side. */ @SideOnly(Side.CLIENT) IIcon getBreakingIcon(int side); /** * Supported material extension callback to load icons from the underlying block/etc., invoked by FMP on the * client. */ @SideOnly(Side.CLIENT) default void loadIcons() {} /** * This function must configure and use the CCRenderState pipeline to draw from the current vertex source. * CCRenderState.lightMatrix will be set. * * @param pass The current render pass, -1 for inventory rendering * @param bounds The cuboid bounds of the face being rendered */ @SideOnly(Side.CLIENT) void renderMicroFace(Vector3 pos, int pass, Cuboid6 bounds); /** Get the render pass for which this material renders in. */ @SideOnly(Side.CLIENT) default boolean canRenderInPass(int pass) { return pass == 0; } /** Return true if this material is not opaque (glass, ice). */ boolean isTransparent(); /** Return the light level emitted by this material (glowstone). */ int getLightValue(); /** Return the strength of this material. */ float getStrength(EntityPlayer player); /** Return the localised name of this material (normally the block name). */ String getLocalizedName(); /** Get the item that this material is cut from (full block to slabs). */ ItemStack getItem(); /** Get the strength of saw requried to cut this material. */ int getCutterStrength(); /** Get the breaking/walking sound. */ SoundType getSound(); /** Return true if this material is solid and opaque (can run wires on etc). */ default boolean isSolid() { return !isTransparent(); } /** Get the explosion resistance of this part to an explosion caused by entity. */ float explosionResistance(Entity entity); } /** * Interface for overriding the default micro placement highlight handler to show the effect of placement on a * certain block/part. */ public interface IMicroHighlightRenderer { /** Return true if a custom highlight was rendered and the default should be skipped. */ boolean renderHighlight(EntityPlayer player, MovingObjectPosition hit, CommonMicroClass mcrClass, int size, int material); } private static final Map typeMap = new HashMap<>(); private static final Map nameMap = new HashMap<>(); private static Tuple2[] idMap; private static final IDWriter idWriter = new IDWriter(); private static final List highlightRenderers = new ArrayList<>(); private static int maxCuttingStrength; private static final Map remap = new HashMap<>(); private static int missingId; private MicroMaterialRegistry() {} /** Register a micro material with unique identifier name. */ public static void registerMaterial(IMicroMaterial material, String name) { if (MultiPartRegistry.loaded()) { throw new IllegalStateException("You must register your materials in the init methods."); } if (typeMap.containsKey(name)) { MicroblockProxy.logger().error("Material with id " + name + " is already registered."); return; } typeMap.put(name, material); } /** Replace a micro material with unique identifier name. */ public static void replaceMaterial(IMicroMaterial material, String name) { if (MultiPartRegistry.loaded()) { throw new IllegalStateException("You must register your materials in the init methods."); } if (typeMap.remove(name) == null) { MicroblockProxy.logger().error("Material with id " + name + " was not registered."); } MicroblockProxy.logger().debug("Replaced micro material: " + name); typeMap.put(name, material); } /** Registers a highlight renderer. */ public static void registerHighlightRenderer(IMicroHighlightRenderer handler) { highlightRenderers.add(handler); } public static void remapName(String oldName, String newName) { remap.put(oldName, newName); } /** * Internal FMP lifecycle step that rebuilds material IDs. Consumers register materials during initialization and * enumerate the initialized map through {@link #materialCount()}; do not rebuild IDs from consumer code. */ @SuppressWarnings("unchecked") public static void setupIDMap() { List> entries = new ArrayList<>(typeMap.entrySet()); entries.sort(Comparator.comparing(Map.Entry::getKey)); idMap = new Tuple2[entries.size()]; for (int i = 0; i < entries.size(); i++) { idMap[i] = new Tuple2<>(entries.get(i).getKey(), entries.get(i).getValue()); } idWriter.setMax(idMap.length); nameMap.clear(); for (int i = 0; i < idMap.length; i++) { nameMap.put(idMap[i]._1(), i); } Integer missing = nameMap.get(MissingMicroMaterial.key()); if (missing == null) { throw new IllegalStateException( "MissingMicroMaterial is not registered; the placeholder must never fall back to id 0."); } missingId = missing; } public static int getMissingId() { return missingId; } /** Internal FMP lifecycle calculation; consumers read {@link #getMaxCuttingStrength()} after initialization. */ public static void calcMaxCuttingStrength() { int max = Integer.MIN_VALUE; boolean found = false; for (Iterator it = Item.itemRegistry.iterator(); it.hasNext();) { Object next = it.next(); if (next instanceof Saw) { max = Math.max(max, ((Saw) next).getMaxCuttingStrength()); found = true; } } if (!found) { // Matches the reference, where max on an empty collection throws. throw new UnsupportedOperationException("empty.max"); } maxCuttingStrength = max; } /** * Internal FMP client texture-lifecycle dispatcher; do not call directly from consumers. The per-material * {@link IMicroMaterial#loadIcons()} callback remains a supported extension hook. */ public static void loadIcons() { if (idMap != null) { for (Tuple2 entry : idMap) { entry._2().loadIcons(); } } } public static int getMaxCuttingStrength() { return maxCuttingStrength; } /** * Internal FMP handshake writer for the entire material map. Consumer part packets use * {@link #writeMaterialID(MCDataOutput, int)} for an individual material ID instead. */ public static void writeIDMap(PacketCustom packet) { packet.writeInt(idMap.length); for (Tuple2 entry : idMap) { packet.writeString(entry._1()); } } /** * Internal FMP handshake reader: replaces this client's ID map with the server's, returning missing material names. * Consumer part packets use {@link #readMaterialID(MCDataInput)} for an individual material ID instead. */ @SuppressWarnings("unchecked") public static List readIDMap(PacketCustom packet) { int k = packet.readInt(); idWriter.setMax(k); idMap = new Tuple2[k]; nameMap.clear(); List missing = new ArrayList<>(); for (int i = 0; i < k; i++) { String s = packet.readString(); IMicroMaterial v = typeMap.get(s); if (v == null) { missing.add(s); } else { idMap[i] = new Tuple2<>(s, v); nameMap.put(s, i); } } return missing; } public static void writeMaterialID(MCDataOutput data, int id) { idWriter.write(data, id); } public static int readMaterialID(MCDataInput data) { return idWriter.read(data); } /** * Returns the persistent name at a numeric ID in the active material map. IDs may change when the map is rebuilt or * synchronized with a server; store material names rather than numeric IDs in persistent data. * * @param id an ID from zero (inclusive) to {@link #materialCount()} (exclusive) * @throws ArrayIndexOutOfBoundsException if the ID is outside the active map * @throws NullPointerException if the map is uninitialized or the ID is unresolved after a failed * handshake */ public static String materialName(int id) { return idMap[id]._1(); } public static int materialID(String name) { Integer id = nameMap.get(remapped(name)); if (id == null) { MicroblockProxy.logger().error("Missing mapping for part with ID: " + name); return missingId; } return id; } public static IMicroMaterial getMaterial(String name) { return typeMap.get(remapped(name)); } /** * Returns the registered material instance at an ID in the active material map; no copy is made. * * @param id an ID from zero (inclusive) to {@link #materialCount()} (exclusive) * @throws ArrayIndexOutOfBoundsException if the ID is outside the active map * @throws NullPointerException if the map is uninitialized or the ID is unresolved after a failed * handshake */ public static IMicroMaterial getMaterial(int id) { return idMap[id]._2(); } /** * Returns the number of numeric ID slots in the active material map, including the missing-material placeholder * when present. Enumerate IDs from zero (inclusive) to this count (exclusive) with {@link #materialName(int)} and * {@link #getMaterial(int)}. * * FMP builds the map during post-initialization and before server startup. A multiplayer handshake replaces the * client map with server ID order. Enumerate on the game/lifecycle thread after initialization; multiplayer clients * must also finish the handshake successfully. The map must not be rebuilt during enumeration. Do not cache numeric * IDs across map replacements. A failed handshake can leave unresolved slots, which this count includes; it is not * a readiness check. * * @return the active map length, or zero for an initialized empty map * @throws IllegalStateException if FMP has not initialized the material ID map */ public static int materialCount() { if (idMap == null) { throw new IllegalStateException("The material ID map has not been initialized."); } return idMap.length; } /** * Returns the legacy live backing array, or null before ID-map initialization. A map replacement leaves earlier * array references pointing at the old map; unresolved IDs after a failed handshake contain null. * * @deprecated For enumeration, use {@link #materialCount()}, {@link #materialName(int)} and * {@link #getMaterial(int)}. The Scala array descriptor is retained for existing consumers. */ @Deprecated public static Tuple2[] getIdMap() { return idMap; } public static boolean renderHighlight(EntityPlayer player, MovingObjectPosition hit, CommonMicroClass mcrClass, int size, int material) { for (IMicroHighlightRenderer renderer : highlightRenderers) { if (renderer.renderHighlight(player, hit, mcrClass, size, material)) { return true; } } MicroblockRender.renderHighlight(player, hit, mcrClass, size, material); return true; } private static String remapped(String name) { String replacement = remap.get(name); return replacement == null ? name : replacement; } }