diff --git a/changelog.d/2926.breaking.rst b/changelog.d/2926.breaking.rst new file mode 100644 index 000000000..ea6ed30a8 --- /dev/null +++ b/changelog.d/2926.breaking.rst @@ -0,0 +1 @@ +Removed :cons:`bank.MAX_BALANCE`, use :meth:`bank.get_max_balance()` from now. \ No newline at end of file diff --git a/changelog.d/2926.feature.rst b/changelog.d/2926.feature.rst new file mode 100644 index 000000000..c68f48d5e --- /dev/null +++ b/changelog.d/2926.feature.rst @@ -0,0 +1 @@ +`[p]bankset maxbal` can be used to set the maximum bank balance. diff --git a/redbot/cogs/bank/bank.py b/redbot/cogs/bank/bank.py index 70f06a344..3f8f8a444 100644 --- a/redbot/cogs/bank/bank.py +++ b/redbot/cogs/bank/bank.py @@ -86,11 +86,12 @@ class Bank(commands.Cog): settings = _( "Bank settings:\n\nBank name: {bank_name}\nCurrency: {currency_name}\n" - "Default balance: {default_balance}" + "Default balance: {default_balance}\nMaximum allowed balance: {maximum_bal}" ).format( bank_name=bank_name, currency_name=currency_name, default_balance=humanize_number(default_balance), + maximum_bal=humanize_number(await bank.get_max_balance(ctx.guild)), ) await ctx.send(box(settings)) @@ -130,4 +131,21 @@ class Bank(commands.Cog): await bank.set_currency_name(name, ctx.guild) await ctx.send(_("Currency name has been set to: {name}").format(name=name)) + @bankset.command(name="maxbal") + @check_global_setting_guildowner() + async def bankset_maxbal(self, ctx: commands.Context, *, amount: int): + """Set the maximum balance a user can get.""" + try: + await bank.set_max_balance(amount, ctx.guild) + except ValueError: + # noinspection PyProtectedMember + return await ctx.send( + _("Amount must be greater than zero and less than {max}.").format( + max=humanize_number(bank._MAX_BALANCE) + ) + ) + await ctx.send( + _("Maximum balance has been set to: {amount}").format(amount=humanize_number(amount)) + ) + # ENDSECTION diff --git a/redbot/cogs/economy/economy.py b/redbot/cogs/economy/economy.py index 1f952140d..f0c27a8f6 100644 --- a/redbot/cogs/economy/economy.py +++ b/redbot/cogs/economy/economy.py @@ -1,7 +1,7 @@ import calendar import logging import random -from collections import defaultdict, deque +from collections import defaultdict, deque, namedtuple from enum import Enum from typing import cast, Iterable @@ -20,6 +20,7 @@ T_ = Translator("Economy", __file__) logger = logging.getLogger("red.economy") NUM_ENC = "\N{COMBINING ENCLOSING KEYCAP}" +MOCK_MEMBER = namedtuple("Member", "id guild") class SMReel(Enum): @@ -159,7 +160,10 @@ class Economy(commands.Cog): bal = await bank.get_balance(user) currency = await bank.get_currency_name(ctx.guild) - + max_bal = await bank.get_max_balance(ctx.guild) + if bal > max_bal: + bal = max_bal + await bank.set_balance(user, bal) await ctx.send( _("{user}'s balance is {num} {currency}").format( user=user.display_name, num=humanize_number(bal), currency=currency @@ -363,6 +367,7 @@ class Economy(commands.Cog): """ guild = ctx.guild author = ctx.author + max_bal = await bank.get_max_balance(ctx.guild) if top < 1: top = 10 if await bank.is_global() and show_global: @@ -372,6 +377,9 @@ class Economy(commands.Cog): bank_sorted = await bank.get_leaderboard(positions=top, guild=guild) try: bal_len = len(humanize_number(bank_sorted[0][1]["balance"])) + bal_len_max = len(humanize_number(max_bal)) + if bal_len > bal_len_max: + bal_len = bal_len_max # first user is the largest we'll see except IndexError: return await ctx.send(_("There are no accounts in the bank.")) @@ -394,8 +402,12 @@ class Economy(commands.Cog): if await ctx.bot.is_owner(ctx.author): user_id = f"({str(acc[0])})" name = f"{acc[1]['name']} {user_id}" - balance = humanize_number(acc[1]["balance"]) + balance = acc[1]["balance"] + if balance > max_bal: + balance = max_bal + await bank.set_balance(MOCK_MEMBER(acc[0], guild), balance) + balance = humanize_number(balance) if acc[0] != author.id: temp_msg += ( f"{f'{humanize_number(pos)}.': <{pound_len+2}} " @@ -560,7 +572,8 @@ class Economy(commands.Cog): "Slot cooldown: {slot_time}\n" "Payday amount: {payday_amount}\n" "Payday cooldown: {payday_time}\n" - "Amount given at account registration: {register_amount}" + "Amount given at account registration: {register_amount}\n" + "Maximum allowed balance: {maximum_bal}" ).format( slot_min=humanize_number(await conf.SLOT_MIN()), slot_max=humanize_number(await conf.SLOT_MAX()), @@ -568,6 +581,7 @@ class Economy(commands.Cog): payday_time=humanize_number(await conf.PAYDAY_TIME()), payday_amount=humanize_number(await conf.PAYDAY_CREDITS()), register_amount=humanize_number(await bank.get_default_balance(guild)), + maximum_bal=humanize_number(await bank.get_max_balance(guild)), ) ) ) @@ -639,9 +653,13 @@ class Economy(commands.Cog): async def paydayamount(self, ctx: commands.Context, creds: int): """Set the amount earned each payday.""" guild = ctx.guild - if creds <= 0 or creds > bank.MAX_BALANCE: - await ctx.send(_("Har har so funny.")) - return + max_balance = await bank.get_max_balance(ctx.guild) + if creds <= 0 or creds > max_balance: + return await ctx.send( + _("Amount must be greater than zero and less than {maxbal}.").format( + maxbal=humanize_number(max_balance) + ) + ) credits_name = await bank.get_currency_name(guild) if await bank.is_global(): await self.config.PAYDAY_CREDITS.set(creds) @@ -657,9 +675,13 @@ class Economy(commands.Cog): async def rolepaydayamount(self, ctx: commands.Context, role: discord.Role, creds: int): """Set the amount earned each payday for a role.""" guild = ctx.guild - if creds <= 0 or creds > bank.MAX_BALANCE: - await ctx.send(_("Har har so funny.")) - return + max_balance = await bank.get_max_balance(ctx.guild) + if creds <= 0 or creds > max_balance: + return await ctx.send( + _("Amount must be greater than zero and less than {maxbal}.").format( + maxbal=humanize_number(max_balance) + ) + ) credits_name = await bank.get_currency_name(guild) if await bank.is_global(): await ctx.send(_("The bank must be per-server for per-role paydays to work.")) @@ -676,10 +698,16 @@ class Economy(commands.Cog): async def registeramount(self, ctx: commands.Context, creds: int): """Set the initial balance for new bank accounts.""" guild = ctx.guild - if creds < 0: - creds = 0 + max_balance = await bank.get_max_balance(ctx.guild) credits_name = await bank.get_currency_name(guild) - await bank.set_default_balance(creds, guild) + try: + await bank.set_default_balance(creds, guild) + except ValueError: + return await ctx.send( + _("Amount must be greater than or equal to zero and less than {maxbal}.").format( + maxbal=humanize_number(max_balance) + ) + ) await ctx.send( _("Registering an account will now give {num} {currency}.").format( num=humanize_number(creds), currency=credits_name diff --git a/redbot/core/bank.py b/redbot/core/bank.py index de7060ea8..599956a29 100644 --- a/redbot/core/bank.py +++ b/redbot/core/bank.py @@ -12,7 +12,6 @@ from .i18n import Translator _ = Translator("Bank API", __file__) __all__ = [ - "MAX_BALANCE", "Account", "get_balance", "set_balance", @@ -30,20 +29,28 @@ __all__ = [ "set_currency_name", "get_default_balance", "set_default_balance", + "get_max_balance", + "set_max_balance", "cost", "AbortPurchase", ] -MAX_BALANCE = 2 ** 63 - 1 +_MAX_BALANCE = 2 ** 63 - 1 _DEFAULT_GLOBAL = { "is_global": False, "bank_name": "Twentysix bank", "currency": "credits", "default_balance": 100, + "max_balance": _MAX_BALANCE, } -_DEFAULT_GUILD = {"bank_name": "Twentysix bank", "currency": "credits", "default_balance": 100} +_DEFAULT_GUILD = { + "bank_name": "Twentysix bank", + "currency": "credits", + "default_balance": 100, + "max_balance": _MAX_BALANCE, +} _DEFAULT_MEMBER = {"name": "", "balance": 0, "created_at": 0} @@ -186,10 +193,11 @@ async def set_balance(member: discord.Member, amount: int) -> int: """ if amount < 0: raise ValueError("Not allowed to have negative balance.") - if amount > MAX_BALANCE: + max_bal = await get_max_balance(member.guild) + if amount > max_bal: currency = await get_currency_name(member.guild) raise errors.BalanceTooHigh( - user=member.display_name, max_balance=MAX_BALANCE, currency_name=currency + user=member.display_name, max_balance=max_bal, currency_name=currency ) if await is_global(): group = _conf.user(member) @@ -329,10 +337,12 @@ async def transfer_credits(from_: discord.Member, to: discord.Member, amount: in ) ) - if await get_balance(to) + amount > MAX_BALANCE: + max_bal = await get_max_balance(to.guild) + + if await get_balance(to) + amount > max_bal: currency = await get_currency_name(to.guild) raise errors.BalanceTooHigh( - user=to.display_name, max_balance=MAX_BALANCE, currency_name=currency + user=to.display_name, max_balance=max_bal, currency_name=currency ) await withdraw_credits(from_, amount) @@ -635,6 +645,75 @@ async def set_currency_name(name: str, guild: discord.Guild = None) -> str: return name +async def get_max_balance(guild: discord.Guild = None) -> int: + """Get the max balance for the bank. + + Parameters + ---------- + guild : `discord.Guild`, optional + The guild to get the max balance for (required if bank is + guild-specific). + + Returns + ------- + int + The maximum allowed balance. + + Raises + ------ + RuntimeError + If the bank is guild-specific and guild was not provided. + + """ + if await is_global(): + return await _conf.max_balance() + elif guild is not None: + return await _conf.guild(guild).max_balance() + else: + raise RuntimeError("Guild must be provided.") + + +async def set_max_balance(amount: int, guild: discord.Guild = None) -> int: + """Set the maximum balance for the bank. + + Parameters + ---------- + amount : int + The new maximum balance. + guild : `discord.Guild`, optional + The guild to set the max balance for (required if bank is + guild-specific). + + Returns + ------- + int + The new maximum balance. + + Raises + ------ + RuntimeError + If the bank is guild-specific and guild was not provided. + ValueError + If the amount is less than 0 or higher than 2 ** 63 - 1. + """ + if not (0 < amount <= _MAX_BALANCE): + raise ValueError( + "Amount must be greater than zero and less than {max}.".format( + max=humanize_number(_MAX_BALANCE, override_locale="en_US") + ) + ) + + if await is_global(): + await _conf.max_balance.set(amount) + elif guild is not None: + await _conf.guild(guild).max_balance.set(amount) + else: + raise RuntimeError( + "Guild must be provided if setting the maximum balance of a guild-specific bank." + ) + return amount + + async def get_default_balance(guild: discord.Guild = None) -> int: """Get the current default balance amount. @@ -684,12 +763,18 @@ async def set_default_balance(amount: int, guild: discord.Guild = None) -> int: RuntimeError If the bank is guild-specific and guild was not provided. ValueError - If the amount is invalid. + If the amount is less than 0 or higher than the max allowed balance. """ amount = int(amount) - if amount < 0: - raise ValueError("Amount must be greater than zero.") + max_bal = await get_max_balance(guild) + + if not (0 < amount <= max_bal): + raise ValueError( + "Amount must be greater than zero and less than {max}.".format( + max=humanize_number(max_bal, override_locale="en_US") + ) + ) if await is_global(): await _conf.default_balance.set(amount) @@ -714,7 +799,7 @@ def cost(amount: int): You can intentionally refund by raising `AbortPurchase` (this error will be consumed and not show to users) - Other exceptions will propogate and will be handled by Red's (and/or + Other exceptions will propagate and will be handled by Red's (and/or any other configured) error handling. """ if not isinstance(amount, int) or amount < 0: diff --git a/redbot/core/errors.py b/redbot/core/errors.py index 66f5f347b..5396ef240 100644 --- a/redbot/core/errors.py +++ b/redbot/core/errors.py @@ -46,7 +46,7 @@ class BalanceTooHigh(BankError, OverflowError): self.currency_name = currency_name def __str__(self) -> str: - return _("{user}'s balance cannot rise above max {currency}.").format( + return _("{user}'s balance cannot rise above {max} {currency}.").format( user=self.user, max=humanize_number(self.max_balance), currency=self.currency_name )