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))