commit
8f88ec9e16
18
.github/workflows/busted.yml
vendored
Normal file
18
.github/workflows/busted.yml
vendored
Normal 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
|
@ -1,5 +1,10 @@
|
||||
unused_args = false
|
||||
|
||||
-- Exclude regression tests / unit tests
|
||||
exclude_files = {
|
||||
"**/spec/**",
|
||||
}
|
||||
|
||||
globals = {
|
||||
"technic", "technic_cnc", "minetest", "wrench"
|
||||
}
|
||||
|
@ -4,7 +4,9 @@ Technic
|
||||
A mod for [minetest](http://www.minetest.net)
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
[](https://www.gnu.org/licenses/old-licenses/lgpl-2.0.en.html)
|
||||
[](https://content.minetest.net/packages/mt-mods/technic_plus/)
|
||||
|
||||
|
@ -9,13 +9,17 @@ local function power_connector_compat()
|
||||
local digtron_technic_run = minetest.registered_nodes["digtron:power_connector"].technic_run
|
||||
minetest.override_item("digtron:power_connector",{
|
||||
technic_run = function(pos, node)
|
||||
local network_id = technic.cables[minetest.hash_node_position(pos)]
|
||||
local sw_pos = network_id and minetest.get_position_from_hash(network_id)
|
||||
if sw_pos then sw_pos.y = sw_pos.y + 1 end
|
||||
local network_id = technic.pos2network(pos)
|
||||
local sw_pos = network_id and technic.network2sw_pos(network_id)
|
||||
local meta = minetest.get_meta(pos)
|
||||
meta:set_string("HV_network", sw_pos and minetest.pos_to_string(sw_pos) or "")
|
||||
return digtron_technic_run(pos, node)
|
||||
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
|
||||
|
||||
|
@ -14,6 +14,8 @@ technic.digilines = {
|
||||
}
|
||||
}
|
||||
|
||||
dofile(path.."/network.lua")
|
||||
|
||||
dofile(path.."/register/init.lua")
|
||||
|
||||
-- Tiers
|
||||
|
636
technic/machines/network.lua
Normal file
636
technic/machines/network.lua
Normal 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
|
@ -13,6 +13,7 @@ local function get_cable(pos)
|
||||
end
|
||||
|
||||
-- return the position of connected cable or nil
|
||||
-- TODO: Make it support every possible orientation
|
||||
local function get_connected_cable_network(pos)
|
||||
local param2 = minetest.get_node(pos).param2
|
||||
-- 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?
|
||||
checkpos = vector.add(minetest.facedir_to_dir(param2),pos)
|
||||
network_id = get_cable(checkpos) and technic.pos2network(checkpos)
|
||||
if network_id then
|
||||
return network_id
|
||||
end
|
||||
return network_id
|
||||
end
|
||||
|
||||
-- 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 = network_id and technic.networks[network_id]
|
||||
local swpos = network and technic.network2sw_pos(network_id)
|
||||
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
|
||||
|
||||
minetest.register_craft({
|
||||
@ -60,7 +59,7 @@ minetest.register_node("technic:power_monitor",{
|
||||
"technic_power_monitor_front.png"
|
||||
},
|
||||
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"},
|
||||
sounds = default.node_sound_wood_defaults(),
|
||||
on_construct = function(pos)
|
||||
@ -97,19 +96,16 @@ minetest.register_node("technic:power_monitor",{
|
||||
return
|
||||
end
|
||||
|
||||
local sw_pos = get_swpos(pos)
|
||||
if not sw_pos then
|
||||
return
|
||||
end
|
||||
local network = get_network(pos)
|
||||
if not network then return end
|
||||
|
||||
local sw_meta = minetest.get_meta(sw_pos)
|
||||
digilines.receptor_send(pos, technic.digilines.rules, channel, {
|
||||
supply = sw_meta:get_int("supply"),
|
||||
demand = sw_meta:get_int("demand"),
|
||||
lag = sw_meta:get_int("lag"),
|
||||
battery_count = sw_meta:get_int("battery_count"),
|
||||
battery_charge = sw_meta:get_int("battery_charge"),
|
||||
battery_charge_max = sw_meta:get_int("battery_charge_max"),
|
||||
supply = network.supply,
|
||||
demand = network.demand,
|
||||
lag = network.lag,
|
||||
battery_count = network.battery_count,
|
||||
battery_charge = network.battery_charge,
|
||||
battery_charge_max = network.battery_charge_max,
|
||||
})
|
||||
end
|
||||
},
|
||||
@ -123,14 +119,10 @@ minetest.register_abm({
|
||||
chance = 1,
|
||||
action = function(pos, node, active_object_count, active_object_count_wider)
|
||||
local meta = minetest.get_meta(pos)
|
||||
local sw_pos = get_swpos(pos)
|
||||
if sw_pos then
|
||||
local sw_meta = minetest.get_meta(sw_pos)
|
||||
local supply = sw_meta:get_int("supply")
|
||||
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)))
|
||||
local network = get_network(pos)
|
||||
if network then
|
||||
meta:set_string("infotext", S("Power Monitor. Supply: @1 Demand: @2",
|
||||
technic.EU_string(network.supply), technic.EU_string(network.demand)))
|
||||
else
|
||||
meta:set_string("infotext",S("Power Monitor Has No Network"))
|
||||
end
|
||||
|
@ -11,110 +11,150 @@ function technic.get_cable_tier(name)
|
||||
return cable_tier[name]
|
||||
end
|
||||
|
||||
local function check_connections(pos)
|
||||
-- Build a table of all machines
|
||||
local machines = {}
|
||||
for tier,list in pairs(technic.machines) do
|
||||
for k,v in pairs(list) do
|
||||
machines[k] = v
|
||||
end
|
||||
local function match_cable_tier_filter(name, tiers)
|
||||
-- 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
|
||||
local connections = {}
|
||||
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 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 _,connected_pos in pairs(positions) do
|
||||
{x=pos.x, y=pos.y, z=pos.z-1},
|
||||
}
|
||||
for _,connected_pos in ipairs(positions) do
|
||||
local name = minetest.get_node(connected_pos).name
|
||||
if machines[name] or technic.get_cable_tier(name) then
|
||||
table.insert(connections,connected_pos)
|
||||
if tier_machines and tier_machines[name] then
|
||||
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
|
||||
return connections
|
||||
return network, cables, machines
|
||||
end
|
||||
|
||||
local function clear_networks(pos)
|
||||
local node = minetest.get_node(pos)
|
||||
local placed = node.name ~= "air"
|
||||
local positions = check_connections(pos)
|
||||
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
|
||||
return
|
||||
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
|
||||
local function place_network_node(pos, tiers, name)
|
||||
-- Get connections and primary network if there's any
|
||||
local network, cables, machines = get_neighbors(pos, tiers)
|
||||
if not network then
|
||||
-- We're evidently not on a network, nothing to add ourselves to
|
||||
return
|
||||
end
|
||||
|
||||
-- Actually add it to the (cached) network
|
||||
-- This is similar to check_node_subp
|
||||
local pos_hash = minetest.hash_node_position(pos)
|
||||
technic.cables[pos_hash] = network_id
|
||||
pos.visited = 1
|
||||
if technic.is_tier_cable(name, tier) then
|
||||
network.all_nodes[pos_hash] = pos
|
||||
elseif technic.machines[tier][node.name] then
|
||||
if technic.machines[tier][node.name] == technic.producer then
|
||||
table.insert(network.PR_nodes,pos)
|
||||
elseif technic.machines[tier][node.name] == technic.receiver then
|
||||
table.insert(network.RE_nodes,pos)
|
||||
elseif technic.machines[tier][node.name] == technic.producer_receiver then
|
||||
table.insert(network.PR_nodes,pos)
|
||||
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
|
||||
-- Attach to primary network, this must be done before building branches from this position
|
||||
technic.add_network_node(pos, network)
|
||||
if not match_cable_tier_filter(name, tiers) then
|
||||
if technic.machines[tiers[1]][name] == technic.producer_receiver then
|
||||
-- FIXME: Multi tier machine like supply converter should also attach to other networks around pos.
|
||||
-- Preferably also with connection rules defined for machine.
|
||||
-- nodedef.connect_sides could be used to generate these rules.
|
||||
-- For now, assume that all multi network machines belong to technic.producer_receiver group:
|
||||
-- Get cables and networks around PR_RE machine
|
||||
local _, machine_cables, _ = get_neighbors(pos)
|
||||
for _,connection in ipairs(machine_cables) do
|
||||
if connection.network and connection.network.id ~= network.id then
|
||||
-- Attach PR_RE machine to secondary networks (last added is primary until above note is resolved)
|
||||
technic.add_network_node(pos, connection.network)
|
||||
end
|
||||
elseif dead_end and not placed then
|
||||
-- Dead end removed, remove it from 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
|
||||
return
|
||||
end
|
||||
else
|
||||
-- Check connected cables for foreign networks, overload if machine was connected to multiple networks
|
||||
for _, connection in ipairs(cables) do
|
||||
if connection.network and connection.network.id ~= network.id then
|
||||
technic.overload_network(connection.network.id)
|
||||
technic.overload_network(network.id)
|
||||
end
|
||||
local network = technic.networks[network_id]
|
||||
|
||||
-- Search for and remove machine
|
||||
technic.cables[minetest.hash_node_position(pos)] = nil
|
||||
for tblname,table in pairs(network) do
|
||||
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
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Not a dead end, so the whole network needs to be recalculated
|
||||
for _,v in pairs(technic.networks[net].all_nodes) do
|
||||
local pos1 = minetest.hash_node_position(v)
|
||||
technic.cables[pos1] = nil
|
||||
end
|
||||
technic.networks[net] = nil
|
||||
end
|
||||
end
|
||||
-- Machine added, skip all network building
|
||||
return
|
||||
end
|
||||
|
||||
-- Attach neighbor machines if cable was added
|
||||
for _,machine_pos in ipairs(machines) do
|
||||
technic.add_network_node(machine_pos, network)
|
||||
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
|
||||
else
|
||||
-- TODO: Check branches around and switching stations for branches:
|
||||
-- remove branches that do not have switching station. Switching stations not tracked but could be easily tracked.
|
||||
-- remove branches not connected to another branch. Individual branches not tracked, requires simple AI heuristics.
|
||||
-- move branches that have switching station to new networks without checking or loading actual nodes in world.
|
||||
-- To do all this network must be aware of individual branches and switching stations, might not be worth it...
|
||||
-- For now remove whole network and let ABM rebuild it
|
||||
technic.remove_network(network.id)
|
||||
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)
|
||||
-- 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 ""
|
||||
override_cable_plate = override_cable_plate or override_cable
|
||||
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,
|
||||
["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+
|
||||
}
|
||||
|
||||
minetest.register_node("technic:"..ltier..prefix.."_cable", override_table({
|
||||
minetest.register_node(node_name, override_table({
|
||||
description = S("%s Cable"):format(tier),
|
||||
tiles = {"technic_"..ltier..prefix.."_cable.png"},
|
||||
inventory_image = "technic_"..ltier..prefix.."_cable_wield.png",
|
||||
wield_image = "technic_"..ltier..prefix.."_cable_wield.png",
|
||||
groups = groups,
|
||||
sounds = default.node_sound_wood_defaults(),
|
||||
drop = "technic:"..ltier..prefix.."_cable",
|
||||
drop = node_name,
|
||||
paramtype = "light",
|
||||
sunlight_propagates = true,
|
||||
drawtype = "nodebox",
|
||||
node_box = node_box,
|
||||
connects_to = {"group:technic_"..ltier.."_cable",
|
||||
"group:technic_"..ltier, "group:technic_all_tiers"},
|
||||
on_construct = clear_networks,
|
||||
on_destruct = clear_networks,
|
||||
on_construct = function(pos) place_network_node(pos, {tier}, node_name) end,
|
||||
on_destruct = function(pos) remove_network_node(pos, {tier}, node_name) end,
|
||||
}, override_cable))
|
||||
|
||||
local xyz = {
|
||||
@ -199,21 +240,22 @@ function technic.register_cable(tier, size, description, prefix, override_cable,
|
||||
return "-"..p
|
||||
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
|
||||
local def = {
|
||||
description = S("%s Cable Plate"):format(tier),
|
||||
tiles = {"technic_"..ltier..prefix.."_cable.png"},
|
||||
groups = table.copy(groups),
|
||||
sounds = default.node_sound_wood_defaults(),
|
||||
drop = "technic:"..ltier..prefix.."_cable_plate_1",
|
||||
drop = node_name .. "_plate_1",
|
||||
paramtype = "light",
|
||||
sunlight_propagates = true,
|
||||
drawtype = "nodebox",
|
||||
node_box = table.copy(node_box),
|
||||
connects_to = {"group:technic_"..ltier.."_cable",
|
||||
"group:technic_"..ltier, "group:technic_all_tiers"},
|
||||
on_construct = clear_networks,
|
||||
on_destruct = clear_networks,
|
||||
on_construct = function(pos) place_network_node(pos, {tier}, node_name.."_plate_"..i) end,
|
||||
on_destruct = function(pos) remove_network_node(pos, {tier}, node_name.."_plate_"..i) end,
|
||||
}
|
||||
def.node_box.fixed = {
|
||||
{-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
|
||||
return item_place_override_node(
|
||||
itemstack, placer, pointed_thing,
|
||||
{name = "technic:"..ltier..prefix.."_cable_plate_"..num}
|
||||
{name = node_name.."_plate_"..num}
|
||||
)
|
||||
end
|
||||
else
|
||||
@ -271,39 +313,43 @@ function technic.register_cable(tier, size, description, prefix, override_cable,
|
||||
num = num + dir
|
||||
num = (num >= 1 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
|
||||
minetest.register_node("technic:"..ltier..prefix.."_cable_plate_"..i, override_table(def, override_cable_plate))
|
||||
cable_tier["technic:"..ltier..prefix.."_cable_plate_"..i] = tier
|
||||
minetest.register_node(node_name.."_plate_"..i, override_table(def, override_cable_plate))
|
||||
cable_tier[node_name.."_plate_"..i] = tier
|
||||
end
|
||||
|
||||
local c = "technic:"..ltier..prefix.."_cable"
|
||||
minetest.register_craft({
|
||||
output = "technic:"..ltier..prefix.."_cable_plate_1 5",
|
||||
output = node_name.."_plate_1 5",
|
||||
recipe = {
|
||||
{"", "", c},
|
||||
{c , c , c},
|
||||
{"", "", c},
|
||||
{"" , "" , node_name},
|
||||
{node_name, node_name, node_name},
|
||||
{"" , "" , node_name},
|
||||
}
|
||||
})
|
||||
|
||||
minetest.register_craft({
|
||||
output = c,
|
||||
output = node_name,
|
||||
recipe = {
|
||||
{"technic:"..ltier..prefix.."_cable_plate_1"},
|
||||
{node_name.."_plate_1"},
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
local function clear_nets_if_machine(pos, node)
|
||||
for tier, machine_list in pairs(technic.machines) do
|
||||
if machine_list[node.name] ~= nil then
|
||||
return clear_networks(pos)
|
||||
end
|
||||
minetest.register_on_mods_loaded(function()
|
||||
-- FIXME: Move this to register.lua or somewhere else where register_on_mods_loaded is not required.
|
||||
-- Possible better option would be to inject these when machine is registered in register.lua.
|
||||
for name, tiers in pairs(technic.machine_tiers) do
|
||||
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
|
||||
|
||||
minetest.register_on_placenode(clear_nets_if_machine)
|
||||
minetest.register_on_dignode(clear_nets_if_machine)
|
||||
|
||||
end)
|
||||
|
@ -143,6 +143,10 @@ local run = function(pos, node, run_stage)
|
||||
|
||||
if from and to then
|
||||
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_supply", 0)
|
||||
meta:set_int(to.."_EU_demand", 0)
|
||||
|
@ -1,29 +1,6 @@
|
||||
-- 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 digilines_path = minetest.get_modpath("digilines")
|
||||
|
||||
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
|
||||
if mesecons_path then
|
||||
mesecon_def = {effector = {
|
||||
@ -60,33 +48,17 @@ minetest.register_node("technic:switching_station",{
|
||||
on_construct = function(pos)
|
||||
local meta = minetest.get_meta(pos)
|
||||
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("formspec", "field[channel;Channel;${channel}]")
|
||||
local poshash = minetest.hash_node_position(pos)
|
||||
technic.redundant_warn.poshash = nil
|
||||
start_network(pos)
|
||||
end,
|
||||
after_dig_node = function(pos)
|
||||
pos.y = pos.y - 1
|
||||
local poshash = minetest.hash_node_position(pos)
|
||||
technic.redundant_warn.poshash = nil
|
||||
on_destruct = function(pos)
|
||||
-- Remove network when switching station is removed, if
|
||||
-- there's another switching station network will be rebuilt.
|
||||
local network_id = technic.sw_pos2network(pos)
|
||||
if technic.networks[network_id] then
|
||||
technic.remove_network(network_id)
|
||||
end
|
||||
end,
|
||||
on_receive_fields = function(pos, formname, fields, sender)
|
||||
if not fields.channel then
|
||||
@ -116,533 +88,83 @@ minetest.register_node("technic:switching_station",{
|
||||
if channel ~= meta:get_string("channel") then
|
||||
return
|
||||
end
|
||||
digilines.receptor_send(pos, technic.digilines.rules, channel, {
|
||||
supply = meta:get_int("supply"),
|
||||
demand = meta:get_int("demand"),
|
||||
lag = meta:get_int("lag")
|
||||
})
|
||||
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, {
|
||||
supply = network.supply,
|
||||
demand = network.demand,
|
||||
lag = network.lag
|
||||
})
|
||||
else
|
||||
digilines.receptor_send(pos, technic.digilines.rules, channel, {
|
||||
error = "No network",
|
||||
})
|
||||
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 --
|
||||
-----------------------------------------------
|
||||
|
||||
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 for a node in case it was disconnected from the network
|
||||
-- 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({
|
||||
label = "Machines: timeout check",
|
||||
nodenames = {"group:technic_machine"},
|
||||
interval = 1,
|
||||
chance = 1,
|
||||
action = function(pos, node, active_object_count, active_object_count_wider)
|
||||
for tier, machines in pairs(technic.machines) do
|
||||
if machines[node.name] and switching_station_timeout_count(pos, tier) then
|
||||
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
|
||||
-- Check for machine timeouts for all tiers
|
||||
local tiers = technic.machine_tiers[node.name]
|
||||
local timed_out = true
|
||||
for _, tier in ipairs(tiers) do
|
||||
local timeout = technic.get_timeout(tier, pos)
|
||||
if timeout > 0 then
|
||||
technic.touch_node(tier, pos, timeout - 1)
|
||||
timed_out = false
|
||||
end
|
||||
end
|
||||
-- If all tiers for machine timed out take action
|
||||
if timed_out then
|
||||
technic.disable_machine(pos, node)
|
||||
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({
|
||||
label = "Machines: re-enable check",
|
||||
nodenames = {"technic:switching_station"},
|
||||
interval = 1,
|
||||
chance = 1,
|
||||
action = function(pos, node, active_object_count, active_object_count_wider)
|
||||
local pos1 = {x=pos.x,y=pos.y-1,z=pos.z}
|
||||
local tier = technic.get_cable_tier(minetest.get_node(pos1).name)
|
||||
if not tier then return end
|
||||
if switching_station_timeout_count(pos, tier) then
|
||||
local network_id = technic.sw_pos2network(pos)
|
||||
-- Check if network is overloaded / conflicts with another network
|
||||
if network_id then
|
||||
local infotext
|
||||
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,
|
||||
})
|
||||
|
||||
for tier, machines in pairs(technic.machines) do
|
||||
-- SPECIAL will not be traversed
|
||||
technic.register_machine(tier, "technic:switching_station", "SPECIAL")
|
||||
end
|
||||
|
@ -1,23 +1,6 @@
|
||||
|
||||
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
|
||||
|
||||
if has_monitoring_mod then
|
||||
@ -32,21 +15,9 @@ if has_monitoring_mod then
|
||||
)
|
||||
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
|
||||
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
|
||||
local timer = 0
|
||||
@ -71,91 +42,78 @@ minetest.register_globalstep(function(dtime)
|
||||
-- normal run_interval
|
||||
technic_run_interval = 1.0
|
||||
end
|
||||
set_default_timeout(math.ceil(technic_run_interval) + 1)
|
||||
|
||||
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
|
||||
|
||||
for hash, switch in pairs(switches) do
|
||||
local pos = minetest.get_position_from_hash(hash)
|
||||
local diff = now - switch.time
|
||||
for network_id, network in pairs(technic.active_networks) do
|
||||
local pos = technic.network2sw_pos(network_id)
|
||||
|
||||
minetest.get_voxel_manip(pos, pos)
|
||||
local node = minetest.get_node(pos)
|
||||
local node = technic.get_or_load_node(pos) or minetest.get_node(pos)
|
||||
|
||||
if node.name ~= "technic:switching_station" then
|
||||
-- station vanished
|
||||
switches[hash] = nil
|
||||
technic.remove_network(network_id)
|
||||
|
||||
elseif diff < off_delay_micros then
|
||||
elseif network.timeout > now then
|
||||
-- station active
|
||||
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()
|
||||
technic.switching_station_run(pos)
|
||||
technic.network_run(network_id)
|
||||
local switch_diff = minetest.get_us_time() - start
|
||||
|
||||
|
||||
local meta = minetest.get_meta(pos)
|
||||
|
||||
-- set lag in microseconds into the "lag" meta field
|
||||
meta:set_int("lag", switch_diff)
|
||||
network.lag = switch_diff
|
||||
|
||||
-- overload detection
|
||||
if switch_diff > 250000 then
|
||||
switch.skip = 30
|
||||
network.skip = 30
|
||||
elseif switch_diff > 150000 then
|
||||
switch.skip = 20
|
||||
network.skip = 20
|
||||
elseif switch_diff > 75000 then
|
||||
switch.skip = 10
|
||||
network.skip = 10
|
||||
elseif switch_diff > 50000 then
|
||||
switch.skip = 2
|
||||
network.skip = 2
|
||||
end
|
||||
|
||||
if switch.skip > 0 then
|
||||
if network.skip > 0 then
|
||||
-- calculate efficiency in percent and display it
|
||||
local efficiency = math.floor(1/switch.skip*100)
|
||||
meta:set_string("infotext", "Polyfuse triggered, current efficiency: " ..
|
||||
local efficiency = math.floor(1/network.skip*100)
|
||||
technic.network_infotext(network_id, "Polyfuse triggered, current efficiency: " ..
|
||||
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
|
||||
-- laggy switching stations won't work well in unloaded areas this way
|
||||
switches[hash] = nil
|
||||
technic.active_networks[network_id] = nil
|
||||
end
|
||||
|
||||
else
|
||||
switch.skip = math.max(switch.skip - 1, 0)
|
||||
end
|
||||
|
||||
|
||||
else
|
||||
-- station timed out
|
||||
switches[hash] = nil
|
||||
technic.active_networks[network_id] = nil
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
local time_usage = minetest.get_us_time() - now
|
||||
|
||||
if has_monitoring_mod then
|
||||
local time_usage = minetest.get_us_time() - now
|
||||
active_switching_stations_metric.set(active_switches)
|
||||
switching_stations_usage_metric.inc(time_usage)
|
||||
end
|
||||
|
||||
|
||||
end)
|
||||
|
||||
|
||||
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 },
|
||||
func = function()
|
||||
switches = {}
|
||||
technic.active_networks = {}
|
||||
end
|
||||
})
|
||||
|
@ -9,17 +9,23 @@ technic.battery = "BA"
|
||||
technic.machines = {}
|
||||
technic.power_tools = {}
|
||||
technic.networks = {}
|
||||
|
||||
technic.machine_tiers = {}
|
||||
|
||||
function technic.register_tier(tier, description)
|
||||
technic.machines[tier] = {}
|
||||
end
|
||||
|
||||
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
|
||||
return
|
||||
end
|
||||
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
|
||||
|
||||
function technic.register_power_tool(craftitem, max_charge)
|
||||
|
355
technic/spec/building_spec.lua
Normal file
355
technic/spec/building_spec.lua
Normal 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
0
technic/spec/fixtures/minetest.cfg
vendored
Normal file
120
technic/spec/fixtures/minetest.lua
vendored
Normal file
120
technic/spec/fixtures/minetest.lua
vendored
Normal 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)
|
702
technic/spec/fixtures/minetest/common/misc_helpers.lua
vendored
Normal file
702
technic/spec/fixtures/minetest/common/misc_helpers.lua
vendored
Normal 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
|
242
technic/spec/fixtures/minetest/common/vector.lua
vendored
Normal file
242
technic/spec/fixtures/minetest/common/vector.lua
vendored
Normal 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
|
262
technic/spec/fixtures/minetest/game/misc.lua
vendored
Normal file
262
technic/spec/fixtures/minetest/game/misc.lua
vendored
Normal 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
|
43
technic/spec/fixtures/minetest/player.lua
vendored
Normal file
43
technic/spec/fixtures/minetest/player.lua
vendored
Normal 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
|
18
technic/spec/fixtures/minetest/protection.lua
vendored
Normal file
18
technic/spec/fixtures/minetest/protection.lua
vendored
Normal 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
23
technic/spec/fixtures/network.lua
vendored
Normal 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
2
technic/spec/fixtures/pipeworks.lua
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
_G.pipeworks = {}
|
417
technic/spec/network_spec.lua
Normal file
417
technic/spec/network_spec.lua
Normal 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)
|
||||
|
||||
--]]
|
147
technic/spec/supply_converter_spec.lua
Normal file
147
technic/spec/supply_converter_spec.lua
Normal 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)
|
79
technic/spec/test_helpers.lua
Normal file
79
technic/spec/test_helpers.lua
Normal 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")
|
Loading…
x
Reference in New Issue
Block a user