rudimentary keyboard input is working

This commit is contained in:
Florian Nücke 2013-09-23 18:13:24 +02:00
parent 48cf012d67
commit 29a79b5ed0
16 changed files with 253 additions and 91 deletions

View File

@ -2,25 +2,25 @@
driver.gpu = {}
function driver.gpu.setResolution(gpu, screen, w, h)
sendToNode(gpu, "gpu.resolution=", screen, w, h)
sendToNode(gpu, "gpu.resolution=", screen, w, h)
end
function driver.gpu.getResolution(gpu, screen)
return sendToNode(gpu, "gpu.resolution", screen)
return sendToNode(gpu, "gpu.resolution", screen)
end
function driver.gpu.getResolutions(gpu, screen)
return sendToNode(gpu, "gpu.resolutions", screen)
return sendToNode(gpu, "gpu.resolutions", screen)
end
function driver.gpu.set(gpu, screen, col, row, value)
sendToNode(gpu, "gpu.set", screen, col, row, value)
sendToNode(gpu, "gpu.set", screen, col, row, value)
end
function driver.gpu.fill(gpu, screen, col, row, w, h, value)
sendToNode(gpu, "gpu.fill", screen, col, row, w, h, value:sub(1, 1))
sendToNode(gpu, "gpu.fill", screen, col, row, w, h, value:sub(1, 1))
end
function driver.gpu.copy(gpu, screen, col, row, w, h, tx, ty)
sendToNode(gpu, "gpu.copy", screen, col, row, w, h, tx, ty)
sendToNode(gpu, "gpu.copy", screen, col, row, w, h, tx, ty)
end

View File

@ -86,12 +86,15 @@ end
-- Main OS loop, keeps everything else running.
while true do
local signal, id = os.signal(nil, 2)
local signal, param = os.signal(nil, 2)
if signal == "component_added" then
onInstall(id)
onInstall(param)
elseif signal == "component_removed" then
onUninstall(id)
onUninstall(param)
elseif signal == "key_down" then
write(param)
else
write("Clock: ")
print(os.clock())
end
write("Clock: ")
print(os.clock())
end

View File

@ -0,0 +1,137 @@
--[[ API for keyboards. ]]
driver.keyboard = {}
driver.keyboard.keys = {
["1"] = 0x02,
["2"] = 0x03,
["3"] = 0x04,
["4"] = 0x05,
["5"] = 0x06,
["6"] = 0x07,
["7"] = 0x08,
["8"] = 0x09,
["9"] = 0x0A,
["0"] = 0x0B,
a = 0x1E,
b = 0x30,
c = 0x2E,
d = 0x20,
e = 0x12,
f = 0x21,
g = 0x22,
h = 0x23,
i = 0x17,
j = 0x24,
k = 0x25,
l = 0x26,
m = 0x32,
n = 0x31,
o = 0x18,
p = 0x19,
q = 0x10,
r = 0x13,
s = 0x1F,
t = 0x14,
u = 0x16,
v = 0x2F,
w = 0x11,
x = 0x2D,
y = 0x15,
z = 0x2C,
apostrophe = 0x28,
at = 0x91,
back = 0x0E, -- backspace
backslash = 0x2B,
colon = 0x92,
comma = 0x33,
enter = 0x1C,
equals = 0x0D,
grave = 0x29, -- accent grave
lbracket = 0x1A,
lcontrol = 0x1D,
lmenu = 0x38, -- left Alt
lshift = 0x2A,
minus = 0x0C,
numlock = 0x45,
pause = 0xC5,
period = 0x34,
rbracket = 0x1B,
rcontrol = 0x9D,
rmenu = 0xB8, -- right Alt
rshift = 0x36,
scroll = 0x46, -- Scroll Lock
semicolon = 0x27,
slash = 0x35, -- / on main keyboard
space = 0x39,
stop = 0x95,
tab = 0x0F,
underline = 0x93,
-- Keypad (and numpad with numlock off)
up = 0xC8,
down = 0xD0,
left = 0xCB,
right = 0xCD,
home = 0xC7,
["end"] = 0xCF,
pageUp = 0xC9,
pageDown = 0xD1,
insert = 0xD2,
delete = 0xD3,
-- Function keys
f1 = 0x3B,
f2 = 0x3C,
f3 = 0x3D,
f4 = 0x3E,
f5 = 0x3F,
f6 = 0x40,
f7 = 0x41,
f8 = 0x42,
f9 = 0x43,
f10 = 0x44,
f11 = 0x57,
f12 = 0x58,
f13 = 0x64,
f14 = 0x65,
f15 = 0x66,
f16 = 0x67,
f17 = 0x68,
f18 = 0x69,
f19 = 0x71,
-- Japanese keyboards
kana = 0x70,
kanji = 0x94,
convert = 0x79,
noconvert = 0x7B,
yen = 0x7D,
circumflex = 0x90,
ax = 0x96,
-- Numpad
numpad0 = 0x52,
numpad1 = 0x4F,
numpad2 = 0x50,
numpad3 = 0x51,
numpad4 = 0x4B,
numpad5 = 0x4C,
numpad6 = 0x4D,
numpad7 = 0x47,
numpad8 = 0x48,
numpad9 = 0x49,
numpadmul = 0x37,
numpaddiv = 0xB5,
numpadsub = 0x4A,
numpadadd = 0x4E,
numpaddecimal = 0x53,
numpadcomma = 0xB3,
numpadenter = 0x9C,
numpadequals = 0x8D,
}
-- Create inverse mapping for name lookup.
for k, v in pairs(driver.keyboard.keys) do
driver.keyboard.keys[v] = k
end

View File

@ -1,15 +1,13 @@
package li.cil.oc
import li.cil.oc.common.block.BlockComputer
import li.cil.oc.common.block.BlockMulti
import li.cil.oc.common.block.BlockScreen
import li.cil.oc.common.block.BlockSpecialMulti
import li.cil.oc.common.block._
object Blocks {
var blockSimple: BlockMulti = null
var blockSpecial: BlockMulti = null
var computer: BlockComputer = null
var screen: BlockScreen = null
var keyboard: BlockKeyboard = null
def init() {
// IMPORTANT: the multi block must come first, since the sub blocks will
@ -20,5 +18,6 @@ object Blocks {
computer = new BlockComputer(blockSimple)
screen = new BlockScreen(blockSimple)
keyboard = new BlockKeyboard(blockSpecial)
}
}

View File

@ -1,6 +1,5 @@
package li.cil.oc.api
import _root_.scala.beans.BeanProperty
import net.minecraft.world.World
/**
@ -10,6 +9,9 @@ import net.minecraft.world.World
* placed in the world, but cannot be modified to or don't want to have their
* `TileEntities` implement `INetworkNode`.
* <p/>
* A block driver is used by proxy blocks to check its neighbors and whether
* those neighbors should be treated as components or not.
* <p/>
* Note that it is possible to write one driver that supports as many different
* blocks as you wish. I'd recommend writing one per device (type), though, to
* keep things modular.
@ -34,15 +36,17 @@ trait IBlockDriver extends IDriver {
/**
* Get a reference to the network node wrapping the specified block.
* <p/>
* This is used to provide context to the driver's methods, for example when
* an API method is called this will always be passed as the first parameter.
* This is used to connect the component to the component network when it is
* detected next to a proxy. Components that are not part of the component
* network probably don't make much sense (can't think of any uses at this
* time), but you may still opt to not implement this.
*
* @param world the world in which the block to get the component for lives.
* @param x the X coordinate of the block to get the component for.
* @param y the Y coordinate of the block to get the component for.
* @param z the Z coordinate of the block to get the component for.
* @return the block component at that location, controlled by this driver.
* @param world the world in which the block to get the node for lives.
* @param x the X coordinate of the block to get the node for.
* @param y the Y coordinate of the block to get the node for.
* @param z the Z coordinate of the block to get the node for.
* @return the network node for the block at that location.
*/
@BeanProperty
def node(world: World, x: Int, y: Int, z: Int): INetworkNode
def node(world: World, x: Int, y: Int, z: Int): INetworkNode =
world.getBlockTileEntity(x, y, z).asInstanceOf[INetworkNode]
}

View File

@ -10,36 +10,36 @@ import java.io.InputStream
* Lua state when the driver is installed, and provide general information used
* by the computer.
* <p/>
* Note that drivers themselves are singletons. They can define a parameter of
* type {@link IComputerContext} in their API functions which will hold the
* context in which they are called - essentially a representation of the
* computer they were called form. This context can be used to get a component
* in the computer (e.g. passed as another parameter) and to send signals to the
* computer.
* <p/>
* Do not implement this interface directly; use the {@link IItemDriver} and
* {@link IBlockDriver} interfaces for the respective component types.
* Do not implement this interface directly; use the `IItemDriver` and
* `IBlockDriver` interfaces for the respective component types.
*/
trait IDriver {
/**
* Some initialization code that is run when the driver is installed.
* <p/>
* This is loaded
* into the Lua state and run in the global, un-sandboxed environment. This
* means your scripts can mess things up bad, so make sure you know what
* you're doing and exposing.
* These will usually be some functions that generate network messages of
* the particular signature the node of the driver handles, but may contain
* arbitrary other functions. However, whatever you do, keep in mind that
* only certain parts of the global namespace will be made available to the
* computer at runtime, so it's best to keep all you declare in the driver
* table (global variable `driver`).
* <p/>
* This can be null to do nothing. Otherwise this is expected to be valid Lua
* code (it is simply loaded via <code>load()</code> and then executed).
* This is loaded into the Lua state and run in the global, un-sandboxed
* environment. This means your scripts can mess things up bad, so make sure
* you know what you're doing and exposing.
* <p/>
* This can be `None` to do nothing. Otherwise this is expected to be valid
* Lua code (it is simply loaded via <code>load()</code> and then executed).
* <p/>
* The stream has to be recreated each time this is called. Normally you will
* return something along the lines of
* <code>Mod.class.getResourceAsStream("/assets/mod/lua/ocapi.lua")</code>
* `Mod.class.getResourceAsStream("/assets/yourmod/lua/ocapi.lua")`
* from this method. If you wish to hard-code the returned script, you can use
* <code>new ByteArrayInputStream(yourScript.getBytes())</code> instead. Note
* that the stream will automatically closed.
* `new ByteArrayInputStream(yourScript.getBytes())` instead.
* <p/>
* IMPORTANT: Note that the stream will automatically closed.
*
* @return the Lua code to run after installing the API table.
* @return the Lua code to run when a computer is started up.
*/
def api: Option[InputStream] = None
}

View File

@ -13,7 +13,7 @@ import net.minecraft.item.ItemStack
* queried using the drivers' `worksWith` functions. The first driver that
* replies positively and whose check against the slot type is successful, i.e.
* for which the `componentType` matches the slot, will be used as the
* component's driver and the component will be installed. If no driver is found
* component's driver and the component will be added. If no driver is found
* the item will be rejected and cannot be installed.
* <p/>
* Note that it is possible to write one driver that supports as many different
@ -47,6 +47,11 @@ trait IItemDriver extends IDriver {
/**
* Gets a reference to the network node interfacing the specified item.
* <p/>
* This is used to connect the component to the component network when it is
* added to a computer, for example. Components that are not part of the
* component network probably don't make much sense (can't think of any uses
* at this time), but you may still opt to not implement this.
*
* @param item the item instance for which to get the node.
* @return the network node for that item.

View File

@ -27,20 +27,22 @@ object PacketSender {
pb.sendToServer()
}
def sendKeyDown[T <: TileEntity with INetworkNode](t: T, c: Char) = {
def sendKeyDown[T <: TileEntity with INetworkNode](t: T, char: Char, code: Int) = {
val pb = new PacketBuilder(PacketType.KeyDown)
pb.writeTileEntity(t)
pb.writeChar(c)
pb.writeChar(char)
pb.writeInt(code)
pb.sendToServer()
}
def sendKeyUp[T <: TileEntity with INetworkNode](t: T, c: Char) = {
def sendKeyUp[T <: TileEntity with INetworkNode](t: T, char: Char, code: Int) = {
val pb = new PacketBuilder(PacketType.KeyUp)
pb.writeTileEntity(t)
pb.writeChar(c)
pb.writeChar(char)
pb.writeInt(code)
pb.sendToServer()
}

View File

@ -1,13 +1,11 @@
package li.cil.oc.client.gui
import li.cil.oc.client.PacketSender
import li.cil.oc.common.tileentity.TileEntityKeyboard
import li.cil.oc.common.tileentity.TileEntityScreen
import net.minecraft.client.renderer.GLAllocation
import net.minecraft.client.renderer.Tessellator
import net.minecraft.client.renderer.texture.TextureManager
import net.minecraft.util.ResourceLocation
import net.minecraftforge.common.ForgeDirection
import org.lwjgl.input.Keyboard
import org.lwjgl.opengl.GL11
@ -49,16 +47,15 @@ class GuiScreen(val tileEntity: TileEntityScreen) extends net.minecraft.client.g
/** Must be called whenever the buffer of the underlying screen changes. */
def updateText() = GuiScreen.compileText(scale, tileEntity.screen.lines)
override def handleKeyboardInput() = {
// Find all keyboards next to this screen and type on them.
for (k <- neighboringKeyboards) {
override def keyTyped(char: Char, code: Int) = {
super.keyTyped(char, code)
if (code != Keyboard.KEY_ESCAPE && code != Keyboard.KEY_F11)
if (Keyboard.getEventKeyState) {
PacketSender.sendKeyDown(k, Keyboard.getEventCharacter)
PacketSender.sendKeyDown(tileEntity, char, code)
}
else {
PacketSender.sendKeyUp(k, Keyboard.getEventCharacter)
PacketSender.sendKeyUp(tileEntity, char, code)
}
}
}
override def initGui() = {
@ -85,16 +82,6 @@ class GuiScreen(val tileEntity: TileEntityScreen) extends net.minecraft.client.g
}
override def doesGuiPauseGame = false
private def neighboringKeyboards =
ForgeDirection.VALID_DIRECTIONS.
map(d => tileEntity.worldObj.getBlockTileEntity(
tileEntity.xCoord + d.offsetX,
tileEntity.yCoord + d.offsetY,
tileEntity.zCoord + d.offsetZ)).
filter(_ != null).
filter(_.isInstanceOf[TileEntityKeyboard]).
map(_.asInstanceOf[TileEntityKeyboard])
}
/** We cache OpenGL stuff in a singleton to avoid having to re-allocate. */

View File

@ -30,6 +30,7 @@ class Proxy {
NetworkRegistry.instance.registerGuiHandler(OpenComputers, GuiHandler)
OpenComputersAPI.addDriver(GraphicsCardDriver)
OpenComputersAPI.addDriver(KeyboardDriver)
MinecraftForge.EVENT_BUS.register(ForgeEventHandler)
}

View File

@ -19,14 +19,16 @@ class TileEntityComputer(isClient: Boolean) extends TileEntityRotatable with ICo
private val hasChanged = new AtomicBoolean(true)
private var isRunning = computer.isRunning
private var isRunning = false
// ----------------------------------------------------------------------- //
// NetworkNode
// ----------------------------------------------------------------------- //
override def receive(message: INetworkMessage) = message.data match {
case Array() if message.name == "network.connect" =>
// The isRunning check is here to avoid network.connect messages being sent
// while loading a chunk (thus leading to "false" component_added signals).
case Array() if message.name == "network.connect" && isRunning =>
computer.signal("component_added", message.source.address); None
case Array() if message.name == "network.disconnect" =>
computer.signal("component_removed", message.source.address); None

View File

@ -7,16 +7,16 @@ class TileEntityKeyboard extends TileEntityRotatable with INetworkNode {
override def name = "keyboard"
override def receive(message: INetworkMessage) = message.data match {
case Array(name: String, p: Player, c: Character) if name == "keyboard.keyDown" => {
case Array(p: Player, char: Char, code: Int) if message.name == "keyboard.keyDown" => {
// TODO check if player is close enough and only consume message if so
network.sendToAll(this, "signal", "key_down", c)
message.cancel()
network.sendToAll(this, "signal", "key_down", char.toString, code)
message.cancel() // One keyboard is enough.
None
}
case Array(name: String, p: Player, c: Character) if name == "keyboard.keyUp" => {
case Array(p: Player, char: Char, code: Int) if message.name == "keyboard.keyUp" => {
// TODO check if player is close enough and only consume message if so
network.sendToAll(this, "signal", "key_up", c)
message.cancel()
network.sendToAll(this, "signal", "key_up", char.toString, code)
message.cancel() // One keyboard is enough.
None
}
case _ => None

View File

@ -67,12 +67,12 @@ class PacketHandler extends CommonPacketHandler {
def onKeyDown(p: PacketParser) =
p.readTileEntity[INetworkNode]() match {
case None => // Invalid packet.
case Some(n) => n.network.sendToAll(n, "keyboard.keyDown", p.player, p.readChar())
case Some(n) => n.network.sendToAll(n, "keyboard.keyDown", p.player, p.readChar(), p.readInt())
}
def onKeyUp(p: PacketParser) =
p.readTileEntity[INetworkNode]() match {
case None => // Invalid packet.
case Some(n) => n.network.sendToAll(n, "keyboard.keyUp", p.player, p.readChar())
case Some(n) => n.network.sendToAll(n, "keyboard.keyUp", p.player, p.readChar(), p.readInt())
}
}

View File

@ -660,12 +660,13 @@ class Computer(val owner: IComputerEnvironment) extends IComputer with Runnable
// resuming the state again.
val sleep = (lua.toNumber(2) * 1000).toLong
lua.pop(results)
state = State.Sleeping
future = Some(Executor.pool.
schedule(this, sleep, TimeUnit.MILLISECONDS))
if (signals.isEmpty) {
state = State.Sleeping
future = Some(Executor.pool.schedule(this, sleep, TimeUnit.MILLISECONDS))
} else future = Some(Executor.pool.submit(this))
}
else if (results == 1 && lua.isFunction(2)) {
// If we get one function it's a wrapper for a driver call.
// If we get one function it's a wrapper for a synchronized call.
state = State.SynchronizedCall
future = None
}
@ -673,7 +674,8 @@ class Computer(val owner: IComputerEnvironment) extends IComputer with Runnable
// Something else, just pop the results and try again.
lua.pop(results)
state = State.Suspended
future = Some(Executor.pool.submit(this))
if (signals.isEmpty) future = None
else future = Some(Executor.pool.submit(this))
}
}

View File

@ -1,5 +1,6 @@
package li.cil.oc.server.computer
import li.cil.oc.OpenComputers
import li.cil.oc.api.INetwork
import li.cil.oc.api.INetworkMessage
import li.cil.oc.api.INetworkNode
@ -35,10 +36,9 @@ class Network private(private val nodes: mutable.Map[Int, ArrayBuffer[Network.No
nodes.values.flatten.foreach(_.data.network = this)
def connect(nodeA: INetworkNode, nodeB: INetworkNode) = try {
def connect(nodeA: INetworkNode, nodeB: INetworkNode) = {
if (locked) throw new IllegalStateException(
"Cannot modify network while it is already updating its structure.")
locked = true
val containsA = nodes.get(nodeA.address).exists(_.exists(_.data == nodeA))
val containsB = nodes.get(nodeB.address).exists(_.exists(_.data == nodeB))
@ -66,9 +66,6 @@ class Network private(private val nodes: mutable.Map[Int, ArrayBuffer[Network.No
else if (containsA) add(nodes(nodeA.address).find(_.data == nodeA).get, nodeB)
else add(nodes(nodeB.address).find(_.data == nodeB).get, nodeA)
}
finally {
locked = false
}
def remove(node: INetworkNode) = nodes.get(node.address) match {
case None => false
@ -103,9 +100,14 @@ class Network private(private val nodes: mutable.Map[Int, ArrayBuffer[Network.No
private def send(message: Network.Message, nodes: Iterator[INetworkNode]) = {
var result = None: Option[Array[Any]]
while (!message.isCanceled && nodes.hasNext) {
nodes.next().receive(message) match {
case None => // Ignore.
case r => result = r
try {
nodes.next().receive(message) match {
case None => // Ignore.
case r => result = r
}
} catch {
case e: Throwable =>
OpenComputers.log.warning("Error in message handler:\n" + e.getStackTraceString)
}
}
result
@ -124,7 +126,6 @@ class Network private(private val nodes: mutable.Map[Int, ArrayBuffer[Network.No
newNode
}
else {
val otherNetwork = node.network.asInstanceOf[Network]
// We have to merge. First create a copy of the old nodes to have the
// list of nodes to which to send "network.connect" messages.
val oldNodes = nodes.values.flatten.map(_.data).toArray
@ -132,7 +133,12 @@ class Network private(private val nodes: mutable.Map[Int, ArrayBuffer[Network.No
// iteration to merge into this network, used to send "network.reconnect"
// messages to old nodes in case we have to change a node's address to
// ensure unique addresses in the merged network.
val otherNetwork = node.network.asInstanceOf[Network]
val otherNodes = otherNetwork.nodes.values.flatten.map(_.data)
// Lock this network to avoid message handlers adding nodes, which could
// lead to addresses getting taken that we will need for the nodes we are
// about to merge into this network.
locked = true
// Pre-merge step: ensure addresses are unique.
for (node <- otherNodes if nodes.contains(node.address)) {
val oldAddress = node.address
@ -148,6 +154,8 @@ class Network private(private val nodes: mutable.Map[Int, ArrayBuffer[Network.No
node.data.network = this
send(new Network.Message(node.data, "network.connect"), oldNodes.iterator)
}
// Done, unlock again.
locked = false
// Return the node object of the newly connected node for the next step.
nodes(node.address).find(_.data == node).get
}

View File

@ -0,0 +1,12 @@
package li.cil.oc.server.drivers
import li.cil.oc.Blocks
import li.cil.oc.api.IBlockDriver
import net.minecraft.world.World
object KeyboardDriver extends IBlockDriver {
override def api = Option(getClass.getResourceAsStream("/assets/opencomputers/lua/keyboard.lua"))
override def worksWith(world: World, x: Int, y: Int, z: Int) =
world.getBlockId(x, y, z) == Blocks.keyboard.blockId
}