+ balance: min and max values + togglable negative ones when registering a currency

+ /currencies add/sub/set/balance/clearunregistered/clearbalance cmds
+ transfer/exchange/exists/clear_balance/remove_unregistered functions
+ changed database structure
master
Giov4 2021-03-04 22:04:05 +01:00
parent 7bc06257fd
commit 68849d5690
5 changed files with 604 additions and 92 deletions

183
api.lua Normal file
View File

@ -0,0 +1,183 @@
local function save_balances() end
local function get_database() end
local function validate_currency() end
local function get_valid_value() end
local storage = minetest.get_mod_storage()
local registered_currencies = {}
--[[
def = {
min_value : number =
the smallest balance that a player can have.
max_value : number =
the biggest balance that a player can have.
negative_balance : bool =
if the balance can be less than 0.
}
]]
function currencies.register(currency, def)
def = def or {}
registered_currencies[currency] = def
end
function currencies.set(currency, pl_name, value)
validate_currency(currency)
local balances = get_database().balances
balances[pl_name] = balances[pl_name] or {}
balances[pl_name][currency] = get_valid_value(currency, value)
save_balances(balances)
end
function currencies.add(currency, pl_name, amount)
validate_currency(currency)
local balances = get_database().balances
balances[pl_name] = balances[pl_name] or {}
balances[pl_name][currency] = balances[pl_name][currency] or 0
currencies.set(currency, pl_name, balances[pl_name][currency] + amount)
end
function currencies.sub(currency, pl_name, amount)
validate_currency(currency)
local balances = get_database().balances
balances[pl_name] = balances[pl_name] or {}
balances[pl_name][currency] = balances[pl_name][currency] or 0
currencies.set(currency, pl_name, balances[pl_name][currency] - amount)
end
function currencies.transfer(currency, from_pl_name, to_pl_name, amount)
currencies.sub(currency, from_pl_name, amount)
currencies.add(currency, to_pl_name, amount)
end
function currencies.exchange(currency, from_pl, from_amount, to_pl, to_amount)
currencies.sub(currency, from_pl, from_amount)
currencies.add(currency, to_pl, to_amount)
end
function currencies.get(currency, pl_name)
validate_currency(currency)
local balances = get_database().balances
balances[pl_name] = balances[pl_name] or {}
balances[pl_name][currency] = balances[pl_name][currency] or 0
return balances[pl_name][currency]
end
function currencies.exists(currency)
return registered_currencies[currency]
end
-- If currency is nil it'll reset every currency.
function currencies.clear_balance(currency, pl_name)
if currency then
validate_currency(currency)
currencies.set(currency, pl_name, 0)
return
end
local balances = get_database().balances
balances[pl_name] = {}
save_balances(balances)
end
function currencies.remove_unregistered()
local balances = get_database().balances
for pl_name, pl_currencies in pairs(balances) do
for currency, balance in pairs(pl_currencies) do
if not currencies.exists(currency) then
pl_currencies[currency] = nil
end
end
end
save_balances(balances)
end
function save_balances(balances)
local db = get_database()
db.balances = balances
storage:set_string("database", minetest.serialize(db))
end
--[[
database = {
balances = {
pl_name : string = {
currency_1 : string = value : number,
...
},
...
}
}
]]
function get_database()
local db = minetest.deserialize(storage:get_string("database"))
-- Recreating the db selecting specific tables, to avoid copying a dirty datababe.
local final_db = {}
final_db.balances = db.balances or {}
return final_db
end
function validate_currency(currency)
assert(
registered_currencies[currency],
"A mod is trying to access to the " .. currency .. " currency, but it hasn't been registered."
)
end
function get_valid_value(currency, value)
local currency_def = registered_currencies[currency]
if not currency_def.negative_balance and value < 0 then value = 0 end
if currency_def.min_value and value < currency_def.min_value then
value = currency_def.min_value
end
if currency_def.max_value and value > currency_def.max_value then
value = currency_def.max_value
end
return value
end

306
chatcmdbuilder.lua Normal file
View File

@ -0,0 +1,306 @@
ChatCmdBuilder = {}
function ChatCmdBuilder.new(name, func, def)
def = def or {}
local cmd = ChatCmdBuilder.build(func)
cmd.def = def
def.func = cmd.run
minetest.register_chatcommand(name, def)
return cmd
end
local STATE_READY = 1
local STATE_PARAM = 2
local STATE_PARAM_TYPE = 3
local bad_chars = {}
bad_chars["("] = true
bad_chars[")"] = true
bad_chars["."] = true
bad_chars["%"] = true
bad_chars["+"] = true
bad_chars["-"] = true
bad_chars["*"] = true
bad_chars["?"] = true
bad_chars["["] = true
bad_chars["^"] = true
bad_chars["$"] = true
local function escape(char)
if bad_chars[char] then
return "%" .. char
else
return char
end
end
local dprint = function() end
ChatCmdBuilder.types = {
pos = "%(? *(%-?[%d.]+) *, *(%-?[%d.]+) *, *(%-?[%d.]+) *%)?",
text = "(.+)",
number = "(%-?[%d.]+)",
int = "(%-?[%d]+)",
word = "([^ ]+)",
alpha = "([A-Za-z]+)",
modname = "([a-z0-9_]+)",
alphascore = "([A-Za-z_]+)",
alphanumeric = "([A-Za-z0-9]+)",
username = "([A-Za-z0-9-_]+)",
}
function ChatCmdBuilder.build(func)
local cmd = {
_subs = {}
}
function cmd:sub(route, func, def)
dprint("Parsing " .. route)
def = def or {}
if string.trim then
route = string.trim(route)
end
local sub = {
pattern = "^",
params = {},
func = func
}
-- End of param reached: add it to the pattern
local param = ""
local param_type = ""
local should_be_eos = false
local function finishParam()
if param ~= "" and param_type ~= "" then
dprint(" - Found param " .. param .. " type " .. param_type)
local pattern = ChatCmdBuilder.types[param_type]
if not pattern then
error("Unrecognised param_type=" .. param_type)
end
sub.pattern = sub.pattern .. pattern
table.insert(sub.params, param_type)
param = ""
param_type = ""
end
end
-- Iterate through the route to find params
local state = STATE_READY
local catching_space = false
local match_space = " " -- change to "%s" to also catch tabs and newlines
local catch_space = match_space.."+"
for i = 1, #route do
local c = route:sub(i, i)
if should_be_eos then
error("Should be end of string. Nothing is allowed after a param of type text.")
end
if state == STATE_READY then
if c == ":" then
dprint(" - Found :, entering param")
state = STATE_PARAM
param_type = "word"
catching_space = false
elseif c:match(match_space) then
print(" - Found space")
if not catching_space then
catching_space = true
sub.pattern = sub.pattern .. catch_space
end
else
catching_space = false
sub.pattern = sub.pattern .. escape(c)
end
elseif state == STATE_PARAM then
if c == ":" then
dprint(" - Found :, entering param type")
state = STATE_PARAM_TYPE
param_type = ""
elseif c:match(match_space) then
print(" - Found whitespace, leaving param")
state = STATE_READY
finishParam()
catching_space = true
sub.pattern = sub.pattern .. catch_space
elseif c:match("%W") then
dprint(" - Found nonalphanum, leaving param")
state = STATE_READY
finishParam()
sub.pattern = sub.pattern .. escape(c)
else
param = param .. c
end
elseif state == STATE_PARAM_TYPE then
if c:match(match_space) then
print(" - Found space, leaving param type")
state = STATE_READY
finishParam()
catching_space = true
sub.pattern = sub.pattern .. catch_space
elseif c:match("%W") then
dprint(" - Found nonalphanum, leaving param type")
state = STATE_READY
finishParam()
sub.pattern = sub.pattern .. escape(c)
else
param_type = param_type .. c
end
end
end
dprint(" - End of route")
finishParam()
sub.pattern = sub.pattern .. "$"
dprint("Pattern: " .. sub.pattern)
table.insert(self._subs, sub)
end
if func then
func(cmd)
end
cmd.run = function(name, param)
for i = 1, #cmd._subs do
local sub = cmd._subs[i]
local res = { string.match(param, sub.pattern) }
if #res > 0 then
local pointer = 1
local params = { name }
for j = 1, #sub.params do
local param = sub.params[j]
if param == "pos" then
local pos = {
x = tonumber(res[pointer]),
y = tonumber(res[pointer + 1]),
z = tonumber(res[pointer + 2])
}
table.insert(params, pos)
pointer = pointer + 3
elseif param == "number" or param == "int" then
table.insert(params, tonumber(res[pointer]))
pointer = pointer + 1
else
table.insert(params, res[pointer])
pointer = pointer + 1
end
end
if table.unpack then
-- lua 5.2 or later
return sub.func(table.unpack(params))
else
-- lua 5.1 or earlier
return sub.func(unpack(params))
end
end
end
return false, "Invalid command"
end
return cmd
end
local function run_tests()
if not (ChatCmdBuilder.build(function(cmd)
cmd:sub("bar :one and :two:word", function(name, one, two)
if name == "singleplayer" and one == "abc" and two == "def" then
return true
end
end)
end)).run("singleplayer", "bar abc and def") then
error("Test 1 failed")
end
local move = ChatCmdBuilder.build(function(cmd)
cmd:sub("move :target to :pos:pos", function(name, target, pos)
if name == "singleplayer" and target == "player1" and
pos.x == 0 and pos.y == 1 and pos.z == 2 then
return true
end
end)
end).run
if not move("singleplayer", "move player1 to 0,1,2") then
error("Test 2 failed")
end
if not move("singleplayer", "move player1 to (0,1,2)") then
error("Test 3 failed")
end
if not move("singleplayer", "move player1 to 0, 1,2") then
error("Test 4 failed")
end
if not move("singleplayer", "move player1 to 0 ,1, 2") then
error("Test 5 failed")
end
if not move("singleplayer", "move player1 to 0, 1, 2") then
error("Test 6 failed")
end
if not move("singleplayer", "move player1 to 0 ,1 ,2") then
error("Test 7 failed")
end
if not move("singleplayer", "move player1 to ( 0 ,1 ,2)") then
error("Test 8 failed")
end
if move("singleplayer", "move player1 to abc,def,sdosd") then
error("Test 9 failed")
end
if move("singleplayer", "move player1 to abc def sdosd") then
error("Test 10 failed")
end
if not (ChatCmdBuilder.build(function(cmd)
cmd:sub("does :one:int plus :two:int equal :three:int", function(name, one, two, three)
if name == "singleplayer" and one + two == three then
return true
end
end)
end)).run("singleplayer", "does 1 plus 2 equal 3") then
error("Test 11 failed")
end
local checknegint = ChatCmdBuilder.build(function(cmd)
cmd:sub("checknegint :x:int", function(name, x)
return x
end)
end).run
if checknegint("checker","checknegint -2") ~= -2 then
error("Test 12 failed")
end
local checknegnumber = ChatCmdBuilder.build(function(cmd)
cmd:sub("checknegnumber :x:number", function(name, x)
return x
end)
end).run
if checknegnumber("checker","checknegnumber -3.3") ~= -3.3 then
error("Test 13 failed")
end
local checknegpos = ChatCmdBuilder.build(function(cmd)
cmd:sub("checknegpos :pos:pos", function(name, pos)
return pos
end)
end).run
local negpos = checknegpos("checker","checknegpos (-13.3,-4.6,-1234.5)")
if negpos.x ~= -13.3 or negpos.y ~= -4.6 or negpos.z ~= -1234.5 then
error("Test 14 failed")
end
local checktypes = ChatCmdBuilder.build(function(cmd)
cmd:sub("checktypes :int:int :number:number :pos:pos :word:word :text:text", function(name, int, number, pos, word, text)
return int, number, pos.x, pos.y, pos.z, word, text
end)
end).run
local int, number, posx, posy, posz, word, text
int, number, posx, posy, posz, word, text = checktypes("checker","checktypes -1 -2.4 (-3,-5.3,6.12) some text to finish off with")
--dprint(int, number, posx, posy, posz, word, text)
if int ~= -1 or number ~= -2.4 or posx ~= -3 or posy ~= -5.3 or posz ~= 6.12 or word ~= "some" or text ~= "text to finish off with" then
error("Test 15 failed")
end
dprint("All tests passed")
end
if not minetest then
run_tests()
end

102
commands.lua Normal file
View File

@ -0,0 +1,102 @@
local function currency_exists() end
minetest.register_privilege("currencies_admin", {
description = "It allows you to use /currencies"
})
ChatCmdBuilder.new("currencies", function(cmd)
cmd:sub("add :plname :currency :amount:int", function(name, pl_name, currency, amount)
if not currency_exists(pl_name, currency) then return end
currencies.add(currency, pl_name, amount)
currencies.print_msg(
name,
"+ " .. amount .. ", ".. pl_name .. "'s " .. currency .. " balance: " .. currencies.get(currency, pl_name)
)
end)
cmd:sub("sub :plname :currency :amount:int", function(name, pl_name, currency, amount)
if not currency_exists(pl_name, currency) then return end
currencies.sub(currency, pl_name, amount)
currencies.print_msg(
name,
"- " .. amount .. ", ".. pl_name .. "'s " .. currency .. " balance: " .. currencies.get(currency, pl_name)
)
end)
cmd:sub("set :plname :currency :amount:int", function(name, pl_name, currency, amount)
if not currency_exists(pl_name, currency) then return end
currencies.set(currency, pl_name, amount)
currencies.print_msg(name, pl_name .. "'s " .. currency .. " balance: " .. currencies.get(currency, pl_name))
end)
cmd:sub("balance :plname :currency", function(name, pl_name, currency)
if not currency_exists(pl_name, currency) then return end
currencies.print_msg(name, pl_name .. "'s " .. currency .. " balance: " .. currencies.get(currency, pl_name))
end)
cmd:sub("clearunregistered", function(name)
currencies.remove_unregistered()
currencies.print_msg(name, "Removed unregistered currencies from the database")
end)
cmd:sub("clearbalance :plname :currency", function(name, pl_name, currency)
if not currency_exists(pl_name, currency) then return end
currencies.clear_balance(currency, pl_name)
currencies.print_msg(name, "Cleared " .. pl_name .. "'s " .. currency .. " balance")
end)
cmd:sub("clearbalance :plname", function(name, pl_name)
currencies.clear_balance(nil, pl_name)
currencies.print_msg(name, "Cleared " .. pl_name .. "'s balances")
end)
end, {
description = [[
ADMIN COMMANDS
(Use /help currencies to read it all)
- balance <pl_name> <currency>
- set <pl_name> <currency> <amount>
- add <pl_name> <currency> <amount>
- sub <pl_name> <currency> <amount>
- clearunregistered: removes from the database every unregistered currency
- clearbalance <pl_name> [currency]: if the currency isn't specified it'll reset each one of them
]],
privs = {currencies_admin = true}
})
function currency_exists(pl_name, currency)
if not currencies.exists(currency) then
currencies.print_error(pl_name, "The " .. currency .. " currency doesn't exist")
return false
end
return true
end

View File

@ -1,94 +1,6 @@
currencies = {}
local function save_database() end
local function get_database() end
local function validate_currency() end
local storage = minetest.get_mod_storage()
local registered_currencies = {}
function currencies.register(currency)
registered_currencies[currency] = true
end
function currencies.set(pl_name, currency, value)
validate_currency(currency)
local db = get_database()
db[pl_name] = db[pl_name] or {}
db[pl_name][currency] = value
save_database(db)
end
function currencies.add(pl_name, currency, value)
validate_currency(currency)
local db = get_database()
db[pl_name] = db[pl_name] or {}
db[pl_name][currency] = db[pl_name][currency] or 0
db[pl_name][currency] = db[pl_name][currency] + value
save_database(db)
end
function currencies.sub(pl_name, currency, value)
validate_currency(currency)
local db = get_database()
db[pl_name] = db[pl_name] or {}
db[pl_name][currency] = db[pl_name][currency] or 0
db[pl_name][currency] = db[pl_name][currency] - value
save_database(db)
end
function currencies.get(pl_name, currency)
validate_currency(currency)
local db = get_database()
db[pl_name] = db[pl_name] or {}
db[pl_name][currency] = db[pl_name][currency] or 0
return db[pl_name][currency]
end
function save_database(value)
storage:set_string("database", minetest.serialize(value))
end
--[[
database = {
pl_name : string = {
currency_1 : string = value : number,
...
},
...
}
]]
function get_database()
return minetest.deserialize(storage:get_string("database")) or {}
end
function validate_currency(currency)
assert(
registered_currencies[currency],
"A mod is trying to access to the " .. currency .. " currency, but it hasn't been registered."
)
end
dofile(minetest.get_modpath("currencies") .. "/api.lua")
dofile(minetest.get_modpath("currencies") .. "/utils.lua")
dofile(minetest.get_modpath("currencies") .. "/chatcmdbuilder.lua")
dofile(minetest.get_modpath("currencies") .. "/commands.lua")

9
utils.lua Normal file
View File

@ -0,0 +1,9 @@
function currencies.print_error(pl_name, msg)
minetest.chat_send_player(pl_name, minetest.colorize("#e6482e", "Currencies > " .. msg))
end
function currencies.print_msg(pl_name, msg)
minetest.chat_send_player(pl_name, "Currencies > " .. msg)
end