Merge pull request #96 from mt-mods/network-ng

Partial network rewrite
This commit is contained in:
SX 2020-10-31 05:05:01 +02:00 committed by GitHub
commit 8f88ec9e16
25 changed files with 3360 additions and 755 deletions

18
.github/workflows/busted.yml vendored Normal file
View File

@ -0,0 +1,18 @@
name: busted
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: apt
run: sudo apt-get install -y luarocks
- name: busted install
run: luarocks install --local busted
- name: busted run
working-directory: ./technic
run: $HOME/.luarocks/bin/busted

View File

@ -1,5 +1,10 @@
unused_args = false unused_args = false
-- Exclude regression tests / unit tests
exclude_files = {
"**/spec/**",
}
globals = { globals = {
"technic", "technic_cnc", "minetest", "wrench" "technic", "technic_cnc", "minetest", "wrench"
} }

View File

@ -4,7 +4,9 @@ Technic
A mod for [minetest](http://www.minetest.net) A mod for [minetest](http://www.minetest.net)
![integration-test](https://github.com/mt-mods/technic/workflows/integration-test/badge.svg) ![integration-test](https://github.com/mt-mods/technic/workflows/integration-test/badge.svg)
![busted](https://github.com/mt-mods/technic/workflows/busted/badge.svg)
![luacheck](https://github.com/mt-mods/technic/workflows/luacheck/badge.svg) ![luacheck](https://github.com/mt-mods/technic/workflows/luacheck/badge.svg)
[![License](https://img.shields.io/badge/license-LGPLv2.0%2B-purple.svg)](https://www.gnu.org/licenses/old-licenses/lgpl-2.0.en.html) [![License](https://img.shields.io/badge/license-LGPLv2.0%2B-purple.svg)](https://www.gnu.org/licenses/old-licenses/lgpl-2.0.en.html)
[![ContentDB](https://content.minetest.net/packages/mt-mods/technic_plus/shields/downloads/)](https://content.minetest.net/packages/mt-mods/technic_plus/) [![ContentDB](https://content.minetest.net/packages/mt-mods/technic_plus/shields/downloads/)](https://content.minetest.net/packages/mt-mods/technic_plus/)

View File

@ -9,13 +9,17 @@ local function power_connector_compat()
local digtron_technic_run = minetest.registered_nodes["digtron:power_connector"].technic_run local digtron_technic_run = minetest.registered_nodes["digtron:power_connector"].technic_run
minetest.override_item("digtron:power_connector",{ minetest.override_item("digtron:power_connector",{
technic_run = function(pos, node) technic_run = function(pos, node)
local network_id = technic.cables[minetest.hash_node_position(pos)] local network_id = technic.pos2network(pos)
local sw_pos = network_id and minetest.get_position_from_hash(network_id) local sw_pos = network_id and technic.network2sw_pos(network_id)
if sw_pos then sw_pos.y = sw_pos.y + 1 end
local meta = minetest.get_meta(pos) local meta = minetest.get_meta(pos)
meta:set_string("HV_network", sw_pos and minetest.pos_to_string(sw_pos) or "") meta:set_string("HV_network", sw_pos and minetest.pos_to_string(sw_pos) or "")
return digtron_technic_run(pos, node) return digtron_technic_run(pos, node)
end, end,
technic_on_disable = function(pos, node)
local meta = minetest.get_meta(pos)
meta:set_string("HV_network", "")
meta:set_string("HV_EU_input", "")
end,
}) })
end end

View File

@ -14,6 +14,8 @@ technic.digilines = {
} }
} }
dofile(path.."/network.lua")
dofile(path.."/register/init.lua") dofile(path.."/register/init.lua")
-- Tiers -- Tiers

View File

@ -0,0 +1,636 @@
--
-- Power network specific functions and data should live here
--
local S = technic.getter
local switch_max_range = tonumber(minetest.settings:get("technic.switch_max_range") or "256")
local off_delay_seconds = tonumber(minetest.settings:get("technic.switch.off_delay_seconds") or "1800")
technic.active_networks = {}
local networks = {}
technic.networks = networks
local cables = {}
technic.cables = cables
local poshash = minetest.hash_node_position
local hashpos = minetest.get_position_from_hash
function technic.create_network(sw_pos)
local network_id = poshash({x=sw_pos.x,y=sw_pos.y-1,z=sw_pos.z})
technic.build_network(network_id)
return network_id
end
function technic.activate_network(network_id, timeout)
-- timeout is optional ttl for network in seconds, if not specified use default
local network = networks[network_id]
if network then
-- timeout is absolute time in microseconds
network.timeout = minetest.get_us_time() + ((timeout or off_delay_seconds) * 1000 * 1000)
technic.active_networks[network_id] = network
end
end
function technic.sw_pos2tier(pos, use_vm)
-- Get cable tier for switching station or nil if no cable
-- use_vm true to use VoxelManip to load node
local cable_pos = {x=pos.x,y=pos.y-1,z=pos.z}
if use_vm then
technic.get_or_load_node(cable_pos)
end
return technic.get_cable_tier(minetest.get_node(cable_pos).name)
end
-- Destroy network data
function technic.remove_network(network_id)
for pos_hash,cable_net_id in pairs(cables) do
if cable_net_id == network_id then
cables[pos_hash] = nil
end
end
networks[network_id] = nil
technic.active_networks[network_id] = nil
end
-- Remove machine or cable from network
local network_node_arrays = {"PR_nodes","BA_nodes","RE_nodes"}
function technic.remove_network_node(network_id, pos)
local network = networks[network_id]
if not network then return end
-- Clear hash tables, cannot use table.remove
local node_id = poshash(pos)
cables[node_id] = nil
network.all_nodes[node_id] = nil
-- TODO: All following things can be skipped if node is not machine
-- check here if it is or is not cable
-- or add separate function to remove cables and move responsibility to caller
-- Clear indexed arrays, do NOT leave holes
local machine_removed = false
for _,tblname in ipairs(network_node_arrays) do
local tbl = network[tblname]
for i=#tbl,1,-1 do
local mpos = tbl[i]
if mpos.x == pos.x and mpos.y == pos.y and mpos.z == pos.z then
table.remove(tbl, i)
machine_removed = true
break
end
end
end
if machine_removed then
-- Machine can still be in world, just not connected to any network. If so then disable it.
local node = minetest.get_node(pos)
technic.disable_machine(pos, node)
end
end
function technic.sw_pos2network(pos)
return cables[poshash({x=pos.x,y=pos.y-1,z=pos.z})]
end
function technic.sw_pos2network(pos)
return cables[poshash({x=pos.x,y=pos.y-1,z=pos.z})]
end
function technic.pos2network(pos)
return cables[poshash(pos)]
end
function technic.network2pos(network_id)
return hashpos(network_id)
end
function technic.network2sw_pos(network_id)
-- Return switching station position for network.
-- It is not guaranteed that position actually contains switching station.
local sw_pos = hashpos(network_id)
sw_pos.y = sw_pos.y + 1
return sw_pos
end
function technic.network_infotext(network_id, text)
if networks[network_id] == nil then return end
if text then
networks[network_id].infotext = text
else
return networks[network_id].infotext
end
end
local node_timeout = {}
local default_timeout = 2
function technic.set_default_timeout(timeout)
default_timeout = timeout or 2
end
function technic.get_timeout(tier, pos)
if node_timeout[tier] == nil then
-- it is normal that some multi tier nodes always drop here when checking all LV, MV and HV tiers
return 0
end
return node_timeout[tier][poshash(pos)] or 0
end
local function touch_node(tier, pos, timeout)
if node_timeout[tier] == nil then
-- this should get built up during registration
node_timeout[tier] = {}
end
node_timeout[tier][poshash(pos)] = timeout or default_timeout
end
technic.touch_node = touch_node
function technic.disable_machine(pos, node)
local nodedef = minetest.registered_nodes[node.name]
if nodedef and nodedef.technic_disabled_machine_name then
node.name = nodedef.technic_disabled_machine_name
minetest.swap_node(pos, node)
elseif nodedef and nodedef.technic_on_disable then
nodedef.technic_on_disable(pos, node)
end
if nodedef then
local meta = minetest.get_meta(pos)
meta:set_string("infotext", S("%s Has No Network"):format(nodedef.description))
end
local node_id = poshash(pos)
for _,nodes in pairs(node_timeout) do
nodes[node_id] = nil
end
end
--
-- Network overloading (incomplete cheat mitigation)
--
local overload_reset_time = tonumber(minetest.settings:get("technic.overload_reset_time") or "20")
local overloaded_networks = {}
local function overload_network(network_id)
local network = networks[network_id]
if network then
network.supply = 0
network.battery_charge = 0
end
overloaded_networks[network_id] = minetest.get_us_time() + (overload_reset_time * 1000 * 1000)
end
technic.overload_network = overload_network
local function reset_overloaded(network_id)
local remaining = math.max(0, overloaded_networks[network_id] - minetest.get_us_time())
if remaining == 0 then
-- Clear cache, remove overload and restart network
technic.remove_network(network_id)
overloaded_networks[network_id] = nil
end
-- Returns 0 when network reset or remaining time if reset timer has not expired yet
return remaining
end
technic.reset_overloaded = reset_overloaded
local function is_overloaded(network_id)
return overloaded_networks[network_id]
end
technic.is_overloaded = is_overloaded
--
-- Functions to traverse the electrical network
--
-- Add a machine node to the LV/MV/HV network
local function add_network_machine(nodes, pos, network_id, all_nodes, multitier)
local node_id = poshash(pos)
local net_id_old = cables[node_id]
if net_id_old == nil or (multitier and net_id_old ~= network_id and all_nodes[node_id] == nil) then
-- Add machine to network only if it is not already added
table.insert(nodes, pos)
-- FIXME: Machines connecting to multiple networks should have way to store multiple network ids
cables[node_id] = network_id
all_nodes[node_id] = pos
return true
elseif not multitier and net_id_old ~= network_id then
-- Do not allow running from multiple networks, trigger overload
overload_network(network_id)
overload_network(net_id_old)
local meta = minetest.get_meta(pos)
meta:set_string("infotext",S("Network Overloaded"))
end
end
-- Add a wire node to the LV/MV/HV network
local function add_cable_node(nodes, pos, network_id, queue)
local node_id = poshash(pos)
if not cables[node_id] then
cables[node_id] = network_id
nodes[node_id] = pos
table.insert(queue, pos)
end
end
-- Generic function to add found connected nodes to the right classification array
local function add_network_node(PR_nodes, RE_nodes, BA_nodes, all_nodes, pos, machines, tier, network_id, queue)
technic.get_or_load_node(pos)
local name = minetest.get_node(pos).name
if technic.is_tier_cable(name, tier) then
add_cable_node(all_nodes, pos, network_id, queue)
elseif machines[name] then
if machines[name] == technic.producer then
add_network_machine(PR_nodes, pos, network_id, all_nodes)
elseif machines[name] == technic.receiver then
add_network_machine(RE_nodes, pos, network_id, all_nodes)
elseif machines[name] == technic.producer_receiver then
if add_network_machine(PR_nodes, pos, network_id, all_nodes, true) then
table.insert(RE_nodes, pos)
end
elseif machines[name] == technic.battery then
add_network_machine(BA_nodes, pos, network_id, all_nodes)
end
end
end
-- Generic function to add single nodes to the right classification array of existing network
function technic.add_network_node(pos, network)
add_network_node(
network.PR_nodes,
network.RE_nodes,
network.BA_nodes,
network.all_nodes,
pos,
technic.machines[network.tier],
network.tier,
network.id,
{}
)
end
-- Traverse a network given a list of machines and a cable type name
local function traverse_network(PR_nodes, RE_nodes, BA_nodes, all_nodes, pos, machines, tier, network_id, queue)
local positions = {
{x=pos.x+1, y=pos.y, z=pos.z},
{x=pos.x-1, y=pos.y, z=pos.z},
{x=pos.x, y=pos.y+1, z=pos.z},
{x=pos.x, y=pos.y-1, z=pos.z},
{x=pos.x, y=pos.y, z=pos.z+1},
{x=pos.x, y=pos.y, z=pos.z-1}}
for i, cur_pos in pairs(positions) do
if not all_nodes[poshash(cur_pos)] then
add_network_node(PR_nodes, RE_nodes, BA_nodes, all_nodes, cur_pos, machines, tier, network_id, queue)
end
end
end
local function touch_nodes(list, tier)
for _, pos in ipairs(list) do
touch_node(tier, pos) -- Touch node
end
end
local function get_network(network_id, tier)
local cached = networks[network_id]
if cached and cached.tier == tier then
touch_nodes(cached.PR_nodes, tier)
touch_nodes(cached.BA_nodes, tier)
touch_nodes(cached.RE_nodes, tier)
return cached.PR_nodes, cached.BA_nodes, cached.RE_nodes
end
return technic.build_network(network_id)
end
function technic.add_network_branch(queue, network)
-- Adds whole branch to network, queue positions can be used to bypass sub branches
local PR_nodes = network.PR_nodes -- Indexed array
local BA_nodes = network.BA_nodes -- Indexed array
local RE_nodes = network.RE_nodes -- Indexed array
local all_nodes = network.all_nodes -- Hash table
local network_id = network.id
local tier = network.tier
local machines = technic.machines[tier]
local sw_pos = technic.network2sw_pos(network_id)
--print(string.format("technic.add_network_branch(%s, %s, %.17g)",queue,minetest.pos_to_string(sw_pos),network.id))
while next(queue) do
local to_visit = {}
for _, pos in ipairs(queue) do
if vector.distance(pos, sw_pos) > switch_max_range then
-- max range exceeded
return
end
traverse_network(PR_nodes, RE_nodes, BA_nodes, all_nodes, pos,
machines, tier, network_id, to_visit)
end
queue = to_visit
end
end
function technic.build_network(network_id)
technic.remove_network(network_id)
local sw_pos = technic.network2sw_pos(network_id)
local tier = technic.sw_pos2tier(sw_pos)
if not tier then
return
end
local network = {
-- Basic network data and lookup table for attached nodes (no switching stations)
id = network_id, tier = tier, all_nodes = {},
-- Indexed arrays for iteration by machine type
PR_nodes = {}, RE_nodes = {}, BA_nodes = {},
-- Power generation, usage and capacity related variables
supply = 0, demand = 0, battery_charge = 0, battery_charge_max = 0,
-- Network activation and excution control
timeout = 0, skip = 0,
}
-- Add first cable (one that is holding network id) and build network
local queue = {}
add_cable_node(network.all_nodes, technic.network2pos(network_id), network_id, queue)
technic.add_network_branch(queue, network)
network.battery_count = #network.BA_nodes
-- Add newly built network to cache array
networks[network_id] = network
-- And return producers, batteries and receivers (should this simply return network?)
return network.PR_nodes, network.BA_nodes, network.RE_nodes
end
--
-- Execute technic power network
--
local function run_nodes(list, run_stage)
for _, pos in ipairs(list) do
technic.get_or_load_node(pos)
local node = minetest.get_node_or_nil(pos)
if node and node.name then
local nodedef = minetest.registered_nodes[node.name]
if nodedef and nodedef.technic_run then
nodedef.technic_run(pos, node, run_stage)
end
end
end
end
local mesecons_path = minetest.get_modpath("mesecons")
local digilines_path = minetest.get_modpath("digilines")
function technic.network_run(network_id)
--
-- !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!!
-- TODO: This function requires a lot of cleanup
-- It is moved here from switching_station.lua and still
-- contain a lot of switching station specific stuff which
-- should be removed and/or refactored.
--
if not technic.powerctrl_state then return end
-- Check if network is overloaded / conflicts with another network
if technic.is_overloaded(network_id) then
-- TODO: Overload check should happen before technic.network_run is called
return
end
local pos = technic.network2sw_pos(network_id)
local t0 = minetest.get_us_time()
local PR_nodes
local BA_nodes
local RE_nodes
local tier = technic.sw_pos2tier(pos)
local network
if tier then
PR_nodes, BA_nodes, RE_nodes = get_network(network_id, tier)
if technic.is_overloaded(network_id) then return end
network = networks[network_id]
else
--dprint("Not connected to a network")
technic.network_infotext(network_id, S("%s Has No Network"):format(S("Switching Station")))
return
end
run_nodes(PR_nodes, technic.producer)
run_nodes(RE_nodes, technic.receiver)
run_nodes(BA_nodes, technic.battery)
-- Strings for the meta data
local eu_demand_str = tier.."_EU_demand"
local eu_input_str = tier.."_EU_input"
local eu_supply_str = tier.."_EU_supply"
-- Distribute charge equally across multiple batteries.
local charge_total = 0
local battery_count = 0
local BA_charge = 0
local BA_charge_max = 0
for n, pos1 in pairs(BA_nodes) do
local meta1 = minetest.get_meta(pos1)
local charge = meta1:get_int("internal_EU_charge")
local charge_max = meta1:get_int("internal_EU_charge_max")
BA_charge = BA_charge + charge
BA_charge_max = BA_charge_max + charge_max
if (meta1:get_int(eu_demand_str) ~= 0) then
charge_total = charge_total + charge
battery_count = battery_count + 1
end
end
local charge_distributed = math.floor(charge_total / battery_count)
for n, pos1 in pairs(BA_nodes) do
local meta1 = minetest.get_meta(pos1)
if (meta1:get_int(eu_demand_str) ~= 0) then
meta1:set_int("internal_EU_charge", charge_distributed)
end
end
-- Get all the power from the PR nodes
local PR_eu_supply = 0 -- Total power
for _, pos1 in pairs(PR_nodes) do
local meta1 = minetest.get_meta(pos1)
PR_eu_supply = PR_eu_supply + meta1:get_int(eu_supply_str)
end
--dprint("Total PR supply:"..PR_eu_supply)
-- Get all the demand from the RE nodes
local RE_eu_demand = 0
for _, pos1 in pairs(RE_nodes) do
local meta1 = minetest.get_meta(pos1)
RE_eu_demand = RE_eu_demand + meta1:get_int(eu_demand_str)
end
--dprint("Total RE demand:"..RE_eu_demand)
-- Get all the power from the BA nodes
local BA_eu_supply = 0
for _, pos1 in pairs(BA_nodes) do
local meta1 = minetest.get_meta(pos1)
BA_eu_supply = BA_eu_supply + meta1:get_int(eu_supply_str)
end
--dprint("Total BA supply:"..BA_eu_supply)
-- Get all the demand from the BA nodes
local BA_eu_demand = 0
for _, pos1 in pairs(BA_nodes) do
local meta1 = minetest.get_meta(pos1)
BA_eu_demand = BA_eu_demand + meta1:get_int(eu_demand_str)
end
--dprint("Total BA demand:"..BA_eu_demand)
technic.network_infotext(network_id, S("@1. Supply: @2 Demand: @3",
S("Switching Station"), technic.EU_string(PR_eu_supply),
technic.EU_string(RE_eu_demand)))
-- If mesecon signal and power supply or demand changed then
-- send them via digilines.
if mesecons_path and digilines_path and mesecon.is_powered(pos) then
if PR_eu_supply ~= network.supply or
RE_eu_demand ~= network.demand then
local meta = minetest.get_meta(pos)
local channel = meta:get_string("channel")
digilines.receptor_send(pos, technic.digilines.rules, channel, {
supply = PR_eu_supply,
demand = RE_eu_demand
})
end
end
-- Data that will be used by the power monitor
network.supply = PR_eu_supply
network.demand = RE_eu_demand
network.battery_count = #BA_nodes
network.battery_charge = BA_charge
network.battery_charge_max = BA_charge_max
-- If the PR supply is enough for the RE demand supply them all
if PR_eu_supply >= RE_eu_demand then
--dprint("PR_eu_supply"..PR_eu_supply.." >= RE_eu_demand"..RE_eu_demand)
for _, pos1 in pairs(RE_nodes) do
local meta1 = minetest.get_meta(pos1)
local eu_demand = meta1:get_int(eu_demand_str)
meta1:set_int(eu_input_str, eu_demand)
end
-- We have a surplus, so distribute the rest equally to the BA nodes
-- Let's calculate the factor of the demand
PR_eu_supply = PR_eu_supply - RE_eu_demand
local charge_factor = 0 -- Assume all batteries fully charged
if BA_eu_demand > 0 then
charge_factor = PR_eu_supply / BA_eu_demand
end
for n, pos1 in pairs(BA_nodes) do
local meta1 = minetest.get_meta(pos1)
local eu_demand = meta1:get_int(eu_demand_str)
meta1:set_int(eu_input_str, math.floor(eu_demand * charge_factor))
--dprint("Charging battery:"..math.floor(eu_demand*charge_factor))
end
local t1 = minetest.get_us_time()
local diff = t1 - t0
if diff > 50000 then
minetest.log("warning", "[technic] [+supply] technic_run took " .. diff .. " us at " .. minetest.pos_to_string(pos))
end
return
end
-- If the PR supply is not enough for the RE demand we will discharge the batteries too
if PR_eu_supply + BA_eu_supply >= RE_eu_demand then
--dprint("PR_eu_supply "..PR_eu_supply.."+BA_eu_supply "..BA_eu_supply.." >= RE_eu_demand"..RE_eu_demand)
for _, pos1 in pairs(RE_nodes) do
local meta1 = minetest.get_meta(pos1)
local eu_demand = meta1:get_int(eu_demand_str)
meta1:set_int(eu_input_str, eu_demand)
end
-- We have a deficit, so distribute to the BA nodes
-- Let's calculate the factor of the supply
local charge_factor = 0 -- Assume all batteries depleted
if BA_eu_supply > 0 then
charge_factor = (PR_eu_supply - RE_eu_demand) / BA_eu_supply
end
for n,pos1 in pairs(BA_nodes) do
local meta1 = minetest.get_meta(pos1)
local eu_supply = meta1:get_int(eu_supply_str)
meta1:set_int(eu_input_str, math.floor(eu_supply * charge_factor))
--dprint("Discharging battery:"..math.floor(eu_supply*charge_factor))
end
local t1 = minetest.get_us_time()
local diff = t1 - t0
if diff > 50000 then
minetest.log("warning", "[technic] [-supply] technic_run took " .. diff .. " us at " .. minetest.pos_to_string(pos))
end
return
end
-- If the PR+BA supply is not enough for the RE demand: Power only the batteries
local charge_factor = 0 -- Assume all batteries fully charged
if BA_eu_demand > 0 then
charge_factor = PR_eu_supply / BA_eu_demand
end
for n, pos1 in pairs(BA_nodes) do
local meta1 = minetest.get_meta(pos1)
local eu_demand = meta1:get_int(eu_demand_str)
meta1:set_int(eu_input_str, math.floor(eu_demand * charge_factor))
end
for n, pos1 in pairs(RE_nodes) do
local meta1 = minetest.get_meta(pos1)
meta1:set_int(eu_input_str, 0)
end
local t1 = minetest.get_us_time()
local diff = t1 - t0
if diff > 50000 then
minetest.log("warning", "[technic] technic_run took " .. diff .. " us at " .. minetest.pos_to_string(pos))
end
end
--
-- Technic power network administrative functions
--
technic.powerctrl_state = true
minetest.register_chatcommand("powerctrl", {
params = "state",
description = "Enables or disables technic's switching station ABM",
privs = { basic_privs = true },
func = function(name, state)
if state == "on" then
technic.powerctrl_state = true
else
technic.powerctrl_state = false
end
end
})
--
-- Metadata cleanup LBM, removes old metadata values from nodes
--
--luacheck: ignore 511
if false then
minetest.register_lbm({
name = "technic:metadata-cleanup",
nodenames = {
"group:technic_machine",
"group:technic_all_tiers",
"technic:switching_station",
"technic:power_monitor",
},
action = function(pos, node)
-- Delete all listed metadata key/value pairs from technic machines
local keys = {
"LV_EU_timeout", "MV_EU_timeout", "HV_EU_timeout",
"LV_network", "MV_network", "HV_network",
"active_pos", "supply", "demand",
"battery_count", "battery_charge", "battery_charge_max",
}
local meta = minetest.get_meta(pos)
for _,key in ipairs(keys) do
-- Value of `""` will delete the key.
meta:set_string(key, "")
end
if node.name == "technic:switching_station" then
meta:set_string("active", "")
end
end,
})
end

View File

@ -13,6 +13,7 @@ local function get_cable(pos)
end end
-- return the position of connected cable or nil -- return the position of connected cable or nil
-- TODO: Make it support every possible orientation
local function get_connected_cable_network(pos) local function get_connected_cable_network(pos)
local param2 = minetest.get_node(pos).param2 local param2 = minetest.get_node(pos).param2
-- should probably also work sideways or upside down but for now it wont -- should probably also work sideways or upside down but for now it wont
@ -26,18 +27,16 @@ local function get_connected_cable_network(pos)
-- Behind? -- Behind?
checkpos = vector.add(minetest.facedir_to_dir(param2),pos) checkpos = vector.add(minetest.facedir_to_dir(param2),pos)
network_id = get_cable(checkpos) and technic.pos2network(checkpos) network_id = get_cable(checkpos) and technic.pos2network(checkpos)
if network_id then
return network_id return network_id
end
end end
-- return the position of the associated switching station or nil -- return the position of the associated switching station or nil
local function get_swpos(pos) local function get_network(pos)
local network_id = get_connected_cable_network(pos) local network_id = get_connected_cable_network(pos)
local network = network_id and technic.networks[network_id] local network = network_id and technic.networks[network_id]
local swpos = network and technic.network2sw_pos(network_id) local swpos = network and technic.network2sw_pos(network_id)
local is_powermonitor = swpos and minetest.get_node(swpos).name == "technic:switching_station" local is_powermonitor = swpos and minetest.get_node(swpos).name == "technic:switching_station"
return (is_powermonitor and network.all_nodes[network_id]) and swpos return (is_powermonitor and network.all_nodes[network_id]) and network
end end
minetest.register_craft({ minetest.register_craft({
@ -60,7 +59,7 @@ minetest.register_node("technic:power_monitor",{
"technic_power_monitor_front.png" "technic_power_monitor_front.png"
}, },
paramtype2 = "facedir", paramtype2 = "facedir",
groups = {snappy=2, choppy=2, oddly_breakable_by_hand=2, technic_all_tiers=1, technic_machine=1}, groups = {snappy=2, choppy=2, oddly_breakable_by_hand=2, technic_all_tiers=1},
connect_sides = {"bottom", "back"}, connect_sides = {"bottom", "back"},
sounds = default.node_sound_wood_defaults(), sounds = default.node_sound_wood_defaults(),
on_construct = function(pos) on_construct = function(pos)
@ -97,19 +96,16 @@ minetest.register_node("technic:power_monitor",{
return return
end end
local sw_pos = get_swpos(pos) local network = get_network(pos)
if not sw_pos then if not network then return end
return
end
local sw_meta = minetest.get_meta(sw_pos)
digilines.receptor_send(pos, technic.digilines.rules, channel, { digilines.receptor_send(pos, technic.digilines.rules, channel, {
supply = sw_meta:get_int("supply"), supply = network.supply,
demand = sw_meta:get_int("demand"), demand = network.demand,
lag = sw_meta:get_int("lag"), lag = network.lag,
battery_count = sw_meta:get_int("battery_count"), battery_count = network.battery_count,
battery_charge = sw_meta:get_int("battery_charge"), battery_charge = network.battery_charge,
battery_charge_max = sw_meta:get_int("battery_charge_max"), battery_charge_max = network.battery_charge_max,
}) })
end end
}, },
@ -123,14 +119,10 @@ minetest.register_abm({
chance = 1, chance = 1,
action = function(pos, node, active_object_count, active_object_count_wider) action = function(pos, node, active_object_count, active_object_count_wider)
local meta = minetest.get_meta(pos) local meta = minetest.get_meta(pos)
local sw_pos = get_swpos(pos) local network = get_network(pos)
if sw_pos then if network then
local sw_meta = minetest.get_meta(sw_pos) meta:set_string("infotext", S("Power Monitor. Supply: @1 Demand: @2",
local supply = sw_meta:get_int("supply") technic.EU_string(network.supply), technic.EU_string(network.demand)))
local demand = sw_meta:get_int("demand")
meta:set_string("infotext",
S("Power Monitor. Supply: @1 Demand: @2",
technic.EU_string(supply), technic.EU_string(demand)))
else else
meta:set_string("infotext",S("Power Monitor Has No Network")) meta:set_string("infotext",S("Power Monitor Has No Network"))
end end

View File

@ -11,110 +11,150 @@ function technic.get_cable_tier(name)
return cable_tier[name] return cable_tier[name]
end end
local function check_connections(pos) local function match_cable_tier_filter(name, tiers)
-- Build a table of all machines -- Helper to check for set of cable tiers
if tiers then
for _, tier in ipairs(tiers) do if cable_tier[name] == tier then return true end end
return false
end
return cable_tier[name] ~= nil
end
local function get_neighbors(pos, tiers)
-- TODO: Move this to network.lua
local tier_machines = tiers and technic.machines[tiers[1]]
local is_cable = match_cable_tier_filter(minetest.get_node(pos).name, tiers)
local network = is_cable and technic.networks[technic.pos2network(pos)]
local cables = {}
local machines = {} local machines = {}
for tier,list in pairs(technic.machines) do
for k,v in pairs(list) do
machines[k] = v
end
end
local connections = {}
local positions = { local positions = {
{x=pos.x+1, y=pos.y, z=pos.z}, {x=pos.x+1, y=pos.y, z=pos.z},
{x=pos.x-1, y=pos.y, z=pos.z}, {x=pos.x-1, y=pos.y, z=pos.z},
{x=pos.x, y=pos.y+1, z=pos.z}, {x=pos.x, y=pos.y+1, z=pos.z},
{x=pos.x, y=pos.y-1, z=pos.z}, {x=pos.x, y=pos.y-1, z=pos.z},
{x=pos.x, y=pos.y, z=pos.z+1}, {x=pos.x, y=pos.y, z=pos.z+1},
{x=pos.x, y=pos.y, z=pos.z-1}} {x=pos.x, y=pos.y, z=pos.z-1},
for _,connected_pos in pairs(positions) do }
for _,connected_pos in ipairs(positions) do
local name = minetest.get_node(connected_pos).name local name = minetest.get_node(connected_pos).name
if machines[name] or technic.get_cable_tier(name) then if tier_machines and tier_machines[name] then
table.insert(connections,connected_pos) table.insert(machines, connected_pos)
elseif match_cable_tier_filter(name, tiers) then
local cable_network = technic.networks[technic.pos2network(connected_pos)]
table.insert(cables,{
pos = connected_pos,
network = cable_network,
})
if not network then network = cable_network end
end end
end end
return connections return network, cables, machines
end end
local function clear_networks(pos) local function place_network_node(pos, tiers, name)
local node = minetest.get_node(pos) -- Get connections and primary network if there's any
local placed = node.name ~= "air" local network, cables, machines = get_neighbors(pos, tiers)
local positions = check_connections(pos) if not network then
if #positions < 1 then return end
local dead_end = #positions == 1
for _,connected_pos in pairs(positions) do
local net = technic.cables[minetest.hash_node_position(connected_pos)]
if net and technic.networks[net] then
if dead_end and placed then
-- Dead end placed, add it to the network
-- Get the network
local network_id = technic.cables[minetest.hash_node_position(positions[1])]
if not network_id then
-- We're evidently not on a network, nothing to add ourselves to -- We're evidently not on a network, nothing to add ourselves to
return return
end end
local sw_pos = minetest.get_position_from_hash(network_id)
sw_pos.y = sw_pos.y + 1
local network = technic.networks[network_id]
local tier = network.tier
-- Actually add it to the (cached) network -- Attach to primary network, this must be done before building branches from this position
-- This is similar to check_node_subp technic.add_network_node(pos, network)
local pos_hash = minetest.hash_node_position(pos) if not match_cable_tier_filter(name, tiers) then
technic.cables[pos_hash] = network_id if technic.machines[tiers[1]][name] == technic.producer_receiver then
pos.visited = 1 -- FIXME: Multi tier machine like supply converter should also attach to other networks around pos.
if technic.is_tier_cable(name, tier) then -- Preferably also with connection rules defined for machine.
network.all_nodes[pos_hash] = pos -- nodedef.connect_sides could be used to generate these rules.
elseif technic.machines[tier][node.name] then -- For now, assume that all multi network machines belong to technic.producer_receiver group:
if technic.machines[tier][node.name] == technic.producer then -- Get cables and networks around PR_RE machine
table.insert(network.PR_nodes,pos) local _, machine_cables, _ = get_neighbors(pos)
elseif technic.machines[tier][node.name] == technic.receiver then for _,connection in ipairs(machine_cables) do
table.insert(network.RE_nodes,pos) if connection.network and connection.network.id ~= network.id then
elseif technic.machines[tier][node.name] == technic.producer_receiver then -- Attach PR_RE machine to secondary networks (last added is primary until above note is resolved)
table.insert(network.PR_nodes,pos) technic.add_network_node(pos, connection.network)
table.insert(network.RE_nodes,pos)
elseif technic.machines[tier][node.name] == "SPECIAL" and
(pos.x ~= sw_pos.x or pos.y ~= sw_pos.y or pos.z ~= sw_pos.z) and
from_below then
table.insert(network.SP_nodes,pos)
elseif technic.machines[tier][node.name] == technic.battery then
table.insert(network.BA_nodes,pos)
end end
end end
elseif dead_end and not placed then else
-- Dead end removed, remove it from the network -- Check connected cables for foreign networks, overload if machine was connected to multiple networks
-- Get the network for _, connection in ipairs(cables) do
local network_id = technic.cables[minetest.hash_node_position(positions[1])] if connection.network and connection.network.id ~= network.id then
if not network_id then technic.overload_network(connection.network.id)
-- We're evidently not on a network, nothing to add ourselves to technic.overload_network(network.id)
end
end
end
-- Machine added, skip all network building
return return
end end
local network = technic.networks[network_id]
-- Search for and remove machine -- Attach neighbor machines if cable was added
technic.cables[minetest.hash_node_position(pos)] = nil for _,machine_pos in ipairs(machines) do
for tblname,table in pairs(network) do technic.add_network_node(machine_pos, network)
if tblname ~= "tier" then
for machinenum,machine in pairs(table) do
if machine.x == pos.x
and machine.y == pos.y
and machine.z == pos.z then
table[machinenum] = nil
end end
-- Attach neighbor cables
for _,connection in ipairs(cables) do
if connection.network then
if connection.network.id ~= network.id then
-- Remove network if position belongs to another network
-- FIXME: Network requires partial rebuild but avoid doing it here if possible.
-- This might cause problems when merging two active networks into one
technic.remove_network(network.id)
technic.remove_network(connection.network.id)
connection.network = nil
end
else
-- There's cable that does not belong to any network, attach whole branch
technic.add_network_node(connection.pos, network)
technic.add_network_branch({connection.pos}, network)
end
end
end
-- NOTE: Exported for tests but should probably be moved to network.lua
technic.network_node_on_placenode = place_network_node
local function remove_network_node(pos, tiers, name)
-- Get the network and neighbors
local network, cables, machines = get_neighbors(pos, tiers)
if not network then return end
if not match_cable_tier_filter(name, tiers) then
-- Machine removed, skip cable checks to prevent unnecessary network cleanups
for _,connection in ipairs(cables) do
if connection.network then
-- Remove machine from all networks around it
technic.remove_network_node(connection.network.id, pos)
end
end
return
end
if #cables == 1 then
-- Dead end cable removed, remove it from the network
technic.remove_network_node(network.id, pos)
-- Remove neighbor machines from network if cable was removed
if match_cable_tier_filter(name, tiers) then
for _,machine_pos in ipairs(machines) do
local net, _, _ = get_neighbors(machine_pos, tiers)
if not net then
-- Remove machine from network if it does not have other connected cables
technic.remove_network_node(network.id, machine_pos)
end end
end end
end end
else else
-- Not a dead end, so the whole network needs to be recalculated -- TODO: Check branches around and switching stations for branches:
for _,v in pairs(technic.networks[net].all_nodes) do -- remove branches that do not have switching station. Switching stations not tracked but could be easily tracked.
local pos1 = minetest.hash_node_position(v) -- remove branches not connected to another branch. Individual branches not tracked, requires simple AI heuristics.
technic.cables[pos1] = nil -- move branches that have switching station to new networks without checking or loading actual nodes in world.
end -- To do all this network must be aware of individual branches and switching stations, might not be worth it...
technic.networks[net] = nil -- For now remove whole network and let ABM rebuild it
end technic.remove_network(network.id)
end
end end
end end
-- NOTE: Exported for tests but should probably be moved to network.lua
technic.network_node_on_dignode = remove_network_node
local function item_place_override_node(itemstack, placer, pointed, node) local function item_place_override_node(itemstack, placer, pointed, node)
-- Call the default on_place function with a fake itemstack -- Call the default on_place function with a fake itemstack
@ -142,7 +182,8 @@ function technic.register_cable(tier, size, description, prefix, override_cable,
prefix = prefix or "" prefix = prefix or ""
override_cable_plate = override_cable_plate or override_cable override_cable_plate = override_cable_plate or override_cable
local ltier = string.lower(tier) local ltier = string.lower(tier)
cable_tier["technic:"..ltier..prefix.."_cable"] = tier local node_name = "technic:"..ltier..prefix.."_cable"
cable_tier[node_name] = tier
local groups = {snappy=2, choppy=2, oddly_breakable_by_hand=2, local groups = {snappy=2, choppy=2, oddly_breakable_by_hand=2,
["technic_"..ltier.."_cable"] = 1} ["technic_"..ltier.."_cable"] = 1}
@ -158,22 +199,22 @@ function technic.register_cable(tier, size, description, prefix, override_cable,
connect_right = {-size, -size, -size, 0.5, size, size}, -- x+ connect_right = {-size, -size, -size, 0.5, size, size}, -- x+
} }
minetest.register_node("technic:"..ltier..prefix.."_cable", override_table({ minetest.register_node(node_name, override_table({
description = S("%s Cable"):format(tier), description = S("%s Cable"):format(tier),
tiles = {"technic_"..ltier..prefix.."_cable.png"}, tiles = {"technic_"..ltier..prefix.."_cable.png"},
inventory_image = "technic_"..ltier..prefix.."_cable_wield.png", inventory_image = "technic_"..ltier..prefix.."_cable_wield.png",
wield_image = "technic_"..ltier..prefix.."_cable_wield.png", wield_image = "technic_"..ltier..prefix.."_cable_wield.png",
groups = groups, groups = groups,
sounds = default.node_sound_wood_defaults(), sounds = default.node_sound_wood_defaults(),
drop = "technic:"..ltier..prefix.."_cable", drop = node_name,
paramtype = "light", paramtype = "light",
sunlight_propagates = true, sunlight_propagates = true,
drawtype = "nodebox", drawtype = "nodebox",
node_box = node_box, node_box = node_box,
connects_to = {"group:technic_"..ltier.."_cable", connects_to = {"group:technic_"..ltier.."_cable",
"group:technic_"..ltier, "group:technic_all_tiers"}, "group:technic_"..ltier, "group:technic_all_tiers"},
on_construct = clear_networks, on_construct = function(pos) place_network_node(pos, {tier}, node_name) end,
on_destruct = clear_networks, on_destruct = function(pos) remove_network_node(pos, {tier}, node_name) end,
}, override_cable)) }, override_cable))
local xyz = { local xyz = {
@ -199,21 +240,22 @@ function technic.register_cable(tier, size, description, prefix, override_cable,
return "-"..p return "-"..p
end end
end end
-- TODO: Does this really need 6 different nodes? Use single node for cable plate if possible.
for p, i in pairs(xyz) do for p, i in pairs(xyz) do
local def = { local def = {
description = S("%s Cable Plate"):format(tier), description = S("%s Cable Plate"):format(tier),
tiles = {"technic_"..ltier..prefix.."_cable.png"}, tiles = {"technic_"..ltier..prefix.."_cable.png"},
groups = table.copy(groups), groups = table.copy(groups),
sounds = default.node_sound_wood_defaults(), sounds = default.node_sound_wood_defaults(),
drop = "technic:"..ltier..prefix.."_cable_plate_1", drop = node_name .. "_plate_1",
paramtype = "light", paramtype = "light",
sunlight_propagates = true, sunlight_propagates = true,
drawtype = "nodebox", drawtype = "nodebox",
node_box = table.copy(node_box), node_box = table.copy(node_box),
connects_to = {"group:technic_"..ltier.."_cable", connects_to = {"group:technic_"..ltier.."_cable",
"group:technic_"..ltier, "group:technic_all_tiers"}, "group:technic_"..ltier, "group:technic_all_tiers"},
on_construct = clear_networks, on_construct = function(pos) place_network_node(pos, {tier}, node_name.."_plate_"..i) end,
on_destruct = clear_networks, on_destruct = function(pos) remove_network_node(pos, {tier}, node_name.."_plate_"..i) end,
} }
def.node_box.fixed = { def.node_box.fixed = {
{-size, -size, -size, size, size, size}, {-size, -size, -size, size, size, size},
@ -254,7 +296,7 @@ function technic.register_cable(tier, size, description, prefix, override_cable,
if num == nil then num = 1 end if num == nil then num = 1 end
return item_place_override_node( return item_place_override_node(
itemstack, placer, pointed_thing, itemstack, placer, pointed_thing,
{name = "technic:"..ltier..prefix.."_cable_plate_"..num} {name = node_name.."_plate_"..num}
) )
end end
else else
@ -271,39 +313,43 @@ function technic.register_cable(tier, size, description, prefix, override_cable,
num = num + dir num = num + dir
num = (num >= 1 and num) or num + 6 num = (num >= 1 and num) or num + 6
num = (num <= 6 and num) or num - 6 num = (num <= 6 and num) or num - 6
minetest.swap_node(pos, {name = "technic:"..ltier..prefix.."_cable_plate_"..num}) minetest.swap_node(pos, {name = node_name.."_plate_"..num})
end end
minetest.register_node("technic:"..ltier..prefix.."_cable_plate_"..i, override_table(def, override_cable_plate)) minetest.register_node(node_name.."_plate_"..i, override_table(def, override_cable_plate))
cable_tier["technic:"..ltier..prefix.."_cable_plate_"..i] = tier cable_tier[node_name.."_plate_"..i] = tier
end end
local c = "technic:"..ltier..prefix.."_cable"
minetest.register_craft({ minetest.register_craft({
output = "technic:"..ltier..prefix.."_cable_plate_1 5", output = node_name.."_plate_1 5",
recipe = { recipe = {
{"", "", c}, {"" , "" , node_name},
{c , c , c}, {node_name, node_name, node_name},
{"", "", c}, {"" , "" , node_name},
} }
}) })
minetest.register_craft({ minetest.register_craft({
output = c, output = node_name,
recipe = { recipe = {
{"technic:"..ltier..prefix.."_cable_plate_1"}, {node_name.."_plate_1"},
} }
}) })
end end
minetest.register_on_mods_loaded(function()
local function clear_nets_if_machine(pos, node) -- FIXME: Move this to register.lua or somewhere else where register_on_mods_loaded is not required.
for tier, machine_list in pairs(technic.machines) do -- Possible better option would be to inject these when machine is registered in register.lua.
if machine_list[node.name] ~= nil then for name, tiers in pairs(technic.machine_tiers) do
return clear_networks(pos) local nodedef = minetest.registered_nodes[name]
local on_construct = type(nodedef.on_construct) == "function" and nodedef.on_construct
local on_destruct = type(nodedef.on_destruct) == "function" and nodedef.on_destruct
minetest.override_item(name,{
on_construct = on_construct
and function(pos) on_construct(pos) place_network_node(pos, tiers, name) end
or function(pos) place_network_node(pos, tiers, name) end,
on_destruct = on_destruct
and function(pos) on_destruct(pos) remove_network_node(pos, tiers, name) end
or function(pos) remove_network_node(pos, tiers, name) end,
})
end end
end end)
end
minetest.register_on_placenode(clear_nets_if_machine)
minetest.register_on_dignode(clear_nets_if_machine)

View File

@ -143,6 +143,10 @@ local run = function(pos, node, run_stage)
if from and to then if from and to then
local input = meta:get_int(from.."_EU_input") local input = meta:get_int(from.."_EU_input")
if (technic.get_timeout(from, pos) <= 0) or (technic.get_timeout(to, pos) <= 0) then
-- Supply converter timed out, either RE or PR network is not running anymore
input = 0
end
meta:set_int(from.."_EU_demand", demand) meta:set_int(from.."_EU_demand", demand)
meta:set_int(from.."_EU_supply", 0) meta:set_int(from.."_EU_supply", 0)
meta:set_int(to.."_EU_demand", 0) meta:set_int(to.."_EU_demand", 0)

View File

@ -1,29 +1,6 @@
-- See also technic/doc/api.md -- See also technic/doc/api.md
technic.networks = {}
technic.cables = {}
technic.redundant_warn = {}
local overload_reset_time = tonumber(minetest.settings:get("technic.overload_reset_time") or "20")
local overloaded_networks = {}
local function overload_network(network_id)
overloaded_networks[network_id] = minetest.get_us_time() + (overload_reset_time * 1000 * 1000)
end
local function reset_overloaded(network_id)
local remaining = math.max(0, overloaded_networks[network_id] - minetest.get_us_time())
if remaining == 0 then
-- Clear cache, remove overload and restart network
technic.remove_network(network_id)
overloaded_networks[network_id] = nil
end
-- Returns 0 when network reset or remaining time if reset timer has not expired yet
return remaining
end
local switch_max_range = tonumber(minetest.settings:get("technic.switch_max_range") or "256")
local mesecons_path = minetest.get_modpath("mesecons") local mesecons_path = minetest.get_modpath("mesecons")
local digilines_path = minetest.get_modpath("digilines")
local S = technic.getter local S = technic.getter
@ -38,6 +15,17 @@ minetest.register_craft({
} }
}) })
local function start_network(pos)
local tier = technic.sw_pos2tier(pos)
if not tier then
local meta = minetest.get_meta(pos)
meta:set_string("infotext", S("%s Has No Network"):format(S("Switching Station")))
return
end
local network_id = technic.sw_pos2network(pos) or technic.create_network(pos)
technic.activate_network(network_id)
end
local mesecon_def local mesecon_def
if mesecons_path then if mesecons_path then
mesecon_def = {effector = { mesecon_def = {effector = {
@ -60,33 +48,17 @@ minetest.register_node("technic:switching_station",{
on_construct = function(pos) on_construct = function(pos)
local meta = minetest.get_meta(pos) local meta = minetest.get_meta(pos)
meta:set_string("infotext", S("Switching Station")) meta:set_string("infotext", S("Switching Station"))
local network_id = technic.sw_pos2network(pos)
local net_sw_pos = network_id and technic.network2sw_pos(network_id)
local net_sw_node = net_sw_pos and minetest.get_node_or_nil(net_sw_pos)
if net_sw_node then
-- There's already network with same id, check if it already has active switching station
if net_sw_node.name == "technic:switching_station" then
-- Another switch found set active to 0 for this switch if another is already active
local net_sw_meta = minetest.get_meta(net_sw_pos)
meta:set_string("active", net_sw_meta:get_int("active") == 1 and 0 or 1)
else
-- Network switching station disappeared, cleanup caches and start new network
technic.remove_network(network_id)
meta:set_string("active", 1)
end
else
-- Clean start, not previous networks, no other switching stations
meta:set_string("active", 1)
end
meta:set_string("channel", "switching_station"..minetest.pos_to_string(pos)) meta:set_string("channel", "switching_station"..minetest.pos_to_string(pos))
meta:set_string("formspec", "field[channel;Channel;${channel}]") meta:set_string("formspec", "field[channel;Channel;${channel}]")
local poshash = minetest.hash_node_position(pos) start_network(pos)
technic.redundant_warn.poshash = nil
end, end,
after_dig_node = function(pos) on_destruct = function(pos)
pos.y = pos.y - 1 -- Remove network when switching station is removed, if
local poshash = minetest.hash_node_position(pos) -- there's another switching station network will be rebuilt.
technic.redundant_warn.poshash = nil local network_id = technic.sw_pos2network(pos)
if technic.networks[network_id] then
technic.remove_network(network_id)
end
end, end,
on_receive_fields = function(pos, formname, fields, sender) on_receive_fields = function(pos, formname, fields, sender)
if not fields.channel then if not fields.channel then
@ -116,533 +88,83 @@ minetest.register_node("technic:switching_station",{
if channel ~= meta:get_string("channel") then if channel ~= meta:get_string("channel") then
return return
end end
local network_id = technic.sw_pos2network(pos)
local network = network_id and technic.networks[network_id]
if network then
digilines.receptor_send(pos, technic.digilines.rules, channel, { digilines.receptor_send(pos, technic.digilines.rules, channel, {
supply = meta:get_int("supply"), supply = network.supply,
demand = meta:get_int("demand"), demand = network.demand,
lag = meta:get_int("lag") lag = network.lag
}) })
else
digilines.receptor_send(pos, technic.digilines.rules, channel, {
error = "No network",
})
end
end end
}, },
}, },
}) })
--------------------------------------------------
-- Functions to traverse the electrical network
--------------------------------------------------
local function flatten(map)
local list = {}
for key, value in pairs(map) do
list[#list + 1] = value
end
return list
end
local function attach_network_machine(network_id, pos)
local pos_hash = minetest.hash_node_position(pos)
local net_id_old = technic.cables[pos_hash]
if net_id_old == nil then
technic.cables[pos_hash] = network_id
elseif net_id_old ~= network_id then
-- do not allow running pos from multiple networks, also disable switch
overload_network(network_id, pos)
overload_network(net_id_old, pos)
technic.cables[pos_hash] = network_id
local meta = minetest.get_meta(pos)
meta:set_string("infotext",S("Network Overloaded"))
end
end
-- Add a wire node to the LV/MV/HV network
local function add_network_node(nodes, pos, network_id)
local node_id = minetest.hash_node_position(pos)
technic.cables[node_id] = network_id
if nodes[node_id] then
return false
end
nodes[node_id] = pos
return true
end
local function add_cable_node(nodes, pos, network_id, queue)
if add_network_node(nodes, pos, network_id) then
queue[#queue + 1] = pos
end
end
-- Generic function to add found connected nodes to the right classification array
local function check_node_subp(PR_nodes, RE_nodes, BA_nodes, SP_nodes, all_nodes, pos, machines, tier, sw_pos, from_below, network_id, queue)
local distance_to_switch = vector.distance(pos, sw_pos)
if distance_to_switch > switch_max_range then
-- max range exceeded
return
end
technic.get_or_load_node(pos)
local name = minetest.get_node(pos).name
if technic.is_tier_cable(name, tier) then
add_cable_node(all_nodes, pos, network_id, queue)
elseif machines[name] then
--dprint(name.." is a "..machines[name])
if machines[name] == technic.producer then
attach_network_machine(network_id, pos)
add_network_node(PR_nodes, pos, network_id)
elseif machines[name] == technic.receiver then
attach_network_machine(network_id, pos)
add_network_node(RE_nodes, pos, network_id)
elseif machines[name] == technic.producer_receiver then
--attach_network_machine(network_id, pos)
add_network_node(PR_nodes, pos, network_id)
add_network_node(RE_nodes, pos, network_id)
elseif machines[name] == "SPECIAL" and
(pos.x ~= sw_pos.x or pos.y ~= sw_pos.y or pos.z ~= sw_pos.z) and
from_below then
-- Another switching station -> disable it
attach_network_machine(network_id, pos)
add_network_node(SP_nodes, pos, network_id)
local meta = minetest.get_meta(pos)
meta:set_int("active", 0)
elseif machines[name] == technic.battery then
attach_network_machine(network_id, pos)
add_network_node(BA_nodes, pos, network_id)
end
technic.touch_node(tier, pos, 2) -- Touch node
end
end
-- Traverse a network given a list of machines and a cable type name
local function traverse_network(PR_nodes, RE_nodes, BA_nodes, SP_nodes, all_nodes, pos, machines, tier, sw_pos, network_id, queue)
local positions = {
{x=pos.x+1, y=pos.y, z=pos.z},
{x=pos.x-1, y=pos.y, z=pos.z},
{x=pos.x, y=pos.y+1, z=pos.z},
{x=pos.x, y=pos.y-1, z=pos.z},
{x=pos.x, y=pos.y, z=pos.z+1},
{x=pos.x, y=pos.y, z=pos.z-1}}
for i, cur_pos in pairs(positions) do
check_node_subp(PR_nodes, RE_nodes, BA_nodes, SP_nodes, all_nodes, cur_pos, machines, tier, sw_pos, i == 3, network_id, queue)
end
end
function technic.remove_network(network_id)
local cables = technic.cables
for pos_hash,cable_net_id in pairs(cables) do
if cable_net_id == network_id then
cables[pos_hash] = nil
end
end
technic.networks[network_id] = nil
end
function technic.sw_pos2network(pos)
return pos and technic.cables[minetest.hash_node_position({x=pos.x,y=pos.y-1,z=pos.z})]
end
function technic.pos2network(pos)
return pos and technic.cables[minetest.hash_node_position(pos)]
end
function technic.network2pos(network_id)
return network_id and minetest.get_position_from_hash(network_id)
end
function technic.network2sw_pos(network_id)
-- Return switching station position for network.
-- It is not guaranteed that position actually contains switching station.
local sw_pos = minetest.get_position_from_hash(network_id)
sw_pos.y = sw_pos.y + 1
return sw_pos
end
local node_timeout = {}
function technic.get_timeout(tier, pos)
if node_timeout[tier] == nil then
-- it is normal that some multi tier nodes always drop here when checking all LV, MV and HV tiers
return 0
end
return node_timeout[tier][minetest.hash_node_position(pos)] or 0
end
function technic.touch_node(tier, pos, timeout)
if node_timeout[tier] == nil then
-- this should get built up during registration
node_timeout[tier] = {}
end
node_timeout[tier][minetest.hash_node_position(pos)] = timeout or 2
end
local function touch_nodes(list, tier)
local touch_node = technic.touch_node
for _, pos in ipairs(list) do
touch_node(tier, pos, 2) -- Touch node
end
end
local function get_network(network_id, sw_pos, pos1, tier)
local cached = technic.networks[network_id]
if cached and cached.tier == tier then
touch_nodes(cached.PR_nodes, tier)
touch_nodes(cached.BA_nodes, tier)
touch_nodes(cached.RE_nodes, tier)
for _, pos in ipairs(cached.SP_nodes) do
local meta = minetest.get_meta(pos)
meta:set_int("active", 0)
meta:set_string("active_pos", minetest.serialize(sw_pos))
technic.touch_node(tier, pos, 2) -- Touch node
end
return cached.PR_nodes, cached.BA_nodes, cached.RE_nodes
end
local PR_nodes = {}
local BA_nodes = {}
local RE_nodes = {}
local SP_nodes = {}
local all_nodes = {}
local queue = {}
add_cable_node(all_nodes, pos1, network_id, queue)
while next(queue) do
local to_visit = {}
for _, pos in ipairs(queue) do
traverse_network(PR_nodes, RE_nodes, BA_nodes, SP_nodes, all_nodes,
pos, technic.machines[tier], tier, sw_pos, network_id, to_visit)
end
queue = to_visit
end
PR_nodes = flatten(PR_nodes)
BA_nodes = flatten(BA_nodes)
RE_nodes = flatten(RE_nodes)
SP_nodes = flatten(SP_nodes)
technic.networks[network_id] = {tier = tier, all_nodes = all_nodes, SP_nodes = SP_nodes,
PR_nodes = PR_nodes, RE_nodes = RE_nodes, BA_nodes = BA_nodes}
return PR_nodes, BA_nodes, RE_nodes
end
----------------------------------------------- -----------------------------------------------
-- The action code for the switching station -- -- The action code for the switching station --
----------------------------------------------- -----------------------------------------------
technic.powerctrl_state = true
minetest.register_chatcommand("powerctrl", {
params = "state",
description = "Enables or disables technic's switching station ABM",
privs = { basic_privs = true },
func = function(name, state)
if state == "on" then
technic.powerctrl_state = true
else
technic.powerctrl_state = false
end
end
})
-- Run all the nodes
local function run_nodes(list, run_stage)
for _, pos in ipairs(list) do
technic.get_or_load_node(pos)
local node = minetest.get_node_or_nil(pos)
if node and node.name then
local nodedef = minetest.registered_nodes[node.name]
if nodedef and nodedef.technic_run then
nodedef.technic_run(pos, node, run_stage)
end
end
end
end
function technic.switching_station_run(pos)
if not technic.powerctrl_state then return end
local t0 = minetest.get_us_time()
local meta = minetest.get_meta(pos)
local meta1
local pos1 = {}
local tier = ""
local PR_nodes
local BA_nodes
local RE_nodes
local machine_name = S("Switching Station")
-- Which kind of network are we on:
pos1 = {x=pos.x, y=pos.y-1, z=pos.z}
--Disable if necessary
if meta:get_int("active") ~= 1 then
meta:set_string("infotext",S("%s Already Present"):format(machine_name))
local poshash = minetest.hash_node_position(pos)
if not technic.redundant_warn[poshash] then
technic.redundant_warn[poshash] = true
print("[TECHNIC] Warning: redundant switching station found near "..minetest.pos_to_string(pos))
end
return
end
local network_id = minetest.hash_node_position(pos1)
-- Check if network is overloaded / conflicts with another network
if overloaded_networks[network_id] then
local remaining = reset_overloaded(network_id)
if remaining > 0 then
meta:set_string("infotext",S("%s Network Overloaded, Restart in %dms"):format(machine_name, remaining / 1000))
-- Set switching station supply value to zero to clean up power monitor supply info
meta:set_int("supply",0)
return
end
meta:set_string("infotext",S("%s Restarting Network"):format(machine_name))
return
end
local name = minetest.get_node(pos1).name
local tier = technic.get_cable_tier(name)
if tier then
PR_nodes, BA_nodes, RE_nodes = get_network(network_id, pos, pos1, tier)
if overloaded_networks[network_id] then return end
else
--dprint("Not connected to a network")
meta:set_string("infotext", S("%s Has No Network"):format(machine_name))
return
end
run_nodes(PR_nodes, technic.producer)
run_nodes(RE_nodes, technic.receiver)
run_nodes(BA_nodes, technic.battery)
-- Strings for the meta data
local eu_demand_str = tier.."_EU_demand"
local eu_input_str = tier.."_EU_input"
local eu_supply_str = tier.."_EU_supply"
-- Distribute charge equally across multiple batteries.
local charge_total = 0
local battery_count = 0
local BA_charge = 0
local BA_charge_max = 0
for n, pos1 in pairs(BA_nodes) do
meta1 = minetest.get_meta(pos1)
local charge = meta1:get_int("internal_EU_charge")
local charge_max = meta1:get_int("internal_EU_charge_max")
BA_charge = BA_charge + charge
BA_charge_max = BA_charge_max + charge_max
if (meta1:get_int(eu_demand_str) ~= 0) then
charge_total = charge_total + charge
battery_count = battery_count + 1
end
end
local charge_distributed = math.floor(charge_total / battery_count)
for n, pos1 in pairs(BA_nodes) do
meta1 = minetest.get_meta(pos1)
if (meta1:get_int(eu_demand_str) ~= 0) then
meta1:set_int("internal_EU_charge", charge_distributed)
end
end
-- Get all the power from the PR nodes
local PR_eu_supply = 0 -- Total power
for _, pos1 in pairs(PR_nodes) do
meta1 = minetest.get_meta(pos1)
PR_eu_supply = PR_eu_supply + meta1:get_int(eu_supply_str)
end
--dprint("Total PR supply:"..PR_eu_supply)
-- Get all the demand from the RE nodes
local RE_eu_demand = 0
for _, pos1 in pairs(RE_nodes) do
meta1 = minetest.get_meta(pos1)
RE_eu_demand = RE_eu_demand + meta1:get_int(eu_demand_str)
end
--dprint("Total RE demand:"..RE_eu_demand)
-- Get all the power from the BA nodes
local BA_eu_supply = 0
for _, pos1 in pairs(BA_nodes) do
meta1 = minetest.get_meta(pos1)
BA_eu_supply = BA_eu_supply + meta1:get_int(eu_supply_str)
end
--dprint("Total BA supply:"..BA_eu_supply)
-- Get all the demand from the BA nodes
local BA_eu_demand = 0
for _, pos1 in pairs(BA_nodes) do
meta1 = minetest.get_meta(pos1)
BA_eu_demand = BA_eu_demand + meta1:get_int(eu_demand_str)
end
--dprint("Total BA demand:"..BA_eu_demand)
meta:set_string("infotext", S("@1. Supply: @2 Demand: @3",
machine_name, technic.EU_string(PR_eu_supply),
technic.EU_string(RE_eu_demand)))
-- If mesecon signal and power supply or demand changed then
-- send them via digilines.
if mesecons_path and digilines_path and mesecon.is_powered(pos) then
if PR_eu_supply ~= meta:get_int("supply") or
RE_eu_demand ~= meta:get_int("demand") then
local channel = meta:get_string("channel")
digilines.receptor_send(pos, technic.digilines.rules, channel, {
supply = PR_eu_supply,
demand = RE_eu_demand
})
end
end
-- Data that will be used by the power monitor
meta:set_int("supply",PR_eu_supply)
meta:set_int("demand",RE_eu_demand)
meta:set_int("battery_count",#BA_nodes)
meta:set_int("battery_charge",BA_charge)
meta:set_int("battery_charge_max",BA_charge_max)
-- If the PR supply is enough for the RE demand supply them all
if PR_eu_supply >= RE_eu_demand then
--dprint("PR_eu_supply"..PR_eu_supply.." >= RE_eu_demand"..RE_eu_demand)
for _, pos1 in pairs(RE_nodes) do
meta1 = minetest.get_meta(pos1)
local eu_demand = meta1:get_int(eu_demand_str)
meta1:set_int(eu_input_str, eu_demand)
end
-- We have a surplus, so distribute the rest equally to the BA nodes
-- Let's calculate the factor of the demand
PR_eu_supply = PR_eu_supply - RE_eu_demand
local charge_factor = 0 -- Assume all batteries fully charged
if BA_eu_demand > 0 then
charge_factor = PR_eu_supply / BA_eu_demand
end
for n, pos1 in pairs(BA_nodes) do
meta1 = minetest.get_meta(pos1)
local eu_demand = meta1:get_int(eu_demand_str)
meta1:set_int(eu_input_str, math.floor(eu_demand * charge_factor))
--dprint("Charging battery:"..math.floor(eu_demand*charge_factor))
end
local t1 = minetest.get_us_time()
local diff = t1 - t0
if diff > 50000 then
minetest.log("warning", "[technic] [+supply] switching station abm took " .. diff .. " us at " .. minetest.pos_to_string(pos))
end
return
end
-- If the PR supply is not enough for the RE demand we will discharge the batteries too
if PR_eu_supply + BA_eu_supply >= RE_eu_demand then
--dprint("PR_eu_supply "..PR_eu_supply.."+BA_eu_supply "..BA_eu_supply.." >= RE_eu_demand"..RE_eu_demand)
for _, pos1 in pairs(RE_nodes) do
meta1 = minetest.get_meta(pos1)
local eu_demand = meta1:get_int(eu_demand_str)
meta1:set_int(eu_input_str, eu_demand)
end
-- We have a deficit, so distribute to the BA nodes
-- Let's calculate the factor of the supply
local charge_factor = 0 -- Assume all batteries depleted
if BA_eu_supply > 0 then
charge_factor = (PR_eu_supply - RE_eu_demand) / BA_eu_supply
end
for n,pos1 in pairs(BA_nodes) do
meta1 = minetest.get_meta(pos1)
local eu_supply = meta1:get_int(eu_supply_str)
meta1:set_int(eu_input_str, math.floor(eu_supply * charge_factor))
--dprint("Discharging battery:"..math.floor(eu_supply*charge_factor))
end
local t1 = minetest.get_us_time()
local diff = t1 - t0
if diff > 50000 then
minetest.log("warning", "[technic] [-supply] switching station abm took " .. diff .. " us at " .. minetest.pos_to_string(pos))
end
return
end
-- If the PR+BA supply is not enough for the RE demand: Power only the batteries
local charge_factor = 0 -- Assume all batteries fully charged
if BA_eu_demand > 0 then
charge_factor = PR_eu_supply / BA_eu_demand
end
for n, pos1 in pairs(BA_nodes) do
meta1 = minetest.get_meta(pos1)
local eu_demand = meta1:get_int(eu_demand_str)
meta1:set_int(eu_input_str, math.floor(eu_demand * charge_factor))
end
for n, pos1 in pairs(RE_nodes) do
meta1 = minetest.get_meta(pos1)
meta1:set_int(eu_input_str, 0)
end
local t1 = minetest.get_us_time()
local diff = t1 - t0
if diff > 50000 then
minetest.log("warning", "[technic] switching station abm took " .. diff .. " us at " .. minetest.pos_to_string(pos))
end
end
-- Timeout ABM -- Timeout ABM
-- Timeout for a node in case it was disconnected from the network -- Timeout for a node in case it was disconnected from the network
-- A node must be touched by the station continuously in order to function -- A node must be touched by the station continuously in order to function
local function switching_station_timeout_count(pos, tier)
local timeout = technic.get_timeout(tier, pos)
if timeout <= 0 then
local meta = minetest.get_meta(pos)
meta:set_int(tier.."_EU_input", 0) -- Not needed anymore <-- actually, it is for supply converter
return true
else
technic.touch_node(tier, pos, timeout - 1)
return false
end
end
minetest.register_abm({ minetest.register_abm({
label = "Machines: timeout check", label = "Machines: timeout check",
nodenames = {"group:technic_machine"}, nodenames = {"group:technic_machine"},
interval = 1, interval = 1,
chance = 1, chance = 1,
action = function(pos, node, active_object_count, active_object_count_wider) action = function(pos, node, active_object_count, active_object_count_wider)
for tier, machines in pairs(technic.machines) do -- Check for machine timeouts for all tiers
if machines[node.name] and switching_station_timeout_count(pos, tier) then local tiers = technic.machine_tiers[node.name]
local nodedef = minetest.registered_nodes[node.name] local timed_out = true
if nodedef and nodedef.technic_disabled_machine_name then for _, tier in ipairs(tiers) do
node.name = nodedef.technic_disabled_machine_name local timeout = technic.get_timeout(tier, pos)
minetest.swap_node(pos, node) if timeout > 0 then
elseif nodedef and nodedef.technic_on_disable then technic.touch_node(tier, pos, timeout - 1)
nodedef.technic_on_disable(pos, node) timed_out = false
end
if nodedef then
local meta = minetest.get_meta(pos)
meta:set_string("infotext", S("%s Has No Network"):format(nodedef.description))
end end
end end
-- If all tiers for machine timed out take action
if timed_out then
technic.disable_machine(pos, node)
end end
end, end,
}) })
--Re-enable disabled switching station if necessary, similar to the timeout above --Re-enable network of switching station if necessary, similar to the timeout above
minetest.register_abm({ minetest.register_abm({
label = "Machines: re-enable check", label = "Machines: re-enable check",
nodenames = {"technic:switching_station"}, nodenames = {"technic:switching_station"},
interval = 1, interval = 1,
chance = 1, chance = 1,
action = function(pos, node, active_object_count, active_object_count_wider) action = function(pos, node, active_object_count, active_object_count_wider)
local pos1 = {x=pos.x,y=pos.y-1,z=pos.z} local network_id = technic.sw_pos2network(pos)
local tier = technic.get_cable_tier(minetest.get_node(pos1).name) -- Check if network is overloaded / conflicts with another network
if not tier then return end if network_id then
if switching_station_timeout_count(pos, tier) then local infotext
local meta = minetest.get_meta(pos) local meta = minetest.get_meta(pos)
meta:set_int("active",1) if technic.is_overloaded(network_id) then
local remaining = technic.reset_overloaded(network_id)
if remaining > 0 then
infotext = S("%s Network Overloaded, Restart in %dms"):format(S("Switching Station"), remaining / 1000)
else
infotext = S("%s Restarting Network"):format(S("Switching Station"))
end
technic.network_infotext(network_id, infotext)
else
-- Network exists and is not overloaded, reactivate network
technic.activate_network(network_id)
infotext = technic.network_infotext(network_id)
end
meta:set_string("infotext", infotext)
else
-- Network does not exist yet, attempt to create new network here
start_network(pos)
end end
end, end,
}) })
for tier, machines in pairs(technic.machines) do
-- SPECIAL will not be traversed
technic.register_machine(tier, "technic:switching_station", "SPECIAL")
end

View File

@ -1,23 +1,6 @@
local has_monitoring_mod = minetest.get_modpath("monitoring") local has_monitoring_mod = minetest.get_modpath("monitoring")
local switches = {} -- pos_hash -> { time = time_us }
local function get_switch_data(pos)
local hash = minetest.hash_node_position(pos)
local switch = switches[hash]
if not switch then
switch = {
time = 0,
skip = 0
}
switches[hash] = switch
end
return switch
end
local active_switching_stations_metric, switching_stations_usage_metric local active_switching_stations_metric, switching_stations_usage_metric
if has_monitoring_mod then if has_monitoring_mod then
@ -32,21 +15,9 @@ if has_monitoring_mod then
) )
end end
-- collect all active switching stations
minetest.register_abm({
nodenames = {"technic:switching_station"},
label = "Switching Station",
interval = 1,
chance = 1,
action = function(pos)
local switch = get_switch_data(pos)
switch.time = minetest.get_us_time()
end
})
-- the interval between technic_run calls -- the interval between technic_run calls
local technic_run_interval = 1.0 local technic_run_interval = 1.0
local set_default_timeout = technic.set_default_timeout
-- iterate over all collected switching stations and execute the technic_run function -- iterate over all collected switching stations and execute the technic_run function
local timer = 0 local timer = 0
@ -71,91 +42,78 @@ minetest.register_globalstep(function(dtime)
-- normal run_interval -- normal run_interval
technic_run_interval = 1.0 technic_run_interval = 1.0
end end
set_default_timeout(math.ceil(technic_run_interval) + 1)
local now = minetest.get_us_time() local now = minetest.get_us_time()
local off_delay_seconds = tonumber(minetest.settings:get("technic.switch.off_delay_seconds") or "1800")
local off_delay_micros = off_delay_seconds*1000*1000
local active_switches = 0 local active_switches = 0
for hash, switch in pairs(switches) do for network_id, network in pairs(technic.active_networks) do
local pos = minetest.get_position_from_hash(hash) local pos = technic.network2sw_pos(network_id)
local diff = now - switch.time
minetest.get_voxel_manip(pos, pos) local node = technic.get_or_load_node(pos) or minetest.get_node(pos)
local node = minetest.get_node(pos)
if node.name ~= "technic:switching_station" then if node.name ~= "technic:switching_station" then
-- station vanished -- station vanished
switches[hash] = nil technic.remove_network(network_id)
elseif diff < off_delay_micros then elseif network.timeout > now then
-- station active -- station active
active_switches = active_switches + 1 active_switches = active_switches + 1
if switch.skip < 1 then if network.skip > 0 then
network.skip = network.skip - 1
else
local start = minetest.get_us_time() local start = minetest.get_us_time()
technic.switching_station_run(pos) technic.network_run(network_id)
local switch_diff = minetest.get_us_time() - start local switch_diff = minetest.get_us_time() - start
local meta = minetest.get_meta(pos)
-- set lag in microseconds into the "lag" meta field -- set lag in microseconds into the "lag" meta field
meta:set_int("lag", switch_diff) network.lag = switch_diff
-- overload detection -- overload detection
if switch_diff > 250000 then if switch_diff > 250000 then
switch.skip = 30 network.skip = 30
elseif switch_diff > 150000 then elseif switch_diff > 150000 then
switch.skip = 20 network.skip = 20
elseif switch_diff > 75000 then elseif switch_diff > 75000 then
switch.skip = 10 network.skip = 10
elseif switch_diff > 50000 then elseif switch_diff > 50000 then
switch.skip = 2 network.skip = 2
end end
if switch.skip > 0 then if network.skip > 0 then
-- calculate efficiency in percent and display it -- calculate efficiency in percent and display it
local efficiency = math.floor(1/switch.skip*100) local efficiency = math.floor(1/network.skip*100)
meta:set_string("infotext", "Polyfuse triggered, current efficiency: " .. technic.network_infotext(network_id, "Polyfuse triggered, current efficiency: " ..
efficiency .. "% generated lag : " .. math.floor(switch_diff/1000) .. " ms") efficiency .. "% generated lag : " .. math.floor(switch_diff/1000) .. " ms")
-- remove laggy switching station from active index -- remove laggy network from active index
-- it will be reactivated when a player is near it -- it will be reactivated when a player is near it
-- laggy switching stations won't work well in unloaded areas this way technic.active_networks[network_id] = nil
switches[hash] = nil
end end
else
switch.skip = math.max(switch.skip - 1, 0)
end end
else else
-- station timed out -- station timed out
switches[hash] = nil technic.active_networks[network_id] = nil
end end
end end
local time_usage = minetest.get_us_time() - now
if has_monitoring_mod then if has_monitoring_mod then
local time_usage = minetest.get_us_time() - now
active_switching_stations_metric.set(active_switches) active_switching_stations_metric.set(active_switches)
switching_stations_usage_metric.inc(time_usage) switching_stations_usage_metric.inc(time_usage)
end end
end) end)
minetest.register_chatcommand("technic_flush_switch_cache", { minetest.register_chatcommand("technic_flush_switch_cache", {
description = "removes all loaded switching stations from the cache", description = "removes all loaded networks from the cache",
privs = { server = true }, privs = { server = true },
func = function() func = function()
switches = {} technic.active_networks = {}
end end
}) })

View File

@ -9,17 +9,23 @@ technic.battery = "BA"
technic.machines = {} technic.machines = {}
technic.power_tools = {} technic.power_tools = {}
technic.networks = {} technic.networks = {}
technic.machine_tiers = {}
function technic.register_tier(tier, description) function technic.register_tier(tier, description)
technic.machines[tier] = {} technic.machines[tier] = {}
end end
function technic.register_machine(tier, nodename, machine_type) function technic.register_machine(tier, nodename, machine_type)
-- Lookup table to get compatible node names and machine type by tier
if not technic.machines[tier] then if not technic.machines[tier] then
return return
end end
technic.machines[tier][nodename] = machine_type technic.machines[tier][nodename] = machine_type
-- Lookup table to get compatible tiers by node name
if not technic.machine_tiers[nodename] then
technic.machine_tiers[nodename] = {}
end
table.insert(technic.machine_tiers[nodename], tier)
end end
function technic.register_power_tool(craftitem, max_charge) function technic.register_power_tool(craftitem, max_charge)

View File

@ -0,0 +1,355 @@
dofile("spec/test_helpers.lua")
--[[
Technic network unit tests.
Execute busted at technic source directory.
--]]
-- Load fixtures required by tests
fixture("minetest")
fixture("minetest/player")
fixture("minetest/protection")
fixture("pipeworks")
fixture("network")
sourcefile("machines/network")
sourcefile("machines/register/cables")
sourcefile("machines/LV/cables")
sourcefile("machines/MV/cables")
sourcefile("machines/HV/cables")
sourcefile("machines/register/generator")
sourcefile("machines/HV/generator")
function get_network_fixture(sw_pos)
-- Build network
local net_id = technic.create_network(sw_pos)
assert.is_number(net_id)
local net = technic.networks[net_id]
assert.is_table(net)
return net
end
describe("Power network building", function()
describe("cable building", function()
world.layout({
{{x=100,y=800,z=100}, "technic:hv_cable"},
{{x=100,y=801,z=100}, "technic:switching_station"},
{{x=101,y=800,z=100}, "technic:hv_cable"},
{{x=101,y=801,z=100}, "technic:hv_generator"},
--{{x=102,y=800,z=100}, "technic:hv_cable"}, -- This cable is built
--{{x=102,y=801,z=100}, "technic:hv_cable"}, -- TODO: Add this cable as test case?
{{x=103,y=800,z=100}, "technic:hv_cable"}, -- This should appear
{{x=103,y=801,z=100}, "technic:hv_generator"}, -- This should appear
})
-- Build network
local net = get_network_fixture({x=100,y=801,z=100})
local build_pos = {x=102,y=800,z=100}
it("does not crash", function()
assert.equals(1, #net.PR_nodes)
assert.equals(3, count(net.all_nodes))
world.set_node(build_pos, {name="technic:hv_cable", param2=0})
technic.network_node_on_placenode(build_pos, {"HV"}, "technic:hv_cable")
end)
it("is added to network", function()
assert.same(build_pos, net.all_nodes[minetest.hash_node_position(build_pos)])
end)
it("adds all network nodes", function()
assert.equals(6, count(net.all_nodes))
end)
it("adds connected machines to network without duplicates", function()
assert.equals(2, #net.PR_nodes)
--assert.equals({x=103,y=801,z=100}, net.PR_nodes[2])
end)
end)
describe("cable building to machine", function()
world.layout({
{{x=100,y=810,z=100}, "technic:hv_cable"},
{{x=100,y=811,z=100}, "technic:switching_station"},
{{x=101,y=810,z=100}, "technic:hv_cable"},
{{x=101,y=811,z=100}, "technic:hv_generator"},
{{x=102,y=810,z=100}, "technic:hv_cable"},
--{{x=102,y=811,z=100}, "technic:hv_cable"}, -- This cable is built
--{{x=103,y=810,z=100}, "technic:hv_cable"}, -- This cable is built
{{x=103,y=811,z=100}, "technic:hv_generator"}, -- This should appear
{{x=103,y=812,z=100}, "technic:hv_cable"}, -- Unconnected cable
})
-- Build network
local net = get_network_fixture({x=100,y=811,z=100})
local build_pos = {x=103,y=810,z=100}
local build_pos2 = {x=102,y=811,z=100}
it("does not crash", function()
assert.equals(1, #net.PR_nodes)
assert.equals(4, count(net.all_nodes))
world.set_node(build_pos, {name="technic:hv_cable", param2=0})
technic.network_node_on_placenode(build_pos, {"HV"}, "technic:hv_cable")
end)
it("is added to network", function()
assert.same(build_pos, net.all_nodes[minetest.hash_node_position(build_pos)])
end)
it("adds all network nodes", function()
assert.equals(6, count(net.all_nodes))
end)
it("adds connected machines to network without duplicates", function()
assert.equals(2, #net.PR_nodes)
--assert.equals({x=103,y=801,z=100}, net.PR_nodes[2])
end)
it("does not add unconnected cables to network", function()
assert.is_nil(net.all_nodes[minetest.hash_node_position({x=103,y=812,z=100})])
end)
it("does not duplicate already added machine", function()
world.set_node(build_pos2, {name="technic:hv_cable", param2=0})
technic.network_node_on_placenode(build_pos2, {"HV"}, "technic:hv_cable")
assert.equals(2, #net.PR_nodes)
assert.equals(7, count(net.all_nodes))
end)
end)
describe("machine building", function()
world.layout({
{{x=100,y=820,z=100}, "technic:hv_cable"},
{{x=100,y=821,z=100}, "technic:switching_station"},
{{x=101,y=820,z=100}, "technic:hv_cable"},
{{x=101,y=821,z=100}, "technic:hv_generator"},
{{x=102,y=820,z=100}, "technic:hv_cable"},
-- {{x=102,y=821,z=100}, "technic:hv_generator"}, -- This machine is built
{{x=102,y=821,z= 99}, "technic:hv_cable"}, -- This should not be added to network
{{x=102,y=821,z=101}, "technic:hv_cable"}, -- This should not be added to network
{{x=103,y=820,z=100}, "technic:hv_cable"},
{{x=103,y=821,z=100}, "technic:hv_generator"},
-- Second network for overload test
{{x=100,y=820,z=102}, "technic:hv_cable"},
{{x=100,y=821,z=102}, "technic:switching_station"},
-- {{x=100,y=820,z=101}, "technic:hv_generator"}, -- This machine is built, it should overload
})
-- Build network
local net = get_network_fixture({x=100,y=821,z=100})
local net2 = get_network_fixture({x=100,y=821,z=102})
local build_pos = {x=102,y=821,z=100}
local build_pos2 = {x=100,y=820,z=101}
it("does not crash", function()
assert.equals(2, #net.PR_nodes)
assert.equals(6, count(net.all_nodes))
world.set_node(build_pos, {name="technic:hv_generator",param2=0})
technic.network_node_on_placenode(build_pos, {"HV"}, "technic:hv_generator")
end)
it("is added to network without duplicates", function()
assert.same(build_pos, net.all_nodes[minetest.hash_node_position(build_pos)])
assert.equals(7, count(net.all_nodes))
assert.equals(3, #net.PR_nodes)
assert.is_nil(technic.is_overloaded(net.id))
assert.is_nil(technic.is_overloaded(net2.id))
end)
it("does not remove connected machines from network", function()
assert.same({x=101,y=821,z=100},net.all_nodes[minetest.hash_node_position({x=101,y=821,z=100})])
assert.same({x=103,y=821,z=100},net.all_nodes[minetest.hash_node_position({x=103,y=821,z=100})])
end)
it("does not remove network", function()
assert.is_hashed(technic.networks[net.id])
end)
it("does not add cables to network", function()
assert.is_nil(net.all_nodes[minetest.hash_node_position({x=102,y=821,z=99})])
assert.is_nil(net.all_nodes[minetest.hash_node_position({x=102,y=821,z=101})])
end)
it("overloads network", function()
world.set_node(build_pos2, {name="technic:hv_generator",param2=0})
technic.network_node_on_placenode(build_pos2, {"HV"}, "technic:hv_generator")
assert.not_nil(technic.is_overloaded(net.id))
assert.not_nil(technic.is_overloaded(net2.id))
end)
end)
describe("cable building between networks", function()
world.layout({
{{x=100,y=830,z=100}, "technic:hv_cable"},
{{x=100,y=831,z=100}, "technic:switching_station"},
--{{x=101,y=830,z=100}, "technic:hv_cable"}, -- This cable is built
--{{x=101,y=831,z=100}, "technic:hv_cable"}, -- TODO: Add this cable as test case?
{{x=102,y=830,z=100}, "technic:hv_cable"},
{{x=102,y=831,z=100}, "technic:switching_station"},
})
-- Build network
local net = get_network_fixture({x=100,y=831,z=100})
local net2 = get_network_fixture({x=102,y=831,z=100})
local build_pos = {x=101,y=830,z=100}
it("does not crash", function()
assert.equals(1, count(net.all_nodes))
assert.equals(1, count(net2.all_nodes))
world.set_node(build_pos, {name="technic:hv_cable", param2=0})
technic.network_node_on_placenode(build_pos, {"HV"}, "technic:hv_cable")
end)
it("removes network", function()
assert.is_nil(technic.networks[net.id])
assert.is_nil(technic.networks[net2.id])
end)
end)
describe("cable cutting", function()
world.layout({
{{x=100,y=900,z=100}, "technic:hv_cable"},
{{x=100,y=901,z=100}, "technic:switching_station"},
{{x=101,y=900,z=100}, "technic:hv_cable"},
{{x=101,y=901,z=100}, "technic:hv_generator"},
{{x=102,y=900,z=100}, "technic:hv_cable"}, -- This cable is digged
{{x=103,y=900,z=100}, "technic:hv_cable"}, -- This should disappear
{{x=103,y=901,z=100}, "technic:hv_generator"}, -- This should disappear
})
-- Build network
local net = get_network_fixture({x=100,y=901,z=100})
local build_pos = {x=102,y=900,z=100}
it("does not crash", function()
assert.equals(2, #net.PR_nodes)
assert.equals(6, count(net.all_nodes))
world.set_node(build_pos, {name="air",param2=0})
technic.network_node_on_dignode(build_pos, {"HV"}, "technic:hv_cable")
end)
--[[ NOTE: Whole network is currently removed when cutting cables
it("is removed from network", function()
assert.is_nil(net.all_nodes[minetest.hash_node_position(build_pos)])
end)
it("removes connected cables from network", function()
--assert.is_nil(net.all_nodes[minetest.hash_node_position({x=103,y=900,z=100})])
assert.equals(3, count(net.all_nodes))
end)
it("removes connected machines from network", function()
--assert.is_nil(net.all_nodes[minetest.hash_node_position({x=103,y=901,z=100})])
assert.equals(1, #net.PR_nodes)
end)
--]]
it("removes network", function()
assert.is_nil(technic.networks[net.id])
end)
end)
describe("cable digging below machine", function()
world.layout({
{{x=100,y=910,z=100}, "technic:hv_cable"},
{{x=100,y=911,z=100}, "technic:switching_station"},
{{x=101,y=910,z=100}, "technic:hv_cable"},
{{x=101,y=911,z=100}, "technic:hv_generator"},
{{x=102,y=910,z=100}, "technic:hv_cable"},
{{x=103,y=910,z=100}, "technic:hv_cable"}, -- This cable is digged
{{x=103,y=911,z=100}, "technic:hv_generator"}, -- This should disappear
-- Multiple cable connections to machine at x 101, vertical cable
{{x=101,y=910,z=101}, "technic:hv_cable"}, -- cables for second connection
{{x=101,y=911,z=101}, "technic:hv_cable"}, -- cables for second connection, this cable is digged
})
-- Build network
local net = get_network_fixture({x=100,y=911,z=100})
local build_pos = {x=103,y=910,z=100}
local build_pos2 = {x=101,y=911,z=101}
it("does not crash", function()
assert.equals(2, #net.PR_nodes)
assert.equals(8, count(net.all_nodes))
world.set_node(build_pos, {name="air",param2=0})
technic.network_node_on_dignode(build_pos, {"HV"}, "technic:hv_cable")
end)
it("is removed from network", function()
assert.is_nil(net.all_nodes[minetest.hash_node_position(build_pos)])
assert.equals(6, count(net.all_nodes))
end)
it("removes connected machines from network", function()
assert.is_nil(net.all_nodes[minetest.hash_node_position({x=103,y=911,z=100})])
assert.equals(1, #net.PR_nodes)
end)
it("does not remove network", function()
assert.is_hashed(technic.networks[net.id])
end)
it("keeps connected machines in network", function()
world.set_node(build_pos2, {name="air",param2=0})
technic.network_node_on_dignode(build_pos2, {"HV"}, "technic:hv_cable")
assert.same({x=101,y=911,z=100}, net.all_nodes[minetest.hash_node_position({x=101,y=911,z=100})])
assert.equals(1, #net.PR_nodes)
assert.equals(5, count(net.all_nodes))
end)
end)
describe("machine digging", function()
world.layout({
{{x=100,y=920,z=100}, "technic:hv_cable"},
{{x=100,y=921,z=100}, "technic:switching_station"},
{{x=101,y=920,z=100}, "technic:hv_cable"},
{{x=101,y=921,z=100}, "technic:hv_generator"},
{{x=102,y=920,z=100}, "technic:hv_cable"},
{{x=102,y=921,z=100}, "technic:hv_generator"}, -- This machine is digged
{{x=103,y=920,z=100}, "technic:hv_cable"},
{{x=103,y=921,z=100}, "technic:hv_generator"},
})
-- Build network
local net = get_network_fixture({x=100,y=921,z=100})
local build_pos = {x=102,y=921,z=100}
it("does not crash", function()
assert.equals(3, #net.PR_nodes)
assert.equals(7, count(net.all_nodes))
world.set_node(build_pos, {name="air",param2=0})
technic.network_node_on_dignode(build_pos, {"HV"}, "technic:hv_generator")
end)
it("is removed from network", function()
assert.is_nil(net.all_nodes[minetest.hash_node_position(build_pos)])
end)
it("does not remove other nodes from network", function()
assert.equals(2, #net.PR_nodes)
assert.equals(6, count(net.all_nodes))
end)
it("does not remove connected machines from network", function()
assert.same({x=101,y=921,z=100},net.all_nodes[minetest.hash_node_position({x=101,y=921,z=100})])
assert.same({x=103,y=921,z=100},net.all_nodes[minetest.hash_node_position({x=103,y=921,z=100})])
assert.equals(2, #net.PR_nodes)
end)
it("does not remove network", function()
assert.is_hashed(technic.networks[net.id])
end)
end)
end)

0
technic/spec/fixtures/minetest.cfg vendored Normal file
View File

120
technic/spec/fixtures/minetest.lua vendored Normal file
View File

@ -0,0 +1,120 @@
local function noop(...) end
local function dummy_coords(...) return { x = 123, y = 123, z = 123 } end
_G.world = { nodes = {} }
local world = _G.world
_G.world.set_node = function(pos, node)
local hash = minetest.hash_node_position(pos)
world.nodes[hash] = node
end
_G.world.clear = function() _G.world.nodes = {} end
_G.world.layout = function(layout, offset)
_G.world.clear()
_G.world.add_layout(layout, offset)
end
_G.world.add_layout = function(layout, offset)
for _, node in ipairs(layout) do
local pos = node[1]
if offset then
pos.x = pos.x + offset.x
pos.y = pos.y + offset.y
pos.z = pos.z + offset.z
end
_G.world.set_node(pos, {name=node[2], param2=0})
end
end
_G.core = {}
_G.minetest = _G.core
local configuration_file = fixture_path("minetest.cfg")
_G.Settings = function(fname)
local settings = {
_data = {},
get = function(self, key)
return self._data[key]
end,
get_bool = function(self, key, default)
return
end,
set = function(...)end,
set_bool = function(...)end,
write = function(...)end,
remove = function(self, key)
self._data[key] = nil
return true
end,
get_names = function(self)
local result = {}
for k,_ in pairs(t) do
table.insert(result, k)
end
return result
end,
to_table = function(self)
local result = {}
for k,v in pairs(self._data) do
result[k] = v
end
return result
end,
}
-- Not even nearly perfect config parser but should be good enough for now
file = assert(io.open(fname, "r"))
for line in file:lines() do
for key, value in string.gmatch(line, "([^= ]+) *= *(.-)$") do
settings._data[key] = value
end
end
return settings
end
_G.core.settings = _G.Settings(configuration_file)
_G.core.register_on_joinplayer = noop
_G.core.register_on_leaveplayer = noop
fixture("minetest/game/misc")
fixture("minetest/common/misc_helpers")
fixture("minetest/common/vector")
_G.minetest.registered_nodes = {
testnode1 = {},
testnode2 = {},
}
_G.minetest.registered_chatcommands = {}
_G.minetest.register_lbm = noop
_G.minetest.register_abm = noop
_G.minetest.register_chatcommand = noop
_G.minetest.chat_send_player = noop
_G.minetest.register_alias = noop
_G.minetest.register_craftitem = noop
_G.minetest.register_craft = noop
_G.minetest.register_node = noop
_G.minetest.register_on_placenode = noop
_G.minetest.register_on_dignode = noop
_G.minetest.register_on_mods_loaded = noop
_G.minetest.item_drop = noop
_G.minetest.get_us_time = function()
local socket = require 'socket'
-- FIXME: Returns the time in seconds, relative to the origin of the universe.
return socket.gettime() * 1000 * 1000
end
_G.minetest.get_node = function(pos)
local hash = minetest.hash_node_position(pos)
return world.nodes[hash] or {name="IGNORE",param2=0}
end
_G.minetest.get_modpath = function(...) return "./unit_test_modpath" end
_G.minetest.get_pointed_thing_position = dummy_coords
--
-- Minetest default noop table
--
local default = { __index = function(...) return function(...)end end }
_G.default = {}
setmetatable(_G.default, default)

View File

@ -0,0 +1,702 @@
-- Minetest: builtin/misc_helpers.lua
--------------------------------------------------------------------------------
-- Localize functions to avoid table lookups (better performance).
local string_sub, string_find = string.sub, string.find
--------------------------------------------------------------------------------
local function basic_dump(o)
local tp = type(o)
if tp == "number" then
return tostring(o)
elseif tp == "string" then
return string.format("%q", o)
elseif tp == "boolean" then
return tostring(o)
elseif tp == "nil" then
return "nil"
-- Uncomment for full function dumping support.
-- Not currently enabled because bytecode isn't very human-readable and
-- dump's output is intended for humans.
--elseif tp == "function" then
-- return string.format("loadstring(%q)", string.dump(o))
else
return string.format("<%s>", tp)
end
end
local keywords = {
["and"] = true,
["break"] = true,
["do"] = true,
["else"] = true,
["elseif"] = true,
["end"] = true,
["false"] = true,
["for"] = true,
["function"] = true,
["goto"] = true, -- Lua 5.2
["if"] = true,
["in"] = true,
["local"] = true,
["nil"] = true,
["not"] = true,
["or"] = true,
["repeat"] = true,
["return"] = true,
["then"] = true,
["true"] = true,
["until"] = true,
["while"] = true,
}
local function is_valid_identifier(str)
if not str:find("^[a-zA-Z_][a-zA-Z0-9_]*$") or keywords[str] then
return false
end
return true
end
--------------------------------------------------------------------------------
-- Dumps values in a line-per-value format.
-- For example, {test = {"Testing..."}} becomes:
-- _["test"] = {}
-- _["test"][1] = "Testing..."
-- This handles tables as keys and circular references properly.
-- It also handles multiple references well, writing the table only once.
-- The dumped argument is internal-only.
function dump2(o, name, dumped)
name = name or "_"
-- "dumped" is used to keep track of serialized tables to handle
-- multiple references and circular tables properly.
-- It only contains tables as keys. The value is the name that
-- the table has in the dump, eg:
-- {x = {"y"}} -> dumped[{"y"}] = '_["x"]'
dumped = dumped or {}
if type(o) ~= "table" then
return string.format("%s = %s\n", name, basic_dump(o))
end
if dumped[o] then
return string.format("%s = %s\n", name, dumped[o])
end
dumped[o] = name
-- This contains a list of strings to be concatenated later (because
-- Lua is slow at individual concatenation).
local t = {}
for k, v in pairs(o) do
local keyStr
if type(k) == "table" then
if dumped[k] then
keyStr = dumped[k]
else
-- Key tables don't have a name, so use one of
-- the form _G["table: 0xFFFFFFF"]
keyStr = string.format("_G[%q]", tostring(k))
-- Dump key table
t[#t + 1] = dump2(k, keyStr, dumped)
end
else
keyStr = basic_dump(k)
end
local vname = string.format("%s[%s]", name, keyStr)
t[#t + 1] = dump2(v, vname, dumped)
end
return string.format("%s = {}\n%s", name, table.concat(t))
end
--------------------------------------------------------------------------------
-- This dumps values in a one-statement format.
-- For example, {test = {"Testing..."}} becomes:
-- [[{
-- test = {
-- "Testing..."
-- }
-- }]]
-- This supports tables as keys, but not circular references.
-- It performs poorly with multiple references as it writes out the full
-- table each time.
-- The indent field specifies a indentation string, it defaults to a tab.
-- Use the empty string to disable indentation.
-- The dumped and level arguments are internal-only.
function dump(o, indent, nested, level)
local t = type(o)
if not level and t == "userdata" then
-- when userdata (e.g. player) is passed directly, print its metatable:
return "userdata metatable: " .. dump(getmetatable(o))
end
if t ~= "table" then
return basic_dump(o)
end
-- Contains table -> true/nil of currently nested tables
nested = nested or {}
if nested[o] then
return "<circular reference>"
end
nested[o] = true
indent = indent or "\t"
level = level or 1
local ret = {}
local dumped_indexes = {}
for i, v in ipairs(o) do
ret[#ret + 1] = dump(v, indent, nested, level + 1)
dumped_indexes[i] = true
end
for k, v in pairs(o) do
if not dumped_indexes[k] then
if type(k) ~= "string" or not is_valid_identifier(k) then
k = "["..dump(k, indent, nested, level + 1).."]"
end
v = dump(v, indent, nested, level + 1)
ret[#ret + 1] = k.." = "..v
end
end
nested[o] = nil
if indent ~= "" then
local indent_str = "\n"..string.rep(indent, level)
local end_indent_str = "\n"..string.rep(indent, level - 1)
return string.format("{%s%s%s}",
indent_str,
table.concat(ret, ","..indent_str),
end_indent_str)
end
return "{"..table.concat(ret, ", ").."}"
end
--------------------------------------------------------------------------------
function string.split(str, delim, include_empty, max_splits, sep_is_pattern)
delim = delim or ","
max_splits = max_splits or -2
local items = {}
local pos, len = 1, #str
local plain = not sep_is_pattern
max_splits = max_splits + 1
repeat
local np, npe = string_find(str, delim, pos, plain)
np, npe = (np or (len+1)), (npe or (len+1))
if (not np) or (max_splits == 1) then
np = len + 1
npe = np
end
local s = string_sub(str, pos, np - 1)
if include_empty or (s ~= "") then
max_splits = max_splits - 1
items[#items + 1] = s
end
pos = npe + 1
until (max_splits == 0) or (pos > (len + 1))
return items
end
--------------------------------------------------------------------------------
function table.indexof(list, val)
for i, v in ipairs(list) do
if v == val then
return i
end
end
return -1
end
--------------------------------------------------------------------------------
function string:trim()
return (self:gsub("^%s*(.-)%s*$", "%1"))
end
--------------------------------------------------------------------------------
function math.hypot(x, y)
local t
x = math.abs(x)
y = math.abs(y)
t = math.min(x, y)
x = math.max(x, y)
if x == 0 then return 0 end
t = t / x
return x * math.sqrt(1 + t * t)
end
--------------------------------------------------------------------------------
function math.sign(x, tolerance)
tolerance = tolerance or 0
if x > tolerance then
return 1
elseif x < -tolerance then
return -1
end
return 0
end
--------------------------------------------------------------------------------
function math.factorial(x)
assert(x % 1 == 0 and x >= 0, "factorial expects a non-negative integer")
if x >= 171 then
-- 171! is greater than the biggest double, no need to calculate
return math.huge
end
local v = 1
for k = 2, x do
v = v * k
end
return v
end
function core.formspec_escape(text)
if text ~= nil then
text = string.gsub(text,"\\","\\\\")
text = string.gsub(text,"%]","\\]")
text = string.gsub(text,"%[","\\[")
text = string.gsub(text,";","\\;")
text = string.gsub(text,",","\\,")
end
return text
end
function core.wrap_text(text, max_length, as_table)
local result = {}
local line = {}
if #text <= max_length then
return as_table and {text} or text
end
for word in text:gmatch('%S+') do
local cur_length = #table.concat(line, ' ')
if cur_length > 0 and cur_length + #word + 1 >= max_length then
-- word wouldn't fit on current line, move to next line
table.insert(result, table.concat(line, ' '))
line = {}
end
table.insert(line, word)
end
table.insert(result, table.concat(line, ' '))
return as_table and result or table.concat(result, '\n')
end
--------------------------------------------------------------------------------
if INIT == "game" then
local dirs1 = {9, 18, 7, 12}
local dirs2 = {20, 23, 22, 21}
function core.rotate_and_place(itemstack, placer, pointed_thing,
infinitestacks, orient_flags, prevent_after_place)
orient_flags = orient_flags or {}
local unode = core.get_node_or_nil(pointed_thing.under)
if not unode then
return
end
local undef = core.registered_nodes[unode.name]
if undef and undef.on_rightclick then
return undef.on_rightclick(pointed_thing.under, unode, placer,
itemstack, pointed_thing)
end
local fdir = placer and core.dir_to_facedir(placer:get_look_dir()) or 0
local above = pointed_thing.above
local under = pointed_thing.under
local iswall = (above.y == under.y)
local isceiling = not iswall and (above.y < under.y)
if undef and undef.buildable_to then
iswall = false
end
if orient_flags.force_floor then
iswall = false
isceiling = false
elseif orient_flags.force_ceiling then
iswall = false
isceiling = true
elseif orient_flags.force_wall then
iswall = true
isceiling = false
elseif orient_flags.invert_wall then
iswall = not iswall
end
local param2 = fdir
if iswall then
param2 = dirs1[fdir + 1]
elseif isceiling then
if orient_flags.force_facedir then
param2 = 20
else
param2 = dirs2[fdir + 1]
end
else -- place right side up
if orient_flags.force_facedir then
param2 = 0
end
end
local old_itemstack = ItemStack(itemstack)
local new_itemstack = core.item_place_node(itemstack, placer,
pointed_thing, param2, prevent_after_place)
return infinitestacks and old_itemstack or new_itemstack
end
--------------------------------------------------------------------------------
--Wrapper for rotate_and_place() to check for sneak and assume Creative mode
--implies infinite stacks when performing a 6d rotation.
--------------------------------------------------------------------------------
local creative_mode_cache = core.settings:get_bool("creative_mode")
local function is_creative(name)
return creative_mode_cache or
core.check_player_privs(name, {creative = true})
end
core.rotate_node = function(itemstack, placer, pointed_thing)
local name = placer and placer:get_player_name() or ""
local invert_wall = placer and placer:get_player_control().sneak or false
return core.rotate_and_place(itemstack, placer, pointed_thing,
is_creative(name),
{invert_wall = invert_wall}, true)
end
end
--------------------------------------------------------------------------------
function core.explode_table_event(evt)
if evt ~= nil then
local parts = evt:split(":")
if #parts == 3 then
local t = parts[1]:trim()
local r = tonumber(parts[2]:trim())
local c = tonumber(parts[3]:trim())
if type(r) == "number" and type(c) == "number"
and t ~= "INV" then
return {type=t, row=r, column=c}
end
end
end
return {type="INV", row=0, column=0}
end
--------------------------------------------------------------------------------
function core.explode_textlist_event(evt)
if evt ~= nil then
local parts = evt:split(":")
if #parts == 2 then
local t = parts[1]:trim()
local r = tonumber(parts[2]:trim())
if type(r) == "number" and t ~= "INV" then
return {type=t, index=r}
end
end
end
return {type="INV", index=0}
end
--------------------------------------------------------------------------------
function core.explode_scrollbar_event(evt)
local retval = core.explode_textlist_event(evt)
retval.value = retval.index
retval.index = nil
return retval
end
--------------------------------------------------------------------------------
function core.rgba(r, g, b, a)
return a and string.format("#%02X%02X%02X%02X", r, g, b, a) or
string.format("#%02X%02X%02X", r, g, b)
end
--------------------------------------------------------------------------------
function core.pos_to_string(pos, decimal_places)
local x = pos.x
local y = pos.y
local z = pos.z
if decimal_places ~= nil then
x = string.format("%." .. decimal_places .. "f", x)
y = string.format("%." .. decimal_places .. "f", y)
z = string.format("%." .. decimal_places .. "f", z)
end
return "(" .. x .. "," .. y .. "," .. z .. ")"
end
--------------------------------------------------------------------------------
function core.string_to_pos(value)
if value == nil then
return nil
end
local p = {}
p.x, p.y, p.z = string.match(value, "^([%d.-]+)[, ] *([%d.-]+)[, ] *([%d.-]+)$")
if p.x and p.y and p.z then
p.x = tonumber(p.x)
p.y = tonumber(p.y)
p.z = tonumber(p.z)
return p
end
p = {}
p.x, p.y, p.z = string.match(value, "^%( *([%d.-]+)[, ] *([%d.-]+)[, ] *([%d.-]+) *%)$")
if p.x and p.y and p.z then
p.x = tonumber(p.x)
p.y = tonumber(p.y)
p.z = tonumber(p.z)
return p
end
return nil
end
--------------------------------------------------------------------------------
function core.string_to_area(value)
local p1, p2 = unpack(value:split(") ("))
if p1 == nil or p2 == nil then
return nil
end
p1 = core.string_to_pos(p1 .. ")")
p2 = core.string_to_pos("(" .. p2)
if p1 == nil or p2 == nil then
return nil
end
return p1, p2
end
local function test_string_to_area()
local p1, p2 = core.string_to_area("(10.0, 5, -2) ( 30.2, 4, -12.53)")
assert(p1.x == 10.0 and p1.y == 5 and p1.z == -2)
assert(p2.x == 30.2 and p2.y == 4 and p2.z == -12.53)
p1, p2 = core.string_to_area("(10.0, 5, -2 30.2, 4, -12.53")
assert(p1 == nil and p2 == nil)
p1, p2 = core.string_to_area("(10.0, 5,) -2 fgdf2, 4, -12.53")
assert(p1 == nil and p2 == nil)
end
test_string_to_area()
--------------------------------------------------------------------------------
function table.copy(t, seen)
local n = {}
seen = seen or {}
seen[t] = n
for k, v in pairs(t) do
n[(type(k) == "table" and (seen[k] or table.copy(k, seen))) or k] =
(type(v) == "table" and (seen[v] or table.copy(v, seen))) or v
end
return n
end
function table.insert_all(t, other)
for i=1, #other do
t[#t + 1] = other[i]
end
return t
end
function table.key_value_swap(t)
local ti = {}
for k,v in pairs(t) do
ti[v] = k
end
return ti
end
function table.shuffle(t, from, to, random)
from = from or 1
to = to or #t
random = random or math.random
local n = to - from + 1
while n > 1 do
local r = from + n-1
local l = from + random(0, n-1)
t[l], t[r] = t[r], t[l]
n = n-1
end
end
--------------------------------------------------------------------------------
-- mainmenu only functions
--------------------------------------------------------------------------------
if INIT == "mainmenu" then
function core.get_game(index)
local games = core.get_games()
if index > 0 and index <= #games then
return games[index]
end
return nil
end
end
if INIT == "client" or INIT == "mainmenu" then
function fgettext_ne(text, ...)
text = core.gettext(text)
local arg = {n=select('#', ...), ...}
if arg.n >= 1 then
-- Insert positional parameters ($1, $2, ...)
local result = ''
local pos = 1
while pos <= text:len() do
local newpos = text:find('[$]', pos)
if newpos == nil then
result = result .. text:sub(pos)
pos = text:len() + 1
else
local paramindex =
tonumber(text:sub(newpos+1, newpos+1))
result = result .. text:sub(pos, newpos-1)
.. tostring(arg[paramindex])
pos = newpos + 2
end
end
text = result
end
return text
end
function fgettext(text, ...)
return core.formspec_escape(fgettext_ne(text, ...))
end
end
local ESCAPE_CHAR = string.char(0x1b)
function core.get_color_escape_sequence(color)
return ESCAPE_CHAR .. "(c@" .. color .. ")"
end
function core.get_background_escape_sequence(color)
return ESCAPE_CHAR .. "(b@" .. color .. ")"
end
function core.colorize(color, message)
local lines = tostring(message):split("\n", true)
local color_code = core.get_color_escape_sequence(color)
for i, line in ipairs(lines) do
lines[i] = color_code .. line
end
return table.concat(lines, "\n") .. core.get_color_escape_sequence("#ffffff")
end
function core.strip_foreground_colors(str)
return (str:gsub(ESCAPE_CHAR .. "%(c@[^)]+%)", ""))
end
function core.strip_background_colors(str)
return (str:gsub(ESCAPE_CHAR .. "%(b@[^)]+%)", ""))
end
function core.strip_colors(str)
return (str:gsub(ESCAPE_CHAR .. "%([bc]@[^)]+%)", ""))
end
function core.translate(textdomain, str, ...)
local start_seq
if textdomain == "" then
start_seq = ESCAPE_CHAR .. "T"
else
start_seq = ESCAPE_CHAR .. "(T@" .. textdomain .. ")"
end
local arg = {n=select('#', ...), ...}
local end_seq = ESCAPE_CHAR .. "E"
local arg_index = 1
local translated = str:gsub("@(.)", function(matched)
local c = string.byte(matched)
if string.byte("1") <= c and c <= string.byte("9") then
local a = c - string.byte("0")
if a ~= arg_index then
error("Escape sequences in string given to core.translate " ..
"are not in the correct order: got @" .. matched ..
"but expected @" .. tostring(arg_index))
end
if a > arg.n then
error("Not enough arguments provided to core.translate")
end
arg_index = arg_index + 1
return ESCAPE_CHAR .. "F" .. arg[a] .. ESCAPE_CHAR .. "E"
elseif matched == "n" then
return "\n"
else
return matched
end
end)
if arg_index < arg.n + 1 then
error("Too many arguments provided to core.translate")
end
return start_seq .. translated .. end_seq
end
function core.get_translator(textdomain)
return function(str, ...) return core.translate(textdomain or "", str, ...) end
end
--------------------------------------------------------------------------------
-- Returns the exact coordinate of a pointed surface
--------------------------------------------------------------------------------
function core.pointed_thing_to_face_pos(placer, pointed_thing)
-- Avoid crash in some situations when player is inside a node, causing
-- 'above' to equal 'under'.
if vector.equals(pointed_thing.above, pointed_thing.under) then
return pointed_thing.under
end
local eye_height = placer:get_properties().eye_height
local eye_offset_first = placer:get_eye_offset()
local node_pos = pointed_thing.under
local camera_pos = placer:get_pos()
local pos_off = vector.multiply(
vector.subtract(pointed_thing.above, node_pos), 0.5)
local look_dir = placer:get_look_dir()
local offset, nc
local oc = {}
for c, v in pairs(pos_off) do
if nc or v == 0 then
oc[#oc + 1] = c
else
offset = v
nc = c
end
end
local fine_pos = {[nc] = node_pos[nc] + offset}
camera_pos.y = camera_pos.y + eye_height + eye_offset_first.y / 10
local f = (node_pos[nc] + offset - camera_pos[nc]) / look_dir[nc]
for i = 1, #oc do
fine_pos[oc[i]] = camera_pos[oc[i]] + look_dir[oc[i]] * f
end
return fine_pos
end
function core.string_to_privs(str, delim)
assert(type(str) == "string")
delim = delim or ','
local privs = {}
for _, priv in pairs(string.split(str, delim)) do
privs[priv:trim()] = true
end
return privs
end
function core.privs_to_string(privs, delim)
assert(type(privs) == "table")
delim = delim or ','
local list = {}
for priv, bool in pairs(privs) do
if bool then
list[#list + 1] = priv
end
end
return table.concat(list, delim)
end

View File

@ -0,0 +1,242 @@
vector = {}
function vector.new(a, b, c)
if type(a) == "table" then
assert(a.x and a.y and a.z, "Invalid vector passed to vector.new()")
return {x=a.x, y=a.y, z=a.z}
elseif a then
assert(b and c, "Invalid arguments for vector.new()")
return {x=a, y=b, z=c}
end
return {x=0, y=0, z=0}
end
function vector.equals(a, b)
return a.x == b.x and
a.y == b.y and
a.z == b.z
end
function vector.length(v)
return math.hypot(v.x, math.hypot(v.y, v.z))
end
function vector.normalize(v)
local len = vector.length(v)
if len == 0 then
return {x=0, y=0, z=0}
else
return vector.divide(v, len)
end
end
function vector.floor(v)
return {
x = math.floor(v.x),
y = math.floor(v.y),
z = math.floor(v.z)
}
end
function vector.round(v)
return {
x = math.floor(v.x + 0.5),
y = math.floor(v.y + 0.5),
z = math.floor(v.z + 0.5)
}
end
function vector.apply(v, func)
return {
x = func(v.x),
y = func(v.y),
z = func(v.z)
}
end
function vector.distance(a, b)
local x = a.x - b.x
local y = a.y - b.y
local z = a.z - b.z
return math.hypot(x, math.hypot(y, z))
end
function vector.direction(pos1, pos2)
return vector.normalize({
x = pos2.x - pos1.x,
y = pos2.y - pos1.y,
z = pos2.z - pos1.z
})
end
function vector.angle(a, b)
local dotp = vector.dot(a, b)
local cp = vector.cross(a, b)
local crossplen = vector.length(cp)
return math.atan2(crossplen, dotp)
end
function vector.dot(a, b)
return a.x * b.x + a.y * b.y + a.z * b.z
end
function vector.cross(a, b)
return {
x = a.y * b.z - a.z * b.y,
y = a.z * b.x - a.x * b.z,
z = a.x * b.y - a.y * b.x
}
end
function vector.add(a, b)
if type(b) == "table" then
return {x = a.x + b.x,
y = a.y + b.y,
z = a.z + b.z}
else
return {x = a.x + b,
y = a.y + b,
z = a.z + b}
end
end
function vector.subtract(a, b)
if type(b) == "table" then
return {x = a.x - b.x,
y = a.y - b.y,
z = a.z - b.z}
else
return {x = a.x - b,
y = a.y - b,
z = a.z - b}
end
end
function vector.multiply(a, b)
if type(b) == "table" then
return {x = a.x * b.x,
y = a.y * b.y,
z = a.z * b.z}
else
return {x = a.x * b,
y = a.y * b,
z = a.z * b}
end
end
function vector.divide(a, b)
if type(b) == "table" then
return {x = a.x / b.x,
y = a.y / b.y,
z = a.z / b.z}
else
return {x = a.x / b,
y = a.y / b,
z = a.z / b}
end
end
function vector.offset(v, x, y, z)
return {x = v.x + x,
y = v.y + y,
z = v.z + z}
end
function vector.sort(a, b)
return {x = math.min(a.x, b.x), y = math.min(a.y, b.y), z = math.min(a.z, b.z)},
{x = math.max(a.x, b.x), y = math.max(a.y, b.y), z = math.max(a.z, b.z)}
end
local function sin(x)
if x % math.pi == 0 then
return 0
else
return math.sin(x)
end
end
local function cos(x)
if x % math.pi == math.pi / 2 then
return 0
else
return math.cos(x)
end
end
function vector.rotate_around_axis(v, axis, angle)
local cosangle = cos(angle)
local sinangle = sin(angle)
axis = vector.normalize(axis)
-- https://en.wikipedia.org/wiki/Rodrigues%27_rotation_formula
local dot_axis = vector.multiply(axis, vector.dot(axis, v))
local cross = vector.cross(v, axis)
return vector.new(
cross.x * sinangle + (v.x - dot_axis.x) * cosangle + dot_axis.x,
cross.y * sinangle + (v.y - dot_axis.y) * cosangle + dot_axis.y,
cross.z * sinangle + (v.z - dot_axis.z) * cosangle + dot_axis.z
)
end
function vector.rotate(v, rot)
local sinpitch = sin(-rot.x)
local sinyaw = sin(-rot.y)
local sinroll = sin(-rot.z)
local cospitch = cos(rot.x)
local cosyaw = cos(rot.y)
local cosroll = math.cos(rot.z)
-- Rotation matrix that applies yaw, pitch and roll
local matrix = {
{
sinyaw * sinpitch * sinroll + cosyaw * cosroll,
sinyaw * sinpitch * cosroll - cosyaw * sinroll,
sinyaw * cospitch,
},
{
cospitch * sinroll,
cospitch * cosroll,
-sinpitch,
},
{
cosyaw * sinpitch * sinroll - sinyaw * cosroll,
cosyaw * sinpitch * cosroll + sinyaw * sinroll,
cosyaw * cospitch,
},
}
-- Compute matrix multiplication: `matrix` * `v`
return vector.new(
matrix[1][1] * v.x + matrix[1][2] * v.y + matrix[1][3] * v.z,
matrix[2][1] * v.x + matrix[2][2] * v.y + matrix[2][3] * v.z,
matrix[3][1] * v.x + matrix[3][2] * v.y + matrix[3][3] * v.z
)
end
function vector.dir_to_rotation(forward, up)
forward = vector.normalize(forward)
local rot = {x = math.asin(forward.y), y = -math.atan2(forward.x, forward.z), z = 0}
if not up then
return rot
end
assert(vector.dot(forward, up) < 0.000001,
"Invalid vectors passed to vector.dir_to_rotation().")
up = vector.normalize(up)
-- Calculate vector pointing up with roll = 0, just based on forward vector.
local forwup = vector.rotate({x = 0, y = 1, z = 0}, rot)
-- 'forwup' and 'up' are now in a plane with 'forward' as normal.
-- The angle between them is the absolute of the roll value we're looking for.
rot.z = vector.angle(forwup, up)
-- Since vector.angle never returns a negative value or a value greater
-- than math.pi, rot.z has to be inverted sometimes.
-- To determine wether this is the case, we rotate the up vector back around
-- the forward vector and check if it worked out.
local back = vector.rotate_around_axis(up, forward, -rot.z)
-- We don't use vector.equals for this because of floating point imprecision.
if (back.x - forwup.x) * (back.x - forwup.x) +
(back.y - forwup.y) * (back.y - forwup.y) +
(back.z - forwup.z) * (back.z - forwup.z) > 0.0000001 then
rot.z = -rot.z
end
return rot
end

View File

@ -0,0 +1,262 @@
-- Minetest: builtin/misc.lua
--
-- Misc. API functions
--
function core.check_player_privs(name, ...)
if core.is_player(name) then
name = name:get_player_name()
elseif type(name) ~= "string" then
error("core.check_player_privs expects a player or playername as " ..
"argument.", 2)
end
local requested_privs = {...}
local player_privs = core.get_player_privs(name)
local missing_privileges = {}
if type(requested_privs[1]) == "table" then
-- We were provided with a table like { privA = true, privB = true }.
for priv, value in pairs(requested_privs[1]) do
if value and not player_privs[priv] then
missing_privileges[#missing_privileges + 1] = priv
end
end
else
-- Only a list, we can process it directly.
for key, priv in pairs(requested_privs) do
if not player_privs[priv] then
missing_privileges[#missing_privileges + 1] = priv
end
end
end
if #missing_privileges > 0 then
return false, missing_privileges
end
return true, ""
end
function core.send_join_message(player_name)
if not core.is_singleplayer() then
core.chat_send_all("*** " .. player_name .. " joined the game.")
end
end
function core.send_leave_message(player_name, timed_out)
local announcement = "*** " .. player_name .. " left the game."
if timed_out then
announcement = announcement .. " (timed out)"
end
core.chat_send_all(announcement)
end
core.register_on_joinplayer(function(player)
local player_name = player:get_player_name()
if not core.is_singleplayer() then
local status = core.get_server_status(player_name, true)
if status and status ~= "" then
core.chat_send_player(player_name, status)
end
end
core.send_join_message(player_name)
end)
core.register_on_leaveplayer(function(player, timed_out)
local player_name = player:get_player_name()
core.send_leave_message(player_name, timed_out)
end)
function core.is_player(player)
-- a table being a player is also supported because it quacks sufficiently
-- like a player if it has the is_player function
local t = type(player)
return (t == "userdata" or t == "table") and
type(player.is_player) == "function" and player:is_player()
end
function core.player_exists(name)
return core.get_auth_handler().get_auth(name) ~= nil
end
-- Returns two position vectors representing a box of `radius` in each
-- direction centered around the player corresponding to `player_name`
function core.get_player_radius_area(player_name, radius)
local player = core.get_player_by_name(player_name)
if player == nil then
return nil
end
local p1 = player:get_pos()
local p2 = p1
if radius then
p1 = vector.subtract(p1, radius)
p2 = vector.add(p2, radius)
end
return p1, p2
end
function core.hash_node_position(pos)
return (pos.z + 32768) * 65536 * 65536
+ (pos.y + 32768) * 65536
+ pos.x + 32768
end
function core.get_position_from_hash(hash)
local pos = {}
pos.x = (hash % 65536) - 32768
hash = math.floor(hash / 65536)
pos.y = (hash % 65536) - 32768
hash = math.floor(hash / 65536)
pos.z = (hash % 65536) - 32768
return pos
end
function core.get_item_group(name, group)
if not core.registered_items[name] or not
core.registered_items[name].groups[group] then
return 0
end
return core.registered_items[name].groups[group]
end
function core.get_node_group(name, group)
core.log("deprecated", "Deprecated usage of get_node_group, use get_item_group instead")
return core.get_item_group(name, group)
end
function core.setting_get_pos(name)
local value = core.settings:get(name)
if not value then
return nil
end
return core.string_to_pos(value)
end
-- To be overriden by protection mods
function core.is_protected(pos, name)
return false
end
function core.record_protection_violation(pos, name)
for _, func in pairs(core.registered_on_protection_violation) do
func(pos, name)
end
end
-- To be overridden by Creative mods
local creative_mode_cache = core.settings:get_bool("creative_mode")
function core.is_creative_enabled(name)
return creative_mode_cache
end
-- Checks if specified volume intersects a protected volume
function core.is_area_protected(minp, maxp, player_name, interval)
-- 'interval' is the largest allowed interval for the 3D lattice of checks.
-- Compute the optimal float step 'd' for each axis so that all corners and
-- borders are checked. 'd' will be smaller or equal to 'interval'.
-- Subtracting 1e-4 ensures that the max co-ordinate will be reached by the
-- for loop (which might otherwise not be the case due to rounding errors).
-- Default to 4
interval = interval or 4
local d = {}
for _, c in pairs({"x", "y", "z"}) do
if minp[c] > maxp[c] then
-- Repair positions: 'minp' > 'maxp'
local tmp = maxp[c]
maxp[c] = minp[c]
minp[c] = tmp
end
if maxp[c] > minp[c] then
d[c] = (maxp[c] - minp[c]) /
math.ceil((maxp[c] - minp[c]) / interval) - 1e-4
else
d[c] = 1 -- Any value larger than 0 to avoid division by zero
end
end
for zf = minp.z, maxp.z, d.z do
local z = math.floor(zf + 0.5)
for yf = minp.y, maxp.y, d.y do
local y = math.floor(yf + 0.5)
for xf = minp.x, maxp.x, d.x do
local x = math.floor(xf + 0.5)
local pos = {x = x, y = y, z = z}
if core.is_protected(pos, player_name) then
return pos
end
end
end
end
return false
end
local raillike_ids = {}
local raillike_cur_id = 0
function core.raillike_group(name)
local id = raillike_ids[name]
if not id then
raillike_cur_id = raillike_cur_id + 1
raillike_ids[name] = raillike_cur_id
id = raillike_cur_id
end
return id
end
-- HTTP callback interface
function core.http_add_fetch(httpenv)
httpenv.fetch = function(req, callback)
local handle = httpenv.fetch_async(req)
local function update_http_status()
local res = httpenv.fetch_async_get(handle)
if res.completed then
callback(res)
else
core.after(0, update_http_status)
end
end
core.after(0, update_http_status)
end
return httpenv
end
function core.close_formspec(player_name, formname)
return core.show_formspec(player_name, formname, "")
end
function core.cancel_shutdown_requests()
core.request_shutdown("", false, -1)
end

View File

@ -0,0 +1,43 @@
fixture("minetest")
local players = {}
_G.minetest.check_player_privs = function(player_or_name, ...)
local player_privs
if type(player_or_name) == "table" then
player_privs = player_or_name._privs
else
player_privs = players[player_or_name]._privs
end
local missing_privs = {}
local has_priv = false
local arg={...}
for _,priv in ipairs(arg) do
if player_privs[priv] then
has_priv = true
else
table.insert(missing_privs, priv)
end
end
return has_priv, missing_privs
end
_G.minetest.get_player_by_name = function(name)
return players[name]
end
_G.Player = function(name, privs)
local player = {
_name = name or "SX",
_privs = privs or { test_priv=1 },
get_player_control = function(self)
return {}
end,
get_player_name = function(self)
return self._name
end
}
table.insert(players, player)
return player
end

View File

@ -0,0 +1,18 @@
fixture("minetest")
_G.ProtectedPos = function()
return { x = 123, y = 123, z = 123 }
end
_G.UnprotectedPos = function()
return { x = -123, y = -123, z = -123 }
end
minetest.is_protected = function(pos, name)
return pos.x == 123 and pos.y == 123 and pos.z == 123
end
minetest.record_protection_violation = function(pos, name)
-- noop
end

23
technic/spec/fixtures/network.lua vendored Normal file
View File

@ -0,0 +1,23 @@
_G.technic = {}
_G.technic.S = string.format
_G.technic.getter = function(...) return "" end
_G.technic.get_or_load_node = minetest.get_node
_G.technic.digilines = {
rules = {
-- digilines.rules.default
{x= 1,y= 0,z= 0},{x=-1,y= 0,z= 0}, -- along x beside
{x= 0,y= 0,z= 1},{x= 0,y= 0,z=-1}, -- along z beside
{x= 1,y= 1,z= 0},{x=-1,y= 1,z= 0}, -- 1 node above along x diagonal
{x= 0,y= 1,z= 1},{x= 0,y= 1,z=-1}, -- 1 node above along z diagonal
{x= 1,y=-1,z= 0},{x=-1,y=-1,z= 0}, -- 1 node below along x diagonal
{x= 0,y=-1,z= 1},{x= 0,y=-1,z=-1}, -- 1 node below along z diagonal
-- added rules for digi cable
{x = 0, y = -1, z = 0}, -- along y below
}
}
sourcefile("register")
technic.register_tier("LV", "Busted LV")
technic.register_tier("MV", "Busted MV")
technic.register_tier("HV", "Busted HV")

2
technic/spec/fixtures/pipeworks.lua vendored Normal file
View File

@ -0,0 +1,2 @@
_G.pipeworks = {}

View File

@ -0,0 +1,417 @@
dofile("spec/test_helpers.lua")
--[[
Technic network unit tests.
Execute busted at technic source directory.
--]]
-- Load fixtures required by tests
fixture("minetest")
fixture("minetest/player")
fixture("minetest/protection")
fixture("pipeworks")
fixture("network")
sourcefile("machines/network")
sourcefile("machines/register/cables")
sourcefile("machines/LV/cables")
sourcefile("machines/MV/cables")
sourcefile("machines/HV/cables")
sourcefile("machines/register/generator")
sourcefile("machines/HV/generator")
world.layout({
{{x=100,y=100,z=100}, "technic:lv_cable"},
{{x=101,y=100,z=100}, "technic:lv_cable"},
{{x=102,y=100,z=100}, "technic:lv_cable"},
{{x=103,y=100,z=100}, "technic:lv_cable"},
{{x=104,y=100,z=100}, "technic:lv_cable"},
{{x=100,y=101,z=100}, "technic:switching_station"},
{{x=100,y=200,z=100}, "technic:mv_cable"},
{{x=101,y=200,z=100}, "technic:mv_cable"},
{{x=102,y=200,z=100}, "technic:mv_cable"},
{{x=103,y=200,z=100}, "technic:mv_cable"},
{{x=104,y=200,z=100}, "technic:mv_cable"},
{{x=100,y=201,z=100}, "technic:switching_station"},
{{x=100,y=300,z=100}, "technic:hv_cable"},
{{x=101,y=300,z=100}, "technic:hv_cable"},
{{x=102,y=300,z=100}, "technic:hv_cable"},
{{x=103,y=300,z=100}, "technic:hv_cable"},
{{x=104,y=300,z=100}, "technic:hv_cable"},
{{x=100,y=301,z=100}, "technic:switching_station"},
-- For network lookup function -> returns correct network for position
{{x=100,y=500,z=100}, "technic:hv_cable"},
{{x=101,y=500,z=100}, "technic:hv_cable"},
{{x=102,y=500,z=100}, "technic:hv_cable"},
{{x=103,y=500,z=100}, "technic:hv_cable"},
{{x=104,y=500,z=100}, "technic:hv_cable"},
{{x=100,y=501,z=100}, "technic:hv_generator"},
{{x=101,y=501,z=100}, "technic:hv_cable"},
{{x=102,y=501,z=100}, "technic:switching_station"},
{{x=100,y=502,z=100}, "technic:hv_cable"},
{{x=101,y=502,z=100}, "technic:hv_cable"},
})
describe("Power network helper", function()
-- Simple network position fixtures
local net_id = 65536
local pos = { x = -32768, y = -32767, z = -32768 }
local sw_pos = { x = -32768, y = -32766, z = -32768 }
describe("network lookup functions", function()
it("does not fail if network missing", function()
assert.is_nil( technic.remove_network(9999) )
end)
it("returns correct position for network", function()
assert.same(pos, technic.network2pos(net_id) )
assert.same(sw_pos, technic.network2sw_pos(net_id) )
end)
it("returns correct network for position", function()
local net_id = technic.create_network({x=100,y=501,z=100})
assert.same(net_id, technic.pos2network({x=100,y=500,z=100}) )
assert.same(net_id, technic.sw_pos2network({x=100,y=501,z=100}) )
end)
it("returns nil tier for empty position", function()
assert.is_nil(technic.sw_pos2tier({x=9999,y=9999,z=9999}))
end)
it("returns correct tier for switching station position", function()
-- World is defined in fixtures/network.lua
assert.same("LV", technic.sw_pos2tier({x=100,y=101,z=100}))
assert.same("MV", technic.sw_pos2tier({x=100,y=201,z=100}))
assert.same("HV", technic.sw_pos2tier({x=100,y=301,z=100}))
end)
end)
describe("network constructors/destructors", function()
-- Build network
local net_id = technic.create_network({x=100,y=501,z=100})
assert.is_number(net_id)
it("creates network", function()
assert.is_hashed(technic.networks[net_id])
end)
it("builds network", function()
local net = technic.networks[net_id]
-- Network table is valid
assert.is_indexed(net.PR_nodes)
assert.is_indexed(net.RE_nodes)
assert.is_indexed(net.BA_nodes)
assert.equals(9, count(net.all_nodes))
assert.is_hashed(net.all_nodes)
end)
it("does not add duplicates to network", function()
local net = technic.networks[net_id]
-- Local network table is still valid
assert.equals(1, count(net.PR_nodes))
assert.equals(0, count(net.RE_nodes))
assert.equals(0, count(net.BA_nodes))
assert.equals(9, count(net.all_nodes))
-- FIXME: This might be wrong if technic.cables should contain only cables and not machines
assert.equals(9, count(technic.cables))
end)
it("removes network", function()
technic.remove_network(net_id)
assert.is_nil(technic.networks[net_id])
-- TODO: Verify that there's no lefover positions in technic.cables
end)
end)
--[[ TODO:
technic.remove_network_node
--]]
describe("Power network timeout functions technic.touch_node and technic.get_timeout", function()
it("returns zero if no data available", function()
assert.equals(0,
technic.get_timeout("LV", {x=9999,y=9999,z=9999})
)
assert.equals(0,
technic.get_timeout("HV", {x=9999,y=9999,z=9999})
)
end)
it("returns timeout if data is available", function()
technic.touch_node("LV", {x=123,y=123,z=123}, 42)
assert.equals(42,
technic.get_timeout("LV", {x=123,y=123,z=123})
)
technic.touch_node("HV", {x=123,y=123,z=123}, 74)
assert.equals(74,
technic.get_timeout("HV", {x=123,y=123,z=123})
)
end)
end)
end)
-- Clean up, left following here just for easy copy pasting stuff from previous proj
--[[
describe("Metatool API protection", function()
it("metatool.is_protected bypass privileges", function()
local value = metatool.is_protected(ProtectedPos(), Player(), "test_priv", true)
assert.equals(false, value)
end)
it("metatool.is_protected no bypass privileges", function()
local value = metatool.is_protected(ProtectedPos(), Player(), "test_priv2", true)
assert.equals(true, value)
end)
it("metatool.is_protected bypass privileges, unprotected", function()
local value = metatool.is_protected(UnprotectedPos(), Player(), "test_priv", true)
assert.equals(false, value)
end)
it("metatool.is_protected no bypass privileges, unprotected", function()
local value = metatool.is_protected(UnprotectedPos(), Player(), "test_priv2", true)
assert.equals(false, value)
end)
end)
describe("Metatool API tool namespace", function()
it("Create invalid namespace", function()
local tool = { ns = metatool.ns, name = 'invalid' }
local value = tool:ns("invalid", {
testkey = "testvalue"
})
assert.is_nil(metatool:ns("testns"))
end)
it("Get nonexistent namespace", function()
assert.is_nil(metatool.ns("nonexistent"))
end)
it("Create tool namespace", function()
-- FIXME: Hack to get fake tool available, replace with real tool
local tool = { ns = metatool.ns, name = 'mytool' }
metatool.tools["metatool:mytool"] = tool
-- Actual tests
local value = tool:ns({
testkey = "testvalue"
})
local expected = {
testkey = "testvalue"
}
assert.same(expected, metatool.ns("mytool"))
end)
end)
describe("Metatool API tool registration", function()
it("Register tool default configuration", function()
-- Tool registration
local definition = {
description = 'UnitTestTool Description',
name = 'UnitTestTool',
texture = 'utt.png',
recipe = {{'air'},{'air'},{'air'}},
on_read_node = function(tooldef, player, pointed_thing, node, pos)
local data, group = tooldef:copy(node, pos, player)
return data, group, "on_read_node description"
end,
on_write_node = function(tooldef, data, group, player, pointed_thing, node, pos)
tooldef:paste(node, pos, player, data, group)
end,
}
local tool = metatool:register_tool('testtool0', definition)
assert.is_table(tool)
assert.equals("metatool:testtool0", tool.name)
assert.is_table(tool)
assert.equals(definition.description, tool.description)
assert.equals(definition.name, tool.nice_name)
assert.equals(definition.on_read_node, tool.on_read_node)
assert.equals(definition.on_write_node, tool.on_write_node)
-- Test configurable tool attributes
assert.is_nil(tool.privs)
assert.same({}, tool.settings)
-- Namespace creation
local mult = function(a,b) return a * b end
tool:ns({ k1 = "v1", fn = mult })
-- Retrieve namespace and and execute tests
local ns = metatool.ns("testtool0")
assert.same({ k1 = "v1", fn = mult }, ns)
assert.equals(8, ns.fn(2,4))
end)
it("Register tool with configuration", function()
-- Tool registration
local definition = {
description = 'UnitTestTool Description',
name = 'UnitTestTool',
texture = 'utt.png',
recipe = {{'air'},{'air'},{'air'}},
on_read_node = function(tooldef, player, pointed_thing, node, pos)
local data, group = tooldef:copy(node, pos, player)
return data, group, "on_read_node description"
end,
on_write_node = function(tooldef, data, group, player, pointed_thing, node, pos)
tooldef:paste(node, pos, player, data, group)
end,
}
local tool = metatool:register_tool('testtool2', definition)
assert.is_table(tool)
assert.equals("metatool:testtool2", tool.name)
assert.is_table(tool)
assert.equals(definition.description, tool.description)
assert.equals(definition.name, tool.nice_name)
assert.equals(definition.on_read_node, tool.on_read_node)
assert.equals(definition.on_write_node, tool.on_write_node)
-- Test configurable tool attributes
assert.equals("test_testtool2_privs", tool.privs)
local expected_settings = {
extra_config_key = "testtool2_extra_config_value",
}
assert.same(expected_settings, tool.settings)
-- Namespace creation
local sum = function(a,b) return a + b end
tool:ns({ k1 = "v1", fn = sum })
-- Retrieve namespace and and execute tests
local ns = metatool.ns("testtool2")
assert.same({ k1 = "v1", fn = sum }, ns)
assert.equals(9, ns.fn(2,7))
end)
end)
describe("Metatool API node registration", function()
it("Register node default configuration", function()
local tool = metatool.tool("testtool0")
assert.is_table(tool)
assert.equals("metatool:testtool0", tool.name)
assert.is_table(tool)
local definition = {
name = 'testnode1',
nodes = {
"testnode1",
"nonexistent1",
"testnode2",
"nonexistent2",
},
tooldef = {
group = 'test node',
protection_bypass_write = "default_bypass_write_priv",
copy = function(node, pos, player)
print("nodedef copy callback executed")
end,
paste = function(node, pos, player, data)
print("nodedef paste callback executed")
end,
}
}
tool:load_node_definition(definition)
assert.is_table(tool.nodes)
assert.is_table(tool.nodes.testnode1)
assert.is_table(tool.nodes.testnode2)
assert.is_nil(tool.nodes.nonexistent1)
assert.is_nil(tool.nodes.nonexistent2)
assert.is_function(tool.nodes.testnode1.before_read)
assert.is_function(tool.nodes.testnode2.before_write)
assert.equals(definition.tooldef.copy, tool.nodes.testnode1.copy)
assert.equals(definition.tooldef.paste, tool.nodes.testnode2.paste)
assert.equals("default_bypass_write_priv", definition.tooldef.protection_bypass_write)
local expected_settings = {
protection_bypass_write = "default_bypass_write_priv"
}
assert.same(expected_settings, tool.nodes.testnode1.settings)
assert.same(expected_settings, tool.nodes.testnode2.settings)
end)
it("Register node with configuration", function()
local tool = metatool.tool("testtool2")
assert.is_table(tool)
assert.equals("metatool:testtool2", tool.name)
assert.is_table(tool)
local definition = {
name = 'testnode2',
nodes = {
"testnode1",
"nonexistent1",
"testnode2",
"nonexistent2",
},
tooldef = {
group = 'test node',
protection_bypass_write = "default_bypass_write_priv",
copy = function(node, pos, player)
print("nodedef copy callback executed")
end,
paste = function(node, pos, player, data)
print("nodedef paste callback executed")
end,
}
}
tool:load_node_definition(definition)
assert.is_table(tool.nodes)
assert.is_table(tool.nodes.testnode1)
assert.is_table(tool.nodes.testnode2)
assert.is_nil(tool.nodes.nonexistent1)
assert.is_nil(tool.nodes.nonexistent2)
assert.is_function(tool.nodes.testnode1.before_read)
assert.is_function(tool.nodes.testnode2.before_write)
assert.equals(definition.tooldef.copy, tool.nodes.testnode1.copy)
assert.equals(definition.tooldef.paste, tool.nodes.testnode2.paste)
assert.equals("testtool2_testnode2_bypass_write", tool.nodes.testnode1.protection_bypass_write)
assert.equals("testtool2_testnode2_bypass_write", tool.nodes.testnode2.protection_bypass_write)
assert.equals("testtool2_testnode2_bypass_info", tool.nodes.testnode1.protection_bypass_info)
assert.equals("testtool2_testnode2_bypass_info", tool.nodes.testnode2.protection_bypass_info)
assert.equals("testtool2_testnode2_bypass_read", tool.nodes.testnode1.protection_bypass_read)
assert.equals("testtool2_testnode2_bypass_read", tool.nodes.testnode2.protection_bypass_read)
local expected_settings = {
protection_bypass_write = "testtool2_testnode2_bypass_write",
protection_bypass_info = "testtool2_testnode2_bypass_info",
protection_bypass_read = "testtool2_testnode2_bypass_read",
}
assert.same(expected_settings, tool.nodes.testnode1.settings)
assert.same(expected_settings, tool.nodes.testnode2.settings)
end)
end)
--]]

View File

@ -0,0 +1,147 @@
dofile("spec/test_helpers.lua")
--[[
Technic network unit tests.
Execute busted at technic source directory.
--]]
-- Load fixtures required by tests
fixture("minetest")
fixture("minetest/player")
fixture("minetest/protection")
fixture("pipeworks")
fixture("network")
sourcefile("machines/network")
sourcefile("machines/register/cables")
sourcefile("machines/LV/cables")
sourcefile("machines/MV/cables")
sourcefile("machines/HV/cables")
sourcefile("machines/supply_converter")
function get_network_fixture(sw_pos)
-- Build network
local net_id = technic.create_network(sw_pos)
assert.is_number(net_id)
local net = technic.networks[net_id]
assert.is_table(net)
return net
end
describe("Supply converter", function()
describe("building", function()
world.layout({
{{x=100,y=820,z=100}, "technic:hv_cable"},
{{x=100,y=821,z=100}, "technic:switching_station"},
{{x=101,y=820,z=100}, "technic:hv_cable"},
{{x=101,y=821,z=100}, "technic:supply_converter"},
{{x=102,y=820,z=100}, "technic:hv_cable"},
-- {{x=102,y=821,z=100}, "technic:supply_converter"}, -- This machine is built
{{x=102,y=822,z=100}, "technic:mv_cable"}, -- Supply network for placed SC
{{x=102,y=823,z=100}, "technic:switching_station"}, -- Supply network for placed SC
{{x=102,y=821,z= 99}, "technic:hv_cable"}, -- This should not be added to network
{{x=102,y=821,z=101}, "technic:hv_cable"}, -- This should not be added to network
{{x=103,y=820,z=100}, "technic:hv_cable"},
-- Second network for overload test
{{x=100,y=820,z=102}, "technic:hv_cable"},
{{x=100,y=821,z=102}, "technic:switching_station"},
-- {{x=100,y=820,z=101}, "technic:supply_converter"}, -- This machine is built, it should overload
})
-- Build network
local net = get_network_fixture({x=100,y=821,z=100}) -- Output network for SC
local net2 = get_network_fixture({x=102,y=823,z=100}) -- Input network for SC
local net3 = get_network_fixture({x=100,y=821,z=102}) -- Overload test network (tests currently disabled)
local build_pos = {x=102,y=821,z=100}
local build_pos2 = {x=100,y=820,z=101}
it("does not crash", function()
assert.equals(1, #net.PR_nodes)
assert.equals(1, #net.RE_nodes)
assert.equals(5, count(net.all_nodes))
assert.equals(0, #net2.PR_nodes)
assert.equals(0, #net2.RE_nodes)
assert.equals(1, count(net2.all_nodes))
world.set_node(build_pos, {name="technic:supply_converter",param2=0})
technic.network_node_on_placenode(build_pos, {"HV"}, "technic:supply_converter")
end)
it("is added to network without duplicates", function()
assert.same(build_pos, net.all_nodes[minetest.hash_node_position(build_pos)])
assert.equals(6, count(net.all_nodes))
assert.equals(2, #net.PR_nodes)
assert.equals(2, #net.RE_nodes)
assert.equals(2, count(net2.all_nodes))
assert.equals(1, #net2.PR_nodes)
assert.equals(1, #net2.RE_nodes)
assert.is_nil(technic.is_overloaded(net.id))
assert.is_nil(technic.is_overloaded(net2.id))
end)
it("does not remove connected machines from network", function()
assert.same({x=101,y=821,z=100},net.all_nodes[minetest.hash_node_position({x=101,y=821,z=100})])
end)
it("does not remove networks", function()
assert.is_hashed(technic.networks[net.id])
assert.is_hashed(technic.networks[net2.id])
end)
it("does not add cables to network", function()
assert.is_nil(net.all_nodes[minetest.hash_node_position({x=102,y=821,z=99})])
assert.is_nil(net.all_nodes[minetest.hash_node_position({x=102,y=821,z=101})])
end)
it("overloads network", function()
pending("overload does not work with supply converter")
world.set_node(build_pos2, {name="technic:supply_converter",param2=0})
technic.network_node_on_placenode(build_pos2, {"HV"}, "technic:supply_converter")
assert.not_nil(technic.is_overloaded(net.id))
assert.is_nil(technic.is_overloaded(net2.id))
assert.not_nil(technic.is_overloaded(net3.id))
end)
end)
describe("digging", function()
world.layout({
{{x=100,y=990,z=100}, "technic:hv_cable"},
{{x=100,y=991,z=100}, "technic:switching_station"},
{{x=101,y=990,z=100}, "technic:hv_cable"},
{{x=102,y=990,z=100}, "technic:hv_cable"},
{{x=102,y=991,z=100}, "technic:supply_converter"}, -- This machine is digged
{{x=102,y=991,z=101}, "technic:hv_cable"},
})
-- Build network
local net = get_network_fixture({x=100,y=991,z=100})
local build_pos = {x=102,y=991,z=100}
it("does not crash", function()
assert.equals(1, #net.PR_nodes)
assert.equals(1, #net.RE_nodes)
assert.equals(4, count(net.all_nodes))
world.set_node(build_pos, {name="air",param2=0})
technic.network_node_on_dignode(build_pos, {"HV"}, "technic:supply_converter")
end)
it("is removed from network", function()
assert.is_nil(technic.pos2network(build_pos))
assert.is_nil(technic.cables[minetest.hash_node_position(build_pos)])
assert.is_nil(net.all_nodes[minetest.hash_node_position(build_pos)])
end)
it("does not remove other nodes from network", function()
assert.equals(3, count(net.all_nodes))
end)
it("does not remove network", function()
assert.is_hashed(technic.networks[net.id])
end)
end)
end)

View File

@ -0,0 +1,79 @@
package.path = "../?.lua;./?.lua;machines/?.lua;" .. package.path
local _fixture_path = "spec/fixtures"
function fixture_path(name)
return string.format("%s/%s", _fixture_path, name)
end
local _fixtures = {}
function fixture(name)
if not _fixtures[name] then
dofile(fixture_path(name) .. ".lua")
end
_fixtures[name] = true
end
local _source_path = "."
function source_path(name)
return string.format("%s/%s", _source_path, name)
end
function sourcefile(name)
dofile(source_path(name) .. ".lua")
end
function timeit(count, func, ...)
local socket = require 'socket'
local t1 = socket.gettime() * 1000
for i=0,count do
func(...)
end
local diff = (socket.gettime() * 1000) - t1
local info = debug.getinfo(func,'S')
print(string.format("\nTimeit: %s:%d took %d ticks", info.short_src, info.linedefined, diff))
end
function count(t)
if type(t) == "table" or type(t) == "userdata" then
local c = 0
for a,b in pairs(t) do
c = c + 1
end
return c
end
end
local function sequential(t)
local p = 1
for i,_ in pairs(t) do
if i ~= p then return false end
p = p +1
end
return true
end
local function tabletype(t)
if type(t) == "table" or type(t) == "userdata" then
if count(t) == #t and sequential(t) then
return "array"
else
return "hash"
end
end
end
-- Busted test framework extensions
local assert = require('luassert.assert')
local say = require("say")
local function is_array(_,args) return tabletype(args[1]) == "array" end
say:set("assertion.is_indexed.negative", "Expected %s to be indexed array")
assert:register("assertion", "is_indexed", is_array, "assertion.is_indexed.negative")
local function is_hash(_,args) return tabletype(args[1]) == "hash" end
say:set("assertion.is_hashed.negative", "Expected %s to be hash table")
assert:register("assertion", "is_hashed", is_hash, "assertion.is_hashed.negative")