aboutsummaryrefslogtreecommitdiff
path: root/ch_core/penize.lua
diff options
context:
space:
mode:
Diffstat (limited to 'ch_core/penize.lua')
-rw-r--r--ch_core/penize.lua552
1 files changed, 552 insertions, 0 deletions
diff --git a/ch_core/penize.lua b/ch_core/penize.lua
new file mode 100644
index 0000000..55ed190
--- /dev/null
+++ b/ch_core/penize.lua
@@ -0,0 +1,552 @@
+ch_core.open_submod("penize", {lib = true})
+
+-- ch_core:kcs_{h,kcs,zcs}
+minetest.register_craftitem("ch_core:kcs_h", {
+ description = "haléř československý",
+ inventory_image = "ch_core_kcs_1h.png",
+ stack_max = 10000,
+ groups = {money = 1},
+})
+minetest.register_craftitem("ch_core:kcs_kcs", {
+ description = "koruna československá (Kčs)",
+ inventory_image = "ch_core_kcs_1kcs.png",
+ stack_max = 10000,
+ groups = {money = 2},
+})
+minetest.register_craftitem("ch_core:kcs_zcs", {
+ description = "zlatka československá (Zčs)",
+ inventory_image = "ch_core_kcs_1zcs.png",
+ stack_max = 10000,
+ groups = {money = 3},
+})
+
+local penize = {
+ ["ch_core:kcs_h"] = 1,
+ ["ch_core:kcs_kcs"] = 100,
+ ["ch_core:kcs_zcs"] = 10000,
+}
+
+local payment_methods = {}
+
+--[[
+ Zformátuje částku do textové podoby, např. "-1 235 123,45".
+ Částka může být záporná. Druhá vrácená hodnota je doporučený
+ hexadecimální colorstring pro hodnotu.
+ -- n : int
+ => text : string, colorstring : string
+]]
+function ch_core.formatovat_castku(n)
+ -- minus, halere, string, division, remainder
+ local m, h, s, d, r, color
+ if n < 0 then
+ m = "-"
+ n = -n
+ else
+ m = ""
+ end
+ n = math.ceil(n)
+ if m ~= "" then
+ color = "#bb0000"
+ elseif n < 100 then
+ color = "#ffffff"
+ else
+ color = "#00ff00"
+ end
+ d = math.floor(n / 100.0)
+ r = n - 100.0 * d
+ if r > 0 then
+ h = string.format("%02d", r)
+ else
+ h = "-"
+ end
+ s = string.format("%d", d)
+ if #s > 3 then
+ local t
+ r = #s % 3
+ t = {s:sub(1, r)}
+ s = s:sub(r + 1, -1)
+ while #s >= 3 do
+ table.insert(t, s:sub(1, 3))
+ s = s:sub(4, -1)
+ end
+ s = table.concat(t, " ")
+ end
+ return m..s..","..h, color
+end
+
+--[[
+ Vrátí tabulku ItemStacků s penězi v dané výši. Částka musí být nezáporná.
+ Případné desetinné číslo se zaokrouhlí dolů. Pro nulu vrací prázdnou tabulku.
+]]
+function ch_core.hotovost(castka)
+ local debug = {"puvodni castka: "..castka}
+ local stacks = {}
+ castka = math.floor(castka)
+ if castka < 0 then
+ return stacks
+ end
+ while castka > 10000 * 10000 do -- 10 000 zlatek
+ table.insert(stacks, ItemStack("ch_core:kcs_zcs 10000"))
+ castka = castka - 10000 * 10000
+ table.insert(debug, "ch_core:kcs_zcs 10000 => "..castka)
+ end
+ while castka > 10000 * 100 do -- 10 000 korun
+ local n = math.floor(castka / 10000)
+ assert(n >= 1 and n <= 10000)
+ table.insert(stacks, ItemStack("ch_core:kcs_zcs "..n))
+ castka = castka - n * 10000
+ table.insert(debug, "ch_core:kcs_zcs "..n.." => "..castka)
+ end
+ local n = math.floor(castka / 100)
+ assert(n >= 0 and n <= 10000)
+ if n > 0 then
+ table.insert(stacks, ItemStack("ch_core:kcs_kcs "..n))
+ table.insert(debug, "ch_core:kcs_kcs "..n.." => "..castka)
+ end
+ castka = castka - n * 100
+ if castka > 0 then
+ assert(castka >= 1 and castka <= 100)
+ table.insert(stacks, ItemStack("ch_core:kcs_h "..castka))
+ table.insert(debug, "ch_core:kcs_h "..castka.." => 0")
+ end
+ return stacks
+end
+
+-- 0 = upřednostňovat platby z/na účet
+-- 1 = přijímat v hotovosti, platit z účtu
+-- 2 = přijímat na účet, platit hotově
+-- 3 = upřednostňovat hotovost
+-- 4 = zakázat platby z účtu
+
+--[[
+function ch_core.nastaveni_prichozich_plateb(player_name)
+ local offline_charinfo = ch_data.offline_charinfo[player_name]
+ if offline_charinfo == nil then
+ return {}
+ end
+ local rezim = offline_charinfo.rezim_plateb
+ return {cash = true, bank = true, prefer_cash = rezim ~= 0 and rezim ~= 2}
+end
+
+function ch_core.nastaveni_odchozich_plateb(player_name)
+ local offline_charinfo = ch_data.offline_charinfo[player_name]
+ if offline_charinfo == nil then
+ return {}
+ end
+ local rezim = offline_charinfo.rezim_plateb
+ return {cash = true, bank = rezim ~= 4, prefer_cash = rezim >= 2}
+end
+]]
+--[[
+ Parametr musí být ItemStack, seznam ItemStacků nebo nil.
+ Je-li to seznam, vrátí součet hodnoty všech nalezených peněz (nepeněžní dávky ignoruje).
+ Je-li to dávka peněz, vrátí jejich hodnotu (nezáporné celé číslo).
+ Jinak vrací nil.
+]]
+function ch_core.precist_hotovost(stacks)
+ if stacks == nil then
+ return nil
+ elseif type(stacks) == "table" then
+ local result = 0
+ for _, stack in ipairs(stacks) do
+ local v = penize[stack:get_name()]
+ if v ~= nil then
+ result = result + v * stack:get_count()
+ end
+ end
+ return result
+ else
+ local stack = stacks
+ local v = penize[stack:get_name()]
+ if v ~= nil then
+ return v * stack:get_count()
+ end
+ end
+end
+
+-- current_count, count_to_remove
+-- vrací: count_to_remove_now, hundreds_to_remove
+-- hodnota count_to_remove_now může být i záporné číslo v rozsahu -100 až -1,
+-- v takovém případě značí absolutní hodnota počet mincí, které je nutno přidat
+local function remove100(current_count, count_to_remove)
+ if count_to_remove <= current_count then
+ return count_to_remove, 0
+ end
+ local count_to_remove_ones = count_to_remove % 100
+ local count_to_remove_hundreds = (count_to_remove - count_to_remove_ones) / 100
+ local new_count = current_count - count_to_remove_ones
+ local new_count_hundreds = math.floor(new_count / 100)
+ return count_to_remove_ones + 100 * new_count_hundreds, count_to_remove_hundreds - new_count_hundreds
+end
+
+--[[
+ Pokusí se z uvedených počtů mincí odebrat mince tak,
+ aby byla odebrána přesně zadaná hodnota. Vrátí nil,
+ pokud je hodnota větší než součet hodnoty všech dostupných mincí.
+ - items: table {["ch_core:kcs_h"] = (int >= 0) or nil, ...}
+ - amount: int >= 0
+ - vrací: {ch_core_kcs_1h = int, ...} or nil
+ vrácený údaj značí, kolik mincí je potřeba odebrat z inventáře;
+ může být záporný, v takovém případě uvádí, kolik mincí je
+ potřeba do inventáře přidat
+]]
+function ch_core.rozmenit(items, amount)
+ local current_h = items["ch_core:kcs_h"] or 0
+ local current_kcs = items["ch_core:kcs_kcs"] or 0
+ local current_zcs = items["ch_core:kcs_zcs"] or 0
+ if current_h < 0 or current_kcs < 0 or current_zcs < 0 then
+ error("Chybné zadání rozměňování! "..dump2({items = items, amount = amount}))
+ end
+ local h_to_remove, kcs_to_remove, zcs_to_remove
+ h_to_remove, kcs_to_remove = remove100(current_h, amount)
+ kcs_to_remove, zcs_to_remove = remove100(current_kcs, kcs_to_remove)
+ if zcs_to_remove <= current_zcs then
+ -- verify the result:
+ if (h_to_remove + 100 * kcs_to_remove + 10000 * zcs_to_remove) ~= amount or h_to_remove > current_h or kcs_to_remove > current_kcs then
+ error("Internal error in ch_core.rozmenit(): "..dump2({current_h = current_h, current_kcs = current_kcs, current_zcs = current_zcs, h_to_remove = h_to_remove, kcs_to_remove = kcs_to_remove, zcs_to_remove = zcs_to_remove, amount = amount, items = items, value_to_remove = h_to_remove + 100 * kcs_to_remove + 10000 * zcs_to_remove}))
+ end
+ return {
+ ["ch_core:kcs_h"] = h_to_remove,
+ ["ch_core:kcs_kcs"] = kcs_to_remove,
+ ["ch_core:kcs_zcs"] = zcs_to_remove,
+ }
+ else
+ return nil
+ end
+end
+
+--[[
+ Všechny stacky s penězi v tabulce vyprázdní a vrátí jejich původní
+ celkovou hodnotu.
+ - stacks: table {ItemStack...}
+ - limit: int >= 0 or nil
+ returns: int >= 0 or nil
+]]
+function ch_core.vzit_vsechnu_hotovost(stacks)
+ local castka = 0
+ for _, stack in ipairs(stacks) do
+ local stack_count = stack:get_count()
+ if stack_count > 0 then
+ local value_per_item = penize[stack:get_name()]
+ if value_per_item ~= nil then
+ castka = castka + value_per_item * stack_count
+ stack:clear()
+ end
+ end
+ end
+ return castka
+end
+
+--[[
+ Odečte ze stacků v tabulce peníze maximálně do zadaného limitu
+ a vrátí celkovou odečtenou částku, nebo nil, pokud se nepodaří
+ vrátit drobné.
+ - stacks: table {ItemStack...}
+ - limit: int >= 0 or nil
+ - strict: bool or nil (je-li true, vrátí nil, pokud nemůže odečíst přesně
+ částku „limit“)
+ returns: int >= 0 or nil
+]]
+function ch_core.vzit_hotovost(stacks, limit, strict)
+ -- Odečte ze stacků v tabulce peníze a vrátí celkovou částku.
+ if limit == nil then
+ return ch_core.vzit_vsechnu_hotovost(stacks)
+ end
+ limit = tonumber(limit)
+ if limit == nil or limit < 0 or math.floor(limit) ~= limit then
+ error("ch_core.vzit_hotovost(): limit must be a non-negative integer!")
+ end
+ local items = {
+ [""] = {count = 0, indices = {}},
+ ["ch_core:kcs_h"] = {count = 0, indices = {}},
+ ["ch_core:kcs_kcs"] = {count = 0, indices = {}},
+ ["ch_core:kcs_zcs"] = {count = 0, indices = {}},
+ }
+ for i, stack in ipairs(stacks) do
+ local name = stack:get_name()
+ local info = items[name]
+ if info ~= nil then
+ info.count = info.count + stack:get_count()
+ table.insert(info.indices, i)
+ end
+ end
+ local total_value = items["ch_core:kcs_h"].count +
+ items["ch_core:kcs_kcs"].count * penize["ch_core:kcs_kcs"] +
+ items["ch_core:kcs_zcs"].count * penize["ch_core:kcs_zcs"]
+
+ if total_value <= limit then
+ if strict and total_value ~= limit then
+ return nil
+ end
+
+ for name, info in pairs(items) do
+ if name ~= "" then
+ for _, i in ipairs(info.indices) do
+ stacks[i]:clear()
+ end
+ end
+ end
+ return total_value
+ end
+
+ local new_stacks = {} -- {i = int, stack = ItemStack or false}
+ local next_empty_index = 1
+ local rinfo = ch_core.rozmenit({
+ ["ch_core:kcs_h"] = items["ch_core:kcs_h"].count,
+ ["ch_core:kcs_kcs"] = items["ch_core:kcs_kcs"].count,
+ ["ch_core:kcs_zcs"] = items["ch_core:kcs_zcs"].count,
+ }, limit)
+ for name, info in pairs(items) do
+ if name ~= "" then
+ local count_to_remove = rinfo[name]
+ if count_to_remove < 0 then
+ local stack_to_add = ItemStack(name.." "..(-count_to_remove))
+ -- try to add to the existing stacks
+ local j = 1
+ while not stack_to_add:is_empty() and j <= #info.indices do
+ local i = info.indices[j]
+ local new_stack = ItemStack(stacks[i])
+ stack_to_add = new_stack:add_item(stack_to_add)
+ table.insert(new_stacks, {i = i, stack = new_stack})
+ end
+ if not stack_to_add:is_empty() then
+ -- need an empty stack...
+ local empty_i = items[""].indices[next_empty_index]
+ if empty_i == nil then
+ return nil -- failure
+ end
+ table.insert(new_stacks, {i = empty_i, stack = stack_to_add})
+ next_empty_index = next_empty_index + 1
+ end
+ else
+ while count_to_remove > 0 do
+ for _, i in ipairs(info.indices) do
+ local current_stack = stacks[i]
+ local stack_count = current_stack:get_count()
+ if stack_count < count_to_remove then
+ count_to_remove = count_to_remove - stack_count
+ table.insert(new_stacks, {i = i, stack = ItemStack()})
+ else
+ local new_stack = ItemStack(current_stack)
+ new_stack:take_item(count_to_remove)
+ table.insert(new_stacks, {i = i, stack = new_stack})
+ count_to_remove = 0
+ break
+ end
+ end
+ end
+ assert(count_to_remove == 0)
+ end
+ end
+ end
+
+ -- commit the transaction
+ for _, pair in ipairs(new_stacks) do
+ stacks[pair.i]:replace(pair.stack)
+ end
+ return limit
+end
+
+function ch_core.register_payment_method(name, pay_from_player, pay_to_player)
+ if payment_methods[name] ~= nil then
+ error("payment method "..name.." is already registered!")
+ end
+ if type(pay_from_player) ~= "function" or type(pay_to_player) ~= "function" then
+ error("ch_core.register_payment_method(): invalid type of arguments!")
+ end
+ payment_methods[name] = {pay_from = pay_from_player, pay_to = pay_to_player}
+end
+
+local function build_methods_to_try(options, allow_bank, prefer_cash)
+ if options[1] ~= nil then
+ return options
+ end
+ local methods_to_consider = {}
+ if options.bank ~= false and allow_bank then
+ methods_to_consider.bank = true
+ end
+ if options.smartshop ~= false and options.shop ~= nil then
+ methods_to_consider.smartshop = true
+ elseif options.cash ~= false then
+ methods_to_consider.cash = true
+ end
+
+ local methods_to_try = {}
+ if methods_to_consider.bank and not prefer_cash then
+ table.insert(methods_to_try, "bank")
+ methods_to_consider.bank = nil
+ end
+ if methods_to_consider.smartshop then
+ table.insert(methods_to_try, "smartshop")
+ methods_to_consider.smartshop = nil
+ end
+ if methods_to_consider.cash then
+ table.insert(methods_to_try, "cash")
+ methods_to_consider.cash = nil
+ end
+ if methods_to_consider.bank then
+ table.insert(methods_to_try, "bank")
+ methods_to_consider.bank = nil
+ end
+ for method, _ in pairs(methods_to_consider) do
+ table.insert(methods_to_try, method)
+ end
+ return methods_to_try
+end
+
+local function pay_from_or_to(dir, player_name, amount, options)
+ if options == nil then options = {} end
+ local rezim = (ch_data.offline_charinfo[player_name] or {}).rezim_plateb or 0
+ local methods_to_try
+ if dir == "from" then
+ methods_to_try = build_methods_to_try(options, rezim ~= 4, rezim >= 2)
+ else
+ methods_to_try = build_methods_to_try(options, true, rezim ~= 0 and rezim ~= 2)
+ end
+ local silent = options.simulation and options.silent
+ local errors = {}
+ local i = 1
+ local method = methods_to_try[i]
+ while method ~= nil do
+ local pm = payment_methods[method]
+ if pm ~= nil then
+ local success, error_message
+ if dir == "from" then
+ success, error_message = pm.pay_from(player_name, amount, options)
+ else
+ success, error_message = pm.pay_to(player_name, amount, options)
+ end
+ if success then
+ if not silent then
+ minetest.log("action", "pay_"..dir.."("..player_name..", "..amount..") succeeded with method "..method)
+ end
+ return true, {method = method}
+ end
+ if error_message ~= nil then
+ table.insert(errors, error_message)
+ end
+ end
+ i = i + 1
+ method = methods_to_try[i]
+ end
+ if #errors == 0 then
+ return false, "Nebyla nalezena žádná použitelná platební metoda."
+ end
+ if options.assert == true then
+ error("Payment assertion failed: pay_"..dir.."("..player_name..", "..amount.."): "..dump2({dir = dir, options = options, errors = errors, methods_to_try = methods_to_try}))
+ end
+ if not silent then
+ minetest.log("action", "pay_"..dir.."("..player_name..", "..amount..") failed! "..#methods_to_try.." methods has been tried. Errors: "..dump2(errors))
+ end
+ return false, {errors = errors}
+end
+
+function ch_core.pay_from(player_name, amount, options)
+ return pay_from_or_to("from", player_name, amount, options)
+end
+
+function ch_core.pay_to(player_name, amount, options)
+ return pay_from_or_to("to", player_name, amount, options)
+end
+
+--[[
+options:
+
+ [method] : bool or nil, // je-li false, daná metoda nemá dovoleno běžet
+ a musí vrátit false bez chybového hlášení
+ assert : bool or nil, // je-li true a platba nebude uskutečněna
+ žádnou platební metodou, shodí server. Tato volba je obsluhována
+ přímo ch_core a platební metody by s ní neměly interferovat.
+ silent : bool or nil, // je-li true a je-li i simulation == true,
+ mělo by potlačit obvyklé logování, aby transakce zanechala co nejméně stop
+ simulation : bool or nil, // je-li true, jen vyzkouší, zda může uspět;
+ ve skutečnosti platbu neprovede a nikam nezaznamená
+
+ player_inv : InvRef or nil, // platí pro metodu "cash"; specifikuje
+ inventář, se kterým se má zacházet jako s hráčovým/iným
+ listname : string or nil, // platí pro metodu "cash"; specifikuje
+ listname v inventáři; není-li zadáno, použije se "main"
+
+ label : string or nil, // platí pro metodu "bank";
+ udává poznámku, která se má uložit do záznamu o platebním převodu
+
+ shop : shop_class or nil, // platí pro metodu "smartshop";
+ odkazuje na objekt obchodního terminálu, který se má použít
+ namísto hráčova/ina inventáře
+
+ Další platební metody mohou mít svoje vlastní parametry.
+]]
+
+
+local function cash_pay_from_player(player_name, amount, options)
+ if options.cash == false then return false end
+ local player_inv = options.player_inv
+ if player_inv == nil then
+ local player = minetest.get_player_by_name(player_name)
+ if player == nil then
+ return false, "Postava není ve hře"
+ end
+ player_inv = player:get_inventory()
+ end
+ local silent = options.simulation and options.silent
+ local listname = options.listname or "main"
+ local inv_list = player_inv:get_list(listname)
+ local hotovost_v_inv_pred = ch_core.vzit_hotovost(player_inv:get_list(listname)) or 0
+ local ziskano = ch_core.vzit_hotovost(inv_list, amount)
+ if ziskano ~= amount then
+ if not silent then
+ minetest.log("action", player_name.." failed to pay "..amount.." in cash (got "..(ziskano or "nil")..")")
+ end
+ return false, "V inventáři není dost peněz v hotovosti."
+ end
+ if not options.simulation then
+ player_inv:set_list(listname, inv_list)
+ minetest.log("action", player_name.." payed "..amount.." in cash")
+ local hotovost_v_inv_po = ch_core.vzit_hotovost(inv_list) or 0
+ if hotovost_v_inv_po ~= hotovost_v_inv_pred - amount then
+ error("ERROR in cash_pay_from_player: pred="..hotovost_v_inv_pred..", po="..hotovost_v_inv_po..", amount="..amount)
+ end
+ end
+ return true
+end
+
+local function cash_pay_to_player(player_name, amount, options)
+ if options.cash == false then return false end
+ local player_inv = options.player_inv
+ if player_inv == nil then
+ local player = minetest.get_player_by_name(player_name)
+ if player == nil then
+ return false, "Postava není ve hře"
+ end
+ player_inv = player:get_inventory()
+ end
+ local silent = options.simulation and options.silent
+ local listname = options.listname or "main"
+ local inv_backup = player_inv:get_list(listname)
+ local hotovost_v_inv_pred = ch_core.vzit_hotovost(player_inv:get_list(listname)) or 0
+ local hotovost = ch_core.hotovost(amount)
+ for _, stack in ipairs(hotovost) do
+ local remains = player_inv:add_item(listname, stack)
+ if not remains:is_empty() then
+ -- failure
+ player_inv:set_list(listname, inv_backup)
+ return false, "Plný inventář, platba v hotovosti se do něj nevejde."
+ end
+ end
+ local hotovost_v_inv_po = ch_core.vzit_hotovost(player_inv:get_list(listname)) or 0
+ if hotovost_v_inv_po ~= hotovost_v_inv_pred + amount then
+ error("ERROR in cash_pay_to_player: pred="..hotovost_v_inv_pred..", po="..hotovost_v_inv_po..", amount="..amount)
+ end
+ if options.simulation then
+ player_inv:set_list(listname, inv_backup)
+ return true
+ end
+ if not silent then
+ minetest.log("action", "to "..player_name.." "..amount.." has been payed in cash")
+ end
+ return true
+end
+
+ch_core.register_payment_method("cash", cash_pay_from_player, cash_pay_to_player)
+
+ch_core.close_submod("penize")