ctMap;
-
- public void put(Names.Clas clas, IClassTransformer ct) {
- ctMap.put(clas.clas.replace('/', '.'), ct);
- }
-
- public AClassTransformer() {
- InitNames.init();
- ctMap = new HashMap<>();
- put(Names.entityRenderer_, new ACTEntityRenderer());
- put(Names.rendererLivingE_, new ACTRendererLivingEntity());
- }
-
- @Override
- public byte[] transform(String name, String transformedName, byte[] basicClass) {
- byte[] bytecode = basicClass;
- IClassTransformer ct = ctMap.get(transformedName);
- if (ct != null) {
- bytecode = ct.transform(name, transformedName, bytecode);
- // HACK: Fix stackframes
- ClassNode node = new ClassNode();
- ClassReader reader = new ClassReader(bytecode);
- reader.accept(node, ClassReader.SKIP_FRAMES);
- ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
- node.accept(writer);
- bytecode = writer.toByteArray();
- // END HACK
- int oldLength = basicClass.length;
- int newLength = bytecode.length;
- AngelicaTweaker.LOGGER.debug(" {} (+{})", newLength, newLength - oldLength);
- }
- return bytecode;
- }
-}
diff --git a/src/main/java/com/gtnewhorizons/angelica/transform/ClassConstantPoolParser.java b/src/main/java/com/gtnewhorizons/angelica/transform/ClassConstantPoolParser.java
new file mode 100644
index 000000000..b74a90512
--- /dev/null
+++ b/src/main/java/com/gtnewhorizons/angelica/transform/ClassConstantPoolParser.java
@@ -0,0 +1,123 @@
+/***
+ * This Class is derived from the ASM ClassReader
+ *
+ * ASM: a very small and fast Java bytecode manipulation framework Copyright (c) 2000-2011 INRIA, France Telecom All
+ * rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+ * following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of
+ * conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation and/or other materials provided with the
+ * distribution. 3. Neither the name of the copyright holders nor the names of its contributors may be used to endorse
+ * or promote products derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.gtnewhorizons.angelica.transform;
+
+import org.objectweb.asm.Opcodes;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Using this class to search for a (single) String reference is > 40 times faster than parsing a class with a ClassReader +
+ * ClassNode while using way less RAM
+ */
+public class ClassConstantPoolParser {
+
+ private static final int UTF8 = 1;
+ private static final int INT = 3;
+ private static final int FLOAT = 4;
+ private static final int LONG = 5;
+ private static final int DOUBLE = 6;
+ private static final int FIELD = 9;
+ private static final int METH = 10;
+ private static final int IMETH = 11;
+ private static final int NAME_TYPE = 12;
+ private static final int HANDLE = 15;
+ private static final int INDY = 18;
+
+ private final byte[][] BYTES_TO_SEARCH;
+
+ public ClassConstantPoolParser(String... strings) {
+ BYTES_TO_SEARCH = new byte[strings.length][];
+ for (int i = 0; i < BYTES_TO_SEARCH.length; i++) {
+ BYTES_TO_SEARCH[i] = strings[i].getBytes(StandardCharsets.UTF_8);
+ }
+ }
+
+ /**
+ * Returns true if the constant pool of the class represented by this byte array contains one of the Strings we are looking
+ * for
+ */
+ public boolean find(byte[] basicClass) {
+ if (basicClass == null || basicClass.length == 0) {
+ return false;
+ }
+ // checks the class version
+ if (readShort(6, basicClass) > Opcodes.V1_8) {
+ return false;
+ }
+ // parses the constant pool
+ int n = readUnsignedShort(8, basicClass);
+ int index = 10;
+ for (int i = 1; i < n; ++i) {
+ int size;
+ switch (basicClass[index]) {
+ case FIELD:
+ case METH:
+ case IMETH:
+ case INT:
+ case FLOAT:
+ case NAME_TYPE:
+ case INDY:
+ size = 5;
+ break;
+ case LONG:
+ case DOUBLE:
+ size = 9;
+ ++i;
+ break;
+ case UTF8:
+ final int strLen = readUnsignedShort(index + 1, basicClass);
+ size = 3 + strLen;
+ label:
+ for (byte[] bytes : BYTES_TO_SEARCH) {
+ if (strLen == bytes.length) {
+ for (int j = index + 3; j < index + 3 + strLen; j++) {
+ if (basicClass[j] != bytes[j - (index + 3)]) {
+ break label;
+ }
+ }
+ return true;
+ }
+ }
+ break;
+ case HANDLE:
+ size = 4;
+ break;
+ default:
+ size = 3;
+ break;
+ }
+ index += size;
+ }
+ return false;
+ }
+
+ private static short readShort(final int index, byte[] basicClass) {
+ return (short) (((basicClass[index] & 0xFF) << 8) | (basicClass[index + 1] & 0xFF));
+ }
+
+ private static int readUnsignedShort(final int index, byte[] basicClass) {
+ return ((basicClass[index] & 0xFF) << 8) | (basicClass[index + 1] & 0xFF);
+ }
+
+}
diff --git a/src/main/java/com/gtnewhorizons/angelica/transform/InitNames.java b/src/main/java/com/gtnewhorizons/angelica/transform/InitNames.java
deleted file mode 100644
index 7fa42b22d..000000000
--- a/src/main/java/com/gtnewhorizons/angelica/transform/InitNames.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.gtnewhorizons.angelica.transform;
-
-import net.minecraft.launchwrapper.Launch;
-
-import com.gtnewhorizons.angelica.loading.AngelicaTweaker;
-
-public class InitNames {
-
- public static void init() {
- final boolean obfuscated = !(Boolean) Launch.blackboard.get("fml.deobfuscatedEnvironment");
- AngelicaTweaker.LOGGER.info("Environment obfuscated: {}", obfuscated);
- if (obfuscated) {
- new NamerSrg().setNames();
- } else {
- new NamerMcp().setNames();
- }
- }
-}
diff --git a/src/main/java/com/gtnewhorizons/angelica/transform/Namer.java b/src/main/java/com/gtnewhorizons/angelica/transform/Namer.java
deleted file mode 100644
index 1cf2735f5..000000000
--- a/src/main/java/com/gtnewhorizons/angelica/transform/Namer.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package com.gtnewhorizons.angelica.transform;
-
-import java.util.ArrayList;
-
-import com.gtnewhorizons.angelica.transform.Names.Clas;
-import com.gtnewhorizons.angelica.transform.Names.Fiel;
-import com.gtnewhorizons.angelica.transform.Names.Meth;
-
-public class Namer {
-
- ArrayList ac = new ArrayList<>();
- ArrayList af = new ArrayList<>();
- ArrayList am = new ArrayList<>();
-
- Clas c(String name) {
- Clas x = new Clas(name);
- if (ac != null) ac.add(x);
- return x;
- }
-
- Fiel f(Clas clas, String name, String desc) {
- Fiel x = new Fiel(clas, name, desc);
- if (af != null) af.add(x);
- return x;
- }
-
- Fiel f(Clas clas, Fiel fiel) {
- Fiel x = new Fiel(clas, fiel.name, fiel.desc);
- if (af != null) af.add(x);
- return x;
- }
-
- Meth m(Clas clas, String name, String desc) {
- Meth x = new Meth(clas, name, desc);
- if (am != null) am.add(x);
- return x;
- }
-
- Meth m(Clas clas, Meth meth) {
- Meth x = new Meth(clas, meth.name, meth.desc);
- if (am != null) am.add(x);
- return x;
- }
-
- public void setNames() {}
-}
diff --git a/src/main/java/com/gtnewhorizons/angelica/transform/Namer1_7_10.java b/src/main/java/com/gtnewhorizons/angelica/transform/Namer1_7_10.java
deleted file mode 100644
index 42225d8c5..000000000
--- a/src/main/java/com/gtnewhorizons/angelica/transform/Namer1_7_10.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.gtnewhorizons.angelica.transform;
-
-import static com.gtnewhorizons.angelica.transform.Names.entityLivingBase_;
-import static com.gtnewhorizons.angelica.transform.Names.entityRenderer_;
-import static com.gtnewhorizons.angelica.transform.Names.entityRenderer_renderHand;
-import static com.gtnewhorizons.angelica.transform.Names.rendererLivingE_;
-import static com.gtnewhorizons.angelica.transform.Names.rendererLivingE_doRender;
-import static com.gtnewhorizons.angelica.transform.Names.rendererLivingE_renderEquippedItems;
-
-public class Namer1_7_10 extends Namer {
-
- public void setNames() {
- setNames1_7_10();
- }
-
- public void setNames1_7_10() {
- entityRenderer_ = c("blt");
- rendererLivingE_ = c("boh");
- entityLivingBase_ = c("sv");
-
- entityRenderer_renderHand = m(entityRenderer_, "b", "(FI)V");
- rendererLivingE_doRender = m(rendererLivingE_, "a", "(" + entityLivingBase_.desc + "DDDFF)V");
- rendererLivingE_renderEquippedItems = m(rendererLivingE_, "c", "(" + entityLivingBase_.desc + "F)V");
- }
-}
diff --git a/src/main/java/com/gtnewhorizons/angelica/transform/NamerMcf.java b/src/main/java/com/gtnewhorizons/angelica/transform/NamerMcf.java
deleted file mode 100644
index 1a7f08ca2..000000000
--- a/src/main/java/com/gtnewhorizons/angelica/transform/NamerMcf.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.gtnewhorizons.angelica.transform;
-
-public class NamerMcf extends NamerMcp {
-
- public void setNames() {
- setNamesSrg();
- rename("../build/unpacked/conf/");
- }
-}
diff --git a/src/main/java/com/gtnewhorizons/angelica/transform/NamerMcp.java b/src/main/java/com/gtnewhorizons/angelica/transform/NamerMcp.java
deleted file mode 100644
index d60745e26..000000000
--- a/src/main/java/com/gtnewhorizons/angelica/transform/NamerMcp.java
+++ /dev/null
@@ -1,121 +0,0 @@
-package com.gtnewhorizons.angelica.transform;
-
-import java.io.BufferedReader;
-import java.io.FileNotFoundException;
-import java.io.FileReader;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-
-import com.gtnewhorizons.angelica.loading.AngelicaTweaker;
-import com.gtnewhorizons.angelica.transform.Names.Clas;
-import com.gtnewhorizons.angelica.transform.Names.Fiel;
-import com.gtnewhorizons.angelica.transform.Names.Meth;
-
-public class NamerMcp extends NamerSrg {
-
- public void setNames() {
- setNamesSrg();
- final String confPath = System.getProperty("net.minecraftforge.gradle.GradleStart.csvDir", "../conf") + "/";
- lookupReobfName(confPath);
- rename(confPath);
- }
-
- public void rename(String confPath) {
- Map nameMap;
- nameMap = loadNameMapCSV(confPath + "fields.csv");
- for (Fiel f : af) {
- String s = nameMap.get(f.name);
- if (s != null) {
- f.name = s;
- }
- }
- nameMap = loadNameMapCSV(confPath + "methods.csv");
- for (Meth m : am) {
- String s = nameMap.get(m.name);
- if (s != null) {
- m.name = s;
- }
- }
- }
-
- Map loadNameMapCSV(String fileName) {
- Map map = new HashMap<>();
- BufferedReader rd = null;
- try {
- rd = new BufferedReader(new FileReader(fileName));
- String line;
- rd.readLine(); // skip first line;
- while ((line = rd.readLine()) != null) {
- String[] tokens = line.split(",");
- if (tokens.length > 1) {
- map.put(tokens[0], tokens[1]);
- }
- }
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- if (rd != null) {
- try {
- rd.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- return map;
- }
-
- void lookupReobfName(String confPath) {
- Map nameMap;
- nameMap = loadReobfMap(confPath + "packaged.srg");
- for (Clas c : ac) {
- String s = nameMap.get(c.name);
- AngelicaTweaker.LOGGER.trace("C {} {}", c.name, s);
- }
- for (Fiel f : af) {
- String s = nameMap.get(f.clas + "/" + f.name);
- AngelicaTweaker.LOGGER.trace("F {} {}", f.name, s);
- }
- for (Meth m : am) {
- String s = nameMap.get(m.clas + "/" + m.name + m.desc);
- AngelicaTweaker.LOGGER.trace("M {} {}", m.name, s);
- }
- }
-
- Map loadReobfMap(String fileName) {
- Map map = new HashMap<>();
- BufferedReader rd = null;
- try {
- rd = new BufferedReader(new FileReader(fileName));
- String line;
- while ((line = rd.readLine()) != null) {
- String[] tokens = line.split(" ");
- if (tokens.length > 1) {
- if ("CL:".equals(tokens[0])) {
- map.put(tokens[2], tokens[1]);
- } else if ("FD:".equals(tokens[0])) {
- map.put(tokens[2], tokens[1]);
- } else if ("MD:".equals(tokens[0])) {
- map.put(tokens[3] + tokens[4], tokens[1].substring(tokens[1].lastIndexOf('/') + 1));
- }
- }
- }
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- if (rd != null) {
- try {
- rd.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- return map;
- }
-}
diff --git a/src/main/java/com/gtnewhorizons/angelica/transform/NamerSrg.java b/src/main/java/com/gtnewhorizons/angelica/transform/NamerSrg.java
deleted file mode 100644
index 95e76b6ee..000000000
--- a/src/main/java/com/gtnewhorizons/angelica/transform/NamerSrg.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.gtnewhorizons.angelica.transform;
-
-import static com.gtnewhorizons.angelica.transform.Names.*;
-
-public class NamerSrg extends Namer {
-
- public void setNames() {
- setNamesSrg();
- }
-
- public void setNamesSrg() {
- entityRenderer_ = c("net/minecraft/client/renderer/EntityRenderer");
- rendererLivingE_ = c("net/minecraft/client/renderer/entity/RendererLivingEntity");
- entityLivingBase_ = c("net/minecraft/entity/EntityLivingBase");
-
- entityRenderer_renderHand = m(entityRenderer_, "func_78476_b", "(FI)V");
- rendererLivingE_doRender = m(rendererLivingE_, "func_76986_a", "(" + entityLivingBase_.desc + "DDDFF)V");
- rendererLivingE_renderEquippedItems = m(rendererLivingE_, "func_77029_c", "(" + entityLivingBase_.desc + "F)V");
- }
-}
diff --git a/src/main/java/com/gtnewhorizons/angelica/transform/Names.java b/src/main/java/com/gtnewhorizons/angelica/transform/Names.java
deleted file mode 100644
index 16e0cbda3..000000000
--- a/src/main/java/com/gtnewhorizons/angelica/transform/Names.java
+++ /dev/null
@@ -1,84 +0,0 @@
-package com.gtnewhorizons.angelica.transform;
-
-public class Names {
-
- public static class Name {
-
- String clas;
- String name;
- String desc;
-
- public Name(String clas, String name, String desc) {
- this.clas = clas;
- this.name = name;
- this.desc = desc;
- }
-
- public Name set(String clas, String name, String desc) {
- this.clas = clas;
- this.name = name;
- this.desc = desc;
- return this;
- }
-
- public boolean equals(String clas, String name, String desc) {
- return this.clas.equals(clas) && this.name.equals(name) && this.desc.equals(desc);
- }
- }
-
- public static class Type extends Name {
-
- public Type(String desc) {
- super("", "", desc);
- }
-
- public Type(String name, String desc) {
- super(name, name, desc);
- }
- }
-
- public static class Clas extends Type {
-
- public Clas(String name) {
- super(name, "L" + name + ";");
- }
-
- public boolean equals(String clas) {
- return this.clas.equals(clas);
- }
- }
-
- public static class Fiel extends Name {
-
- public Fiel(Clas clas, String name, String desc) {
- super(clas.clas, name, desc);
- }
-
- public boolean equals(String clas, String name) {
- return this.clas.equals(clas) && this.name.equals(name);
- }
- }
-
- public static class Meth extends Name {
-
- public Meth(Clas clas, String name, String desc) {
- super(clas.clas, name, desc);
- }
-
- public boolean equalsNameDesc(String name, String desc) {
- return this.name.equals(name) && this.desc.equals(desc);
- }
- }
-
- static Clas entityRenderer_;
- static Clas rendererLivingE_;
- static Clas entityLivingBase_;
-
- static Meth entityRenderer_renderHand;
- static Meth rendererLivingE_doRender;
- static Meth rendererLivingE_renderEquippedItems;
-
- public static boolean equals(String clas1, String name1, String desc1, String clas2, String name2, String desc2) {
- return clas1.equals(clas2) && name1.equals(name2) && desc1.equals(desc2);
- }
-}
diff --git a/src/main/java/com/gtnewhorizons/angelica/transform/RedirectorTransformer.java b/src/main/java/com/gtnewhorizons/angelica/transform/RedirectorTransformer.java
new file mode 100644
index 000000000..124e84239
--- /dev/null
+++ b/src/main/java/com/gtnewhorizons/angelica/transform/RedirectorTransformer.java
@@ -0,0 +1,325 @@
+package com.gtnewhorizons.angelica.transform;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.gtnewhorizons.angelica.loading.AngelicaTweaker;
+import net.coderbot.iris.IrisLogging;
+import net.minecraft.launchwrapper.IClassTransformer;
+import net.minecraft.launchwrapper.Launch;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.spongepowered.asm.lib.ClassReader;
+import org.spongepowered.asm.lib.ClassWriter;
+import org.spongepowered.asm.lib.Opcodes;
+import org.spongepowered.asm.lib.tree.AbstractInsnNode;
+import org.spongepowered.asm.lib.tree.ClassNode;
+import org.spongepowered.asm.lib.tree.FieldInsnNode;
+import org.spongepowered.asm.lib.tree.InsnList;
+import org.spongepowered.asm.lib.tree.InsnNode;
+import org.spongepowered.asm.lib.tree.IntInsnNode;
+import org.spongepowered.asm.lib.tree.LdcInsnNode;
+import org.spongepowered.asm.lib.tree.MethodInsnNode;
+import org.spongepowered.asm.lib.tree.MethodNode;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This transformer redirects all Tessellator.instance field accesses to go through our TessellatorManager.
+ * As well as redirect some GL calls to our custom GLStateManager
+ */
+public class RedirectorTransformer implements IClassTransformer {
+
+ private static final boolean ASSERT_MAIN_THREAD = Boolean.parseBoolean(System.getProperty("angelica.assertMainThread", "false"));
+ private static final boolean DUMP_CLASSES = Boolean.parseBoolean(System.getProperty("angelica.dumpClass", "false"));
+ private static final String Drawable = "org/lwjgl/opengl/Drawable";
+ private static final String GLStateManager = "com/gtnewhorizons/angelica/glsm/GLStateManager";
+ private static final String GL11 = "org/lwjgl/opengl/GL11";
+ private static final String GL13 = "org/lwjgl/opengl/GL13";
+ private static final String GL14 = "org/lwjgl/opengl/GL14";
+ private static final String OpenGlHelper = "net/minecraft/client/renderer/OpenGlHelper";
+ private static final String EXTBlendFunc = "org/lwjgl/opengl/EXTBlendFuncSeparate";
+ private static final String ARBMultiTexture = "org/lwjgl/opengl/ARBMultitexture";
+ private static final String TessellatorClass = "net/minecraft/client/renderer/Tessellator";
+ private static final String BlockClass = "net/minecraft/block/Block";
+ private static final String MinecraftClient = "net.minecraft.client";
+ private static final String SplashProgress = "cpw.mods.fml.client.SplashProgress";
+ private static final String ThreadedBlockData = "com/gtnewhorizons/angelica/glsm/ThreadedBlockData";
+ private static final Set ExcludedMinecraftMainThreadChecks = ImmutableSet.of(
+ "startGame", "func_71384_a",
+ "initializeTextures", "func_77474_a"
+ );
+
+ private static final List> BlockBoundsFields = ImmutableList.of(
+ Pair.of("minX", "field_149759_B"),
+ Pair.of("minY", "field_149760_C"),
+ Pair.of("minZ", "field_149754_D"),
+ Pair.of("maxX", "field_149755_E"),
+ Pair.of("maxY", "field_149756_F"),
+ Pair.of("maxZ", "field_149757_G")
+ );
+
+ private static final ClassConstantPoolParser cstPoolParser = new ClassConstantPoolParser(GL11, GL13, GL14, OpenGlHelper, EXTBlendFunc, ARBMultiTexture, TessellatorClass, BlockClass);
+ private static final Map> methodRedirects = new HashMap<>();
+ private static final Map glCapRedirects = new HashMap<>();
+ private static final List TransformerExclusions = Arrays.asList(
+ "org.lwjgl",
+ "com.gtnewhorizons.angelica.glsm.",
+ "com.gtnewhorizons.angelica.transform",
+ "me.eigenraven.lwjgl3ify"
+ );
+ private static int remaps = 0;
+
+ static {
+ glCapRedirects.put(org.lwjgl.opengl.GL11.GL_ALPHA_TEST, "AlphaTest");
+ glCapRedirects.put(org.lwjgl.opengl.GL11.GL_BLEND, "Blend");
+ glCapRedirects.put(org.lwjgl.opengl.GL11.GL_DEPTH_TEST, "DepthTest");
+ glCapRedirects.put(org.lwjgl.opengl.GL11.GL_CULL_FACE, "Cull");
+ glCapRedirects.put(org.lwjgl.opengl.GL11.GL_LIGHTING, "Lighting");
+ glCapRedirects.put(org.lwjgl.opengl.GL11.GL_TEXTURE_2D, "Texture");
+ glCapRedirects.put(org.lwjgl.opengl.GL11.GL_FOG, "Fog");
+ glCapRedirects.put(org.lwjgl.opengl.GL12.GL_RESCALE_NORMAL, "RescaleNormal");
+ methodRedirects.put(GL11, RedirectMap.newMap()
+ .add("glAlphaFunc")
+ .add("glBindTexture")
+ .add("glBlendFunc")
+ .add("glClearColor")
+ .add("glColor3b")
+ .add("glColor3d")
+ .add("glColor3f")
+ .add("glColor3ub")
+ .add("glColor4b")
+ .add("glColor4d")
+ .add("glColor4f")
+ .add("glColor4ub")
+ .add("glColorMask")
+ .add("glDeleteTextures")
+ .add("glDepthFunc")
+ .add("glDepthMask")
+ .add("glDrawArrays")
+ .add("glEndList")
+ .add("glFog")
+ .add("glFogf")
+ .add("glFogi")
+ .add("glNewList")
+ .add("glPushAttrib")
+ .add("glPopAttrib")
+ .add("glShadeModel")
+ .add("glTexImage2D")
+ );
+ methodRedirects.put(GL13, RedirectMap.newMap().add("glActiveTexture"));
+ methodRedirects.put(GL14, RedirectMap.newMap().add("glBlendFuncSeparate", "tryBlendFuncSeparate"));
+ methodRedirects.put(OpenGlHelper, RedirectMap.newMap()
+ .add("glBlendFunc", "tryBlendFuncSeparate")
+ .add("func_148821_a", "tryBlendFuncSeparate"));
+ methodRedirects.put(EXTBlendFunc, RedirectMap.newMap().add("glBlendFuncSeparateEXT", "tryBlendFuncSeparate"));
+ methodRedirects.put(ARBMultiTexture, RedirectMap.newMap().add("glActiveTextureARB"));
+ }
+
+ @Override
+ public byte[] transform(final String className, String transformedName, byte[] basicClass) {
+ if (basicClass == null) return null;
+
+ // Ignore classes that are excluded from transformation - Doesn't fully work without the
+ // TransformerExclusions due to some nested classes
+ for (String exclusion : TransformerExclusions) {
+ if (className.startsWith(exclusion)) {
+ return basicClass;
+ }
+ }
+
+ if (!cstPoolParser.find(basicClass)) {
+ return basicClass;
+ }
+
+ final ClassReader cr = new ClassReader(basicClass);
+ final ClassNode cn = new ClassNode();
+ cr.accept(cn, 0);
+
+ boolean changed = false;
+ for (MethodNode mn : cn.methods) {
+ if (transformedName.equals("net.minecraft.client.renderer.OpenGlHelper") && (mn.name.equals("glBlendFunc") || mn.name.equals("func_148821_a"))) {
+ continue;
+ }
+ boolean redirectInMethod = false;
+ for (AbstractInsnNode node : mn.instructions.toArray()) {
+ if (node instanceof MethodInsnNode mNode) {
+ if (mNode.owner.equals(GL11) && (mNode.name.equals("glEnable") || mNode.name.equals("glDisable")) && mNode.desc.equals("(I)V")) {
+ final AbstractInsnNode prevNode = node.getPrevious();
+ String name = null;
+ if (prevNode instanceof LdcInsnNode ldcNode) {
+ name = glCapRedirects.get(((Integer) ldcNode.cst));
+ } else if (prevNode instanceof IntInsnNode intNode) {
+ name = glCapRedirects.get(intNode.operand);
+ }
+ if (name != null) {
+ if (mNode.name.equals("glEnable")) {
+ name = "enable" + name;
+ } else {
+ name = "disable" + name;
+ }
+ }
+ if (IrisLogging.ENABLE_SPAM) {
+ if (name == null) {
+ AngelicaTweaker.LOGGER.info("Redirecting call in {} from GL11.{}(I)V to GLStateManager.{}(I)V", transformedName, mNode.name, mNode.name);
+ } else {
+ AngelicaTweaker.LOGGER.info("Redirecting call in {} from GL11.{}(I)V to GLStateManager.{}()V", transformedName, mNode.name, name);
+ }
+ }
+ mNode.owner = GLStateManager;
+ if (name != null) {
+ mNode.name = name;
+ mNode.desc = "()V";
+ mn.instructions.remove(prevNode);
+ }
+ changed = true;
+ redirectInMethod = true;
+ remaps++;
+ } else if (mNode.owner.startsWith(Drawable) && mNode.name.equals("makeCurrent")) {
+ mNode.setOpcode(Opcodes.INVOKESTATIC);
+ mNode.owner = GLStateManager;
+ mNode.desc = "(L" + Drawable + ";)V";
+ mNode.itf = false;
+ changed = true;
+ if (IrisLogging.ENABLE_SPAM) {
+ AngelicaTweaker.LOGGER.info("Redirecting call in {} to GLStateManager.makeCurrent()", transformedName);
+ }
+ } else {
+ final Map redirects = methodRedirects.get(mNode.owner);
+ if (redirects != null && redirects.containsKey(mNode.name)) {
+ if (IrisLogging.ENABLE_SPAM) {
+ final String shortOwner = mNode.owner.substring(mNode.owner.lastIndexOf("/") + 1);
+ AngelicaTweaker.LOGGER.info("Redirecting call in {} from {}.{}{} to GLStateManager.{}{}", transformedName, shortOwner, mNode.name, mNode.desc, redirects.get(mNode.name), mNode.desc);
+ }
+ mNode.owner = GLStateManager;
+ mNode.name = redirects.get(mNode.name);
+ changed = true;
+ redirectInMethod = true;
+ remaps++;
+ }
+ }
+ }
+ else if (node.getOpcode() == Opcodes.GETSTATIC && node instanceof FieldInsnNode fNode) {
+ if ((fNode.name.equals("field_78398_a") || fNode.name.equals("instance")) && fNode.owner.equals(TessellatorClass)) {
+ if (IrisLogging.ENABLE_SPAM) {
+ AngelicaTweaker.LOGGER.info("Redirecting Tessellator.instance field in {} to TessellatorManager.get()", transformedName);
+ }
+ mn.instructions.set(node, new MethodInsnNode(Opcodes.INVOKESTATIC, "com/gtnewhorizons/angelica/glsm/TessellatorManager", "get", "()Lnet/minecraft/client/renderer/Tessellator;", false));
+ changed = true;
+ }
+ }
+ else if ((node.getOpcode() == Opcodes.GETFIELD || node.getOpcode() == Opcodes.PUTFIELD) && node instanceof FieldInsnNode fNode) {
+ if(fNode.owner.equals(BlockClass)) {
+ Pair fieldToRedirect = null;
+ for(Pair blockPairs : BlockBoundsFields) {
+ if(fNode.name.equals(blockPairs.getLeft()) || fNode.name.equals(blockPairs.getRight())) {
+ fieldToRedirect = blockPairs;
+ break;
+ }
+ }
+ if(fieldToRedirect != null) {
+ if (IrisLogging.ENABLE_SPAM) {
+ AngelicaTweaker.LOGGER.info("Redirecting Block.{} in {} to thread-safe wrapper", fNode.name, transformedName);
+ }
+ // Perform the redirect
+ fNode.name = fieldToRedirect.getLeft(); // use unobfuscated name
+ fNode.owner = ThreadedBlockData;
+ // Inject getter before the field access, to turn Block -> ThreadedBlockData
+ MethodInsnNode getter = new MethodInsnNode(Opcodes.INVOKESTATIC, ThreadedBlockData, "get", "(L" + BlockClass + ";)L" + ThreadedBlockData + ";", false);
+ if(node.getOpcode() == Opcodes.GETFIELD) {
+ mn.instructions.insertBefore(fNode, getter);
+ } else if(node.getOpcode() == Opcodes.PUTFIELD) {
+ // FIXME: this code assumes doubles
+ // Stack: Block, double
+ final InsnList beforePut = new InsnList();
+ beforePut.add(new InsnNode(Opcodes.DUP2_X1));
+ // Stack: double, Block, double
+ beforePut.add(new InsnNode(Opcodes.POP2));
+ // Stack: double, Block
+ beforePut.add(getter);
+ // Stack: double, ThreadedBlockData
+ beforePut.add(new InsnNode(Opcodes.DUP_X2));
+ // Stack: ThreadedBlockData, double, ThreadedBlockData
+ beforePut.add(new InsnNode(Opcodes.POP));
+ // Stack: ThreadedBlockData, double
+ mn.instructions.insertBefore(fNode, beforePut);
+ }
+ changed = true;
+ }
+
+ }
+ }
+ }
+ if (ASSERT_MAIN_THREAD && redirectInMethod && !transformedName.startsWith(SplashProgress) && !(transformedName.startsWith(MinecraftClient) && ExcludedMinecraftMainThreadChecks.contains(mn.name))) {
+ mn.instructions.insert(new MethodInsnNode(Opcodes.INVOKESTATIC, GLStateManager, "assertMainThread", "()V", false));
+ }
+ }
+
+ if (changed) {
+ ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
+ cn.accept(cw);
+ final byte[] bytes = cw.toByteArray();
+ saveTransformedClass(bytes, transformedName);
+ return bytes;
+ }
+ return basicClass;
+ }
+
+ private File outputDir = null;
+
+ private void saveTransformedClass(final byte[] data, final String transformedName) {
+ if (!DUMP_CLASSES) {
+ return;
+ }
+ if (outputDir == null) {
+ outputDir = new File(Launch.minecraftHome, "ASM_REDIRECTOR");
+ try {
+ FileUtils.deleteDirectory(outputDir);
+ } catch (IOException ignored) {}
+ if (!outputDir.exists()) {
+ //noinspection ResultOfMethodCallIgnored
+ outputDir.mkdirs();
+ }
+ }
+ final String fileName = transformedName.replace('.', File.separatorChar);
+ final File classFile = new File(outputDir, fileName + ".class");
+ final File outDir = classFile.getParentFile();
+ if (!outDir.exists()) {
+ //noinspection ResultOfMethodCallIgnored
+ outDir.mkdirs();
+ }
+ if (classFile.exists()) {
+ //noinspection ResultOfMethodCallIgnored
+ classFile.delete();
+ }
+ try (final OutputStream output = Files.newOutputStream(classFile.toPath())) {
+ output.write(data);
+ } catch (IOException e) {
+ AngelicaTweaker.LOGGER.error("Could not save transformed class (byte[]) " + transformedName, e);
+ }
+ }
+
+ private static class RedirectMap extends HashMap {
+ public static RedirectMap newMap() {
+ return new RedirectMap<>();
+ }
+
+ public RedirectMap add(K name) {
+ this.put(name, name);
+ return this;
+ }
+
+ public RedirectMap add(K name, K newName) {
+ this.put(name, newName);
+ return this;
+ }
+ }
+
+}
diff --git a/src/main/java/com/gtnewhorizons/angelica/utils/AnimationMode.java b/src/main/java/com/gtnewhorizons/angelica/utils/AnimationMode.java
new file mode 100644
index 000000000..0dee6b633
--- /dev/null
+++ b/src/main/java/com/gtnewhorizons/angelica/utils/AnimationMode.java
@@ -0,0 +1,7 @@
+package com.gtnewhorizons.angelica.utils;
+
+public enum AnimationMode {
+ NONE,
+ VISIBLE_ONLY,
+ ALL
+}
diff --git a/src/main/java/com/gtnewhorizons/angelica/utils/AnimationsRenderUtils.java b/src/main/java/com/gtnewhorizons/angelica/utils/AnimationsRenderUtils.java
new file mode 100644
index 000000000..38ed419a7
--- /dev/null
+++ b/src/main/java/com/gtnewhorizons/angelica/utils/AnimationsRenderUtils.java
@@ -0,0 +1,31 @@
+package com.gtnewhorizons.angelica.utils;
+
+import com.gtnewhorizons.angelica.mixins.interfaces.IPatchedTextureAtlasSprite;
+import com.gtnewhorizons.angelica.mixins.interfaces.ITexturesCache;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.texture.TextureAtlasSprite;
+import net.minecraft.client.renderer.texture.TextureMap;
+import net.minecraft.util.IIcon;
+import net.minecraft.world.IBlockAccess;
+
+public class AnimationsRenderUtils {
+
+ public static void markBlockTextureForUpdate(IIcon icon) {
+ markBlockTextureForUpdate(icon, null);
+ }
+
+ public static void markBlockTextureForUpdate(IIcon icon, IBlockAccess blockAccess) {
+ final TextureMap textureMap = Minecraft.getMinecraft().getTextureMapBlocks();
+ final TextureAtlasSprite textureAtlasSprite = textureMap.getAtlasSprite(icon.getIconName());
+
+ if (textureAtlasSprite != null && textureAtlasSprite.hasAnimationMetadata()) {
+ // null if called by anything but chunk render cache update (for example to get blocks rendered as items in
+ // inventory)
+ if (blockAccess instanceof ITexturesCache) {
+ ((ITexturesCache) blockAccess).getRenderedTextures().add(textureAtlasSprite);
+ } else {
+ ((IPatchedTextureAtlasSprite) textureAtlasSprite).markNeedsAnimationUpdate();
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/gtnewhorizons/angelica/utils/ManagedEnum.java b/src/main/java/com/gtnewhorizons/angelica/utils/ManagedEnum.java
new file mode 100644
index 000000000..c1bf72c99
--- /dev/null
+++ b/src/main/java/com/gtnewhorizons/angelica/utils/ManagedEnum.java
@@ -0,0 +1,52 @@
+package com.gtnewhorizons.angelica.utils;
+
+public class ManagedEnum> {
+
+ private final T[] allValues;
+ private T value;
+
+ public ManagedEnum(T initialValue) {
+ if (initialValue == null) throw new IllegalArgumentException();
+ value = initialValue;
+ @SuppressWarnings("unchecked")
+ T[] allValues = (T[]) value.getClass().getEnumConstants();
+ this.allValues = allValues;
+ }
+
+ public boolean is(T value) {
+ return this.value == value;
+ }
+
+ public T next() {
+ value = allValues[(value.ordinal() + 1) % allValues.length];
+ return value;
+ }
+
+ public void set(T value) {
+ this.value = value;
+ }
+
+ public void set(int ordinal) {
+ this.value = allValues[Math.min(Math.max(0, ordinal), allValues.length - 1)];
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ ManagedEnum> that = (ManagedEnum>) o;
+
+ return value == that.value;
+ }
+
+ @Override
+ public int hashCode() {
+ return value.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return value.toString();
+ }
+}
diff --git a/src/main/java/com/gtnewhorizons/angelica/utils/Mipmaps.java b/src/main/java/com/gtnewhorizons/angelica/utils/Mipmaps.java
new file mode 100644
index 000000000..c7df94c19
--- /dev/null
+++ b/src/main/java/com/gtnewhorizons/angelica/utils/Mipmaps.java
@@ -0,0 +1,25 @@
+package com.gtnewhorizons.angelica.utils;
+
+public class Mipmaps {
+
+ private static final float[] VALS = new float[256];
+
+ public static float get(int i) {
+ return VALS[i & 0xFF];
+ }
+
+ public static int getColorComponent(int one, int two, int three, int four, int bits) {
+ final float f = Mipmaps.get(one >> bits);
+ final float g = Mipmaps.get(two >> bits);
+ final float h = Mipmaps.get(three >> bits);
+ final float i = Mipmaps.get(four >> bits);
+ final float j = (float) Math.pow((f + g + h + i) * 0.25, 0.45454545454545453);
+ return (int) (j * 255.0);
+ }
+
+ static {
+ for (int i = 0; i < VALS.length; ++i) {
+ VALS[i] = (float) Math.pow((float) i / 255.0F, 2.2);
+ }
+ }
+}
diff --git a/src/main/java/de/odysseus/ithaka/digraph/Digraph.java b/src/main/java/de/odysseus/ithaka/digraph/Digraph.java
new file mode 100644
index 000000000..4c3af75ca
--- /dev/null
+++ b/src/main/java/de/odysseus/ithaka/digraph/Digraph.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2012 Odysseus Software GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.odysseus.ithaka.digraph;
+
+import java.util.Collection;
+import java.util.OptionalInt;
+import java.util.Set;
+
+/**
+ * Directed graph interface.
+ *
+ * @param vertex type
+ */
+public interface Digraph extends EdgeWeights {
+ /**
+ * Get an edge.
+ *
+ * @param source source vertex
+ * @param target target vertex
+ * @return edge weight (0
if there is no edge from source
to target
)
+ */
+ OptionalInt get(V source, V target);
+
+ /**
+ * Edge test.
+ *
+ * @param source source vertex
+ * @param target target vertex
+ * @return true
iff this de.odysseus.ithaka.digraph contains an edge from source
to target
+ */
+ boolean contains(V source, V target);
+
+ /**
+ * Vertex test
+ *
+ * @return true
iff this de.odysseus.ithaka.digraph contains vertex
+ */
+ boolean contains(V vertex);
+
+ /**
+ * Add vertex.
+ *
+ * @return true
iff vertex
has been added
+ */
+ boolean add(V vertex);
+
+ /**
+ * Put an edge.
+ * Vertices are added automatically if they appear in an edge.
+ *
+ * @param source source vertex
+ * @param target target vertex
+ * @param weight edge weight
+ * @return edge weight that has been previously set (0
if there was no edge from source
+ * to target
)
+ */
+ OptionalInt put(V source, V target, int weight);
+
+ /**
+ * Remove an edge.
+ *
+ * @param source source vertex
+ * @param target target vertex
+ * @return edge weight that has been previously set (0
if there was no edge from source
+ * to target
)
+ */
+ OptionalInt remove(V source, V target);
+
+ /**
+ * Remove a vertex.
+ *
+ * @param vertex vertex
+ * @return true
iff this de.odysseus.ithaka.digraph contained vertex
+ */
+ boolean remove(V vertex);
+
+ /**
+ * Remove all vertices.
+ *
+ * @param vertices vertices
+ */
+ void removeAll(Collection vertices);
+
+ /**
+ * Iterate over vertices.
+ *
+ * @return vertices
+ */
+ Iterable vertices();
+
+ /**
+ * Iterate over edge targets for given source vertex.
+ *
+ * @param source source vertex
+ * @return edge targets of edges starting at source
+ */
+ Iterable targets(V source);
+
+ /**
+ * @return number of vertices in this de.odysseus.ithaka.digraph
+ */
+ int getVertexCount();
+
+ /**
+ * @return sum of edge weights
+ */
+ int totalWeight();
+
+ /**
+ * @return number of edges starting at vertex
+ */
+ int getOutDegree(V vertex);
+
+ /**
+ * @return number of edges in this de.odysseus.ithaka.digraph
+ */
+ int getEdgeCount();
+
+ /**
+ * @return true
iff this de.odysseus.ithaka.digraph is acyclic (i.e. it is a DAG)
+ */
+ boolean isAcyclic();
+
+ /**
+ * Get reverse de.odysseus.ithaka.digraph (same vertices, with edges reversed).
+ *
+ * @return reverse de.odysseus.ithaka.digraph
+ */
+ Digraph reverse();
+
+ /**
+ * Get induced subgraph (with vertices in this de.odysseus.ithaka.digraph and the given vertex set and edges that appear in this de.odysseus.ithaka.digraph over the given vertex set).
+ *
+ * @return subgraph
+ */
+ Digraph subgraph(Set vertices);
+}
diff --git a/src/main/java/de/odysseus/ithaka/digraph/DigraphAdapter.java b/src/main/java/de/odysseus/ithaka/digraph/DigraphAdapter.java
new file mode 100644
index 000000000..120992254
--- /dev/null
+++ b/src/main/java/de/odysseus/ithaka/digraph/DigraphAdapter.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2012 Odysseus Software GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.odysseus.ithaka.digraph;
+
+import java.util.Collection;
+import java.util.OptionalInt;
+import java.util.Set;
+
+/**
+ * Abstract Digraph adapter.
+ * A de.odysseus.ithaka.digraph adapter delegates to a de.odysseus.ithaka.digraph supplied at construction time.
+ *
+ * @param vertex type
+ */
+public abstract class DigraphAdapter implements Digraph {
+ private final Digraph delegate;
+
+ public DigraphAdapter(Digraph delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public boolean add(V vertex) {
+ return delegate.add(vertex);
+ }
+
+ @Override
+ public boolean contains(V source, V target) {
+ return delegate.contains(source, target);
+ }
+
+ @Override
+ public boolean contains(V vertex) {
+ return delegate.contains(vertex);
+ }
+
+ @Override
+ public OptionalInt get(V source, V target) {
+ return delegate.get(source, target);
+ }
+
+ @Override
+ public int getOutDegree(V vertex) {
+ return delegate.getOutDegree(vertex);
+ }
+
+ @Override
+ public int getEdgeCount() {
+ return delegate.getEdgeCount();
+ }
+
+ @Override
+ public int getVertexCount() {
+ return delegate.getVertexCount();
+ }
+
+ @Override
+ public int totalWeight() {
+ return delegate.totalWeight();
+ }
+
+ @Override
+ public Iterable vertices() {
+ return delegate.vertices();
+ }
+
+ @Override
+ public OptionalInt put(V source, V target, int edge) {
+ return delegate.put(source, target, edge);
+ }
+
+ @Override
+ public OptionalInt remove(V source, V target) {
+ return delegate.remove(source, target);
+ }
+
+ @Override
+ public boolean remove(V vertex) {
+ return delegate.remove(vertex);
+ }
+
+ @Override
+ public void removeAll(Collection vertices) {
+ delegate.removeAll(vertices);
+ }
+
+ @Override
+ public Digraph reverse() {
+ return delegate.reverse();
+ }
+
+ @Override
+ public Digraph subgraph(Set vertices) {
+ return delegate.subgraph(vertices);
+ }
+
+ @Override
+ public boolean isAcyclic() {
+ return delegate.isAcyclic();
+ }
+
+ @Override
+ public Iterable targets(V source) {
+ return delegate.targets(source);
+ }
+
+ @Override
+ public String toString() {
+ return delegate.toString();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+
+ if (this.getClass() != obj.getClass()) {
+ return false;
+ }
+
+ return delegate.equals(((DigraphAdapter>) obj).delegate);
+ }
+
+ @Override
+ public int hashCode() {
+ return delegate.hashCode();
+ }
+}
diff --git a/src/main/java/de/odysseus/ithaka/digraph/DigraphFactory.java b/src/main/java/de/odysseus/ithaka/digraph/DigraphFactory.java
new file mode 100644
index 000000000..63aa63afc
--- /dev/null
+++ b/src/main/java/de/odysseus/ithaka/digraph/DigraphFactory.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2012 Odysseus Software GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.odysseus.ithaka.digraph;
+
+/**
+ * Digraph factory interface.
+ *
+ * @param de.odysseus.ithaka.digraph type
+ */
+public interface DigraphFactory> {
+ /**
+ * Create a de.odysseus.ithaka.digraph.
+ *
+ * @return de.odysseus.ithaka.digraph
+ */
+ G create();
+}
diff --git a/src/main/java/de/odysseus/ithaka/digraph/DigraphProvider.java b/src/main/java/de/odysseus/ithaka/digraph/DigraphProvider.java
new file mode 100644
index 000000000..021dfe086
--- /dev/null
+++ b/src/main/java/de/odysseus/ithaka/digraph/DigraphProvider.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2012 Odysseus Software GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.odysseus.ithaka.digraph;
+
+/**
+ * Digraph provider interface.
+ *
+ * @param de.odysseus.ithaka.digraph sub-type
+ * @param de.odysseus.ithaka.digraph type
+ */
+public interface DigraphProvider> {
+ /**
+ * Get a de.odysseus.ithaka.digraph.
+ *
+ * @param value value associated with a de.odysseus.ithaka.digraph
+ * @return de.odysseus.ithaka.digraph
+ */
+ G get(T value);
+}
diff --git a/src/main/java/de/odysseus/ithaka/digraph/Digraphs.java b/src/main/java/de/odysseus/ithaka/digraph/Digraphs.java
new file mode 100644
index 000000000..973d5a3d4
--- /dev/null
+++ b/src/main/java/de/odysseus/ithaka/digraph/Digraphs.java
@@ -0,0 +1,352 @@
+/*
+ * Copyright 2012 Odysseus Software GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.odysseus.ithaka.digraph;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.OptionalInt;
+import java.util.Set;
+import java.util.Stack;
+
+/**
+ * This class provides some common de.odysseus.ithaka.digraph utilities.
+ */
+public class Digraphs {
+ /**
+ * Get an unmodifiable empty de.odysseus.ithaka.digraph.
+ *
+ * @return empty de.odysseus.ithaka.digraph
+ */
+ public static DoubledDigraph emptyDigraph() {
+ return new EmptyDigraph<>();
+ }
+
+ /**
+ * Wraps the given de.odysseus.ithaka.digraph to make it unmodifiable. Whenever a method
+ * is called on the resulting de.odysseus.ithaka.digraph that could modify the underlying
+ * de.odysseus.ithaka.digraph, an exception is thrown.
+ *
+ * @param vertex type
+ * @return unmodifiable de.odysseus.ithaka.digraph equivalent to the given de.odysseus.ithaka.digraph
+ */
+ public static