diff --git a/bootstrap.py b/bootstrap.py index 2e3de87..f8727b0 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -100,7 +100,7 @@ try: else: db.shared = db else: - raise Exception("Unknown database type '%s'" % config.database.type) + raise Exception(f"Unknown database type '{config.database.type}'") # run db upgrade scripts run_upgrades() diff --git a/conf/logging_settings.py b/conf/logging_settings.py index 8448be4..6172c12 100644 --- a/conf/logging_settings.py +++ b/conf/logging_settings.py @@ -18,8 +18,13 @@ name = "bot" # So we're saving the logs into ./logs/##bot_name##/ if len(sys.argv) > 1: name = sys.argv[1] + try: - os.mkdir(f"./logs/{name}") + os.mkdir(f"./logs", mode=4600) +except FileExistsError as error: + pass +try: + os.mkdir(f"./logs/{name}", mode=4600) except FileExistsError as error: pass diff --git a/core/logger.py b/core/logger.py index 0718cb6..e890e8d 100644 --- a/core/logger.py +++ b/core/logger.py @@ -22,12 +22,12 @@ class Logger: def log_chat(self, conn_id, channel, sender, msg): if sender: - self.info("(%s) [%s] %s: %s" % (conn_id, channel, sender, self.format_chat_message(msg))) + self.info(f"({conn_id}) [{channel}] {sender}: {self.format_chat_message(msg)}") else: - self.info("(%s) [%s] %s" % (conn_id, channel, self.format_chat_message(msg))) + self.info(f"({conn_id}) [{channel}] {self.format_chat_message(msg)}") def log_tell(self, conn_id, direction, sender, msg): - self.info("(%s) %s %s: %s" % (conn_id, direction.capitalize(), sender, self.format_chat_message(msg))) + self.info(f"({conn_id}) {direction.capitalize()} {sender}: {self.format_chat_message(msg)}") def format_chat_message(self, msg): msg = re.sub(r"", "[link]", msg, flags=re.UNICODE | re.DOTALL) diff --git a/core/message_hub_service.py b/core/message_hub_service.py index 5135417..65b0209 100644 --- a/core/message_hub_service.py +++ b/core/message_hub_service.py @@ -47,16 +47,15 @@ class MessageHubService: invalid_sources = [] if len(inspect.signature(callback).parameters) != 1: raise Exception( - "Incorrect number of arguments for handler '%s.%s()'" % (callback.__module__, callback.__name__)) + f"Incorrect number of arguments for handler '{callback.__module__}.{callback.__name__}()'") if destination in self.hub: - raise Exception("Message hub destination '%s' already subscribed" % destination) + raise Exception(f"Message hub destination '{destination}' already subscribed") for source in default_sources: if source not in self.sources: self.logger.warning( - "Could not subscribe destination '%s' to source '%s' because source does not exist" % ( - destination, source)) + f"Could not subscribe destination '{destination}' to source '{source}' because source does not exist") self.hub[destination] = (DictObject({"name": destination, "callback": callback, @@ -85,11 +84,11 @@ class MessageHubService: def subscribe_to_source(self, destination, source): if source not in self.sources: - raise Exception("Message hub source '%s' doeselecs not exist" % source) + raise Exception(f"Message hub source '{source}' does not exist") obj = self.hub.get(destination, None) if not obj: - raise Exception("Message hub destination '%s' does not exist" % destination) + raise Exception(f"Message hub destination '{destination}' does not exist") if source not in obj.sources: self.db.exec("DELETE FROM message_hub_subscriptions WHERE destination = ?", [destination]) @@ -105,7 +104,7 @@ class MessageHubService: obj = self.hub.get(destination, None) if not obj: - raise Exception("Message hub destination '%s' does not exist" % destination) + raise Exception(f"Message hub destination '{destination}' does not exist") if source in obj.sources: self.db.exec("DELETE FROM message_hub_subscriptions WHERE destination = ?", [destination]) diff --git a/core/public_channel_service.py b/core/public_channel_service.py index e0126c7..35f0554 100644 --- a/core/public_channel_service.py +++ b/core/public_channel_service.py @@ -71,8 +71,8 @@ class PublicChannelService(BaseModule): else: data = self.event_service.db.query_single('SELECT org_name from all_orgs where org_id=?', [self.org_id]) self.org_name = data.org_name if data else 'Unknown Org' - self.logger.info("Org Id: %d" % self.org_id) - self.logger.info("Org Name: %s" % self.org_name) + self.logger.info(f"Org Id: {self.org_id:d}") + self.logger.info(f"Org Name: {self.org_name}") def remove(self, conn: Conn, packet: server_packets.PublicChannelLeft): if conn.id != "main": diff --git a/core/setting_service.py b/core/setting_service.py index 85a183c..f8adfb4 100644 --- a/core/setting_service.py +++ b/core/setting_service.py @@ -52,15 +52,14 @@ class SettingService: row = self.db.query_single("SELECT name, value, description FROM setting WHERE name = ?", [name]) if row is None: - self.logger.debug("Adding setting '%s'" % name) + self.logger.debug(f"Adding setting '{name}'") self.db.exec("INSERT INTO setting (name, value, description, module, verified) VALUES (?, ?, ?, ?, ?)", [name, "", description, module, 1]) - print(2, name) # verify default value is a valid value, and is formatted appropriately setting.set_value(value) else: - self.logger.debug("Updating setting '%s'" % name) + self.logger.debug(f"Updating setting '{name}'") self.db.exec("UPDATE setting SET description = ?, verified = ?, module = ? WHERE name = ?", [description, 1, module, name]) self.settings[name] = setting diff --git a/core/setting_types.py b/core/setting_types.py index 63331df..4ab80d7 100644 --- a/core/setting_types.py +++ b/core/setting_types.py @@ -105,7 +105,7 @@ class DictionarySettingType(SettingType): return value def get_display_value(self): - return "%s" % (self.get_value() or "<empty>") + return f"{self.get_value() or '<empty>'}" def get_display(self): return """This setting is controlled by the bot and cannot be set manually.""" @@ -121,6 +121,7 @@ class HiddenSettingType(TextSettingType): else: return "<empty>" + @property def get_display(self): text = Registry.get_instance("text") diff --git a/core/text.py b/core/text.py index e68ba95..2f64690 100644 --- a/core/text.py +++ b/core/text.py @@ -60,19 +60,19 @@ class Text: def make_chatcmd(self, name, msg, style=""): msg = msg.strip() msg = msg.replace("'", "'") - return "%s" % (style, msg, name) + return f"{name}" def make_tellcmd(self, name, msg, style="", char=""): return self.make_chatcmd(name, f"/tell {char} {msg}", style) def make_charlink(self, char, style=""): - return "%s" % (style, char, char) + return f"{char}" def make_item(self, low_id, high_id, ql, name): - return "%s" % (low_id, high_id, ql, name) + return f"{name}" def make_image(self, image_id, image_db="rdb"): - return "" % (image_db, image_id) + return f"" def format_item(self, item, ql=None, with_icon=True): if not item: @@ -89,8 +89,8 @@ class Text: def generate_item(self, item, ql, synonym=None): if synonym: - return {"icon_%s" % synonym: self.make_item(item.lowid, item.highid, ql, self.make_image(item.icon)), - "text_%s" % synonym: self.make_item(item.lowid, item.highid, ql, item.name)} + return {f"icon_{synonym}": self.make_item(item.lowid, item.highid, ql, self.make_image(item.icon)), + f"text_{synonym}": self.make_item(item.lowid, item.highid, ql, item.name)} else: return {"icon": self.make_item(item.lowid, item.highid, ql, self.make_image(item.icon)), "text": self.make_item(item.lowid, item.highid, ql, item.name)} @@ -120,10 +120,10 @@ class Text: count = len(selected) pages = "" if page > 1: - pages += "Pages: " + self.make_tellcmd("«« Page %d" % (page - 1), f'{cmd} --page={page - 1}') + pages += "Pages: " + self.make_tellcmd(f"«« Page {page - 1:d}", f'{cmd} --page={page - 1}') if offset + page_size < len(data): pages += f" Page {page}/{math.ceil(len(data) / page_size)}" - pages += " " + self.make_tellcmd("Page %d »»" % (page + 1), f'{cmd} --page={page + 1}') + pages += " " + self.make_tellcmd(f"Page {page + 1:d} »»", f'{cmd} --page={page + 1}') pages += "\n" if count == 0: return no_data_msg diff --git a/core/tyrbot.py b/core/tyrbot.py index 51aee23..4707a31 100644 --- a/core/tyrbot.py +++ b/core/tyrbot.py @@ -232,12 +232,12 @@ class Tyrbot: if not self.iterate(1): time_waited += 1 - self.logger.info("Login complete (%fs)" % (time.time() - start)) + self.logger.info(f"Login complete ({time.time() - start:.2f}s)") start = time.time() self.event_service.fire_event("connect", None) self.event_service.run_timer_events_at_startup() - self.logger.info("Connect events finished (%fs)" % (time.time() - start)) + self.logger.info(f"Connect events finished ({time.time() - start:.2f}s)") self.ready = True self.command_service.ignore = [] timestamp = int(time.time()) @@ -275,7 +275,7 @@ class Tyrbot: if len(inspect.signature(handler).parameters) != 2: raise Exception( - "Incorrect number of arguments for handler '%s.%s()'" % (handler.__module__, handler.__name__)) + f"Incorrect number of arguments for handler '{handler.__module__}.{handler.__name__}()'") handlers = self.packet_handlers.get(packet_id, []) handlers.append(DictObject({"priority": priority, "handler": handler})) diff --git a/modules/core/accounting/account_controller.py b/modules/core/accounting/account_controller.py index d49fa73..4f7d186 100644 --- a/modules/core/accounting/account_controller.py +++ b/modules/core/accounting/account_controller.py @@ -62,7 +62,7 @@ class AccountController: msg = sorted(entries, key=lambda k: k[0]) for _, mess in msg: out += mess - self.bot.send_mass_message(request.sender.char_id, ChatBlob("All accounts", out)) + self.bot.send_mass_message(request.sender.char_id, ChatBlob(f"All accounts ({len(entries)})", out)) @command(command="points", params=[], access_level="member", description="View your points") diff --git a/modules/core/accounting/alts_controller.py b/modules/core/accounting/alts_controller.py index 71f0acd..dcfde5d 100644 --- a/modules/core/accounting/alts_controller.py +++ b/modules/core/accounting/alts_controller.py @@ -179,9 +179,10 @@ class AltsController: blob += f"{self.util.get_prof_icon(alt.profession)} " \ f"{self.text.zfill(alt.level, 220)}:" \ f"{self.text.zfill(alt.ai_level, 30)} " \ - f":: <{alt.faction.lower()}>{name} " \ - f"[<{alt.faction.lower()}>{alt.org_name} - " \ - f"{alt.org_rank_id + 1}]" + f":: <{alt.faction.lower()}>{name}" + if alt.org_name != "": + blob += f" [<{alt.faction.lower()}>{alt.org_name} - " \ + f"{alt.org_rank_id + 1}]" if self.buddy_service.is_online(alt.char_id): blob += " [Online]" blob += "\n" diff --git a/modules/core/accounting/preference_controller.py b/modules/core/accounting/preference_controller.py index af36472..8f53a0b 100644 --- a/modules/core/accounting/preference_controller.py +++ b/modules/core/accounting/preference_controller.py @@ -8,6 +8,7 @@ from core.logger import Logger from core.lookup.pork_service import PorkService from core.setting_service import SettingService from core.text import Text +from core.translation_service import TranslationService from core.tyrbot import Tyrbot from core.util import Util from modules.core.accounting.services.account_service import AccountService @@ -31,6 +32,8 @@ class PreferenceController: self.setting_service: SettingService = registry.get_instance("setting_service") self.discord: DiscordController = registry.get_instance("discord_controller") self.job_scheduler = registry.get_instance("job_scheduler") + self.ts: TranslationService = registry.get_instance("translation_service") + self.getresp = self.ts.get_response def start(self): self.command_alias_service.add_alias("prefs", "preferences") @@ -68,6 +71,15 @@ class PreferenceController: return f"{main.name}'s {pref.capitalize()} " \ f"preference has been set to {value}." + @command(command="prefadmin", params=[Character('character')], description="View the preferences of a player", access_level="moderator") + def show_prefadmin(self, request, char): + if not char.char_id: + return self.getresp("global", "char_not_found", {"char": char.name}) + account = self.account_service.get_account(char.char_id) + if not account: + return f"{char.name} has no preferences you could manage...." + return ChatBlob(f"{account.name}'s Preferences", self.get_pref_view_full(account, account)) + def get_prefs(self, char_id): return self.account_service.get_account(char_id) @@ -82,25 +94,31 @@ class PreferenceController: self.db.exec(f"UPDATE account set {pref}={value} where char_id=(SELECT main from account where char_id=?)", [char_id]) - def get_pref_view_small(self, prefs): + def get_pref_view_small(self, prefs, owner=None): return f"\n" \ - f" └ [{self._make_cmd('news', prefs.news_spam)}] News - " \ - f"Autoinvite [{self._make_cmd('autoinvite', prefs.auto_invite)}], \n" \ - f" └ [{self._make_cmd('raidinvite', prefs.raid_invite)}] Raidinvite - " \ - f"Massmessage [{self._make_cmd('raidspam', prefs.raid_invite)}], \n" \ - f" └ [{self._make_cmd('subtilespam', prefs.subtile_spam)}] Subtilespam" + f" └ [{self._make_cmd('news', prefs.news_spam, owner)}] News - " \ + f"Autoinvite [{self._make_cmd('autoinvite', prefs.auto_invite, owner)}], \n" \ + f" └ [{self._make_cmd('raidinvite', prefs.raid_invite, owner)}] Raidinvite - " \ + f"Massmessage [{self._make_cmd('raidspam', prefs.raid_invite, owner)}], \n" \ + f" └ [{self._make_cmd('subtilespam', prefs.subtile_spam, owner)}] Subtilespam" - def get_pref_view_full(self, prefs): + def get_pref_view_full(self, prefs, owner=None): return f"\n" \ - f"└ [ {self._make_cmd('news', prefs.news_spam)} ] Do you want your News on Logon? \n" \ - f"└ [ {self._make_cmd('autoinvite', prefs.auto_invite)} ] Do you want to receive autoinvites? \n" \ - f"└ [ {self._make_cmd('raidinvite', prefs.raid_invite)} ] Do you want to receive raidinvites? \n" \ - f"└ [ {self._make_cmd('raidspam', prefs.raid_invite)} ] Do you want to receive massmessages? \n" \ - f"└ [ {self._make_cmd('subtilespam', prefs.subtile_spam)} ] Do you want a subtile invite spam? \n" + f"└ [ {self._make_cmd('news', prefs.news_spam, owner)} ] Do you want your News on Logon? \n" \ + f"└ [ {self._make_cmd('autoinvite', prefs.auto_invite, owner)} ] Do you want to receive autoinvites? \n" \ + f"└ [ {self._make_cmd('raidinvite', prefs.raid_invite, owner)} ] Do you want to receive raidinvites? \n" \ + f"└ [ {self._make_cmd('raidspam', prefs.raid_invite, owner)} ] Do you want to receive massmessages? \n" \ + f"└ [ {self._make_cmd('subtilespam', prefs.subtile_spam, owner)} ] Do you want a subtile invite spam? \n" - def _make_cmd(self, pref, value): + def _make_cmd(self, pref, value, owner=None): # --- - # [ ON | OFF ] + # [ YES | NO ] + if owner: + if value == 1: + return f"YES | {self.text.make_chatcmd('NO', f'/tell prefadmin set {owner.name} {pref} off')}" + else: + return f"{self.text.make_chatcmd('YES', f'/tell prefadmin set {owner.name} {pref} on')} | NO" + if value == 1: return f"YES | {self.text.make_chatcmd('NO', f'/tell preferences set {pref} off')}" else: diff --git a/modules/core/accounting/services/access_service.py b/modules/core/accounting/services/access_service.py index d8048a3..5ec078f 100644 --- a/modules/core/accounting/services/access_service.py +++ b/modules/core/accounting/services/access_service.py @@ -41,6 +41,8 @@ class AccessService: def get_access_level(self, char_id) -> dict: account = self.account_service.get_main(char_id) + # For performance's sake, we'll assume that the main has the highest rank; + # if it only has the rank "all" (no rank), we'll also check the toon using the command. if account: al = self.get_single_access_level(account.char_id) if al["label"] == "all": @@ -48,21 +50,6 @@ class AccessService: return al else: return self.get_single_access_level(char_id) - # access_level1 = self.get_single_access_level(char_id) - # - # alts = self.account_service.get_alts(char_id) - # if not alts: - # return access_level1 - # - # main = alts[0] - # if main.char_id == char_id: - # return access_level1 - # else: - # access_level2 = self.get_single_access_level(main.char_id) - # if access_level1["level"] < access_level2["level"]: - # return access_level1 - # else: - # return access_level2 def compare_access_levels(self, access_level1, access_level2) -> int: """ diff --git a/modules/core/accounting/services/account_service.py b/modules/core/accounting/services/account_service.py index ade92b4..3319808 100644 --- a/modules/core/accounting/services/account_service.py +++ b/modules/core/accounting/services/account_service.py @@ -147,13 +147,18 @@ class AccountService: acc_cache = {} def get_account(self, char_id) -> DictObject: - + # The way the caching is handled does not come without side effects: + # For example, after purging an account, + # you could still update it (i.e. add points, alts, ...) + # which would result in log entries, + # without an associated account if char_id not in self.acc_cache.keys(): out = self.db.query_single( "SELECT a.*, p.* from account a left join player p on a.char_id = p.char_id where " "a.char_id=(SELECT main from account where char_id=?) " "and a.char_id not in (SELECT char_id from org_bots)", [char_id]) or DictObject({}) + self.acc_cache[char_id] = out self.bot.job_scheduler.delayed_job(lambda x: self.acc_cache.pop(char_id), 5) else: @@ -419,7 +424,7 @@ class AccountService: f"LEFT JOIN (SELECT * FROM online WHERE bot=?) o ON a.char_id=o.char_id " f"WHERE a.char_id NOT IN (SELECT char_id from org_bots) " f"and a.disabled = 0 {'and o.char_id is not null' if online_only else ''} " - f"order by a.main, a.main=a.char_id desc, p.level desc, p.ai_level desc", + f"group by o.char_id order by a.main, a.main=a.char_id desc, p.level desc, p.ai_level desc", [self.bot.get_char_id()]) def get_by_group(self, group) -> List[DictObject]: diff --git a/modules/core/private_channel/private_channel_controller.py b/modules/core/private_channel/private_channel_controller.py index a0267b5..6d1e011 100644 --- a/modules/core/private_channel/private_channel_controller.py +++ b/modules/core/private_channel/private_channel_controller.py @@ -183,7 +183,7 @@ class PrivateChannelController: char_name = self.character_service.resolve_char_to_name(event_data.char_id) sender = DictObject({"char_id": event_data.char_id, "name": char_name}) char = self.text.make_charlink(char_name) - formatted_message = f"{self.PRIVATE_CHANNEL_PREFIX} {char}: {event_data.message}" + formatted_message = f"{self.PRIVATE_CHANNEL_PREFIX}{char}: {event_data.message}" self.message_hub_service.send_message(self.MESSAGE_SOURCE, sender, event_data.message, formatted_message) @event(event_type=PrivateChannelService.JOINED_PRIVATE_CHANNEL_EVENT, @@ -193,8 +193,8 @@ class PrivateChannelController: if main: info = "" if self.account_service.check_superadmin(main.char_id) else \ ":: Admin " if self.account_service.check_admin(main.char_id) else \ - ":: Moderator " if self.account_service.check_moderator(main.char_id) else \ - ":: Raidleader " if self.account_service.check_leader(main.char_id) else "" + ":: Moderator " if self.account_service.check_moderator(main.char_id) else \ + ":: Raidleader " if self.account_service.check_leader(main.char_id) else "" if main.char_id != event_data.char_id: info += f":: Alt of <{main.faction.lower()}>{main.name}" else: @@ -223,23 +223,23 @@ class PrivateChannelController: description="Relay commands from the private channel to the relay hub", is_hidden=True) def outgoing_private_channel_message_event(self, _, event_data): if isinstance(event_data.message, ChatBlob): - pages = self.text.paginate(ChatBlob(event_data.message.title, event_data.message.msg), + pages = self.text.paginate(event_data.message, self.setting_service.get("org_channel_max_page_length").get_value()) if len(pages) < 4: for page in pages: - message = "{priv} {message}".format(priv=self.PRIVATE_CHANNEL_PREFIX, message=page) + message = "{priv}{message}".format(priv=self.PRIVATE_CHANNEL_PREFIX, message=page) self.message_hub_service.send_message(self.MESSAGE_SOURCE, None, page, message) else: - message = "{priv} {message}".format(priv=self.PRIVATE_CHANNEL_PREFIX, message=event_data.message.title) + message = "{priv}{message}".format(priv=self.PRIVATE_CHANNEL_PREFIX, message=event_data.message.title) self.message_hub_service.send_message(self.MESSAGE_SOURCE, None, event_data.message.title, message) else: - message = "{priv} {message}".format(priv=self.PRIVATE_CHANNEL_PREFIX, message=event_data.message) + message = "{priv}{message}".format(priv=self.PRIVATE_CHANNEL_PREFIX, message=event_data.message) self.message_hub_service.send_message(self.MESSAGE_SOURCE, None, message, diff --git a/modules/orgbot/org/org_controller.py b/modules/orgbot/org/org_controller.py index 5b96c89..ca22e71 100644 --- a/modules/orgbot/org/org_controller.py +++ b/modules/orgbot/org/org_controller.py @@ -109,7 +109,7 @@ class OrgChannelController: is_hidden=True) def outgoing_org_message_event(self, _, event_data): if isinstance(event_data.message, ChatBlob): - pages = self.text.paginate(ChatBlob(event_data.message.title, event_data.message.msg), + pages = self.text.paginate(event_data.message, self.setting_service.get("org_channel_max_page_length").get_value()) if len(pages) < 4: for page in pages: diff --git a/modules/raidbot/tower/tower_service.py b/modules/raidbot/tower/tower_service.py index 1c56dee..6839fad 100644 --- a/modules/raidbot/tower/tower_service.py +++ b/modules/raidbot/tower/tower_service.py @@ -18,7 +18,8 @@ from modules.standard.helpbot.playfield_controller import PlayfieldController @instance() class TowerService(BaseModule): - # [{'org_name': "Dragons", 'hot': time.time() + 60 * 60}, {'org_name': "Most Wanted", 'hot': time.time() + 60 * 60}] + # For this Module to work properly you might need to + # contact the API host of your choice to whitelist your IP addresses. attack_hot = [] plant = [] @@ -112,6 +113,10 @@ class TowerService(BaseModule): # ?limit is a parameter not documented anywhere, but allows pulling the whole list # On other API implementations this parameter has no effect, # as the server always responds with the full list. + + # In the Future, this Event will get moved to an external process, + # which maintains the tower Cache for all connected bots, + # like it has been done with the Worldboss timers. from torpy.http.requests import TorRequests with TorRequests() as tor_request: with tor_request.get_session(1) as session: diff --git a/modules/standard/news/worldboss_controller.py b/modules/standard/news/worldboss_controller.py index 330bb70..a52fcf0 100644 --- a/modules/standard/news/worldboss_controller.py +++ b/modules/standard/news/worldboss_controller.py @@ -1,7 +1,5 @@ -import sys import time -from conf.config import BotConfig from core.command_alias_service import CommandAliasService from core.decorators import instance, command, event from core.dict_object import DictObject @@ -17,18 +15,18 @@ from modules.standard.datanet.ws_controller import WebsocketRelayController @instance() class WorldBossController: + # Timers are provided through an local websocket relay, which gets fed by an external API; + # If you intend to take advantage of this module, + # you **will** need to contact the API host of your choice to whitelist + # The IP Addresses with which you intend to access the API. + # example timer data: + # [{"name":"Tarasque","time": }, + # {"name":"Vizaresh","time": }] + timer_data = [] + alerts = [480 * 60, 360 * 60, 240 * 60, 120 * 60, 60 * 60, 60 * 15, 60 * 5, 60 * 3, 60 * 2, 60, 30, 15, 10, 5, 4, 3, 2, 1, 0] jobs = [] - timer_data = [] - - def __init__(self): - mod = __import__(f'conf.{sys.argv[1]}', fromlist=['BotConfig']) - config: BotConfig = getattr(mod, 'BotConfig') - if hasattr(config, "timer_url"): - self.timer_api = config.timer_url - else: - self.timer_api = None def inject(self, registry): self.logger = Logger(__name__) diff --git a/modules/standard/perks/perks_controller.py b/modules/standard/perks/perks_controller.py index 4668394..c104f8c 100644 --- a/modules/standard/perks/perks_controller.py +++ b/modules/standard/perks/perks_controller.py @@ -57,6 +57,6 @@ class PerksController: if row.perk_name != current_perk: blob += f"\n{row.perk_name} {row.max_perk_level}\n" current_perk = row.perk_name - blob += f"{row.skill} {row.buff_amount:d}\n" + blob += f"{row.skill} {row.buff_amount}\n" - return ChatBlob(f"Buff Perks for {level:d} {prof}", blob) + return ChatBlob(f"Buff Perks for {level} {prof}", blob)