1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
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")
|