Banking
Banking works with the bank accounts the player has at the game’s banks. You can read accounts, open one, deposit and withdraw, take and repay loans, and exchange between OC and RM. Each bank account is a BankAccount, a snapshot taken when you asked.
Banking the bank accounts at the bank you are standing at
Use All() or Get(bankId) to read accounts. DepositWithdraw and Exchange only work while that bank’s screen is open.
public static class Banking
{
public static List<BankAccount> All();
public static BankAccount Get(string bankId);
public static bool OpenAccount(string bankId);
public static bool DepositWithdraw(string bankId, int oc, float rm);
public static bool TakeLoan(string bankId, MoneyCurrency currency, int amount);
public static bool RepayLoan(string bankId, int amount);
public static float ExchangeQuote(string bankId, float amount, bool toRm);
public static float ExchangeRate(string bankId, bool toRm);
public static bool Exchange(string bankId, int amount, bool toRm);
public static bool IsFrozen(string bankId);
public static BankResult Freeze(string bankId);
public static BankResult Unfreeze(string bankId);
}
| Method | Returns | What it does |
|---|---|---|
All() | List<BankAccount> | Every bank in the scene you are in. |
Get(bankId) | BankAccount | One bank’s account. |
OpenAccount(bankId) | bool | Opens an account. true when it worked. |
DepositWithdraw(bankId, oc, rm) | bool | Deposits money. A negative number withdraws. |
TakeLoan(bankId, currency, amount) | bool | Takes a loan. |
RepayLoan(bankId, amount) | bool | Repays a loan. |
ExchangeQuote(bankId, amount, toRm) | float | The game’s rate for an amount. -1 when refused. |
ExchangeRate(bankId, toRm) | float | The quote for one unit. |
Exchange(bankId, amount, toRm) | bool | Exchanges money. toRm true changes OC to RM. Otherwise it changes RM to OC. |
BankAccount the player's account at one bank
Banking.All() and Banking.Get(bankId) give you these. Exists is false when the player has no account at that bank.
public sealed class BankAccount
{
public string BankId { get; }
public string BankName { get; }
public bool Exists { get; }
public bool Frozen { get; }
public int Oc { get; }
public float Rm { get; }
public int DebtOc { get; }
public float DebtRm { get; }
public MoneyCurrency LoanCurrency { get; }
public int MaxLoanOc { get; }
public float MaxLoanRm { get; }
}
| Property | Type | What it means |
|---|---|---|
BankId | string | Names the bank. Use it with Get, OpenAccount and the other calls. |
BankName | string | The bank’s name. |
Exists | bool | true when the player has an account here. |
Frozen | bool | true when the account is frozen. |
Oc | int | The OC in the account. |
Rm | float | The RM in the account. |
DebtOc | int | The OC loan still owed. |
DebtRm | float | The RM loan still owed. |
LoanCurrency | MoneyCurrency | The money the loan is in. |
MaxLoanOc | int | The most OC the bank will lend. 0 when there is no account. |
MaxLoanRm | float | The most RM the bank will lend. |
Things to know
bankIdis theGUIDof the bank object in the scene.OcandRmshow what is in the account, even if the account is frozen. The game reports a frozen balance as zero, so checkFrozento know which case you are in.- Pass a negative
ocorrmtoDepositWithdrawto withdraw. A deposit larger than the wallet is cut down by the game. TakeLoanneeds an open, unfrozen account. Open the account first withOpenAccount. The game’s own loan method does not check the limit, so the framework does.RepayLoanpays in the loan’s own currency (LoanCurrency). The payment is cut down to the debt and to what the player has, as the game does. It returnsfalsewhen nothing can be paid. (an RM debt is paid in whole units throughBank.PayLoan. Whenamountcovers one more unit, the rest of the debt is taken from the wallet in whole tenths, the wallet’s smallest step (Money.RMoneyAmount). The part below 0.1 RM can’t be held by the wallet, so it is written off and the debt is cleared to zero. The player is never charged more than the debt, and a payment in tenths that the wallet can’t cover is not taken, so the debt stays at 0.1 RM or more and a later call can clear it.)OpenAccountskips the requirements the game’s dialogue would check, which means the money or items some banks ask for. (it skipsrequiredOCand the required items.)ExchangeQuoteis a preview of what an exchange would give. It gives the game’s rate foramountwithout cutting it down to the wallet or the bank’s supply, and without rounding. (it usesBank.GetExchangeValues.)Exchangereturnstruewhen the game’s exchange ran. The framework never lets the game cut an exchange down. An amount above the wallet or the source supply is refused, so the amount moved is exactlyamount. (the refusal uses the game’s own limits, and the credit the check tests is the game’s formula after those same limits (Bank.Exchange).)- An exchange moves money in the normal way.
Cash.MoneyChangedfires for both sides, and it also changes the bank’s supplies and the game’s “out of funds” and “replenished” triggers. (it goes through the game’sAddMoneyandRemoveMoney.) - The game’s bank interest uses whole-number division (
num / 7inMoney). When 7 or more days pass between two checks, the game addsfloor(days / 7)periods of interest at once and drops the leftover days. Ordinary daily ticks add no interest. OC is cut to whole units and RM is rounded to 2 decimals. The framework does not change this.
Limits
DepositWithdrawonly works while the player has that bank’s screen open. The game’s deposit method reads the open bank’s mode, so this is the only time it is valid. (it needsBankPanel.instance.activeBankto be that bank.)- A withdrawal larger than the account balance is refused. The game’s deposit method does not check this itself (only its slider does). Without the check the account would go negative, and
int.MinValuewould corrupt the balances. - A withdrawal is also refused when it would push the wallet past the most money the game can store. That is the same limit
Giveuses. A deposit is refused when it would push the account’s OC, or the bank’s total OC supply, past that limit. The framework counts the amount the way the game rounds it. (the wallet sum uses the amount as the game rounds it to a float before it truncates to an int. The limit isint.MaxValue, and the supply isTotalOCSupply. Above 2^24 the game can add a little more than the amount typed.) TakeLoanrefuses an amount larger thanMaxLoanOcorMaxLoanRm. It also refuses a loan that would overflow where the money goes: the wallet when the bank pays out to the wallet, or the account’s OC balance otherwise.- A loan taken in the other currency can’t be repaid early.
TakeLoanaccepts either currency, but the game’s monthly invoices pay back a loan in the other currency. ExchangeQuotereturns -1 when the bank is missing, the amount is not above 0 or is above 16,000,000, or the result is not a real number. In the RM to OC direction, the credit you really get can be lower than the quote, because the game’s quote and its exchange check different supplies.Exchangeonly works while the player has that bank’s screen open, and only for banks that exchange money. It works for a bank whose mode isExchangeorBoth, and a bank inDepositorWithdrawOnlymode is refused. That mode check is the framework’s own rule, because the game’s screen does not check it. (it needsBankPanel.instance.activeBankto be that bank, likeDepositWithdraw.)Exchangereturnsfalsewithout moving money when it refuses. It refuses when a save is loading or being written, or when a handler is nested too deep.Exchangealso refuses an amount that is not 1 to 16,000,000, or that is above the wallet or the source supply. The credit must be at least 1, because the game cuts the credit to whole units. The worst-case credit must fit the game’s storage limit.- An OC to RM exchange is also refused when the bank’s OC supply would pass
int.MaxValue. An RM to OC exchange is refused when the bank’s OC supply is below its daily OC spending, because the game would then credit zero OC for the RM it takes. - You can freeze and unfreeze a bank account, and read whether it is frozen.
FreezeandUnfreezechange the same flag the game uses, through the game’s own setter, so anything the game hangs off it still runs.IsFrozenreads the live account record. - A frozen account is blocked where the game blocks it: the bank screen refuses deposits and withdrawals and shows
Account frozen, and the game skips the account’s credit interest. Debt interest still runs, and the game’s own loan screen is not blocked, because it never checks the flag. - The flag is saved with the account, so an account your mod freezes stays frozen after your mod is removed. The player’s way out is the legal services unfreeze action. Treat freezing as something you offer to undo, not something you leave behind.
- The bank screen reads the flag when it refreshes rather than watching it, so a freeze made while the screen is open shows at the next refresh.
- While an account is frozen, the game’s scam letter handler adds a second account record with the same bank name (
Economy/FakeInvoiceReceiver.cs:106and its else branch).
Online banking helpers
These work from any scene and need no bank object. They are for things like paying from the account without standing at a bank. Every one that changes something gives back a BankResult, which says what happened.
Banking the helpers, usable from any scene
Accounts() lists the accounts, and Charge, Refund, PayInvoiceFromAccount, RepayLoanFromAccount and WithdrawByPost move money through them. HasDebitCard() tells you whether the player has the debit card.
public static class Banking
{
public static List<AccountSummary> Accounts();
public static BankAccount GetByName(string bankName);
public static bool HasDebitCard();
public static BankResult Charge(string bankName, MoneyCurrency currency, float amount);
public static BankResult Refund(string bankName, MoneyCurrency currency, float amount);
public static BankResult PayInvoiceFromAccount(string bankName, int invoiceId);
public static BankResult RepayLoanFromAccount(string bankName, MoneyCurrency currency, int amount);
public static BankResult WithdrawByPost(string bankName, MoneyCurrency currency, float amount, float fee);
}
| Method | Returns | What it does |
|---|---|---|
Accounts() | List<AccountSummary> | Every bank account the game lists. |
GetByName(bankName) | BankAccount | The account at that bank. null when there is none. |
HasDebitCard() | bool | true when the player has the debit card. |
Charge(bankName, currency, amount) | BankResult | Takes money from the account only. |
Refund(bankName, currency, amount) | BankResult | Puts money into the account only. |
PayInvoiceFromAccount(bankName, invoiceId) | BankResult | Pays an invoice from the account. |
RepayLoanFromAccount(bankName, currency, amount) | BankResult | Repays a loan from the account. |
WithdrawByPost(bankName, currency, amount, fee) | BankResult | Sends cash by post. |
BankResult what a helper call returned
Each helper that changes money returns one of these. Ok means it worked, and every other value says why it did not.
public enum BankResult
{
Ok,
NoAccount,
Frozen,
Insufficient,
Busy,
Unavailable,
Refused,
NotFound,
Failed
}
| Value | Meaning |
|---|---|
Ok | It worked. |
NoAccount | The player has no account at that bank.Open one first with OpenAccount, or pick a bank where the player already banks. Nothing changed. |
Frozen | The account is frozen.A frozen account refuses deposits and withdrawals, and the game skips its credit interest while debt interest still runs. The player clears a freeze through the legal services unfreeze action. |
Insufficient | The balance does not cover it.Read the balance first and ask for less. No money moved. |
Busy | Not now.A save is loading or being written, or money handlers are nested too deep. Try again after the save finishes, and do not bank from inside another money handler. Nothing changed. |
Unavailable | The bank or the money system was not ready.Call once a save has loaded. If an error happened part way, the change was undone and the framework logged it once. |
Refused | The request itself was not valid.Check the amount and the currency against the rules on this page. Nothing changed. |
NotFound | The invoice does not exist.List the invoices first and use an id from that list. |
Failed | Something went wrong.For example the stored balance is not a number. The framework logs it once and leaves the account alone. |
AccountSummary a short view of one account
Banking.Accounts() gives you one of these for each account. For the loan fields, use GetByName, which returns a full BankAccount.
public sealed class AccountSummary
{
public string BankName { get; }
public int Oc { get; }
public float Rm { get; }
public bool Frozen { get; }
}
| Property | Type | What it means |
|---|---|---|
BankName | string | The bank’s name. |
Oc | int | The OC in the account. |
Rm | float | The RM in the account. |
Frozen | bool | true when the account is frozen. |
Things to know
- Every helper checks first and changes nothing if a check fails. The checks are, in order: that no save is loading and handlers are not nested too deep (
Busy), that the currency is a realMoneyCurrency(Refusedif not), that the amount is valid, that the account exists (NoAccount), that it is not frozen (Frozen, andRefunddoes not check this), and that the balance covers it (Insufficient). - An amount is valid when it is a real number above 0 and not above 16,000,000, and it is not 0 after RM is rounded to 0.1 or OC drops its fraction (
Refusedif not). A charge changes its account once. Refundalways lands, even on a frozen account. The money can be used once the account is unfrozen. It runs the other checks, and adds the amount instead of taking it. It returnsRefusedwhen the OC balance would passint.MaxValue.- RM amounts are rounded to 0.1 before any charge. The wallet can’t hold anything finer, so an amount with two decimals would lose or gain money when the letter arrives.
- A charge works when the balance is within 0.0001 of the amount, and the balance is then written back rounded to 2 decimals and never below 0. A balance that is not a number gives
Failedand nothing is written. GetByNameonly reads the account.BankIdis empty (""),LoanCurrencyis RM (the game’s default) and the loan limits are 0, because those need the bank to be loaded in the scene. UseGetorAllwhen the bank is loaded.PayInvoiceFromAccountpays in the invoice’s own currency. It givesNotFoundwhen the invoice does not exist, andRefusedwhen it has expired or its amount can’t be paid. (it follows the order of the game’sInvoiceController.PayInvoice: the debit, the invoice’s paid callback, then removal of the invoice. The fake-invoice achievement runs after removal in its own try, so a failure there is only logged. Then the ledger is reconciled andInvoices.Paidis raised. On a failed callback the ledger is reconciled again, so the refunded debit leaves no phantom entry.)RepayLoanFromAccounttakes whole units. It is cut down to the debt, which can have a fraction, so it never overpays, and an RM debt under 1 is cleared in full. It returnsInsufficientwhen the balance does not cover that amount, andRefusedwhen nothing is owed.WithdrawByPosttakesamountfrom the account and postsamount - feeas a letter. The game’s mail delay applies. The fee is taken from the account and goes to nobody. (the letter is sent throughMail.SendMoney.)Chargeonly changes the account. The bank’s money supply is not changed, as in the game’s own purchases.HasDebitCard()istruewhen the player has the debit card. (it checks for the task itemdebit_card.)
Limits
- If the invoice’s “paid” step throws an error before the invoice is removed, the account gets back what it had, the invoice stays open, and the result is
Unavailable. If that step changed other things before it threw, those changes stay. - If the letter can’t be queued,
WithdrawByPostputs the money back and the result isUnavailable. IfMail.SendMoneysays it failed after the letter was queued, the withdrawal stands, the result isOk, and one error goes to the log.