diff --git a/assets/items.psd b/assets/items.psd index b8354d978..42bc35c6c 100644 Binary files a/assets/items.psd and b/assets/items.psd differ diff --git a/src/main/resources/assets/opencomputers/textures/gui/manual.png b/src/main/resources/assets/opencomputers/textures/gui/manual.png new file mode 100644 index 000000000..95322e531 Binary files /dev/null and b/src/main/resources/assets/opencomputers/textures/gui/manual.png differ diff --git a/src/main/resources/assets/opencomputers/textures/items/Manual.png b/src/main/resources/assets/opencomputers/textures/items/Manual.png new file mode 100644 index 000000000..2a9960f23 Binary files /dev/null and b/src/main/resources/assets/opencomputers/textures/items/Manual.png differ diff --git a/src/main/scala/li/cil/oc/Constants.scala b/src/main/scala/li/cil/oc/Constants.scala index da34a0c77..175828dca 100644 --- a/src/main/scala/li/cil/oc/Constants.scala +++ b/src/main/scala/li/cil/oc/Constants.scala @@ -102,6 +102,7 @@ object Constants { final val LinkedCard = "linkedCard" final val LootDisk = "lootDisk" final val LuaBios = "luaBios" + final val Manual = "manual" final val MicrocontrollerCaseCreative = "microcontrollerCaseCreative" final val MicrocontrollerCaseTier1 = "microcontrollerCase1" final val MicrocontrollerCaseTier2 = "microcontrollerCase2" diff --git a/src/main/scala/li/cil/oc/client/GuiHandler.scala b/src/main/scala/li/cil/oc/client/GuiHandler.scala index 0352fe976..12a39c0ca 100644 --- a/src/main/scala/li/cil/oc/client/GuiHandler.scala +++ b/src/main/scala/li/cil/oc/client/GuiHandler.scala @@ -54,6 +54,8 @@ object GuiHandler extends CommonGuiHandler { } case Some(GuiType.Category.Item) => Delegator.subItem(player.getCurrentEquippedItem) match { + case Some(manual: item.Manual) if id == GuiType.Manual.id => + new gui.Manual() case Some(database: item.UpgradeDatabase) if id == GuiType.Database.id => new gui.Database(player.inventory, new DatabaseInventory { override def tier = database.tier diff --git a/src/main/scala/li/cil/oc/client/Textures.scala b/src/main/scala/li/cil/oc/client/Textures.scala index 888a66207..e0815f42b 100644 --- a/src/main/scala/li/cil/oc/client/Textures.scala +++ b/src/main/scala/li/cil/oc/client/Textures.scala @@ -25,6 +25,7 @@ object Textures { val guiDisassembler = new ResourceLocation(Settings.resourceDomain, "textures/gui/disassembler.png") 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 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 new file mode 100644 index 000000000..d14faea12 --- /dev/null +++ b/src/main/scala/li/cil/oc/client/gui/Manual.scala @@ -0,0 +1,48 @@ +package li.cil.oc.client.gui + +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.GuiScreen +import net.minecraft.client.gui.ScaledResolution + +class Manual extends GuiScreen { + val document = PseudoMarkdown.parse( """# Headline + | + |The Adapter block is the core of most of OpenComputers' mod integration. + | + |*This* is *italic* text, ~~strikethrough~~ maybe a-ter **some** text **in bold**. Is _this underlined_? Oh, no, _it's also italic!_ Well, this \*isn't bold*. + | + |## Smaller headline + | + |This is *italic + |over two* lines. But *this ... no *this is* **_bold italic_** *text*. + | + |### even smaller + | + |*not italic *because ** why would it be*eh + | + |isn't*. + | + | # not a header + | + |![](https://avatars1.githubusercontent.com/u/514903) + | + |And finally, [this is a link!](https://avatars1.githubusercontent.com/u/514903).""".stripMargin) + + override def drawScreen(mouseX: Int, mouseY: Int, dt: Float): Unit = { + val mc = Minecraft.getMinecraft + val screenSize = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight) + val guiSize = new ScaledResolution(mc, 256, 192) + val (midX, midY) = (screenSize.getScaledWidth / 2, screenSize.getScaledHeight / 2) + val (left, top) = (midX - guiSize.getScaledWidth / 2, midY - guiSize.getScaledHeight / 2) + + mc.renderEngine.bindTexture(Textures.guiManual) + Gui.func_146110_a(left, top, 0, 0, guiSize.getScaledWidth, guiSize.getScaledHeight, 256, 192) + + super.drawScreen(mouseX, mouseY, dt) + + PseudoMarkdown.render(document, left + 8, top + 8, 220, 176, 0, fontRendererObj) + } +} diff --git a/src/main/scala/li/cil/oc/common/GuiType.scala b/src/main/scala/li/cil/oc/common/GuiType.scala index 17927f40b..707a57428 100644 --- a/src/main/scala/li/cil/oc/common/GuiType.scala +++ b/src/main/scala/li/cil/oc/common/GuiType.scala @@ -21,6 +21,7 @@ object GuiType extends ScalaEnum { val Disassembler = new EnumVal { def name = "Disassembler"; def subType = GuiType.Category.Block } val DiskDrive = new EnumVal { def name = "DiskDrive"; def subType = GuiType.Category.Block } val Drone = new EnumVal { def name = "Drone"; def subType = GuiType.Category.Entity } + val Manual = new EnumVal { def name = "Manual"; def subType = GuiType.Category.Item } val Printer = new EnumVal { def name = "Printer"; def subType = GuiType.Category.Block } val Rack = new EnumVal { def name = "Rack"; def subType = GuiType.Category.Block } val Raid = new EnumVal { def name = "Raid"; def subType = GuiType.Category.Block } diff --git a/src/main/scala/li/cil/oc/common/init/Items.scala b/src/main/scala/li/cil/oc/common/init/Items.scala index 7d44dc8aa..f8c758e2a 100644 --- a/src/main/scala/li/cil/oc/common/init/Items.scala +++ b/src/main/scala/li/cil/oc/common/init/Items.scala @@ -458,5 +458,8 @@ object Items extends ItemAPI { Recipes.addSubItem(new item.InkCartridge(multi), Constants.ItemName.InkCartridge, "oc:inkCartridge") Recipes.addSubItem(new item.Chamelium(multi), Constants.ItemName.Chamelium, "oc:chamelium") Recipes.addSubItem(new item.TexturePicker(multi), Constants.ItemName.TexturePicker, "oc:texturePicker") + + // 1.5.7 + Recipes.addSubItem(new item.Manual(multi), Constants.ItemName.Manual, "oc:manual") } } diff --git a/src/main/scala/li/cil/oc/common/item/Manual.scala b/src/main/scala/li/cil/oc/common/item/Manual.scala new file mode 100644 index 000000000..5d68e219f --- /dev/null +++ b/src/main/scala/li/cil/oc/common/item/Manual.scala @@ -0,0 +1,14 @@ +package li.cil.oc.common.item + +import li.cil.oc.OpenComputers +import li.cil.oc.common.GuiType +import net.minecraft.entity.player.EntityPlayer +import net.minecraft.item.ItemStack +import net.minecraft.world.World + +class Manual(val parent: Delegator) extends Delegate { + override def onItemRightClick(stack: ItemStack, world: World, player: EntityPlayer): ItemStack = { + player.openGui(OpenComputers, GuiType.Manual.id, world, 0, 0, 0) + super.onItemRightClick(stack, world, player) + } +} diff --git a/src/main/scala/li/cil/oc/util/PseudoMarkdown.scala b/src/main/scala/li/cil/oc/util/PseudoMarkdown.scala new file mode 100644 index 000000000..d42b46096 --- /dev/null +++ b/src/main/scala/li/cil/oc/util/PseudoMarkdown.scala @@ -0,0 +1,243 @@ +package li.cil.oc.util + +import net.minecraft.client.gui.FontRenderer +import net.minecraft.util.EnumChatFormatting +import org.lwjgl.opengl.GL11 + +import scala.collection.mutable +import scala.util.matching.Regex + +/** + * Primitive Markdown parser, only supports a very small subset. Used for + * parsing documentation into segments, to be displayed in a GUI somewhere. + */ +object PseudoMarkdown { + /** + * Parses a plain text document into a list of segments. + */ + def parse(document: String): Iterable[Segment] = { + var segments = document.lines.flatMap(line => Iterable(new TextSegment(null, line), new NewLineSegment())).toArray + for ((pattern, factory) <- segmentTypes) { + segments = segments.flatMap(_.refine(pattern, factory)) + } + segments + } + + /** + * Renders a list of segments and tooltips if a segment with a tooltip is hovered. + * Returns a link address if a link is hovered. + */ + def render(document: Iterable[Segment], x: Int, y: Int, maxWidth: Int, height: Int, offset: Int, renderer: FontRenderer): Option[String] = { + var currentX = 0 + var currentY = 0 + for (segment <- document) { + if (currentY >= offset) { + segment.render(x, y + currentY, currentX, maxWidth, renderer) + } + currentY += segment.height(currentX, maxWidth, renderer) - renderer.FONT_HEIGHT + currentX = segment.width(currentX, maxWidth, renderer) + } + + None + } + + // ----------------------------------------------------------------------- // + + trait Segment { + // Used when rendering, to compute the style of a nested segment. + protected def parent: Segment + + // Used during construction, checks a segment for inner segments. + private[PseudoMarkdown] def refine(pattern: Regex, factory: (Segment, Regex.Match) => Segment): Iterable[Segment] = Iterable(this) + + /** + * Computes the height of this segment, in pixels, given it starts at the + * specified indent into the current line, with the specified maximum + * allowed width. + */ + def height(indent: Int, maxWidth: Int, renderer: FontRenderer): Int = 0 + + /** + * Computes the width of the last line of this segment, given it starts + * at the specified indent into the current line, with the specified + * maximum allowed width. + * If the segment remains on the same line, returns the new end of the + * line (i.e. indent plus width of the segment). + */ + def width(indent: Int, maxWidth: Int, renderer: FontRenderer): Int = 0 + + def render(x: Int, y: Int, indent: Int, width: Int, renderer: FontRenderer): Unit = {} + } + + // ----------------------------------------------------------------------- // + + private class TextSegment(protected val parent: Segment, val text: String) extends Segment { + override def refine(pattern: Regex, factory: (Segment, Regex.Match) => Segment): Iterable[Segment] = { + val result = mutable.Buffer.empty[Segment] + + // Keep track of last matches end, to generate plain text segments. + var textStart = 0 + for (m <- pattern.findAllMatchIn(text)) { + // Create segment for leading plain text. + if (m.start > textStart) { + result += new TextSegment(this, text.substring(textStart, m.start)) + } + textStart = m.end + + // Create segment for formatted text. + result += factory(this, m) + } + + // Create segment for remaining plain text. + if (textStart == 0) { + result += this + } + else if (textStart < text.length) { + result += new TextSegment(this, text.substring(textStart)) + } + result + } + + override def height(indent: Int, maxWidth: Int, renderer: FontRenderer): Int = { + var lines = 1 + var chars = text + var lineChars = maxChars(chars, maxWidth - indent, renderer) + while (chars.length > lineChars) { + lines += 1 + chars = chars.drop(lineChars) + lineChars = maxChars(chars, maxWidth, renderer) + } + lines * renderer.FONT_HEIGHT + } + + override def width(indent: Int, maxWidth: Int, renderer: FontRenderer): Int = { + var currentX = indent + var chars = text + var lineChars = maxChars(chars, maxWidth - indent, renderer) + while (chars.length > lineChars) { + chars = chars.drop(lineChars) + lineChars = maxChars(chars, maxWidth, renderer) + currentX = 0 + } + currentX + renderer.getStringWidth(fullFormat + chars) + } + + override def render(x: Int, y: Int, indent: Int, maxWidth: Int, renderer: FontRenderer): Unit = { + var currentX = x + indent + var currentY = y + var chars = text + var numChars = maxChars(chars, maxWidth - indent, renderer) + while (chars.length > 0) { + renderer.drawString(fullFormat + chars.take(numChars), currentX, currentY, 0xFFFFFF) + currentX = x + currentY += renderer.FONT_HEIGHT + chars = chars.drop(numChars) + numChars = maxChars(chars, maxWidth, renderer) + } + } + + protected def format = "" + + protected def stringWidth(s: String, renderer: FontRenderer): Int = renderer.getStringWidth(s) + + private def fullFormat = parent match { + case segment: TextSegment => segment.format + format + case _ => format + } + + private def maxChars(s: String, maxWidth: Int, renderer: FontRenderer): Int = { + val breaks = Set(' ', '-', '.', '+', '*', '_', '/') + var pos = 1 + var lastBreak = -1 + while (pos < s.length) { + val width = stringWidth(fullFormat + s.take(pos), renderer) + if (breaks.contains(s.charAt(pos))) lastBreak = pos + if (width > maxWidth) return lastBreak + 1 + pos += 1 + } + pos + } + + override def toString: String = s"{TextSegment: text = $text}" + } + + private class HeaderSegment(parent: Segment, text: String, val level: Int) extends TextSegment(parent, text) { + private def scale = math.max(2, 5 - level) / 2f + + override protected def format = EnumChatFormatting.BOLD.toString + + override protected def stringWidth(s: String, renderer: FontRenderer): Int = (super.stringWidth(s, renderer) * scale).toInt + + override def height(indent: Int, maxWidth: Int, renderer: FontRenderer): Int = (super.height(indent, maxWidth, renderer) * scale).toInt + + override def render(x: Int, y: Int, indent: Int, maxWidth: Int, renderer: FontRenderer): Unit = { + GL11.glPushMatrix() + GL11.glTranslatef(x, y, 0) + GL11.glScalef(scale, scale, scale) + GL11.glTranslatef(-x, -y, 0) + super.render(x, y, indent, maxWidth, renderer) + GL11.glPopMatrix() + } + + override def toString: String = s"{HeaderSegment: text = $text, level = $level}" + } + + private class LinkSegment(parent: Segment, text: String, val url: String) extends TextSegment(parent, text) { + override def toString: String = s"{LinkSegment: text = $text, url = $url}" + } + + private class BoldSegment(parent: Segment, text: String) extends TextSegment(parent, text) { + override protected def format = EnumChatFormatting.BOLD.toString + + override def toString: String = s"{BoldSegment: text = $text}" + } + + private class ItalicSegment(parent: Segment, text: String) extends TextSegment(parent, text) { + override protected def format = EnumChatFormatting.ITALIC.toString + + override def toString: String = s"{ItalicSegment: text = $text}" + } + + private class StrikethroughSegment(parent: Segment, text: String) extends TextSegment(parent, text) { + override protected def format = EnumChatFormatting.STRIKETHROUGH.toString + + override def toString: String = s"{StrikethroughSegment: text = $text}" + } + + private class ImageSegment(val parent: Segment, val tooltip: String, val url: String) extends Segment { + override def toString: String = s"{ImageSegment: tooltip = $tooltip, url = $url}" + } + + private class NewLineSegment extends Segment { + override protected def parent: Segment = null + + override def height(indent: Int, maxWidth: Int, renderer: FontRenderer): Int = renderer.FONT_HEIGHT * 2 + + override def toString: String = s"{NewLineSegment}" + } + + // ----------------------------------------------------------------------- // + + private def HeaderSegment(s: Segment, m: Regex.Match) = new HeaderSegment(s, m.group(2), m.group(1).length) + + private def LinkSegment(s: Segment, m: Regex.Match) = new LinkSegment(s, m.group(1), m.group(2)) + + private def BoldSegment(s: Segment, m: Regex.Match) = new BoldSegment(s, m.group(2)) + + private def ItalicSegment(s: Segment, m: Regex.Match) = new ItalicSegment(s, m.group(2)) + + private def StrikethroughSegment(s: Segment, m: Regex.Match) = new StrikethroughSegment(s, m.group(1)) + + private def ImageSegment(s: Segment, m: Regex.Match) = new ImageSegment(s, m.group(1), m.group(2)) + + // ----------------------------------------------------------------------- // + + private val segmentTypes = Array( + """^(#+)\s(.*)""".r -> HeaderSegment _, // headers: # ... + """!\[([^\[]*)\]\(([^\)]+)\)""".r -> ImageSegment _, // images: ![...](...) + """\[([^\[]+)\]\(([^\)]+)\)""".r -> LinkSegment _, // links: [...](...) + """(\*\*|__)(\S.*?\S|$)\1""".r -> BoldSegment _, // bold: **...** | __...__ + """(\*|_)(\S.*?\S|$)\1""".r -> ItalicSegment _, // italic: *...* | _..._ + """~~(\S.*?\S|$)~~""".r -> StrikethroughSegment _ // strikethrough: ~~...~~ + ) +}