diff --git a/api.lua b/api.lua new file mode 100644 index 0000000..551f760 --- /dev/null +++ b/api.lua @@ -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 \ No newline at end of file diff --git a/chatcmdbuilder.lua b/chatcmdbuilder.lua new file mode 100644 index 0000000..705b965 --- /dev/null +++ b/chatcmdbuilder.lua @@ -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 diff --git a/commands.lua b/commands.lua new file mode 100644 index 0000000..d979512 --- /dev/null +++ b/commands.lua @@ -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 + - set + - add + - sub + - clearunregistered: removes from the database every unregistered currency + - clearbalance [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 diff --git a/init.lua b/init.lua index b62a195..ff57141 100644 --- a/init.lua +++ b/init.lua @@ -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 \ No newline at end of file +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") \ No newline at end of file diff --git a/utils.lua b/utils.lua new file mode 100644 index 0000000..d4b53cc --- /dev/null +++ b/utils.lua @@ -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 \ No newline at end of file