--[[ Core API and Data Handling -------------------------- This file manages all low-level reading from and writing to the 'accounts' data file. It provides a safe and centralized API for all other mod files to interact with player data. --]] local world_path = minetest.get_worldpath() local file_path = world_path .. "/accounts" -- Normalizes a string to use '.' as a decimal separator and converts to a number. -- Handles both "1,23" and "1.23". function normalize_and_tonumber(str) if not str then return nil end str = str:gsub(",", ".") return tonumber(str) end -- Internal function to read the entire accounts database file. -- Returns a populated table or a clean default structure on any error. local function read_accounts_file() local f = io.open(file_path, "r") if not f then return { balance = {}, pin = {}, credit = {}, history = {} } end local data = f:read("*a") f:close() if data == "" or data == nil then return { balance = {}, pin = {}, credit = {}, history = {} } end return minetest.deserialize(data) or { balance = {}, pin = {}, credit = {}, history = {} } end -- Internal function to write the entire accounts database file. -- Includes a locking mechanism to prevent race conditions with the interest script. local function save_accounts_file(data) -- If the daily interest calculation is running, wait in short intervals until it's done. while bank_accounts and bank_accounts.is_calculating_interest do minetest.log("action", "[bank_accounts] Interest calculation in progress, delaying save operation...") minetest.sleep(0.5) end local f, err = io.open(file_path, "w") if not f then minetest.log("error", "[bank_accounts] Could not open accounts file for writing: " .. tostring(err)) return false end f:write(minetest.serialize(data)) f:close() return true end -- Internal function to log a single transaction to a player's history. local function log_transaction(data, player_name, account_type, amount, new_total, trans_type, purpose, other_party) -- Ensure history tables exist. if not data.history then data.history = {} end if not data.history[player_name] then data.history[player_name] = {} end local transaction = { timestamp = os.time(), type = trans_type or "unknown", account = account_type, -- "balance" or "credit" amount = amount, new_total = new_total, purpose = purpose or "", -- e.g., "Teller: Rage87" or user-defined text other = other_party or "" -- e.g., recipient or sender name } -- Insert at the beginning of the list to show newest transactions first. table.insert(data.history[player_name], 1, transaction) -- Limit history to the last 200 entries to prevent the file from growing indefinitely. while #data.history[player_name] > 200 do table.remove(data.history[player_name]) end end -- On server start, create the accounts file if it doesn't exist. do local f = io.open(file_path, "r") if not f then save_accounts_file({ balance = {}, pin = {}, credit = {}, history = {} }) else f:close() end end --------------------------------------------------- -- Public API accessible via `bank_accounts.*` --------------------------------------------------- -- Gets all relevant data for a single player. function bank_accounts.get_account_data(player_name) local data = read_accounts_file() -- Backwards compatibility check for old save files without a history table. if not data.history then data.history = {} end return { balance = data.balance[player_name] or 0, pin = data.pin[player_name] or "0000", credit = data.credit[player_name] or 0, history = data.history[player_name] or {}, } end -- Gets all data from the database (for background scripts like interest calculation). function bank_accounts.get_all_data() return read_accounts_file() end -- Saves the complete data object (for background scripts). function bank_accounts.save_all(data) return save_accounts_file(data) end -- Simple getter functions. function bank_accounts.get_balance(player_name) local data = read_accounts_file(); return data.balance[player_name] or 0 end function bank_accounts.get_credit(player_name) local data = read_accounts_file(); return data.credit[player_name] or 0 end function bank_accounts.get_pin(player_name) local data = read_accounts_file(); return data.pin[player_name] or "0000" end -- Adds a value to a player's balance and logs the transaction. function bank_accounts.add_balance(player_name, amount, trans_type, purpose, other_party) local data = read_accounts_file() local current_balance = data.balance[player_name] or 0 local new_balance = current_balance + tonumber(amount) data.balance[player_name] = new_balance log_transaction(data, player_name, "balance", amount, new_balance, trans_type, purpose, other_party) return save_accounts_file(data) end -- Sets a player's balance to an absolute value and logs the transaction. function bank_accounts.set_balance(player_name, amount, trans_type, purpose, other_party) local data = read_accounts_file() local current_balance = data.balance[player_name] or 0 local new_balance = tonumber(amount) local diff = new_balance - current_balance data.balance[player_name] = new_balance log_transaction(data, player_name, "balance", diff, new_balance, trans_type or "admin_set", purpose, other_party) return save_accounts_file(data) end -- Adds a value to a player's credit debt and logs the transaction. function bank_accounts.add_credit(player_name, amount, trans_type, purpose, other_party) local data = read_accounts_file() local current_credit = data.credit[player_name] or 0 local new_credit = current_credit + tonumber(amount) data.credit[player_name] = new_credit log_transaction(data, player_name, "credit", amount, new_credit, trans_type, purpose, other_party) return save_accounts_file(data) end -- Sets a player's credit debt to an absolute value and logs the transaction. function bank_accounts.set_credit(player_name, amount, trans_type, purpose, other_party) local data = read_accounts_file() local current_credit = data.credit[player_name] or 0 local new_credit = tonumber(amount) local diff = new_credit - current_credit data.credit[player_name] = new_credit log_transaction(data, player_name, "credit", diff, new_credit, trans_type or "admin_set_credit", purpose, other_party) return save_accounts_file(data) end -- Sets a player's PIN. function bank_accounts.set_pin(player_name, pin) local data = read_accounts_file() data.pin[player_name] = tostring(pin) return save_accounts_file(data) end -- Checks if a player has an entry in the database. function bank_accounts.player_has_account(player_name) local data = read_accounts_file() return data.balance[player_name] ~= nil end -- Creates a new, empty account for a player. function bank_accounts.create_account(player_name) if bank_accounts.player_has_account(player_name) then return false end local data = read_accounts_file() data.balance[player_name] = 0 data.pin[player_name] = "0000" data.credit[player_name] = 0 if not data.history then data.history = {} end data.history[player_name] = {} log_transaction(data, player_name, "system", 0, 0, "Account Created", "Initial account setup") return save_accounts_file(data) end