diff --git a/src/main/java/li/cil/oc/api/API.java b/src/main/java/li/cil/oc/api/API.java
index 24315aebf..d37844ed4 100644
--- a/src/main/java/li/cil/oc/api/API.java
+++ b/src/main/java/li/cil/oc/api/API.java
@@ -4,6 +4,7 @@ import li.cil.oc.api.detail.DriverAPI;
import li.cil.oc.api.detail.FileSystemAPI;
import li.cil.oc.api.detail.ItemAPI;
import li.cil.oc.api.detail.MachineAPI;
+import li.cil.oc.api.detail.ManualAPI;
import li.cil.oc.api.detail.NetworkAPI;
/**
@@ -15,11 +16,12 @@ import li.cil.oc.api.detail.NetworkAPI;
*/
public class API {
public static final String ID_OWNER = "OpenComputers|Core";
- public static final String VERSION = "5.0.0";
+ public static final String VERSION = "5.1.0";
public static DriverAPI driver = null;
public static FileSystemAPI fileSystem = null;
public static ItemAPI items = null;
public static MachineAPI machine = null;
+ public static ManualAPI manual = null;
public static NetworkAPI network = null;
}
diff --git a/src/main/java/li/cil/oc/api/Manual.java b/src/main/java/li/cil/oc/api/Manual.java
new file mode 100644
index 000000000..655a1bc08
--- /dev/null
+++ b/src/main/java/li/cil/oc/api/Manual.java
@@ -0,0 +1,142 @@
+package li.cil.oc.api;
+
+import li.cil.oc.api.manual.ContentProvider;
+import li.cil.oc.api.manual.PathProvider;
+import li.cil.oc.api.manual.TabIconRenderer;
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.item.ItemStack;
+import net.minecraft.world.World;
+
+/**
+ * This API allows interfacing with the in-game manual of OpenComputers.
+ *
+ * It allows opening the manual at a desired specific page, as well as
+ * registering custom tabs and content callback handlers.
+ *
+ * Note: this is a client side only API. It will do nothing on
+ * dedicated servers (i.e. API.manual will be null).
+ */
+public class Manual {
+ /**
+ * Register a tab to be displayed next to the manual.
+ *
+ * These are intended to link to index pages, and for the time being there
+ * a relatively low number of tabs that can be displayed, so I'd ask you to
+ * only register as many tabs as actually, technically *needed*. Which will
+ * usually be one, for your main index page.
+ *
+ * @param renderer the renderer used to render the icon on your tab.
+ * @param path the path to the page to open when the tab is clicked.
+ */
+ public static void addTab(TabIconRenderer renderer, String path) {
+ if (API.manual != null)
+ API.manual.addTab(renderer, path);
+ }
+
+ /**
+ * Register a path provider.
+ *
+ * Path providers are used to find documentation entries for item stacks
+ * and blocks in the world.
+ *
+ * @param provider the provider to register.
+ */
+ public static void addProvider(PathProvider provider) {
+ if (API.manual != null)
+ API.manual.addProvider(provider);
+ }
+
+ /**
+ * Register a content provider.
+ *
+ * Content providers are used to resolve paths to page content, if the
+ * standard system (using Minecraft's resource loading facilities) fails.
+ *
+ * This can be useful for providing dynamic content, for example.
+ *
+ * @param provider the provider to register.
+ */
+ public static void addProvider(ContentProvider provider) {
+ if (API.manual != null)
+ API.manual.addProvider(provider);
+ }
+
+ // ----------------------------------------------------------------------- //
+
+ /**
+ * Look up the documentation path for the specified item stack.
+ *
+ * @param stack the stack to find the documentation path for.
+ * @return the path to the page, null if none is known.
+ */
+ public static String pathFor(ItemStack stack) {
+ if (API.manual != null)
+ return API.manual.pathFor(stack);
+ return null;
+ }
+
+ /**
+ * Look up the documentation for the specified block in the world.
+ *
+ * @param world the world containing the block.
+ * @param x the X coordinate of the block.
+ * @param y the Y coordinate of the block.
+ * @param z the Z coordinate of the block.
+ * @return the path to the page, null if none is known.
+ */
+ public static String pathFor(World world, int x, int y, int z) {
+ if (API.manual != null)
+ return API.manual.pathFor(world, x, y, z);
+ return null;
+ }
+
+ /**
+ * Get the content of the documentation page at the specified location.
+ *
+ * @param path the path of the page to get the content of.
+ * @return the content of the page, or null if none exists.
+ */
+ public static Iterable contentFor(String path) {
+ if (API.manual != null)
+ return API.manual.contentFor(path);
+ return null;
+ }
+
+ // ----------------------------------------------------------------------- //
+
+ /**
+ * Open the manual for the specified player.
+ *
+ * If you wish to display a specific page, call {@link #navigate(String)}
+ * after this function returns, with the path to the page to show.
+ *
+ * @param player the player to open the manual for.
+ */
+ public static void openFor(EntityPlayer player) {
+ if (API.manual != null)
+ API.manual.openFor(player);
+ }
+
+ /**
+ * Reset the history of the manual.
+ */
+ public static void reset() {
+ if (API.manual != null)
+ API.manual.reset();
+ }
+
+ /**
+ * Navigate to a page in the manual.
+ *
+ * @param path the path to navigate to.
+ */
+ public static void navigate(String path) {
+ if (API.manual != null)
+ API.manual.navigate(path);
+ }
+
+ // ----------------------------------------------------------------------- //
+
+ private Manual() {
+ }
+}
diff --git a/src/main/java/li/cil/oc/api/detail/ManualAPI.java b/src/main/java/li/cil/oc/api/detail/ManualAPI.java
new file mode 100644
index 000000000..b626c3ddd
--- /dev/null
+++ b/src/main/java/li/cil/oc/api/detail/ManualAPI.java
@@ -0,0 +1,102 @@
+package li.cil.oc.api.detail;
+
+import li.cil.oc.api.manual.ContentProvider;
+import li.cil.oc.api.manual.PathProvider;
+import li.cil.oc.api.manual.TabIconRenderer;
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.item.ItemStack;
+import net.minecraft.world.World;
+
+public interface ManualAPI {
+ /**
+ * Register a tab to be displayed next to the manual.
+ *
+ * These are intended to link to index pages, and for the time being there
+ * a relatively low number of tabs that can be displayed, so I'd ask you to
+ * only register as many tabs as actually, technically *needed*. Which will
+ * usually be one, for your main index page.
+ *
+ * @param renderer the renderer used to render the icon on your tab.
+ * @param path the path to the page to open when the tab is clicked.
+ */
+ void addTab(TabIconRenderer renderer, String path);
+
+ /**
+ * Register a path provider.
+ *
+ * Path providers are used to find documentation entries for item stacks
+ * and blocks in the world.
+ *
+ * @param provider the provider to register.
+ */
+ void addProvider(PathProvider provider);
+
+ /**
+ * Register a content provider.
+ *
+ * Content providers are used to resolve paths to page content, if the
+ * standard system (using Minecraft's resource loading facilities) fails.
+ *
+ * This can be useful for providing dynamic content, for example.
+ *
+ * @param provider the provider to register.
+ */
+ void addProvider(ContentProvider provider);
+
+ // ----------------------------------------------------------------------- //
+
+ /**
+ * Look up the documentation path for the specified item stack.
+ *
+ * @param stack the stack to find the documentation path for.
+ * @return the path to the page, null if none is known.
+ */
+ String pathFor(ItemStack stack);
+
+ /**
+ * Look up the documentation for the specified block in the world.
+ *
+ * @param world the world containing the block.
+ * @param x the X coordinate of the block.
+ * @param y the Y coordinate of the block.
+ * @param z the Z coordinate of the block.
+ * @return the path to the page, null if none is known.
+ */
+ String pathFor(World world, int x, int y, int z);
+
+ /**
+ * Get the content of the documentation page at the specified location.
+ *
+ * The provided path may contain the special variable %LANGUAGE%,
+ * which will be resolved to the currently set language, falling back to
+ * en_US.
+ *
+ * @param path the path of the page to get the content of.
+ * @return the content of the page, or null if none exists.
+ */
+ Iterable contentFor(String path);
+
+ // ----------------------------------------------------------------------- //
+
+ /**
+ * Open the manual for the specified player.
+ *
+ * If you wish to display a specific page, call {@link #navigate(String)}
+ * after this function returns, with the path to the page to show.
+ *
+ * @param player the player to open the manual for.
+ */
+ void openFor(EntityPlayer player);
+
+ /**
+ * Reset the history of the manual.
+ */
+ void reset();
+
+ /**
+ * Navigate to a page in the manual.
+ *
+ * @param path the path to navigate to.
+ */
+ void navigate(String path);
+}
diff --git a/src/main/java/li/cil/oc/api/manual/ContentProvider.java b/src/main/java/li/cil/oc/api/manual/ContentProvider.java
new file mode 100644
index 000000000..36c33c5ee
--- /dev/null
+++ b/src/main/java/li/cil/oc/api/manual/ContentProvider.java
@@ -0,0 +1,26 @@
+package li.cil.oc.api.manual;
+
+/**
+ * This interface allows implementation of content providers for the manual.
+ *
+ * Content providers can be used to provide possibly dynamic page content for
+ * arbitrary paths. Note that content providers have lower priority
+ * than content found in resource packs, i.e. content providers will only be
+ * queried for missing pages, so to speak.
+ */
+public interface ContentProvider {
+ /**
+ * Called to get the content of a path pointed to by the specified path.
+ *
+ * This should provide an iterable over the lines of a Markdown document
+ * (with the formatting provided by the in-game manual, which is a small
+ * subset of "normal" Markdown).
+ *
+ * If this provider cannot provide the requested path, it should return
+ * null to indicate so, allowing other providers to be queried.
+ *
+ * @param path the path to the manual page we're looking for.
+ * @return the content of the document at that path, or null.
+ */
+ Iterable getContent(String path);
+}
diff --git a/src/main/java/li/cil/oc/api/manual/PathProvider.java b/src/main/java/li/cil/oc/api/manual/PathProvider.java
new file mode 100644
index 000000000..c31b7f75c
--- /dev/null
+++ b/src/main/java/li/cil/oc/api/manual/PathProvider.java
@@ -0,0 +1,45 @@
+package li.cil.oc.api.manual;
+
+import net.minecraft.item.ItemStack;
+import net.minecraft.world.World;
+
+/**
+ * Allows providing paths for item stacks and blocks in the world.
+ *
+ * This is used for generating NEI usage pages with a button opening the manual
+ * on the page at the specified path, or for opening the manual when held in
+ * hand and sneak-activating a block in the world.
+ *
+ * This way you can easily make entries in your documentation available the
+ * same way OpenComputers does it itself.
+ *
+ * Note that you can use the special variable %LANGUAGE% in your
+ * paths, for language agnostic paths. These will be resolved to the currently
+ * set language, falling back to en_US, during actual content lookup.
+ */
+public interface PathProvider {
+ /**
+ * Get the path to the documentation page for the provided item stack.
+ *
+ * Return null if there is no known page for this item, allowing
+ * other providers to be queried.
+ *
+ * @param stack the stack to get the documentation path to.
+ * @return the path to the page, null if none is known.
+ */
+ String pathFor(ItemStack stack);
+
+ /**
+ * Get the path to the documentation page for the provided block.
+ *
+ * Return null if there is no known page for this item, allowing
+ * other providers to be queried.
+ *
+ * @param world the world containing the block.
+ * @param x the X coordinate of the block.
+ * @param y the Y coordinate of the block.
+ * @param z the Z coordinate of the block.
+ * @return the path to the page, null if none is known.
+ */
+ String pathFor(World world, int x, int y, int z);
+}
diff --git a/src/main/java/li/cil/oc/api/manual/TabIconRenderer.java b/src/main/java/li/cil/oc/api/manual/TabIconRenderer.java
new file mode 100644
index 000000000..95d5ffd52
--- /dev/null
+++ b/src/main/java/li/cil/oc/api/manual/TabIconRenderer.java
@@ -0,0 +1,22 @@
+package li.cil.oc.api.manual;
+
+/**
+ * Allows defining a renderer for a manual tab.
+ *
+ * Each renderer instance represents the single graphic it is drawing. To
+ * provide different graphics for different tabs you'll need to create
+ * multiple tab renderer instances.
+ *
+ * See the prefabs {@link li.cil.oc.api.prefab.ItemStackTabIconRenderer} and
+ * {@link li.cil.oc.api.prefab.IconTabIconRenderer} for simple standard
+ * implementations.
+ */
+public interface TabIconRenderer {
+ /**
+ * Called when icon of a tab should be rendered.
+ *
+ * This should render something in a 16x16 area. The OpenGL state has been
+ * adjusted so that drawing starts at (0,0,0), and should go to (16,16,0).
+ */
+ void render();
+}
diff --git a/src/main/java/li/cil/oc/api/manual/package-info.java b/src/main/java/li/cil/oc/api/manual/package-info.java
new file mode 100644
index 000000000..81ae1eec8
--- /dev/null
+++ b/src/main/java/li/cil/oc/api/manual/package-info.java
@@ -0,0 +1,14 @@
+/**
+ * This package contains manual related interfaces.
+ *
+ * The manual represents the in-game documentation of OpenComputers and any
+ * other mod that may choose to add its documentation to it, such as addon
+ * mods.
+ */
+@cpw.mods.fml.common.API(
+ owner = API.ID_OWNER,
+ provides = "OpenComputersAPI|Manual",
+ apiVersion = API.VERSION)
+package li.cil.oc.api.manual;
+
+import li.cil.oc.api.API;
\ No newline at end of file
diff --git a/src/main/java/li/cil/oc/api/prefab/IconTabIconRenderer.java b/src/main/java/li/cil/oc/api/prefab/IconTabIconRenderer.java
new file mode 100644
index 000000000..66df1d0d7
--- /dev/null
+++ b/src/main/java/li/cil/oc/api/prefab/IconTabIconRenderer.java
@@ -0,0 +1,28 @@
+package li.cil.oc.api.prefab;
+
+import li.cil.oc.api.manual.TabIconRenderer;
+import net.minecraft.client.renderer.Tessellator;
+import net.minecraft.util.IIcon;
+
+/**
+ * Simple implementation of a tab icon renderer using an icon as its graphic.
+ */
+@SuppressWarnings("UnusedDeclaration")
+public class IconTabIconRenderer implements TabIconRenderer {
+ private final IIcon icon;
+
+ public IconTabIconRenderer(IIcon icon) {
+ this.icon = icon;
+ }
+
+ @Override
+ public void render() {
+ final Tessellator t = Tessellator.instance;
+ t.startDrawingQuads();
+ t.addVertexWithUV(0, 16, 0, icon.getMinU(), icon.getMaxV());
+ t.addVertexWithUV(16, 16, 0, icon.getMaxU(), icon.getMaxV());
+ t.addVertexWithUV(16, 0, 0, icon.getMaxU(), icon.getMinV());
+ t.addVertexWithUV(0, 0, 0, icon.getMinU(), icon.getMinV());
+ t.draw();
+ }
+}
diff --git a/src/main/java/li/cil/oc/api/prefab/ItemStackTabIconRenderer.java b/src/main/java/li/cil/oc/api/prefab/ItemStackTabIconRenderer.java
new file mode 100644
index 000000000..6281fd588
--- /dev/null
+++ b/src/main/java/li/cil/oc/api/prefab/ItemStackTabIconRenderer.java
@@ -0,0 +1,31 @@
+package li.cil.oc.api.prefab;
+
+import li.cil.oc.api.manual.TabIconRenderer;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.OpenGlHelper;
+import net.minecraft.client.renderer.RenderHelper;
+import net.minecraft.client.renderer.entity.RenderItem;
+import net.minecraft.item.ItemStack;
+import org.lwjgl.opengl.GL11;
+import org.lwjgl.opengl.GL12;
+
+/**
+ * Simple implementation of a tab icon renderer using an item stack as its graphic.
+ */
+@SuppressWarnings("UnusedDeclaration")
+public class ItemStackTabIconRenderer implements TabIconRenderer {
+ private final ItemStack stack;
+
+ public ItemStackTabIconRenderer(ItemStack stack) {
+ this.stack = stack;
+ }
+
+ @Override
+ public void render() {
+ GL11.glEnable(GL12.GL_RESCALE_NORMAL);
+ RenderHelper.enableGUIStandardItemLighting();
+ OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240, 240);
+ RenderItem.getInstance().renderItemAndEffectIntoGUI(Minecraft.getMinecraft().fontRenderer, Minecraft.getMinecraft().getTextureManager(), stack, 0, 0);
+ RenderHelper.disableStandardItemLighting();
+ }
+}
diff --git a/src/main/java/li/cil/oc/api/prefab/TextureTabIconRenderer.java b/src/main/java/li/cil/oc/api/prefab/TextureTabIconRenderer.java
new file mode 100644
index 000000000..bfb06562b
--- /dev/null
+++ b/src/main/java/li/cil/oc/api/prefab/TextureTabIconRenderer.java
@@ -0,0 +1,30 @@
+package li.cil.oc.api.prefab;
+
+import li.cil.oc.api.manual.TabIconRenderer;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.Tessellator;
+import net.minecraft.util.ResourceLocation;
+
+/**
+ * Simple implementation of a tab icon renderer using a full texture as its graphic.
+ */
+@SuppressWarnings("UnusedDeclaration")
+public class TextureTabIconRenderer implements TabIconRenderer {
+ private final ResourceLocation location;
+
+ public TextureTabIconRenderer(ResourceLocation location) {
+ this.location = location;
+ }
+
+ @Override
+ public void render() {
+ Minecraft.getMinecraft().getTextureManager().bindTexture(location);
+ final Tessellator t = Tessellator.instance;
+ t.startDrawingQuads();
+ t.addVertexWithUV(0, 16, 0, 0, 1);
+ t.addVertexWithUV(16, 16, 0, 1, 1);
+ t.addVertexWithUV(16, 0, 0, 1, 0);
+ t.addVertexWithUV(0, 0, 0, 0, 0);
+ t.draw();
+ }
+}
diff --git a/src/main/resources/assets/opencomputers/textures/gui/manual_tab.png b/src/main/resources/assets/opencomputers/textures/gui/manual_tab.png
new file mode 100644
index 000000000..9a96ae861
Binary files /dev/null and b/src/main/resources/assets/opencomputers/textures/gui/manual_tab.png differ
diff --git a/src/main/scala/li/cil/oc/client/Manual.scala b/src/main/scala/li/cil/oc/client/Manual.scala
new file mode 100644
index 000000000..1c8c6e38f
--- /dev/null
+++ b/src/main/scala/li/cil/oc/client/Manual.scala
@@ -0,0 +1,131 @@
+package li.cil.oc.client
+
+import cpw.mods.fml.common.FMLCommonHandler
+import li.cil.oc.OpenComputers
+import li.cil.oc.api.detail.ManualAPI
+import li.cil.oc.api.manual.ContentProvider
+import li.cil.oc.api.manual.PathProvider
+import li.cil.oc.api.manual.TabIconRenderer
+import li.cil.oc.common.GuiType
+import net.minecraft.client.Minecraft
+import net.minecraft.entity.player.EntityPlayer
+import net.minecraft.item.ItemStack
+import net.minecraft.world.World
+
+import scala.annotation.tailrec
+import scala.collection.convert.WrapAsJava._
+import scala.collection.convert.WrapAsScala._
+import scala.collection.mutable
+
+object Manual extends ManualAPI {
+ final val LanguageKey = "%LANGUAGE%"
+
+ final val FallbackLanguage = "en_US"
+
+ class History(val path: String, var offset: Int = 0)
+
+ class Tab(val renderer: TabIconRenderer, val path: String)
+
+ val tabs = mutable.Buffer.empty[Tab]
+
+ val pathProviders = mutable.Buffer.empty[PathProvider]
+
+ val contentProviders = mutable.Buffer.empty[ContentProvider]
+
+ val history = new mutable.Stack[History]
+
+ reset()
+
+ override def addTab(renderer: TabIconRenderer, path: String): Unit = {
+ tabs += new Tab(renderer, path)
+ }
+
+ override def addProvider(provider: PathProvider): Unit = {
+ pathProviders += provider
+ }
+
+ override def addProvider(provider: ContentProvider): Unit = {
+ contentProviders += provider
+ }
+
+ override def pathFor(stack: ItemStack): String = {
+ for (provider <- pathProviders) {
+ val path = try provider.pathFor(stack) catch {
+ case t: Throwable =>
+ OpenComputers.log.warn("A path provider threw an error when queried with an item.", t)
+ null
+ }
+ if (path != null) return path
+ }
+ null
+ }
+
+ override def pathFor(world: World, x: Int, y: Int, z: Int): String = {
+ for (provider <- pathProviders) {
+ val path = try provider.pathFor(world, x, y, z) catch {
+ case t: Throwable =>
+ OpenComputers.log.warn("A path provider threw an error when queried with a block.", t)
+ null
+ }
+ if (path != null) return path
+ }
+ null
+ }
+
+ override def contentFor(path: String): java.lang.Iterable[String] = {
+ val language = FMLCommonHandler.instance.getCurrentLanguage
+ contentForWithRedirects(path.replaceAll(LanguageKey, language)).
+ orElse(contentForWithRedirects(path.replaceAll(LanguageKey, FallbackLanguage))).
+ getOrElse(asJavaIterable(Iterable("Document not found: " + path)))
+ }
+
+ override def openFor(player: EntityPlayer): Unit = {
+ if (player.getEntityWorld.isRemote) {
+ player.openGui(OpenComputers, GuiType.Manual.id, player.getEntityWorld, 0, 0, 0)
+ }
+ }
+
+ def reset(): Unit = {
+ history.clear()
+ history.push(new History(s"doc/$LanguageKey/index.md"))
+ }
+
+ override def navigate(path: String): Unit = {
+ Minecraft.getMinecraft.currentScreen match {
+ case manual: gui.Manual => manual.pushPage(path)
+ case _ => history.push(new History(path))
+ }
+ }
+
+ def makeRelative(path: String, base: String): String =
+ if (path.startsWith("/")) path
+ else {
+ val splitAt = base.lastIndexOf('/')
+ if (splitAt >= 0) base.splitAt(splitAt)._1 + "/" + path
+ else path
+ }
+
+ @tailrec private def contentForWithRedirects(path: String, seen: List[String] = List.empty): Option[java.lang.Iterable[String]] = {
+ if (seen.contains(path)) return Some(asJavaIterable(Iterable("Redirection loop: ") ++ seen ++ Iterable(path)))
+ doContentLookup(path) match {
+ case Some(content) => content.headOption match {
+ case Some(line) if line.toLowerCase.startsWith("#redirect ") =>
+ contentForWithRedirects(makeRelative(line.substring("#redirect ".length), path), seen :+ path)
+ case _ => Some(content)
+ }
+ case _ => None
+ }
+ }
+
+ private def doContentLookup(path: String): Option[java.lang.Iterable[String]] = {
+ for (provider <- contentProviders) {
+ val lines = try provider.getContent(path) catch {
+ case t: Throwable =>
+ OpenComputers.log.warn("A content provider threw an error when queried.", t)
+ null
+ }
+ if (lines != null) return Some(lines)
+ }
+ None
+ }
+}
diff --git a/src/main/scala/li/cil/oc/client/Proxy.scala b/src/main/scala/li/cil/oc/client/Proxy.scala
index 1921263ad..504231f5f 100644
--- a/src/main/scala/li/cil/oc/client/Proxy.scala
+++ b/src/main/scala/li/cil/oc/client/Proxy.scala
@@ -9,6 +9,7 @@ import cpw.mods.fml.common.network.NetworkRegistry
import li.cil.oc.Constants
import li.cil.oc.OpenComputers
import li.cil.oc.Settings
+import li.cil.oc.api
import li.cil.oc.client
import li.cil.oc.client.renderer.HighlightRenderer
import li.cil.oc.client.renderer.PetRenderer
@@ -33,6 +34,8 @@ private[oc] class Proxy extends CommonProxy {
override def preInit(e: FMLPreInitializationEvent) {
super.preInit(e)
+ api.API.manual = client.Manual
+
MinecraftForge.EVENT_BUS.register(Sound)
MinecraftForge.EVENT_BUS.register(gui.Icons)
MinecraftForge.EVENT_BUS.register(HighlightRenderer)
diff --git a/src/main/scala/li/cil/oc/client/Textures.scala b/src/main/scala/li/cil/oc/client/Textures.scala
index e0815f42b..0b8b3126d 100644
--- a/src/main/scala/li/cil/oc/client/Textures.scala
+++ b/src/main/scala/li/cil/oc/client/Textures.scala
@@ -26,6 +26,7 @@ object Textures {
val guiDrone = new ResourceLocation(Settings.resourceDomain, "textures/gui/drone.png")
val guiKeyboardMissing = new ResourceLocation(Settings.resourceDomain, "textures/gui/keyboard_missing.png")
val guiManual = new ResourceLocation(Settings.resourceDomain, "textures/gui/manual.png")
+ val guiManualTab = new ResourceLocation(Settings.resourceDomain, "textures/gui/manual_tab.png")
val guiPrinter = new ResourceLocation(Settings.resourceDomain, "textures/gui/printer.png")
val guiPrinterInk = new ResourceLocation(Settings.resourceDomain, "textures/gui/printer_ink.png")
val guiPrinterMaterial = new ResourceLocation(Settings.resourceDomain, "textures/gui/printer_material.png")
diff --git a/src/main/scala/li/cil/oc/client/gui/Manual.scala b/src/main/scala/li/cil/oc/client/gui/Manual.scala
index 48880b542..0cd100c71 100644
--- a/src/main/scala/li/cil/oc/client/gui/Manual.scala
+++ b/src/main/scala/li/cil/oc/client/gui/Manual.scala
@@ -1,44 +1,23 @@
package li.cil.oc.client.gui
-import java.io.FileNotFoundException
-import java.io.InputStream
import java.net.URI
import java.util
-import com.google.common.base.Charsets
-import cpw.mods.fml.common.FMLCommonHandler
import li.cil.oc.Localization
-import li.cil.oc.Settings
+import li.cil.oc.api
+import li.cil.oc.client.Manual
import li.cil.oc.client.Textures
import li.cil.oc.util.PseudoMarkdown
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.Gui
+import net.minecraft.client.gui.GuiButton
import net.minecraft.client.gui.GuiScreen
import net.minecraft.client.gui.ScaledResolution
-import net.minecraft.util.ResourceLocation
import org.lwjgl.input.Mouse
+import org.lwjgl.opengl.GL11
import scala.collection.convert.WrapAsJava._
-import scala.collection.mutable
-import scala.io.Source
-
-object Manual {
- final val LanguageKey = "%LANGUAGE%"
-
- val history = new mutable.Stack[History]
-
- reset()
-
- def reset(): Unit = {
- history.clear()
- history.push(new History(s"doc/$LanguageKey/index.md"))
- }
-
- class History(val path: String) {
- var offset = 0
- }
-
-}
+import scala.collection.convert.WrapAsScala._
class Manual extends GuiScreen {
final val documentMaxWidth = 230
@@ -47,6 +26,10 @@ class Manual extends GuiScreen {
final val scrollPosY = 6
final val scrollWidth = 6
final val scrollHeight = 180
+ final val tabPosX = -23
+ final val tabPosY = 7
+ final val tabWidth = 23
+ final val tabHeight = 26
var guiLeft = 0
var guiTop = 0
@@ -74,37 +57,8 @@ class Manual extends GuiScreen {
else path
}
- def loadPage(path: String, localized: Boolean = false, seen: List[String] = List.empty): Iterator[String] = {
- val language = FMLCommonHandler.instance.getCurrentLanguage
- val resolvedPath = path.replaceAll(Manual.LanguageKey, language)
- if (seen.contains(resolvedPath)) return Iterator("Redirection loop: ") ++ seen.iterator ++ Iterator(path)
- val location = new ResourceLocation(Settings.resourceDomain, resolvedPath)
- var is: InputStream = null
- try {
- val resource = Minecraft.getMinecraft.getResourceManager.getResource(location)
- is = resource.getInputStream
- // Force resolving immediately via toArray, otherwise we return a read
- // iterator on a closed input stream (because of the finally).
- val lines = Source.fromInputStream(is)(Charsets.UTF_8).getLines().toArray
- lines.headOption match {
- case Some(line) if line.toLowerCase.startsWith("#redirect ") =>
- loadPage(resolveLink(line.substring("#redirect ".length), resolvedPath), localized = false, seen :+ path)
- case _ => lines.iterator
- }
- }
- catch {
- case e: FileNotFoundException if !localized && language != "en_US" =>
- loadPage(path.replaceAll(Manual.LanguageKey, "en_US"), localized = true, seen)
- case t: Throwable =>
- Iterator(s"Failed loading page '$path':") ++ t.toString.lines
- }
- finally {
- Option(is).foreach(_.close())
- }
- }
-
def refreshPage(): Unit = {
- document = PseudoMarkdown.parse(loadPage(Manual.history.top.path))
+ document = PseudoMarkdown.parse(api.Manual.contentFor(Manual.history.top.path))
documentHeight = PseudoMarkdown.height(document, documentMaxWidth, fontRendererObj)
scrollTo(offset)
}
@@ -129,6 +83,12 @@ class Manual extends GuiScreen {
override def doesGuiPauseGame = false
+ override def actionPerformed(button: GuiButton): Unit = {
+ if (button.id >= 0 && button.id < Manual.tabs.length) {
+ pushPage(Manual.tabs(button.id).path)
+ }
+ }
+
override def initGui(): Unit = {
super.initGui()
@@ -141,9 +101,15 @@ class Manual extends GuiScreen {
xSize = guiSize.getScaledWidth
ySize = guiSize.getScaledHeight
- scrollButton = new ImageButton(1, guiLeft + scrollPosX, guiTop + scrollPosY, 6, 13, Textures.guiButtonScroll)
+ scrollButton = new ImageButton(-1, guiLeft + scrollPosX, guiTop + scrollPosY, 6, 13, Textures.guiButtonScroll)
add(buttonList, scrollButton)
+ for ((tab, i) <- Manual.tabs.zipWithIndex if i < 7) {
+ val x = guiLeft + tabPosX
+ val y = guiTop + tabPosY + i * (tabHeight - 1)
+ add(buttonList, new ImageButton(0, x, y, tabWidth, tabHeight, Textures.guiManualTab))
+ }
+
refreshPage()
}
@@ -156,6 +122,15 @@ class Manual extends GuiScreen {
super.drawScreen(mouseX, mouseY, dt)
+ for ((tab, i) <- Manual.tabs.zipWithIndex if i < 7) {
+ val x = guiLeft + tabPosX
+ val y = guiTop + tabPosY + i * (tabHeight - 1)
+ GL11.glPushMatrix()
+ GL11.glTranslated(x + 5, y + 5, zLevel)
+ tab.renderer.render()
+ GL11.glPopMatrix()
+ }
+
PseudoMarkdown.render(document, guiLeft + 8, guiTop + 8, documentMaxWidth, documentMaxHeight, offset, fontRendererObj, mouseX, mouseY) match {
case Some(segment) =>
segment.tooltip match {
diff --git a/src/main/scala/li/cil/oc/common/item/Manual.scala b/src/main/scala/li/cil/oc/common/item/Manual.scala
index 742dbbf14..4c60fa9a7 100644
--- a/src/main/scala/li/cil/oc/common/item/Manual.scala
+++ b/src/main/scala/li/cil/oc/common/item/Manual.scala
@@ -1,13 +1,7 @@
package li.cil.oc.common.item
-import li.cil.oc.OpenComputers
import li.cil.oc.api
-import li.cil.oc.client.gui
-import li.cil.oc.common.GuiType
-import li.cil.oc.common.block.SimpleBlock
import li.cil.oc.util.BlockPosition
-import li.cil.oc.util.ExtendedWorld._
-import net.minecraft.client.Minecraft
import net.minecraft.entity.player.EntityPlayer
import net.minecraft.item.ItemStack
import net.minecraft.world.World
@@ -16,26 +10,21 @@ class Manual(val parent: Delegator) extends Delegate {
override def onItemRightClick(stack: ItemStack, world: World, player: EntityPlayer): ItemStack = {
if (world.isRemote) {
if (player.isSneaking) {
- gui.Manual.reset()
+ api.Manual.reset()
}
- player.openGui(OpenComputers, GuiType.Manual.id, world, 0, 0, 0)
+ api.Manual.openFor(player)
}
super.onItemRightClick(stack, world, player)
}
override def onItemUse(stack: ItemStack, player: EntityPlayer, position: BlockPosition, side: Int, hitX: Float, hitY: Float, hitZ: Float): Boolean = {
val world = player.getEntityWorld
- world.getBlock(position) match {
- case block: SimpleBlock =>
+ api.Manual.pathFor(world, position.x, position.y, position.z) match {
+ case path: String =>
if (world.isRemote) {
- player.openGui(OpenComputers, GuiType.Manual.id, world, 0, 0, 0)
- Minecraft.getMinecraft.currentScreen match {
- case manual: gui.Manual =>
- gui.Manual.reset()
- val descriptor = api.Items.get(new ItemStack(block))
- manual.pushPage("block/" + descriptor.name + ".md")
- case _ =>
- }
+ api.Manual.openFor(player)
+ api.Manual.reset()
+ api.Manual.navigate(path)
}
true
case _ => super.onItemUse(stack, player, position, side, hitX, hitY, hitZ)
diff --git a/src/main/scala/li/cil/oc/integration/nei/ManualUsageHandler.scala b/src/main/scala/li/cil/oc/integration/nei/ManualUsageHandler.scala
index cacc9f594..c102a87be 100644
--- a/src/main/scala/li/cil/oc/integration/nei/ManualUsageHandler.scala
+++ b/src/main/scala/li/cil/oc/integration/nei/ManualUsageHandler.scala
@@ -8,10 +8,7 @@ import codechicken.nei.api.IOverlayHandler
import codechicken.nei.api.IRecipeOverlayRenderer
import codechicken.nei.recipe.GuiRecipe
import codechicken.nei.recipe.IUsageHandler
-import li.cil.oc.OpenComputers
import li.cil.oc.api
-import li.cil.oc.client.gui
-import li.cil.oc.common.GuiType
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiButton
import net.minecraft.client.gui.inventory.GuiContainer
@@ -30,9 +27,8 @@ class ManualUsageHandler(path: Option[String]) extends IUsageHandler {
override def getUsageHandler(input: String, ingredients: AnyRef*): IUsageHandler = {
if (input == "item") {
ingredients.collectFirst {
- case stack: ItemStack if api.Items.get(stack) != null =>
- val descriptor = api.Items.get(stack)
- new ManualUsageHandler(Option((if (descriptor.block != null) "block/" else "item/") + descriptor.name + ".md"))
+ case stack: ItemStack if api.Manual.pathFor(stack) != null =>
+ new ManualUsageHandler(Option(api.Manual.pathFor(stack)))
}.getOrElse(this)
}
else this
@@ -76,11 +72,8 @@ class ManualUsageHandler(path: Option[String]) extends IUsageHandler {
val pos = GuiDraw.getMousePosition
val mc = Minecraft.getMinecraft
if (button.mousePressed(mc, pos.x - container.guiLeft - 5, pos.y - container.guiTop - 16)) {
- mc.thePlayer.openGui(OpenComputers, GuiType.Manual.id, mc.theWorld, 0, 0, 0)
- mc.currentScreen match {
- case manual: gui.Manual => path.foreach(manual.pushPage)
- case _ =>
- }
+ api.Manual.openFor(mc.thePlayer)
+ path.foreach(api.Manual.navigate)
true
}
else false
diff --git a/src/main/scala/li/cil/oc/integration/opencomputers/ModOpenComputers.scala b/src/main/scala/li/cil/oc/integration/opencomputers/ModOpenComputers.scala
index e2136a7a3..92a90ea1a 100644
--- a/src/main/scala/li/cil/oc/integration/opencomputers/ModOpenComputers.scala
+++ b/src/main/scala/li/cil/oc/integration/opencomputers/ModOpenComputers.scala
@@ -1,15 +1,24 @@
package li.cil.oc.integration.opencomputers
+import java.io.InputStream
+import java.lang.Iterable
+
+import com.google.common.base.Charsets
import cpw.mods.fml.common.FMLCommonHandler
import cpw.mods.fml.common.event.FMLInterModComms
import li.cil.oc.Constants
import li.cil.oc.OpenComputers
+import li.cil.oc.Settings
import li.cil.oc.api
+import li.cil.oc.api.detail.ItemInfo
import li.cil.oc.api.internal
+import li.cil.oc.api.manual.ContentProvider
+import li.cil.oc.api.manual.PathProvider
import li.cil.oc.common.EventHandler
import li.cil.oc.common.Loot
import li.cil.oc.common.SaveHandler
import li.cil.oc.common.asm.SimpleComponentTickHandler
+import li.cil.oc.common.block.SimpleBlock
import li.cil.oc.common.event._
import li.cil.oc.common.item.Analyzer
import li.cil.oc.common.item.Delegator
@@ -22,10 +31,17 @@ import li.cil.oc.integration.util.BundledRedstone
import li.cil.oc.integration.util.WirelessRedstone
import li.cil.oc.server.network.WirelessNetwork
import li.cil.oc.util.ExtendedNBT._
+import net.minecraft.client.Minecraft
+import net.minecraft.item.ItemStack
import net.minecraft.nbt.NBTTagCompound
+import net.minecraft.util.ResourceLocation
+import net.minecraft.world.World
import net.minecraftforge.common.ForgeChunkManager
import net.minecraftforge.common.MinecraftForge
+import scala.collection.convert.WrapAsJava._
+import scala.io.Source
+
object ModOpenComputers extends ModProxy {
override def getMod = Mods.OpenComputers
@@ -188,6 +204,9 @@ object ModOpenComputers extends ModProxy {
case _ =>
}
}
+
+ api.Manual.addProvider(DefinitionPathProvider)
+ api.Manual.addProvider(ResourceContentProvider)
}
private def blacklistHost(host: Class[_], itemNames: String*) {
@@ -199,4 +218,46 @@ object ModOpenComputers extends ModProxy {
FMLInterModComms.sendMessage("OpenComputers", "blacklistHost", nbt)
}
}
+
+ object DefinitionPathProvider extends PathProvider {
+ private final val Blacklist = Set(
+ "debugger"
+ )
+
+ override def pathFor(stack: ItemStack): String = Option(api.Items.get(stack)) match {
+ case Some(definition) => checkBlacklisted(definition)
+ case _ => null
+ }
+
+ override def pathFor(world: World, x: Int, y: Int, z: Int): String = world.getBlock(x, y, z) match {
+ case block: SimpleBlock => checkBlacklisted(api.Items.get(new ItemStack(block)))
+ case _ => null
+ }
+
+ private def checkBlacklisted(info: ItemInfo): String =
+ if (info == null || Blacklist.contains(info.name)) null
+ else if (info.block != null) "block/" + info.name + ".md"
+ else "item/" + info.name + ".md"
+ }
+
+ object ResourceContentProvider extends ContentProvider {
+ override def getContent(path: String): Iterable[String] = {
+ val location = new ResourceLocation(Settings.resourceDomain, path)
+ var is: InputStream = null
+ try {
+ val resource = Minecraft.getMinecraft.getResourceManager.getResource(location)
+ is = resource.getInputStream
+ // Force resolving immediately via toArray, otherwise we return a read
+ // iterator on a closed input stream (because of the finally).
+ asJavaIterable(Source.fromInputStream(is)(Charsets.UTF_8).getLines().toArray.toIterable)
+ }
+ catch {
+ case t: Throwable => null
+ }
+ finally {
+ Option(is).foreach(_.close())
+ }
+ }
+ }
+
}
diff --git a/src/main/scala/li/cil/oc/util/PseudoMarkdown.scala b/src/main/scala/li/cil/oc/util/PseudoMarkdown.scala
index cf33ba3f3..0af5cf10f 100644
--- a/src/main/scala/li/cil/oc/util/PseudoMarkdown.scala
+++ b/src/main/scala/li/cil/oc/util/PseudoMarkdown.scala
@@ -35,7 +35,7 @@ object PseudoMarkdown {
/**
* Parses a plain text document into a list of segments.
*/
- def parse(document: Iterator[String]): Iterable[Segment] = {
+ def parse(document: Iterable[String]): Iterable[Segment] = {
var segments = document.flatMap(line => Iterable(new TextSegment(null, Option(line).getOrElse("")), new NewLineSegment())).toArray
for ((pattern, factory) <- segmentTypes) {
segments = segments.flatMap(_.refine(pattern, factory))