123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627 |
- currency = currency or {}
- currency.modpath = minetest.get_modpath("currency")
- currency.stackmax = 100
- currency.data = currency.data or {}
- currency.dirty = true
- currency.filename = minetest.get_worldpath() .. "/currency.txt"
- -- Localize for performance.
- local math_floor = math.floor
- local math_min = math.min
- local math_max = math.max
- -- Test functions. These are also part of the public API, and work with the player's main inventory ("main").
- --
- -- function currency.room(pname, amount)
- -- function currency.add(pname, amount)
- -- function currency.remove(pname, amount)
- -- function currency.tell(pname)
- -- function currency.has(pname, amount)
- --
- -- Base API functions for managing fungible currency as itemstacks.
- --
- -- function currency.is_currency(name)
- -- function currency.get_stack_value(name, count)
- -- function currency.room_for_cash(inv, name, amount)
- -- function currency.add_cash(inv, name, amount)
- -- function currency.remove_cash(inv, name, amount)
- -- function currency.has_cash_amount(inv, name, amount)
- -- function currency.get_cash_value(inv, name)
- -- function currency.needed_empty_slots(amount)
- -- function currency.safe_to_remove_cash(inv, name, amount)
- local currency_names = {
- "currency:minegeld",
- "currency:minegeld_2",
- "currency:minegeld_5",
- "currency:minegeld_10",
- "currency:minegeld_20",
- "currency:minegeld_50",
- "currency:minegeld_100",
- }
- local currency_values = {1, 2, 5, 10, 20, 50, 100}
- local currency_values_by_name = {
- ["currency:minegeld"] = 1,
- ["currency:minegeld_2"] = 2,
- ["currency:minegeld_5"] = 5,
- ["currency:minegeld_10"] = 10,
- ["currency:minegeld_20"] = 20,
- ["currency:minegeld_50"] = 50,
- ["currency:minegeld_100"] = 100,
- }
- local currency_count = 7 -- Number of denominations.
- -- Export as public API (indexed arrays).
- currency.note_names = currency_names
- currency.note_values = currency_values
- -- Obtain the total value given a denomination and a count of the number of banknotes.
- function currency.get_stack_value(name, count)
- if count <= 0 then
- return 0
- end
- local val = currency_values_by_name[name]
- if not val then
- return 0
- end
- return val * count
- end
- function currency.is_currency(name)
- for k, v in ipairs(currency_names) do
- if v == name then
- return true
- end
- end
- return false
- end
- -- This computes the number of inventory slots that would be needed to store the
- -- given amount of cash as itemstacks, assuming the cash is not combined with any
- -- cash stacks already in the inventory. This can be used when it is necessary to
- -- absolutely guarantee that an inventory has enough space.
- function currency.needed_empty_slots(amount)
- local wanted_slots = 0
- local stackmax = currency.stackmax
- local remainder = amount
- local idx = currency_count
- while idx > 0 do
- local denom = currency_values[idx]
- local count = math.modf(remainder / denom)
- while count > 0 do
- local can_add = math_min(count, stackmax)
- remainder = remainder - (can_add * denom)
- wanted_slots = wanted_slots + 1
- count = count - can_add
- end
- idx = idx - 1
- end
- return wanted_slots
- end
- -- Tell whether the inventory has enough room for the given amount of cash.
- -- Try largest denominations first.
- -- Note: function assumes that cash stacks are combined whenever possible when adding the cash.
- -- However, the order in which cash may be combined with preexisting stacks is not specified.
- -- This means that you may need a few empty slots to be available, depending on how the remainder is split up.
- -- 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.
- -- The solution is to keep your inventory from becoming clogged, so you always have a few empty slots.
- function currency.room_for_cash(inv, name, amount)
- if amount < 0 then
- return true
- end
- local size = inv:get_size(name)
- local stackmax = currency.stackmax
- local remainder = amount
- local available = {}
- -- First, iterate the inventory and find all existing cash stacks.
- -- We must sort them so that largest denominations come first.
- for i=1, size, 1 do
- local stack = inv:get_stack(name, i)
- if not stack:is_empty() then
- local sn = stack:get_name()
- if currency.is_currency(sn) then
- table.insert(available, {name=sn, index=i});
- end
- else
- table.insert(available, {name="", index=i})
- end
- end
- -- Sort! Largest denomination first, empty slots last.
- table.sort(available,
- function(a, b)
- -- If the slot is empty (has a blank name) then its value is 0.
- local v1 = currency_values_by_name[a.name] or 0
- local v2 = currency_values_by_name[b.name] or 0
- if v1 > v2 then
- return true
- end
- end)
- -- We check each slot individually.
- for i=1, #available, 1 do
- local stack = inv:get_stack(name, available[i].index)
- if stack:is_empty() then
- local denom
- local count = 0
- -- Find the denomination value just smaller than the remaining cash we need to fit.
- local idx = currency_count
- while count < 1 and idx > 0 do
- denom = currency_values[idx]
- count = math.modf(remainder / denom)
- --minetest.chat_send_player("MustTest", "# Server: Denom is " .. denom)
- idx = idx - 1
- end
- if count > 0 then
- local can_add = math_min(count, stackmax)
- remainder = remainder - (denom * can_add)
- end
- else
- -- If the stack is not empty, check if it's a currency type.
- -- If not a currency type, then we cannot use this inventory slot.
- local sn = stack:get_name()
- if currency.is_currency(sn) then
- local freespace = stack:get_free_space()
- if freespace > 0 then
- local denom = currency_values_by_name[sn]
- local count = math.modf(remainder / denom)
- -- We must ignore the slot if its denomination value is larger than the
- -- remainding value we need to check space for; this is because we can't
- -- put any of that remaining value in this slot. If, on the other hand,
- -- the slot's denomination value was smaller than the remaining value,
- -- then we could put part of the remaining value in the slot and continue
- -- checking other slots for space to hold the rest.
- if count > 0 then
- local can_add = math_min(count, freespace)
- remainder = remainder - (denom * can_add)
- end
- end
- end
- end
- -- Check if we managed to fit everything.
- -- Exit inventory checking as early as possible.
- if remainder <= 0 then
- return true
- end
- end
- -- Inventory does not have space for cash.
- return false
- end
- -- Test func.
- function currency.room(pname, amount)
- local player = minetest.get_player_by_name(pname)
- if not player or not player:is_player() then
- return false
- end
- local inv = player:get_inventory()
- if not inv then
- return false
- end
- local room = currency.room_for_cash(inv, "main", amount)
- --[[
- if room then
- minetest.chat_send_player("MustTest", "# Server: <" .. rename.gpn(pname) .. "> has room for " .. amount .. " minegeld!")
- else
- minetest.chat_send_player("MustTest", "# Server: <" .. rename.gpn(pname) .. "> does NOT have room for " .. amount .. " minegeld!")
- end
- --]]
- return room
- end
- -- Try to add the given amount of cash to the inventory.
- -- It is not an error if the inventory does not have enough space.
- -- Note: it is critical to combine stacks first, before taking up free slots.
- -- All cash is guaranteed to be added only if you have first checked if all the cash can fit with `currency.room_for_cash`.
- function currency.add_cash(inv, name, amount)
- if amount < 0 then
- return
- end
- local size = inv:get_size(name)
- local stackmax = currency.stackmax
- local remainder = amount
- local largest_denom = currency_count
- local available = {}
- -- First, iterate the inventory and find all existing cash stacks.
- -- We must sort them so that largest denominations come first.
- for i=1, size, 1 do
- local stack = inv:get_stack(name, i)
- if not stack:is_empty() then
- local sn = stack:get_name()
- if currency.is_currency(sn) then
- table.insert(available, {name=sn, index=i});
- end
- else
- table.insert(available, {name="", index=i})
- end
- end
- -- Sort! Largest denomination first, empty slots last.
- table.sort(available,
- function(a, b)
- -- If the slot is empty (has a blank name) then its value is 0.
- local v1 = currency_values_by_name[a.name] or 0
- local v2 = currency_values_by_name[b.name] or 0
- if v1 > v2 then
- return true
- end
- end)
- -- Now that the slots have been ordered, we can go through them and add the cash as needed.
- -- We check each slot individually.
- for i=1, #available, 1 do
- local stack = inv:get_stack(name, available[i].index)
- if stack:is_empty() then
- -- Calculate how many of our (current) largest denomination we need to get close to the remaining value.
- local count
- ::try_again::
- count = math.modf(remainder / currency_values[largest_denom])
- -- If none of the (current) largest denomination fit, we need to switch to smaller notes.
- -- Since the smallest note has a value of 1, then `largest_denom` should never go to 0.
- if count <= 0 then
- largest_denom = largest_denom - 1
- if largest_denom <= 0 then
- return -- Should never happen anyway.
- end
- goto try_again
- else
- -- Fill this slot with our (current) largest denomination and subtract the value from the remaining value.
- local can_add = math_min(count, stackmax)
- inv:set_stack(name, available[i].index, ItemStack(currency_names[largest_denom] .. " " .. can_add))
- remainder = remainder - (currency_values[largest_denom] * can_add)
- end
- else
- -- If the stack is not empty, check if it's a currency type.
- -- If not a currency type, then we cannot use this inventory slot.
- local sn = stack:get_name()
- if currency.is_currency(sn) then
- local freespace = stack:get_free_space()
- if freespace > 0 then
- -- 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.
- local count = math.modf(remainder / currency_values_by_name[sn])
- -- We must ignore the slot if the denomination value is larger than the remaining cash we need to add.
- if count > 0 then
- -- Calculate the number of notes we can/should add to this slot.
- -- Add them, and subtract the applied value from the remaining value.
- local can_add = math_min(count, freespace)
- stack:set_count(stack:get_count() + can_add)
- inv:set_stack(name, available[i].index, stack)
- remainder = remainder - (currency_values_by_name[sn] * can_add)
- end
- end
- end
- end
- -- If all value was added, we can quit early.
- if remainder <= 0 then
- return
- end
- end
- end
- -- Test func.
- function currency.add(pname, amount)
- local player = minetest.get_player_by_name(pname)
- if not player or not player:is_player() then
- return
- end
- local inv = player:get_inventory()
- if not inv then
- return
- end
- currency.add_cash(inv, "main", amount)
- end
- -- In general, it is always safe to remove a given amount of cash from an inventory,
- -- if the SAME amount can be added to it. This is because, while removing cash can
- -- cause stacks to be split, those split stacks together will never be more than
- -- the total value to be removed, and therefore they can always be safely added
- -- if there was room to add the whole value in the first place.
- function currency.safe_to_remove_cash(inv, name, amount)
- if currency.room_for_cash(inv, name, amount) then
- return true
- end
- return false
- end
- -- Try to remove a given amount of cash from the inventory.
- -- It is not an error if the inventory has less than the wanted amount.
- -- Warning: in some cases it is necessary to split large bills into smaller ones
- -- in order to remove the requested amount of value! If the inventory does not
- -- have enough space to fit the smaller bills once the large bill has been split,
- -- then value will be LOST.
- function currency.remove_cash(inv, name, amount)
- if amount < 0 then
- return
- end
- local available = {}
- local remainder = amount
- local do_stack_split = false
- local size
- -- On the first iteration through the inventory, we try to fulfill the removing of cash
- -- using just the banknotes in the inventory. If we cannot remove the requested amount
- -- of cash using this method, then we iterate the inventory again, this time in order to
- -- find and split banknotes as needed.
- ::try_again::
- -- Will store data relating to all available cash stacks in the inventory.
- -- Stores stack name, count, and inventory slot index.
- available = {}
- -- Iterate the inventory and find all cash stacks.
- size = inv:get_size(name)
- for i=1, size, 1 do
- local stack = inv:get_stack(name, i)
- if not stack:is_empty() then
- local sn = stack:get_name()
- if currency.is_currency(sn) then
- table.insert(available, {name=sn, count=stack:get_count(), index=i})
- end
- end
- end
- if do_stack_split then
- -- Sort table so that SMALLEST denominations come first.
- -- This is done in order to prevent the proliferation of small bills in a given inventory,
- -- if cash is removed often. Basically, by sorting smallest first, we ensure that
- -- smaller bills are consumed first when removing cash.
- table.sort(available,
- function(a, b)
- if currency_values_by_name[a.name] < currency_values_by_name[b.name] then
- return true
- end
- end)
- else
- -- Sort table so that largest denominations come first.
- table.sort(available,
- function(a, b)
- if currency_values_by_name[a.name] > currency_values_by_name[b.name] then
- return true
- end
- end)
- end
- -- For each cash stack, remove bits from the inventory until the whole amount
- -- of cash to remove has been accounted for. Note: this requires the cash
- -- stacks to be sorted largest first!
- for k, v in ipairs(available) do
- local value = currency_values_by_name[v.name]
- local count = math.modf(remainder / value)
- if count > 0 then
- local can_del = math_min(count, v.count)
- local stack = ItemStack(v.name .. " " .. (v.count - can_del))
- inv:set_stack(name, v.index, stack)
- remainder = remainder - (can_del * value)
- else
- -- The current cash stack is of a denomination much larger than the remaining cash we need to remove.
- -- If this is our second iteration through the cash stacks, then we'll have to split the stack into a smaller denomination.
- if do_stack_split then
- -- Remove 1 banknote from the stack, this should cover the whole of the remaining amount + some overcost.
- local stack = ItemStack(v.name .. " " .. (v.count - 1))
- inv:set_stack(name, v.index, stack)
- remainder = remainder - value
- -- Add back the overcost.
- if remainder < 0 then
- local add_back = math.abs(remainder)
- if add_back > 0 then -- Should never be less than 1, but just in case.
- -- If this doesn't fit, oh well, the player has lost some cash.
- -- They shouldn't be letting their inventory become clogged!
- currency.add_cash(inv, name, add_back) -- Might fail to add the whole amount.
- remainder = remainder + add_back
- -- We should only have to split a large denomination ONCE. We can exit here.
- return
- end
- end
- end
- end
- if remainder <= 0 then
- break
- end
- end
- -- If we didn't remove as much cash as we should have, try again, this time splitting the larger denominations.
- if not do_stack_split then
- if remainder > 0 then
- do_stack_split = true
- goto try_again
- end
- end
- end
- -- Test func.
- function currency.remove(pname, amount)
- local player = minetest.get_player_by_name(pname)
- if not player or not player:is_player() then
- return
- end
- local inv = player:get_inventory()
- if not inv then
- return
- end
- currency.remove_cash(inv, "main", amount)
- end
- -- Tell whether the inventory has at least a given amount of cash.
- function currency.has_cash_amount(inv, name, amount)
- return (currency.get_cash_value(inv, name) >= amount)
- end
- function currency.has(pname, amount)
- return (currency.tell(pname) >= amount)
- end
- -- Get the amount of cash in the inventory.
- function currency.get_cash_value(inv, name)
- local amount = 0
- local size = inv:get_size(name)
- for i=1, size, 1 do
- local stack = inv:get_stack(name, i)
- if not stack:is_empty() then
- local n = stack:get_name()
- for k, v in ipairs(currency_names) do
- if n == v then
- amount = amount + (currency_values_by_name[n] * stack:get_count())
- break
- end
- end
- end
- end
- return amount
- end
- -- Test func.
- function currency.tell(pname)
- local player = minetest.get_player_by_name(pname)
- if not player or not player:is_player() then
- return 0
- end
- local inv = player:get_inventory()
- if not inv then
- return 0
- end
- local amount = currency.get_cash_value(inv, "main")
- --minetest.chat_send_player("MustTest", "# Server: <" .. rename.gpn(pname) .. "> has " .. amount .. " minegeld!")
- return amount
- end
- -- Helper function to calculate tax based on whether transaction is a purchase or a deposit.
- function currency.calculate_tax(amount, type, tax)
- local calc_part = function(w, p) local x = (w * p) return x / 100 end
- if type == 1 then
- -- Purchasing.
- local wtax = amount + calc_part(amount, tax)
- return math_floor(wtax)
- elseif type == 2 then
- -- Depositing.
- local wtax = amount - calc_part(amount, tax)
- wtax = math_max(wtax, 1)
- return math_floor(wtax)
- end
- -- Fallback (should never happen).
- return math_floor(amount)
- end
- -- Shall be called whenever stuff is purchased (vending/depositing) and tax is added/deducted.
- -- The tax value is stored so we keep track of how much currency from taxes we have.
- function currency.record_tax_income(amount)
- if amount <= 0 then
- return
- end
- if not currency.data.taxes_stored then
- currency.data.taxes_stored = 0
- end
- currency.data.taxes_stored = currency.data.taxes_stored + amount
- currency.dirty = true
- end
- function currency.load()
- currency.data = {}
- local file, err = io.open(currency.filename, "r")
- if err then
- minetest.log("error", "Failed to open " .. currency.filename .. " for reading: " .. err)
- else
- local datastring = file:read("*all")
- if datastring and datastring ~= "" then
- local data = minetest.deserialize(datastring)
- if data and type(data) == "table" then
- currency.data = data
- end
- end
- file:close()
- end
- currency.dirty = false
- end
- function currency.save()
- if currency.dirty then
- -- Save data.
- local file, err = io.open(currency.filename, "w")
- if err then
- minetest.log("error", "Failed to open " .. currency.filename .. " for writing: " .. err)
- else
- local datastring = minetest.serialize(currency.data)
- if datastring then
- file:write(datastring)
- end
- file:close()
- end
- end
- currency.dirty = false
- end
- if not currency.registered then
- dofile(currency.modpath .. "/craftitems.lua")
- dofile(currency.modpath .. "/crafting.lua")
- currency.load()
- local c = "currency:core"
- local f = currency.modpath .. "/init.lua"
- reload.register_file(c, f, false)
- currency.registered = true
- end
|