techage/manuals/api.md

22 KiB

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
-- 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:

local Tube = tubelib2.Tube:new(...) 

Hier die Parameter:

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:

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:

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:

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
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
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
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

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:

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:

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):

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:

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:

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:

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

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.

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

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:

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.

-- after_place_node decorator
after_place_node = function(pos, placer, itemstack, pointed_thing)
    local res = <node>.after_place_node(pos, placer, itemstack, pointed_thing)
    <Tube>:after_place_node(pos)
    return res
end,
-- after_dig_node decorator
after_dig_node = function(pos, oldnode, oldmetadata, digger)
    <Tube>:after_dig_node(pos)
    minetest.after(0.1, tubelib2.del_mem, pos)  -- At latest...
    return <node>.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 <node>.after_tube_update(node, pos, out_dir, peer_pos, peer_in_dir)
end,

Und es erfolgt eine Registrierung bei Tube:

<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:

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

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:

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:

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/<error string>
        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

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

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.

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:

	tubelib2_on_update2 = function(pos, node, tlib2)
		networks.update_network(pos, tlib2)
	end,

Zusätzlich muss eine Table pro Netzwerk angelegt werden:

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:

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:

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??

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