init.lua 18 KB

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