cc-haxe/doc/Concepts.md

254 lines
11 KiB
Markdown
Raw Permalink Normal View History

2023-06-25 19:23:13 +00:00
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"};
2023-07-30 13:38:46 +00:00
Net.sendAndAwait(netID,"protoname",data).map((response)->{
2023-06-25 19:23:13 +00:00
switch (response){
case Success(data):
trace(data);
case Failure(error):
trace(error);
}
});
2023-07-30 13:38:46 +00:00
Net.registerProto("res",(pack: GenericPackage)->{
2023-06-25 19:23:13 +00:00
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
2023-07-27 18:38:23 +00:00
var back = Peripheral.getRedstone("back");
2023-06-25 19:23:13 +00:00
back.setOutput(true);
2024-08-05 11:59:23 +00:00
var drive = Peripheral.getDrive("drive_0");
2023-06-25 19:23:13 +00:00
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
2023-07-27 18:41:23 +00:00
var ctx = WindowManager.createNewBufferedContext();
2023-06-25 19:23:13 +00:00
ctx.setCursorPos(0, 0);
ctx.setCursorBlink(false);
ctx.setBackgroundColor(Blue);
ctx.setForegroundColor(White);
ctx.write("Hello world!");
```
2023-06-30 21:35:57 +00:00
## 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.
2023-06-25 19:23:13 +00:00
# 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.
2024-08-05 12:39:16 +00:00
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.
2023-06-25 19:23:13 +00:00
2024-08-05 12:39:16 +00:00
More on that at [Applications](#applications).
2023-06-25 19:23:13 +00:00
# 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.
2024-01-22 22:37:02 +00:00
# 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();
}
});
```
2024-03-20 23:22:53 +00:00
# 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');
}
});
```
2024-08-05 11:59:23 +00:00
# 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.
2024-08-05 12:39:16 +00:00
# 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.