Init repo with working copy from my private server

This commit is contained in:
Rainer 2025-06-16 01:00:23 +02:00
commit a2e53b161d
38 changed files with 2413 additions and 0 deletions

146
README.md Normal file
View file

@ -0,0 +1,146 @@
# Bank Accounts (Redo & Extended)
A complete overhaul and extension of the original `bank_accounts` mod by Tmanyo and Trent Lasich. This project has been restructured from the ground up to ensure stability, security, and a significantly expanded feature set.
## About This Version
This version is a "Redo" of the original code. The focus was on fixing critical bugs, preventing exploits, and drastically improving stability, especially in multiplayer environments. Furthermore, numerous new features such as an automatic interest system, a detailed account statement, and a wire transfer machine have been added to provide a comprehensive in-game economic system.
## Features Overview
* **Complete Account System:** Players automatically receive an account with a balance and a credit line.
* **5 Interactive Machines:** ATM, Teller Computer, PIN Terminal, Card Swipe, and Wire Transfer Machine (WTM).
* **Detailed Account Statement:** All transactions are logged and can be viewed via the ATM or Teller Computer.
* **Automatic Interest System:** A performant background process calculates daily interest on balances and credit debts.
* **Secure Transactions:** All actions are secured against exploits (e.g., duplication bugs) and invalid inputs.
* **API for Other Mods:** Allows other mods to safely interact with the banking system.
* **Fully Translatable (i18n):** All texts can be easily adapted for other languages.
## Dependencies
This mod requires the **Currency Mod** to function.
* **ContentDB Link:** [https://content.luanti.org/packages/mt-mods/currency/](https://content.luanti.org/packages/mt-mods/currency/)
## Known Issues & WIP
This version is significantly more stable than the original mod, especially on multiplayer servers. Most critical multiplayer bugs and data loss issues have been resolved.
However, there is a known theoretical race condition that can occur under very specific circumstances:
* **If two different players interact with two different machines (e.g., two ATMs) at the exact same moment, it is possible for a transaction to be misdirected to the wrong machine.**
The time window for this to occur is extremely small (typically less than a few seconds between the two players' clicks), making it a very rare event on most servers.
This is a known limitation resulting from a workaround needed to ensure stability across different Minetest clients. It is considered a work-in-progress (WIP) issue that may be addressed in future updates.
## Getting Started (For Players)
To use the banking system, you first need to set your personal PIN.
1. Find a **PIN Terminal** (a small, wall-mounted device).
2. Right-click it. As a new customer, you will be prompted to set a 4-digit PIN.
3. After successful entry, you will automatically receive your **Bank Card** (`ATM Card`) in your inventory. You will need this card to access most of the other machines.
## The Machines (Nodes) in Detail
### PIN Terminal
* **Purpose:** The primary point of contact for new players and for PIN management.
* **Functions:**
* **New Players:** Are prompted to set their first, personal 4-digit PIN. They then receive their first bank card.
* **Existing Players:** Can change their PIN (requires entering the old PIN for security) or request a new bank card if the old one was lost.
### ATM (Automatic Teller Machine)
* **Purpose:** Standard interactions with one's own account.
* **Operation:** Requires a Bank Card (`ATM Card`) and PIN entry.
* **Functions:**
* **Deposit:** Deposit cash (`currency` items).
* **Withdrawal:** Withdraw cash (integers only).
* **Pay Credit:** Provides the option to pay the due rate or a custom amount of the credit debt.
* **Account Statement:** Shows the transaction history for balance and credit.
* **Request Cards:** Issues new debit or credit cards.
### Teller Computer
* **Purpose:** For players with the `bank_teller` privilege to manage customer accounts.
* **Functions:**
* All functions of a normal ATM, but for any customer entered by name.
* Additional admin functions: Wipe Account and Reset PIN.
* Admin Search (Shift + Right-click): Server admins can check if an account is seized.
### Wire Transfer Machine (WTM)
* **Purpose:** Secure money transfers between players.
* **Operation:** Requires Bank Card and PIN.
* **Functions:**
* Transfer from your balance.
* Transfer from your credit line.
* Input fields for **Recipient**, **Amount** (decimals allowed), and **Purpose**.
* The recipient receives a chat message about the incoming funds.
* The transaction is logged in the account statements of both the sender and the recipient with all details.
### Card Swipe
* **Purpose:** Player-owned shops to sell items via card payment.
* **Functions:**
* **Seller:** Places the device, puts items in its inventory, and sets a price (decimals allowed).
* **Buyer:** Right-clicks the device with a debit or credit card to complete the purchase. The process is theft-proof items are only transferred automatically after successful payment.
## Automatic Interest System (`interest.lua`)
* A performant background process calculates interest once every 24 hours (real-time).
* The interest rates for balance (default 0.5%) and credit debt (default 3%) can be easily configured at the beginning of the `interest.lua` file.
* Interest transactions are automatically logged in the account statement.
## Chat Commands
All commands start with `bank_`.
#### For All Players
* `/bank_account [<playername>]` - Shows your own account balance. Players with `bank_teller` or `server` privs can also view others' accounts.
* `/bank_set_pin <4-digit-PIN>` - Sets your own PIN. (Replaced in function by the PIN Terminal but remains as a command).
#### For Admins Only (`server` privilege)
* `/bank_add <name> <number>` - Adds money to a player's account (integers only).
* `/bank_subtract <name> <number>` - Subtracts money from a player's account (integers only).
* `/bank_balance <name> <number>` - Sets a player's balance to an exact value (decimals allowed).
* `/bank_credit <name> <number>` - Sets a player's credit debt to an exact value (decimals allowed). `/bank_credit <name> 0` forgives all debt.
* `/bank_wipe <name>` - Wipes a player's account balance to 0.
* `/bank_seize <name>` - Seizes a player's account (locks access).
* `/bank_unseize <name>` - Unseizes an account.
## Improvements Over the Original Version
* **Stability:** The fundamental logic was overhauled to fix crashes and "freezing" or silently closing windows.
* **Security / Exploits:** Numerous exploits have been fixed, e.g., money duplication by moving items between slots, and withdrawing fractional amounts of cash.
* **Multiplayer Capability:** Almost all global variables that caused conflicts between players have been removed. The mod is now significantly more stable on servers. *(Note: One global variable for position was retained for compatibility with certain Minetest clients, which could in theory cause issues with perfectly simultaneous interactions at two devices, but this is extremely unlikely in practice).*
* **API:** A clean and safe API in `functions.lua` now allows other mods to interact with the system easily and without conflicts.
* **New Features:** Account Statements, Daily Interest System, Wire Transfer Machine, and the PIN Terminal have been newly added.
* **User Experience:** The onboarding for new players via the PIN Terminal is much more immersive. The UI has been polished in many places.
* **Internationalization (i18n):** The entire mod is now fully translatable.
---
## License
```
License: MIT
Copyright 2016 Trent Lasich
Copyright 2025 Rage87
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```
## Credits
* Original Mod: Tmanyo (Code) & Trent Lasich (Textures, Models)
* Complete Redo, Extension & Bugfixing: **Rage87**

322
atm.lua Normal file
View file

@ -0,0 +1,322 @@
--[[
ATM Node
--------
This file defines the Automatic Teller Machine (ATM) node and all
of its associated forms and logic for player interaction.
--]]
-- A global variable to store the position of the last used machine.
-- This is necessary for stability in the user's specific environment.
pos_info = {}
-- The interest rate for the monthly credit payment.
local credit_rate = 0.04
-- Shows the account statement form for the player using the machine.
-- @param player: The player object.
-- @param account_type: "balance" or "credit" to determine which history to show.
local function show_atm_statement_form(player, account_type)
local player_name = player:get_player_name()
local data = bank_accounts.get_account_data(player_name)
local history = data.history
local lines = {}
local current_total_label = ""
-- Header for the textlist
table.insert(lines, minetest.formspec_escape(
string.format("%-11s | %-13s | %-13s | %-20s | %-20s | %s",
S("Date"), S("Amount"), S("New Balance"), S("Method"), S("Purpose"), S("Partner"))
))
table.insert(lines, minetest.formspec_escape("-----------------------------------------------------------------------------------------"))
-- Populate the list with transaction data
if history then
for _, t in ipairs(history) do
if t.account == account_type then
local parts = {}
table.insert(parts, string.format("%-11s", os.date("%Y-%m-%d", t.timestamp)))
table.insert(parts, string.format("%12s", string.format("%+.2f", t.amount) .. " MG"))
table.insert(parts, string.format("%12s", string.format("%.2f", t.new_total).." MG"))
table.insert(parts, string.format("%-20s", S(t.type)))
local purpose = t.purpose or ""
local partner = t.other or ""
if purpose ~= "" or partner ~= "" then
table.insert(parts, string.format("%-20s", purpose))
if partner ~= "" then
table.insert(parts, partner)
end
end
table.insert(lines, minetest.formspec_escape(table.concat(parts, " | ")))
end
end
end
-- Set the label for the current total based on the selected account type
if account_type == "balance" then
current_total_label = S("Current Balance: @1", string.format("%.2f", data.balance) .. " MG")
else
current_total_label = S("Current Credit Debt: @1", string.format("%.2f", data.credit) .. " MG")
end
local form_name = "bank_accounts:atm_statement@" .. player_name
local formspec = "size[13,9]" ..
"label[0,0;"..S("Account Statement for @1", player_name).."]" ..
"button_exit[0,0.5;2,1;view_balance;"..S("Balance").."]" ..
"button_exit[2,0.5;2,1;view_credit;"..S("Credit").."]" ..
"textlist[0,1.2;13,7;statement_list;"..table.concat(lines, ",").."]"..
"label[0,8.4;"..current_total_label.."]"..
"button_exit[11,8.4;2,1;back;"..S("Back").."]"
minetest.show_formspec(player_name, form_name, formspec)
end
-- Shows the main menu form for the ATM.
function main_form(player, pos)
local player_name = player:get_player_name()
local data = bank_accounts.get_account_data(player_name)
local next_rate = 0
if data.credit > 0 then
next_rate = math.max(1, math.floor(data.credit * credit_rate))
end
minetest.show_formspec(player_name, "bank_accounts:atm_options",
"size[8,8]" ..
"button_exit[1,.5;2,1;withdrawal;"..S("Withdraw").."]" ..
"button_exit[1,1.5;2,1;deposit;"..S("Deposit").."]" ..
"button_exit[1,2.5;3,1;pay_credit;"..S("Pay Credit Debt").."]" ..
"label[4.5,0.8;"..S("Account Balance: @1", string.format("%.2f", data.balance) .. " MG").."]" ..
"label[4.5,1.3;"..S("Total Credit Debt: @1", string.format("%.2f", data.credit) .. " MG").."]" ..
"label[4.5,1.8;"..S("Next Rate: @1", tostring(next_rate) .. " MG").."]" ..
"button_exit[4.5,2.5;3,1;statement;"..S("Account Statement").."]" ..
"button_exit[1,3.5;3,1;credit_card;"..S("Get Credit Card").."]" ..
"button_exit[1,4.5;3,1;debit_card;"..S("Get Debit Card").."]" ..
"button_exit[5,7;2,1;exit;"..S("Close").."]")
end
-- Node definition for the ATM
minetest.register_node("bank_accounts:atm", {
description = S("Automatic Teller Machine"),
drawtype = "mesh",
mesh = "atm.obj",
paramtype = "light",
paramtype2 = "facedir",
tiles = {"atm_col.png"},
groups = {cracky=3, crumbly=3, oddly_breakable_by_hand=2},
-- Create inventories when the node is placed in the world.
on_construct = function(pos)
local meta = minetest.get_meta(pos)
local inv = meta:get_inventory()
inv:set_size("ones", 1)
inv:set_size("fives", 1)
inv:set_size("tens", 1)
inv:set_size("withdrawal", 3)
end,
-- Called when a player right-clicks the node.
on_rightclick = function(pos, node, player, itemstack, pointed_thing)
pos_info = pos -- Set the global position for other functions to use.
if itemstack:get_name() ~= "bank_accounts:atm_card" then
minetest.chat_send_player(player:get_player_name(), S("[ATM] Must use ATM card."))
return
end
-- Show the initial PIN entry form.
minetest.show_formspec(player:get_player_name(), "bank_accounts:atm_home",
"size[8,8]" ..
"pwdfield[2,4;4,1;fourdigitpin;"..S("Four Digit Pin:").."]" ..
"button_exit[5,6;2,1;enter;"..S("Enter").."]" ..
"button_exit[3,6;2,1;exit;"..S("Cancel").."]")
end,
-- Prevents players from putting incorrect items into the deposit slots.
allow_metadata_inventory_put = function(pos, listname, index, stack, player)
if listname == "ones" and stack:get_name() ~= "currency:minegeld" then return 0 end
if listname == "fives" and stack:get_name() ~= "currency:minegeld_5" then return 0 end
if listname == "tens" and stack:get_name() ~= "currency:minegeld_10" then return 0 end
return stack:get_count()
end,
-- Prevents players from moving items between the typed deposit slots.
allow_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player)
local inv = minetest.get_meta(pos):get_inventory()
local stack = inv:get_stack(from_list, from_index)
if to_list == "ones" and stack:get_name() ~= "currency:minegeld" then return 0 end
if to_list == "fives" and stack:get_name() ~= "currency:minegeld_5" then return 0 end
if to_list == "tens" and stack:get_name() ~= "currency:minegeld_10" then return 0 end
return count
end
})
-- Central handler for all formspec interactions related to the ATM.
minetest.register_on_player_receive_fields(function(player, formname, fields)
-- Only process forms belonging to this node.
if not formname:find("bank_accounts:atm") then
return
end
local player_name = player:get_player_name()
local pos = pos_info
if not pos then
return
end
-- Handle PIN entry form.
if formname == "bank_accounts:atm_home" then
if fields.enter then
if bank_accounts.get_pin(player_name) == fields.fourdigitpin then
main_form(player, pos)
else
minetest.chat_send_player(player_name, S("[ATM] Invalid Pin."))
end
end
-- Handle main menu options.
elseif formname == "bank_accounts:atm_options" then
if fields.withdrawal then
minetest.get_meta(pos):get_inventory():set_size("withdrawal", 3)
minetest.show_formspec(player_name, "bank_accounts:atm_withdrawal",
"size[8,8]" ..
"field[2,4;5,1;money;"..S("Amount:")..";]" ..
"button_exit[3,6;2,1;exit;"..S("Cancel").."]" ..
"button_exit[5,6;2,1;enter;"..S("Enter").."]")
elseif fields.deposit then
local inv = minetest.get_meta(pos):get_inventory()
inv:set_size("ones", 1); inv:set_size("fives", 1); inv:set_size("tens", 1)
local list_name = "nodemeta:" .. pos.x .. "," .. pos.y .. "," .. pos.z
minetest.show_formspec(player_name, "bank_accounts:atm_deposit",
"size[8,8]" ..
"label[1,.5;1 MG]" .. "label[2,.5;5 MG]" .. "label[3,.5;10 MG]" ..
"list["..list_name..";ones;.75,1;1,1]" ..
"list["..list_name..";fives;1.75,1;1,1]" ..
"list["..list_name..";tens;2.75,1;1,1]" ..
"list[current_player;main;0,3;8,4;]" ..
"button_exit[3,7;2,1;exit;"..S("Cancel").."]" ..
"button_exit[5,7;2,1;enter;"..S("Enter").."]")
elseif fields.pay_credit then
local credit_debt = bank_accounts.get_credit(player_name)
local next_rate = 0
if credit_debt > 0 then
next_rate = math.max(1, math.floor(credit_debt * credit_rate))
end
minetest.show_formspec(player_name, "bank_accounts:atm_pay_credit",
"size[8,8]" ..
"label[0,0.5;"..S("Total Credit Debt: @1", string.format("%.2f", credit_debt).." MG").."]"..
"label[0,2;"..S("Next calculated rate:").."]"..
"button_exit[0,2.5;3.5,1;pay_rate;"..S("Pay Next Rate (@1 MG)", next_rate).."]" ..
"label[4.2,2;"..S("Or enter a custom amount:").."]" ..
"field[4.5,2.8;3.5,1;custom_amount;"..""..";]" ..
"button_exit[4.2,3.5;3.5,1;pay_custom;"..S("Pay Custom Amount").."]" ..
"button_exit[3,7;2,1;exit;"..S("Cancel").."]")
elseif fields.statement then
show_atm_statement_form(player, "balance")
elseif fields.credit_card then
player:get_inventory():add_item("main", "bank_accounts:credit_card")
main_form(player, pos)
elseif fields.debit_card then
player:get_inventory():add_item("main", "bank_accounts:debit_card")
main_form(player, pos)
end
-- Handle withdrawal form.
elseif formname == "bank_accounts:atm_withdrawal" then
if fields.enter then
local amount = tonumber(fields.money)
if amount and amount > 0 and amount % 1 == 0 and bank_accounts.get_balance(player_name) >= amount then
bank_accounts.add_balance(player_name, -amount, "ATM Withdrawal", "", "")
local tens=math.floor(amount/10); local rem=amount%10; local fives=math.floor(rem/5); local ones=rem%5
local inv = minetest.get_meta(pos):get_inventory()
inv:set_stack("withdrawal", 1, {name="currency:minegeld_10", count=tens})
inv:set_stack("withdrawal", 2, {name="currency:minegeld_5", count=fives})
inv:set_stack("withdrawal", 3, {name="currency:minegeld", count=ones})
local list_name = "nodemeta:"..pos.x..","..pos.y..","..pos.z
minetest.show_formspec(player_name, "bank_accounts:withdrawn_money",
"size[8,8]" ..
"label[1,0.5;"..S("Please take your money.").. "]" ..
"list["..list_name..";withdrawal;1,1;3,1]" ..
"list[current_player;main;0,3;8,4;]" ..
"button_exit[3,7;2,1;exit;"..S("Close").."]")
else
minetest.chat_send_player(player_name, S("[ATM] Insufficient funds or invalid amount."))
main_form(player, pos)
end
else
main_form(player, pos)
end
-- Handle deposit form.
elseif formname == "bank_accounts:atm_deposit" then
local inv = minetest.get_meta(pos):get_inventory()
if fields.enter then
local total_deposit = inv:get_stack("ones",1):get_count() + inv:get_stack("fives",1):get_count()*5 + inv:get_stack("tens",1):get_count()*10
if total_deposit > 0 then
bank_accounts.add_balance(player_name, total_deposit, "ATM Deposit", "", "")
minetest.chat_send_player(player_name, S("[ATM] Deposited @1.", tostring(total_deposit).." MG"))
end
else
-- Return items to player if they cancel.
local player_inv = player:get_inventory()
player_inv:add_item("main", inv:get_stack("ones",1))
player_inv:add_item("main", inv:get_stack("fives",1))
player_inv:add_item("main", inv:get_stack("tens",1))
end
inv:set_stack("ones",1,nil); inv:set_stack("fives",1,nil); inv:set_stack("tens",1,nil)
main_form(player, pos)
-- Handle form after taking withdrawn money.
elseif formname == "bank_accounts:withdrawn_money" then
main_form(player, pos)
-- Handle credit payment form.
elseif formname == "bank_accounts:atm_pay_credit" then
local amount_to_pay = 0
local is_valid = false
if fields.pay_rate then
local credit_debt = bank_accounts.get_credit(player_name)
if credit_debt > 0 then
amount_to_pay = math.max(1, math.floor(credit_debt * credit_rate))
end
is_valid = true
elseif fields.pay_custom then
amount_to_pay = normalize_and_tonumber(fields.custom_amount)
local min_payment = 0
local credit_debt = bank_accounts.get_credit(player_name)
if credit_debt > 0 then
min_payment = math.max(1, math.floor(credit_debt * credit_rate))
end
if not amount_to_pay or amount_to_pay < min_payment then
minetest.chat_send_player(player_name, S("[ATM] You must pay at least the minimum monthly rate."))
else
is_valid = true
end
end
if is_valid and amount_to_pay and amount_to_pay > 0 then
if bank_accounts.get_balance(player_name) < amount_to_pay then
minetest.chat_send_player(player_name, S("[ATM] Insufficient funds."))
elseif bank_accounts.get_credit(player_name) < amount_to_pay then
minetest.chat_send_player(player_name, S("[ATM] You don't have that much credit debt."))
else
bank_accounts.add_balance(player_name, -amount_to_pay, "Rate Payment", "", "")
bank_accounts.add_credit(player_name, -amount_to_pay, "Rate Payment", "", "")
minetest.chat_send_player(player_name, S("[ATM] Paid @1 of credit debt.", string.format("%.2f", amount_to_pay).." MG"))
end
elseif fields.pay_custom and is_valid == false then
-- Do nothing, error message was already sent.
elseif not fields.exit and not fields.quit and (fields.pay_rate or fields.pay_custom) then
-- Catch other invalid inputs.
minetest.chat_send_player(player_name, S("[ATM] Invalid amount."))
end
main_form(player, pos)
-- Handle statement form.
elseif formname:find("bank_accounts:atm_statement@") then
local target_name = formname:match("bank_accounts:atm_statement@(.*)")
if not target_name then return end
if fields.back then
main_form(player, pos)
else
show_atm_statement_form(player, fields.view_credit and "credit" or "balance")
end
end
end)

213
card_swipe.lua Normal file
View file

@ -0,0 +1,213 @@
--[[
Card Swipe Node
---------------
This file defines the Card Swipe node, which allows players
to set up shops and sell items via card payments.
--]]
-- Uses the global 'pos_info' variable for consistency.
-- Node definition for the Card Swipe.
minetest.register_node("bank_accounts:card_swipe", {
description = S("Card Swipe"),
drawtype = "mesh",
mesh = "card_swipe.obj",
paramtype = "light",
paramtype2 = "facedir",
tiles = {"card_reader_col.png"},
groups = {cracky=3, crumbly=3, oddly_breakable_by_hand=2},
selection_box = { type = "fixed", fixed = {{-.3,-.5,-.3,.4,-.2,.3}} },
collision_box = { type = "fixed", fixed = {{-.3,-.5,-.3,.4,-.2,.3}} },
-- Creates the item inventory when the node is placed.
on_construct = function(pos)
minetest.get_meta(pos):get_inventory():set_size("items", 8)
end,
-- Sets the owner of the node after it has been placed.
after_place_node = function(pos, placer)
local meta = minetest.get_meta(pos)
local owner = placer:get_player_name()
meta:set_string("infotext", S("Card Swipe (owned by @1)", owner))
meta:set_string("owner", owner)
end,
-- Ensures only the owner can dig the node, and only if it's empty.
can_dig = function(pos, player)
local meta = minetest.get_meta(pos)
if player:get_player_name() == meta:get_string("owner") then
return meta:get_inventory():is_empty("items")
end
return false
end,
-- Called when a player right-clicks the node.
on_rightclick = function(pos, node, player, itemstack, pointed_thing)
pos_info = pos -- Set global position.
local meta = minetest.get_meta(pos)
local player_name = player:get_player_name()
local owner = meta:get_string("owner")
local list_name = "nodemeta:" .. pos.x .. "," .. pos.y .. "," .. pos.z
-- Show seller interface if the player is the owner.
if player_name == owner then
minetest.show_formspec(player_name, "bank_accounts:card_swipe_seller",
"size[8,9]" ..
"field[1,0.5;4,1;price;"..S("Price in MG:")..";"..minetest.formspec_escape(meta:get_string("price") or "").."]" ..
"label[0,1.75;"..S("Items for Sale:").."]"..
"list["..list_name..";items;0,2.25;8,2]" ..
"label[0,4.5;"..S("Your Inventory:").."]"..
"list[current_player;main;0,5;8,3]" ..
"button_exit[1,8.25;3,1;reset;"..S("Reset Price").."]" ..
"button_exit[4.5,8.25;3,1;set_price;"..S("Set Price").."]")
-- Show buyer interface for everyone else.
else
if meta:get_inventory():is_empty("items") then
minetest.chat_send_player(player_name, S("[Card Swipe] This machine is empty."))
return
end
local price_str = meta:get_string("price")
if not price_str or price_str == "" then
minetest.chat_send_player(player_name, S("[Card Swipe] No price has been set."))
return
end
local wielded_item = itemstack:get_name()
if wielded_item ~= "bank_accounts:debit_card" and wielded_item ~= "bank_accounts:credit_card" then
minetest.chat_send_player(player_name, S("[Card Swipe] Must use a debit or credit card."))
return
end
if minetest.check_player_privs(player_name, {seized=true}) then
minetest.chat_send_player(player_name, S("[Card Swipe] Your account has been seized! Transaction denied."))
return
end
local price = tonumber(price_str)
if wielded_item == "bank_accounts:debit_card" and bank_accounts.get_balance(player_name) < price then
minetest.chat_send_player(player_name, S("[Card Swipe] Card declined. Insufficient funds."))
return
end
minetest.show_formspec(player_name, "bank_accounts:card_swipe_buyer",
"size[8,8]" ..
"label[1,1;"..S("Price: @1", string.format("%.2f", price).." MG").."]" ..
"label[1,1.5;"..S("Owner: @1", owner).."]"..
"label[1,2;"..S("Items for Sale (Click 'Buy' to receive):").."]"..
"list["..list_name..";items;0,2.5;8,2]" ..
"list[current_player;main;0,5;8,2;]" ..
"button_exit[2,7.4;2,1;cancel;"..S("Cancel").."]" ..
"button_exit[4,7.4;2,1;buy;"..S("Buy").."]")
end
end,
-- Only the owner can put items into the swipe machine.
allow_metadata_inventory_put = function(pos, listname, index, stack, player)
if player:get_player_name() == minetest.get_meta(pos):get_string("owner") then
return stack:get_count()
end
return 0
end,
-- THEFT-PREVENTION: Players cannot take items manually.
-- The script transfers them automatically upon successful purchase.
allow_metadata_inventory_take = function(pos, listname, index, stack, player)
return 0
end,
})
-- Crafting recipe for the Card Swipe machine.
minetest.register_craft({
output = "bank_accounts:card_swipe",
recipe = {
{"default:steel_ingot", "default:copper_ingot", "default:steel_ingot"},
{"bank_accounts:credit_card", "default:mese", "bank_accounts:debit_card"},
{"default:steel_ingot", "default:copper_ingot", "default:steel_ingot"},
},
})
-- Handles formspec submissions for the Card Swipe.
minetest.register_on_player_receive_fields(function(player, formname, fields)
if not formname:find("bank_accounts:card_swipe") then
return
end
local player_name = player:get_player_name()
local pos = pos_info
if not pos then
return
end
local meta = minetest.get_meta(pos)
if not meta then
return
end
-- Logic for the seller's interface.
if formname == "bank_accounts:card_swipe_seller" then
if fields.set_price then
local price = normalize_and_tonumber(fields.price)
if price and price > 0 then
meta:set_string("price", tostring(price))
minetest.chat_send_player(player_name, S("[Card Swipe] Price set to @1.", string.format("%.2f", price).." MG"))
else
minetest.chat_send_player(player_name, S("[Card Swipe] Invalid price. Must be a number greater than 0."))
end
elseif fields.reset then
meta:set_string("price", "")
minetest.chat_send_player(player_name, S("[Card Swipe] Price has been reset."))
end
end
-- Logic for the buyer's interface.
if formname == "bank_accounts:card_swipe_buyer" then
if fields.buy then
local owner = meta:get_string("owner")
local price = tonumber(meta:get_string("price") or "0")
local shop_inv = meta:get_inventory()
local wielded_item = player:get_wielded_item():get_name()
if price <= 0 or shop_inv:is_empty("items") then
minetest.chat_send_player(player_name, S("[Card Swipe] This offer is no longer valid."))
return
end
local payment_successful = false
local description_for_buyer = S("Items from @1", owner)
local description_for_seller = S("Items to @1", player_name)
-- Debit Card Transaction
if wielded_item == "bank_accounts:debit_card" then
if bank_accounts.get_balance(player_name) >= price then
bank_accounts.add_balance(player_name, -price, "Purchase", "", description_for_buyer, owner)
bank_accounts.add_balance(owner, price, "Sale", "", description_for_seller, player_name)
payment_successful = true
end
-- Credit Card Transaction
elseif wielded_item == "bank_accounts:credit_card" then
bank_accounts.add_credit(player_name, price, "Credit Purchase", "", description_for_buyer, owner)
bank_accounts.add_balance(owner, price, "Sale", "", description_for_seller, player_name)
payment_successful = true
end
-- If payment was successful, transfer items.
if payment_successful then
local player_inv = player:get_inventory()
for i=1, shop_inv:get_size("items") do
local stack = shop_inv:get_stack("items", i)
if not stack:is_empty() then
player_inv:add_item("main", stack)
end
end
shop_inv:set_list("items", {})
meta:set_string("price", "")
minetest.chat_send_player(player_name, S("[Card Swipe] Purchase successful!"))
minetest.chat_send_player(owner, S("[Card Swipe] Your items have been sold for @1.", string.format("%.2f", price).." MG"))
player:get_inventory():add_item("main", "bank_accounts:receipt")
else
minetest.chat_send_player(player_name, S("[Card Swipe] Payment declined!"))
end
end
end
end)

30
cards.lua Normal file
View file

@ -0,0 +1,30 @@
--[[
Item Definitions
----------------
This file defines all physical items from the mod, like
the different types of cards and the receipt.
--]]
-- The credit card for purchases via credit line.
minetest.register_craftitem("bank_accounts:credit_card", {
description = S("Credit Card"),
inventory_image = "credit_card.png",
groups = {not_in_creative_inventory=1},
stack_max = 1,
})
-- The debit card for purchases from the account balance.
minetest.register_craftitem("bank_accounts:debit_card", {
description = S("Debit Card"),
inventory_image = "debit_card.png",
groups = {not_in_creative_inventory=1},
stack_max = 1,
})
-- A receipt item given after a card swipe purchase.
minetest.register_craftitem("bank_accounts:receipt", {
description = S("Receipt"),
inventory_image = "receipt.png",
groups = {not_in_creative_inventory=1},
stack_max = 1,
})

212
chatcommands.lua Normal file
View file

@ -0,0 +1,212 @@
--[[
Chat Commands
-------------
This file defines all chat commands for players and admins
to interact with the banking system.
--]]
-- Privilege for seized accounts.
minetest.register_privilege("seized", {
description = S("Account seized."),
give_to_singleplayer = false,
})
-- Allows any player to see their own balance, or privileged players to see others'.
minetest.register_chatcommand("bank_account", {
params = S("[<playername>]"),
description = S("Shows your account balance or the balance of another player (if you have permission)."),
func = function(name, param)
local target_name = param
if not target_name or target_name == "" then
target_name = name
end
if target_name ~= name and not minetest.check_player_privs(name, {bank_teller=true, server=true}) then
minetest.chat_send_player(name, S("You do not have permission to view other players' accounts."))
return
end
if not bank_accounts.player_has_account(target_name) then
minetest.chat_send_player(name, S("Player '@1' not found or has no account.", target_name))
return
end
local data = bank_accounts.get_account_data(target_name)
minetest.chat_send_player(name, S("Balance for @1: @2", target_name, string.format("%.2f", data.balance) .. " MG"))
minetest.chat_send_player(name, S("Credit Debt for @1: @2", target_name, string.format("%.2f", data.credit) .. " MG"))
end,
})
-- Allows any player to set their own PIN.
minetest.register_chatcommand("bank_set_pin", {
params = S("<4-digit-pin>"),
description = S("Set pin for your bank account."),
func = function(name, param)
if not param or param == "" then
minetest.chat_send_player(name, S("[Account] No numbers entered."))
return
end
if param:match("^[0-9][0-9][0-9][0-9]$") then
bank_accounts.set_pin(name, param)
minetest.chat_send_player(name, S("[Account] Pin successfully set!"))
local player = minetest.get_player_by_name(name)
if player and not player:get_inventory():contains_item("main", "bank_accounts:atm_card") then
player:get_inventory():add_item("main", "bank_accounts:atm_card")
end
else
minetest.chat_send_player(name, S("[Account] Invalid number entered. Must be exactly 4 digits."))
end
end
})
-- Helper function to parse admin commands that require an integer amount.
local function parse_admin_params_int(params)
local parts = params:split(" ", 2)
local playername = parts[1]
local number_str = parts[2]
if not playername or playername=="" then return nil, nil, S("[Bank] Player name missing.") end
if not bank_accounts.player_has_account(playername) then return nil, nil, S("[Bank] Invalid player name entered.") end
if not number_str or number_str=="" then return playername, nil, S("[Bank] No number entered.") end
local number = normalize_and_tonumber(number_str)
if not number then return playername, nil, S("[Bank] Invalid number entered.") end
if number % 1 ~= 0 then return playername, nil, S("[Bank] Amount must be a whole number.") end
return playername, number
end
-- Helper function to parse admin commands that allow a float amount.
local function parse_admin_params_float(params)
local parts = params:split(" ", 2)
local playername = parts[1]
local number_str = parts[2]
if not playername or playername=="" then return nil, nil, S("[Bank] Player name missing.") end
if not bank_accounts.player_has_account(playername) then return nil, nil, S("[Bank] Invalid player name entered.") end
if not number_str or number_str=="" then return playername, nil, S("[Bank] No number entered.") end
local number = normalize_and_tonumber(number_str)
if not number then return playername, nil, S("[Bank] Invalid number entered.") end
return playername, number
end
-- Adds an integer amount to a player's balance. (Admin only)
minetest.register_chatcommand("bank_add", {
params = S("<name> <number>"),
description = S("Add to a player's account balance."),
privs = {server = true},
func = function(name, params)
local target_name, amount, err = parse_admin_params_int(params)
if err then
minetest.chat_send_player(name, err)
return
end
if not amount or amount <= 0 then
minetest.chat_send_player(name, S("[Bank] Number must be greater than 0."))
return
end
bank_accounts.add_balance(target_name, amount, "Admin Deposit", S("By Admin: @1", name), name)
minetest.chat_send_player(name, S("[Bank] @1 successfully added to @2's account.", tostring(amount).." MG", target_name))
end
})
-- Subtracts an integer amount from a player's balance. (Admin only)
minetest.register_chatcommand("bank_subtract", {
params = S("<name> <number>"),
description = S("Subtract from a player's account balance."),
privs = {server = true},
func = function(name, params)
local target_name, amount, err = parse_admin_params_int(params)
if err then
minetest.chat_send_player(name, err)
return
end
if not amount or amount <= 0 then
minetest.chat_send_player(name, S("[Bank] Number must be greater than 0."))
return
end
bank_accounts.add_balance(target_name, -amount, "Admin Withdrawal", S("By Admin: @1", name), name)
minetest.chat_send_player(name, S("[Bank] @1 successfully subtracted from @2's account.", tostring(amount).." MG", target_name))
end
})
-- Sets a player's balance to a specific (float) amount. (Admin only)
minetest.register_chatcommand("bank_balance", {
params = S("<name> <number>"),
description = S("Set a player's account balance."),
privs = {server = true},
func = function(name, params)
local target_name, amount, err = parse_admin_params_float(params)
if err then
minetest.chat_send_player(name, err)
return
end
if amount == nil or amount < 0 then
minetest.chat_send_player(name, S("[Bank] Number must be 0 or greater."))
return
end
bank_accounts.set_balance(target_name, amount, "Admin Set Balance", S("By Admin: @1", name), name)
minetest.chat_send_player(name, S("[Bank] Funds successfully set for @1.", target_name))
end
})
-- Sets a player's credit debt to a specific (float) amount. (Admin only)
minetest.register_chatcommand("bank_credit", {
params = S("<name> <number>"),
description = S("Set a player's credit debt."),
privs = {server = true},
func = function(name, params)
local target_name, amount, err = parse_admin_params_float(params)
if err then
minetest.chat_send_player(name, err)
return
end
if amount == nil or amount < 0 then
minetest.chat_send_player(name, S("[Bank] Number must be 0 or greater."))
return
end
bank_accounts.set_credit(target_name, amount, "Admin Set Credit", S("By Admin: @1", name), name)
minetest.chat_send_player(name, S("[Bank] Credit debt for @1 has been set to @2.", string.format("%.2f", amount).." MG", target_name))
end
})
-- Wipes a player's balance to 0. (Admin only)
minetest.register_chatcommand("bank_wipe", {
params = S("<name>"),
description = S("Wipe a player's bank account."),
privs = {server = true},
func = function(name, param)
if not param or not bank_accounts.player_has_account(param) then
minetest.chat_send_player(name, S("[Bank] Invalid player name entered."))
return
end
bank_accounts.set_balance(param, 0, "Admin Wipe", S("By Admin: @1", name), name)
minetest.chat_send_player(name, S("[Bank] Account successfully wiped for @1!", param))
end
})
-- Seizes a player's account. (Admin only)
minetest.register_chatcommand("bank_seize", {
params = S("<name>"),
description = S("Seize a player's account."),
privs = {server = true},
func = function(name, param)
if not param or not bank_accounts.player_has_account(param) then
minetest.chat_send_player(name, S("[Bank] Invalid player name entered."))
return
end
minetest.set_player_privs(param, {seized = true})
minetest.chat_send_player(name, S("[Bank] Account successfully seized for @1!", param))
end
})
-- Unseizes a player's account. (Admin only)
minetest.register_chatcommand("bank_unseize", {
params = S("<name>"),
description = S("Unseize a player's account."),
privs = {server = true},
func = function(name, param)
if not param or not bank_accounts.player_has_account(param) then
minetest.chat_send_player(name, S("[Bank] Invalid player name entered."))
return
end
minetest.set_player_privs(param, {seized = nil})
minetest.chat_send_player(name, S("[Bank] Account successfully unseized for @1!", param))
end
})

265
computer.lua Normal file
View file

@ -0,0 +1,265 @@
--[[
Teller Computer Node
--------------------
This file defines the Teller Computer node, which allows players with
the 'bank_teller' privilege to manage other players' accounts.
--]]
-- Uses the global 'pos_info' variable defined in atm.lua for consistency.
pos_data = {} -- Kept for historical reasons, but pos_info is used.
-- Shows the main form for the teller.
-- @param player: The teller player object.
-- @param pos: The position of the computer node.
-- @param customer_name: The name of the customer being managed.
local function show_teller_form(player, pos, customer_name)
local list_name = "nodemeta:" .. pos.x .. "," .. pos.y .. "," .. pos.z
local data = {balance = 0, credit = 0}
if customer_name and customer_name ~= "" and bank_accounts.player_has_account(customer_name) then
data = bank_accounts.get_account_data(customer_name)
end
local formspec = "size[8,9.5]" ..
"label[0,-.25;"..S("Deposit:").."]" ..
"list["..list_name..";ones;0,.25;1,1]" .. "list["..list_name..";fives;1.25,.25;1,1]" .. "list["..list_name..";tens;2.5,.25;1,1]" ..
"label[0.25,1.2;1 MG]" .. "label[1.5,1.2;5 MG]" .. "label[2.75,1.2;10 MG]" ..
"field[4,.5;4,1;playername;"..S("Player:")..";"..minetest.formspec_escape(customer_name or "").."]" ..
"field[.3,2.25;4,1;withdrawal;"..S("Withdraw:")..";]" ..
"field[.3,3.25;4,1;credit_debt;"..S("Credit Payment:")..";]" ..
"label[.5,4.5;"..S("Balance: @1", string.format("%.2f", data.balance).." MG").."]" ..
"label[.5,4.75;"..S("Credit Debt: @1", string.format("%.2f", data.credit).." MG").."]" ..
"button_exit[5.5,1;2,1;stats;"..S("Show Account").."]" ..
"button_exit[5.5,2;2,1;statement;"..S("Account Statement").."]" ..
"button_exit[5.5,3;2,1;wipe;"..S("Wipe Account").."]" ..
"button_exit[5.5,4;2,1;reset_pin;"..S("Reset PIN").."]" ..
"button_exit[3,5.5;2,1;exit;"..S("Cancel").."]" ..
"button_exit[5.5,5.5;2,1;enter;"..S("Enter").."]" ..
"list[current_player;main;0,7;8,2.5;]"
minetest.show_formspec(player:get_player_name(), "bank_accounts:teller", formspec)
end
-- Shows the account statement form for a given customer.
local function show_statement_form(player, pos, customer_name, account_type)
local data = bank_accounts.get_account_data(customer_name)
local history = data.history
local lines = {}
local current_total_label = ""
table.insert(lines, minetest.formspec_escape(string.format("%-11s | %-13s | %-13s | %-20s | %-20s | %s", S("Date"), S("Amount"), S("New Balance"), S("Method"), S("Purpose"), S("Partner"))))
table.insert(lines, minetest.formspec_escape("-----------------------------------------------------------------------------------------"))
if history then
for _, t in ipairs(history) do
if t.account == account_type then
local parts = {}
table.insert(parts, string.format("%-11s", os.date("%Y-%m-%d", t.timestamp)))
table.insert(parts, string.format("%12s", string.format("%+.2f", t.amount) .. " MG"))
table.insert(parts, string.format("%12s", string.format("%.2f", t.new_total).." MG"))
table.insert(parts, string.format("%-20s", S(t.type)))
local purpose = t.purpose or ""
local partner = t.other or ""
if purpose ~= "" or partner ~= "" then
table.insert(parts, string.format("%-20s", purpose))
if partner ~= "" then
table.insert(parts, partner)
end
end
table.insert(lines, minetest.formspec_escape(table.concat(parts, " | ")))
end
end
end
if account_type == "balance" then
current_total_label = S("Current Balance: @1", string.format("%.2f", data.balance).." MG")
else
current_total_label = S("Current Credit Debt: @1", string.format("%.2f", data.credit).." MG")
end
local form_name = "bank_accounts:statement@" .. customer_name
local formspec = "size[13,9]" ..
"label[0,0;"..S("Account Statement for @1",customer_name).."]" ..
"button_exit[0,0.5;2,1;view_balance;"..S("Balance").."]" ..
"button_exit[2,0.5;2,1;view_credit;"..S("Credit").."]" ..
"textlist[0,1.2;13,7;statement_list;"..table.concat(lines,",").."]"..
"label[0,8.4;"..current_total_label.."]"..
"button_exit[11,8.4;2,1;back;"..S("Back").."]"
minetest.show_formspec(player:get_player_name(), form_name, formspec)
end
-- Node definition for the Teller Computer.
minetest.register_node("bank_accounts:teller_computer", {
description = S("Bank Teller's Computer"),
drawtype = "mesh",
mesh = "computer.obj",
paramtype = "light",
paramtype2 = "facedir",
light_source = 5,
tiles = { {name="computer.png"},{name="computer_screen.png"} },
groups = {cracky=3, crumbly=3, oddly_breakable_by_hand=2},
selection_box = { type = "fixed", fixed = {{-.5,-.5,-.5,.5,.4,.2}} },
collision_box = { type = "fixed", fixed = {{-.5,-.5,-.5,.5,.4,.2}} },
on_construct = function(pos)
local meta = minetest.get_meta(pos)
local inv = meta:get_inventory()
inv:set_size("ones", 1)
inv:set_size("fives", 1)
inv:set_size("tens", 1)
end,
on_rightclick = function(pos, node, player, itemstack, pointed_thing)
pos_info = pos
local player_name = player:get_player_name()
if player:get_player_control().aux1 and minetest.check_player_privs(player_name, {server=true}) then
minetest.show_formspec(player_name, "bank_accounts:admin_teller",
"size[8,4]" ..
"field[.5,.5;4,1;search;"..S("Search:")..";]" ..
"button_exit[4.5,.22;2,1;search_button;"..S("Search").."]" ..
"label[.5,1.25;"..S("Player:").."]".. "label[3.5,1.25;"..S("Seized:").."]")
elseif minetest.check_player_privs(player_name, {bank_teller=true}) then
show_teller_form(player, pos, "")
else
minetest.chat_send_player(player_name, S("[Bank] Insufficient privileges."))
end
end,
allow_metadata_inventory_put = function(pos, listname, index, stack, player)
if listname=="ones" and stack:get_name()~="currency:minegeld" then return 0 end
if listname=="fives" and stack:get_name()~="currency:minegeld_5" then return 0 end
if listname=="tens" and stack:get_name()~="currency:minegeld_10" then return 0 end
return stack:get_count()
end,
allow_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player)
local inv=minetest.get_meta(pos):get_inventory()
local stack=inv:get_stack(from_list, from_index)
if to_list=="ones" and stack:get_name()~="currency:minegeld" then return 0 end
if to_list=="fives" and stack:get_name()~="currency:minegeld_5" then return 0 end
if to_list=="tens" and stack:get_name()~="currency:minegeld_10" then return 0 end
return count
end,
})
-- Central handler for all Teller and Admin forms.
minetest.register_on_player_receive_fields(function(player, formname, fields)
if not formname:find("bank_accounts:teller") and not formname:find("bank_accounts:admin_teller") and not formname:find("bank_accounts:statement") then
return
end
local player_name = player:get_player_name()
local pos = pos_info
if not pos then
return
end
-- Admin search form.
if formname == "bank_accounts:admin_teller" then
if fields.search_button and fields.search and fields.search ~= "" then
local search_name=fields.search
local status
if not bank_accounts.player_has_account(search_name) then
status=S("No Account")
elseif minetest.check_player_privs(search_name,{seized=true}) then
status=S("Yes")
else
status=S("No")
end
minetest.show_formspec(player_name,formname,
"size[8,4]"..
"field[.5,.5;4,1;search;"..S("Search:")..";"..minetest.formspec_escape(search_name).."]"..
"button_exit[4.5,.22;2,1;search_button;"..S("Search").."]"..
"label[.5,1.25;"..S("Player:").."]".."label[3.5,1.25;"..S("Seized:").."]"..
"label[.5,1.75;"..minetest.formspec_escape(search_name).."]".."label[3.5,1.75;"..status.."]")
end
return
end
-- Main teller form.
if formname == "bank_accounts:teller" then
local customer_name = fields.playername
-- This validation logic is quirky but proven to be stable in the user's environment.
if (not customer_name or customer_name=="") and not fields.exit then
minetest.chat_send_player(player_name, S("[Bank] Must enter a player name."))
return
end
if (customer_name and customer_name~="" and not bank_accounts.player_has_account(customer_name)) and not fields.exit then
minetest.chat_send_player(player_name, S("[Bank] Invalid player name entered."))
return
end
if fields.stats then
show_teller_form(player, pos, customer_name)
elseif fields.statement then
show_statement_form(player, pos, customer_name, "balance")
elseif fields.wipe then
bank_accounts.set_balance(customer_name, 0, "Teller Wipe", S("Teller: @1", player_name), player_name)
show_teller_form(player, pos, customer_name)
elseif fields.reset_pin then
bank_accounts.set_pin(customer_name, "0000")
show_teller_form(player, pos, customer_name)
elseif fields.enter then
local meta=minetest.get_meta(pos)
local inv=meta:get_inventory()
-- Deposit
local total_deposit=inv:get_stack("ones",1):get_count()+inv:get_stack("fives",1):get_count()*5+inv:get_stack("tens",1):get_count()*10
if total_deposit>0 then
bank_accounts.add_balance(customer_name,total_deposit,"Teller Deposit",S("Teller: @1",player_name),player_name)
end
-- Withdrawal
local withdrawal = tonumber(fields.withdrawal)
if withdrawal and withdrawal>0 then
if withdrawal%1~=0 then
minetest.chat_send_player(player_name,S("[Bank] Withdrawal amount must be a whole number."))
elseif bank_accounts.get_balance(customer_name)>=withdrawal then
bank_accounts.add_balance(customer_name,-withdrawal,"Teller Withdrawal",S("Teller: @1",player_name),player_name)
local tens=math.floor(withdrawal/10);local rem=withdrawal%10;local fives=math.floor(rem/5);local ones=rem%5
player:get_inventory():add_item("main",{name="currency:minegeld_10",count=tens})
player:get_inventory():add_item("main",{name="currency:minegeld_5",count=fives})
player:get_inventory():add_item("main",{name="currency:minegeld",count=ones})
else
minetest.chat_send_player(player_name,S("[Bank] Player has insufficient funds for withdrawal."))
end
end
-- Credit Payment
local credit_payment = normalize_and_tonumber(fields.credit_debt)
if credit_payment and credit_payment>0 then
if bank_accounts.get_balance(customer_name)>=credit_payment then
if bank_accounts.get_credit(customer_name)>=credit_payment then
bank_accounts.add_balance(customer_name,-credit_payment,"Teller Credit Payment",S("Teller: @1",player_name),player_name)
bank_accounts.add_credit(customer_name,-credit_payment,"Teller Credit Payment",S("Teller: @1",player_name),player_name)
else
minetest.chat_send_player(player_name,S("[Bank] Player does not have that much credit debt."))
end
else
minetest.chat_send_player(player_name,S("[Bank] Player has insufficient funds for credit payment."))
end
end
inv:set_stack("ones",1,nil)
inv:set_stack("fives",1,nil)
inv:set_stack("tens",1,nil)
show_teller_form(player,pos,customer_name)
end
end
-- Statement form.
if formname:find("bank_accounts:statement@") then
local customer_name=formname:match("bank_accounts:statement@(.*)")
if not customer_name then
return
end
if fields.view_balance then
show_statement_form(player, pos, customer_name, "balance")
elseif fields.view_credit then
show_statement_form(player, pos, customer_name, "credit")
elseif fields.back then
show_teller_form(player, pos, customer_name)
end
end
end)

1
depends.txt Normal file
View file

@ -0,0 +1 @@
currency

193
functions.lua Normal file
View file

@ -0,0 +1,193 @@
--[[
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

8
i18n.lua Normal file
View file

@ -0,0 +1,8 @@
--[[
Internationalization (i18n) Setup
---------------------------------
Defines the global translation function S() for the mod.
It dynamically gets the mod's name to support renaming the mod folder.
--]]
S = minetest.get_translator(minetest.get_current_modname())

25
init.lua Normal file
View file

@ -0,0 +1,25 @@
--[[
Bank Accounts Mod - Initialization File
---------------------------------------
This file defines the global namespace and loads all other
mod files in the correct order.
--]]
-- Global namespace for the mod to organize functions and data.
bank_accounts = {}
-- Load helper scripts first.
dofile(minetest.get_modpath("bank_accounts") .. "/i18n.lua")
dofile(minetest.get_modpath("bank_accounts") .. "/functions.lua")
-- Load item and node definitions.
dofile(minetest.get_modpath("bank_accounts") .. "/cards.lua")
dofile(minetest.get_modpath("bank_accounts") .. "/atm.lua")
dofile(minetest.get_modpath("bank_accounts") .. "/computer.lua")
dofile(minetest.get_modpath("bank_accounts") .. "/card_swipe.lua")
dofile(minetest.get_modpath("bank_accounts") .. "/pin_terminal.lua")
dofile(minetest.get_modpath("bank_accounts") .. "/wtm.lua")
-- Load systems that run in the background or provide commands.
dofile(minetest.get_modpath("bank_accounts") .. "/chatcommands.lua")
dofile(minetest.get_modpath("bank_accounts") .. "/interest.lua")

67
interest.lua Normal file
View file

@ -0,0 +1,67 @@
-- interest.lua (Nutzt die neue, dedizierte API-Funktion)
-- KONFIGURATION
local BALANCE_INTEREST_RATE = 0.005
local CREDIT_INTEREST_RATE = 0.03
local INTEREST_CHECK_INTERVAL = 600
local ONE_DAY_IN_SECONDS = 86400
local timestamp_file_path = minetest.get_worldpath() .. "/bank_interest_timestamp.txt"
local function read_timestamp() local f = io.open(timestamp_file_path, "r"); if not f then return 0 end; local time = tonumber(f:read("*a")); f:close(); return time or 0 end
local function write_timestamp(time) local f = io.open(timestamp_file_path, "w"); if not f then return end; f:write(tostring(time)); f:close() end
local time_since_last_check = 0
bank_accounts.is_calculating_interest = false
minetest.register_globalstep(function(dtime)
time_since_last_check = time_since_last_check + dtime
if time_since_last_check < INTEREST_CHECK_INTERVAL then return end
time_since_last_check = 0
local last_timestamp = read_timestamp()
if os.time() - last_timestamp >= ONE_DAY_IN_SECONDS then
if bank_accounts.is_calculating_interest then return end
minetest.log("action", "[bank_accounts] Starting daily interest calculation...")
bank_accounts.is_calculating_interest = true
-- KORREKTUR: Nutzt die neue, dedizierte Funktion
local data = bank_accounts.get_all_data()
local changes_made = false
-- Zinsen auf Guthaben
if BALANCE_INTEREST_RATE > 0 then
for player_name, balance in pairs(data.balance) do
if balance > 0 then
local interest = balance * BALANCE_INTEREST_RATE
data.balance[player_name] = balance + interest
if not data.history then data.history = {} end; if not data.history[player_name] then data.history[player_name] = {} end
table.insert(data.history[player_name], 1, { timestamp = os.time(), type = "Interest Paid", account = "balance", amount = interest, new_total = data.balance[player_name], desc = S("Daily interest (@1%)", BALANCE_INTEREST_RATE * 100), other = "Bank" })
changes_made = true
end
end
end
-- Zinsen auf Kredit
if CREDIT_INTEREST_RATE > 0 then
for player_name, credit in pairs(data.credit) do
if credit > 0 then
local interest = credit * CREDIT_INTEREST_RATE
data.credit[player_name] = credit + interest
if not data.history then data.history = {} end; if not data.history[player_name] then data.history[player_name] = {} end
table.insert(data.history[player_name], 1, { timestamp = os.time(), type = "Interest Charged", account = "credit", amount = interest, new_total = data.credit[player_name], desc = S("Daily interest (@1%)", CREDIT_INTEREST_RATE * 100), other = "Bank" })
changes_made = true
end
end
end
if changes_made then
bank_accounts.save_all(data)
minetest.log("action", "[bank_accounts] Daily interest calculation complete.")
end
write_timestamp(os.time())
bank_accounts.is_calculating_interest = false
end
end)

21
license.txt Normal file
View file

@ -0,0 +1,21 @@
License: MIT
Copyright 2016 Trent Lasich
Copyright 2025 Rage87 (complete redo + extend)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

229
locale/bank_accounts.de.tr Normal file
View file

@ -0,0 +1,229 @@
# textdomain: bank_accounts
# Items (cards.lua)
ATM Card=Bankkarte
Credit Card=Kreditkarte
Debit Card=Debitkarte
Receipt=Kassenbon
# ATM, Computer & WTM (core.lua / atm.lua, computer.lua, wtm.lua)
Automatic Teller Machine=Geldautomat
[ATM] Must use ATM card.=[ATM] Bitte eine Bankkarte benutzen.
Four Digit Pin:=Vierstellige PIN:
Enter=Bestätigen
Cancel=Abbrechen
[ATM] Invalid Pin.=[ATM] Ungültige PIN.
Withdraw=Auszahlung
Deposit=Einzahlung
Pay Credit Debt=Kredit zurückzahlen
Account Balance: @1=Kontostand: @1
Total Credit Debt: @1=Kreditsumme: @1
Next Rate: @1=Nächste Rate: @1
Get Credit Card=Kreditkarte anfordern
Get Debit Card=Debitkarte anfordern
Close=Schließen
Amount:=Betrag:
Amount to pay:=Zu zahlender Betrag:
[ATM] Invalid amount.=[ATM] Ungültiger Betrag.
[ATM] Funds Successfully Withdrawn!=[ATM] Geld erfolgreich ausgezahlt!
Please take your money.=Bitte entnimm dein Geld.
[ATM] Insufficient funds.=[ATM] Guthaben nicht ausreichend.
[ATM] Deposited @1.=[ATM] @1 eingezahlt.
[ATM] Withdrawn money has been moved to your inventory.=[ATM] Ausgezahltes Geld wurde in dein Inventar verschoben.
[ATM] Please enter a PIN.=[ATM] Bitte eine PIN eingeben.
[ATM] Insufficient funds or invalid amount.=[ATM] Guthaben nicht ausreichend oder ungültiger Betrag.
[ATM] You don't have that much credit debt.=[ATM] Du hast keine so hohen Kreditschulden.
[ATM] Paid @1 of credit debt.=[ATM] @1 der Kreditschulden bezahlt.
[ATM] You must pay at least the minimum monthly rate.=[ATM] Die Zahlung muss mindestens der Rate entsprechen.
Qualified Bank Teller=Qualifizierter Bankangestellter
Bank Teller's Computer=Bankschalter-Computer
Search:=Suche:
Search=Suchen
Player:=Spieler:
Seized:=Gesperrt:
[Bank] Insufficient privileges.=[Bank] Unzureichende Berechtigungen.
Deposit:=Einzahlung:
Withdraw:=Auszahlung:
Credit Payment:=Kreditzahlung:
Balance: @1=Kontostand: @1
Credit Debt: @1=Kreditschulden: @1
Show Account=Konto anzeigen
Wipe Account=Konto leeren
Reset PIN=PIN zurücksetzen
No Results!=Keine Ergebnisse!
[Bank] Must enter a player name.=[Bank] Spielername muss eingegeben werden.
[Bank] Invalid player name entered.=[Bank] Ungültiger Spielername eingegeben.
[Bank] Player's debt has been forgiven!=[Bank] Die Schulden des Spielers wurden erlassen!
[Bank] Account successfully wiped!=[Bank] Konto erfolgreich geleert!
[Bank] Your account has been wiped by teller @1.=[Bank] Dein Konto wurde von Kassierer @1 geleert.
[Bank] Player's pin successfully reset!=[Bank] Die PIN des Spielers wurde erfolgreich zurückgesetzt!
[Bank] Your PIN has been reset by a teller.=[Bank] Deine PIN wurde von einem Kassierer zurückgesetzt.
[Bank] Deposited @1 for @2.=[Bank] @1 für @2 eingezahlt.
[Bank] @1 MG was deposited into your account by teller @2. New balance: @3 MG=[Bank] @1 MG wurden von Kassierer @2 auf dein Konto eingezahlt. Neuer Kontostand: @3 MG
[Bank] Withdrawal amount must be a whole number.=[Bank] Auszahlungsbetrag muss eine ganze Zahl sein.
[Bank] Player has insufficient funds for withdrawal.=[Bank] Der Spieler hat nicht genug Guthaben für die Auszahlung.
[Bank] @1 MG was withdrawn from your account by teller @2. New balance: @3 MG=[Bank] @1 MG wurden von Kassierer @2 von deinem Konto abgehoben. Neuer Kontostand: @3 MG
[Bank] Credit payment must be a whole number.=[Bank] Kreditzahlung muss eine ganze Zahl sein.
[Bank] Player does not have that much credit debt.=[Bank] Der Spieler hat keine so hohen Kreditschulden.
[Bank] Player has insufficient funds for credit payment.=[Bank] Der Spieler hat nicht genug Guthaben für die Kreditzahlung.
No Account=Kein Konto
Yes=Ja
No=Nein
Wire Transfer Machine=Überweisungs-Maschine
[WTM] Must use ATM card.=[WTM] Bitte eine Bankkarte benutzen.
[WTM] Invalid Pin.=[WTM] Ungültige PIN.
Transfer from Balance=Von Guthaben überweisen
Transfer from Credit=Von Kredit überweisen
Transfer from Balance (Available: @1 MG)=Von Guthaben überweisen (Verfügbar: @1 MG)
Transfer from Credit (Debt: @1 MG)=Von Kredit überweisen (Schulden: @1 MG)
Recipient:=Empfänger:
Purpose:=Verwendungszweck:
Send=Senden
[WTM] Recipient not found or has no account.=[WTM] Empfänger nicht gefunden oder hat kein Konto.
[WTM] Invalid amount.=[WTM] Ungültiger Betrag.
[WTM] Insufficient funds.=[WTM] Guthaben nicht ausreichend.
[WTM] Successfully transferred @1 MG to @2.=[WTM] Erfolgreich @1 MG an @2 überwiesen.
[WTM] You received a transfer of @1 MG from @2.=[WTM] Du hast eine Überweisung über @1 MG von @2 erhalten.
# Card Swipe (card_swipe.lua)
Card Swipe=Kartenleser
Card Swipe (owned by @1)=Kartenleser (im Besitz von @1)
Price in MG:=Preis in MG:
Items for Sale:=Zu verkaufen:
Your Inventory:=Dein Inventar:
Reset Price=Preis zurücksetzen
Set Price=Preis festlegen
[Card Swipe] No price has been set.=[Kartenleser] Es wurde kein Preis festgelegt.
[Card Swipe] This machine is empty.=[Kartenleser] Dieses Gerät ist leer.
[Card Swipe] Must use a debit or credit card.=[Kartenleser] Es muss eine EC- oder Kreditkarte verwendet werden.
[Card Swipe] Your account has been seized! Transaction denied.=[Kartenleser] Dein Konto wurde gesperrt! Transaktion abgelehnt.
[Card Swipe] Card declined. Insufficient funds.=[Kartenleser] Karte abgelehnt. Guthaben nicht ausreichend.
[Card Swipe] A buyer tried to purchase your items but had insufficient funds.=[Kartenleser] Ein Käufer hatte nicht genug Geld für deine Items.
Price: @1=Preis: @1
Owner: @1=Besitzer: @1
Items for Sale (Click 'Buy' to receive):=Zu verkaufen (Klicke 'Kaufen' zum Erhalten):
Buy=Kaufen
[Card Swipe] Invalid price. Must be a whole number greater than 0.=[Kartenleser] Ungültiger Preis. Muss eine ganze Zahl größer 0 sein.
[Card Swipe] Invalid price. Must be a number greater than 0.=[Kartenleser] Ungültiger Preis. Muss eine Zahl größer 0 sein.
[Card Swipe] Price has been reset.=[Kartenleser] Preis wurde zurückgesetzt.
[Card Swipe] This offer is no longer valid.=[Kartenleser] Dieses Angebot ist nicht mehr gültig.
[Card Swipe] Purchase successful!=[Kartenleser] Kauf erfolgreich!
[Card Swipe] Your items have been sold for @1.=[Kartenleser] Deine Items wurden für @1 verkauft.
[Card Swipe] Payment declined!=[Kartenleser] Zahlung abgelehnt!
# Chat Commands (chatcommands.lua)
Account seized.=Konto gesperrt.
Shows your current account balance and credit debt.=Zeigt deinen aktuellen Kontostand und deine Kreditschulden.
Shows your account balance or the balance of another player (if you have permission).=Zeigt deinen Kontostand oder den eines anderen Spielers an (falls du die Berechtigung hast).
Set pin for your bank account.=Setzt die PIN für dein Bankkonto.
[Account] No numbers entered.=[Konto] Keine Zahl eingegeben.
[Account] Pin successfully set!=[Konto] PIN erfolgreich gesetzt!
[Account] Invalid number entered. Must be exactly 4 digits.=[Konto] Ungültige Nummer. Muss genau 4 Ziffern lang sein.
[Bank] Player name missing.=[Bank] Spielername fehlt.
[Bank] No number entered.=[Bank] Keine Zahl eingegeben.
[Bank] Invalid number entered.=[Bank] Ungültige Zahl eingegeben.
[Bank] Amount must be a whole number.=[Bank] Betrag muss eine ganze Zahl sein.
Add to a player's account balance.=Fügt dem Konto eines Spielers Geld hinzu.
[Bank] Number must be greater than 0.=[Bank] Zahl muss größer als 0 sein.
[Bank] @1 successfully added to @2's account.=[Bank] @1 wurde dem Konto von @2 erfolgreich hinzugefügt.
Subtract from a player's account balance.=Zieht Geld vom Konto eines Spielers ab.
[Bank] @1 successfully subtracted from @2's account.=[Bank] @1 wurde vom Konto von @2 erfolgreich abgezogen.
Set a player's account balance.=Setzt den Kontostand eines Spielers.
[Bank] Number must be 0 or greater.=[Bank] Zahl muss 0 oder größer sein.
[Bank] Funds successfully set for @1.=[Bank] Kontostand für @1 erfolgreich gesetzt.
Set a player's credit debt.=Setzt die Kreditschulden eines Spielers.
[Bank] Credit debt for @1 has been set to @2.=[Bank] Kreditschulden für @1 wurden auf @2 gesetzt.
Wipe a player's bank account.=Leert das Bankkonto eines Spielers.
[Bank] Account successfully wiped for @1!=[Bank] Konto von @1 erfolgreich geleert!
Seize a player's account.=Sperrt das Konto eines Spielers.
[Bank] Account successfully seized for @1!=[Bank] Konto von @1 erfolgreich gesperrt!
Unseize a player's account.=Entsperrt das Konto eines Spielers.
[Bank] Account successfully unseized for @1!=[Bank] Konto von @1 erfolgreich entsperrt!
Your Balance: @1=Dein Kontostand: @1
Your Credit Debt: @1=Deine Kreditschulden: @1
Balance for @1: @2=Kontostand für @1: @2
Credit Debt for @1: @2=Kreditschulden für @1: @2
Player '@1' not found or has no account.=Spieler '@1' nicht gefunden oder hat kein Konto.
You do not have permission to view other players' accounts.=Du hast keine Berechtigung, die Konten anderer Spieler einzusehen.
[<playername>]=[<Spielername>]
<4-digit-pin>=<4-stellige-PIN>
<name> <number>=<Name> <Zahl>
<name>=<Name>
# Kontoauszug
Account Statement=Kontoauszug
Date=Datum
Amount=Betrag
New Balance=Neuer Stand
Method=Methode
Purpose=Verwendungszweck
Description=Beschreibung
Partner=Partner
Account Statement for @1=Kontoauszug für @1
Balance=Guthaben
Credit=Kredit
Current Balance: @1=Aktueller Kontostand: @1
Current Credit Debt: @1=Aktuelle Kreditschulden: @1
Back=Zurück
Pay Next Rate (@1 MG)=Nächste Rate zahlen (@1 MG)
Or enter a custom amount:=Oder einen eigenen Betrag eingeben:
Next calculated rate:=Nächste kalkulierte Rate:
Pay Custom Amount=Eigenen Betrag zahlen
# Transaktionstypen
ATM Deposit=Einzahlung (ATM)
ATM Withdrawal=Auszahlung (ATM)
Credit Payment=Kreditzahlung
Rate Payment=Ratenzahlung
Teller Wipe=Kontoleerung (Schalter)
Teller Deposit=Einzahlung (Schalter)
Teller Withdrawal=Auszahlung (Schalter)
Teller Credit Payment=Kreditzahlung (Schalter)
Purchase=Kauf
Sale=Verkauf
Credit Purchase=Kauf (Kredit)
Admin Add=Admin: Einzahlung
Admin Subtract=Admin: Abhebung
Admin Set Balance=Admin: Stand gesetzt
Admin Set Credit=Admin: Kredit gesetzt
Admin Wipe=Admin: Konto geleert
By Admin: @1=Durch Admin: @1
Teller: @1=Kassierer: @1
Deposit by teller @1=Einzahlung durch Kassierer @1
Withdrawal by teller @1=Auszahlung durch Kassierer @1
Credit payment by teller @1=Kreditzahlung durch Kassierer @1
Account wiped by teller @1=Konto geleert durch Kassierer @1
Items from @1=Artikel von @1
Items to @1=Artikel an @1
Transfer Sent=Überweisung (Gesendet)
Transfer Sent (Credit)=Überweisung (Kredit)
Transfer Received=Überweisung (Empfangen)
# Interest
Interest Paid=Zinsen (Guthaben)
Interest Charged=Zinsen (Kredit)
# PIN Terminal
PIN Terminal=PIN-Terminal
Welcome! Please set your personal 4-digit PIN.=Willkommen! Bitte lege eine persönliche 4-stellige PIN fest.
New PIN:=Neue PIN:
Confirm=Bestätigen
[PIN Terminal] Your PIN must be 4 digits and not '0000'.=[PIN-Terminal] Deine PIN muss 4 Ziffern lang und darf nicht '0000' sein.
[PIN Terminal] Your PIN has been set! Your new bank card is in your inventory.=[PIN-Terminal] Deine PIN wurde gesetzt! Deine neue Bankkarte ist in deinem Inventar.
PIN & Card Service=PIN- & Karten-Service
What would you like to do?=Was möchtest du tun?
Change PIN=PIN ändern
Request New Card=Neue Karte anfordern
[PIN Terminal] You already have a bank card.=[PIN-Terminal] Du besitzt bereits eine Bankkarte.
[PIN Terminal] A new bank card has been added to your inventory.=[PIN-Terminal] Eine neue Bankkarte wurde deinem Inventar hinzugefügt.
Change Your PIN=Ändere deine PIN
Old PIN:=Alte PIN:
Confirm New PIN:=Neue PIN bestätigen:
[PIN Terminal] Your old PIN is incorrect.=[PIN-Terminal] Deine alte PIN ist falsch.
[PIN Terminal] The new PINs do not match.=[PIN-Terminal] Die neuen PINs stimmen nicht überein.
[PIN Terminal] Your PIN has been successfully changed.=[PIN-Terminal] Deine PIN wurde erfolgreich geändert.

7
mod.conf Normal file
View file

@ -0,0 +1,7 @@
name = bank_accounts
title = Bank Accounts (Redo & Extended)
description = A complete overhaul of the classic bank_accounts mod. Adds ATMs, a Teller Computer, PIN Terminals, Card Swipes, and Wire Transfer Machines. Features a full account system with balance and credit, a detailed transaction history (account statement), and an automatic daily interest system.
author = Tmanyo, Rage87
version = 2037
license = MIT
depends = currency

BIN
models/ATM.blend Normal file

Binary file not shown.

10
models/atm.mtl Normal file
View file

@ -0,0 +1,10 @@
# Blender MTL File: 'None'
# Material Count: 1
newmtl None
Ns 0
Ka 0.000000 0.000000 0.000000
Kd 0.8 0.8 0.8
Ks 0.8 0.8 0.8
d 1
illum 2

91
models/atm.obj Normal file
View file

@ -0,0 +1,91 @@
# Blender v2.75 (sub 0) OBJ File: ''
# www.blender.org
mtllib atm.mtl
o atm
v 0.500000 -0.500000 -0.500000
v 0.500000 0.200000 -0.200000
v 0.500000 0.500000 0.300000
v 0.500000 -0.500000 0.300000
v -0.500000 -0.500000 0.300000
v -0.500000 0.500000 0.300000
v -0.500000 0.200000 -0.200000
v -0.500000 -0.500000 -0.500000
v 0.451229 -0.500000 -0.500000
v 0.451229 -0.300000 -0.100000
v 0.451229 0.450000 0.300000
v -0.451229 0.450000 0.300000
v -0.451229 -0.300000 -0.100000
v -0.451229 -0.500000 -0.500000
v 0.500000 0.500000 0.500000
v 0.500000 -0.500000 0.500000
v 0.451229 0.500000 0.500000
v -0.451229 -0.500000 0.500000
v -0.451229 0.500000 0.500000
v -0.500000 0.500000 0.500000
v -0.500000 -0.500000 0.500000
v 0.451229 -0.500000 0.500000
vt 0.625000 0.625000
vt 0.875000 0.734375
vt 0.984375 0.906250
vt 0.625000 0.906250
vt 0.515625 0.281250
vt 0.359375 0.250000
vt 0.343750 0.062500
vt 0.515625 0.265625
vt 0.156250 0.031250
vt 0.140625 0.015625
vt 0.140625 0.390625
vt 0.156250 0.359375
vt 0.359375 0.156250
vt 0.343750 0.343750
vt 0.390625 0.984375
vt 0.015625 0.984375
vt 0.015625 0.640625
vt 0.390625 0.640625
vt 0.515625 0.140625
vt 0.515625 0.125000
vt 0.390625 0.453125
vt 0.015625 0.453125
vt 0.984375 0.984375
vt 0.625000 0.984375
vt 0.078125 0.031250
vt 0.640625 0.015625
vt 0.640625 0.359375
vt 0.625000 0.359375
vt 0.625000 0.015625
vt 0.984375 0.015625
vt 0.984375 0.359375
vt 0.968750 0.359375
vt 0.968750 0.015625
vt 0.078125 0.359375
vt 0.078125 0.375000
vn 1.000000 0.000000 0.000000
vn -1.000000 -0.000000 -0.000000
vn -0.969500 0.150400 -0.193400
vn -0.975600 0.142700 -0.166500
vn 0.975600 0.142700 -0.166500
vn 0.000000 0.470600 -0.882400
vn 0.969500 0.150400 -0.193400
vn 0.000000 0.894400 -0.447200
vn -0.453400 0.884400 -0.110600
vn -0.000000 0.000000 1.000000
vn 0.000000 0.970100 -0.242500
vn 0.453400 0.884400 -0.110600
usemtl None
s 1
f 1/1/1 2/2/1 3/3/1 4/4/1
f 5/4/2 6/3/2 7/2/2 8/1/2
f 9/5/3 10/6/3 2/7/3 1/8/3
f 11/9/4 3/10/4 2/7/4 10/6/4
f 6/11/5 12/12/5 13/13/5 7/14/5
f 12/15/6 11/16/6 10/17/6 13/18/6
f 8/19/7 7/14/7 13/13/7 14/20/7
f 14/21/8 13/18/8 10/17/8 9/22/8
f 4/4/1 3/3/1 15/23/1 16/24/1
f 3/10/9 11/9/9 17/25/9 15/25/9
f 18/26/10 19/27/10 20/28/10 21/29/10
f 16/30/10 15/31/10 17/32/10 22/33/10
f 22/33/10 17/32/10 19/27/10 18/26/10
f 6/3/2 5/4/2 21/24/2 20/23/2
f 11/9/11 12/12/11 19/34/11 17/25/11
f 12/12/12 6/11/12 20/35/12 19/34/12

10
models/card_swipe.mtl Normal file
View file

@ -0,0 +1,10 @@
# Blender MTL File: 'None'
# Material Count: 1
newmtl None
Ns 0
Ka 0.000000 0.000000 0.000000
Kd 0.8 0.8 0.8
Ks 0.8 0.8 0.8
d 1
illum 2

54
models/card_swipe.obj Normal file
View file

@ -0,0 +1,54 @@
# Blender v2.75 (sub 0) OBJ File: ''
# www.blender.org
mtllib card_swipe.mtl
o card_swipe
v 0.250000 -0.500000 -0.250000
v 0.250000 -0.300000 -0.250000
v 0.250000 -0.300000 0.250000
v 0.250000 -0.500000 0.250000
v -0.250000 -0.300000 0.250000
v -0.250000 -0.500000 0.250000
v -0.250000 -0.300000 -0.250000
v -0.250000 -0.500000 -0.250000
v -0.350000 -0.500000 0.200000
v -0.350000 -0.400000 0.200000
v -0.350000 -0.400000 -0.200000
v -0.350000 -0.500000 -0.200000
v -0.250000 -0.400000 0.200000
v -0.250000 -0.400000 -0.200000
v -0.250000 -0.500000 0.200000
v -0.250000 -0.500000 -0.200000
vt 0.015625 0.718750
vt 0.015625 0.531250
vt 0.500000 0.531250
vt 0.500000 0.718750
vt 0.515625 0.500000
vt 0.015625 0.500000
vt 0.015625 0.015625
vt 0.515625 0.015625
vt 0.531250 0.125000
vt 0.625000 0.125000
vt 0.625000 0.515625
vt 0.531250 0.515625
vt 0.734375 0.125000
vt 0.734375 0.515625
vt 0.734375 0.015625
vt 0.625000 0.015625
vt 0.734375 0.625000
vt 0.625000 0.625000
vn 1.000000 0.000000 0.000000
vn -0.000000 0.000000 1.000000
vn -1.000000 -0.000000 -0.000000
vn 0.000000 -0.000000 -1.000000
vn 0.000000 1.000000 -0.000000
usemtl None
s 1
f 1/1/1 2/2/1 3/3/1 4/4/1
f 4/1/2 3/2/2 5/3/2 6/4/2
f 6/1/3 5/2/3 7/3/3 8/4/3
f 8/1/4 7/2/4 2/3/4 1/4/4
f 5/5/5 3/6/5 2/7/5 7/8/5
f 9/9/3 10/10/3 11/11/3 12/12/3
f 13/13/5 14/14/5 11/11/5 10/10/5
f 15/15/2 13/13/2 10/10/2 9/16/2
f 14/14/4 16/17/4 12/18/4 11/11/4

10
models/computer.mtl Normal file
View file

@ -0,0 +1,10 @@
# Blender MTL File: 'None'
# Material Count: 1
newmtl None
Ns 0
Ka 0.000000 0.000000 0.000000
Kd 0.8 0.8 0.8
Ks 0.8 0.8 0.8
d 1
illum 2

167
models/computer.obj Normal file
View file

@ -0,0 +1,167 @@
# Blender v2.78 (sub 4) OBJ File: 'bank-computer.blend'
# www.blender.org
o Cube.001
v 0.440909 -0.300000 -0.009091
v 0.440909 0.300000 -0.009091
v 0.440909 -0.300000 0.090909
v 0.440909 0.300000 0.090909
v -0.459091 -0.300000 -0.009091
v -0.459091 0.300000 -0.009091
v -0.459091 -0.300000 0.090909
v -0.459091 0.300000 0.090909
v 0.440909 -0.500000 -0.409091
v 0.440909 -0.440000 -0.409091
v 0.440909 -0.500000 -0.109091
v 0.440909 -0.440000 -0.109091
v -0.259091 -0.500000 -0.409091
v -0.259091 -0.440000 -0.409091
v -0.259091 -0.500000 -0.109091
v -0.259091 -0.440000 -0.109091
v -0.309091 -0.500000 -0.409091
v -0.309091 -0.440000 -0.409091
v -0.309091 -0.500000 -0.209091
v -0.309091 -0.440000 -0.209091
v -0.459091 -0.500000 -0.409091
v -0.459091 -0.440000 -0.409091
v -0.459091 -0.500000 -0.209091
v -0.459091 -0.440000 -0.209091
v 0.090909 -0.400000 0.090909
v 0.090909 -0.100000 0.090909
v 0.090909 -0.400000 0.190909
v 0.090909 -0.100000 0.190909
v -0.109091 -0.400000 0.090909
v -0.109091 -0.100000 0.090909
v -0.109091 -0.400000 0.190909
v -0.109091 -0.100000 0.190909
v 0.090909 -0.500000 0.190909
v 0.090909 -0.500000 0.090909
v -0.109091 -0.500000 0.190909
v -0.109091 -0.500000 0.090909
v -0.309091 -0.450000 -0.009091
v -0.309091 -0.450000 0.090909
v -0.309091 -0.500000 -0.009091
v -0.309091 -0.500000 0.090909
v 0.290909 -0.450000 0.090909
v 0.290909 -0.450000 -0.009091
v 0.290909 -0.500000 0.090909
v 0.290909 -0.500000 -0.009091
vt 0.066820 0.466486
vt 0.666318 0.466486
vt 0.666318 0.533096
vt 0.066820 0.533097
vt 0.666318 0.066820
vt 0.732929 0.066820
vt 0.732929 0.466486
vt 0.066820 0.066820
vt 0.000209 0.466486
vt 0.000209 0.066820
vt 0.066820 0.000209
vt 0.666318 0.000209
vt 0.000209 0.573481
vt 0.040176 0.573481
vt 0.040176 0.773314
vt 0.000209 0.773314
vt 0.040176 0.813281
vt 0.506452 0.773314
vt 0.506452 0.813280
vt 0.546419 0.773314
vt 0.506452 0.573481
vt 0.546419 0.573481
vt 0.506452 0.533515
vt 0.040176 0.533515
vt 0.913197 0.440259
vt 0.873230 0.440259
vt 0.873230 0.307037
vt 0.913197 0.307037
vt 0.873230 0.267071
vt 0.773314 0.307037
vt 0.773314 0.267071
vt 0.733347 0.307037
vt 0.773314 0.440259
vt 0.733347 0.440259
vt 0.773314 0.480226
vt 0.873230 0.480226
vt 0.799958 0.000209
vt 0.799958 0.200042
vt 0.733347 0.200042
vt 0.733347 0.000209
vt 0.695784 0.733348
vt 0.695784 0.533515
vt 0.829006 0.533515
vt 0.829005 0.733348
vt 0.999791 0.000209
vt 0.999791 0.200042
vt 0.933180 0.200042
vt 0.933180 0.000209
vt 0.829005 0.799958
vt 0.695784 0.799958
vt 0.933180 0.266653
vt 0.799958 0.266653
vt 0.546837 0.799958
vt 0.546837 0.766653
vt 0.328753 0.893736
vt 0.328753 0.813699
vt 0.466256 0.828528
vt 0.449682 0.853351
vt 0.168677 0.893736
vt 0.168677 0.813699
vt 0.467696 0.907275
vt 0.504691 0.888922
vt 0.322185 0.961134
vt 0.977952 0.766653
vt 0.977952 0.799958
vt 0.051072 0.854687
vt 0.029923 0.905485
vt 0.000209 0.890334
vt 0.030511 0.830905
vt 0.174376 0.961042
vt 1.000000 0.000000
vt 1.000000 1.000000
vt 0.000000 1.000000
vt 0.000000 0.000000
vn 0.0000 -1.0000 0.0000
vn -1.0000 0.0000 0.0000
vn -0.0000 0.0000 1.0000
vn 1.0000 0.0000 -0.0000
vn 0.0000 1.0000 0.0000
vn 0.0000 0.0000 -1.0000
vn 0.4472 0.0000 0.8944
vn 0.4472 0.0000 -0.8944
vn -0.2425 0.9701 0.0000
vn -0.4472 0.0000 0.8944
vn 0.2425 0.9701 0.0000
vn -0.4472 0.0000 -0.8944
g Cube.001_Cube.001_Material
s off
f 3/1/1 7/2/1 5/3/1 1/4/1
f 7/2/2 8/5/2 6/6/2 5/7/2
f 3/1/3 4/8/3 8/5/3 7/2/3
f 1/9/4 2/10/4 4/8/4 3/1/4
f 8/5/5 4/8/5 2/11/5 6/12/5
f 9/13/4 10/14/4 12/15/4 11/16/4
f 11/17/3 12/15/3 16/18/3 15/19/3
f 15/20/2 16/18/2 14/21/2 13/22/2
f 13/23/6 14/21/6 10/14/6 9/24/6
f 16/18/5 12/15/5 10/14/5 14/21/5
f 17/25/4 18/26/4 20/27/4 19/28/4
f 19/29/3 20/27/3 24/30/3 23/31/3
f 23/32/2 24/30/2 22/33/2 21/34/2
f 21/35/6 22/33/6 18/26/6 17/36/6
f 24/30/5 20/27/5 18/26/5 22/33/5
f 25/37/4 26/38/4 28/39/4 27/40/4
f 27/41/3 28/42/3 32/43/3 31/44/3
f 31/45/2 32/46/2 30/47/2 29/48/2
f 29/48/6 30/47/6 26/38/6 25/37/6
f 27/41/3 31/44/3 35/49/3 33/50/3
f 32/51/5 28/52/5 26/38/5 30/47/5
f 27/41/7 33/50/7 43/53/7 41/54/7
f 29/55/8 36/56/8 39/57/8 37/58/8
f 29/55/6 25/59/6 34/60/6 36/56/6
f 38/61/2 37/58/2 39/57/2 40/62/2
f 31/63/9 29/55/9 37/58/9 38/61/9
f 35/49/10 31/44/10 38/64/10 40/65/10
f 42/66/4 41/67/4 43/68/4 44/69/4
f 25/59/11 27/70/11 41/67/11 42/66/11
f 34/60/12 25/59/12 42/66/12 44/69/12
g Cube.001_Cube.001_Material.001
f 5/71/6 6/72/6 2/73/6 1/74/6

135
pin_terminal.lua Normal file
View file

@ -0,0 +1,135 @@
--[[
PIN Terminal Node
-----------------
This file defines the PIN Terminal, which allows players to
set their initial PIN, change it, and request a new card.
It provides an immersive replacement for the /set_pin command.
--]]
-- Shows the form for a new player to set their first PIN.
local function show_pin_create_form(player, error_msg)
local player_name = player:get_player_name()
local formspec = "size[8,5]" ..
"label[1,0.5;"..S("Welcome! Please set your personal 4-digit PIN.").."]" ..
"pwdfield[2,2;4,1;new_pin;"..S("New PIN:").."]" ..
"button_exit[3,4;2,1;confirm_new_pin;"..S("Confirm").."]"
if error_msg then
formspec = formspec .. "label[1,3;`"..minetest.formspec_escape(S(error_msg)).."`]" .. "style[label;color=red]"
end
minetest.show_formspec(player_name, "bank_accounts:pin_create", formspec)
end
-- Shows the main menu for an existing player at the PIN terminal.
local function show_pin_options_form(player)
local player_name = player:get_player_name()
minetest.show_formspec(player_name, "bank_accounts:pin_options",
"size[8,6]" ..
"label[1,0.5;"..S("PIN & Card Service").."]" ..
"label[1,1.5;"..S("What would you like to do?").."]" ..
"button_exit[2,3;4,1;change_pin;"..S("Change PIN").."]" ..
"button_exit[2,4;4,1;new_card;"..S("Request New Card").."]")
end
-- Shows the form to change an existing PIN.
-- Can display an error message if something goes wrong.
local function show_pin_change_form(player, error_msg)
local player_name = player:get_player_name()
local formspec = "size[8,7]" ..
"label[1,0.5;"..S("Change Your PIN").."]" ..
"pwdfield[2,2;4,1;old_pin;"..S("Old PIN:").."]" ..
"pwdfield[2,3;4,1;new_pin1;"..S("New PIN:").."]" ..
"pwdfield[2,4;4,1;new_pin2;"..S("Confirm New PIN:").."]" ..
"button_exit[3,6;2,1;confirm_change;"..S("Confirm").."]"
if error_msg then
formspec = formspec .. "label[1,5;`"..minetest.formspec_escape(S(error_msg)).."`]" .. "style[label;color=red]"
end
minetest.show_formspec(player_name, "bank_accounts:pin_change", formspec)
end
-- Node definition for the PIN Terminal.
minetest.register_node("bank_accounts:pin_terminal", {
description = S("PIN Terminal"),
drawtype = "mesh",
mesh = "card_swipe.obj",
paramtype = "light",
paramtype2 = "facedir",
tiles = {"pin_terminal.png"},
groups = {cracky=3, crumbly=3, oddly_breakable_by_hand=2},
-- Called when a player right-clicks the node.
-- It does not require a card to operate.
on_rightclick = function(pos, node, player, itemstack, pointed_thing)
local player_name = player:get_player_name()
-- Check if the player is new (has the default PIN).
if bank_accounts.get_pin(player_name) == "0000" then
show_pin_create_form(player)
else
show_pin_options_form(player)
end
end,
})
-- Handles formspec submissions for all PIN Terminal forms.
minetest.register_on_player_receive_fields(function(player, formname, fields)
-- Only process forms belonging to this node.
if not formname:find("bank_accounts:pin_") then
return
end
local player_name = player:get_player_name()
-- Logic for initial PIN creation.
if formname == "bank_accounts:pin_create" then
if fields.confirm_new_pin then
local new_pin = fields.new_pin
if new_pin and new_pin:match("^[0-9][0-9][0-9][0-9]$") and new_pin ~= "0000" then
bank_accounts.set_pin(player_name, new_pin)
local inv = player:get_inventory()
if not inv:contains_item("main", "bank_accounts:atm_card") then
inv:add_item("main", "bank_accounts:atm_card")
end
minetest.chat_send_player(player_name, S("[PIN Terminal] Your PIN has been set! Your new bank card is in your inventory."))
else
-- Re-show form with an error message.
show_pin_create_form(player, "[PIN Terminal] Your PIN must be 4 digits and not '0000'.")
end
end
-- Logic for the options menu.
elseif formname == "bank_accounts:pin_options" then
if fields.change_pin then
show_pin_change_form(player)
elseif fields.new_card then
local inv = player:get_inventory()
if inv:contains_item("main", "bank_accounts:atm_card") then
minetest.chat_send_player(player_name, S("[PIN Terminal] You already have a bank card."))
else
inv:add_item("main", "bank_accounts:atm_card")
minetest.chat_send_player(player_name, S("[PIN Terminal] A new bank card has been added to your inventory."))
end
end
-- Logic for the PIN change form.
elseif formname == "bank_accounts:pin_change" then
if fields.confirm_change then
local old_pin = fields.old_pin
local new_pin1 = fields.new_pin1
local new_pin2 = fields.new_pin2
if old_pin ~= bank_accounts.get_pin(player_name) then
show_pin_change_form(player, "[PIN Terminal] Your old PIN is incorrect.")
elseif not new_pin1 or not new_pin1:match("^[0-9][0-9][0-9][0-9]$") or new_pin1 == "0000" then
show_pin_change_form(player, "[PIN Terminal] Your PIN must be 4 digits and not '0000'.")
elseif new_pin1 ~= new_pin2 then
show_pin_change_form(player, "[PIN Terminal] The new PINs do not match.")
else
bank_accounts.set_pin(player_name, new_pin1)
minetest.chat_send_player(player_name, S("[PIN Terminal] Your PIN has been successfully changed."))
end
end
end
end)

BIN
textures/atm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
textures/atm_card.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
textures/atm_col.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
textures/atm_col.png.old Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 B

BIN
textures/atm_uv.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
textures/card_reader_uv.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

BIN
textures/computer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

BIN
textures/credit_card.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
textures/debit_card.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
textures/pin_terminal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
textures/receipt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 B

BIN
textures/wtm_col.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
textures/wtm_uv.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

197
wtm.lua Normal file
View file

@ -0,0 +1,197 @@
--[[
Wire Transfer Machine (WTM) Node
--------------------------------
This file defines the WTM, which allows players to securely transfer
money from their balance or credit line to another player's account.
--]]
-- Uses the global 'pos_info' variable for consistency.
-- Shows the account statement form.
-- This function is identical to the one in atm.lua but is kept here
-- in case of future WTM-specific modifications.
local function show_wtm_statement_form(player, account_type)
local player_name = player:get_player_name()
local data = bank_accounts.get_account_data(player_name)
local history = data.history
local lines = {}
local current_total_label = ""
table.insert(lines, minetest.formspec_escape(string.format("%-11s | %-13s | %-13s | %-20s | %-20s | %s", S("Date"), S("Amount"), S("New Balance"), S("Method"), S("Purpose"), S("Partner"))))
table.insert(lines, minetest.formspec_escape("-----------------------------------------------------------------------------------------"))
if history then
for _, t in ipairs(history) do
if t.account == account_type then
local parts = {}
table.insert(parts, string.format("%-11s", os.date("%Y-%m-%d", t.timestamp)))
table.insert(parts, string.format("%12s", string.format("%+.2f", t.amount) .. " MG"))
table.insert(parts, string.format("%12s", string.format("%.2f", t.new_total).." MG"))
table.insert(parts, string.format("%-20s", S(t.type)))
local purpose = t.purpose or ""
local partner = t.other or ""
if purpose ~= "" or partner ~= "" then
table.insert(parts, string.format("%-20s", purpose))
if partner ~= "" then
table.insert(parts, partner)
end
end
table.insert(lines, minetest.formspec_escape(table.concat(parts, " | ")))
end
end
end
if account_type == "balance" then
current_total_label = S("Current Balance: @1", string.format("%.2f", data.balance) .. " MG")
else
current_total_label = S("Current Credit Debt: @1", string.format("%.2f", data.credit) .. " MG")
end
local form_name = "bank_accounts:wtm_statement@" .. player_name
local formspec = "size[13,9]"..
"label[0,0;"..S("Account Statement for @1", player_name).."]"..
"button_exit[0,0.5;2,1;view_balance;"..S("Balance").."]"..
"button_exit[2,0.5;2,1;view_credit;"..S("Credit").."]"..
"textlist[0,1.2;13,7;statement_list;"..table.concat(lines,",").."]"..
"label[0,8.4;"..current_total_label.."]"..
"button_exit[11,8.4;2,1;back;"..S("Back").."]"
minetest.show_formspec(player_name, form_name, formspec)
end
-- Shows the main menu for the WTM.
function wtm_main_form(player, pos)
local player_name = player:get_player_name()
local data = bank_accounts.get_account_data(player_name)
minetest.show_formspec(player_name, "bank_accounts:wtm_options",
"size[8,8]" ..
"button_exit[0.5,1;3.5,1;transfer_balance;"..S("Transfer from Balance").."]" ..
"button_exit[0.5,2.0;3.5,1;transfer_credit;"..S("Transfer from Credit").."]" ..
"button_exit[5,2.5;2.4,1;statement;"..S("Account Statement").."]" ..
"label[5,1;"..S("Balance: @1", string.format("%.2f", data.balance) .. " MG").."]" ..
"label[5,1.5;"..S("Credit Debt: @1", string.format("%.2f", data.credit) .. " MG").."]" ..
"button_exit[0.5,6;3,1;credit_card;"..S("Get Credit Card").."]" ..
"button_exit[0.5,7;3,1;debit_card;"..S("Get Debit Card").."]" ..
"button_exit[5,7;2,1;exit;"..S("Close").."]")
end
-- Node definition for the Wire Transfer Machine.
minetest.register_node("bank_accounts:wtm", {
description = S("Wire Transfer Machine"),
drawtype = "mesh",
mesh = "atm.obj", -- Uses ATM model as a base
paramtype = "light",
paramtype2 = "facedir",
tiles = {"wtm_col.png"}, -- Custom texture
groups = {cracky=3, crumbly=3, oddly_breakable_by_hand=2},
-- Standard right-click function for PIN entry.
on_rightclick = function(pos, node, player, itemstack, pointed_thing)
pos_info = pos
if itemstack:get_name() ~= "bank_accounts:atm_card" then
minetest.chat_send_player(player:get_player_name(), S("[WTM] Must use ATM card."))
return
end
minetest.show_formspec(player:get_player_name(), "bank_accounts:wtm_home",
"size[8,8]" ..
"pwdfield[2,4;4,1;fourdigitpin;"..S("Four Digit Pin:").."]" ..
"button_exit[5,6;2,1;enter;"..S("Enter").."]" ..
"button_exit[3,6;2,1;exit;"..S("Cancel").."]")
end,
})
-- Central handler for all formspec interactions related to the WTM.
minetest.register_on_player_receive_fields(function(player, formname, fields)
if not formname:find("bank_accounts:wtm") then
return
end
local player_name = player:get_player_name()
local pos = pos_info
if not pos then
return
end
-- Handle PIN entry form.
if formname == "bank_accounts:wtm_home" then
if fields.enter then
if bank_accounts.get_pin(player_name) == fields.fourdigitpin then
wtm_main_form(player, pos)
else
minetest.chat_send_player(player_name, S("[WTM] Invalid Pin."))
end
end
-- Handle main menu options.
elseif formname == "bank_accounts:wtm_options" then
if fields.statement then
show_wtm_statement_form(player, "balance")
elseif fields.transfer_balance or fields.transfer_credit then
local source_account = fields.transfer_balance and "balance" or "credit"
local source_label = ""
if source_account == "balance" then
source_label = S("Transfer from Balance (Available: @1 MG)", string.format("%.2f", bank_accounts.get_balance(player_name)))
else
source_label = S("Transfer from Credit (Debt: @1 MG)", string.format("%.2f", bank_accounts.get_credit(player_name)))
end
minetest.show_formspec(player_name, "bank_accounts:wtm_transfer@"..source_account,
"size[8,8]" ..
"label[0,0;"..source_label.."]" ..
"field[0.5,1.5;7.5,1;recipient;"..S("Recipient:")..";]" ..
"field[0.5,2.5;7.5,1;amount;"..S("Amount:")..";]" ..
"field[0.5,3.5;7.5,1;purpose;"..S("Purpose:")..";]" ..
"button_exit[4,7;2,1;send;"..S("Send").."]" ..
"button_exit[2,7;2,1;cancel;"..S("Cancel").."]")
elseif fields.credit_card then
player:get_inventory():add_item("main", "bank_accounts:credit_card")
wtm_main_form(player, pos)
elseif fields.debit_card then
player:get_inventory():add_item("main", "bank_accounts:debit_card")
wtm_main_form(player, pos)
end
-- Handle the transfer form itself.
elseif formname:find("bank_accounts:wtm_transfer@") then
if fields.send then
local source_account = formname:match("bank_accounts:wtm_transfer@(.*)")
local recipient = fields.recipient
local amount = normalize_and_tonumber(fields.amount)
local purpose = fields.purpose
-- Validation checks
if not recipient or recipient == "" or not bank_accounts.player_has_account(recipient) then
minetest.chat_send_player(player_name, S("[WTM] Recipient not found or has no account."))
elseif not amount or amount <= 0 then
minetest.chat_send_player(player_name, S("[WTM] Invalid amount."))
elseif source_account == "balance" and bank_accounts.get_balance(player_name) < amount then
minetest.chat_send_player(player_name, S("[WTM] Insufficient funds."))
else
-- Process the transaction
if source_account == "balance" then
bank_accounts.add_balance(player_name, -amount, "Transfer Sent", purpose, recipient)
else -- source is "credit"
bank_accounts.add_credit(player_name, amount, "Transfer Sent (Credit)", purpose, recipient)
end
bank_accounts.add_balance(recipient, amount, "Transfer Received", purpose, player_name)
-- Send chat notifications
minetest.chat_send_player(player_name, S("[WTM] Successfully transferred @1 MG to @2.", string.format("%.2f", amount), recipient))
minetest.chat_send_player(recipient, S("[WTM] You received a transfer of @1 MG from @2.", string.format("%.2f", amount), player_name))
end
end
wtm_main_form(player, pos) -- Always return to the main menu
-- Handle the statement form.
elseif formname:find("bank_accounts:wtm_statement@") then
local target_name = formname:match("bank_accounts:wtm_statement@(.*)")
if not target_name then return end
if fields.back then
wtm_main_form(player, pos)
else
show_wtm_statement_form(player, fields.view_credit and "credit" or "balance")
end
end
end)