init.lua 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. currency = currency or {}
  2. currency.modpath = minetest.get_modpath("currency")
  3. currency.stackmax = 100
  4. currency.data = currency.data or {}
  5. currency.dirty = true
  6. currency.filename = minetest.get_worldpath() .. "/currency.txt"
  7. -- Localize for performance.
  8. local math_floor = math.floor
  9. local math_min = math.min
  10. local math_max = math.max
  11. -- Test functions. These are also part of the public API, and work with the player's main inventory ("main").
  12. --
  13. -- function currency.room(pname, amount)
  14. -- function currency.add(pname, amount)
  15. -- function currency.remove(pname, amount)
  16. -- function currency.tell(pname)
  17. -- function currency.has(pname, amount)
  18. --
  19. -- Base API functions for managing fungible currency as itemstacks.
  20. --
  21. -- function currency.is_currency(name)
  22. -- function currency.get_stack_value(name, count)
  23. -- function currency.room_for_cash(inv, name, amount)
  24. -- function currency.add_cash(inv, name, amount)
  25. -- function currency.remove_cash(inv, name, amount)
  26. -- function currency.has_cash_amount(inv, name, amount)
  27. -- function currency.get_cash_value(inv, name)
  28. -- function currency.needed_empty_slots(amount)
  29. -- function currency.safe_to_remove_cash(inv, name, amount)
  30. local currency_names = {
  31. "currency:minegeld",
  32. "currency:minegeld_2",
  33. "currency:minegeld_5",
  34. "currency:minegeld_10",
  35. "currency:minegeld_20",
  36. "currency:minegeld_50",
  37. "currency:minegeld_100",
  38. }
  39. local currency_values = {1, 2, 5, 10, 20, 50, 100}
  40. local currency_values_by_name = {
  41. ["currency:minegeld"] = 1,
  42. ["currency:minegeld_2"] = 2,
  43. ["currency:minegeld_5"] = 5,
  44. ["currency:minegeld_10"] = 10,
  45. ["currency:minegeld_20"] = 20,
  46. ["currency:minegeld_50"] = 50,
  47. ["currency:minegeld_100"] = 100,
  48. }
  49. local currency_count = 7 -- Number of denominations.
  50. -- Export as public API (indexed arrays).
  51. currency.note_names = currency_names
  52. currency.note_values = currency_values
  53. -- Obtain the total value given a denomination and a count of the number of banknotes.
  54. function currency.get_stack_value(name, count)
  55. if count <= 0 then
  56. return 0
  57. end
  58. local val = currency_values_by_name[name]
  59. if not val then
  60. return 0
  61. end
  62. return val * count
  63. end
  64. function currency.is_currency(name)
  65. for k, v in ipairs(currency_names) do
  66. if v == name then
  67. return true
  68. end
  69. end
  70. return false
  71. end
  72. -- This computes the number of inventory slots that would be needed to store the
  73. -- given amount of cash as itemstacks, assuming the cash is not combined with any
  74. -- cash stacks already in the inventory. This can be used when it is necessary to
  75. -- absolutely guarantee that an inventory has enough space.
  76. function currency.needed_empty_slots(amount)
  77. local wanted_slots = 0
  78. local stackmax = currency.stackmax
  79. local remainder = amount
  80. local idx = currency_count
  81. while idx > 0 do
  82. local denom = currency_values[idx]
  83. local count = math.modf(remainder / denom)
  84. while count > 0 do
  85. local can_add = math_min(count, stackmax)
  86. remainder = remainder - (can_add * denom)
  87. wanted_slots = wanted_slots + 1
  88. count = count - can_add
  89. end
  90. idx = idx - 1
  91. end
  92. return wanted_slots
  93. end
  94. -- Tell whether the inventory has enough room for the given amount of cash.
  95. -- Try largest denominations first.
  96. -- Note: function assumes that cash stacks are combined whenever possible when adding the cash.
  97. -- However, the order in which cash may be combined with preexisting stacks is not specified.
  98. -- This means that you may need a few empty slots to be available, depending on how the remainder is split up.
  99. -- If no empty slots are found in such a case, this function will return false, even if there would be another possible way to combine the stacks.
  100. -- The solution is to keep your inventory from becoming clogged, so you always have a few empty slots.
  101. function currency.room_for_cash(inv, name, amount)
  102. if amount < 0 then
  103. return true
  104. end
  105. local size = inv:get_size(name)
  106. local stackmax = currency.stackmax
  107. local remainder = amount
  108. local available = {}
  109. -- First, iterate the inventory and find all existing cash stacks.
  110. -- We must sort them so that largest denominations come first.
  111. for i=1, size, 1 do
  112. local stack = inv:get_stack(name, i)
  113. if not stack:is_empty() then
  114. local sn = stack:get_name()
  115. if currency.is_currency(sn) then
  116. table.insert(available, {name=sn, index=i});
  117. end
  118. else
  119. table.insert(available, {name="", index=i})
  120. end
  121. end
  122. -- Sort! Largest denomination first, empty slots last.
  123. table.sort(available,
  124. function(a, b)
  125. -- If the slot is empty (has a blank name) then its value is 0.
  126. local v1 = currency_values_by_name[a.name] or 0
  127. local v2 = currency_values_by_name[b.name] or 0
  128. if v1 > v2 then
  129. return true
  130. end
  131. end)
  132. -- We check each slot individually.
  133. for i=1, #available, 1 do
  134. local stack = inv:get_stack(name, available[i].index)
  135. if stack:is_empty() then
  136. local denom
  137. local count = 0
  138. -- Find the denomination value just smaller than the remaining cash we need to fit.
  139. local idx = currency_count
  140. while count < 1 and idx > 0 do
  141. denom = currency_values[idx]
  142. count = math.modf(remainder / denom)
  143. --minetest.chat_send_player("MustTest", "# Server: Denom is " .. denom)
  144. idx = idx - 1
  145. end
  146. if count > 0 then
  147. local can_add = math_min(count, stackmax)
  148. remainder = remainder - (denom * can_add)
  149. end
  150. else
  151. -- If the stack is not empty, check if it's a currency type.
  152. -- If not a currency type, then we cannot use this inventory slot.
  153. local sn = stack:get_name()
  154. if currency.is_currency(sn) then
  155. local freespace = stack:get_free_space()
  156. if freespace > 0 then
  157. local denom = currency_values_by_name[sn]
  158. local count = math.modf(remainder / denom)
  159. -- We must ignore the slot if its denomination value is larger than the
  160. -- remainding value we need to check space for; this is because we can't
  161. -- put any of that remaining value in this slot. If, on the other hand,
  162. -- the slot's denomination value was smaller than the remaining value,
  163. -- then we could put part of the remaining value in the slot and continue
  164. -- checking other slots for space to hold the rest.
  165. if count > 0 then
  166. local can_add = math_min(count, freespace)
  167. remainder = remainder - (denom * can_add)
  168. end
  169. end
  170. end
  171. end
  172. -- Check if we managed to fit everything.
  173. -- Exit inventory checking as early as possible.
  174. if remainder <= 0 then
  175. return true
  176. end
  177. end
  178. -- Inventory does not have space for cash.
  179. return false
  180. end
  181. -- Test func.
  182. function currency.room(pname, amount)
  183. local player = minetest.get_player_by_name(pname)
  184. if not player or not player:is_player() then
  185. return false
  186. end
  187. local inv = player:get_inventory()
  188. if not inv then
  189. return false
  190. end
  191. local room = currency.room_for_cash(inv, "main", amount)
  192. --[[
  193. if room then
  194. minetest.chat_send_player("MustTest", "# Server: <" .. rename.gpn(pname) .. "> has room for " .. amount .. " minegeld!")
  195. else
  196. minetest.chat_send_player("MustTest", "# Server: <" .. rename.gpn(pname) .. "> does NOT have room for " .. amount .. " minegeld!")
  197. end
  198. --]]
  199. return room
  200. end
  201. -- Try to add the given amount of cash to the inventory.
  202. -- It is not an error if the inventory does not have enough space.
  203. -- Note: it is critical to combine stacks first, before taking up free slots.
  204. -- All cash is guaranteed to be added only if you have first checked if all the cash can fit with `currency.room_for_cash`.
  205. function currency.add_cash(inv, name, amount)
  206. if amount < 0 then
  207. return
  208. end
  209. local size = inv:get_size(name)
  210. local stackmax = currency.stackmax
  211. local remainder = amount
  212. local largest_denom = currency_count
  213. local available = {}
  214. -- First, iterate the inventory and find all existing cash stacks.
  215. -- We must sort them so that largest denominations come first.
  216. for i=1, size, 1 do
  217. local stack = inv:get_stack(name, i)
  218. if not stack:is_empty() then
  219. local sn = stack:get_name()
  220. if currency.is_currency(sn) then
  221. table.insert(available, {name=sn, index=i});
  222. end
  223. else
  224. table.insert(available, {name="", index=i})
  225. end
  226. end
  227. -- Sort! Largest denomination first, empty slots last.
  228. table.sort(available,
  229. function(a, b)
  230. -- If the slot is empty (has a blank name) then its value is 0.
  231. local v1 = currency_values_by_name[a.name] or 0
  232. local v2 = currency_values_by_name[b.name] or 0
  233. if v1 > v2 then
  234. return true
  235. end
  236. end)
  237. -- Now that the slots have been ordered, we can go through them and add the cash as needed.
  238. -- We check each slot individually.
  239. for i=1, #available, 1 do
  240. local stack = inv:get_stack(name, available[i].index)
  241. if stack:is_empty() then
  242. -- Calculate how many of our (current) largest denomination we need to get close to the remaining value.
  243. local count
  244. ::try_again::
  245. count = math.modf(remainder / currency_values[largest_denom])
  246. -- If none of the (current) largest denomination fit, we need to switch to smaller notes.
  247. -- Since the smallest note has a value of 1, then `largest_denom` should never go to 0.
  248. if count <= 0 then
  249. largest_denom = largest_denom - 1
  250. if largest_denom <= 0 then
  251. return -- Should never happen anyway.
  252. end
  253. goto try_again
  254. else
  255. -- Fill this slot with our (current) largest denomination and subtract the value from the remaining value.
  256. local can_add = math_min(count, stackmax)
  257. inv:set_stack(name, available[i].index, ItemStack(currency_names[largest_denom] .. " " .. can_add))
  258. remainder = remainder - (currency_values[largest_denom] * can_add)
  259. end
  260. else
  261. -- If the stack is not empty, check if it's a currency type.
  262. -- If not a currency type, then we cannot use this inventory slot.
  263. local sn = stack:get_name()
  264. if currency.is_currency(sn) then
  265. local freespace = stack:get_free_space()
  266. if freespace > 0 then
  267. -- Calculate how many notes of the slot's denomination we need to try and stuff into this slot to get close to the remaining value.
  268. local count = math.modf(remainder / currency_values_by_name[sn])
  269. -- We must ignore the slot if the denomination value is larger than the remaining cash we need to add.
  270. if count > 0 then
  271. -- Calculate the number of notes we can/should add to this slot.
  272. -- Add them, and subtract the applied value from the remaining value.
  273. local can_add = math_min(count, freespace)
  274. stack:set_count(stack:get_count() + can_add)
  275. inv:set_stack(name, available[i].index, stack)
  276. remainder = remainder - (currency_values_by_name[sn] * can_add)
  277. end
  278. end
  279. end
  280. end
  281. -- If all value was added, we can quit early.
  282. if remainder <= 0 then
  283. return
  284. end
  285. end
  286. end
  287. -- Test func.
  288. function currency.add(pname, amount)
  289. local player = minetest.get_player_by_name(pname)
  290. if not player or not player:is_player() then
  291. return
  292. end
  293. local inv = player:get_inventory()
  294. if not inv then
  295. return
  296. end
  297. currency.add_cash(inv, "main", amount)
  298. end
  299. -- In general, it is always safe to remove a given amount of cash from an inventory,
  300. -- if the SAME amount can be added to it. This is because, while removing cash can
  301. -- cause stacks to be split, those split stacks together will never be more than
  302. -- the total value to be removed, and therefore they can always be safely added
  303. -- if there was room to add the whole value in the first place.
  304. function currency.safe_to_remove_cash(inv, name, amount)
  305. if currency.room_for_cash(inv, name, amount) then
  306. return true
  307. end
  308. return false
  309. end
  310. -- Try to remove a given amount of cash from the inventory.
  311. -- It is not an error if the inventory has less than the wanted amount.
  312. -- Warning: in some cases it is necessary to split large bills into smaller ones
  313. -- in order to remove the requested amount of value! If the inventory does not
  314. -- have enough space to fit the smaller bills once the large bill has been split,
  315. -- then value will be LOST.
  316. function currency.remove_cash(inv, name, amount)
  317. if amount < 0 then
  318. return
  319. end
  320. local available = {}
  321. local remainder = amount
  322. local do_stack_split = false
  323. local size
  324. -- On the first iteration through the inventory, we try to fulfill the removing of cash
  325. -- using just the banknotes in the inventory. If we cannot remove the requested amount
  326. -- of cash using this method, then we iterate the inventory again, this time in order to
  327. -- find and split banknotes as needed.
  328. ::try_again::
  329. -- Will store data relating to all available cash stacks in the inventory.
  330. -- Stores stack name, count, and inventory slot index.
  331. available = {}
  332. -- Iterate the inventory and find all cash stacks.
  333. size = inv:get_size(name)
  334. for i=1, size, 1 do
  335. local stack = inv:get_stack(name, i)
  336. if not stack:is_empty() then
  337. local sn = stack:get_name()
  338. if currency.is_currency(sn) then
  339. table.insert(available, {name=sn, count=stack:get_count(), index=i})
  340. end
  341. end
  342. end
  343. if do_stack_split then
  344. -- Sort table so that SMALLEST denominations come first.
  345. -- This is done in order to prevent the proliferation of small bills in a given inventory,
  346. -- if cash is removed often. Basically, by sorting smallest first, we ensure that
  347. -- smaller bills are consumed first when removing cash.
  348. table.sort(available,
  349. function(a, b)
  350. if currency_values_by_name[a.name] < currency_values_by_name[b.name] then
  351. return true
  352. end
  353. end)
  354. else
  355. -- Sort table so that largest denominations come first.
  356. table.sort(available,
  357. function(a, b)
  358. if currency_values_by_name[a.name] > currency_values_by_name[b.name] then
  359. return true
  360. end
  361. end)
  362. end
  363. -- For each cash stack, remove bits from the inventory until the whole amount
  364. -- of cash to remove has been accounted for. Note: this requires the cash
  365. -- stacks to be sorted largest first!
  366. for k, v in ipairs(available) do
  367. local value = currency_values_by_name[v.name]
  368. local count = math.modf(remainder / value)
  369. if count > 0 then
  370. local can_del = math_min(count, v.count)
  371. local stack = ItemStack(v.name .. " " .. (v.count - can_del))
  372. inv:set_stack(name, v.index, stack)
  373. remainder = remainder - (can_del * value)
  374. else
  375. -- The current cash stack is of a denomination much larger than the remaining cash we need to remove.
  376. -- If this is our second iteration through the cash stacks, then we'll have to split the stack into a smaller denomination.
  377. if do_stack_split then
  378. -- Remove 1 banknote from the stack, this should cover the whole of the remaining amount + some overcost.
  379. local stack = ItemStack(v.name .. " " .. (v.count - 1))
  380. inv:set_stack(name, v.index, stack)
  381. remainder = remainder - value
  382. -- Add back the overcost.
  383. if remainder < 0 then
  384. local add_back = math.abs(remainder)
  385. if add_back > 0 then -- Should never be less than 1, but just in case.
  386. -- If this doesn't fit, oh well, the player has lost some cash.
  387. -- They shouldn't be letting their inventory become clogged!
  388. currency.add_cash(inv, name, add_back) -- Might fail to add the whole amount.
  389. remainder = remainder + add_back
  390. -- We should only have to split a large denomination ONCE. We can exit here.
  391. return
  392. end
  393. end
  394. end
  395. end
  396. if remainder <= 0 then
  397. break
  398. end
  399. end
  400. -- If we didn't remove as much cash as we should have, try again, this time splitting the larger denominations.
  401. if not do_stack_split then
  402. if remainder > 0 then
  403. do_stack_split = true
  404. goto try_again
  405. end
  406. end
  407. end
  408. -- Test func.
  409. function currency.remove(pname, amount)
  410. local player = minetest.get_player_by_name(pname)
  411. if not player or not player:is_player() then
  412. return
  413. end
  414. local inv = player:get_inventory()
  415. if not inv then
  416. return
  417. end
  418. currency.remove_cash(inv, "main", amount)
  419. end
  420. -- Tell whether the inventory has at least a given amount of cash.
  421. function currency.has_cash_amount(inv, name, amount)
  422. return (currency.get_cash_value(inv, name) >= amount)
  423. end
  424. function currency.has(pname, amount)
  425. return (currency.tell(pname) >= amount)
  426. end
  427. -- Get the amount of cash in the inventory.
  428. function currency.get_cash_value(inv, name)
  429. local amount = 0
  430. local size = inv:get_size(name)
  431. for i=1, size, 1 do
  432. local stack = inv:get_stack(name, i)
  433. if not stack:is_empty() then
  434. local n = stack:get_name()
  435. for k, v in ipairs(currency_names) do
  436. if n == v then
  437. amount = amount + (currency_values_by_name[n] * stack:get_count())
  438. break
  439. end
  440. end
  441. end
  442. end
  443. return amount
  444. end
  445. -- Test func.
  446. function currency.tell(pname)
  447. local player = minetest.get_player_by_name(pname)
  448. if not player or not player:is_player() then
  449. return 0
  450. end
  451. local inv = player:get_inventory()
  452. if not inv then
  453. return 0
  454. end
  455. local amount = currency.get_cash_value(inv, "main")
  456. --minetest.chat_send_player("MustTest", "# Server: <" .. rename.gpn(pname) .. "> has " .. amount .. " minegeld!")
  457. return amount
  458. end
  459. -- Helper function to calculate tax based on whether transaction is a purchase or a deposit.
  460. function currency.calculate_tax(amount, type, tax)
  461. local calc_part = function(w, p) local x = (w * p) return x / 100 end
  462. if type == 1 then
  463. -- Purchasing.
  464. local wtax = amount + calc_part(amount, tax)
  465. return math_floor(wtax)
  466. elseif type == 2 then
  467. -- Depositing.
  468. local wtax = amount - calc_part(amount, tax)
  469. wtax = math_max(wtax, 1)
  470. return math_floor(wtax)
  471. end
  472. -- Fallback (should never happen).
  473. return math_floor(amount)
  474. end
  475. -- Shall be called whenever stuff is purchased (vending/depositing) and tax is added/deducted.
  476. -- The tax value is stored so we keep track of how much currency from taxes we have.
  477. function currency.record_tax_income(amount)
  478. if amount <= 0 then
  479. return
  480. end
  481. if not currency.data.taxes_stored then
  482. currency.data.taxes_stored = 0
  483. end
  484. currency.data.taxes_stored = currency.data.taxes_stored + amount
  485. currency.dirty = true
  486. end
  487. function currency.load()
  488. currency.data = {}
  489. local file, err = io.open(currency.filename, "r")
  490. if err then
  491. minetest.log("error", "Failed to open " .. currency.filename .. " for reading: " .. err)
  492. else
  493. local datastring = file:read("*all")
  494. if datastring and datastring ~= "" then
  495. local data = minetest.deserialize(datastring)
  496. if data and type(data) == "table" then
  497. currency.data = data
  498. end
  499. end
  500. file:close()
  501. end
  502. currency.dirty = false
  503. end
  504. function currency.save()
  505. if currency.dirty then
  506. -- Save data.
  507. local file, err = io.open(currency.filename, "w")
  508. if err then
  509. minetest.log("error", "Failed to open " .. currency.filename .. " for writing: " .. err)
  510. else
  511. local datastring = minetest.serialize(currency.data)
  512. if datastring then
  513. file:write(datastring)
  514. end
  515. file:close()
  516. end
  517. end
  518. currency.dirty = false
  519. end
  520. if not currency.registered then
  521. dofile(currency.modpath .. "/craftitems.lua")
  522. dofile(currency.modpath .. "/crafting.lua")
  523. currency.load()
  524. local c = "currency:core"
  525. local f = currency.modpath .. "/init.lua"
  526. reload.register_file(c, f, false)
  527. currency.registered = true
  528. end