690 lines
20 KiB
Lua
690 lines
20 KiB
Lua
--[[
|
|
|
|
TechAge
|
|
=======
|
|
|
|
Copyright (C) 2019-2020 Joachim Stolberg
|
|
|
|
AGPL v3
|
|
See LICENSE.txt for more information
|
|
|
|
Basis functions for inter-node communication
|
|
|
|
]]--
|
|
|
|
--- for lazy programmers
|
|
local S = function(pos) if pos then return minetest.pos_to_string(pos) end end
|
|
--local P = minetest.string_to_pos
|
|
--local M = minetest.get_meta
|
|
local has_mesecons = minetest.global_exists("mesecon")
|
|
|
|
local NodeInfoCache = {}
|
|
local NumbersToBeRecycled = {}
|
|
local MP = minetest.get_modpath("techage")
|
|
local techage_use_sqlite = minetest.settings:get_bool('techage_use_sqlite', false)
|
|
|
|
-- Localize functions to avoid table lookups (better performance)
|
|
local string_split = string.split
|
|
local NodeDef = techage.NodeDef
|
|
local Tube = techage.Tube
|
|
local is_cart_available = minecart.is_nodecart_available
|
|
local techage_counting_hit = techage.counting_hit
|
|
local tubelib2_side_to_dir = tubelib2.side_to_dir
|
|
|
|
-------------------------------------------------------------------
|
|
-- Database
|
|
-------------------------------------------------------------------
|
|
local backend
|
|
if techage_use_sqlite then
|
|
backend = dofile(MP .. "/basis/numbers_sqlite.lua")
|
|
else
|
|
backend = dofile(MP .. "/basis/numbers_storage.lua")
|
|
end
|
|
|
|
local function update_nodeinfo(number)
|
|
local pos = backend.get_nodepos(number)
|
|
if pos then
|
|
NodeInfoCache[number] = {pos = pos, name = techage.get_node_lvm(pos).name}
|
|
return NodeInfoCache[number]
|
|
end
|
|
end
|
|
|
|
local function delete_nodeinfo_entry(number)
|
|
if number and NodeInfoCache[number] then
|
|
number = next(NodeInfoCache, number)
|
|
if number then
|
|
NodeInfoCache[number] = nil
|
|
end
|
|
else
|
|
number = next(NodeInfoCache, nil)
|
|
end
|
|
return number
|
|
end
|
|
|
|
-- Keep the cache size small by deleting entries randomly
|
|
local function keep_small(number)
|
|
number = delete_nodeinfo_entry(number)
|
|
minetest.after(10, keep_small, number)
|
|
end
|
|
|
|
keep_small()
|
|
|
|
minetest.after(2, backend.delete_invalid_entries, NodeDef)
|
|
|
|
-------------------------------------------------------------------
|
|
-- Local helper functions
|
|
-------------------------------------------------------------------
|
|
local function in_list(list, x)
|
|
for _, v in ipairs(list) do
|
|
if v == x then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Determine position related node number for addressing purposes
|
|
local function get_number(pos, new)
|
|
local meta = minetest.get_meta(pos)
|
|
if meta:contains("node_number") then
|
|
return meta:get_string("node_number")
|
|
end
|
|
-- generate new number
|
|
if new then
|
|
local num = backend.add_nodepos(pos)
|
|
meta:set_string("node_number", num)
|
|
return num
|
|
end
|
|
end
|
|
|
|
local function not_protected(pos, placer_name, clicker_name)
|
|
local meta = minetest.get_meta(pos)
|
|
if meta then
|
|
if placer_name and not minetest.is_protected(pos, placer_name) then
|
|
if clicker_name == nil or placer_name == clicker_name then
|
|
return true
|
|
end
|
|
if not minetest.is_protected(pos, clicker_name) then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function register_lbm(name, nodenames)
|
|
minetest.register_lbm({
|
|
label = "[TechAge] Node update",
|
|
name = name.."update",
|
|
nodenames = nodenames,
|
|
run_at_every_load = true,
|
|
action = function(pos, node)
|
|
if NodeDef[node.name] and NodeDef[node.name].on_node_load then
|
|
NodeDef[node.name].on_node_load(pos, node)
|
|
end
|
|
end
|
|
})
|
|
end
|
|
|
|
local SideToDir = {B=1, R=2, F=3, L=4, D=5, U=6}
|
|
|
|
local function side_to_dir(side, param2)
|
|
return tubelib2_side_to_dir(side, param2)
|
|
end
|
|
|
|
techage.side_to_outdir = side_to_dir
|
|
|
|
function techage.side_to_indir(side, param2)
|
|
return tubelib2.Turn180Deg[side_to_dir(side, param2)]
|
|
end
|
|
|
|
local function get_next_node(pos, out_dir)
|
|
local res, npos, node = Tube:compatible_node(pos, out_dir)
|
|
local in_dir = tubelib2.Turn180Deg[out_dir]
|
|
return res, npos, in_dir, node.name
|
|
end
|
|
|
|
local function get_dest_node(pos, out_dir)
|
|
local spos, in_dir = Tube:get_connected_node_pos(pos, out_dir)
|
|
local _,node = Tube:get_node(spos)
|
|
return spos, in_dir, node.name
|
|
end
|
|
|
|
local function item_handling_node(name)
|
|
local node_def = name and NodeDef[name]
|
|
if node_def then
|
|
return node_def.on_pull_item or node_def.on_push_item or node_def.is_pusher
|
|
end
|
|
end
|
|
|
|
local function is_air_like(name)
|
|
local ndef = minetest.registered_nodes[name]
|
|
if ndef and ndef.buildable_to then
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
techage.SystemTime = 0
|
|
minetest.register_globalstep(function(dtime)
|
|
techage.SystemTime = techage.SystemTime + dtime
|
|
end)
|
|
|
|
-- used by TA1 hammer: dug_node[player_name] = pos
|
|
techage.dug_node = {}
|
|
minetest.register_on_dignode(function(pos, oldnode, digger)
|
|
if not digger then return end
|
|
-- store the position of the dug block for tools like the TA1 hammer
|
|
techage.dug_node[digger:get_player_name()] = pos
|
|
end)
|
|
|
|
-------------------------------------------------------------------
|
|
-- API helper functions
|
|
-------------------------------------------------------------------
|
|
|
|
-- Check if both strings are the same or one string starts with the other string.
|
|
function techage.string_compare(s1, s2)
|
|
if s1 and s2 then
|
|
local minLength = math.min(#s1, #s2)
|
|
return string.sub(s1, 1, minLength) == string.sub(s2, 1, minLength)
|
|
end
|
|
end
|
|
|
|
-- Function returns { pos, name } for the node referenced by number
|
|
function techage.get_node_info(dest_num)
|
|
return NodeInfoCache[dest_num] or update_nodeinfo(dest_num)
|
|
end
|
|
|
|
-- Function returns the node number from the given position or
|
|
-- nil, if no node number for this position is assigned.
|
|
function techage.get_node_number(pos)
|
|
return get_number(pos)
|
|
end
|
|
|
|
function techage.get_pos(pos, side)
|
|
local node = techage.get_node_lvm(pos)
|
|
local dir = nil
|
|
if node.name ~= "air" and node.name ~= "ignore" then
|
|
dir = side_to_dir(side, node.param2)
|
|
end
|
|
return tubelib2.get_pos(pos, dir)
|
|
end
|
|
|
|
-- Function is used for available nodes with lost numbers, only.
|
|
function techage.get_new_number(pos, name)
|
|
-- store position
|
|
return get_number(pos, true)
|
|
end
|
|
|
|
-- extract ident and value from strings like "ident=value"
|
|
function techage.ident_value(s)
|
|
local ident, value = unpack(string.split(s, "=", true, 1))
|
|
return (ident or ""):trim(), (value or ""):trim()
|
|
end
|
|
|
|
-------------------------------------------------------------------
|
|
-- Node construction/destruction functions
|
|
-------------------------------------------------------------------
|
|
|
|
-- Add node to the techage lists.
|
|
-- Function determines and returns the node position number,
|
|
-- needed for message communication.
|
|
-- If TA2 node, return '-' instead of a real number, because
|
|
-- TA2 nodes should not support number based commands.
|
|
function techage.add_node(pos, name, is_ta2)
|
|
if item_handling_node(name) then
|
|
Tube:after_place_node(pos)
|
|
end
|
|
if is_ta2 then
|
|
return "-"
|
|
end
|
|
local key = minetest.hash_node_position(pos)
|
|
local num = NumbersToBeRecycled[key]
|
|
if num then
|
|
NodeInfoCache[num] = nil
|
|
backend.set_nodepos(num, pos)
|
|
NumbersToBeRecycled[key] = nil
|
|
return num
|
|
end
|
|
return get_number(pos, true)
|
|
end
|
|
|
|
-- Function removes the node from the techage lists.
|
|
function techage.remove_node(pos, oldnode, oldmetadata)
|
|
local number = oldmetadata and oldmetadata.fields and (oldmetadata.fields.node_number or oldmetadata.fields.number)
|
|
number = number or get_number(pos)
|
|
if number and tonumber(number) then
|
|
local key = minetest.hash_node_position(pos)
|
|
NumbersToBeRecycled[key] = number
|
|
NodeInfoCache[number] = nil
|
|
end
|
|
if oldnode and item_handling_node(oldnode.name) then
|
|
Tube:after_dig_node(pos)
|
|
end
|
|
end
|
|
|
|
-- Repairs the node number after it was erased by `backend.delete_invalid_entries`
|
|
function techage.repair_number(pos)
|
|
local number = techage.get_node_number(pos)
|
|
if number then
|
|
backend.set_nodepos(number, pos)
|
|
end
|
|
end
|
|
|
|
-- Like techage.add_node, but use the old number again
|
|
function techage.unpack_node(pos, name, number)
|
|
if item_handling_node(name) then
|
|
Tube:after_place_node(pos)
|
|
end
|
|
local key = minetest.hash_node_position(pos)
|
|
NumbersToBeRecycled[key] = nil
|
|
if number then
|
|
backend.set_nodepos(number, pos)
|
|
end
|
|
end
|
|
|
|
-- Like techage.remove_node but don't store the number for this position
|
|
function techage.pack_node(pos, oldnode, number)
|
|
if number then
|
|
NodeInfoCache[number] = nil
|
|
end
|
|
if oldnode and item_handling_node(oldnode.name) then
|
|
Tube:after_dig_node(pos)
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------
|
|
-- Used by the assembly tool
|
|
-------------------------------------------------------------------
|
|
function techage.pre_add_node(pos, number)
|
|
local key = minetest.hash_node_position(pos)
|
|
NumbersToBeRecycled[key] = number
|
|
end
|
|
|
|
function techage.post_remove_node(pos)
|
|
local key = minetest.hash_node_position(pos)
|
|
NumbersToBeRecycled[key] = nil
|
|
end
|
|
|
|
-------------------------------------------------------------------
|
|
-- Node register function
|
|
-------------------------------------------------------------------
|
|
|
|
-- Register node for techage communication
|
|
-- Call this function only at load time!
|
|
-- Param names: List of node names like {"techage:pusher_off", "techage:pusher_on"}
|
|
-- Param node_definition: A table according to:
|
|
-- {
|
|
-- on_inv_request = func(pos, in_dir, access_type)
|
|
-- on_pull_item = func(pos, in_dir, num, (opt.) item_name),
|
|
-- on_push_item = func(pos, in_dir, item),
|
|
-- on_unpull_item = func(pos, in_dir, item),
|
|
-- on_recv_message = func(pos, src, topic, payload),
|
|
-- on_node_load = func(pos), -- LBM function
|
|
-- on_transfer = func(pos, in_dir, topic, payload),
|
|
-- }
|
|
function techage.register_node(names, node_definition)
|
|
-- store facedir table for all known node names
|
|
for _,n in ipairs(names) do
|
|
NodeDef[n] = node_definition
|
|
end
|
|
if node_definition.on_pull_item or node_definition.on_push_item or
|
|
node_definition.is_pusher then
|
|
Tube:add_secondary_node_names(names)
|
|
|
|
for _,n in ipairs(names) do
|
|
techage.KnownNodes[n] = true
|
|
end
|
|
end
|
|
-- register LBM
|
|
if node_definition.on_node_load then
|
|
register_lbm(names[1], names)
|
|
end
|
|
|
|
-- register mvps stopper
|
|
if has_mesecons then
|
|
for _, name in ipairs(names) do
|
|
mesecon.register_mvps_stopper(name)
|
|
end
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------
|
|
-- Send message functions
|
|
-------------------------------------------------------------------
|
|
|
|
function techage.not_protected(number, placer_name, clicker_name)
|
|
local ninfo = NodeInfoCache[number] or update_nodeinfo(number)
|
|
if ninfo and ninfo.pos then
|
|
return not_protected(ninfo.pos, placer_name, clicker_name)
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Check the given number value.
|
|
-- Returns true if the number is valid, point to real node and
|
|
-- and the node is not protected for the given player_name.
|
|
function techage.check_number(number, placer_name)
|
|
if number then
|
|
if not techage.not_protected(number, placer_name, nil) then
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Check the given list of numbers.
|
|
-- Returns true if number(s) is/are valid, point to real nodes and
|
|
-- and the nodes are not protected for the given player_name.
|
|
function techage.check_numbers(numbers, placer_name)
|
|
if numbers then
|
|
for _,num in ipairs(string_split(numbers, " ")) do
|
|
if not techage.not_protected(num, placer_name, nil) then
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
function techage.send_multi(src, numbers, topic, payload)
|
|
--print("send_multi", src, numbers, topic)
|
|
for _,num in ipairs(string_split(numbers, " ")) do
|
|
local ninfo = NodeInfoCache[num] or update_nodeinfo(num)
|
|
if ninfo and ninfo.name and ninfo.pos then
|
|
local ndef = NodeDef[ninfo.name]
|
|
if ndef and ndef.on_recv_message then
|
|
techage_counting_hit()
|
|
ndef.on_recv_message(ninfo.pos, src, topic, payload)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function techage.send_single(src, number, topic, payload)
|
|
--print("send_single", src, number, topic)
|
|
local ninfo = NodeInfoCache[number] or update_nodeinfo(number)
|
|
if ninfo and ninfo.name and ninfo.pos then
|
|
local ndef = NodeDef[ninfo.name]
|
|
if ndef and ndef.on_recv_message then
|
|
techage_counting_hit()
|
|
return ndef.on_recv_message(ninfo.pos, src, topic, payload)
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- The destination node location is either:
|
|
-- A) a destination position, specified by pos
|
|
-- B) a neighbor position, specified by caller pos/outdir, or pos/side
|
|
-- C) a tubelib2 network connection, specified by caller pos/outdir, or pos/side
|
|
-- outdir is one of: 1..6
|
|
-- side is one of: "B", "R", "F", "L", "D", "U"
|
|
-- network is a tuebelib2 network instance
|
|
-- opt: nodenames is a table of valid the callee node names
|
|
function techage.transfer(pos, outdir, topic, payload, network, nodenames)
|
|
-- determine out-dir
|
|
if outdir and type(outdir) == "string" then
|
|
local param2 = techage.get_node_lvm(pos).param2
|
|
outdir = side_to_dir(outdir, param2)
|
|
end
|
|
-- determine destination pos
|
|
local dpos, indir
|
|
if network then
|
|
dpos, indir = network:get_connected_node_pos(pos, outdir)
|
|
else
|
|
dpos, indir = tubelib2.get_pos(pos, outdir), outdir
|
|
end
|
|
-- check node name
|
|
local name = techage.get_node_lvm(dpos).name
|
|
if nodenames and not in_list(nodenames, name) then
|
|
return false
|
|
end
|
|
-- call "on_transfer"
|
|
local ndef = NodeDef[name]
|
|
if ndef and ndef.on_transfer then
|
|
return ndef.on_transfer(dpos, indir, topic, payload)
|
|
end
|
|
return false
|
|
end
|
|
|
|
-------------------------------------------------------------------
|
|
-- Beduino functions (see "bep-005_ta_cmnd.md")
|
|
-------------------------------------------------------------------
|
|
function techage.beduino_send_cmnd(src, number, topic, payload)
|
|
--print("beduino_send_cmnd", src, number, topic)
|
|
local ninfo = NodeInfoCache[number] or update_nodeinfo(number)
|
|
if ninfo and ninfo.name and ninfo.pos then
|
|
local ndef = NodeDef[ninfo.name]
|
|
if ndef and ndef.on_beduino_receive_cmnd then
|
|
return ndef.on_beduino_receive_cmnd(ninfo.pos, src, topic, payload or {})
|
|
end
|
|
end
|
|
return 1, ""
|
|
end
|
|
|
|
function techage.beduino_request_data(src, number, topic, payload)
|
|
--print("beduino_request_data", src, number, topic)
|
|
local ninfo = NodeInfoCache[number] or update_nodeinfo(number)
|
|
if ninfo and ninfo.name and ninfo.pos then
|
|
local ndef = NodeDef[ninfo.name]
|
|
if ndef and ndef.on_beduino_request_data then
|
|
return ndef.on_beduino_request_data(ninfo.pos, src, topic, payload or {})
|
|
end
|
|
end
|
|
return 1, ""
|
|
end
|
|
|
|
-------------------------------------------------------------------
|
|
-- Client side Push/Pull item functions
|
|
-------------------------------------------------------------------
|
|
|
|
function techage.get_inv_access(pos, out_dir, access_type)
|
|
local npos, in_dir, name = get_dest_node(pos, out_dir)
|
|
if npos and NodeDef[name] and NodeDef[name].on_inv_request then
|
|
return NodeDef[name].on_inv_request(npos, in_dir, access_type)
|
|
end
|
|
end
|
|
|
|
function techage.pull_items(pos, out_dir, num, item_name)
|
|
local npos, in_dir, name = get_dest_node(pos, out_dir)
|
|
if npos and NodeDef[name] and NodeDef[name].on_pull_item then
|
|
return NodeDef[name].on_pull_item(npos, in_dir, num, item_name)
|
|
end
|
|
end
|
|
|
|
function techage.push_items(pos, out_dir, stack, idx)
|
|
local npos, in_dir, name = get_dest_node(pos, out_dir)
|
|
if npos and NodeDef[name] and NodeDef[name].on_push_item then
|
|
return NodeDef[name].on_push_item(npos, in_dir, stack, idx)
|
|
elseif is_air_like(name) or is_cart_available(npos) then
|
|
minetest.add_item(npos, stack)
|
|
return true
|
|
end
|
|
return stack
|
|
end
|
|
|
|
-- Check for recursion and too long distances
|
|
local start_pos
|
|
function techage.safe_push_items(pos, out_dir, stack, idx)
|
|
local mem = techage.get_mem(pos)
|
|
if not mem.pushing then
|
|
if not start_pos then
|
|
start_pos = pos
|
|
mem.pushing = true
|
|
local res = techage.push_items(pos, out_dir, stack, idx)
|
|
mem.pushing = nil
|
|
start_pos = nil
|
|
return res
|
|
else
|
|
local npos, in_dir, name = get_dest_node(pos, out_dir)
|
|
if vector.distance(start_pos, npos) < (Tube.max_tube_length or 100) then
|
|
mem.pushing = true
|
|
local res = techage.push_items(pos, out_dir, stack, idx)
|
|
mem.pushing = nil
|
|
return res
|
|
end
|
|
end
|
|
end
|
|
return stack
|
|
end
|
|
|
|
function techage.unpull_items(pos, out_dir, stack)
|
|
local npos, in_dir, name = get_dest_node(pos, out_dir)
|
|
if npos and NodeDef[name] and NodeDef[name].on_unpull_item then
|
|
return NodeDef[name].on_unpull_item(npos, in_dir, stack)
|
|
end
|
|
return false
|
|
end
|
|
|
|
-------------------------------------------------------------------
|
|
-- Server side helper functions
|
|
-------------------------------------------------------------------
|
|
|
|
-- Get the given number of items from the inv. The position within the list
|
|
-- is incremented each time so that different item stacks will be considered.
|
|
-- Returns nil if ItemList is empty.
|
|
function techage.get_items(pos, inv, listname, num)
|
|
if inv:is_empty(listname) then
|
|
return nil
|
|
end
|
|
local size = inv:get_size(listname)
|
|
local mem = techage.get_mem(pos)
|
|
mem.ta_startpos = mem.ta_startpos or 0
|
|
for idx = mem.ta_startpos, mem.ta_startpos+size do
|
|
idx = (idx % size) + 1
|
|
local items = inv:get_stack(listname, idx)
|
|
if items:get_count() > 0 then
|
|
local taken = items:take_item(num)
|
|
inv:set_stack(listname, idx, items)
|
|
mem.ta_startpos = idx
|
|
return taken
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- Put the given stack into the given ItemList/inventory.
|
|
-- Function returns:
|
|
-- - true, if all items are moved
|
|
-- - false, if no item is moved
|
|
-- - leftover, if less than all items are moved
|
|
-- (true/false is the legacy mode and can't be removed)
|
|
function techage.put_items(inv, listname, item, idx)
|
|
local leftover
|
|
if idx and inv and idx <= inv:get_size(listname) then
|
|
local stack = inv:get_stack(listname, idx)
|
|
leftover = stack:add_item(item)
|
|
inv:set_stack(listname, idx, stack)
|
|
elseif inv then
|
|
leftover = inv:add_item(listname, item)
|
|
else
|
|
return false
|
|
end
|
|
|
|
local cnt = leftover:get_count()
|
|
if cnt == item:get_count() then
|
|
return false
|
|
elseif cnt == 0 then
|
|
return true
|
|
else
|
|
return leftover
|
|
end
|
|
|
|
end
|
|
|
|
-- Return "full", "loaded", or "empty" depending
|
|
-- on the inventory load.
|
|
-- Full is returned, when no empty stack is available.
|
|
function techage.get_inv_state(inv, listname)
|
|
local state
|
|
if inv:is_empty(listname) then
|
|
state = "empty"
|
|
else
|
|
local list = inv:get_list(listname)
|
|
state = "full"
|
|
for _, item in ipairs(list) do
|
|
if item:is_empty() then
|
|
return "loaded"
|
|
end
|
|
end
|
|
end
|
|
return state
|
|
end
|
|
|
|
-- Beduino variant
|
|
function techage.get_inv_state_num(inv, listname)
|
|
local state
|
|
if inv:is_empty(listname) then
|
|
state = 0
|
|
else
|
|
local list = inv:get_list(listname)
|
|
state = 2
|
|
for _, item in ipairs(list) do
|
|
if item:is_empty() then
|
|
return 1
|
|
end
|
|
end
|
|
end
|
|
return state
|
|
end
|
|
|
|
minetest.register_chatcommand("ta_send", {
|
|
description = minetest.formspec_escape(
|
|
"Send a techage command to the block with the number given: /ta_send <number> <command> [<data>]"),
|
|
func = function(name, param)
|
|
local num, cmnd, payload = param:match('^([0-9]+)%s+(%w+)%s*(.*)$')
|
|
|
|
if num and cmnd then
|
|
if techage.not_protected(num, name) then
|
|
local resp = techage.send_single("0", num, cmnd, payload)
|
|
if type(resp) == "string" then
|
|
return true, resp
|
|
else
|
|
return true, dump(resp)
|
|
end
|
|
else
|
|
return false, "Destination block is protected"
|
|
end
|
|
end
|
|
return false, "Syntax: /ta_send <number> <command> [<data>]"
|
|
end
|
|
})
|
|
|
|
minetest.register_chatcommand("expoints", {
|
|
privs = {
|
|
server = true
|
|
},
|
|
func = function(name, param)
|
|
local player_name, points = param:match("^(%S+)%s*(%d*)$")
|
|
if player_name then
|
|
local player = minetest.get_player_by_name(player_name)
|
|
if player then
|
|
if points and points ~= "" then
|
|
if techage.set_expoints(player, tonumber(points)) then
|
|
return true, "The player "..player_name.." now has "..points.." experience points."
|
|
end
|
|
else
|
|
points = techage.get_expoints(player)
|
|
return true, "The player "..player_name.." has "..points.." experience points."
|
|
end
|
|
else
|
|
return false, "Unknown player "..player_name
|
|
end
|
|
end
|
|
return false, "Syntax error! Syntax: /expoints <name> [<points>]"
|
|
end
|
|
})
|
|
|
|
minetest.register_chatcommand("my_expoints", {
|
|
func = function(name, param)
|
|
local player = minetest.get_player_by_name(name)
|
|
if player then
|
|
local points = techage.get_expoints(player)
|
|
if points then
|
|
return true, "You have "..points.." experience points."
|
|
end
|
|
end
|
|
end
|
|
})
|