254 lines
11 KiB
Markdown
254 lines
11 KiB
Markdown
Use this if you want to know how the OS works.
|
|
|
|
# Network
|
|
|
|
Every computer has a id assigned by the mod. It is a unique number that identifies the computer and cannot be changed.
|
|
The native implementation of the network stack works with channels. If you listen on a channel you will receive all messages sent to that channel.
|
|
Every computer listens to the channel with its id and a brodcast channel.
|
|
The broadcast channel is used for Routing Tables and GPS packets.
|
|
|
|
Network messages can be forwarded by other computers. This is done by the Routing Table. The routing algorithm prefers wire over wireless connections.
|
|
|
|
Packages also have a time-to-live (TTL) value. This is used to prevent packages from being forwarded forever. You will get a response if a package is dropped because of TTL.
|
|
|
|
There is also a concept of protocols. A protocol is used to distinguish between different types of packages.
|
|
A protocol is basically a string and that is used to forward packages to the correct handler. You can use `registerProto` to listen for packages with a specific protocol.
|
|
|
|
There are 2 ways of sending messages to other computers: `sendAndAwait` and `sendAndForget`.
|
|
`sendAndAwait` will wait for a response from the remote computer and return it.
|
|
`sendAndForget` will send the message and return immediately and does not care about the response.
|
|
You can compare it to UDP and TCP.
|
|
|
|
## Usage
|
|
|
|
```haxe
|
|
import net.Net;
|
|
|
|
var data = {"foo": "bar"};
|
|
|
|
Net.sendAndAwait(netID,"protoname",data).map((response)->{
|
|
switch (response){
|
|
case Success(data):
|
|
trace(data);
|
|
case Failure(error):
|
|
trace(error);
|
|
}
|
|
});
|
|
|
|
Net.registerProto("res",(pack: GenericPackage)->{
|
|
var requestPack: Package<MyType> = cast pack; // Try not to use Dynamic
|
|
|
|
requestPack.respond("Hello Back");
|
|
});
|
|
|
|
```
|
|
|
|
# Peripherals
|
|
|
|
Peripherals are devices that are connected to the computer. They can be used to interact with the world.
|
|
Every peripheral has an address and a type. The address can be "back" or "right" to refer to the peripheral on the back or right side of the computer or
|
|
something like "energyCell_0" to refer to something connected via cable. Peripherals can be accessed via the Peripheral class.
|
|
|
|
Also peripherals can be made accessible via the network. More on that later.
|
|
|
|
## Usage
|
|
|
|
```haxe
|
|
var back = Peripheral.getRedstone("back");
|
|
back.setOutput(true);
|
|
|
|
var drive = Peripheral.getDrive("drive_0");
|
|
drive.eject();
|
|
```
|
|
|
|
# GUI
|
|
|
|
If you want to write something to the screen you have to create a `WindowContext` via the `WindowManager`. This allows programs to write to the screen without interfering with each other.
|
|
|
|
There are currently 2 types of `WindowContext`: the `BufferedVirtualTermWriter` that stores the state of the screen in a buffer and prints it to the screen
|
|
when it is activeted and the `StatelessVirtualTermWriter` which calls a render method when it is activated. Currently i prefer the `StatelessVirtualTermWriter` because its not so heavy on the RAM but both work.
|
|
|
|
They both can be used just like the [nativ implmentation](https://tweaked.cc/module/term.html).
|
|
|
|
## Usage
|
|
|
|
```haxe
|
|
var ctx = WindowManager.createNewBufferedContext();
|
|
ctx.setCursorPos(0, 0);
|
|
ctx.setCursorBlink(false);
|
|
ctx.setBackgroundColor(Blue);
|
|
ctx.setForegroundColor(White);
|
|
ctx.write("Hello world!");
|
|
```
|
|
|
|
## Under the hood
|
|
|
|
There are a number of interfaces and classes that needs to be explained to understand how the GUI works.
|
|
|
|
`TermWriteable` is an interface that allows the usage of the normal CC terminal write methods. Stuff like `write`, `setCursorPos` and `setCursorBlink` are defined here. This is of course implemented by the physical screens and the main terminal.
|
|
|
|
Most of the time you will not write directory to a real screen but to a `VirtualTermWriter` which extends `TermWriteable` with some more methods like `enable`
|
|
and `setTarget`. The `setTarget` is used as the proxy target of a `VirtualTermWriter` and with `enable` and `disable` you can enable and disable the forwarding of the write methods to the target.
|
|
|
|
The `StatelessVirtualTermWriter` and `BufferedVirtualTermWriter` are both `VirtualTermWriter`. They can have a real output as a target. Or they can have another `VirtualTermWriter` as target like the `BufferedVirtualTermWriter` which uses a `TermBuffer` as an intermediate target.
|
|
|
|
All of that is just for printing to the screen. If you want to read input you have to use the `WindowContext` which is a `TermWriteable`.
|
|
`WindowContext` also handles events like `onClick` or `onKey`. This is need so that the right program gets the input depending on the active window on the
|
|
screen or terminal.
|
|
|
|
All of the `WindowContext` are managed by the `WindowManager`. The `WindowManager` also delegates the events to the right `WindowContext`.
|
|
|
|
## GUI helper classes
|
|
|
|
Because we want a more abstract way of writing to the screen we have some "helper" classes. I call them "helper" but they a very essential to the GUI.
|
|
|
|
First there is the `Pixel` class which is nothing more that a char and a foreground and background color.
|
|
|
|
A collection of `Pixel` is called a `Canvas` which is nothing more than a 2D array of `Pixel` with some functions strapped to it.
|
|
|
|
# Proceses
|
|
|
|
The concept of processes tryes to encapsulate programs. A process is basically an interface with the `run(handle: ProcessHandle)` method.
|
|
The idea is that you can register all you disposable resources in the handle and they will be disposed when the process is killed or crashes.
|
|
|
|
A process can be used as a command on the terminal or as a service. Basically everything that runs and is not part of the kernel is a process.
|
|
|
|
More on that at [Applications](#applications).
|
|
|
|
# EndOfLoop
|
|
|
|
You can imagine the whole runtime like the event loop is JS. The `EndOfLoop` class is used to register callbacks that are called at the end of the loop.
|
|
This is like the `setTimeout(0, callback)` in JS.
|
|
|
|
# Turtle Thread
|
|
|
|
Computercraft is mostly event-based. Listening to `Coroutine.yield()` waits for events with an option parameter for filtering events.
|
|
The problem with turtle arises when we call turtle functions like `move` or `place`. These functions wait internally for
|
|
the operation to finish with a `Coroutine.yield("turtle_response")` call. There is no way to prevent this. So in order to keep the main loop from
|
|
being blocked, we run all the turtle code in a separate thread.
|
|
|
|
There are no real threads in Lua, but we can create a coroutine for the turtles. Lua now switches between the main thread and the turtle thread.
|
|
Switching occurs when a `turtle_response` events is fired. This will be forwarded to the turtle thread. Once the turtle thread has run its code
|
|
and waits for the turtle operation to finish, we switch back to the main thread. The main thread is running normally until a `turtle_response` fires
|
|
and the circle repeats.
|
|
|
|
Be aware that no one is stopping you from running turtle commands in the main thread.
|
|
|
|
See `KernelEvents.hx` for more.
|
|
|
|
## Using Turtle threads
|
|
|
|
Example:
|
|
|
|
```haxe
|
|
// In the context of a process
|
|
|
|
if (!handle.claimTurtleMutex()) {
|
|
Log.warn("Failed to claim turtle thread");
|
|
return;
|
|
}
|
|
|
|
// Btw. no one is stopping you from calling this without claiming the mutex.
|
|
TurtleMutex.runInTThread(() -> {
|
|
while(true){
|
|
Turtle.turnLeft();
|
|
}
|
|
});
|
|
```
|
|
|
|
# RPC
|
|
|
|
With the help of dark and badly documented magic also known as "macros", we can create quickly create remote procedure call Classes to call functions on other
|
|
computers. A problem that arises is that since all data gets send over the network that we kinda lose the type safty. We cloud trust ourself to cast
|
|
the result of the request to the right type or we cloud just make use of macros. The RPC macro will create a RPC class that implements all functions of an
|
|
interface just that the return type is wrapped in a Promise. Now if we call a function of that RPC class it fires a request to the other computer and waits
|
|
for a response. On the other side the service makes use of that generated package handle function for the RPC class.
|
|
|
|
A simple example:
|
|
|
|
```haxe
|
|
interface IExampleRPC {
|
|
function addNumber(a:Int, b:Int):Int;
|
|
}
|
|
|
|
@:build(macros.rpc.RPC.buildRPC(IExampleRPC))
|
|
class ExampleRPC extends RPCBase {}
|
|
|
|
|
|
@:build(macros.Binstore.includeBin("Example SRV", ["example-srv"]))
|
|
class ExampleService implements IProcess implements IExampleRPC {
|
|
private var handle:ProcessHandle;
|
|
|
|
public function new() {}
|
|
|
|
public function run(handle:ProcessHandle) {
|
|
this.handle = handle;
|
|
|
|
kernel.net.Net.registerProto("example", (pack) -> {
|
|
ExampleRPC.handlePackage(this, pack);
|
|
});
|
|
}
|
|
|
|
public function addNumber(a:Int, b:Int):Int {
|
|
return a + b;
|
|
}
|
|
}
|
|
|
|
// ...
|
|
|
|
var rpc = new ExampleRPC(12,"example");
|
|
|
|
rpc.addNumber(3,7).handle((p)->{
|
|
switch p {
|
|
case Success(r):
|
|
Log.info('3+7=$r');
|
|
case Failure(err):
|
|
Log.error('Error: $err');
|
|
}
|
|
});
|
|
```
|
|
|
|
# Build system and flags
|
|
|
|
We use `make` to build the project. If you want a prod build just run `make` and if you want the debug build run `make debug`.
|
|
The `build` directory should contain all needed files.
|
|
|
|
- `bundle.min.lua`: The minified final file
|
|
- `bundle.polyfill.lua`: The same as above just not minified. use this when debugging.
|
|
- `haxe.lua`: Intermediate file. Polyfill not yet added.
|
|
- `haxe.zip`: The compressed `bundle.min.lua`.
|
|
- `unpack.*`: Same as the bundle files. Used to unpack and run the `haxe.zip` file.
|
|
|
|
The follwing haxe flags can be used in the code or can be set in the makefile.
|
|
|
|
- `debug`: Is set when debug
|
|
- `kv_use_native`: If set use the native CC serialize and unserialize functions instead of the Haxe ones. See [KVStore.hx](../src/lib/KVStore.hx) for more.
|
|
|
|
# Applications
|
|
|
|
If you want to make an application you just need to create a class that implements IProcess and and has no arguments in the constructor.
|
|
In order to have the programm available from the terminal you need to add the `Binstore.includeBin` macro for the class. Here is an example:
|
|
|
|
```haxe
|
|
@:build(macros.Binstore.includeBin("HelloWorld", ["hello", "helloworld"]))
|
|
class HelloWorld implements IProcess {
|
|
public function new() {}
|
|
|
|
public function run(handle:ProcessHandle) {
|
|
handle.writeLine("Hello World");
|
|
handle.close(true);
|
|
}
|
|
}
|
|
```
|
|
|
|
Let's break this down:
|
|
|
|
- The `includeBin` macro is required to automaticly inlcude the app and to defeat the DCE optimisation. In early versions refections were used but DCE did not like that
|
|
- The first argument on `includeBin` is the name of the app. Only ever used in displaying.
|
|
- The second argument are the aliases. These are used to call the app from the terminal and other places
|
|
- The constructor can't have any arguments. It's also recommended to not do any major setup stuff in there.
|
|
- The `run` method is the entry point in the app.
|
|
- The handle is used to interact with the outside world. (theoretically at least)
|
|
- The `handle.close(true)` call is required to end a process. The app does not end when the run function is finished. The first arguments represents if the app was successfull.
|
|
|