diff options
Diffstat (limited to 'ch_core/penize.lua')
-rw-r--r-- | ch_core/penize.lua | 552 |
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") |