在此文章中我会讲解OC模组的架构是如何工作的,以及如何添加自定义架构。一句警告:架构的实现并非易事,别指望着轻而易举就能完成。
首先让我们来申明OC模组语境下的“架构”是什么。架构指将某种代码执行器(以下称虚拟机)和OC模组的组件/Minecraft结合起来的程序。OC将组件逻辑与电脑逻辑严格分开。换言之,组件(例如显卡)不知道它自身在使用什么架构。反过来,架构也不知道组件如何工作。传递的一切信息都由执行机处理。执行机驱动架构,并提供回调函数使架构能与外界通信。
为了让内容不那么难以理解,这里将讲解如何实际应用架构。这大概能让读者更好地理解架构实现是怎么融入OC模组生态的。要使用架构,则必须创建一个使用此架构的执行机。
执行机封装了架构,并负责线程调度、信号队列、网络连接等。执行机创建好以后,你只需每tick调用一次update()
函数(还可能需要向其传递一些调用,例如start()
)。
架构的生命周期大致为:实例化,initialize()
,反复执行runThreaded()
/ runSynchronized()
,close()
[, 转到 initialize()
]。
initialize()
自身。通常来说代表着创建底层虚拟机。runThreaded()
函数,它由执行器线程调用。架构的状态应在此处处理。其返回值类型被执行机用于调度。注意:此方法不应抛出异常。你有责任处理所有错误,并在需要的时候将它们转换为代表报错的返回值。 runSynchronized()
来驱动架构。这种调用一定会由MC服务端线程执行,代表着可以安全地在此回调函数中与世界交互。close()
函数。然后释放所有打开的资源。
在最简单的样例中,每次调用runThreaded()
时在你的虚拟机中仅仅进行若干run
调用。不过这样做实际用处不大,因为你会想要以某种方式与外界通信。为了实现此功能,需要将与组件的通信提供给虚拟机。你需要在虚拟机内定义用于与执行机API通信的API(例如:components()
和invoke()
)。
组件可能有一些回调函数声明为“直接调用”,此外它们可能还会定义“调用上限”。默认情况下,所有的组件回调函数都是同步调用,即架构必须保证这些函数只能由runSynchronized()
调用。
直接调用的调用上限限制了单个执行机每tick调用某一个函数的次数。限制本身由执行机的invoke
方法确保实现,此方法会在调用次数过多时抛出LimitReachedException
异常。此时架构应当短暂休眠,或者改为执行非同步调用。
Component
接口中可用。现在的话,要么将它们反射进去,要么从OC源码自行构建模组。我对此表示抱歉。Let's say you have some VM that provides the following interfaces:
/** The VM itself. This is just an example, it's not a "real" interface. */ public interface PseudoVM { Object[] run(Object[] args) throws Exception; void setApiFunction(String name, PseudoNativeFunction value); } /** Interface defining callbacks provided by the host. */ public interface PseudoNativeFunction { Object invoke(Object[] args); }
A very primitive architecture implementation might look something like this:
/** This is the class you implement; Architecture is from the OC API. */ @Architecture.Name("Pseudolang") public class PseudoArchitecture implements Architecture { private final Machine machine; private PseudoVM vm; /** The constructor must have exactly this signature. */ public PseudoArchitecture(Machine machine) { this.machine = machine; } public boolean isInitialized() { return true; } public void recomputeMemory() {} public boolean initialize() { // Set up new VM here, and register all API callbacks you want to // provide to it. vm = new PseudoVM(); vm.setApiFunction("invoke", new PseudoNativeFunction() { public Object invoke(Object[] args) { final String address = (String)args[0]; final String method = (String)args[1]; final Object[] params = (Object[])args[2]; try { return new Object[]{true, machine.invoke(address, method, params)}; } catch (e LimitReachedException) { // Perform logic also used to sleep / perform synchronized calls. // In this example we'll follow a protocol where if this returns // (true, something) the call succeeded, if it returns (false) // the limit was reached. // The script running in the VM is then supposed to return control // to the caller initiating the current execution (e.g. by yielding // if supported, or just returning, when in an event driven system). return new Object[]{false}; } } }); vm.setApiFunction("isDirect", new PseudoNativeFunction() { public Object invoke(Object[] args) { final String address = (String)args[0]; final String method = (String)args[1]; final Node node = machine.node().network().node(address); if (node instanceof Component) { final Component component = (Component) node; if (component.canBeSeenFrom(machine.node())) { final Callback callback = machine.methods(node.host()).get(method); if (callback != null) { return callback.direct(); } } } return false; } }); // ... more callbacks. return true; } void close() { vm = null; } ExecutionResult runThreaded(boolean isSynchronizedReturn) { // Perform stepping in here. Usually you'll want to resume the VM // by passing it the next signal from the queue, but you may decide // to allow your VM to poll for signals manually. try { final Signal signal; if (isSynchronizedReturn) { // Don't pull signals when we're coming back from a sync call, // since we're in the middle of something else! signal = null; } else { signal = machine.popSignal(); } final Object[] result; if (signal != null) { result = vm.run(new Object[]{signal.name(), signal.args()}); } else { result = vm.run(null); } // You'll want to define some internal protocol by which to decide // when to perform a synchronized call. Let's say we expect the VM // to return either a number for a sleep, a boolean to indicate // shutdown/reboot and anything else a pending synchronous call. if (result != null) { if (result[0] instanceof Boolean) { return new ExecutionResult.Shutdown((Boolean)result[0]); } if (result[0] instanceof Integer) { return new ExecutionResult.Sleep((Integer)result[0]); } } // If this is returned, the next 'resume' will be runSynchronized. // The next call to runThreaded after that call will have the // isSynchronizedReturn argument set to true. return new ExecutionResult.SynchronizedCall(); } catch (Throwable t) { return new ExecutionResult.Error(t.toString); } } void runSynchronized() { // Synchronized calls are run from the MC server thread, making it // easier for callbacks to interact with the world (because sync is // taken care for them by the machine / architecture). // This means that if some code in the VM starts a sync call it has // to *pause* and relinquish control to the host, where we then // switch to sync call mode (see runThreaded), wait for the MC server // thread, and then do the actual call. It'd be possible to pass the // info required for the call out in runThreaded, keep it around in // the arch and do the call directly here. For this example, let's // assume the state info is kept inside the VM, and the next resume // makes it perform the *actual* call. For some pseudo-code handling // this in the VM, see below. vm.run(null); } void onConnect() {} // Use this to load the VM state, if it can be persisted. void load(NBTTagCompound nbt) {} // Use this to save the VM state, if it can be persisted. void save(NBTTagCompound nbt) {} }
Some pseudo-code for handling synchronized calls in the VM:
private def invokeSynchronous(address, method, ...) { yield; // This is where it returns to runThreaded(). // This is where we enter in runSynchronized(); val result = native.invoke(address, method, ...); // See definition of invoke in initialize() for values of result. yield; // And return to runSynchronized(); // And the next runThreaded() enters again. return result[1]; } private def invokeDirect(address, method, ...) { val result = native.invoke(address, method, ...); // See definition of invoke in initialize() for values of result. if (result[0] == true) { return result[1]; } else { return invokeSynchronous(address, method, ...); } } def invoke(address, method, ...) { if (isDirect(address, method)) { return invokeDirect(address, method, ...); } else { return invokeSynchronous(address, method, ...); } }
这一步非常简单,只需调用li.cil.oc.api.Machine.add(PseudoArchitecture.class)
即可。注册架构后CPU即可提供被注册的架构。如果你不想让OC模组的CPU提供你的架构,那么你可以添加自己的Processor
驱动程序以用于访问架构。或者更进一步,添加你自己的电脑方块,专用于运行你的架构。
教程 | 模组特有内容 | 基础电脑 - 编写代码 - 硬盘 - Autorun与Startup自启动脚本 | |
---|---|---|---|
模组制作 | 自定义架构 - IMC 信息 - OC 1.3 中的API改动 - OC 1.4 中的API改动 - 从源码构建并运行 master-MC1.7.10 版本的 OC | ||
程序 | OPPM - install | ||
其它 | 自定义操作系统 - 中文翻译守则 |