working on drivers

This commit is contained in:
Florian Nücke 2013-09-10 17:26:25 +02:00
parent baad2ebbda
commit f9cd7fd3b6
20 changed files with 629 additions and 292 deletions

View File

@ -58,7 +58,7 @@ do
end
end
end
wrapRecursive(drivers)
wrapRecursive(driver)
end
--[[ Permanent value tables.

View File

@ -72,6 +72,8 @@ local function buildSandbox()
yield = coroutine.yield
},
driver = driver,
math = {
abs = math.abs,
acos = math.acos,
@ -139,6 +141,8 @@ local function buildSandbox()
unpack = table.unpack
}
}
-- Make the sandbox its own globals table.
sandbox._G = sandbox
-- Allow sandboxes to load code, but only in text form, and in the sandbox.

View File

@ -750,7 +750,7 @@ public class LuaState {
*/
public synchronized void pushJavaObject(Object object) {
check();
getConverter().convertJavaObject(this, object);
converter.convertJavaObject(this, object);
}
/**
@ -1961,6 +1961,23 @@ public class LuaState {
throw getArgException(index, String.format("invalid option '%s'", s));
}
/**
* Checks if the value of the specified function argument is a boolean. If
* so, the argument value is returned as a boolean. Otherwise, the method
* throws a Lua runtime exception with a descriptive error message.
*
* @param index
* the argument index
* @return the integer value
*/
public synchronized boolean checkBoolean(int index) {
check();
if (!isBoolean(index)) {
throw getArgTypeException(index, LuaType.BOOLEAN);
}
return toBoolean(index);
}
/**
* Checks if the value of the specified function argument is a number or a
* string convertible to a number. If so, the argument value is returned as

View File

@ -5,6 +5,7 @@ object Config {
var blockMonitorId = 3651
var itemHDDId = 4600
var itemGPUId = 4601
var threads = 4
}

View File

@ -1,5 +1,7 @@
package li.cil.oc
import java.util.logging.Logger
import cpw.mods.fml.common.Mod
import cpw.mods.fml.common.Mod.EventHandler
import cpw.mods.fml.common.SidedProxy
@ -12,6 +14,9 @@ import li.cil.oc.common.CommonProxy
@Mod(modid = "OpenComputers", name = "OpenComputers", version = "0.0.0", dependencies = "required-after:Forge@[9.10.0.804,)", modLanguage = "scala")
@NetworkMod(clientSideRequired = true, serverSideRequired = false)
object OpenComputers {
/** Logger used all throughout this mod. */
val log = Logger.getLogger("OpenComputers")
@SidedProxy(
clientSide = "li.cil.oc.client.ClientProxy",
serverSide = "li.cil.oc.common.CommonProxy")

View File

@ -6,29 +6,39 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* This annotation is used by components to mark methods that are part of the
* API they expose to computers.
* This annotation is used by {@see IDriver}s to mark methods that are part of
* the API they expose to computers.
*
* If the component provides an API, each method annotated like this an entry in
* the API table will be generated. The actual entry will be a wrapper that
* If the driver provides an API, for each method annotated like this an entry
* in the API table will be generated. The actual entry will be a wrapper that
* takes care of parameter transformation and all the Lua protocol stuff. This
* does limit the types you can use for parameters and return types, however.
* requires fixed parameters and return types, however, i.e. one parameter will
* always have to be one specific type (which is not necessary in Lua since it
* is a dynamic language).
*
* Supported parameter types are: Boolean, Integer, Double and String. These map
* to boolean, number, number, and string respectively. If a function parameter
* is of an unsupported type its value will always be the type's default value.
* Any type is supported as long as it can be converted by JNLua's {@see
* com.naef.jnlua.DefaultConverter}. However, so as not to break persistence
* (i.e. saving a computer's state and seamlessly resume after reloading) only
* simple types such as booleans, numbers and strings and table types such as
* <tt>Map</tt>,<tt>List</tt> and arrays should be passed along.
*
* Supported return types are: Boolean, Integer, Double, String and Array. These
* map to the same types as parameter types, with the exception of arrays which
* are interpreted as tuples, meaning they will be dissolved into multiple
* return values. If a return value type is of an unsupported type it will be
* ignored.
* If you wish more flexibility in how you handle parameters, define the
* callback with the following signature:
*
* <pre>
* Object[] callback(IComputerContext, Object[])
* </pre>
*
* In this case the first argument is the computer from which the function was
* called, the second is the array of objects passed along from Lua, which may
* be an arbitrary number of arbitrarily typed object (although still only basic
* types as described above are supported). The returned array must also contain
* only basic types as described above and represents the values returned as
* results to Lua as with automatically wrapped functions.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Callback {
/**
* The name under which the method will be available in Lua.
*/
/** The name under which the method will be available in Lua. */
String name();
}

View File

@ -12,5 +12,4 @@ object ComponentType extends Enumeration {
val RAM = Value("RAM")
val HDD = Value("HDD")
val PCI = Value("PCI")
val GPU = Value("GPU")
}

View File

@ -9,22 +9,34 @@ import net.minecraft.block.Block
* placed in the world, and particularly: next to computers. An example for
* this are external drives, monitors and modems.
*
* When a block component is added next to a computer, the computer's OS will
* be notified via a signal so that it may install the component's driver, for
* example. After that the OS may start to interact with the component via the
* API functions it provides.
* When a block component is placed next to a computer, the list of registered
* drivers is queried using the drivers' {@see #worksWith} functions. The first
* driver that replies positively will be used as the component's driver and
* the component will be installed. If no driver is found the item will be
* ignored.
*
* The computer will store a list of installed components, the values of which
* are based on what the driver returns from its {@see #component} function
* at the point of time the component is installed.
* If a driver's API function queries a component via the context using
* {@see IComputerContext#component()} the returned value will be exactly that.
*
* 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 and the {@see IDriver#componentName} more meaningful.
*/
trait IBlockDriver extends IDriver {
/**
* The type of block this driver handles.
* Used to determine the block types this driver handles.
*
* When a block is added next to a computer and has this type, this driver
* will be used for the block. The return value must not change over the
* lifetime of this driver.
* This is used to determine which driver to use for a block placed next to a
* computer. Note that the return value should not change over time; if it
* does, though, an already installed component will not be ejected, since
* this value is only checked when adding components.
*
* @return the block type this driver is used for.
* param block the block type to check for.
*/
def blockType: Block
def worksWith(block: Block): Boolean
/**
* Get a reference to the actual component.
@ -38,5 +50,5 @@ trait IBlockDriver extends IDriver {
* @param z the Z coordinate of the block to get the component for.
* @return the block component at that location, controlled by this driver.
*/
def getComponent(x: Int, y: Int, z: Int): Object
def component(x: Int, y: Int, z: Int): Object
}

View File

@ -1,5 +1,7 @@
package li.cil.oc.api
import java.io.InputStream
/**
* This interface specifies the structure of a driver for a component.
*
@ -7,6 +9,16 @@ package li.cil.oc.api
* used as computer components. They specify an API that is injected into the
* Lua state when the driver is installed, and provide general information used
* by the computer.
*
* Note that drivers themselves are singletons. They can define a parameter of
* type {@see 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.
*
* Do not implement this interface directly; use the {@see IItemDriver} and
* {@see IBlockDriver} interfaces for the respective component types.
*/
trait IDriver {
/**
@ -15,49 +27,100 @@ trait IDriver {
* This is used to allow computer programs to check the type of a component.
* Components attached to a computer are identified by a number. This value
* is returned when the type for such an ID is requested, so it should be
* unique for each driver.
* unique for each driver. For example, when a component is installed and the
* install signal was sent, the Lua code may check the type of the installed
* component using <code>drivers.component</code>. The returned string will
* be this one.
*/
def componentName: String
/**
* The name of the API this component exposes to computers.
* The name of the API this component exposes to computers, if any.
*
* The component may return null or an empty string if it does not wish to
* define an API. If this is the case, we will not look for methods marked
* with the {@link Callback} annotation.
*
* This is the name of the table made available in the global drivers table,
* for example if this were to return 'disk', then the API will be available
* in Lua via <code>drivers.disk</code>.
*
* This should be unique for individual component types. If not, functions
* in that API table may block another component's API from being installed
* properly: existing entries are not overwritten.
*
* Note that this <em>must not</em> be 'component', since that is reserved
* for a function that returns the type of an installed component given its
* ID.
*
* @return the name of the API made available to Lua.
*/
def apiName: String
def apiName: String = null
/**
* Some initialization code that is run when the driver is installed.
*
* This is run after the driver's API table has been installed. It is loaded
* into the Lua state in a temporary environment that has access to the
* globals table and is discarded after the script has run.
* 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.
*
* This can be null or an empty string to do nothing. Otherwise this is
* expected to be valid Lua code.
* 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).
*
* 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>
* 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.
*
* @return the Lua code to run after installing the API table.
*/
def apiCode: String
def apiCode: InputStream = null
/**
* This is called when a component is added to a computer.
*
* This happens if either of the following takes place:
* - The component is an item component and added in the computer.
* - The component is a block component and placed next to the computer.
* - The component is already in / next to the computer and the computer was
* off and is now starting up again. In this case this is called before the
* computer thread is started.
*
* You can use this to initialize some internal state or send some signal(s)
* to the computer. For example, graphics cards will scan for unbound
* monitors and automatically bind to the first free one.
*
* This is called before the install signal is sent to the computer.
*
* @param computer the computer to which the component is being added.
* @param component a handle to the component, as it was provided by the
* driver in its {@link IBlockDriver#component}/{@link IItemDriver#component}
* function.
*/
def onInstall(computer: IComputerContext, component: Any) = {}
/**
* This is called when a component is removed from a computer.
*
* This happens if either of the following takes place:
* - The component is an item component and removed from the computer.
* - The component is a block component and broken or the computer is broken.
* - The component is already in / next to the computer and the computer was
* on and is now shutting down.
*
* The component should remove all handles it currently provides to the
* computer. For example, it should close any open files if it provides some
* form of file system.
*
* This is called before the uninstall signal is sent to the computer.
*
* @param computer the computer from which the component is being removed.
* @param component a handle to the component, as it was provided by the
* driver in its {@link IBlockDriver#getComponent} or
* {@link IItemDriver#getComponent} function.
* driver in its {@link IBlockDriver#component}/{@link IItemDriver#component}
* function.
*/
def close(component: Object)
def onUninstall(computer: IComputerContext, component: Any) = {}
}

View File

@ -9,42 +9,65 @@ import net.minecraft.item.ItemStack
* inserted into computers. An example for this are internal drives, memory
* and power supply units.
*
* When an item component is added to a computer, the computer's OS will be
* notified via a signal so that it may install the component's driver, for
* example. After that the OS may start to interact with the component via the
* API functions it provides.
* When trying to add an item to a computer the list of registered drivers is
* queried using the drivers' {@see #worksWith} functions. The first driver
* that replies positively and whose check against the slot type is successful,
* i.e. for which the {@see #componentType} matches the slot, will be used as
* the component's driver and the component will be installed. If no driver is
* found the item will be rejected and cannot be installed.
*
* The computer will store a list of installed components, the values of which
* are based on what the driver returns from its {@see #component} function
* at the point of time the component is installed.
* If a driver's API function queries a component via the context using
* {@see IComputerContext#component()} the returned value will be exactly that.
*
* Note that it is possible to write one driver that supports as many different
* items as you wish. I'd recommend writing one per device (type), though, to
* keep things modular and the {@see IDriver#componentName} more meaningful.
*/
trait IItemDriver extends IDriver {
/**
* The component type of this item component.
* Used to determine the item types this driver handles.
*
* This is used to determine into which slot of a computer this component may
* go.
* This is used to determine which driver to use for an item when installed
* in a computer. Note that the return value should not change over time; if
* it does, though, an already installed component will not be ejected, since
* this value is only checked when adding components.
*
* @return the component type.
* @param item the item to check.
* @return true if the item is supported; false otherwise.
*/
def componentType: ComponentType.Value
def worksWith(item: ItemStack): Boolean
/**
* The type of item this driver handles.
* The component type of the specified item this driver supports.
*
* When an item is added into a computer and has this type, this driver will
* be used for the block. The return value must not change over the lifetime
* of this driver.
* This is used to determine into which slot of a computer the components
* this driver supports may go. This will only be called if a previous call
* to {@see #worksWith} with the same item type returned true.
*
* @return the item type this driver is used for.
* @return the component type of the specified item.
*/
def itemType: ItemStack
def componentType(item: ItemStack): ComponentType.Value
/**
* Get a reference to the actual component.
* Gets a reference to the actual component.
*
* 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. It is also passed to the {@link IDriver#close} method.
* It is called once when a component is installed in a computer. At that
* time, the component will be assigned a unique ID by which it is referred
* to by from the Lua side. The computer keeps track of the mapping from ID
* to the actual component, which will be the value returned from this.
*
* This is used to provide context to the driver's API methods. The driver
* may get a reference to a component via the ID passed from a Lua program,
* and act accordingly (for that it must also have a context parameter, see
* the general interface documentation).
*
* This value also passed to the driver's {@link IDriver#close} method.
*
* @param item the item instance for which to get the component.
* @return the item component for that item, controlled by this driver.
*/
def getComponent(item: ItemStack): Object
def component(item: ItemStack): Any
}

View File

@ -5,11 +5,11 @@ import li.cil.oc.server.computer.Drivers
object OpenComputersAPI {
def addDriver(driver: IBlockDriver) {
// TODO Use reflection to allow distributing the API.
Drivers.addDriver(driver)
Drivers.add(driver)
}
def addDriver(driver: IItemDriver) {
// TODO Use reflection to allow distributing the API.
Drivers.addDriver(driver)
Drivers.add(driver)
}
}

View File

@ -1,19 +1,44 @@
package li.cil.oc.client.computer
import li.cil.oc.common.computer.IInternalComputerContext
import scala.reflect.runtime.universe._
import li.cil.oc.api.IComputerContext
import li.cil.oc.common.computer.IComputer
import net.minecraft.block.Block
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NBTTagCompound
class Computer(val owner: AnyRef) extends IInternalComputerContext {
def luaState = null
/**
* This is a dummy class for the client side. It does nothing, really, just
* saves us a couple of side checks.
*/
class Computer(val owner: AnyRef) extends IComputerContext with IComputer {
// ----------------------------------------------------------------------- //
// IComputerContext
// ----------------------------------------------------------------------- //
def world = throw new NotImplementedError
def signal(name: String, args: Any*) = throw new NotImplementedError
def component[T: TypeTag](id: Int) = throw new NotImplementedError
// ----------------------------------------------------------------------- //
// IComputer
// ----------------------------------------------------------------------- //
def add(item: ItemStack, id: Int) = None
def add(block: Block, x: Int, y: Int, z: Int, id: Int) = None
def remove(id: Int) = false
def start() = false
def stop() {}
def update() {}
def signal(name: String, args: Any*) {}
def readFromNBT(nbt: NBTTagCompound) {}
def writeToNBT(nbt: NBTTagCompound) {}

View File

@ -8,6 +8,7 @@ import li.cil.oc.Blocks
import li.cil.oc.Config
import li.cil.oc.Items
import li.cil.oc.server.computer.Computer
import li.cil.oc.server.computer.Drivers
class CommonProxy {
def preInit(e: FMLPreInitializationEvent): Unit = {
@ -30,5 +31,10 @@ class CommonProxy {
new Computer(null)
}
def postInit(e: FMLPostInitializationEvent): Unit = {}
def postInit(e: FMLPostInitializationEvent): Unit = {
// Lock the driver registry to avoid drivers being added after computers
// may have already started up. This makes sure the driver API won't change
// over the course of a game, since that could lead to weird effects.
Drivers.locked = true
}
}

View File

@ -1,20 +0,0 @@
package li.cil.oc.common.computer
import scala.collection.Seq
import net.minecraft.nbt.NBTTagCompound
import li.cil.oc.server.computer.IComputerContext
import com.naef.jnlua.LuaState
trait IInternalComputerContext extends IComputerContext {
def luaState: LuaState
def start(): Boolean
def stop(): Unit
def update()
def readFromNBT(nbt: NBTTagCompound)
def writeToNBT(nbt: NBTTagCompound)
}

View File

@ -34,7 +34,6 @@ class TileEntityComputer(isClient: Boolean) extends TileEntity with IComputerEnv
}
override def writeToNBT(nbt: NBTTagCompound) = {
println("SAVING")
super.writeToNBT(nbt)
computer.writeToNBT(nbt)
}
@ -52,14 +51,12 @@ class TileEntityComputer(isClient: Boolean) extends TileEntity with IComputerEnv
@ForgeSubscribe
def onChunkUnload(e: ChunkEvent.Unload) = {
println("CHUNK UNLOADING")
MinecraftForge.EVENT_BUS.unregister(this)
computer.stop()
}
@ForgeSubscribe
def onWorldUnload(e: WorldEvent.Unload) = {
println("WORLD UNLOADING")
MinecraftForge.EVENT_BUS.unregister(this)
computer.stop()
}

View File

@ -2,22 +2,58 @@ package li.cil.oc.server.computer
import java.util.concurrent._
import java.util.concurrent.atomic.AtomicInteger
import scala.Array.canBuildFrom
import scala.collection.JavaConversions._
import scala.collection.mutable._
import scala.reflect.runtime.universe._
import scala.util.Random
import com.naef.jnlua._
import li.cil.oc.Config
import li.cil.oc.common.computer.IInternalComputerContext
import li.cil.oc.api.IComputerContext
import li.cil.oc.common.computer.IComputer
import net.minecraft.block.Block
import net.minecraft.item.ItemStack
import net.minecraft.nbt._
import java.util.Date
import java.util.Calendar
import java.util.Locale
class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext with Runnable {
/**
* Wrapper class for Lua states set up to behave like a pseudo-OS.
*
* This class takes care of the following:
* - Creating a new Lua state when started from a previously stopped state.
* - Updating the Lua state in a parallel thread so as not to block the game.
* - Managing a list of installed components and their drivers.
* - Synchronizing calls from the computer thread to drivers.
* - Saving the internal state of the computer across chunk saves/loads.
* - Closing the Lua state when stopping a previously running computer.
*
* See {@see Driver} to read more about component drivers and how they interact
* with computers - and through them the components they interface.
*/
class Computer(val owner: IComputerEnvironment) extends IComputerContext with IComputer with Runnable {
// ----------------------------------------------------------------------- //
// General
// ----------------------------------------------------------------------- //
/**
* This is the list of components currently attached to the computer. It is
* updated whenever a component is placed into the computer or removed from
* it, as well as when a block component is placed next to it or removed.
*
* On the Lua side we only refer to components by an ID, which is the array
* index for the component array. Drivers may fetch the underlying component
* object via the IComputerContext.
*
* Since the Lua program may query a component's type from its own thread we
* have to synchronize access to the component map. Note that there's a
* chance the Lua programs may see a component before its install signal has
* been processed, but I can't think of a scenario where this could become
* a real problem.
*/
private val components = new HashMap[Int, (Any, Driver)] with SynchronizedMap[Int, (Any, Driver)]
/**
* The current execution state of the computer. This is used to track how to
* resume the computers main thread, if at all, and whether to accept new
@ -26,7 +62,7 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
private var state = State.Stopped
/** The internal Lua state. Only set while the computer is running. */
private var lua: LuaState = null
private[computer] var lua: LuaState = null
/**
* The base memory consumption of the kernel. Used to permit a fixed base
@ -42,7 +78,7 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
* means to communicate actively with the computer (passively only drivers
* can interact with the computer by providing API functions).
*/
private val signals = new LinkedBlockingQueue[Signal](256)
private val signals = new LinkedBlockingQueue[Signal](100)
// ----------------------------------------------------------------------- //
@ -82,22 +118,61 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
private val saveMonitor = new Object()
// ----------------------------------------------------------------------- //
// State
// IComputerContext
// ----------------------------------------------------------------------- //
/** Starts asynchronous execution of this computer if it isn't running. */
def start(): Boolean = stateMonitor.synchronized(
def world = owner.world
def signal(name: String, args: Any*) = {
args.foreach {
case null | _: Byte | _: Char | _: Short | _: Int | _: Long | _: Float | _: Double | _: String => Unit
case _ => throw new IllegalArgumentException()
}
stateMonitor.synchronized(state match {
// We don't push new signals when stopped or shutting down.
case State.Stopped | State.Stopping => false
// Currently sleeping. Cancel that and start immediately.
case State.Sleeping =>
future.cancel(true)
state = State.Suspended
signals.offer(new Signal(name, args.toArray))
future = Executor.pool.submit(this)
true
// Running or in driver call or only a short yield, just push the signal.
case _ =>
signals.offer(new Signal(name, args.toArray))
true
})
}
def component[T: TypeTag](id: Int) = components.get(id) match {
case None => throw new IllegalArgumentException("no such component")
case Some(component) =>
if (component.getClass() == typeOf[T]) component.asInstanceOf[T]
else throw new IllegalArgumentException("bad component type")
}
// ----------------------------------------------------------------------- //
// IComputer
// ----------------------------------------------------------------------- //
def start() = stateMonitor.synchronized(
state == State.Stopped && init() && {
state = State.Suspended
// Inject a dummy signal so that real one don't get swallowed. This way
// we can just ignore the parameters the first time the kernel is run.
// we can just ignore the parameters the first time the kernel is run
// and all actual signals will be read using coroutine.yield().
signal("dummy")
// Initialize any installed components.
for ((component, driver) <- components.values)
driver.instance.onInstall(this, component)
future = Executor.pool.submit(this)
true
})
/** Stops a computer, possibly asynchronously. */
def stop(): Unit = saveMonitor.synchronized(stateMonitor.synchronized {
def stop() = saveMonitor.synchronized(stateMonitor.synchronized {
if (state != State.Stopped) {
if (state != State.Running) {
// If the computer is not currently running we can simply close it,
@ -118,12 +193,6 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
}
})
// ----------------------------------------------------------------------- //
// IComputerContext
// ----------------------------------------------------------------------- //
def luaState = lua
def update() {
stateMonitor.synchronized(state match {
case State.Stopped | State.Stopping => return
@ -152,34 +221,47 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
// Update world time for computer threads.
worldTime = owner.world.getWorldInfo().getWorldTotalTime()
if (worldTime % 47 == 0) {
signal("test", "ha!")
}
// Update last time run to let our executor thread know it doesn't have to
// pause, and wake it up if it did pause (because the game was paused).
// pause.
lastUpdate = System.currentTimeMillis
}
def signal(name: String, args: Any*) = {
args.foreach {
case _: Byte | _: Short | _: Int | _: Long | _: Float | _: Double | _: String => Unit
case _ => throw new IllegalArgumentException()
// ----------------------------------------------------------------------- //
// Note: driver interaction is synchronized, so we don't have to lock here.
def add(item: ItemStack, id: Int) =
Drivers.driverFor(item) match {
case None => None
case Some(driver) if !components.contains(id) =>
val component = driver.instance.component(item)
components += id -> (component, driver)
driver.instance.onInstall(this, component)
signal("component_install", id)
Some(driver.instance)
}
stateMonitor.synchronized(state match {
// We don't push new signals when stopped or shutting down.
case State.Stopped | State.Stopping => /* Nothing. */
// Currently sleeping. Cancel that and start immediately.
case State.Sleeping =>
future.cancel(true)
state = State.Suspended
signals.offer(new Signal(name, args.toArray))
future = Executor.pool.submit(this)
// Running or in driver call or only a short yield, just push the signal.
case _ =>
signals.offer(new Signal(name, args.toArray))
})
}
def add(block: Block, x: Int, y: Int, z: Int, id: Int) =
Drivers.driverFor(block) match {
case None => None
case Some(driver) if !components.contains(id) =>
val component = driver.instance.component(x, y, z)
components += id -> (component, driver)
driver.instance.onInstall(this, component)
signal("component_install", id)
Some(driver.instance)
}
def remove(id: Int) =
if (components.contains(id)) {
val (component, driver) = components(id)
components.remove(id)
driver.instance.onUninstall(this, component)
signal("component_uninstall", id)
true
}
else false
// ----------------------------------------------------------------------- //
def readFromNBT(nbt: NBTTagCompound): Unit =
saveMonitor.synchronized(this.synchronized {
@ -347,7 +429,11 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
return false
}
def init(): Boolean = {
// ----------------------------------------------------------------------- //
// Internals
// ----------------------------------------------------------------------- //
private def init(): Boolean = {
// Creates a new state with all base libraries and the persistence library
// loaded into it. This means the state has much more power than it
// rightfully should have, so we sandbox it a bit in the following.
@ -359,18 +445,38 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
try {
// Push a couple of functions that override original Lua API functions or
// that add new functionality to it.1)
lua.getGlobal("os")
// that add new functionality to it.
lua.newTable()
// Return ingame time for os.time().
// Set up driver information callbacks, i.e. to query for the presence of
// components with a given ID, and their type.
lua.pushJavaFunction(new JavaFunction() {
def invoke(lua: LuaState): Int = {
// Minecraft starts days at 6 o'clock, so we add those six hours.
lua.pushNumber(worldTime + 6000)
lua.pushBoolean(components.contains(lua.checkInteger(1)))
return 1
}
})
lua.setField(-2, "time")
lua.setField(-2, "exists")
lua.pushJavaFunction(new JavaFunction() {
def invoke(lua: LuaState): Int = {
val id = lua.checkInteger(1)
components.get(id) match {
case None => lua.pushNil()
case Some((component, driver)) =>
lua.pushString(driver.instance.componentName)
}
return 1
}
})
lua.setField(-2, "type")
// "Pop" the components table.
lua.setGlobal("component")
// Push a couple of functions that override original Lua API functions or
// that add new functionality to it.
lua.getGlobal("os")
// Custom os.clock() implementation returning the time the computer has
// been running, instead of the native library...
@ -385,6 +491,29 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
})
lua.setField(-2, "clock")
// Return ingame time for os.time().
lua.pushJavaFunction(new JavaFunction() {
def invoke(lua: LuaState): Int = {
// Game time is in ticks, so that each day has 24000 ticks, meaning
// one hour is game time divided by one thousand. Also, Minecraft
// starts days at 6 o'clock, so we add those six hours. Thus:
// timestamp = (time + 6000) / 1000[h] * 60[m] * 60[s] * 1000[ms]
lua.pushNumber((worldTime + 6000) * 60 * 60)
return 1
}
})
lua.setField(-2, "time")
// Date-time formatting using Java's formatting capabilities.
lua.pushJavaFunction(new JavaFunction() {
def invoke(lua: LuaState): Int = {
val calendar = Calendar.getInstance(Locale.ENGLISH);
calendar.setTimeInMillis(lua.checkInteger(1))
return 1
}
})
lua.setField(-2, "date")
// Custom os.difftime(). For most Lua implementations this would be the
// same anyway, but just to be on the safe side.
lua.pushJavaFunction(new JavaFunction() {
@ -494,8 +623,8 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
// building a table of permanent values used when persisting/unpersisting
// the state.
lua.newTable()
lua.setGlobal("drivers")
Drivers.injectInto(this)
lua.setGlobal("driver")
Drivers.installOn(this)
// Run the boot script. This creates the global sandbox variable that is
// used as the environment for any processes the kernel spawns, adds a
@ -538,9 +667,14 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
return false
}
def close(): Unit = stateMonitor.synchronized(
private def close(): Unit = stateMonitor.synchronized(
if (state != State.Stopped) {
state = State.Stopped
// Shutdown any installed components.
for ((component, driver) <- components.values)
driver.instance.onUninstall(this, component)
lua.setTotalMemory(Integer.MAX_VALUE);
lua.close()
lua = null
@ -595,15 +729,7 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
// Got a signal, inject it and call any handlers (if any).
case signal => {
lua.pushString(signal.name)
signal.args.foreach {
case arg: Byte => lua.pushInteger(arg)
case arg: Short => lua.pushInteger(arg)
case arg: Int => lua.pushInteger(arg)
case arg: Long => lua.pushNumber(arg)
case arg: Float => lua.pushNumber(arg)
case arg: Double => lua.pushNumber(arg)
case arg: String => lua.pushString(arg)
}
signal.args.foreach(lua.pushJavaObject)
lua.resume(1, 1 + signal.args.length)
}
}
@ -700,27 +826,27 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
/** The computer is paused and waiting for the game to resume. */
val DriverReturnPaused = Value("DriverReturnPaused")
}
/** Singleton for requesting executors that run our Lua states. */
private object Executor {
val pool = Executors.newScheduledThreadPool(Config.threads,
new ThreadFactory() {
private val threadNumber = new AtomicInteger(1)
private val group = System.getSecurityManager() match {
case null => Thread.currentThread().getThreadGroup()
case s => s.getThreadGroup()
}
def newThread(r: Runnable): Thread = {
val name = "OpenComputers-" + threadNumber.getAndIncrement()
val thread = new Thread(group, r, name)
if (!thread.isDaemon())
thread.setDaemon(true)
if (thread.getPriority() != Thread.MIN_PRIORITY)
thread.setPriority(Thread.MIN_PRIORITY)
return thread
}
})
}
}
/** Singleton for requesting executors that run our Lua states. */
private[computer] object Executor {
val pool = Executors.newScheduledThreadPool(Config.threads,
new ThreadFactory() {
private val threadNumber = new AtomicInteger(1)
private val group = System.getSecurityManager() match {
case null => Thread.currentThread().getThreadGroup()
case s => s.getThreadGroup()
}
def newThread(r: Runnable): Thread = {
val name = "OpenComputers-" + threadNumber.getAndIncrement()
val thread = new Thread(group, r, name)
if (!thread.isDaemon())
thread.setDaemon(true)
if (thread.getPriority() != Thread.MIN_PRIORITY)
thread.setPriority(Thread.MIN_PRIORITY)
return thread
}
})
}

View File

@ -1,23 +1,43 @@
package li.cil.oc.server.computer
import java.lang.reflect.Method
import scala.compat.Platform.EOL
import scala.reflect.runtime.universe._
import com.naef.jnlua.JavaFunction
import com.naef.jnlua.LuaRuntimeException
import com.naef.jnlua.LuaState
import li.cil.oc.OpenComputers
import li.cil.oc.api._
import li.cil.oc.api.Callback
import li.cil.oc.api.IDriver
import li.cil.oc.common.computer.IInternalComputerContext
class ItemDriver(val instance: IItemDriver) extends Driver
class BlockDriver(val instance: IBlockDriver) extends Driver
private[oc] class Driver(val driver: IDriver) {
def injectInto(context: IInternalComputerContext) {
/**
* Wrapper for external drivers.
*
* We create one instance per registered driver of this. It is used to inject
* the API the driver offers into the computer, and, in particular, for
* generating the wrappers for the API functions, which are closures with the
* computer the API was installed into to provide context should a function
* require it.
*/
abstract private[oc] class Driver {
/** The actual driver as registered via the Drivers registry. */
def instance: IDriver
/** Installs this driver's API on the specified computer. */
def installOn(computer: Computer) {
// Check if the component actually provides an API.
val api = driver.apiName
val api = instance.apiName
if (api == null || api.isEmpty()) return
val lua = context.luaState
if (api.equals("component")) {
OpenComputers.log.warning("Trying to register API with reserved name 'component'.")
return
}
val lua = computer.lua
// Get or create table holding API tables.
lua.getGlobal("drivers") // ... drivers?
lua.getGlobal("driver") // ... drivers?
assert(!lua.isNil(-1)) // ... drivers
// Get or create API table.
@ -29,95 +49,125 @@ private[oc] class Driver(val driver: IDriver) {
lua.setField(-3, api) // ... drivers api
} // ... drivers api
for (method <- driver.getClass().getMethods())
method.getAnnotation(classOf[Callback]) match {
case null => Unit // No annotation.
case annotation => {
val mirror = runtimeMirror(instance.getClass.getClassLoader)
val instanceMirror = mirror.reflect(instance)
val instanceType = instanceMirror.symbol.typeSignature
for (method <- instanceType.members collect { case m if m.isMethod => m.asMethod })
method.annotations collect {
case annotation: Callback => {
val name = annotation.name
lua.getField(-1, name) // ... drivers api func?
if (lua.isNil(-1)) { // ... drivers api nil
// No such entry yet.
lua.pop(1) // ... drivers api
lua.pushJavaFunction(new MethodWrapper(context, method)) // ... drivers api func
lua.pushJavaFunction(new APIClosure(mirror, instanceMirror.reflectMethod(method), computer)) // ... drivers api func
lua.setField(-2, name) // ... drivers api
}
else { // ... drivers api func
// Entry already exists, skip it.
lua.pop(1) // ... drivers api
// TODO Log warning properly via a logger.
println("WARNING: Duplicate API entry, ignoring: " + api + "." + name)
// Note that we can be sure it's an issue with two drivers
// colliding, because this is guaranteed to run before any user
// code had a chance to mess with the table.
OpenComputers.log.warning(String.format(
"Duplicate API entry, ignoring %s.%s of driver %s.",
api, name, instance.componentName))
}
}
} // ... drivers api
lua.pop(2) // ...
}
private class MethodWrapper(val context: IInternalComputerContext, val method: Method) extends JavaFunction {
def invoke(state: LuaState): Int = {
return 0
// Run custom init script.
val apiCode = instance.apiCode
if (apiCode != null) {
try {
lua.load(apiCode, instance.apiName, "t") // ... func
apiCode.close()
lua.call(0, 0) // ...
}
catch {
case e: LuaRuntimeException =>
OpenComputers.log.warning(String.format(
"Initialization code of driver %s threw an error: %s",
instance.componentName, e.getLuaStackTrace.mkString("", EOL, EOL)))
case e: Throwable =>
OpenComputers.log.warning(String.format(
"Initialization code of driver %s threw an error: %s",
instance.componentName, e.getStackTraceString))
}
}
}
/*
private class MethodWrapper(val context: IInternalComputerContext, val method: Method) extends JavaFunction {
private val classOfBoolean = classOf[Boolean]
private val classOfByte = classOf[Byte]
private val classOfShort = classOf[Short]
private val classOfInteger = classOf[Int]
private val classOfLong = classOf[Long]
private val classOfFloat = classOf[Float]
private val classOfDouble = classOf[Double]
private val classOfString = classOf[String]
private val parameterTypes = method.getParameterTypes.zipWithIndex
private val parameterCount = parameterTypes.size
private val returnType = method.getReturnType
private val returnsTuple = returnType.isInstanceOf[Array[Object]]
private val returnsNothing = returnType.equals(Void.TYPE)
/**
* This class is used to represent closures for driver API callbacks.
*
* It stores the computer it is used by, to allow passing it along as the
* {@see IComputerContext} for callbacks if they specify it as a parameter,
* and for interacting with the Lua state to pull parameters and push results
* returned from the callback.
*/
private class APIClosure(mirror: Mirror, val method: MethodMirror, val computer: Computer) extends JavaFunction {
/**
* Based on the method's parameters we build a list of transformations
* that convert Lua input to the expected parameter type.
*/
val parameterTransformations = buildParameterTransformations(mirror, method.symbol)
// TODO Rework all of this, most likely won't work because of type erasure.
def invoke(state: LuaState): Int = {
// Parse the parameters, convert them to Java types.
val parameters = Array(context) ++ parameterTypes.map {
//case (classOfBoolean, i) => boolean2Boolean(state.checkBoolean(i + 1))
case (classOfByte, i) => java.lang.Byte.valueOf(state.checkInteger(i + 1).toByte)
case (classOfShort, i) => java.lang.Short.valueOf(state.checkInteger(i + 1).toShort)
case (classOfInteger, i) => java.lang.Integer.valueOf(state.checkInteger(i + 1))
case (classOfLong, i) => java.lang.Long.valueOf(state.checkInteger(i + 1).toLong)
case (classOfFloat, i) => java.lang.Float.valueOf(state.checkNumber(i + 1).toFloat)
case (classOfDouble, i) => java.lang.Double.valueOf(state.checkNumber(i + 1))
case (classOfString, i) => state.checkString(i + 1)
case _ => null
}
/**
* Based on the method's return value we build a callback that will push
* that result to the stack, if any, and return the number of pushed values.
*/
val returnTransformation = buildReturnTransformation(method.symbol)
// Call the actual function, grab the result, if any.
val result = call(parameters: _*)
// Check the result, convert it to Lua.
if (returnsTuple) {
val array = result.asInstanceOf[Array[Object]]
array.foreach(v => push(state, v, v.getClass()))
return array.length
}
else if (returnsNothing) {
return 0
}
else {
push(state, result, returnType)
return 1
}
}
private def push(state: LuaState, value: Object, clazz: Class[_]) = clazz match {
case classOfBoolean => state.pushBoolean(value.asInstanceOf[Boolean])
case classOfInteger => state.pushNumber(value.asInstanceOf[Int])
case classOfDouble => state.pushNumber(value.asInstanceOf[Double])
case classOfString => state.pushString(value.asInstanceOf[String])
case _ => state.pushNil()
}
protected def call(args: AnyRef*) = {
method.invoke(driver, args)
/** This is the function actually called from the Lua state. */
def invoke(lua: LuaState): Int = {
return returnTransformation(computer,
method(parameterTransformations.map(_(computer)): _*))
}
}
*/
/**
* This generates the transformation functions that are used to convert the
* Lua parameters to Java types to be passed on to the API function.
*/
private def buildParameterTransformations(mirror: Mirror, method: MethodSymbol): Array[Computer => Any] = {
// No parameters?
if (method.paramss.length == 0 || method.paramss(0).length == 0) {
return Array()
}
val params = method.paramss(0)
// Do we have a callback function that wants to handle its arguments
// manually? If so, convert all arguments and pack them into an array.
// TODO test if this really works.
if (params.length == 2 &&
params(0) == typeOf[IComputerContext].typeSymbol &&
params(1) == typeOf[Array[Any]].typeSymbol) {
Array(
(c: Computer) => c,
(c: Computer) => (1 to c.lua.getTop()).map(
c.lua.toJavaObject(_, classOf[Object])))
}
// Otherwise build converters based on the method's signature.
else params.zipWithIndex.map {
case (t, i) if t == typeOf[IComputerContext].typeSymbol => (c: Computer) => c
case (t, i) => {
val clazz = mirror.runtimeClass(t.asClass)
(c: Computer) => c.lua.toJavaObject(i, clazz)
}
}.toArray
}
/**
* This generates the transformation function that is used to convert the
* return value of an API function to a Lua type and push it onto the stack,
* returning the number of values pushed. We need that number in case we
* return a tuple (i.e. the function returned an array).
*/
private def buildReturnTransformation(method: MethodSymbol): (Computer, Any) => Int =
method.returnType match {
case t if t == Unit => (c, v) => 0
case t => (c, v) => c.lua.pushJavaObject(v); 1
}
}

View File

@ -1,37 +1,48 @@
package li.cil.oc.server.computer
import scala.collection.mutable.Map
import scala.collection.mutable.ArrayBuffer
import li.cil.oc.api.IBlockDriver
import li.cil.oc.api.IItemDriver
import li.cil.oc.common.computer.IInternalComputerContext
import net.minecraft.block.Block
import net.minecraft.item.ItemStack
/**
* This class keeps track of registered drivers and provides installation logic
* for each registered component type.
* for each registered driver.
*
* Each component type must register its driver with this class to be used with
* computers, since this class is used to determine whether an object is a
* valid component or not.
*
* All drivers must be installed once the game starts - in the init phase - and
* are then injected into all computers started up past that point. A driver is
* a set of functions made available to the computer. These functions will
* usually require a component of the type the driver wraps to be installed in
* the computer, but may also provide context-free functions.
*/
private[oc] object Drivers {
private val blocks = Map.empty[Int, Driver]
private val items = Map.empty[Int, Driver]
/** The list of registered block drivers. */
private val blocks = ArrayBuffer.empty[BlockDriver]
/** The list of registered item drivers. */
private val items = ArrayBuffer.empty[ItemDriver]
/** Used to keep track of whether we're past the init phase. */
var locked = false
/**
* Registers a new driver for a block component.
*
* Whenever the neighboring blocks of a computer change, it checks if there
* exists a driver for the changed block, and if so adds it to the list of
* available components.
* exists a driver for the changed block, and if so installs it.
*
* @param driver the driver for that block type.
*/
def addDriver(driver: IBlockDriver) {
if (blocks.contains(driver.blockType.blockID)) return
blocks += driver.blockType.blockID -> new Driver(driver)
def add(driver: IBlockDriver) {
if (locked) throw new IllegalStateException("Please register all drivers in the init phase.")
if (!blocks.exists(entry => entry.instance == driver))
blocks += new BlockDriver(driver)
}
/**
@ -42,15 +53,34 @@ private[oc] object Drivers {
*
* @param driver the driver for that item type.
*/
def addDriver(driver: IItemDriver) {
if (items.contains(driver.itemType.itemID)) return
items += driver.itemType.itemID -> new Driver(driver)
def add(driver: IItemDriver) {
if (locked) throw new IllegalStateException("Please register all drivers in the init phase.")
if (!blocks.exists(entry => entry.instance == driver))
items += new ItemDriver(driver)
}
def getDriver(block: Block) = blocks(block.blockID)
/**
* Used when a new block is placed next to a computer to see if we have a
* driver for it. If we have one, we'll return it.
*
* @param block the type of block to check for a driver for.
* @return the driver for that block type if we have one.
*/
def driverFor(block: Block) = blocks.find(_.instance.worksWith(block))
def getDriver(item: ItemStack) = blocks(item.itemID)
/**
* Used when an item component is added to a computer to see if we have a
* driver for it. If we have one, we'll return it.
*
* @param item the type of item to check for a driver for.
* @return the driver for that item type if we have one.
*/
def driverFor(item: ItemStack) = items.find(_.instance.worksWith(item))
def injectInto(context: IInternalComputerContext) =
(blocks.values ++ items.values).foreach(_.injectInto(context))
/**
* Used by the computer to initialize its Lua state, injecting the APIs of
* all known drivers.
*/
private[computer] def installOn(computer: Computer) =
(blocks ++ items).foreach(_.installOn(computer))
}

View File

@ -1,9 +0,0 @@
package li.cil.oc.server.computer
import com.naef.jnlua.LuaState
import net.minecraft.nbt.NBTTagCompound
trait IComputerContext {
def signal(name: String, args: Any*)
}

View File

@ -1,37 +1,35 @@
package li.cil.oc.server.drivers
import li.cil.oc.Items
import li.cil.oc.Config
import li.cil.oc.api.Callback
import li.cil.oc.api.ComponentType
import li.cil.oc.api.IItemDriver
import li.cil.oc.server.components.Disk
import net.minecraft.item.ItemStack
import li.cil.oc.api.Callback
object HDDDriver extends IItemDriver {
def componentName = "disk"
def apiName = "disk"
def apiCode = null
override def apiName = "disk"
@Callback(name = "mount")
def mount(component: Object, path: String) {
def mount(component: Any, path: String) {
}
def componentType = ComponentType.HDD
def worksWith(item: ItemStack) = item.itemID == Config.itemHDDId
def itemType = new ItemStack(Items.hdd)
def componentType(item: ItemStack) = ComponentType.HDD
def getComponent(item: ItemStack) = new HDDComponent(item)
def component(item: ItemStack) = new HDDComponent(item)
def close(component: Object) {
def close(component: Any) {
component.asInstanceOf[HDDComponent].close()
}
}
class HDDComponent(val item: ItemStack) {
val disk = new Disk()
def close() = disk.close()
}