init.lua 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. if not minetest.global_exists("currency") then currency = {} end
  2. currency.modpath = minetest.get_modpath("currency")
  3. currency.stackmax = 256
  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. idx = idx - 1
  144. end
  145. if count > 0 then
  146. local can_add = math_min(count, stackmax)
  147. remainder = remainder - (denom * can_add)
  148. end
  149. else
  150. -- If the stack is not empty, check if it's a currency type.
  151. -- If not a currency type, then we cannot use this inventory slot.
  152. local sn = stack:get_name()
  153. if currency.is_currency(sn) then
  154. local freespace = stack:get_free_space()
  155. if freespace > 0 then
  156. local denom = currency_values_by_name[sn]
  157. local count = math.modf(remainder / denom)
  158. -- We must ignore the slot if its denomination value is larger than the
  159. -- remainding value we need to check space for; this is because we can't
  160. -- put any of that remaining value in this slot. If, on the other hand,
  161. -- the slot's denomination value was smaller than the remaining value,
  162. -- then we could put part of the remaining value in the slot and continue
  163. -- checking other slots for space to hold the rest.
  164. if count > 0 then
  165. local can_add = math_min(count, freespace)
  166. remainder = remainder - (denom * can_add)
  167. end
  168. end
  169. end
  170. end
  171. -- Check if we managed to fit everything.
  172. -- Exit inventory checking as early as possible.
  173. if remainder <= 0 then
  174. return true
  175. end
  176. end
  177. -- Inventory does not have space for cash.
  178. return false
  179. end
  180. -- Test func.
  181. function currency.room(pname, amount)
  182. local player = minetest.get_player_by_name(pname)
  183. if not player or not player:is_player() then
  184. return false
  185. end
  186. local inv = player:get_inventory()
  187. if not inv then
  188. return false
  189. end
  190. local room = currency.room_for_cash(inv, "main", amount)
  191. return room
  192. end
  193. -- Try to add the given amount of cash to the inventory.
  194. -- It is not an error if the inventory does not have enough space.
  195. -- Note: it is critical to combine stacks first, before taking up free slots.
  196. -- All cash is guaranteed to be added only if you have first checked if all the cash can fit with `currency.room_for_cash`.
  197. function currency.add_cash(inv, name, amount)
  198. if amount < 0 then
  199. return
  200. end
  201. local size = inv:get_size(name)
  202. local stackmax = currency.stackmax
  203. local remainder = amount
  204. local largest_denom = currency_count
  205. local available = {}
  206. -- First, iterate the inventory and find all existing cash stacks.
  207. -- We must sort them so that largest denominations come first.
  208. for i=1, size, 1 do
  209. local stack = inv:get_stack(name, i)
  210. if not stack:is_empty() then
  211. local sn = stack:get_name()
  212. if currency.is_currency(sn) then
  213. table.insert(available, {name=sn, index=i});
  214. end
  215. else
  216. table.insert(available, {name="", index=i})
  217. end
  218. end
  219. -- Sort! Largest denomination first, empty slots last.
  220. table.sort(available,
  221. function(a, b)
  222. -- If the slot is empty (has a blank name) then its value is 0.
  223. local v1 = currency_values_by_name[a.name] or 0
  224. local v2 = currency_values_by_name[b.name] or 0
  225. if v1 > v2 then
  226. return true
  227. end
  228. end)
  229. -- Now that the slots have been ordered, we can go through them and add the cash as needed.
  230. -- We check each slot individually.
  231. for i=1, #available, 1 do
  232. local stack = inv:get_stack(name, available[i].index)
  233. if stack:is_empty() then
  234. -- Calculate how many of our (current) largest denomination we need to get close to the remaining value.
  235. local count
  236. ::try_again::
  237. count = math.modf(remainder / currency_values[largest_denom])
  238. -- If none of the (current) largest denomination fit, we need to switch to smaller notes.
  239. -- Since the smallest note has a value of 1, then `largest_denom` should never go to 0.
  240. if count <= 0 then
  241. largest_denom = largest_denom - 1
  242. if largest_denom <= 0 then
  243. return -- Should never happen anyway.
  244. end
  245. goto try_again
  246. else
  247. -- Fill this slot with our (current) largest denomination and subtract the value from the remaining value.
  248. local can_add = math_min(count, stackmax)
  249. inv:set_stack(name, available[i].index, ItemStack(currency_names[largest_denom] .. " " .. can_add))
  250. remainder = remainder - (currency_values[largest_denom] * can_add)
  251. end
  252. else
  253. -- If the stack is not empty, check if it's a currency type.
  254. -- If not a currency type, then we cannot use this inventory slot.
  255. local sn = stack:get_name()
  256. if currency.is_currency(sn) then
  257. local freespace = stack:get_free_space()
  258. if freespace > 0 then
  259. -- 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.
  260. local count = math.modf(remainder / currency_values_by_name[sn])
  261. -- We must ignore the slot if the denomination value is larger than the remaining cash we need to add.
  262. if count > 0 then
  263. -- Calculate the number of notes we can/should add to this slot.
  264. -- Add them, and subtract the applied value from the remaining value.
  265. local can_add = math_min(count, freespace)
  266. stack:set_count(stack:get_count() + can_add)
  267. inv:set_stack(name, available[i].index, stack)
  268. remainder = remainder - (currency_values_by_name[sn] * can_add)
  269. end
  270. end
  271. end
  272. end
  273. -- If all value was added, we can quit early.
  274. if remainder <= 0 then
  275. return
  276. end
  277. end
  278. end
  279. -- Test func.
  280. function currency.add(pname, amount)
  281. local player = minetest.get_player_by_name(pname)
  282. if not player or not player:is_player() then
  283. return
  284. end
  285. local inv = player:get_inventory()
  286. if not inv then
  287. return
  288. end
  289. currency.add_cash(inv, "main", amount)
  290. end
  291. -- In general, it is always safe to remove a given amount of cash from an inventory,
  292. -- if the SAME amount can be added to it. This is because, while removing cash can
  293. -- cause stacks to be split, those split stacks together will never be more than
  294. -- the total value to be removed, and therefore they can always be safely added
  295. -- if there was room to add the whole value in the first place.
  296. function currency.safe_to_remove_cash(inv, name, amount)
  297. if currency.room_for_cash(inv, name, amount) then
  298. return true
  299. end
  300. return false
  301. end
  302. -- Try to remove a given amount of cash from the inventory.
  303. -- It is not an error if the inventory has less than the wanted amount.
  304. -- Warning: in some cases it is necessary to split large bills into smaller ones
  305. -- in order to remove the requested amount of value! If the inventory does not
  306. -- have enough space to fit the smaller bills once the large bill has been split,
  307. -- then value will be LOST.
  308. function currency.remove_cash(inv, name, amount)
  309. if amount < 0 then
  310. return
  311. end
  312. local available = {}
  313. local remainder = amount
  314. local do_stack_split = false
  315. local size
  316. -- On the first iteration through the inventory, we try to fulfill the removing of cash
  317. -- using just the banknotes in the inventory. If we cannot remove the requested amount
  318. -- of cash using this method, then we iterate the inventory again, this time in order to
  319. -- find and split banknotes as needed.
  320. ::try_again::
  321. -- Will store data relating to all available cash stacks in the inventory.
  322. -- Stores stack name, count, and inventory slot index.
  323. available = {}
  324. -- Iterate the inventory and find all cash stacks.
  325. size = inv:get_size(name)
  326. for i=1, size, 1 do
  327. local stack = inv:get_stack(name, i)
  328. if not stack:is_empty() then
  329. local sn = stack:get_name()
  330. if currency.is_currency(sn) then
  331. table.insert(available, {name=sn, count=stack:get_count(), index=i})
  332. end
  333. end
  334. end
  335. if do_stack_split then
  336. -- Sort table so that SMALLEST denominations come first.
  337. -- This is done in order to prevent the proliferation of small bills in a given inventory,
  338. -- if cash is removed often. Basically, by sorting smallest first, we ensure that
  339. -- smaller bills are consumed first when removing cash.
  340. table.sort(available,
  341. function(a, b)
  342. if currency_values_by_name[a.name] < currency_values_by_name[b.name] then
  343. return true
  344. end
  345. end)
  346. else
  347. -- Sort table so that largest denominations come first.
  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. end
  355. -- For each cash stack, remove bits from the inventory until the whole amount
  356. -- of cash to remove has been accounted for. Note: this requires the cash
  357. -- stacks to be sorted largest first!
  358. for k, v in ipairs(available) do
  359. local value = currency_values_by_name[v.name]
  360. local count = math.modf(remainder / value)
  361. if count > 0 then
  362. local can_del = math_min(count, v.count)
  363. local stack = ItemStack(v.name .. " " .. (v.count - can_del))
  364. inv:set_stack(name, v.index, stack)
  365. remainder = remainder - (can_del * value)
  366. else
  367. -- The current cash stack is of a denomination much larger than the remaining cash we need to remove.
  368. -- If this is our second iteration through the cash stacks, then we'll have to split the stack into a smaller denomination.
  369. if do_stack_split then
  370. -- Remove 1 banknote from the stack, this should cover the whole of the remaining amount + some overcost.
  371. local stack = ItemStack(v.name .. " " .. (v.count - 1))
  372. inv:set_stack(name, v.index, stack)
  373. remainder = remainder - value
  374. -- Add back the overcost.
  375. if remainder < 0 then
  376. local add_back = math.abs(remainder)
  377. if add_back > 0 then -- Should never be less than 1, but just in case.
  378. -- If this doesn't fit, oh well, the player has lost some cash.
  379. -- They shouldn't be letting their inventory become clogged!
  380. currency.add_cash(inv, name, add_back) -- Might fail to add the whole amount.
  381. remainder = remainder + add_back
  382. -- We should only have to split a large denomination ONCE. We can exit here.
  383. return
  384. end
  385. end
  386. end
  387. end
  388. if remainder <= 0 then
  389. break
  390. end
  391. end
  392. -- If we didn't remove as much cash as we should have, try again, this time splitting the larger denominations.
  393. if not do_stack_split then
  394. if remainder > 0 then
  395. do_stack_split = true
  396. goto try_again
  397. end
  398. end
  399. end
  400. -- Test func.
  401. function currency.remove(pname, amount)
  402. local player = minetest.get_player_by_name(pname)
  403. if not player or not player:is_player() then
  404. return
  405. end
  406. local inv = player:get_inventory()
  407. if not inv then
  408. return
  409. end
  410. currency.remove_cash(inv, "main", amount)
  411. end
  412. -- Tell whether the inventory has at least a given amount of cash.
  413. function currency.has_cash_amount(inv, name, amount)
  414. return (currency.get_cash_value(inv, name) >= amount)
  415. end
  416. function currency.has(pname, amount)
  417. return (currency.tell(pname) >= amount)
  418. end
  419. -- Get the amount of cash in the inventory.
  420. function currency.get_cash_value(inv, name)
  421. local amount = 0
  422. local size = inv:get_size(name)
  423. for i=1, size, 1 do
  424. local stack = inv:get_stack(name, i)
  425. if not stack:is_empty() then
  426. local n = stack:get_name()
  427. for k, v in ipairs(currency_names) do
  428. if n == v then
  429. amount = amount + (currency_values_by_name[n] * stack:get_count())
  430. break
  431. end
  432. end
  433. end
  434. end
  435. return amount
  436. end
  437. -- Test func.
  438. function currency.tell(pname)
  439. local player = minetest.get_player_by_name(pname)
  440. if not player or not player:is_player() then
  441. return 0
  442. end
  443. local inv = player:get_inventory()
  444. if not inv then
  445. return 0
  446. end
  447. local amount = currency.get_cash_value(inv, "main")
  448. return amount
  449. end
  450. -- Helper function to calculate tax based on whether transaction is a purchase or a deposit.
  451. function currency.calculate_tax(amount, type, tax)
  452. local calc_part = function(w, p) local x = (w * p) return x / 100 end
  453. if type == 1 then
  454. -- Purchasing.
  455. local wtax = amount + calc_part(amount, tax)
  456. return math_floor(wtax)
  457. elseif type == 2 then
  458. -- Depositing.
  459. local wtax = amount - calc_part(amount, tax)
  460. wtax = math_max(wtax, 1)
  461. return math_floor(wtax)
  462. end
  463. -- Fallback (should never happen).
  464. return math_floor(amount)
  465. end
  466. -- Shall be called whenever stuff is purchased (vending/depositing) and tax is added/deducted.
  467. -- The tax value is stored so we keep track of how much currency from taxes we have.
  468. function currency.record_tax_income(amount)
  469. if amount <= 0 then
  470. return
  471. end
  472. if not currency.data.taxes_stored then
  473. currency.data.taxes_stored = 0
  474. end
  475. currency.data.taxes_stored = currency.data.taxes_stored + amount
  476. currency.dirty = true
  477. end
  478. function currency.load()
  479. currency.data = {}
  480. local file, err = io.open(currency.filename, "r")
  481. if err then
  482. if not err:find("No such file") then
  483. minetest.log("error", "Failed to open " .. currency.filename .. " for reading: " .. err)
  484. end
  485. else
  486. local datastring = file:read("*all")
  487. if datastring and datastring ~= "" then
  488. local data = minetest.deserialize(datastring)
  489. if data and type(data) == "table" then
  490. currency.data = data
  491. end
  492. end
  493. file:close()
  494. end
  495. currency.dirty = false
  496. end
  497. function currency.save()
  498. if currency.dirty then
  499. -- Save data.
  500. local file, err = io.open(currency.filename, "w")
  501. if err then
  502. minetest.log("error", "Failed to open " .. currency.filename .. " for writing: " .. err)
  503. else
  504. local datastring = minetest.serialize(currency.data)
  505. if datastring then
  506. file:write(datastring)
  507. end
  508. file:close()
  509. end
  510. end
  511. currency.dirty = false
  512. end
  513. if not currency.registered then
  514. dofile(currency.modpath .. "/craftitems.lua")
  515. dofile(currency.modpath .. "/crafting.lua")
  516. currency.load()
  517. local c = "currency:core"
  518. local f = currency.modpath .. "/init.lua"
  519. reload.register_file(c, f, false)
  520. currency.registered = true
  521. end