Skip to content

Commit

Permalink
Initial work on ingame documentation via a manual / book.
Browse files Browse the repository at this point in the history
Very, *very* basic Markdown parser and renderer working.
  • Loading branch information
fnuecke committed Apr 7, 2015
1 parent 2d778f2 commit ca8f1e3
Show file tree
Hide file tree
Showing 11 changed files with 313 additions and 0 deletions.
Binary file modified assets/items.psd
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/main/scala/li/cil/oc/Constants.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions src/main/scala/li/cil/oc/client/GuiHandler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/main/scala/li/cil/oc/client/Textures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
48 changes: 48 additions & 0 deletions src/main/scala/li/cil/oc/client/gui/Manual.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}
1 change: 1 addition & 0 deletions src/main/scala/li/cil/oc/common/GuiType.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
3 changes: 3 additions & 0 deletions src/main/scala/li/cil/oc/common/init/Items.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
14 changes: 14 additions & 0 deletions src/main/scala/li/cil/oc/common/item/Manual.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}
243 changes: 243 additions & 0 deletions src/main/scala/li/cil/oc/util/PseudoMarkdown.scala
Original file line number Diff line number Diff line change
@@ -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: ~~...~~
)
}

0 comments on commit ca8f1e3

Please sign in to comment.