# Techage APIs und Design *Hinweis: Dieses Dokument folgt dem markdown Standard und ist mit Typora erstellt. Damit hat man links das Inhaltsverzeichnis zur Übersicht und zum Navigieren. Zur Not geht aber jeder Editor.* ## History - v1.0 - 03.10.2019 - Erster Entwurf - v1.1 - 26.10.2019 - `networks.lua` hinzugefügt ## Hierarchiediagramm ``` +-------------------------------------------------------------+ +-------------------+ | consumer | | liquid | | (tubing/commands/states/formspec/power/connections/node) | | (Pipe/put/take) | +-------------------------------------------------------------+ +-------------------+ | | | | V V V V +-----------------+ +-----------------+ +-------------------+ +-------------------+ | command | | node_states | | power | | networks | |(tubing/commands)| |(states/formspec)| |(power,connections)| | (connections) | +-----------------+ +-----------------+ +-------------------+ +-------------------+ | | | | V V V V +-----------------------------------------------------------------------------------+ | Tube/tubelib2 | | (tubes, mem, get_node_pos) | +-----------------------------------------------------------------------------------+ ``` ## Klasse `Tube` (Mod tubelib2) Da Techage auf tubelib2 aufsetzt, soll auch diese Mod hier soweit behandelt werden, wie notwendig. `tubelib2` dient zur Verknüpfung von Blöcken über tubes/pipes/cables. Tubes sind dabei "primary nodes", die Blöcke "secundary nodes". Die Features dabei sind: - platzieren von Tubes, so dass diese mit benachbarten Tubes oder registrierten Blöcken eine Verbindung eingehen - Event-Handling, so dass registrierte Blöcke über Änderungen an den Tube-Verbindungen informiert werden - API-Funktionen, um die Position des Blockes gegenüber (peer node) zu bestimmen ```lua -- From source node to destination node via tubes. -- pos is the source node position, dir the output dir -- The returned pos is the destination position, dir -- is the direction into the destination node. function Tube:get_connected_node_pos(pos, dir) local key = S(pos) if self.connCache[key] and self.connCache[key][dir] then local item = self.connCache[key][dir] return item.pos2, Turn180Deg[item.dir2] end local fpos,fdir = self:walk_tube_line(pos, dir) local spos = get_pos(fpos,fdir) self:add_to_cache(pos, dir, spos, Turn180Deg[fdir]) self:add_to_cache(spos, Turn180Deg[fdir], pos, dir) return spos, fdir end -- Check if node at given position is a tubelib2 compatible node, -- able to receive and/or deliver items. -- If dir == nil then node_pos = pos -- Function returns the result (true/false), new pos, and the node function Tube:compatible_node(pos, dir) local npos = vector.add(pos, Dir6dToVector[dir or 0]) local node = self:get_node_lvm(npos) return self.secondary_node_names[node.name], npos, node end ``` Um mit `tubelib2` arbeiten zu können, muss zuvor eine Tube Instanz angelegt werden: ```lua local Tube = tubelib2.Tube:new(...) ``` Hier die Parameter: ```lua dirs_to_check = attr.dirs_to_check or {1,2,3,4,5,6}, max_tube_length = attr.max_tube_length or 1000, primary_node_names = Tbl(attr.primary_node_names or {}), secondary_node_names = Tbl(attr.secondary_node_names or {}), show_infotext = attr.show_infotext or false, force_to_use_tubes = attr.force_to_use_tubes or false, -- Block an Block oder Tubes dazw. clbk_after_place_tube = attr.after_place_tube, -- hiermit wird die Tube ausgetauscht (1) tube_type = attr.tube_type or "unknown", -- hier einen eindeutigen Namen für die Instanz ``` zu (1): Bei einfachen Tubes reicht hier: ```lua minetest.swap_node(pos, {name = "tubelib2:tube"..tube_type, param2 = param2}) ``` tube_type bei "swap_node" ist "S" oder "A" (straight or angled) ### Registrierung Alle Blöcke mit Tube-Support müssen bei `tubelib2` registriert werden über: ```lua Tube:add_secondary_node_names({names}) ``` ### Events #### Änderungen an den Nodes Damit die Tubes und die gegenüber angeschlossenen Blöcke über Änderungen informiert werden, existieren 2 Funktionen: ```lua after_place_node = function(pos, placer) Tube:after_place_node(pos [, {tube_dir}]) end, after_dig_node = function(pos, oldnode, oldmetadata, digger) Tube:after_dig_node(pos [, {tube_dir}]) end, ``` Diese müssen in jedem Fall aufgerufen werden, sonst werden die Daten der benachbarten Tubes nicht aktualisiert. Der Parameter `tube_dir` ist optional, macht aber Sinn, so dass nicht alle 6 Seiten geprüft werden müssen. #### Änderungen an Tubes/anderen Nodes Damit der Block über Änderungen an Tubes oder Peer-Blöcken informiert wird, gibt es zwei Möglichkeiten: 1. Knoten-spezifische callback Funktionen 2. Zentrale callback Funktionen ##### 1. Knoten-spezifische callback Funktion `tubelib2_on_update` ```lua tubelib2_on_update(node, pos, out_dir, peer_pos, peer_in_dir) ``` Die Funktion muss Teil von `minetest.register_node()` sein. ##### 2. Knoten-spezifische callback Funktion `tubelib2_on_update2` ```lua tubelib2_on_update2(node, pos, self) ``` Dies ist eine neue Funktion von tubelib2 v1.6 Durch den Paramater `self` können nun unterschiedliche Instanzen den tubelib2 unterschieden werden, was für die verschiedenen Kabel/Röhren von techage notwendig wurde. ##### 3. Zentrale callback Funktion `register_on_tube_update` ```lua Tube:register_on_tube_update(function(node, pos, out_dir, peer_pos, peer_in_dir) ... end) ``` Wird 1) oder 2) aufgerufen, wird 3) **nicht** mehr gerufen! ### API Funktionen ```lua tubelib2.get_pos(pos, dir) ``` ## Techage `command` ### Dir vs. Side `tubelib2` arbeitet nur mit dirs (siehe oben). Oft ist aber die Arbeitsweise mit `sides` einfacher. Techage definiert `sides` , die wie folgt definiert sind `{B=1, R=2, F=3, L=4, D=5, U=6}`: ``` sides: dirs: U | B | / 6 (N) +--|-----+ | 1 / o /| | / +--------+ | |/ L <----| |o----> R (W) 4 <-------+-------> 2 (O) | o | | /| | / | + / | | / |/ 3 | +-/------+ (S) 5 / | F | D ``` `techage/command.lua` definiert hier: ```lua techage.side_to_outdir(side, param2) -- "B/R/F/L/D/U", node.param2 ``` In Ergänzung zu `tubelib2` sind in `command` Funktionen für den Austausch von Items von Inventar zu Inventar (Tubing) und Kommandos für Datenaustausch definiert. Zusätzlich etabliert `command` das Knoten-Nummern-System für die Adressierung bei Kommandos. Dazu muss jeder Knoten bei `command` an- und abgemeldet werden: ```lua techage.add_node(pos, name) --> number techage.remove_node(pos) ``` Soll der Knoten Kommandos empfangen und/oder Items austauschen können, ist folgende Registrierung notwendig (alle Funktionen sind optional): ```lua techage.register_node(names, { on_pull_item = func(pos, in_dir, num), on_push_item = func(pos, in_dir, item), on_unpull_item = func(pos, in_dir, item), on_recv_message = func(pos, src, topic, payload), on_node_load = func(pos), -- LBM function on_transfer = func(pos, in_dir, topic, payload), }) ``` ### Client API Bspw. der Pusher als Client nutzt: ```lua techage.pull_items(pos, out_dir, num) techage.push_items(pos, out_dir, stack) techage.unpull_items(pos, out_dir, stack) ``` ### Server API Für den Server (chest mit Inventar) existieren dazu folgende Funktionen: ```lua techage.get_items(inv, listname, num) techage.put_items(inv, listname, stack) techage.get_inv_state(inv, listname) ``` ### Hopper API Es gibt bspw. mit dem Hopper aber auch einen Block, der nicht über Tubes sondern nur mit direkten Nachbarn Items austauschen soll. Dazu dient dieser Satz an Funktionen: ```lua techage.neighbour_pull_items(pos, out_dir, num) techage.neighbour_push_items(pos, out_dir, stack) techage.neighbour_unpull_items(pos, out_dir, stack) ``` ### Nummern bezogene Kommando API Kommunikation ohne Tubes, Addressierung nur über Knoten-Nummern ```lua techage.not_protected(number, placer_name, clicker_name) --> true/false techage.check_numbers(numbers, placer_name) --> true/false (for send_multi) techage.send_multi(src, numbers, topic, payload) --> to many nodes techage.send_single(src, number, topic, payload) --> to one node with response ``` ### Positions bezogene Kommando API Kommunikation mit Tubes oder mit direkten Nachbar-Knoten über pos/dir. Im Falle von Tubes muss bei `network` die Tube Instanz angegeben werden. ```lua techage.transfer(pos, outdir, topic, payload, network, nodenames) -- The destination node location is either: -- A) a destination position, specified by pos -- B) a neighbor position, specified by caller pos/outdir, or pos/side -- C) a tubelib2 network connection, specified by caller pos/outdir, or pos/side -- outdir is one of: 1..6 or alternative a 'side' -- side is one of: "B", "R", "F", "L", "D", "U" -- network is a tuebelib2 network instance -- opt: nodenames is a table of valid callee node names ``` ### Sonstige API ```lua techage.side_to_indir(side, param2) --> indir techage.get_node_info(dest_num) --> { pos, name } techage.get_node_number(pos) --> number techage.get_new_number(pos, name) --> should ne be needed (repair function) ``` ## Wrapper `power` (veraltet) Im Gegensatz zu `tubelib2` und `command` verwaltet `power` ganze Netzwerke und nicht nur Einzelverbindungen zwischen zwei Knoten. Dazu muss `power` in jedem Knoten eine Connection-Liste anlegen, die alle angeschlossenen Tubes beinhaltet. Nur so können mit der internen Funktion `connection_walk` alle Knoten im Netzwerk erreicht werden. `power` besitzt die Funktion: ```lua techage.power.register_node(names, { conn_sides = {"L", "R", "U", "D", "F", "B"}, on_power = func(pos, mem), -- für Verbraucher (einschalten) on_nopower = func(pos, mem), -- für Verbraucher (ausschalten) on_getpower = func(pos, mem), -- für Solarzellen (Strom einsammeln) power_network = Tube, -- tubelib2 Instanz after_place_node = func(pos, placer, itemstack, pointed_thing), after_dig_node = func(pos, oldnode, oldmetadata, digger) after_tube_update = func(node, pos, out_dir, peer_pos, peer_in_dir) }) ``` Durch die Registrierung des Nodes werden die Knoten-eigenen `after_...` Funktionen überschrieben. Optional können deshalb eigene Funktionen bei `register_node` übergeben werden. ```lua -- after_place_node decorator after_place_node = function(pos, placer, itemstack, pointed_thing) local res = .after_place_node(pos, placer, itemstack, pointed_thing) :after_place_node(pos) return res end, -- after_dig_node decorator after_dig_node = function(pos, oldnode, oldmetadata, digger) :after_dig_node(pos) minetest.after(0.1, tubelib2.del_mem, pos) -- At latest... return .after_dig_node(pos, oldnode, oldmetadata, digger) end, -- called after any connection change via -- --> tubelib2 -- --> register_on_tube_update callback (cable) -- --> after_tube_update (power) after_tube_update = function(node, pos, out_dir, peer_pos, peer_in_dir) mem.connections = ... -- aktualisieren/löschen return .after_tube_update(node, pos, out_dir, peer_pos, peer_in_dir) end, ``` Und es erfolgt eine Registrierung bei Tube: ``` :add_secondary_node_names({name}) ``` **Damit ist es nicht mehr notwendig, die `tubelib2` callback Funktionen `after_place_node` und `after_dig_node` sowie `after_tube_update` selbst zu codieren.** **Soll aber der Knoten außer Power auch Kommandos empfangen oder senden können, oder am Tubing teilnehmen, so müssen die `command` bezogenen Funktionen zusätzlich beachtet werden.** ### Alternative API Sollen die Knoten-eigenen `after_...` Funktionen nicht überschrieben werden, so bietet sich folgende, alternative API an: ```lua techage.power.enrich_node(names, pwr_def) techage.power.after_place_node(pos) techage.power.after_dig_node(pos, oldnode) techage.power.after_tube_update2(node, pos, out_dir, peer_pos, peer_in_dir) ``` ### `power`/`power2` API ```lua techage.power.side_to_dir(param2, side) --> outdir techage.power.side_to_outdir(pos, side) --> outdir techage.power.set_conn_dirs(pos, sides) --> store as meta "power_dirs" techage.get_pos(pos, side) --> new pos techage.power.after_rotate_node(pos, cable) -- update cables techage.power.percent(max_val, curr_val) --> percent value techage.power.formspec_power_bar(max_power, current_power) --> formspec string techage.power.power_cut(pos, dir, cable, cut) -- for switches techage.power.network_changed(pos, mem) -- for each network change from any node techage.power.generator_start(pos, mem, available) -- on start techage.power.generator_update(pos, mem, available) -- on any change of performance techage.power.generator_stop(pos, mem) -- on stop techage.power.generator_alive(pos, mem) -- every 2 s techage.power.consumer_start(pos, mem, cycle_time, needed) techage.power.consumer_stop(pos, mem) techage.power.consumer_alive(pos, mem) techage.power.power_available(pos, mem, needed) -- lamp turn on function techage.power.secondary_start(pos, mem, available, needed) techage.power.secondary_stop(pos, mem) techage.power.secondary_alive(pos, mem, capa_curr, capa_max) techage.power.power_accounting(pos, mem) --> {network data...} (used by info tool) techage.power.get_power(start_pos) --> sum (used by solar cells) techage.power.power_network_available(start_pos) --> bool (used by TES generator) techage.power.mark_nodes(name, start_pos) -- used by debugging tool techage.power.limited_connection_walk(pos, clbk) --> num_nodes (used by terminal) ``` ## Klasse `NodeStates` `NodeStates` abstrahiert die Zustände einer Maschine: ```lua techage.RUNNING = 1 -- in normal operation/turned on techage.BLOCKED = 2 -- a pushing node is blocked due to a full destination inventory techage.STANDBY = 3 -- nothing to do (e.g. no input items), or node (world) not loaded techage.NOPOWER = 4 -- only for power consuming nodes, no operation techage.FAULT = 5 -- any fault state (e.g. wrong source items) techage.STOPPED = 6 -- not operational/turned off ``` Dazu muss eine Instanz von `NodeStates` angelegt werden: ```lua State = techage.NodeStates:new({ node_name_passive = "mymod:name_pas", node_name_active = "mymod:name_act", infotext_name = "MyBlock", cycle_time = 2, standby_ticks = 6, formspec_func = func(self, pos, mem), --> string on_state_change = func(pos, old_state, new_state), can_start = func(pos, mem, state) --> true or false/ has_power = func(pos, mem, state), --> true/false (for consumer) start_node = func(pos, mem, state), stop_node = func(pos, mem, state), }) ``` **Wird `NodeStates` verwendet, muss der Knoten die definierten Zustände unterstützen und sollte die formspec mit dem Button und die callbacks `can_start`, `start_node` und `stop_node` implementieren.** Ein Beispiel dafür ist der Boiler aus `./steam_engine/boiler.lua` ### Methods ```lua node_init(pos, mem, number) -- to be called once stop(pos, mem) start(pos, mem) start_from_timer(pos, mem) -- to be used from node timer functions standby(pos, mem) blocked(pos, mem) nopower(pos, mem) fault(pos, mem, err_string) -- err_string is optional get_state(mem) --> state is_active(mem) start_if_standby(pos) -- used from allow_metadata_inventory functions idle(pos, mem) -- To be called if node is idle keep_running(pos, mem, val, num_items) -- to keep the node in state RUNNING state_button_event(pos, mem, fields) -- called from on_receive_fields get_state_button_image -- see techage.state_button() on_receive_message(pos, topic, payload) -- for command interface on_node_load(pos, not_start_timer) -- LBM actions ``` ### Helper API ```lua techage.state_button(state) --> button layout for formspec techage.get_power_image(pos, mem) --> power symbol for formspec techage.is_operational(mem) -- true if node_timer should be executed techage.needs_power(mem) --> true/false state dependent techage.needs_power2(state) --> true/false state dependent techage.get_state_string(mem) --> "running" NodeStates:node_init(pos, mem, number) ``` ## Wrapper `consumer` Wie auch `power` bietet `consumer` einen Registrierungs-Wrapper, der dem Knoten einige Eigenschaften und Funktionen hinzufügt. ```lua techage.register_consumer("autocrafter", S("Autocrafter"), tiles, { drawtype = "normal", cycle_time = CYCLE_TIME, standby_ticks = STANDBY_TICKS, formspec = formspec, tubing -- anstatt 'techage.register_node' after_place_node = func(pos, placer), -- knotenspezifischer Teil can_dig = fubnc(pos, player), -- knotenspezifischer Teil node_timer = func(pos, elapsed), -- knotenspezifischer Teil on_receive_fields = func(pos, formname, fields, player), -- knotenspez. Teil allow_metadata_inventory_put = allow_metadata_inventory_put, allow_metadata_inventory_move = allow_metadata_inventory_move, allow_metadata_inventory_take = allow_metadata_inventory_take, groups = {choppy=2, cracky=2, crumbly=2}, sounds = default.node_sound_wood_defaults(), num_items = {0,1,2,4}, -- Verarbeitungsleistung in items/cycle power_consumption = {0,4,6,9}, -- Stromverbrauch (optional) }) --> node_name_ta2, node_name_ta3, node_name_ta4 ``` Diese `register_consumer` Funktion deckt alles generische ab, was ein Knoten bzgl. Power, Tubing, Kommandos (Status, on/off), formspec, swap_node(act/pas) benötigt, damit auch node_states, tubelib2. Dabei werden auch bereits definiert: - `push` und `pull` Richtung für das Tubing (links/rechts) - Umschalten des Knotens zwischen aktiv und passiv - `has_power` / `start_node` / `stop_node` / `on_power` / `on_nopower` - Unterstützung Achsenantrieb (TA2) oder Strom (TA3+) - Strom/Achsen von vorne oder hinten (alles andere muss selbst definiert werden) Ein einfaches Beispiele dafür wäre: `pusher.lua` **Es darf in `after_place_node` kein `tubelib2.init_mem(pos)` aufgerufen werden, sonst werden die Definitionen wieder zerstört!!!** ## Modul `networks` Tubelib2 verwaltet nur 1:1 Beziehungen, keine Netzwerke. Dazu wurde `power` entwickelt. Allerdings lässt `power` nicht mehrere unterschiedliche Netzwerke pro Knoten zu, da die Daten nicht unterschiedenen werden können. Deshalb wurde das Modul `networks.lua` entwickelt. Dies wird primär für Liquids benötigt, soll aber später `power` ablösen. `liquid_pipe.lua` ist die erste Implementierung von `networks`. ### Anwendung Um `networks` nutzen zu können muss tubelib2_on_update2 implementiert werden: ```lua tubelib2_on_update2 = function(pos, node, tlib2) networks.update_network(pos, tlib2) end, ``` Zusätzlich muss eine Table pro Netzwerk angelegt werden: ```lua networks = { pipe = { -- network name/type sides = {R = 1}, -- connection sides for pipes ntype = "junc", -- node type }, }, ``` Durch die Abstufung in bspw. `pipe` sind parallel auch andere Netze möglich wie: - `power` für Strom - `solar` für die roten Solarkabel - `steam` usw. Im Speicher wird lediglich die Netzwerk ID gespeichert: ```lua mem.pipe.netID = val mem.solar.netID = val mem.power.netID = val ``` Damit wird der Speicherbedarf pro Knoten drastisch reduziert. Allein schon deshalb macht die Umstellung von `power` auf `networks` Sinn. ### Implementierung Die netID ist wie bei `power` die größte hash-Nummer aller Knoten-Positionen. Diese Nummer muss in jedem Knoten gespeichert werden. Im Gegensatz zu `power` werden die `connections` zu anderen Knoten nicht mehr unter `mem`, sondern nur als eine Zahl in Node `meta` gespeichert. Dabei wird pro Richtungen (dir) nur ein Bit abgespeichert. Die Verbindung zum nächsten Knoten muss über `tlib2:get_connected_node_pos(pos, outdir)` bestimmt werden. Da tubelib2 diese Verbindungen auch schon im Cache speichert, sollte dies völlig ausreichend sein. Besonders dann, wenn für den Betrieb die `networks` Tabellen genutzt werden. Bei jeder Änderung an Netzwerk wird folgendes aufgerufen: ```lua function techage.networks.update_network(pos, tlib2) node_connections(pos, tlib2) local netID = determine_netID(pos, tlib2) Networks[netID] = store_netID(pos, netID, tlib2) end ``` Dabei werden: - die Verbindingsinformationen im Knoten aktualisiert - die netID bestimmt und in allen Knoten gespeichert - die Netzwerk Tabellen generiert (pro Knotentyp `ntype` eine Tabelle) Um bspw. alle Consumer durchzuklappern, kann dann einfach die Tabelle über ``` networks.get_network(pos, Pipe).con ``` abgerufen werden. ### API `networks` setzt auch auf `sides`, wie `command` oder `power` und nutzt dafür aber seine eigenen Funktionen. Bei `power` fliegt das dann irgendwann raus, bei `command` evtl. auch?? ```lua techage.networks.side_to_outdir(pos, side) -- beim after_place_node techage.networks.valid_indir(pos, indir, param2, net_name) -- bei bspw. on_push_item techage.networks.update_network(pos, tlib2) -- from tubelib2_on_update2 techage.networks.get_network(pos, tlib2) techage.networks.get_network_table(pos, tlib2, ntype) -- comfort function techage.networks.connection_walk(pos, tlib2, clbk) -- classic way ``` ## Anhang ### Unschönheiten #### Problem: Verbindungen zu zwei Netzwerken Es ist nicht möglich, einen Knoten in zwei unterschiedlichen Netzwerken (bspw. Strom, Dampf) über `techage.power.register_node()` anzumelden. `power` würde zweimal übereinander die gleichen Knoten-internen Variablen wie `mem.connections` im Knoten anlegen und nutzen. Das geht und muss schief gehen. Aktuell gibt es dafür keine Lösung. ### ToDo - tubelib2.mem beschreiben - Aufteilung in node/meta/mem/cache beschreiben