From 17c776faecb041bc4cfe2f79e42d7821bdc1a808 Mon Sep 17 00:00:00 2001 From: Minidodo Date: Thu, 25 Nov 2021 14:09:43 +0100 Subject: [PATCH] Fixed: -> !wants -> !orgs info -> special cmd's -> !assist -> "afk" for players without active account -> !loot add => nolonger breaks !account Changes: -> grouped !tara, !gaunt, .. into !wb -> Display the most recent news entry on logon (default: enabled) -> improved grouping of !items -> Added the option to authentificate WS connections (Datanet module). This is used in special cases, where the Websocket Server requires the clien tto authentificate itself. (Server sends "#auth", client responds with the auth string) -> Add main name to relaying (priv <-> org) [default: disabled] -> Added logon/logoff messages back -> restricted default access to "dangerous" commands to moderator -> Added optional logging (Private Channel, Org Channel, Tells, ... disabled by default) Rewrite of the Tower Module. -> More verbosity, if enabled in config. by default, GAS and Hot timer only. -> !hot displays currently hot (and in penalty) sites, and these which go hot in < 60 minutes -> !attacks filterable by PF and Site -> display current contract QL's grouped by org: !contracts (requires managed cache) --- core/command_param_types.py | 2 + core/igncore.py | 37 +- core/private_channel_service.py | 13 +- core/public_channel_service.py | 26 +- core/text.py | 2 +- modules/core/accounting/account_controller.py | 5 +- .../accounting/services/account_service.py | 10 +- .../private_channel_controller.py | 23 +- modules/core/system/util_controller.py | 6 +- .../alliance/alliance_relay_controller.py | 14 +- modules/orgbot/org/cloak_controller.py | 4 +- modules/orgbot/org/online_controller.py | 36 ++ modules/orgbot/org/org_controller.py | 23 +- modules/orgbot/org/org_roster_controller.py | 19 +- modules/orgbot/org/wave_counter_controller.py | 3 +- modules/orgbot/raidspy/raidspy_controller.py | 86 ---- modules/raidbot/tower/plant_controller.py | 76 ---- .../raidbot/tower/tower_attack_controller.py | 368 ------------------ modules/raidbot/tower/tower_hot_controller.py | 204 ---------- modules/raidbot/tower/tower_service.py | 298 -------------- modules/raidbot/tower/tower_site.sql | 5 - modules/standard/datanet/ws_controller.py | 19 +- modules/standard/datanet/ws_worker.py | 17 +- .../standard/helpbot/playfield_controller.py | 9 +- modules/standard/items/items_controller.py | 25 +- modules/standard/loot/loot_controller.py | 13 +- modules/standard/news/news_controller.py | 73 +++- modules/standard/news/worldboss_controller.py | 112 +++--- modules/standard/online/online_controller.py | 7 +- modules/standard/online/online_display.py | 19 +- modules/standard/raid/assist_controller.py | 3 +- .../standard/specials/specials_controller.py | 22 +- modules/standard/timers/timer_controller.py | 14 +- .../tower/contract_controller.py | 28 +- modules/standard/tower/hot_controller.py | 166 ++++++++ modules/standard/tower/land_controller.py | 279 +++++++++++++ .../standard/tower/tower_attack_controller.py | 247 ++++++++++++ modules/standard/tower/tower_controller.py | 170 ++++++++ .../tower/tower_events.py} | 43 +- modules/standard/tower/tower_sites.sql | 282 ++++++++++++++ .../standard/tower/tower_spam_controller.py | 97 +++++ modules/standard/track/track_controller.py | 10 +- modules/standard/wants/wants_controller.py | 1 + modules/standard/whois/org_list_controller.py | 2 +- 44 files changed, 1669 insertions(+), 1249 deletions(-) delete mode 100644 modules/orgbot/raidspy/raidspy_controller.py delete mode 100644 modules/raidbot/tower/plant_controller.py delete mode 100644 modules/raidbot/tower/tower_attack_controller.py delete mode 100644 modules/raidbot/tower/tower_hot_controller.py delete mode 100644 modules/raidbot/tower/tower_service.py delete mode 100644 modules/raidbot/tower/tower_site.sql rename modules/{raidbot => standard}/tower/contract_controller.py (60%) create mode 100644 modules/standard/tower/hot_controller.py create mode 100644 modules/standard/tower/land_controller.py create mode 100644 modules/standard/tower/tower_attack_controller.py create mode 100644 modules/standard/tower/tower_controller.py rename modules/{raidbot/tower/tower_controller.py => standard/tower/tower_events.py} (85%) create mode 100644 modules/standard/tower/tower_sites.sql create mode 100644 modules/standard/tower/tower_spam_controller.py diff --git a/core/command_param_types.py b/core/command_param_types.py index 728e2e8..5bc7d5f 100644 --- a/core/command_param_types.py +++ b/core/command_param_types.py @@ -257,6 +257,8 @@ class Character(Any): if char_id is None: return SenderObj(char_id, val.capitalize(), None) else: + if int(char_id) >= 4294967290: + return SenderObj(None, val.capitalize(), None) return SenderObj(char_id, val.capitalize(), access_service.get_access_level(char_id)) diff --git a/core/igncore.py b/core/igncore.py index aa07789..f8cb2bc 100644 --- a/core/igncore.py +++ b/core/igncore.py @@ -17,6 +17,7 @@ from core.logger import Logger from core.lookup.character_service import CharacterService from core.public_channel_service import PublicChannelService from core.setting_service import SettingService +from core.setting_types import BooleanSettingType from core.text import Text from modules.core.accounting.services.access_service import AccessService @@ -41,8 +42,8 @@ class IgnCore: self.dimension = None self.last_timer_event = 0 self.start_time = int(time.time()) - self.major_version = "IGNCore v2.7" - self.minor_version = "2" + self.major_version = "IGNCore v2.8" + self.minor_version = "0" self.incoming_queue = FifoQueue() self.mass_message_queue = None self.conns = DictObject() @@ -151,6 +152,15 @@ class IgnCore: def start(self): self.register_packet_handler(server_packets.PrivateMessage.id, self.handle_private_message, priority=40) + self.setting_service.register("core.logging", "log_tells", "false", + BooleanSettingType(), + "Should tells get logged to file") + self.setting_service.register("core.logging", "log_priv", "false", + BooleanSettingType(), + "Should the private channel get logged to file") + self.setting_service.register("core.logging", "log_org", "false", + BooleanSettingType(), + "Should the org channel get logged to file") def connect(self, config): conn = self.create_conn("main") @@ -352,6 +362,13 @@ class IgnCore: else: color = self.setting_service.get("private_message_color").get_font_color() if add_color else "" pages = self.get_text_pages(msg, self.setting_service.get("private_message_max_page_length").get_value()) + if self.setting_service.get_value("log_tells") == "1": + if type(msg) == ChatBlob: + self.logger.log_tell('spam', '->', self.character_service.get_char_name(char_id), + f"[link]{msg.title}[/link]") + + else: + self.logger.log_tell('spam', '->', self.character_service.get_char_name(char_id), msg) for page in pages: # self.logger.log_tell(conn_id, "To", self.character_service.get_char_name(char_id), page) packet = client_packets.PrivateMessage(char_id, color + page, "\0") @@ -376,18 +393,23 @@ class IgnCore: DictObject({"private_channel_id": private_channel_id, "message": msg})) def send_mass_message(self, char_id, msg, add_color=True, log_message=False): - # self.logger.log_tell('spam', 'To', self.character_service.get_char_name(char_id), msg) if not char_id: self.logger.warning("Could not send message to empty char_id") if len(self.conns.items()) == 1: self.send_private_message(char_id, msg, add_color, log_message) else: + if self.setting_service.get_value("log_tells") == "1": + if type(msg) == ChatBlob: + self.logger.log_tell('spam', '->', self.character_service.get_char_name(char_id), + f"[link]{msg.title}[/link]") + + else: + self.logger.log_tell('spam', '->', self.character_service.get_char_name(char_id), msg) color = self.setting_service.get("private_message_color").get_font_color() if add_color else "" pages = self.get_text_pages(msg, self.setting_service.get("private_message_max_page_length").get_value()) for page in pages: - if log_message: - self.logger.log_tell("spam", "To", self.character_service.get_char_name(char_id), page) - + # if self.log_mass_tell().get_value(): + # self.logger.log_tell("spam", "->", self.character_service.get_char_name(char_id), page) if self.mass_message_queue: packet = client_packets.PrivateMessage(char_id, color + page, "\0") self.mass_message_queue.put(packet) @@ -396,7 +418,8 @@ class IgnCore: self.conns["main"].send_packet(packet) def handle_private_message(self, conn: Conn, packet: server_packets.PrivateMessage): - # self.logger.log_tell(conn.id, "From", self.character_service.get_char_name(packet.char_id), packet.message) + if self.setting_service.get_value("log_tells") == "1": + self.logger.log_tell(conn.id, "<-", self.character_service.get_char_name(packet.char_id), packet.message) self.event_service.fire_event(self.PRIVATE_MSG_EVENT, packet) def get_text_pages(self, msg, max_page_length): diff --git a/core/private_channel_service.py b/core/private_channel_service.py index 4eb4f4d..bf4ffcc 100644 --- a/core/private_channel_service.py +++ b/core/private_channel_service.py @@ -2,6 +2,7 @@ from core.aochat import server_packets, client_packets from core.conn import Conn from core.decorators import instance from core.logger import Logger +from core.setting_service import SettingService @instance() @@ -19,6 +20,7 @@ class PrivateChannelService: self.event_service = registry.get_instance("event_service") self.character_service = registry.get_instance("character_service") self.access_service = registry.get_instance("access_service") + self.setting_service: SettingService = registry.get_instance("setting_service") def pre_start(self): self.event_service.register_event_type(self.JOINED_PRIVATE_CHANNEL_EVENT) @@ -38,15 +40,19 @@ class PrivateChannelService: def handle_private_channel_message(self, conn: Conn, packet: server_packets.PrivateChannelMessage): if conn.id != "main": return - + if self.setting_service.get_value("log_priv") == "1": + char_name = self.character_service.get_char_name(packet.char_id) + self.logger.log_chat(conn, "Private Channel", char_name, packet.message) if packet.private_channel_id == self.bot.get_char_id(): self.event_service.fire_event(self.PRIVATE_CHANNEL_MESSAGE_EVENT, packet) def handle_private_channel_client_joined(self, conn: Conn, packet: server_packets.PrivateChannelClientJoined): if conn.id != "main": return - if packet.private_channel_id == self.bot.get_char_id(): + if self.setting_service.get_value("log_priv") == "1": + char_name = self.character_service.get_char_name(packet.char_id) + self.logger.log_chat(conn, "Private Channel", None, f"{char_name} joined the channel.") self.private_channel_chars[packet.char_id] = packet self.event_service.fire_event(self.JOINED_PRIVATE_CHANNEL_EVENT, packet) @@ -55,6 +61,9 @@ class PrivateChannelService: return if packet.private_channel_id == self.bot.get_char_id(): + if self.setting_service.get_value("log_priv") == "1": + char_name = self.character_service.get_char_name(packet.char_id) + self.logger.log_chat(conn, "Private Channel", None, f"{char_name} left the channel.") del self.private_channel_chars[packet.char_id] self.event_service.fire_event(self.LEFT_PRIVATE_CHANNEL_EVENT, packet) diff --git a/core/public_channel_service.py b/core/public_channel_service.py index e590d84..3e9a6ad 100644 --- a/core/public_channel_service.py +++ b/core/public_channel_service.py @@ -87,20 +87,22 @@ class PublicChannelService(BaseModule): return if self.is_org_channel_id(packet.channel_id): - # char_name = self.character_service.get_char_name(packet.char_id) - # if packet.extended_message: - # message = packet.extended_message.get_message() - # else: - # message = packet.message - # # self.logger.log_chat(conn.id, "Org Channel", char_name, message) + if self.setting_service.get_value("log_org") == "1" and packet.char_id == self.bot.get_char_id(): + char_name = self.character_service.get_char_name(packet.char_id) + if packet.extended_message: + message = packet.extended_message.get_message() + else: + message = packet.message + self.logger.log_chat(conn.id, "Org Channel", char_name, message) self.event_service.fire_event(self.ORG_CHANNEL_MESSAGE_EVENT, packet) elif packet.channel_id == self.ORG_MSG_CHANNEL_ID: - # char_name = self.character_service.get_char_name(packet.char_id) - # if packet.extended_message: - # message = packet.extended_message.get_message() - # else: - # message = packet.message - # self.logger.log_chat(conn.id, "Org Msg", char_name, message) + if self.setting_service.get_value("log_org") == "1" and packet.char_id == self.bot.get_char_id(): + char_name = self.character_service.get_char_name(packet.char_id) + if packet.extended_message: + message = packet.extended_message.get_message() + else: + message = packet.message + self.logger.log_chat(conn.id, "Org Msg", char_name, message) self.event_service.fire_event(self.ORG_MSG_EVENT, packet) def is_org_channel_id(self, channel_id): diff --git a/core/text.py b/core/text.py index 8985519..27f501a 100644 --- a/core/text.py +++ b/core/text.py @@ -128,7 +128,7 @@ class Text: if count == 0: return no_data_msg else: - blob = "" + blob = "" blob += "" + pages + "\n" blob += headline index = offset diff --git a/modules/core/accounting/account_controller.py b/modules/core/accounting/account_controller.py index 0cde41d..05a29e0 100644 --- a/modules/core/accounting/account_controller.py +++ b/modules/core/accounting/account_controller.py @@ -94,7 +94,8 @@ class AccountController: # 0 is member, # any number above 0 indicates that its an org_member of the same ID) self.account_service.account_add_member(user.char_id) - self.buddy_service.add_buddy(user.char_id, "member") + for char in self.account_service.get_alts(user.char_id): + self.buddy_service.add_buddy(char.char_id, "member") self.account_service.add_log(request.sender.char_id, "system", f"Opened Account for {user.name}.", request.sender.char_id) @@ -256,7 +257,7 @@ class AccountController: response += f" Status: {'Open' if alts[0].disabled == 0 else 'Closed'}\n" response += f" Created at: {self.util.format_datetime(alts[0].created)}\n" if last_seen: - response += f" Last seen on {last_seen.name} {self.util.time_to_readable(time.time()-last_seen.last_seen)} ago\n" + response += f" Last seen on {last_seen.name} {self.util.time_to_readable(time.time() - last_seen.last_seen)} ago\n" response += f" Permissions: {', '.join(perms)}\n" if alts[0].discord_joined == 1: joined = ' (Joined server)' diff --git a/modules/core/accounting/services/account_service.py b/modules/core/accounting/services/account_service.py index b98a2c6..4d70e90 100644 --- a/modules/core/accounting/services/account_service.py +++ b/modules/core/accounting/services/account_service.py @@ -5,13 +5,13 @@ from core.buddy_service import BuddyService from core.db import DB, SqlException from core.decorators import instance, timerevent, event from core.dict_object import DictObject +from core.igncore import IgnCore from core.logger import Logger from core.lookup.character_service import CharacterService from core.lookup.pork_service import PorkService from core.setting_service import SettingService from core.setting_types import BooleanSettingType from core.text import Text -from core.igncore import IgnCore from modules.core.accounting.services.access_service import AccessService @@ -184,6 +184,14 @@ class AccountService: "where a.char_id=? and a.char_id not in (SELECT char_id from org_bots)", [char_id]) or DictObject({}) + def set_logon(self, char_id, logon="") -> DictObject: + return self.db.exec("UPDATE account SET logon=? where char_id=?", + [logon, char_id]) + + def set_logoff(self, char_id, logoff="") -> DictObject: + return self.db.exec("UPDATE account SET logoff=? where char_id=?", + [logoff, char_id]) + def add_pending_alt(self, main, alt) -> [str, bool]: data = self.check_alt(alt) if data: diff --git a/modules/core/private_channel/private_channel_controller.py b/modules/core/private_channel/private_channel_controller.py index e2d4b55..d96069f 100644 --- a/modules/core/private_channel/private_channel_controller.py +++ b/modules/core/private_channel/private_channel_controller.py @@ -4,18 +4,20 @@ from core.buddy_service import BuddyService from core.chat_blob import ChatBlob from core.command_param_types import Character, Multiple from core.db import DB, SqlException -from core.decorators import instance, command, event +from core.decorators import instance, command, event, setting from core.dict_object import DictObject +from core.igncore import IgnCore from core.lookup.character_service import CharacterService from core.lookup.pork_service import PorkService from core.private_channel_service import PrivateChannelService from core.setting_service import SettingService +from core.setting_types import BooleanSettingType from core.text import Text from core.translation_service import TranslationService -from core.igncore import IgnCore from core.util import Util from modules.core.accounting.services.account_service import AccountService from modules.core.ban.ban_service import BanService +from modules.orgbot.org.org_controller import OrgChannelController from modules.standard.online.online_display import OnlineDisplay @@ -76,7 +78,14 @@ class PrivateChannelController: return hjson.load(f) def handle_incoming_relay_message(self, ctx): - self.bot.send_private_channel_message(ctx.formatted_message, fire_outgoing_event=False) + if not self.display_main().get_value() == "1" and ctx.source == "org_channel" and ctx.sender: + name = f"{OrgChannelController.ORG_CHANNEL_PREFIX} {ctx.sender.name}" + if account := self.account_service.get_account(ctx.sender.char_id): + if account.main != ctx.sender.char_id: + name += f" ({account.name})" + self.bot.send_private_channel_message(name + ": " + ctx.message, fire_outgoing_event=False) + else: + self.bot.send_private_channel_message(ctx.formatted_message, fire_outgoing_event=False) @event(event_type="member_logon", description="Send autoinvites to players logging in") def logon_event(self, _, data): @@ -85,7 +94,7 @@ class PrivateChannelController: account = data.account if account.disabled == 1: return - if account.member == self.bot.public_channel_service.org_id: + if self.pork.get_character_info(data.packet.char_id).org_id == self.bot.public_channel_service.org_id: return if account.auto_invite == 1: self.reinvite.append(data.packet.char_id) @@ -210,7 +219,7 @@ class PrivateChannelController: if self.online_controller: afk_list = self.online_controller.afk_list od = OnlineDisplay(self.text, self.util, self.db, afk_list) - od = OnlineDisplay(self.text, self.util, self.db) + # od = OnlineDisplay(self.text, self.util, self.db) params = [self.bot.name, self.bot.get_char_id()] self.bot.send_mass_message(event_data.char_id, od.format_blob(od.format_by_channel_prof("and channel_id IN (1, 2) ", params))) @@ -248,3 +257,7 @@ class PrivateChannelController: None, message, message) + + @setting(name="display_main", value="false", description="Should the main be displayed in relayed messages") + def display_main(self) -> BooleanSettingType: + return BooleanSettingType() diff --git a/modules/core/system/util_controller.py b/modules/core/system/util_controller.py index 1258419..73686a7 100644 --- a/modules/core/system/util_controller.py +++ b/modules/core/system/util_controller.py @@ -46,7 +46,7 @@ class UtilController: {"char": char.name, "rank_main": char.access_level["label"]}) - @command(command="macro", params=[Any("command1|command2|command3...")], access_level="member", + @command(command="macro", params=[Any("command1|command2|command3...")], access_level="moderator", description="Execute multiple commands at once") def macro_cmd(self, request, commands): commands = commands.split("|") @@ -58,7 +58,7 @@ class UtilController: request.reply, request.conn) - @command(command="echo", params=[Any("message")], access_level="member", + @command(command="echo", params=[Any("message")], access_level="moderator", description="Echo back a message") def echo_cmd(self, _, message): return html.escape(message) @@ -96,7 +96,7 @@ class UtilController: bots_connected += f"{_id} - {conn.char_name} ({conn.char_id})\n" for channel_id, name in self.public_channel_service.get_all_public_channels().items(): - pub_channels += f"{name} - {channel_id:d}\n" + pub_channels += f"{name} - {channel_id}\n" for event_type in self.event_service.get_event_types(): event_types += f"{event_type}\n" diff --git a/modules/onlinebot/alliance/alliance_relay_controller.py b/modules/onlinebot/alliance/alliance_relay_controller.py index 46fda8f..5417266 100644 --- a/modules/onlinebot/alliance/alliance_relay_controller.py +++ b/modules/onlinebot/alliance/alliance_relay_controller.py @@ -1,12 +1,13 @@ from core.aochat import server_packets, client_packets from core.conn import Conn from core.decorators import instance +from core.igncore import IgnCore from core.logger import Logger from core.lookup.character_service import CharacterService from core.setting_service import SettingService from core.setting_types import TextSettingType, BooleanSettingType, ColorSettingType from core.text import Text -from core.igncore import IgnCore +from modules.onlinebot.online.org_alias_controller import OrgAliasController @instance("AllianceRelayController") @@ -25,6 +26,7 @@ class AllianceRelayController: self.message_hub_service = registry.get_instance("message_hub_service") self.public_channel_service = registry.get_instance("public_channel_service") self.text: Text = registry.get_instance("text") + self.alias_controller: OrgAliasController = registry.get_instance("org_alias_controller") def pre_start(self): self.message_hub_service.register_message_source(self.MESSAGE_SOURCE) @@ -87,21 +89,21 @@ class AllianceRelayController: if not self.setting_service.get("arelay_enabled").get_value(): return - plain_msg = ctx.message or ctx.formatted_message + plain_msg = ctx.message invite = self.text.make_chatcmd("click here", "/tell discord invite", style="style='text-decoration:none'") - + name = f"[{self.alias_controller.get_alias(ctx.sender.org_id)}] {ctx.sender.name}" blob = self.text.format_page('Info', f"
::: Information :::


" f"This message has been sent to you by:

" f"Igncom
" - f"{ctx.sender[1].name + '#' + ctx.sender[1].discriminator}
" - f"{ctx.sender[0]} on Alliance Discord.

" + f"{ctx.sender.discord_handle}
" + f"{name} on Alliance Discord.

" f"To reply, either respond in the relay or " f"contact them directly at the provided handles.

" f"Have you joined The Alliance Discord yet? " f"If not {invite} to receive an invite.") - self.send_message_to_alliance(plain_msg + f" [{blob}]") + self.send_message_to_alliance(f"{name}: {plain_msg}" + f" [{blob}]") def send_message_to_alliance(self, msg): if self.relay_channel_id: diff --git a/modules/orgbot/org/cloak_controller.py b/modules/orgbot/org/cloak_controller.py index e831b94..c926c30 100644 --- a/modules/orgbot/org/cloak_controller.py +++ b/modules/orgbot/org/cloak_controller.py @@ -74,8 +74,8 @@ class CloakController: time_until_change = row.created_at + one_hour - t if row.action == "off" and time_until_change <= 0: time_str = self.util.time_to_readable(t - row.created_at) - msg = "The cloaking device is disabled but can be enabled. " \ - "%s disabled it %s ago." % (row.name, time_str) + msg = f"The cloaking device is disabled but can be enabled. " \ + f"{row.name} disabled it {time_str} ago." self.message_hub_service.send_message(self.MESSAGE_SOURCE, None, None, msg) @event(event_type=CLOAK_EVENT, description="Set a timer for when cloak can be raised and lowered") diff --git a/modules/orgbot/org/online_controller.py b/modules/orgbot/org/online_controller.py index 8343704..171ec99 100644 --- a/modules/orgbot/org/online_controller.py +++ b/modules/orgbot/org/online_controller.py @@ -15,6 +15,42 @@ class OrgOnlineController(OnlineController): if self.bot.is_ready(): self.awaiting_data.put([event_data, 'org', False]) + @command(command="logon", params=[Const("clear")], access_level="member", + description="Clears your logon message") + def logon_clear_cmd(self, request, message): + self.account_service.set_logon(request.sender.char_id) + return "Your logon message has been cleared." + + @command(command="logoff", params=[Const("clear")], access_level="member", + description="Clears your logoff message") + def logoff_clear_cmd(self, request, message): + self.account_service.set_logoff(request.sender.char_id) + return "Your logoff message has been cleared." + + @command(command="logon", params=[Any("message", is_optional=True)], access_level="member", + description="Sets or shows your logon message") + def logon_cmd(self, request, message): + if message: + self.account_service.set_logon(request.sender.char_id, message) + return f"Your new logon message has been set: {message}" + else: + entry = self.account_service.get_entry(request.sender.char_id) + if entry.logon: + return f"Your current logon message is: {entry.logon}" + return f"You do not have a logon message set." + + @command(command="logoff", params=[Any("message", is_optional=True)], access_level="member", + description="Sets or shows your logoff message ") + def logoff_cmd(self, request, message): + if message: + self.account_service.set_logoff(request.sender.char_id, message) + return f"Your new logoff message has been set: {message}" + else: + entry = self.account_service.get_entry(request.sender.char_id) + if entry.logon: + return f"Your current logoff message is: {entry.logon}" + return f"You do not have a logoff message set." + @command(command="online", params=[Const('all', is_optional=True), Int("min_level", is_optional=True), Any("profession", is_optional=True)], diff --git a/modules/orgbot/org/org_controller.py b/modules/orgbot/org/org_controller.py index 7806ad9..0bed43b 100644 --- a/modules/orgbot/org/org_controller.py +++ b/modules/orgbot/org/org_controller.py @@ -1,13 +1,13 @@ from core.chat_blob import ChatBlob from core.db import DB -from core.decorators import instance, event +from core.decorators import instance, event, setting from core.dict_object import DictObject +from core.igncore import IgnCore from core.logger import Logger from core.public_channel_service import PublicChannelService from core.setting_service import SettingService from core.setting_types import BooleanSettingType from core.text import Text -from core.igncore import IgnCore from core.util import Util from modules.core.accounting.services.account_service import AccountService from modules.orgbot.org.org_roster_controller import OrgRosterController @@ -44,11 +44,16 @@ class OrgChannelController: ["private_channel", "websocket_relay", "cloak_reminder", "wave_counter", "shutdown_notice"], [self.MESSAGE_SOURCE]) - self.setting_service.register(self.module_name, "prefix_org_priv", True, BooleanSettingType(), - "Should the prefix [org] be displayed in relayed messages") - def handle_incoming_relay_message(self, ctx): - self.bot.send_org_message(ctx.formatted_message, fire_outgoing_event=False) + # {'source': 'org_channel', 'sender': {'char_id': 384018, 'name': 'Risianna'}, 'message': 'Sooo', 'formatted_message': "[Org] Risianna: Sooo"} + if not self.display_main().get_value() == "1" and ctx.source == "private_channel" and ctx.sender: + name = f"{OrgChannelController.ORG_CHANNEL_PREFIX} {ctx.sender.name}" + if account := self.account_service.get_account(ctx.sender.char_id): + if account.main != ctx.sender.char_id: + name += f" ({account.name})" + self.bot.send_org_message(name + ": " + ctx.message, fire_outgoing_event=False) + else: + self.bot.send_org_message(ctx.formatted_message, fire_outgoing_event=False) @event(event_type=PublicChannelService.ORG_CHANNEL_MESSAGE_EVENT, description="Relay messages from the org channel to the relay hub", @@ -101,7 +106,7 @@ class OrgChannelController: if not self.bot.is_ready(): return char_name = self.character_service.resolve_char_to_name(event_data.packet.char_id) - logoff = f" :: {event_data.account.logon}" if event_data.account.logon else "" + logoff = f" :: {event_data.account.logoff}" if event_data.account.logoff else "" msg = f"{char_name} logged off.{logoff}" self.bot.send_org_message(msg, fire_outgoing_event=False) self.message_hub_service.send_message(self.MESSAGE_SOURCE, None, None, "[Org] " + msg) @@ -132,3 +137,7 @@ class OrgChannelController: None, event_data.message, message) + + @setting(name="display_main", value="false", description="Should the main be displayed in relayed messages") + def display_main(self) -> BooleanSettingType: + return BooleanSettingType() diff --git a/modules/orgbot/org/org_roster_controller.py b/modules/orgbot/org/org_roster_controller.py index b610b27..fdc5895 100644 --- a/modules/orgbot/org/org_roster_controller.py +++ b/modules/orgbot/org/org_roster_controller.py @@ -3,6 +3,7 @@ import time import requests +from core.aochat import server_packets from core.buddy_service import BuddyService from core.cache_service import CacheService from core.chat_blob import ChatBlob @@ -11,12 +12,12 @@ from core.db import DB from core.decorators import instance, command, event, timerevent from core.dict_object import DictObject from core.event_service import EventService +from core.igncore import IgnCore from core.logger import Logger from core.lookup.character_service import CharacterService from core.lookup.org_pork_service import OrgPorkService from core.lookup.pork_service import PorkService from core.public_channel_service import PublicChannelService -from core.igncore import IgnCore from core.util import Util from modules.core.accounting.services.account_service import AccountService @@ -43,6 +44,7 @@ class OrgRosterController: JOINED_ORG = [508, 5146599] def __init__(self): + self.readd_cache = [] self.logger = Logger(__name__) def inject(self, registry): @@ -63,12 +65,13 @@ class OrgRosterController: def pre_start(self): self.db.exec("CREATE TABLE IF NOT EXISTS org_activity (" "id int primary key AUTO_INCREMENT, " - "message varchar(32) NOT NULL, " + "message varchar(255) NOT NULL, " "time int not null)") self.event_service.register_event_type(self.ORG_MEMBER_LOGON_EVENT) self.event_service.register_event_type(self.ORG_MEMBER_LOGOFF_EVENT) self.access_service.register_access_level(self.ORG_ACCESS_LEVEL, 60, self.check_org_member) + self.bot.register_packet_handler(server_packets.BuddyRemoved.id, self.handle_remove) def check_org_member(self, char_id): return (self.account_service.get_account(char_id) or {}).get("member", 0) == self.public_channel_service.org_id @@ -128,7 +131,7 @@ class OrgRosterController: self.bot.send_org_message("Updating roster...") cache = self.cache.retrieve('org_roster', f"{self.public_channel_service.org_id}.5.json") if cache: - if cache.last_modified > time.time() - 16 * 60 * 60: + if cache.last_modified < time.time() - 16 * 60 * 60: result = requests.get(self.org_pork_service.get_pork_url(5, self.public_channel_service.org_id)).json() if result: self.cache.store('org_roster', f"{self.public_channel_service.org_id}.5.json", json.dumps(result)) @@ -250,7 +253,15 @@ class OrgRosterController: def update_buddylist(self, char_id, mode): if mode in [self.MODE_ADD_MANUAL, self.MODE_ADD_AUTO]: + if not self.buddy_service.get_buddy(char_id): + self.buddy_service.add_buddy(char_id, self.ORG_BUDDY_TYPE) + return + self.readd_cache.append(char_id) self.buddy_service.remove_buddy(char_id, "member") - self.buddy_service.add_buddy(char_id, self.ORG_BUDDY_TYPE) else: self.buddy_service.remove_buddy(char_id, self.ORG_BUDDY_TYPE) + + def handle_remove(self, conn, packet): + if packet.char_id in self.readd_cache: + self.readd_cache.remove(packet.char_id) + self.buddy_service.add_buddy(packet.char_id, self.ORG_BUDDY_TYPE) diff --git a/modules/orgbot/org/wave_counter_controller.py b/modules/orgbot/org/wave_counter_controller.py index 5529468..2ccb050 100644 --- a/modules/orgbot/org/wave_counter_controller.py +++ b/modules/orgbot/org/wave_counter_controller.py @@ -42,8 +42,7 @@ class WaveCounterController: self.send_message("General incoming. DO NOT enter the city!") self.scheduled_job_id = None else: - self.send_message("Wave %d incoming. " - "DO NOT enter the city!" % wave_number) + self.send_message(f"Wave {wave_number} incoming. DO NOT enter the city!") self.scheduled_job_id = self.job_scheduler.scheduled_job(self.timer_alert, t + self.ALERT_TIMES[wave_number], wave_number) diff --git a/modules/orgbot/raidspy/raidspy_controller.py b/modules/orgbot/raidspy/raidspy_controller.py deleted file mode 100644 index fdbaaa0..0000000 --- a/modules/orgbot/raidspy/raidspy_controller.py +++ /dev/null @@ -1,86 +0,0 @@ -import re - -from core.aochat.client_packets import PrivateMessage -from core.command_param_types import Const -from core.decorators import instance, setting, command, timerevent, event -from core.logger import Logger -from core.lookup.character_service import CharacterService -from core.message_hub_service import MessageHubService -from core.public_channel_service import PublicChannelService -from core.setting_service import SettingService -from core.setting_types import TextSettingType -from core.text import Text -from core.igncore import IgnCore -from core.util import Util - - -@instance() -class RaidSpyController: - planned = "" - - def __init__(self): - self.logger = Logger(__name__) - - def inject(self, registry): - self.bot: IgnCore = registry.get_instance("bot") - self.setting_service: SettingService = registry.get_instance("setting_service") - self.character_service: CharacterService = registry.get_instance("character_service") - self.public_channel_service: PublicChannelService = registry.get_instance("public_channel_service") - self.util: Util = registry.get_instance("util") - self.text: Text = registry.get_instance("text") - self.relay_hub_service: MessageHubService = registry.get_instance("message_hub_service") - - @setting(name="raidbot-name", value="", description="The Raidbot") - def raidbot(self): - return TextSettingType(allow_empty=True) - - @event(event_type=IgnCore.PRIVATE_MSG_EVENT, description="update raidlist", is_enabled=False) - def handle_raidbot_msg(self, _, textblob: PrivateMessage): - if self.character_service.get_char_name(textblob.char_id) != self.setting_service.get_value("raidbot-name"): - return - tag = re.search( - r"Planned Raids last updated \w+ \d+\w+, \d+ \d+:\d+:: " - r".+", - textblob.message, - re.DOTALL) - if tag: - textblob.message = tag[1] - with open("data/latest_raids.txt", "w") as f: - f.write(textblob.message) - self.planned = textblob.message - self.bot.send_org_message("Die Raids wurden geupdatet: " + - self.text.format_page("Die Raids der Woche", self.planned), fire_outgoing_event=False) - self.bot.send_private_channel_message("Die Raids wurden geupdatet: " + - self.text.format_page("Die Raids der Woche", self.planned), - fire_outgoing_event=False) - - @event(event_type="connect", description="update raidlist", is_enabled=False) - def handle_log_raidlog(self, _, _1): - try: - with open("data/latest_raids.txt", "r") as f: - self.planned = f.read() - except FileNotFoundError: - self.planned = "
:::: Planned Raids ::::" \ - "

Es sind mir leider keine Raids bekannt." - - # @command(command="raids", params=[], - # description="Shows planned raids", access_level="org_member") - # def raids_list(self, request): - # return self.text.format_page("Die Raids der Woche", self.planned) - - @command(command="raids", params=[Const("update")], - description="Shows planned raids", access_level="moderator", sub_command="update") - def raids_patch(self, request, _): - self.bot.send_private_message(self.character_service.resolve_char_to_id(self.raidbot().get_value()), - "!raids", - add_color=False) - return "Das Updaten der Raidliste wurde eingeleitet... Sollte es neues geben, " \ - "Informiere ich Alle Mitglieder für dich." - - @timerevent(budatime="12h", description="Update Raid list") - def check_for_raids(self, _, _2): - if self.raidbot().get_value() == "": - return - self.bot.send_private_message(self.character_service.resolve_char_to_id(self.raidbot().get_value()), - "!raids", - add_color=False) diff --git a/modules/raidbot/tower/plant_controller.py b/modules/raidbot/tower/plant_controller.py deleted file mode 100644 index 91f6b8d..0000000 --- a/modules/raidbot/tower/plant_controller.py +++ /dev/null @@ -1,76 +0,0 @@ -import time - -from core.aochat.BaseModule import BaseModule -from core.chat_blob import ChatBlob -from core.command_alias_service import CommandAliasService -from core.db import DB -from core.decorators import instance, command -from core.event_service import EventService -from core.lookup.pork_service import PorkService -from core.public_channel_service import PublicChannelService -from core.text import Text -from core.igncore import IgnCore -from core.util import Util -from modules.raidbot.tower.tower_service import TowerService -from modules.standard.helpbot.playfield_controller import PlayfieldController - - -@instance() -class PlantController(BaseModule): - # noinspection DuplicatedCode - def inject(self, registry): - self.bot: IgnCore = registry.get_instance("bot") - self.db: DB = registry.get_instance("db") - self.util: Util = registry.get_instance("util") - self.text: Text = registry.get_instance("text") - self.event_service: EventService = registry.get_instance("event_service") - self.pork_service: PorkService = registry.get_instance("pork_service") - self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller") - self.public_channel_service: PublicChannelService = registry.get_instance("public_channel_service") - self.towercache: TowerService = registry.get_instance("tower_service") - self.command_alias_service: CommandAliasService = registry.get_instance("command_alias_service") - - @command(command="penalty", params=[], access_level="member", description="Shows planttimers") - def penalty(self, _): - blob = "" - self.towercache.attack_hot.sort(key=lambda k: k['org_name']) - rem = [] - for key, value in enumerate(sorted(self.towercache.attack_hot, key=lambda k: k['org_name'])): - if value["hot"] < time.time(): - rem.append(key) - continue - - lca = self.towercache.get_towers_by_org_name(value["org_name"]) - for site in lca: - blob += self.towercache.format_entry(site, 10) - - blob += "" - for i in reversed(rem): - self.towercache.attack_hot.pop(i) - if len(self.towercache.attack_hot) == 0: - blob = "" - return ChatBlob(f"Warhot tower sites", blob.strip("\n")) if blob != "" else f"There are no orgs in penalty." - - @command(command="plant", params=[], access_level="member", description="Shows planttimers") - def plant(self, _): - blob = "" - rem = [] - for key, value in enumerate(sorted(self.towercache.plant, key=lambda k: k['pf'])): - print(key, value) - if value["plant"] < time.time(): - rem.append(key) - continue - - lca = self.playfield_controller.get_playfield_by_id(value['pf']) - blob += f"[{lca.short_name}] x{value['site']} in " \ - f"{self.util.format_time(value['plant'] - time.time())}\n" - - blob += "" - for i in reversed(rem): - self.towercache.plant.pop(i) - if len(self.towercache.plant) == 0: - blob = "" - if blob != "": - return ChatBlob(f"Awaiting plants ({len(self.towercache.plant)})", blob.strip("\n")) - else: - return f"There are no sites awaiting plants." diff --git a/modules/raidbot/tower/tower_attack_controller.py b/modules/raidbot/tower/tower_attack_controller.py deleted file mode 100644 index 952efd6..0000000 --- a/modules/raidbot/tower/tower_attack_controller.py +++ /dev/null @@ -1,368 +0,0 @@ -import time - -from core.chat_blob import ChatBlob -from core.command_param_types import Const, Int, NamedParameters -from core.decorators import instance, command, event, setting -from core.job_scheduler import JobScheduler -from core.logger import Logger -from core.public_channel_service import PublicChannelService -from core.setting_service import SettingService -from core.setting_types import BooleanSettingType -from core.text import Text -from core.igncore import IgnCore -from modules.core.accounting.services.account_service import AccountService -from modules.raidbot.tower.tower_controller import TowerController -from modules.raidbot.tower.tower_service import TowerService -from modules.standard.helpbot.playfield_controller import PlayfieldController - -# TODO: This module should get split again in the future, allowing tower-tracking in orgbots, or other types. -# - - -@instance() -class TowerAttackController: - def __init__(self): - self.logger = Logger(__name__) - - def inject(self, registry): - self.bot: IgnCore = registry.get_instance("bot") - self.db = registry.get_instance("db") - self.text: Text = registry.get_instance("text") - self.settings: SettingService = registry.get_instance("setting_service") - self.util = registry.get_instance("util") - self.tower: TowerController = registry.get_instance("tower_controller") - self.towerservice: TowerService = registry.get_instance("tower_service") - self.event_service = registry.get_instance("event_service") - self.command_alias_service = registry.get_instance("command_alias_service") - self.public_channel_service: PublicChannelService = registry.get_instance("public_channel_service") - self.job_scheduler: JobScheduler = registry.get_instance("job_scheduler") - self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller") - self.account_service: AccountService = registry.get_instance("account_service") - - def start(self): - self.db.exec( - "CREATE TABLE IF NOT EXISTS tower_attacker (" - "id INT PRIMARY KEY AUTO_INCREMENT, " - "att_org_name VARCHAR(50) NOT NULL, " - "att_faction VARCHAR(10) NOT NULL, " - "att_char_id INT, att_char_name VARCHAR(20) NOT NULL, " - "att_level INT NOT NULL, " - "att_ai_level INT NOT NULL, " - "att_profession VARCHAR(15) NOT NULL, " - "x_coord INT NOT NULL, " - "y_coord INT NOT NULL, " - "is_victory SMALLINT NOT NULL, " - "tower_battle_id INT NOT NULL, " - "created_at INT NOT NULL)") - self.db.exec( - "CREATE TABLE IF NOT EXISTS tower_battle (" - "id INT PRIMARY KEY AUTO_INCREMENT, " - "playfield_id INT NOT NULL, " - "site_number INT NOT NULL, " - "def_org_name VARCHAR(50) NOT NULL, " - "def_faction VARCHAR(10) NOT NULL, " - "is_finished INT NOT NULL, " - "battle_type VARCHAR(20) NOT NULL, " - "last_updated INT NOT NULL)") - - self.command_alias_service.add_alias("victory", "attacks") - - @command(command="attacks", params=[NamedParameters(["page"])], access_level="member", - description="Show recent tower attacks and victories") - def attacks_cmd(self, _, named_params): - page = int(named_params.page or "1") - - page_size = 30 - offset = (page - 1) * page_size - - sql = """ - SELECT - b.*, - a.*, - COALESCE(a.att_level, 0) AS att_level, - COALESCE(a.att_ai_level, 0) AS att_ai_level, - p.short_name, - b.id AS battle_id - FROM - tower_battle b - LEFT JOIN tower_attacker a ON - a.tower_battle_id = b.id - LEFT JOIN playfields p ON - p.id = b.playfield_id - ORDER BY - b.last_updated DESC, - a.created_at DESC - LIMIT %d, %d - """ % (offset, page_size) - - data = self.db.query(sql) - t = int(time.time()) - - blob = self.check_for_all_towers_channel() - - if page > 1: - blob += " " + self.text.make_chatcmd("<< Page %d" % (page - 1), self.get_chat_command(page - 1)) - if len(data) > 0: - blob += " Page " + str(page) - blob += " " + self.text.make_chatcmd("Page %d >>" % (page + 1), self.get_chat_command(page + 1)) - blob += "\n" - - current_battle_id = -1 - for row in data: - if current_battle_id != row.battle_id: - blob += "\n" - current_battle_id = row.battle_id - blob += self.format_battle_info(row, t) - blob += self.text.make_tellcmd("More Info", "attacks battle %d" % row.battle_id) + "\n" - blob += "Attackers:\n" - - blob += "" + self.format_attacker(row) + "\n" - - return ChatBlob("Tower Attacks", blob) - - @command(command="attacks", params=[Const("battle"), Int("battle_id")], access_level="member", - description="Show battle info for a specific battle") - def attacks_battle_cmd(self, _, _1, battle_id): - battle = self.db.query_single( - "SELECT b.*, p.short_name FROM tower_battle b " - "LEFT JOIN playfields p ON p.id = b.playfield_id WHERE b.id = ?", - [battle_id]) - if not battle: - return "Could not find battle with ID %d." % battle_id - - t = int(time.time()) - - attackers = self.db.query("SELECT * FROM tower_attacker WHERE tower_battle_id = ? ORDER BY created_at DESC", - [battle_id]) - - first_activity = attackers[-1].created_at if len(attackers) > 0 else battle.last_updated - - blob = self.check_for_all_towers_channel() - blob += self.format_battle_info(battle, t) - blob += f"Duration: " \ - f"{self.util.time_to_readable(battle.last_updated - first_activity)}\n\n" - blob += "Attackers:\n" - - for row in attackers: - blob += "" + self.format_attacker(row) - blob += " " + self.format_timestamp(row.created_at, t) - blob += "\n" - - return ChatBlob(f"Battle Info {battle_id}", blob) - - @event(event_type=TowerController.TOWER_ATTACK_EVENT, description="Create logentries for tower attacks", is_hidden=True) - def tower_attack_event(self, _, event_data): - t = int(time.time()) - site_number = self.find_closest_site_number(event_data.location.playfield.id, event_data.location.x_coord, - event_data.location.y_coord) - - attacker = event_data.attacker or {} - defender = event_data.defender - - battle = self.find_or_create_battle(event_data.location.playfield.id, site_number, defender.org_name, - defender.faction, "attack", t) - # print(battle) - self.db.exec( - "INSERT INTO tower_attacker (att_org_name, att_faction, att_char_id, att_char_name, " - "att_level, att_ai_level, att_profession, " - "x_coord, y_coord, is_victory, tower_battle_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - [attacker.get("org_name", ""), attacker.get("faction", ""), attacker.get("char_id", 0), - attacker.get("name", ""), attacker.get("level", 0), - attacker.get("ai_level", 0), attacker.get("profession", ""), event_data.location.x_coord, - event_data.location.y_coord, 0, battle.id, t]) - - @setting(name="tower_notify_type", value=False, description="Only notify when our orgs are involved") - def tower_notify_type(self) -> BooleanSettingType: - return BooleanSettingType() - - @event(event_type=TowerController.TOWER_ATTACK_EVENT, description="Notify whenever a tower attack happens") - def tower_def_event(self, _, event_data): - if self.tower_notify_type().get_value(): - if not (event_data.attacker.get("org_name", None) in self.account_service.get_org_names() or event_data.defender.org_name in self.account_service.get_org_names()): - return - if event_data.attacker.get("name", None) is not None: - field_id = self.find_closest_site_number(event_data.location.playfield.id, event_data.location.x_coord, - event_data.location.y_coord) - row = self.db.query_single( - "SELECT t.*, p.short_name, p.long_name FROM tower_site t " - "JOIN playfields p ON t.playfield_id = p.id WHERE t.playfield_id = ? AND site_number = ?", - [event_data.location.playfield.id, field_id]) - lca = self.text.format_page(f"{event_data.location.playfield.long_name} - {field_id:d}", - self.tower.format_site_info(row)) - attacker = self.text.format_char_info(event_data.attacker) - add = "" - # Disable for now... - # if account := self.account_service.get_account(event_data.attacker.char_id) and event_data.defender.org_name in self.account_service.get_org_names(): - # if self.account_service.simple_checks(account): - # add = " :: He's a Raider!" - self.bot.send_private_channel_message( - f"[NW] " - f"<{event_data.defender.faction.lower()}>" - f"{event_data.defender.org_name}" - f" " - f"attacked by {attacker} in {lca}{add}") - - @event(event_type=TowerController.TOWER_VICTORY_EVENT, description="Record tower victories", is_hidden=True) - def tower_victory_event(self, _, event_data): - t = int(time.time()) - if event_data.type == "attack": - row = self.get_last_attack(event_data.winner.faction, event_data.winner.org_name, event_data.loser.faction, - event_data.loser.org_name, event_data.location.playfield.id, t) - - if not row: - site_number = 0 - is_finished = 1 - self.db.exec( - "INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, " - "is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)", - [event_data.location.playfield.id, site_number, event_data.loser.org_name, event_data.loser.faction, - is_finished, event_data.type, t]) - battle_id = self.db.last_insert_id() - - attacker = event_data.winner or {} - self.db.exec( - "INSERT INTO tower_attacker (att_org_name, att_faction, att_char_id, " - "att_char_name, att_level, att_ai_level, att_profession, " - "x_coord, y_coord, is_victory, tower_battle_id, created_at) " - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - [attacker.get("org_name", ""), attacker.get("faction", ""), attacker.get("char_id", 0), - attacker.get("name", ""), attacker.get("level", 0), - attacker.get("ai_level", 0), attacker.get("profession", ""), 0, 0, 0, battle_id, t]) - else: - is_victory = 1 - self.db.exec("UPDATE tower_attacker SET is_victory = ? WHERE id = ?", [is_victory, row.attack_id]) - - is_finished = 1 - self.db.exec("UPDATE tower_battle SET is_finished = ?, last_updated = ? WHERE id = ?", - [is_finished, t, row.battle_id]) - elif event_data.type == "terminated": - site_number = 0 - is_finished = 1 - self.db.exec( - "INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, " - "is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)", - [event_data.location.playfield.id, site_number, event_data.loser.org_name, event_data.loser.faction, - is_finished, event_data.type, t]) - else: - raise Exception("Unknown victory event type: '%s'" % event_data.type) - - def format_attacker(self, row): - level = f"{row.att_level}/{row.att_ai_level}" if row.att_ai_level > 0 else f"{row.att_level}" - org = row.att_org_name + " " if row.att_org_name else "" - victor = " - Winner!" if row.is_victory else "" - return f"{row.att_char_name or 'Unknown attacker'} ({level} {row.att_profession})" \ - f" {org}({row.att_faction}){victor}" - - def find_closest_site_number(self, playfield_id, x_coord, y_coord): - # noinspection SqlUnused - sql = """ - SELECT - site_number, - ((x_distance * x_distance) + (y_distance * y_distance)) radius - FROM - (SELECT - playfield_id, - site_number, - min_ql, - max_ql, - x_coord, - y_coord, - site_name, - (x_coord - ?) as x_distance, - (y_coord - ?) as y_distance - FROM - tower_site - WHERE - playfield_id = ?) t - ORDER BY - radius - LIMIT 1""" - - row = self.db.query_single(sql, [x_coord, y_coord, playfield_id]) - if row: - return row.site_number - else: - return 0 - - def find_or_create_battle(self, playfield_id, site_number, org_name, faction, battle_type, t): - last_updated = t - (8 * 3600) - is_finished = 0 - - sql = """ - SELECT - * - FROM - tower_battle - WHERE - playfield_id = ? - AND site_number = ? - AND is_finished = ? - AND def_org_name = ? - AND def_faction = ? - AND last_updated >= ? - """ - - battle = self.db.query_single(sql, [playfield_id, site_number, is_finished, org_name, faction, last_updated]) - - if battle: - self.db.exec("UPDATE tower_battle SET last_updated = ? WHERE id = ?", [t, battle.id]) - return battle - else: - self.db.exec( - "INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, " - "is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)", - [playfield_id, site_number, org_name, faction, is_finished, battle_type, t]) - return self.db.query_single(sql, [playfield_id, site_number, is_finished, org_name, faction, last_updated]) - - def get_last_attack(self, att_faction, att_org_name, def_faction, def_org_name, playfield_id, t): - last_updated = t - (8 * 3600) - is_finished = 0 - - sql = """ - SELECT - b.id AS battle_id, - a.id AS attack_id, - b.playfield_id as pf_id, - b.site_number as site - FROM - tower_battle b - JOIN tower_attacker a ON - a.tower_battle_id = b.id - WHERE - a.att_faction = ? - AND a.att_org_name = ? - AND b.def_faction = ? - AND b.def_org_name = ? - AND b.playfield_id = ? - AND b.is_finished = ? - AND b.last_updated >= ? - ORDER BY - last_updated DESC - LIMIT 1""" - - return self.db.query_single(sql, - [att_faction, att_org_name, def_faction, def_org_name, playfield_id, is_finished, - last_updated]) - - def format_battle_info(self, row, t): - blob = "" - defeated = " - Defeated!" if row.is_finished else "" - blob += f"Site: {row.short_name} {row.site_number or '?'}\n" - blob += f"Defender: {row.def_org_name} ({row.def_faction}){defeated}\n" - blob += f"Last Activity: {self.format_timestamp(row.last_updated, t)}\n" - return blob - - def format_timestamp(self, t, current_t): - return f"{self.util.format_datetime(t)} " \ - f"({self.util.time_to_readable(current_t - t)} ago)" - - def get_chat_command(self, page): - return f"/tell attacks --page={page}" - - def check_for_all_towers_channel(self): - if not self.public_channel_service.get_channel_name(TowerController.ALL_TOWERS_ID): - return "Notice: The bot must belong to an org and be promoted to a rank that is high enough " \ - "to have the All Towers channel (e.g., Squad Commander) in order for the " \ - "attacks command to work correctly.\n\n" - else: - return "" diff --git a/modules/raidbot/tower/tower_hot_controller.py b/modules/raidbot/tower/tower_hot_controller.py deleted file mode 100644 index a0e45a9..0000000 --- a/modules/raidbot/tower/tower_hot_controller.py +++ /dev/null @@ -1,204 +0,0 @@ -from core.aochat.BaseModule import BaseModule -from core.chat_blob import ChatBlob -from core.command_alias_service import CommandAliasService -from core.command_param_types import Options, Int, Any, Const, NamedParameters -from core.db import DB -from core.decorators import instance, command -from core.dict_object import DictObject -from core.event_service import EventService -from core.lookup.pork_service import PorkService -from core.public_channel_service import PublicChannelService -from core.text import Text -from core.igncore import IgnCore -from core.util import Util -from modules.raidbot.tower.tower_service import TowerService -from modules.standard.helpbot.playfield_controller import PlayfieldController - - -@instance() -class TowerHotController(BaseModule): - PAGE_SIZE = 9 - - # noinspection DuplicatedCode - - def inject(self, registry): - self.bot: IgnCore = registry.get_instance("bot") - self.db: DB = registry.get_instance("db") - self.util: Util = registry.get_instance("util") - self.text: Text = registry.get_instance("text") - self.event_service: EventService = registry.get_instance("event_service") - self.pork_service: PorkService = registry.get_instance("pork_service") - self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller") - self.public_channel_service: PublicChannelService = registry.get_instance("public_channel_service") - self.towercache: TowerService = registry.get_instance("tower_service") - self.command_alias_service: CommandAliasService = registry.get_instance("command_alias_service") - - def pre_start(self): - self.command_alias_service.add_alias('towers', "lc") - self.command_alias_service.add_alias('tower', "lc") - - @command(command="hot", - params=[Options(['tl1', 'tl2', 'tl3', 'tl4', 'tl5', 'tl6', 'tl7']), Any('faction', is_optional=True), - NamedParameters(["page"])], - access_level="member", - description="Shows hot playfields") - def hot_tl(self, _, tl, faction: str, named_params): - if faction: - if faction.startswith("--page="): - named_params = DictObject({'page': faction[7:]}) - faction = None - if faction is not None and faction.lower() not in ['omni', 'clan', 'neut', 'neutral']: - return f"Unknown faction: {faction}" - tl = tl[2:] - page = int(named_params.page or "1") - offset = (page - 1) * self.PAGE_SIZE - towers = self.towercache.get_towers_hot_tl(int(tl), faction) - return self.text.format_pagination(towers, offset, page, self.formatter, f"Hot Sites TL{tl} ({len(towers)})", - f"There are no hot sites for TL {tl}.", - f'hot tl{tl} {faction or ""}', 9) - - def formatter(self, row, _, data): - return self.towercache.format_entry(row, len(data)) - - @command(command="hot", - params=[Int('level', is_optional=True), Any('faction', is_optional=True), NamedParameters(["page"])], - access_level="member", - description="Shows hot playfields by level") - def hot_level(self, _, level, faction, named_params): - if faction: - if faction.startswith("--page="): - named_params = DictObject({'page': faction[7:]}) - faction = None - if faction is not None and faction.lower() not in ['omni', 'clan', 'neut', 'neutral']: - return f"Unknown faction: {faction}" - if level: - if level < 0 | level > 220: - return f"Level out of range: {level}" - page = int(named_params.page or "1") - offset = (page - 1) * self.PAGE_SIZE - towers = self.towercache.get_towers_hot_level(level, faction) - level = f"{level}" if level else "" - faction = f"{faction} " if faction else "" - return self.text.format_pagination(towers, offset, page, self.formatter, f"Hot Towersites ({len(towers)})", - f"There are no hot sites.", f'hot {level}{faction}', 9) - - @command(command="free", params=[], - access_level="member", - description="Shows hot playfields by level") - def free(self, _, ): - blob = "" - towers = self.towercache.get_free() - for row in towers: - blob += self.towercache.format_entry(row, len(towers)) - - return ChatBlob(f"FREE Towersites ({len(towers)})", blob) if blob else f"No free towersites found." - - @command(command="lc", params=[], access_level="member", - description="See a list of land control tower sites in a particular playfield") - def lc(self, _): - hot, cold, unplanted = 0, 0, 0 - clan, omni, neut = 0, 0, 0 - last_pf = 0 - previous = {} - blob = "" - - def number(numb): - if numb < 10: - return f"0{numb}" - return numb - - for tower in self.towercache.get_towers_all(): - if tower.id != last_pf: - if last_pf == 0: - previous = tower - last_pf = tower.id - continue - blob += f"{number(hot)} {number(cold)} {number(unplanted)} :: " \ - f"{number(clan)} {number(neut)} {number(omni)} " \ - f"[{self.text.make_tellcmd(previous.short_name, f'lc {previous.long_name}')}] " \ - f"{previous.long_name}\n" - hot, cold, unplanted = 0, 0, 0 - clan, omni, neut = 0, 0, 0 - - previous = tower - last_pf = tower.id - - site = self.towercache.is_hot(tower) - if site == 0: - cold += 1 - elif site == -1: - unplanted += 1 - elif site == 1: - hot += 1 - faction = tower.get('faction', None) - if faction: - faction = faction.lower - if faction == "omni": - omni += 1 - elif faction == "clan": - clan += 1 - else: - neut += 1 - - return ChatBlob('All Tower Sites', blob) - - @command(command="lc", params=[Const("org"), Any("org_name", is_optional=True)], access_level="member", - description="See a list of land control tower sites in a particular playfield") - def lc_org(self, _, _1, org): - towers = [] - try: - org = int(org) - except ValueError: - pass - if type(org) == str: - orgs = self.db.query( - "SELECT * from all_orgs where org_name LIKE ? and org_id in " - "(SELECT org_id from towers group by org_id)", - [f"%{org.replace(' ', '%')}%"]) - if len(orgs) == 1: - towers = self.towercache.get_towers_by_org(orgs[0].org_id) - - elif len(orgs) == 0: - return "Your search returned no orgs." - else: - blob = "Your search had multiple results; please pick an org:
" - for org in orgs: - blob += "[%s] %s (%s) <%s>%s [%s " \ - "members]
" \ - % (self.text.make_chatcmd("Towers", "/tell lc org %s" % org.org_id), - org.org_name, org.org_id, org.faction.lower(), org.faction, org.member_count) - return ChatBlob("Pick an Org", blob) - elif type(org) == int: - if len(self.db.query("SELECT org_id from all_orgs where org_id=?", [int(org)])) == 0: - return "Your search returned no orgs." - else: - towers = self.towercache.get_towers_by_org(org) - - title = f"Towersites of the org {org}" - - blob = "" - for tower in towers: - blob += self.towercache.format_entry(tower, len(towers)) - - return ChatBlob(title, blob) - - @command(command="lc", params=[Any("playfield"), Int("site_number", is_optional=True)], access_level="member", - description="See a list of land control tower sites in a particular playfield") - def lc_playfield(self, _, playfield_name, site_number): - playfield = self.playfield_controller.get_playfield_by_name(playfield_name) - if not playfield: - return "Could not find playfield %s." % playfield_name - if site_number: - title = f"Tower site x{site_number} in {playfield.long_name}" - towers = self.towercache.get_towers_by_pf_site(playfield.id, site_number) - else: - title = f"Tower sites in {playfield.long_name}" - towers = self.towercache.get_towers_by_pf(playfield.id) - - blob = "" - for tower in towers: - blob += self.towercache.format_entry(tower, len(towers)) - if site_number: - blob += "More to come... stay tuned." - - return ChatBlob(title, blob) diff --git a/modules/raidbot/tower/tower_service.py b/modules/raidbot/tower/tower_service.py deleted file mode 100644 index 29793fa..0000000 --- a/modules/raidbot/tower/tower_service.py +++ /dev/null @@ -1,298 +0,0 @@ -import time - -from core.aochat.BaseModule import BaseModule -from core.db import DB -from core.decorators import instance, event, setting -from core.igncore import IgnCore -from core.job_scheduler import JobScheduler -from core.setting_types import BooleanSettingType -from core.text import Text -from core.util import Util -from modules.core.accounting.services.account_service import AccountService -from modules.raidbot.tower.tower_controller import TowerController -from modules.standard.helpbot.playfield_controller import PlayfieldController - - -@instance() -class TowerService(BaseModule): - # 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 = [] - - def inject(self, registry): - self.bot: IgnCore = registry.get_instance("bot") - self.db: DB = registry.get_instance("db") - self.util: Util = registry.get_instance("util") - self.text: Text = registry.get_instance("text") - self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller") - self.job_scheduler: JobScheduler = registry.get_instance("job_scheduler") - self.account_service: AccountService = registry.get_instance("account_service") - - def pre_start(self): - self.db.shared.exec("CREATE TABLE IF NOT EXISTS towers(" - "pf_id int not null, " - "site_number int not null," - "ql int," - "x_coord int not null," - "y_coord int not null," - "org_id int," - "org_name varchar(255), " - "faction varchar(32), " - "close_time int," - "planted int," - "enabled tinyint, " - "PRIMARY KEY (pf_id, site_number), " - "INDEX ql(ql), INDEX close(close_time), " - "INDEX planted(planted), INDEX enabled(enabled)) ENGINE MEMORY") - self.db.create_view("towers") - - @event(event_type=TowerController.TOWER_ATTACK_EVENT, description="Track planthot", is_hidden=True) - def tower_attack(self, _, event_data): - if event_data.attacker.get("org_id", None): - self.attack_hot.append({'org_name': event_data.attacker.org_name, 'hot': time.time() + 60 * 60}) - - @setting(name="tower_notify_type", value=False, description="Only notify when our orgs are involved") - def tower_notify_type(self) -> BooleanSettingType: - return BooleanSettingType() - - @event(event_type=TowerController.TOWER_VICTORY_EVENT, description="Send NW warnings") - def victory(self, _, event_data): - t = int(time.time()) - if event_data.type == "attack": - if self.tower_notify_type().get_value(): - if not (event_data.winner.get("org_name", None) in self.account_service.get_org_names() - or event_data.loser.org_name in self.account_service.get_org_names()): - return - row = self.get_last_attack(event_data.winner.faction, event_data.winner.org_name, event_data.loser.faction, - event_data.loser.org_name, event_data.location.playfield.id, t) - self.send_nw_warn(0, - f'<{event_data.loser.faction.lower()}>' - f'{event_data.loser.org_name}' - f'' - f' Lost their Site at {event_data.location.playfield.short_name} x{row.site} - ' - f'<{event_data.winner.faction.lower()}>' - f'{event_data.winner.org_name}' - f' won!!') - self.plant.append({'pf': row.pf_id, 'site': row.site, 'plant': time.time() + 20 * 60 - 1}) - self.prepare_nw_warn(row.pf_id, row.site) - - elif event_data.type == "terminated": - # DEBUG: terminated sites.. behave strange. - # for that reason, we'll just output these events to the console, but not the log. - print(event_data) - field = self.db.query("SELECT * FROM towers t where t.org_name=? and t.pf_id=?", - [event_data.loser.org_name, event_data.playfield.id]) - if len(field) == 1: - field = field[0] - self.plant.append({'pf': event_data.location.playfield.id, - 'site': field.site_number, - 'plant': time.time() + 20 * 60 - 1}) - self.send_nw_warn(0, f'<{event_data.loser.faction.lower()}>' - f'{event_data.loser.org_name}' - f' Lost their Site at ' - f'{event_data.location.playfield.short_name} x{field.site_number}') - self.prepare_nw_warn(event_data.location.playfield.id, field.site_number) - return - self.plant.append({'pf': event_data.location.playfield.id, - 'site': f"(UKN) PO: {event_data.loser.org_name}|{event_data.loser.faction}", - 'plant': time.time() + 20 * 60 - 1}) - - self.send_nw_warn(0, - f'<{event_data.loser.faction.lower()}>' - f'{event_data.loser.org_name}' - f' ' - f'Lost their Site in {event_data.location.playfield.long_name}') - self.prepare_nw_warn(event_data.playfield.id, "(UKN)", - f"(UKN) PO: {event_data.loser.org_name}|{event_data.loser.faction}") - - def day_time(self, day_t): - if day_t > 86400: - day_t -= 86400 - elif day_t < 0: - day_t += 86400 - return day_t - - def get(self, where_order="", param=None): - if param is None: - param = [] - return self.db.query(f"SELECT a.org_id, a.planted, a.close_time, a.ql, a.org_name, a.faction, " - f"b.min_ql, b.max_ql, b.site_name, b.site_number, b.x_coord, b.y_coord, " - f"c.id, c.long_name, c.short_name, " - f"e.pvp_min, e.pvp_max " - f"FROM towers a " - f"INNER JOIN tower_site b ON a.site_number=b.site_number " - f"LEFT JOIN playfields c ON a.pf_id = c.id " - f"LEFT JOIN level e ON e.level = a.ql " - f"WHERE a.pf_id = b.playfield_id and enabled = 1 " - f"{where_order}", param) - - def get_towers_by_tl(self, tl, faction=None): - min_ql, max_ql = self.util.get_level_range_tl(tl) - if faction: - return self.get("and ql between ? and ? and faction LIKE ? order by c.short_name", - [min_ql, max_ql, faction]) - return self.get("and ql between ? and ? order by c.short_name", [min_ql, max_ql]) - - def get_towers_all(self): - return self.get("order by c.long_name", []) - - def get_towers_by_pf(self, pf): - return self.get("and a.pf_id=? order by a.site_number", [pf]) - - def get_towers_by_pf_site(self, pf, site): - return self.get("and a.pf_id=? and a.site_number=?", [pf, site]) - - def get_towers_by_org(self, org): - return self.get("and a.org_id" - "=? order by c.long_name", [org]) - - def get_towers_by_org_name(self, org): - return self.get("and a.org_name=? order by c.long_name", [org]) - - def get_free(self): - return self.get("and a.org_id IS NULL order by a.site_number", []) - - def get_towers_hot_tl(self, tl, faction=None): - towers = self.get_towers_by_tl(tl, faction) - out = [] - - for tower in towers: - if self.is_hot(tower) in [1, 2]: - out.append(tower) - return out - - def get_towers_hot_level(self, level, faction=None): - out = [] - - if level: - if faction: - towers = self.get("and pvp_min <= ? and pvp_max >= ? and faction LIKE ?", [level, level, faction]) - else: - towers = self.get("and pvp_min <= ? and pvp_max >= ?", [level, level]) - - else: - if faction: - towers = self.get("and faction LIKE ?", [faction]) - else: - towers = self.get() - - for tower in towers: - if self.is_hot(tower) in [1, 2]: - out.append(tower) - return out - - def format_entry(self, entry, _): - h3 = "" - now = self.day_time(int(time.time()) % 86400) - row0 = f"Site: {entry.short_name} x{entry.site_number} " \ - f"[R:{entry.min_ql} - {entry.max_ql}] " \ - f"[{self.text.make_tellcmd('More', f'lc {entry.short_name} {entry.site_number}')}]\n" - row1 = f'UKN :: No Owner -> Unplanted\n' - row2 = "" - row3 = "\n" - hot = self.is_hot(entry) - if hot != -1: - h1 = "COLD" - if hot == 2: - h1 = "WARHOT" - for org in self.attack_hot: - if org['org_name'] == entry.org_name: - hot_normal = self.is_hot(entry, False) - # print(hot, hot_normal, org['hot'] - now) - if hot_normal == 1: - h3 = f"COLD in " \ - f"{self.util.format_time(self.day_time(int(entry.close_time - now)))}" - if hot_normal == 0: - h3 = f"COLD in {self.util.format_time(org['hot'] - now)}" - - elif hot == 1: - h1 = 'HOT' - h3 = f"COLD in {self.util.format_time(self.day_time(int(entry.close_time - now)))}" - else: - hot_time = self.day_time(entry.close_time - 6 * 60 * 60) - h3 = f"HOT in " \ - f"{self.util.format_time((18 * 80 * 60 - hot_time) if hot_time > 18 * 60 * 60 else hot_time)}" - org = f"<{entry.faction.lower()}>{entry.org_name} " \ - f"[{self.text.make_tellcmd('View org', f'lc org {entry.org_name}')}]" - row1 = f"{h1} :: {h3} :: {org} :: \n" - row3 = f" » Planted: {self.util.format_datetime(entry.planted)}
\n\n" - if entry.pvp_min: - pvp = f"[{entry.pvp_min} - {entry.pvp_max}]" - else: - pvp = f"[175 - 220]" - # noinspection LongLine - row2 = f" » QL: {entry.ql} PvP: {pvp} " \ - f"[{self.text.make_chatcmd(f'{entry.x_coord} x {entry.y_coord}', f'/waypoint {entry.x_coord} {entry.y_coord} {entry.id}')}]\n" - return row0 + row1 + row2 + row3 + "" - - def is_hot(self, entry, with_war=True) -> int: - if entry.get("close_time", None): - now = self.day_time((time.time()) % 86400) - self.attack_hot.sort(key=lambda k: k['org_name']) - rem = [] - inside = False - for index, i in enumerate(self.attack_hot): - if i['hot'] < time.time(): - rem.append(index) - continue - if i['org_name'] == entry.org_name: - inside = True - for index in reversed(rem): - self.attack_hot.pop(index) - if inside and with_war: - return 2 - if self.day_time(entry.close_time - int(now)) > 6 * 60 * 60: - return 0 - return 1 - return -1 - - def get_last_attack(self, att_faction, att_org_name, def_faction, def_org_name, playfield_id, t): - last_updated = t - (8 * 3600) - is_finished = 1 - - sql = """ - SELECT - b.id AS battle_id, - a.id AS attack_id, - b.playfield_id as pf_id, - b.site_number as site - FROM - tower_battle b - JOIN tower_attacker a ON - a.tower_battle_id = b.id - WHERE - a.att_faction = ? - AND a.att_org_name = ? - AND b.def_faction = ? - AND b.def_org_name = ? - AND b.playfield_id = ? - AND b.is_finished = ? - AND b.last_updated >= ? - ORDER BY - last_updated DESC - LIMIT 1""" - - return self.db.query_single(sql, - [att_faction, att_org_name, def_faction, def_org_name, playfield_id, is_finished, - last_updated]) - - def prepare_nw_warn(self, pf_id, site, bonus=""): - pf = self.playfield_controller.get_playfield_by_id(pf_id) - site = f"{pf.short_name} » x{site}" + bonus - self.job_scheduler.delayed_job(self.send_nw_warn, 0, f"{site} plantable in 20 minutes!") - self.job_scheduler.delayed_job(self.send_nw_warn, 10 * 60 - 1, f"{site} plantable in 10 minutes!") - self.job_scheduler.delayed_job(self.send_nw_warn, 15 * 60 - 1, f"{site} plantable in 5 minutes!") - self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 - 1, f"{site} plantable in 1 minute!") - self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 + 30 - 1, f"{site} plantable in 30 seconds!") - self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 + 45 - 1, f"{site} plantable in 15 seconds!") - self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 + 50 - 1, f"{site} plantable in 10 seconds!") - self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 + 55 - 1, f"{site} plantable in 5 seconds!") - self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 + 56 - 1, f"{site} plantable in 4 seconds!") - self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 + 57 - 1, f"{site} plantable in 3 seconds!") - self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 + 58 - 1, f"{site} plantable in 2 seconds!") - self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 + 59 - 1, f"{site} plantable in 1 second!") - self.job_scheduler.delayed_job(self.send_nw_warn, 20 * 60 - 1, f"{site} plantable NOW!") - - def send_nw_warn(self, _, msg): - self.bot.send_private_channel_message(f"[NW] {msg}") diff --git a/modules/raidbot/tower/tower_site.sql b/modules/raidbot/tower/tower_site.sql deleted file mode 100644 index 1ecae8b..0000000 --- a/modules/raidbot/tower/tower_site.sql +++ /dev/null @@ -1,5 +0,0 @@ -# noinspection LongLineForFile - -DROP TABLE IF EXISTS tower_site; -CREATE TABLE tower_site (playfield_id INT NOT NULL, site_number SMALLINT NOT NULL, min_ql SMALLINT NOT NULL, max_ql SMALLINT NOT NULL, x_coord SMALLINT NOT NULL, y_coord SMALLINT NOT NULL, site_name varchar(50) NOT NULL, PRIMARY KEY (playfield_id, site_number)); -INSERT INTO tower_site (playfield_id, site_number, min_ql, max_ql, x_coord, y_coord, site_name) VALUES(505,1,60,90,2740,4260,'Griffon Frontier'), (505,2,80,110,540,4180,'Draught'), (505,3,70,95,1740,3460,'Dreadfire Volcano'), (505,4,80,120,2780,3420,'Northeast Barren Lands'), (505,5,60,90,580,3140,'Western Desert'), (505,6,50,75,2420,1900,'Waylander Mines'), (505,7,70,100,1860,1700,'North of Main Omni Base'), (505,8,61,82,460,1380,'Dome Ore'), (505,9,100,150,2700,620,'Crystal Forge Volcano'), (505,10,100,150,660,460,'SW Low Plateau'), (550,1,10,20,2660,2020,'Sifter Beach'), (550,2,20,30,1780,1780,'Academy Ore'), (550,3,15,25,1980,1340,'Athen Fault'), (550,4,10,20,2660,820,'Grindmoore'), (550,5,15,23,1380,380,'Gladius Grove'), (551,1,40,90,1700,3700,'Styx Magma'), (551,2,35,50,2220,3340,'Carbon Grove'), (551,3,26,50,980,3140,'Between the Craters'), (551,4,25,35,340,2420,'Powdered Dunes'), (551,5,32,45,2540,2060,'Dust Bank'), (551,6,20,30,580,1740,'Charred Groove'), (551,7,12,45,940,1540,'West of Perdition'), (551,8,15,30,660,900,'North of Yuttos'), (560,1,100,170,1500,3420,'Terraform Edge'), (560,2,170,250,3060,3020,'West Spirals'), (560,3,170,250,3500,2980,'East Spirals'), (560,4,130,170,1220,2220,'Middle Mort Desert'), (560,5,1,100,900,1460,'Green Crater'), (560,6,110,160,3100,1460,'Oasis Ore'), (560,7,150,200,2740,700,'South East Craterwall'), (560,8,100,150,540,540,'South West Craterwall'), (560,9,160,210,2780,540,'Stormshelter'), (565,1,25,40,2940,2900,'Rich Desert Ridge'), (565,2,30,45,1980,2580,'East of Meetmedere'), (565,3,50,75,540,2020,'Middle of Western Desert'), (565,4,40,60,2580,1940,'North of Rhino Village'), (565,5,40,60,2700,1260,'South of Rhino Village'), (567,1,12,20,1220,1060,'In the Newland Desert'), (567,2,15,25,540,460,'West of Newland Lake'), (570,1,200,300,3220,3020,'North of Cyborg Hideout'), (570,2,191,250,3780,2540,'Middle of Liberty'), (570,3,120,180,980,2060,'South of Sabulum'), (570,4,190,230,3940,2060,'Cyborg Border'), (570,5,200,300,2820,1820,'Middle of Perpetual Wastelands'), (570,6,200,300,3740,1700,'South of Cyborg Hideout'), (570,7,100,150,1500,1340,'Lower Plateu Zone'), (570,8,100,150,2100,1380,'The Mid Canyon Crossing'), (570,9,120,180,3020,1220,'Plains of dust'), (570,10,100,150,900,1060,'West of Canyon'), (570,11,100,150,3180,940,'The Canyon Mines'), (570,12,190,230,2300,780,'South of Canyon'), (585,1,40,60,1220,2740,'Northern Wastelands'), (585,2,11,16,2180,2580,'West Wastelands'), (585,3,40,55,1020,2460,'Mid Wastelands'), (585,4,30,45,2140,1660,'Giant Green River Bank North'), (585,5,11,16,1180,1340,'West of the Dead Forest'), (585,6,30,45,2100,1340,'Giant Green River Bank South'), (585,7,15,22,1420,1020,'Canyon East'), (585,8,25,35,820,780,'Canyon South'), (585,9,35,50,900,460,'By the River'), (590,1,140,200,1740,3100,'By the Fisher Village'), (590,2,140,200,2100,3060,'Fisher Village Approach'), (590,3,100,170,2900,2820,'North Forest'), (590,4,90,130,3340,2700,'North-east Forest'), (590,5,130,170,860,1220,'North-west of Lava Ditches'), (590,6,100,150,3100,980,'Mid Clutching Forest'), (590,7,130,170,860,780,'South-west of Lava Ditches'), (590,8,100,150,3180,620,'South Clutching Forest'), (595,1,100,150,1140,3380,'Old ruins'), (595,2,100,150,3180,2900,'Plains of defense'), (595,3,130,180,1740,2300,'The haunted forest outskirt'), (595,4,130,180,900,2220,'Forest of Xzawkaz'), (595,5,200,300,2260,1860,'In the Swamp of Horrors'), (595,6,130,180,1420,1500,'Island of Control'), (595,7,130,180,1340,1140,'The swamp of hope'), (595,8,200,300,2900,1100,'South of the Medusa'), (595,9,140,210,2140,780,'Middle of the Foul Forest'), (595,10,200,300,540,540,'Southern Forest of Xzawkaz'), (600,1,30,45,2420,2980,'By the Rivers Edge'), (600,2,50,75,620,2900,'North Forest Road'), (600,3,25,50,1300,2660,'Along the Rivers Edge'), (600,4,30,45,3740,2500,'East Forest'), (600,5,25,50,3140,2020,'Rhino Hills'), (600,6,50,75,580,1700,'West Forest'), (600,7,60,90,1940,1620,'Crossroads'), (600,8,60,90,1140,1500,'Forestdawn'), (600,9,50,75,3220,1140,'East of Crater'), (605,1,160,200,2940,2820,'Forest Waters'), (605,2,110,120,1100,2620,'Muddy Pools'), (605,3,100,150,1700,2300,'West of Wine'), (605,4,120,180,2940,2260,'East of Wine'), (605,5,130,195,1900,1740,'Central Belial Forest'), (605,6,130,190,2500,1660,'River Delta'), (605,7,160,200,2540,1220,'Junction Forest'), (605,8,100,150,2340,860,'Borderline'), (605,9,120,180,2020,420,'Southern belial Mine'), (605,10,140,200,620,380,'Southwest Belial Mining District'), (610,1,60,90,1380,2780,'Tetlies Land control area'), (610,2,80,120,2900,2660,'East of the Great Marsh'), (610,3,60,90,660,2460,'West of outpost 10-3'), (610,4,100,150,2300,2020,'Defense of Geholva'), (610,5,106,143,2740,1180,'South of Forest of Geholva'), (610,6,120,180,860,900,'Avid Crater'), (610,7,120,180,1540,900,'East of Avid Crater'), (610,8,100,150,2460,540,'Bendelham forest Defense'), (615,1,60,100,1900,3020,'North of Lenne'), (615,2,100,150,860,2820,'Little Hawaii Defense'), (615,3,90,120,2620,2660,'Defense of Zoto'), (615,4,60,100,900,2100,'By the Ocean'), (615,5,61,100,2300,1180,'Birm'), (615,6,120,180,2700,660,'SFH Defense'), (615,7,100,150,1860,500,'South in Nightplain'), (620,1,150,200,2700,3860,'Krud the Lost Valley Defense'), (620,2,150,225,1900,3180,'Pranade'), (620,3,120,180,620,2980,'Plains of Jarga Defense'), (620,4,200,300,2460,2260,'Old Plains'), (620,5,200,300,1540,1780,'Middle of Easter Fouls Plains'), (620,6,130,200,1540,1140,'Clefre Defense'), (620,7,100,150,2020,860,'Central Sharewood'), (620,8,200,300,820,540,'Pegradul'), (625,1,90,130,1460,1940,'The Resilient Forest - North'), (625,2,90,120,1900,1540,'The Resilient Forest - East'), (625,3,125,170,2780,1380,'Central Prowler Waste'), (625,4,100,125,1380,1180,'Central Resilient Forest'), (625,5,125,170,2860,1020,'Southern Prowler Waste'), (625,6,100,150,4020,980,'The Barren Hills'), (625,7,100,125,1740,860,'The Resilient Forest - South'), (625,8,50,75,2460,540,'The Silent Woods - East'), (630,1,40,60,1540,2660,'Pleasant Range Offense Hill'), (630,2,60,90,2380,2500,'Central Pleasant Range'), (630,3,50,75,580,2420,'West of 20K'), (630,4,30,70,3220,2220,'Pleasant Range Defense'), (630,5,60,90,3220,1980,'Pleasant River Defense'), (630,6,60,90,3260,1500,'Pleasant River Offense'), (630,7,40,60,2260,1140,'Central Pleasant Plains'), (630,8,30,70,3020,1020,'East Pleasant Plains'), (630,9,30,45,740,460,'West of Versailles Tower'), (635,1,55,70,700,2420,'Northern River Bank'), (635,2,60,90,1780,2460,'Hawker Trench'), (635,3,70,105,1460,1740,'Klapam Forest Defense'), (635,4,55,70,2020,1740,'Klompfot Defense'), (635,5,70,105,1900,1220,'South of Trench'), (635,6,80,120,1140,940,'Nile Hills'), (635,7,55,70,1780,700,'Aprils Rock Offense'), (635,8,80,150,820,420,'Southern Lower River Bank'), (635,9,80,150,1700,340,'Aprils Rock Defense'), (646,1,10,15,460,1300,'Great W. Forest Vein'), (646,2,10,15,2940,980,'The Hidden Notum Canal'), (646,3,20,30,3220,620,'Mountain Areas'), (646,4,10,15,580,580,'Great W. Forest Dorsal'), (646,5,10,15,1500,460,'Western Mountain Areas'), (647,1,90,135,1100,3100,'The Mineral Mine'), (647,2,20,30,2900,2940,'NE Desert Aperient'), (647,3,37,64,1900,2700,'SurroundingTemple of Three Winds'), (647,4,25,40,2220,1900,'Piercing Thundertube'), (647,5,30,45,2820,1940,'Central Striking Ant'), (647,6,25,40,620,1660,'Tir Prairie'), (647,7,25,40,1180,1700,'Crater Swamp'), (650,1,50,75,540,2820,'West Pass'), (650,2,65,75,900,2300,'Crowning Shallows'), (650,3,100,150,1660,2180,'Haven Notum Crematorium'), (650,4,70,140,2020,1740,'Stret Vale Deux Drilling Field'), (650,5,120,180,1340,1620,'The Flooded Bottomland'), (650,6,75,90,1820,740,'Stret Woods'), (650,7,60,90,940,420,'Greenslopes'), (655,1,30,45,420,2700,'Skop Notum Mine'), (655,2,30,80,2820,2340,'Klor'), (655,3,60,80,2820,1660,'Harstad'), (655,4,40,90,540,1580,'Ubleo'), (655,5,40,60,1420,1580,'Flubu Notum Mine'), (655,6,40,70,4340,900,'Plago'), (655,7,60,80,2260,380,'jucha'), (655,8,70,105,4380,380,'Mune'), (655,9,30,60,820,340,'Mocnuf Notum Mine'), (665,1,80,150,940,4820,'Central Desert north'), (665,2,45,75,1260,3860,'Notum Disruption Mountain'), (665,3,75,110,1940,3860,'The Notum Plains'), (665,4,100,150,940,3380,'Near Clan Outpost'), (665,5,45,80,1300,3060,'Central Mountains'), (665,6,55,150,380,2300,'Surrounding Evil'), (665,7,45,60,1260,2140,'Notum Mountain'), (665,8,55,100,2020,1980,'Near Omni-Tek Outpost'), (665,9,100,150,420,820,'Shores Notum Vein'), (670,1,30,45,1100,4340,'Yukon Source'), (670,2,35,50,1460,2540,'Frisko'), (670,3,30,45,2140,2420,'Round Hills'), (670,4,50,75,2140,1900,'Dense Drewen'), (670,5,35,50,1260,1820,'Borrowed Hill'), (670,6,35,50,1340,1340,'Narrow Lune'), (670,7,10,15,2500,1220,'Micron Slopes Notum Mine'), (670,8,50,75,2100,540,'High Juniper'), (670,9,50,75,2300,460,'High Juniper Notum Vein'), (685,1,35,50,2140,2620,'Nature Reverve - East'), (685,2,35,50,1900,2580,'Nature Reverve - West'), (685,3,50,75,1300,1900,'Poole - West'), (685,4,50,75,1580,1820,'Poole - East'), (685,5,15,25,1140,1100,'V-Hill'), (685,6,20,30,1580,700,'Lunder Hills - North'), (685,7,25,40,2740,460,'Galway hills'), (685,8,20,30,1220,380,'Lunder Hills'), (685,9,25,40,2260,380,'South-east Woods'), (687,1,10,15,500,1900,'Blossom Valley'), (687,2,10,15,380,1300,'Konty Passage Plains'), (687,3,17,28,900,1220,'Vas'' Pass'), (687,4,15,25,780,900,'Arthur''s Pass'), (687,5,10,15,380,580,'Kontys Sixth Passage - West'), (687,6,10,15,620,540,'Kontys Sixth Passage - East'), (695,1,30,45,940,3260,'North West Lush Fields'), (695,2,20,30,2420,3180,'North East Lush Fields'), (695,3,10,40,3460,2940,'Stret River Island'), (695,4,40,60,1260,2460,'West of Outpost'), (695,5,35,60,1740,2460,'East of Outpost'), (695,6,20,30,1780,1820,'Central Lush Fields'), (695,7,10,15,2860,420,'South East Lush Fields'), (695,8,30,45,980,380,'South West Lush Fields'), (696,1,15,25,780,1420,'Mutant Domain North'), (696,2,20,30,500,860,'Mutant Domain Central'), (696,3,25,40,780,460,'Mutant Domain South'), (716,1,20,35,500,3220,'Northern Grassland'), (716,2,15,30,980,3020,'Moderate Grassland'), (716,3,10,20,460,2180,'Dungeon Hilltop'), (716,4,10,15,700,2180,'Rocky Upsurge'), (716,5,15,25,340,1420,'Northern Easy Swamps Notum Field'), (716,6,15,26,460,820,'Ocean Inlet'), (717,1,30,45,1620,2660,'Greater Omni Forest Swamps'), (717,2,15,25,1180,2460,'Dragonback Ridge'), (717,3,30,45,1900,1820,'Mountainous Regions'), (717,4,20,35,1860,1340,'Waterfall Swamp'), (717,5,10,15,1500,1300,'Greater Omni Forest South'), (717,6,25,40,900,1220,'Northern Semi-Barren Area'), (717,7,10,25,1940,900,'Ring Mountain Range'), (717,8,14,25,940,460,'Southern Isle'), (760,1,60,90,1580,2380,'Notum Ore in Buttu'), (760,2,35,50,940,2020,'Mountain of Fourtyone'), (760,3,35,50,1300,1980,'Mountain in 4Holes'), (760,4,45,60,1660,1740,'South of Ahenus'), (760,5,70,100,1820,1340,'Ibreri Woods North'), (760,6,35,50,1460,1260,'Mountain of Fourtytwo'), (760,7,100,150,1740,1060,'Ibreri Woods'), (760,8,45,70,1180,500,'Ibreri'), (760,9,45,70,460,420,'Jall Mountain'), (790,1,20,30,1700,3100,'Hells Courtyard'), (790,2,15,25,2300,2860,'Pondus Beach'), (790,3,15,30,1700,2780,'Hound Land'), (790,4,20,40,1980,2780,'Hound Notum Field'), (790,5,20,30,1700,1940,'East Mutie'), (790,6,12,30,1340,1220,'Omni Outpost'), (790,7,20,30,660,1180,'South Mutie'), (790,8,30,60,2260,1140,'The Beach'), (791,1,15,26,420,2020,'Populous Mountain'), (791,2,12,22,660,1500,'Hound Land Mining'), (791,3,12,20,220,1060,'Stret West Notum Ore'), (791,4,10,15,740,820,'Snake Mountain'), (791,5,20,40,780,460,'Southern Empty Wastes and Roads'), (791,6,10,20,380,340,'Transit Valley Ore'), (795,1,40,60,4220,1580,'Illuminati'), (795,2,100,150,500,1540,'Northern Forest of Illuminations'), (795,3,25,50,3420,1540,'Fate Notum Field'), (795,4,71,120,580,820,'Pegrama'), (795,5,84,120,1220,700,'Grazeland Notum Field'), (795,6,50,75,4020,620,'Winterbottom'), (795,7,90,120,540,500,'Southern Forest of Illuminations'), (795,8,60,90,2900,500,'Summer'); \ No newline at end of file diff --git a/modules/standard/datanet/ws_controller.py b/modules/standard/datanet/ws_controller.py index 826beb1..51ce1da 100644 --- a/modules/standard/datanet/ws_controller.py +++ b/modules/standard/datanet/ws_controller.py @@ -6,7 +6,7 @@ from core.decorators import instance, timerevent from core.event_service import EventService from core.logger import Logger from core.setting_service import SettingService -from core.setting_types import TextSettingType +from core.setting_types import TextSettingType, HiddenSettingType from modules.standard.datanet.ws_worker import WebsocketRelayWorker @@ -37,10 +37,16 @@ class WebsocketRelayController(BaseModule): def pre_start(self): self.event_service.register_event_type(self.WS_RELAY) self.setting_service.register(self.module_name, - 'relay_address', - 'ws://localhost:25500', + 'relay_address', + 'ws://localhost:25500', TextSettingType([], allow_empty=True), - "relay for timers, tower info, ...") + "relay for timers, tower info, ...") + + self.setting_service.register(self.module_name, + 'datanet_auth', + '', + HiddenSettingType([], allow_empty=True), + "Auth Challenge response") @timerevent(budatime="1s", description="Relay messages from Data relay to the internal message hub", is_hidden=True) @@ -48,8 +54,6 @@ class WebsocketRelayController(BaseModule): while self.queue: obj = self.queue.pop(0) self.event_service.fire_event(self.WS_RELAY, obj) - if obj.type == "connected": - self.send_relay_message('join', f"{self.bot.name}") @timerevent(budatime="1m", description="Ensure the bot is connected to Data relay", is_hidden=True, run_at_startup=True) @@ -68,7 +72,8 @@ class WebsocketRelayController(BaseModule): def connect(self): self.disconnect() - self.worker = WebsocketRelayWorker(self.queue, self.setting_service.get_value("relay_address"), False) + self.worker = WebsocketRelayWorker(self.queue, self.setting_service.get_value("relay_address"), + self.setting_service.get_value("datanet_auth")) self.dthread = threading.Thread(target=self.worker.run, daemon=True) self.dthread.start() diff --git a/modules/standard/datanet/ws_worker.py b/modules/standard/datanet/ws_worker.py index 02cb931..2c5ba49 100644 --- a/modules/standard/datanet/ws_worker.py +++ b/modules/standard/datanet/ws_worker.py @@ -1,16 +1,17 @@ import json -from websocket import create_connection +from websocket import create_connection, WebSocketConnectionClosedException from core.dict_object import DictObject from core.logger import Logger class WebsocketRelayWorker: - def __init__(self, inbound_queue, url, proxy): + def __init__(self, inbound_queue, url, auth): self.logger = Logger(__name__) self.inbound_queue = inbound_queue self.url = url + self.auth = auth self.ws = None def run(self): @@ -21,13 +22,19 @@ class WebsocketRelayWorker: result = self.ws.recv() while result: - obj = DictObject(json.loads(result)) - self.inbound_queue.append(obj) + if result == "#auth": + self.ws.send(self.auth) + else: + obj = DictObject(json.loads(result)) + self.inbound_queue.append(obj) result = self.ws.recv() - self.ws.close() except ConnectionRefusedError: pass + except ConnectionResetError: + pass + except WebSocketConnectionClosedException: + pass def send_message(self, message): if self.ws: diff --git a/modules/standard/helpbot/playfield_controller.py b/modules/standard/helpbot/playfield_controller.py index 36c3bc2..6ba1d9e 100644 --- a/modules/standard/helpbot/playfield_controller.py +++ b/modules/standard/helpbot/playfield_controller.py @@ -29,7 +29,7 @@ class PlayfieldController: blob = "" for row in data: - blob += "[%d] %s (%s)\n" % (row.id, row.long_name, row.short_name) + blob += f"[{row.id:d}] {row.long_name} ({row.short_name})\n" return ChatBlob("Playfields", blob) @@ -86,6 +86,13 @@ class PlayfieldController: "OR short_name LIKE ? " "LIMIT 1", [name, name]) + def get_playfield_by_name_or_id(self, name): + return self.db.query_single("SELECT * FROM playfields " + "WHERE long_name LIKE ? " + "OR short_name LIKE ? " + "OR id LIKE ? " + "LIMIT 1", [name, name, name]) + def get_playfield_by_id(self, playfield_id): return self.db.query_single("SELECT * FROM playfields " "WHERE id = ?", [playfield_id]) diff --git a/modules/standard/items/items_controller.py b/modules/standard/items/items_controller.py index 86c6b87..5f1a2f8 100644 --- a/modules/standard/items/items_controller.py +++ b/modules/standard/items/items_controller.py @@ -67,20 +67,19 @@ class ItemsController: blob += f"Search: QL {ql:d} {search}\n" else: blob += f"Search: {search}\n" - blob += "\n" - + head_foot = "" if page > 1: - blob += " " + self.text.make_chatcmd(f"<< Page {page - 1:d}", - self.get_chat_command(ql, search, page - 1)) + head_foot += " " + self.text.make_chatcmd(f"«« Page {page - 1:d}", + self.get_chat_command(ql, search, page - 1)) if offset + self.PAGE_SIZE < len(all_items): - blob += " Page " + str(page) - blob += " " + self.text.make_chatcmd(f"Page {page + 1:d} >>", - self.get_chat_command(ql, search, page + 1)) + head_foot += " Page " + str(page) + head_foot += " " + self.text.make_chatcmd(f"Page {page + 1:d} »»", + self.get_chat_command(ql, search, page + 1)) if self.PAGE_SIZE < len(all_items): blob += "\n" - blob += "\n" - + blob += head_foot + "\n\n" blob += self.format_items(items, ql) + blob += head_foot # noinspection LongLine blob += f"\nItem DB rips created using the {self.text.make_chatcmd('Budabot Items Extractor', '/start https://github.com/Budabot/ItemsExtractor')} tool." @@ -102,7 +101,7 @@ class ItemsController: msg = "" msg += item_group[0].name - for item in reversed(item_group): + for item in item_group: if ql: if item.lowql != item.highql: msg += f" {self.text.make_item(item.lowid, item.highid, ql, ql)}" @@ -134,7 +133,7 @@ class ItemsController: params.append(ql) params.append(ql) - sql += " ORDER BY name ASC, highql DESC" + sql += " ORDER BY name, highid, highql DESC" return self.db.query(sql, params, extended_like=True) @@ -223,10 +222,10 @@ class ItemIter: item = self.items[self.current_index] if item.name != current_item.name \ or item.icon != current_item.icon \ - or item.highql == current_item.highql: + or item.highql == current_item.highql \ + or item.highql < current_item.highql: break current_item = item grouped.append(item) self.current_index += 1 - return grouped diff --git a/modules/standard/loot/loot_controller.py b/modules/standard/loot/loot_controller.py index 250483c..52ea7bb 100644 --- a/modules/standard/loot/loot_controller.py +++ b/modules/standard/loot/loot_controller.py @@ -8,9 +8,9 @@ from core.command_alias_service import CommandAliasService from core.command_param_types import Const, Int, Any from core.db import DB from core.decorators import instance, command, timerevent +from core.igncore import IgnCore from core.setting_service import SettingService from core.text import Text -from core.igncore import IgnCore from modules.core.accounting.services.account_service import AccountService from modules.raidbot.raid.raidbot_controller import Raider from modules.standard.items.items_controller import ItemsController @@ -336,9 +336,16 @@ class LootController: loot += item self.add_item_to_loot(item) else: - loot += item - self.add_item_to_loot(item, item_count=item_count) + out = re.match(r"(([^<]+)?([^<]+)([^<]+)?)", item) + if out: + # print(out.groups()) + loot += item + item = self.text.make_item(int(out[3]), int(out[4]), int(out[5]), out[6]) + self.add_item_to_loot((out[2] or "") + item + (out[7] or ""), item_count=item_count) + else: + loot += item + self.add_item_to_loot(item, item_count=item_count) self.bot.send_private_channel_message(f"{loot} was added to loot list.") @timerevent(budatime="1h", diff --git a/modules/standard/news/news_controller.py b/modules/standard/news/news_controller.py index 9f354c8..16afcfb 100644 --- a/modules/standard/news/news_controller.py +++ b/modules/standard/news/news_controller.py @@ -8,14 +8,15 @@ from core.buddy_service import BuddyService from core.chat_blob import ChatBlob from core.command_param_types import Options, Int, Const, Any from core.db import DB -from core.decorators import instance, event, command +from core.decorators import instance, event, command, setting from core.dict_object import DictObject +from core.igncore import IgnCore from core.job_scheduler import JobScheduler from core.logger import Logger from core.lookup.pork_service import PorkService from core.setting_service import SettingService +from core.setting_types import BooleanSettingType from core.text import Text -from core.igncore import IgnCore from core.util import Util from modules.core.accounting.preference_controller import PreferenceController from modules.core.accounting.services.account_service import AccountService @@ -76,6 +77,7 @@ hh:mm - DD.MM.YYYY "added_by int not null, " "added_at timestamp, " "headline int not null default 0)") + self.db.exec("CREATE TABLE IF NOT EXISTS news_read(main INT NOT NULL PRIMARY KEY, post_id INT NOT NULL)") def start(self): self.commands = f" [{self.text.make_chatcmd('raids', '/tell raids')}] " \ @@ -89,7 +91,8 @@ hh:mm - DD.MM.YYYY def logon_event(self, _, data): if not self.bot.is_ready(): return - if "member" in self.buddy_service.get_buddy(data.packet.char_id)["types"]: + if "member" in self.buddy_service.get_buddy(data.packet.char_id)["types"] \ + or "org_member" in self.buddy_service.get_buddy(data.packet.char_id)["types"]: account = data.account # Apply standard checks. (User Banned, Account disabled, ...) if not self.account_service.simple_checks(data.account): @@ -101,18 +104,21 @@ hh:mm - DD.MM.YYYY discord = f"Your Account is not connected to Discord " \ f"[{self.text.make_chatcmd('Join', '/tell discord invite')}]" else: + prefix = "" + if self.setting_service.get_value('is_alliance_bot') == '1': + prefix = f"[{self.alias_controller.get_alias(account.org_id)}] " discord = f"Your Account is connected to Discord as " \ - f"[{self.alias_controller.get_alias(account.org_id)}] " \ - f"{account.name}" + f"{prefix}{account.name}" + user = self.pork.get_character_info(data.packet.char_id) self.job_schedule.delayed_job(self.send_news, 15, user, self.account_service.get_alts(account.main), discord, self.preferences.get_pref_view_small(account)) - @command(command="raids", params=[], description="Show the Raids", access_level="member") - def show_raids(self, _): - with open("data/latest_raids.txt", "r") as f: - return self.text.format_page("Raidschedule", f.read()) + # @command(command="raids", params=[], description="Show the Raids", access_level="member") + # def show_raids(self, _): + # with open("data/latest_raids.txt", "r") as f: + # return self.text.format_page("Raidschedule", f.read()) def get_timers(self): events = [self.weekly.get_next_bs(), @@ -140,12 +146,21 @@ hh:mm - DD.MM.YYYY self.weekly.get_next_dio()] return sorted(events, key=lambda timer: timer.start)[0] + @setting(name="preview_recent", value="true", + description="Sends the newest News entry to the user directly, without wrapping it into a blob") + def preview_recent(self): + return BooleanSettingType() + def send_news(self, _, sender, alts, discord, prefs, auto=True): if auto: if self.buddy_service.get_buddy(sender.char_id)["online"] == 0: return timers = self.get_timers() next_event = self.get_next_event() + last_updated: datetime = (self.db.query_single("SELECT added_at FROM news order by ID desc") or {}).get( + "added_at", 0) + if type(last_updated) != int: + last_updated = last_updated.astimezone().timestamp() news = self.layout.format(time=datetime.now(timezone.utc).strftime("%H:%M - %d.%m.%Y") + " [UTC-0]", main=alts[0].name, alts_show=self.text.make_chatcmd(len(alts), "/tell alts"), @@ -155,15 +170,38 @@ hh:mm - DD.MM.YYYY prefs=prefs, discord=discord) name = self.weekly.get_long_name(next_event.type) + if last_updated > 0: + last_updated = f"[{self.util.time_to_readable(datetime.utcfromtimestamp(time.time()).timestamp() - last_updated)} ago]" + else: + last_updated = "" blob = f"{random.choice(self.greetings)} {sender.name} :: " \ - f"{self.text.format_page('Your News', textwrap.dedent(news))} " \ + f"{self.text.format_page('Your News', textwrap.dedent(news))} {last_updated} " \ f"{f':: {name} running' if next_event.is_running() else ''}" self.bot.send_mass_message(sender.char_id, blob) + if self.preview_recent().get_value() and auto: + entry = self.get_newest_entry() + if not entry: + return + if entry.id > self.get_read_id(sender.char_id): + data = f":: Your most recent unread news :: \n" \ + f"{entry.text}\n" \ + f"{self.text.format_page('Mark as read', self.text.make_tellcmd('Mark as read', f'news read {entry.id}'))}" + self.bot.send_mass_message(sender.char_id, data) ##################### # News Management # ##################### + @command(command="news", params=[Const("read"), Int("ID", is_optional=True)], + description="Marks All news until [ID] as read, or all of none specified.", + access_level="member", + extended_description="It is also possible to move the read marker back, i.e. from ID 5 -> 1.\nThis has no effect if the preview_recent setting is off.") + def cmd_news_read(self, sender, _, entry_id): + if not entry_id: + entry_id = (self.db.query_single("SELECT id from news ORDER BY id desc limit 1") or {}).get("id", 0) + self.mark_as_read(sender.sender.char_id, entry_id) + return f"Your News Read marker has been moved to ID {entry_id}." + @command(command="news", params=[], description="Show the news", access_level="member") def show_news(self, sender): user = self.db.query_single("SELECT * FROM player where char_id=?", [sender.sender.char_id]) @@ -243,6 +281,8 @@ hh:mm - DD.MM.YYYY normal = "" headline = "" for entry in data: + if len(self.text.strip_html_tags(headline + normal) or []) > limit: + break if entry.headline == 1: if len(self.text.strip_html_tags(headline) or []) > limit / 2: continue @@ -268,3 +308,16 @@ hh:mm - DD.MM.YYYY def add_entry(self, text, sender): self.db.exec("INSERT INTO news(text, added_by, added_at) VALUES(?, ?, ?)", [text, sender.char_id, datetime.utcfromtimestamp(time.time())]) + + def get_newest_entry(self): + return self.db.query_single("SELECT * from news order by id desc") + + def get_read_id(self, char_id): + return (self.db.query_single( + "SELECT post_id from news_read where main = (SELECT main from account where char_id=?)", + [char_id]) or {}).get('post_id', 0) + + def mark_as_read(self, char_id, post_id): + self.db.exec( + "INSERT INTO news_read(main, post_id) VALUES((SELECT main from account where char_id=? LIMIT 1), ?) ON DUPLICATE KEY UPDATE post_id=?", + [char_id, post_id, post_id]) diff --git a/modules/standard/news/worldboss_controller.py b/modules/standard/news/worldboss_controller.py index c949f85..db628b9 100644 --- a/modules/standard/news/worldboss_controller.py +++ b/modules/standard/news/worldboss_controller.py @@ -1,31 +1,30 @@ import time +from core.chat_blob import ChatBlob from core.command_alias_service import CommandAliasService +from core.command_param_types import Any from core.decorators import instance, command, event from core.dict_object import DictObject +from core.igncore import IgnCore from core.job_scheduler import JobScheduler from core.logger import Logger from core.setting_service import SettingService from core.setting_types import BooleanSettingType from core.text import Text -from core.igncore import IgnCore from core.util import Util 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. + # Timers are provided through a local websocket relay, which gets fed by an external 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] + 60 * 5, 60 * 3, 60 * 2, 60, 30, 15, 5, 0] jobs = [] def inject(self, registry): @@ -38,25 +37,46 @@ class WorldBossController: self.setting_service: SettingService = registry.get_instance("setting_service") def pre_start(self): - self.setting_service.register(self.module_name, 'timer_spam', True, BooleanSettingType(), - "should timers be spammed") + self.setting_service.register(self.module_name, 'timer_spam', False, BooleanSettingType(), + "should timers be spammed") + self.command_alias_service.add_alias("tara", "wb tara") + self.command_alias_service.add_alias("gaunt", "wb gaunt") + self.command_alias_service.add_alias("loren", "wb loren") + self.command_alias_service.add_alias("reaper", "wb reaper") - @event(WebsocketRelayController.WS_RELAY, "save most current timers") + @event(WebsocketRelayController.WS_RELAY, "save most current local_timers") def get_timer(self, _, data): - if data.type == "timer": - self.timer_data = data.payload - - def test(test_data): - if test_data: + if data.type != "timer": + return + local_timers = {} + spam = True if self.setting_service.get_value("timer_spam") == "1" else False + for x in self.timer_data: + local_timers[x['name']] = x['time'] + for row in data.payload: + spawn = self.get_spawn(row) + if not spawn: + continue + if row['name'] not in local_timers: + self.timer_data.append(row) + if spam: + for x in self.jobs: + if x['name'] == row['name']: + self.job_scheduler.cancel_job(x['id']) + self.jobs.append( + {'name': row['name'], 'id': self.job_scheduler.delayed_job(self.timer_alert, 2, spawn)}) + continue + elif (local_timers[row['name']] + 1) < row['time']: + for timer in self.timer_data: + if timer['name'] == row['name']: + timer['time'] = row['time'] + if spam: for job in self.jobs: - if job["name"] == test_data.name: - return - self.job_scheduler.delayed_job(self.timer_alert, 2, test_data) - - if self.setting_service.get_value("timer_spam") == "1": - for row in self.timer_data: - data = self.get_spawn(row) - test(data) + if job['name'] == row['name']: + self.job_scheduler.cancel_job(job['id']) + alert_duration = self.get_next_alert(spawn.at - time.time()) + job_id = self.job_scheduler.scheduled_job(self.timer_alert, 2, + spawn) + job['id'] = job_id def get_spawn(self, timer): timer = DictObject(timer) @@ -108,26 +128,26 @@ class WorldBossController: return duration - alert - 1 return duration - @command(command="gaunt", params=[], description="Displays the next Vizaresh pop time", access_level="member") - def show_gaunt(self, _): - for timer in self.timer_data: - if timer['name'] == "Vizaresh": - return self.show_user(timer) - return "Timer not found" - - @command(command="loren", params=[], description="Displays the next Loren Warr pop time", access_level="member") - def show_loren(self, _): - for timer in self.timer_data: - if timer['name'] == "Loren Warr": - return self.show_user(timer) - return "Timer not found" - - @command(command="tara", params=[], description="Displays the next Tara pop time", access_level="member") - def show_tara(self, _): - for timer in self.timer_data: - if timer['name'] == "Tarasque": - return self.show_user(timer) - return "Timer not found" + @command(command="wb", params=[Any("worldboss", is_optional=True)], + description="Displays the next worldboss spawns", access_level="member") + def show_worldboss(self, request, boss: str): + if boss: + boss = boss.lower() + if boss in ["tara", "tarasque"]: + boss = "Tarasque" + elif boss in ["viza", "vizaresh", "gaunt", "gauntlet"]: + boss = "Vizaresh" + elif boss in ["loren", "loren warr", "loren war", "lw"]: + boss = "Loren Warr" + elif boss in ["thr", "the hollow", "the hollow reaper", "reaper"]: + boss = "The Hollow Reaper" + for x in self.timer_data: + if x['name'] == boss: + return self.show_user(x) + else: + blob = "\n".join([self.show_user(x) for x in self.timer_data if + self.show_user(x) != "No timers cached; please try again later."]) + return ChatBlob("Next Worldboss spawns", blob) def show_user(self, timer): timer = self.get_spawn(timer) @@ -144,7 +164,9 @@ class WorldBossController: for row in self.timer_data: if row["name"] == timer.name: timer = self.get_spawn(row) - alert_duration = self.get_next_alert(timer.at - t) + if not timer.at: + return + alert_duration = self.get_next_alert(timer.at - time.time()) if timer.at - time.time() < 1: if timer.type == "mortal": self.send_warn(f"{timer.name} :: is now mortal") @@ -152,7 +174,7 @@ class WorldBossController: elif timer.type == "spawn": self.send_warn(f"{timer.name} :: has just spawned") self.jobs = [x for x in self.jobs if x['name'] != timer.name] - else: # timer.at > time.time(): + else: if timer.type == "mortal": self.send_warn(f"{timer.name} :: mortal in {self.util.format_time(timer.time)}") elif timer.type == "spawn": @@ -162,7 +184,7 @@ class WorldBossController: for row in self.timer_data: if row["name"] == timer.name: timer = self.get_spawn(row) - job_id = self.job_scheduler.scheduled_job(self.timer_alert, t + alert_duration, timer) + job_id = self.job_scheduler.scheduled_job(self.timer_alert, time.time() + alert_duration, timer) for job in self.jobs: if job['name'] == timer.name: job['id'] = job_id diff --git a/modules/standard/online/online_controller.py b/modules/standard/online/online_controller.py index bc59b5d..479b50a 100644 --- a/modules/standard/online/online_controller.py +++ b/modules/standard/online/online_controller.py @@ -10,13 +10,13 @@ from core.db import DB from core.decorators import instance, event, command from core.dict_object import DictObject from core.fifo_queue import FifoQueue +from core.igncore import IgnCore from core.logger import Logger from core.lookup.character_service import CharacterService from core.private_channel_service import PrivateChannelService from core.public_channel_service import PublicChannelService from core.setting_service import SettingService from core.text import Text -from core.igncore import IgnCore from core.util import Util from modules.core.accounting.services.account_service import AccountService from modules.standard.online.online_display import OnlineDisplay @@ -163,6 +163,8 @@ class OnlineController: alts = self.account_service.get_alts(char_id) for alt in alts: self.afk_list[alt.char_id] = DictObject({"message": message, "time": time.time()}) + # Dirty fix for players without account + self.afk_list[char_id] = DictObject({"message": message, "time": time.time()}) elif char_id in self.afk_list.keys(): # TODO handle multiple rows @@ -173,7 +175,8 @@ class OnlineController: if data: continue data = out + if not alts: + data = self.afk_list.pop(char_id) char_name = self.character_service.resolve_char_to_name(char_id) time_string = self.util.time_to_readable(int(time.time()) - data.time) channel_reply(f"{char_name} is back after {time_string}.") - diff --git a/modules/standard/online/online_display.py b/modules/standard/online/online_display.py index 75cd6f9..59879a8 100644 --- a/modules/standard/online/online_display.py +++ b/modules/standard/online/online_display.py @@ -55,6 +55,9 @@ class OnlineDisplay: org, priv, notify = 0, 0, 0 previous = DictObject({'char_id': 0}) for player in players: + if player.faction == "": + player.faction = "unknown" + rank = "" if 'rank' in player: @@ -74,7 +77,8 @@ class OnlineDisplay: if main_id != player.main_id: main_id = player.main_id afk = "" if not afk else f" [{afk.message} - since {self.util.time_to_readable(int(time.time() - afk.time))}]" - blob += f"\n{player.main_name}{rank}:{afk}\n" + style = "style='text-decoration:none'" + blob += f"\n{self.text.make_tellcmd(player.main_name, f'alts {player.main_name}', style=style)}{rank}:{afk}\n" if channel_id == 1: org += 1 elif channel_id == 2: @@ -99,6 +103,8 @@ class OnlineDisplay: org, priv, notify = 0, 0, 0 previous = DictObject({'char_id': 0}) for player in players: + if player.faction == "": + player.faction = "unknown" rank = "" if player.char_id in in_org_priv: continue @@ -139,6 +145,8 @@ class OnlineDisplay: } last = 0 for user in query: + if user.faction == "": + user.faction = "unknown" if last == user.char_id: continue last = user.char_id @@ -168,23 +176,26 @@ class OnlineDisplay: def format_org(self, player, rank="", afk="", main_order=False): main = f"[{self.text.make_tellcmd(player.main_name, f'alts {player.main_name}')}]" if main_order else "" + org = f"({player.org_rank_name}) " if player.org_rank_name else "(Applicant)" return f" {self.util.get_prof_icon(player.profession)} {rank}" \ f"{self.text.zfill(player.level, 220)}/{self.text.zfill(player.ai_level, 30)} " \ - f"<{player.faction.lower()}>{player.name} ({player.org_rank_name}){afk} {main}\n" + f"<{player.faction.lower()}>{player.name} {org}{afk} {main}\n" def format_priv(self, player, rank="", afk="", main_order=False): main = f"[{self.text.make_tellcmd(player.main_name, f'alts {player.main_name}')}]" if main_order else "" + org = f"({player.org_name}|{player.org_rank_name}) " if player.org_name else "" return f" {self.util.get_prof_icon(player.profession)} {rank}" \ f"{self.text.zfill(player.level, 220)}/{self.text.zfill(player.ai_level, 30)} " \ f"<{player.faction.lower()}>{player.name} " \ - f"({player.org_name}|{player.org_rank_name}){afk} {main}\n" + f"{org}{afk} {main}\n" def format_notify(self, player, rank="", afk="", main_order=False): main = f"[{self.text.make_tellcmd(player.main_name, f'alts {player.main_name}')}]" if main_order else "" + org = f"({player.org_name}|{player.org_rank_name}) " if player.org_name else "" return f" {self.util.get_prof_icon(player.profession)} {rank}" \ f"{self.text.zfill(player.level, 220)}/{self.text.zfill(player.ai_level, 30)} " \ f"<{player.faction.lower()}>{player.name} " \ - f"({player.org_name}|{player.org_rank_name}){afk} {main}\n" + f"{org}{afk} {main}\n" def count_prof(self, query, params, filters): if filters: diff --git a/modules/standard/raid/assist_controller.py b/modules/standard/raid/assist_controller.py index 47df520..b452b58 100644 --- a/modules/standard/raid/assist_controller.py +++ b/modules/standard/raid/assist_controller.py @@ -6,7 +6,6 @@ from core.command_param_types import Any, Const, Character, Options from core.decorators import instance, command, event, timerevent from core.igncore import IgnCore from core.text import Text -from modules.core.config.alias_controller import AliasController from modules.standard.raid.leader_controller import LeaderController @@ -32,7 +31,7 @@ class AssistController: blob = "" for caller in self.assist: blob += caller.capitalize() - blob += f" - [{self.text.make_chatcmd('assist', f'assist {caller}')}]" + blob += f" - [{self.text.make_chatcmd('assist', f'/assist {caller}')}]" blob += f" [{self.text.make_tellcmd('REM', f'assist del {caller}')}]
" blob += self.get_assist_output() self.last_mod = time.time() diff --git a/modules/standard/specials/specials_controller.py b/modules/standard/specials/specials_controller.py index edfe787..03c54f6 100644 --- a/modules/standard/specials/specials_controller.py +++ b/modules/standard/specials/specials_controller.py @@ -189,14 +189,14 @@ class SpecialsController: blob = f"Attack: {weapon_attack:.2f} secs\n" blob += f"Recharge: {weapon_recharge:.2f} secs\n" - blob += f"Full Auto Recharge: {full_auto_recharge:d}\n" - blob += f"Full Auto Skill: {full_auto_skill:d}\n\n" + blob += f"Full Auto Recharge: {full_auto_recharge:.2f}\n" + blob += f"Full Auto Skill: {full_auto_skill:.2f}\n\n" - blob += f"Full Auto Recharge: {full_auto_info.recharge:d} secs\n" - blob += f"Max Number of Bullets: {full_auto_info.max_bullets:d}\n\n" + blob += f"Full Auto Recharge: {full_auto_info.recharge:.2f} secs\n" + blob += f"Max Number of Bullets: {full_auto_info.max_bullets:.2f}\n\n" - blob += f"You need {full_auto_info.skill_cap:d} Full Auto Skill " \ - f"to cap your recharge at {full_auto_info.hard_cap:d} secs.\n\n" + blob += f"You need {full_auto_info.skill_cap:.2f} Full Auto Skill " \ + f"to cap your recharge at {full_auto_info.hard_cap:.2f} secs.\n\n" blob += "From 0 to 10K damage, the bullet damage is unchanged.\n" blob += "From 10K to 11.5K damage, each bullet damage is halved.\n" @@ -235,7 +235,7 @@ class SpecialsController: nano_cast_info = self.get_nano_cast_info(nano_cast_init, nano_attack_time) blob = f"Attack: {nano_attack_time:.2f} secs\n" - blob += f"Nano Cast Init: {nano_cast_init:d}\n\n" + blob += f"Nano Cast Init: {nano_cast_init}\n\n" blob += f"Cast Time Reduction: {nano_cast_info.cast_time_reduction:.2f}\n" blob += f"Effective Cast Time: {nano_cast_info.effective_cast_time:.2f}\n\n" @@ -247,13 +247,13 @@ class SpecialsController: f"to instacast this nano.\n\n" blob += f"NanoC. Init needed to instacast at Full Agg (100%): " \ - f"{nano_cast_info.instacast_full_agg:d}\n" + f"{nano_cast_info.instacast_full_agg:.2f}\n" blob += f"NanoC. Init needed to instacast at Neutral (87.5%): " \ - f"{nano_cast_info.instacast_neutral:d}\n" + f"{nano_cast_info.instacast_neutral:.2f}\n" blob += f"NanoC. Init needed to instacast at Half (50%): " \ - f"{nano_cast_info.instacast_half:d}\n" + f"{nano_cast_info.instacast_half:.2f}\n" blob += f"NanoC. Init needed to instacast at Full Def (0%): " \ - f"{nano_cast_info.instacast_full_def:d}\n\n" + f"{nano_cast_info.instacast_full_def:.2f}\n\n" blob += f"Cast time at Full Agg (100%): {nano_cast_info.cast_time_full_agg:.2f}\n" blob += f"Cast time at Neutral (87.5%): {nano_cast_info.cast_time_neutral:.2f}\n" diff --git a/modules/standard/timers/timer_controller.py b/modules/standard/timers/timer_controller.py index c6782c9..e459208 100644 --- a/modules/standard/timers/timer_controller.py +++ b/modules/standard/timers/timer_controller.py @@ -2,7 +2,7 @@ import time from core.chat_blob import ChatBlob from core.command_param_types import Any, Const, Time, Options -from core.decorators import instance, command, event +from core.decorators import instance, command from core.igncore import IgnCore from core.registry import Registry from modules.standard.news.worldboss_controller import WorldBossController @@ -130,18 +130,18 @@ class TimerController: else: return f"Error! Insufficient access level to remove timer {timer.name}." - @event("connect", description="reload timers on restart") - def reload_timers(self, _, _1): - timers = self.db.query("SELECT * from timer") - for timer in timers: - self.timer_alert(time.time(), timer.name) + # @event("connect", description="reload timers on restart") + # def reload_timers(self, _, _1): + # timers = self.db.query("SELECT * from timer") + # for timer in timers: + # self.timer_alert(time.time(), timer.name) @command(command="rtimer", params=[Const("add", is_optional=True), TimerTime("start_time"), TimerTime("repeating_time"), Any("name", is_optional=True)], - access_level="member", + access_level="moderator", description="Add a timer") def rtimer_add_cmd(self, request, _, start_time, repeating_time, timer_name): timer_name = timer_name or self.get_timer_name(request.sender.name) diff --git a/modules/raidbot/tower/contract_controller.py b/modules/standard/tower/contract_controller.py similarity index 60% rename from modules/raidbot/tower/contract_controller.py rename to modules/standard/tower/contract_controller.py index a039709..43a7b15 100644 --- a/modules/raidbot/tower/contract_controller.py +++ b/modules/standard/tower/contract_controller.py @@ -2,18 +2,11 @@ import math from core.aochat.BaseModule import BaseModule from core.chat_blob import ChatBlob -from core.command_alias_service import CommandAliasService from core.command_param_types import Int, NamedParameters from core.db import DB from core.decorators import instance, command -from core.event_service import EventService -from core.lookup.pork_service import PorkService -from core.public_channel_service import PublicChannelService -from core.text import Text from core.igncore import IgnCore -from core.util import Util -from modules.raidbot.tower.tower_service import TowerService -from modules.standard.helpbot.playfield_controller import PlayfieldController +from core.text import Text # noinspection DuplicatedCode @@ -25,14 +18,7 @@ class ContractController(BaseModule): def inject(self, registry): self.bot: IgnCore = registry.get_instance("bot") self.db: DB = registry.get_instance("db") - self.util: Util = registry.get_instance("util") self.text: Text = registry.get_instance("text") - self.event_service: EventService = registry.get_instance("event_service") - self.pork_service: PorkService = registry.get_instance("pork_service") - self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller") - self.public_channel_service: PublicChannelService = registry.get_instance("public_channel_service") - self.towercache: TowerService = registry.get_instance("tower_service") - self.command_alias_service: CommandAliasService = registry.get_instance("command_alias_service") @command(command="contracts", params=[Int('mininum', is_optional=True), NamedParameters(['page'])], @@ -41,9 +27,9 @@ class ContractController(BaseModule): def cotracts(self, _, min_ql, named_params): if not min_ql: min_ql = 200 - data = self.db.query("SELECT CAST(SUM(ql)*2 AS INTEGER) AS contracts, " - "COUNT(*) as sites, org_name, org_id, faction FROM towers " - "where org_name IS NOT NULL GROUP BY org_name ORDER BY contracts desc", []) + data = self.db.query("SELECT e.*, CAST(SUM(ql)*2 AS INTEGER) AS contracts, COUNT(*) AS sites FROM towers a " + "LEFT JOIN all_orgs e on a.org_id = e.org_id WHERE close_time IS NOT NULL " + "GROUP BY org_name ORDER BY contracts desc", []) page = int(named_params.page or "1") offset = (page - 1) * self.PAGE_SIZE data = [x for x in data if x.contracts and x.contracts > min_ql] @@ -57,15 +43,15 @@ class ContractController(BaseModule): count = len(selected) pages = "" if page > 1: - pages += "Pages: " + self.text.make_tellcmd("«« Page %d" % (page - 1), f'{cmd} --page={page - 1}') + pages += "Pages: " + self.text.make_tellcmd(f"«« Page {page - 1:d}", f'{cmd} --page={page - 1}') if offset + self.PAGE_SIZE < len(data): pages += f" Page {page}/{math.ceil(len(data) / self.PAGE_SIZE)}" - pages += " " + self.text.make_tellcmd("Page %d »»" % (page + 1), f'{cmd} --page={page + 1}') + pages += " " + self.text.make_tellcmd(f"Page {page + 1:d} »»", f'{cmd} --page={page + 1}') pages += "\n" if count == 0: return nullmsg else: - blob = "" + blob = "" blob += "" + pages + "\n" index = offset for entry in selected: diff --git a/modules/standard/tower/hot_controller.py b/modules/standard/tower/hot_controller.py new file mode 100644 index 0000000..7e726e7 --- /dev/null +++ b/modules/standard/tower/hot_controller.py @@ -0,0 +1,166 @@ +import time + +from core.aochat.BaseModule import BaseModule +from core.command_alias_service import CommandAliasService +from core.command_param_types import Options, Int, Any, NamedParameters +from core.db import DB +from core.decorators import instance, command, event +from core.dict_object import DictObject +from core.event_service import EventService +from core.igncore import IgnCore +from core.lookup.pork_service import PorkService +from core.public_channel_service import PublicChannelService +from core.text import Text +from core.util import Util +from modules.standard.helpbot.playfield_controller import PlayfieldController +from modules.standard.tower.tower_events import TowerEventController + + +@instance() +class TowerHotController(BaseModule): + PAGE_SIZE = 30 + + # noinspection DuplicatedCode + + def inject(self, registry): + self.bot: IgnCore = registry.get_instance("bot") + self.db: DB = registry.get_instance("db") + self.util: Util = registry.get_instance("util") + self.text: Text = registry.get_instance("text") + self.event_service: EventService = registry.get_instance("event_service") + self.pork_service: PorkService = registry.get_instance("pork_service") + self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller") + self.public_channel_service: PublicChannelService = registry.get_instance("public_channel_service") + self.command_alias_service: CommandAliasService = registry.get_instance("command_alias_service") + + @event(event_type=TowerEventController.TOWER_ATTACK_EVENT, description="Mark Sites in penalty as in penalty", + is_enabled=False) + def tower_victory_event(self, _, event_data): + if event_data.attacker.org_id: + self.db.exec("UPDATE towers SET penalty_until=? where org_id=?", + [time.time() + 60 * 60, event_data.attacker.org_id]) + + @command(command="hot", + params=[Options(['tl1', 'tl2', 'tl3', 'tl4', 'tl5', 'tl6', 'tl7']), Any('faction', is_optional=True), + NamedParameters(["page"])], + access_level="member", + description="Shows hot playfields") + def hot_tl(self, _, tl, faction: str, named_params): + if faction: + if faction.startswith("--page="): + named_params = DictObject({'page': faction[7:]}) + faction = None + if faction is not None and faction.lower() not in ['omni', 'clan', 'neut', 'neutral']: + return f"Unknown faction: {faction}" + tl = tl[2:] + page = int(named_params.page or "1") + offset = (page - 1) * self.PAGE_SIZE + towers = self.get_hot_sites_tl(int(tl), faction) + return self.text.format_pagination(towers, offset, page, self.formatter, f"Hot Sites TL{tl} ({len(towers)})", + f"There are no hot sites for TL {tl}.", + f'hot tl{tl} {faction or ""}', self.PAGE_SIZE) + + def formatter(self, row, index, data): + d = {} + if index > 1: + d = data[index - 2] + status = "" + if row.status_time <= 3600: + status += f"5% (closes in {self.util.time_to_readable(row.status_time)})" + elif row.status_time <= (3600 * 6): + status += f"25% (closes in {self.util.time_to_readable(row.status_time)})" + else: + status += f"75% (opens in {self.util.time_to_readable(row.status_time - (3600 * 6))})" + if row.penalty_until > time.time(): + status += f" In Penalty for: {self.util.time_to_readable(row.penalty_until - time.time())}" + blob = "" + if self.get_ct_type(d.get("ql", 0)) < (tl := self.get_ct_type(row.ql)): + blob += f"TL{tl}
" + space = f"{row.short_name} x{row.site_number}" + place = "_" * (7 - len(space)) + return blob + "" + self.text.make_tellcmd(space, + f'lc {row.short_name} {row.site_number}') + \ + f"{place} QL {row.min_ql}/{row.ql}/{row.max_ql} - " \ + f"{self.text.get_formatted_faction(row.faction, row.org_name)}, {status}\n" + + @command(command="hot", + params=[Int('level', is_optional=True), Any('faction', is_optional=True), NamedParameters(["page"])], + access_level="member", + description="Shows hot playfields by level") + def hot_level(self, _, level, faction, named_params): + if faction: + if faction.startswith("--page="): + named_params = DictObject({'page': faction[7:]}) + faction = None + if faction is not None and faction.lower() not in ['omni', 'clan', 'neut', 'neutral']: + return f"Unknown faction: {faction}" + if level: + if level < 0 | level > 220: + return f"Level out of range: {level}" + page = int(named_params.page or "1") + offset = (page - 1) * self.PAGE_SIZE + towers = self.get_hot_sites(level, faction) + level = f"{level}" if level else "" + faction = f"{faction} " if faction else "" + return self.text.format_pagination(towers, offset, page, self.formatter, f"Hot Towersites ({len(towers)})", + f"There are no hot sites.", f'hot {level}{faction}', self.PAGE_SIZE) + + def get_hot_sites(self, level=None, faction=None): + where = "" + now = time.time() % 86400 + params = [now, now, now] + if level: + where += " AND l.pvp_min <=? and pvp_max >= ? " + params.append(level) + params.append(level) + if faction: + where += " AND c.faction LIKE ? " + params.append(faction.capitalize()) + data = self.db.query("SELECT a.*, f.short_name, c.org_name, b.min_ql, b.max_ql, " + "(CASE WHEN (a.close_time-?) < 0 THEN a.close_time-? +86400 ELSE a.close_time-? END) " + " AS status_time FROM tower_sites b " + "LEFT JOIN towers a ON a.pf_id = b.playfield_id AND a.site_number = b.site_number " + "LEFT JOIN level l on a.ql = l.level " + "LEFT JOIN playfields f on a.pf_id = f.id " + f"LEFT JOIN all_orgs c ON a.org_id = c.org_id WHERE close_time IS NOT NULL {where} ORDER BY a.ql", + params) + return [x for x in data if x.status_time - (3600 * 6) < 60 * 60 or x.penalty_until > time.time()] + + def get_hot_sites_tl(self, tl=7, faction=None): + min_ql, max_ql = self.util.get_level_range_tl(tl) + where = "" + now = time.time() % 86400 + params = [now, now, now] + where += " AND ql between ? and ? " + params.append(min_ql) + params.append(max_ql) + if faction: + where += " AND c.faction LIKE " + params.append("%" + faction.capitalize() + "%") + data = self.db.query("SELECT a.*, f.short_name, c.org_name, b.min_ql, b.max_ql, " + "(CASE WHEN (a.close_time-?) < 0 THEN a.close_time-? +86400 ELSE a.close_time-? END) " + " AS status_time FROM tower_sites b " + "LEFT JOIN towers a ON a.pf_id = b.playfield_id AND a.site_number = b.site_number " + "LEFT JOIN level l on a.ql = l.level " + "LEFT JOIN playfields f on a.pf_id = f.id " + f"LEFT JOIN all_orgs c ON a.org_id = c.org_id WHERE close_time IS NOT NULL {where} ORDER BY a.ql", + params) + return [x for x in data if x.status_time - (3600 * 6) < 60 * 60 or x.penalty_until > time.time()] + + def get_ct_type(self, ql): + if ql == 0: + return 0 + elif ql < 34: + return 1 + elif ql < 82: + return 2 + elif ql < 129: + return 3 + elif ql < 177: + return 4 + elif ql < 201: + return 5 + elif ql < 226: + return 6 + else: + return 7 diff --git a/modules/standard/tower/land_controller.py b/modules/standard/tower/land_controller.py new file mode 100644 index 0000000..d6795f2 --- /dev/null +++ b/modules/standard/tower/land_controller.py @@ -0,0 +1,279 @@ +import time +from datetime import datetime + +import pytz + +from core.chat_blob import ChatBlob +from core.command_alias_service import CommandAliasService +from core.command_param_types import Any, Int, Const +from core.db import DB +from core.decorators import instance, command, event, setting +from core.event_service import EventService +from core.igncore import IgnCore +from core.lookup.pork_service import PorkService +from core.public_channel_service import PublicChannelService +from core.setting_types import BooleanSettingType +from core.text import Text +from core.util import Util +from modules.standard.helpbot.playfield_controller import PlayfieldController +# legacy(0), EU - friendly(1) or US - friendly(2) => timing +from modules.standard.tower.tower_attack_controller import TowerAttackController +from modules.standard.tower.tower_events import TowerEventController + +FIXED_TIMES = {1: 4, + 2: 20} + + +@instance() +class LandController: + def inject(self, registry): + self.bot: IgnCore = registry.get_instance("bot") + self.db: DB = registry.get_instance("db") + self.text: Text = registry.get_instance("text") + self.util: Util = registry.get_instance("util") + self.event_service: EventService = registry.get_instance("event_service") + self.pork_service: PorkService = registry.get_instance("pork_service") + self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller") + self.public_channel_service: PublicChannelService = registry.get_instance("public_channel_service") + self.command_alias_service: CommandAliasService = registry.get_instance("command_alias_service") + self.tac: TowerAttackController = registry.get_instance("tower_attack_controller") + + def pre_start(self): + self.command_alias_service.add_alias('towers', "lc") + self.command_alias_service.add_alias('tower', "lc") + self.command_alias_service.add_alias('lca', "lc") + self.db.load_sql_file(self.module_dir + "/" + "tower_sites.sql", pre_optimized=True) + self.db.shared.exec("CREATE TABLE IF NOT EXISTS `towers` (" + "`tower_id` INT(11) NOT NULL," + "`pf_id` INT(11) NOT NULL," + "`site_number` INT(11) NOT NULL," + "`x_coord` INT(11) NOT NULL," + "`y_coord` INT(11) NOT NULL," + "`high_id` INT(11) NOT NULL," + "`ql` INT(11) NOT NULL," + "`org_id` INT(11)," + "`faction` VARCHAR(11)," + "`planted` INT(11)," + "`close_time` INT(11)," + "`penalty_until` INT(11)," + "PRIMARY KEY (`tower_id`) USING HASH," + "INDEX `site` (`pf_id`, `site_number`, `org_id`, `close_time`, `penalty_until`) USING HASH," + "INDEX `tower` (`planted`, `ql`, `x_coord`, `y_coord`) USING HASH) " + "COLLATE='utf8mb4_general_ci' ENGINE=MEMORY;") + self.db.create_view("towers") + + @setting(name="lc_cmd_full", value="false", description="Toggle the verbosity of !lc commands") + def lc_full(self) -> BooleanSettingType: + return BooleanSettingType() + + @event(event_type=TowerEventController.TOWER_VICTORY_EVENT, description="Purge sites which got wiped from DB", + is_enabled=False) + def tower_victory_event(self, _, event_data): + row = self.tac.get_last_attack(event_data.winner.faction, event_data.winner.org_name, event_data.loser.faction, + event_data.loser.org_name, event_data.location.playfield.id, time.time()) + if row: + self.db.exec("DELETE FROM towers where pf_id=? and site_number=?", [row.pf_id, row.site]) + + @command(command="lc", params=[], access_level="member", + description="See a list of playfields containing land control tower sites") + def lc_list_cmd(self, request): + data = self.db.query( + "SELECT id, long_name, short_name FROM playfields " + "WHERE id IN (SELECT DISTINCT playfield_id FROM tower_sites) ORDER BY short_name") + + blob = "" + for row in data: + blob += f"[{row.id:d}] {self.text.make_tellcmd(row.long_name, f'lc {row.short_name}')} {row.short_name}\n" + + return ChatBlob(f"Land Control Playfields ({len(data):d})", blob) + + @command(command="lc", params=[Const("org"), Any("search")], access_level="member", + description="See a list of land control tower sites in a particular playfield") + def sites_org_cmd(self, request, _, search): + if search.isdigit(): + org_id = int(search) + result = search + else: + orgs = self.find_orgs(search) + num_orgs = len(orgs) + if num_orgs == 0: + char_info = self.pork_service.get_character_info(search) + if char_info: + if not char_info.org_id: + return f"{search.capitalize()} does not appear to belong to an org." + else: + org_id = char_info.org_id + result = char_info.org_name + else: + return f"Character or org {search} does not own any sites." + elif num_orgs == 1: + result = orgs[0].org_name + org_id = orgs[0].org_id + else: + blob = "" + for org in orgs: + blob += self.text.make_tellcmd(f"{org.org_name} ({org.org_id})", + f"lc org {org.org_id}") + "\n" + return ChatBlob(f"Orgs matching your search criteria ({num_orgs})", blob) + + data = self.get_towers_by_org(org_id) + blob = "" + ql = 0 + for x in data: + blob += f"{self.format_site_info(x, time.time(), len(data))}" + blob += f"Dist: {x.guard} Conductors and {x.turrets} Turrets planted\n\n" + ql += x.ql + blob += f"Stats: QL{ql}, contracts up to QL{ql * 2}" + return ChatBlob(f"Sites owned by {result}", blob) + + @command(command="lc", params=[Any("playfield"), Int("site_number", is_optional=True)], access_level="member", + description="See a list of land control tower sites in a particular playfield") + def lc_playfield_cmd(self, request, playfield_name, site_number): + playfield = self.playfield_controller.get_playfield_by_name_or_id(playfield_name) + if not playfield: + return f"Could not find playfield {playfield_name}." + + data = self.get_towers(playfield.id, site_number) + + blob = "" + t = int(time.time()) + if site_number: + data = self.get_towers(playfield.id, site_number) + blob += "" + self.format_site_info(data, t) + else: + for row in data: + blob += f"{self.format_site_info(row, t, len(data))}\n" + + if site_number: + title = f"Tower Info: {playfield.long_name} x{site_number}" + else: + title = f"Tower Info: {playfield.long_name} ({len(data)})" + + return ChatBlob(title, blob) + + @command(command="free", params=[], + access_level="member", + description="Shows potentially free towerfields") + def free(self, _, ): + blob = "" + data = self.get_free() + for row in data: + blob += f"{self.format_site_info(row, time.time(), len(data))}\n" + + return ChatBlob(f"FREE Towersites ({len(data)})", blob) if blob else f"No free towersites found." + + def format_site_info(self, row, t, count=0): + data = row + if count == 0 and data: + row = data[0] + blob = f"{row.short_name} x{row.site_number} ({row.site_name})\n" + blob += f"Level Range: {row.min_ql} - {row.max_ql} " + if row.timing == 0: + blob += f"[Legacy]\n" + if row.timing == 1: + blob += f"[04 UTC]\n" + if row.timing == 2: + blob += f"[20 UTC]\n" + blob += f"Coordinates: " + blob += self.text.make_chatcmd(f"{row.x_coord:d}x{row.y_coord:d}", + f"/waypoint {row.x_coord:d} {row.y_coord:d} {row.pf_id:d}") + "\n" + if row.get("org_name", None): + current_day_time = t % 86400 + if row.timing > 0: + row.close_time = FIXED_TIMES[row.timing] * 3600 + row.planted % 3600 + value = datetime.fromtimestamp(row.close_time, tz=pytz.UTC) + current_status_time = row.close_time - current_day_time + if current_status_time < 0: + current_status_time += 86400 + status = "" + if current_status_time <= 3600: + status += f"5% (closes in {self.util.time_to_readable(current_status_time)})" + elif current_status_time <= (3600 * 6): + status += f"25% (closes in {self.util.time_to_readable(current_status_time)})" + else: + status += f"75% (opens in {self.util.time_to_readable(current_status_time - (3600 * 6))})" + + if row.penalty_until > t: + status += f" Penalty (for {self.util.time_to_readable(row.penalty_until - t)})" + blob += f"CT: QL{row.ql} ({self.text.get_formatted_faction(row.faction, row.org_name)}) T{self.get_ct_type(row.ql)} - Planted {self.util.time_to_readable(t - row.planted)} ago\n" + blob += f"Gas: {status}\n" + if self.lc_full().get_value(): + towers = "" + cond, turret = 0, 0 + if count == 0: + for tower in data: + if tower.name.__contains__("Turret"): + turret += 1 + elif tower.name.__contains__("Conductor"): + cond += 1 + towers += f" - QL{self.text.zfill(tower.ql, 220)} {tower.name}\n" + blob += f"Dist: {cond} Conductors and {turret} Turrets\n" + blob += "\n Towers:\n" + blob += towers + else: + if not row.enabled: + blob += "Disabled\n" + else: + blob += "This site is potentially unplanted\n" + + return blob + + def get_ct_type(self, ql): + if ql < 34: + return 1 + elif ql < 82: + return 2 + elif ql < 129: + return 3 + elif ql < 177: + return 4 + elif ql < 201: + return 5 + elif ql < 226: + return 6 + else: + return 7 + + def get_free(self): + return self.db.query("""SELECT d.playfield_id AS pf_id,d.site_number, d.site_name, d.min_ql, d.max_ql, d.x_coord, d.y_coord, d.timing, d.enabled, a.tower_id, a.ql, a.close_time, a.penalty_until, a.planted, b.*, c.*, e.* FROM tower_sites d + LEFT JOIN towers a on a.pf_id = d.playfield_id and a.site_number = d.site_number + LEFT JOIN aodb b ON a.high_id = b.highid + LEFT JOIN playfields c on d.playfield_id = c.id + LEFT JOIN all_orgs e on a.org_id = e.org_id + WHERE a.org_id IS NULL AND d.enabled = 1 GROUP BY d.playfield_id, d.site_number """) + + def get_towers(self, pf, site=None): + if site: + return self.db.query("""SELECT d.playfield_id AS pf_id,d.site_number, d.site_name, d.min_ql, d.max_ql, d.x_coord, d.y_coord, d.timing, d.enabled, a.tower_id, a.ql, a.close_time, a.penalty_until, a.planted, b.*, c.*, e.* FROM tower_sites d + LEFT JOIN towers a on a.pf_id = d.playfield_id and a.site_number = d.site_number + LEFT JOIN aodb b ON a.high_id = b.highid + LEFT JOIN playfields c on d.playfield_id = c.id + LEFT JOIN all_orgs e on a.org_id = e.org_id + WHERE playfield_id=? AND d.site_number=? ORDER BY close_time IS NULL, ql desc""", + [pf, site]) + else: + return self.db.query("""SELECT d.playfield_id AS pf_id, d.site_number, d.site_name, d.min_ql, d.max_ql, d.x_coord, d.y_coord, d.timing, d.enabled, a.tower_id, a.ql, a.close_time, a.penalty_until, a.planted, b.*, c.*, e.* FROM tower_sites d + LEFT JOIN towers a on a.pf_id = d.playfield_id and a.site_number = d.site_number + LEFT JOIN aodb b ON a.high_id = b.highid + LEFT JOIN playfields c on d.playfield_id = c.id + LEFT JOIN all_orgs e on a.org_id = e.org_id + WHERE playfield_id=? + GROUP BY playfield_id, site_number + ORDER BY site_number, ql DESC + """, [pf]) + + def get_towers_by_org(self, org_id): + return self.db.query( + "SELECT COUNT(CASE WHEN name LIKE '%Turret%' THEN 1 WHEN name LIKE '%SAM Battery%' THEN 1 END) turrets, " + "COUNT(CASE WHEN name LIKE '%Guard%' THEN 1 END) guard, " + "a.*, b.*, c.*, d.site_name, d.min_ql, d.max_ql, d.timing, d.enabled, e.* FROM towers a " + "LEFT JOIN aodb b ON a.high_id = b.highid " + "LEFT JOIN playfields c on a.pf_id = c.id " + "LEFT JOIN tower_sites d on a.pf_id = d.playfield_id and a.site_number = d.site_number " + "LEFT JOIN all_orgs e on a.org_id = e.org_id " + "WHERE a.org_id=? GROUP BY a.pf_id, a.site_number ORDER BY ql, close_time IS NOT NULL", [org_id]) + + def find_orgs(self, search): + return self.db.query("SELECT DISTINCT a.org_name, a.org_id FROM all_orgs a " + "LEFT JOIN towers b ON a.org_id = b.org_id WHERE org_name ? AND b.org_id IS NOT NULL", + [search], extended_like=True) diff --git a/modules/standard/tower/tower_attack_controller.py b/modules/standard/tower/tower_attack_controller.py new file mode 100644 index 0000000..71512cc --- /dev/null +++ b/modules/standard/tower/tower_attack_controller.py @@ -0,0 +1,247 @@ +import time + +from core.decorators import instance, event +from core.igncore import IgnCore +from core.logger import Logger +from core.text import Text +from modules.standard.helpbot.playfield_controller import PlayfieldController +from modules.standard.tower.tower_events import TowerEventController + + +@instance() +class TowerAttackController: + def __init__(self): + self.logger = Logger(__name__) + + def inject(self, registry): + self.bot: IgnCore = registry.get_instance("bot") + self.db = registry.get_instance("db") + self.text: Text = registry.get_instance("text") + self.tower: TowerEventController = registry.get_instance("tower_controller") + self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller") + + def start(self): + self.db.shared.exec( + "CREATE TABLE IF NOT EXISTS tower_attacker (" + "id INT PRIMARY KEY AUTO_INCREMENT, " + "att_org_name VARCHAR(50) NOT NULL, " + "att_faction VARCHAR(10) NOT NULL, " + "att_char_id INT, att_char_name VARCHAR(20) NOT NULL, " + "att_level INT NOT NULL, " + "att_ai_level INT NOT NULL, " + "att_profession VARCHAR(15) NOT NULL, " + "x_coord INT NOT NULL, " + "y_coord INT NOT NULL, " + "is_victory SMALLINT NOT NULL, " + "tower_battle_id INT NOT NULL, " + "created_at INT NOT NULL)") + self.db.shared.exec( + "CREATE TABLE IF NOT EXISTS tower_battle (" + "id INT PRIMARY KEY AUTO_INCREMENT, " + "playfield_id INT NOT NULL, " + "site_number INT NOT NULL, " + "def_org_name VARCHAR(50) NOT NULL, " + "def_faction VARCHAR(10) NOT NULL, " + "is_finished INT NOT NULL, " + "battle_type VARCHAR(20) NOT NULL, " + "last_updated INT NOT NULL)" + "") + self.db.create_view("tower_battle") + self.db.create_view("tower_attacker") + + @event(event_type=TowerEventController.TOWER_ATTACK_EVENT, description="Create logentries for tower attacks", + is_enabled=False) + def tower_attack_event(self, _, event_data): + t = int(time.time()) + site_number = self.find_closest_site_number(event_data.location.playfield.id, event_data.location.x_coord, + event_data.location.y_coord) + + attacker = event_data.attacker or {} + defender = event_data.defender + + battle = self.find_or_create_battle(event_data.location.playfield.id, site_number, defender.org_name, + defender.faction, "attack", t) + self.db.exec( + "INSERT INTO tower_attacker (att_org_name, att_faction, att_char_id, att_char_name, " + "att_level, att_ai_level, att_profession, " + "x_coord, y_coord, is_victory, tower_battle_id, created_at) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + [attacker.get("org_name", ""), attacker.get("faction", ""), attacker.get("char_id", 0), + attacker.get("name", ""), attacker.get("level", 0), + attacker.get("ai_level", 0), attacker.get("profession", ""), event_data.location.x_coord, + event_data.location.y_coord, 0, battle.id, t]) + + @event(event_type=TowerEventController.TOWER_VICTORY_EVENT, description="Record tower victories", is_enabled=False) + def tower_victory_event(self, _, event_data): + t = int(time.time()) + if event_data.type == "attack": + row = self.get_last_attack(event_data.winner.faction, event_data.winner.org_name, event_data.loser.faction, + event_data.loser.org_name, event_data.location.playfield.id, t, is_finished=0) + + if not row: + site_number = 0 + is_finished = 1 + self.db.exec( + "INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, " + "is_finished, battle_type, last_updated) " + "VALUES (?, ?, ?, ?, ?, ?, ?)", + [event_data.location.playfield.id, site_number, event_data.loser.org_name, event_data.loser.faction, + is_finished, event_data.type, t]) + battle_id = self.db.last_insert_id() + + attacker = event_data.winner or {} + self.db.exec( + "INSERT INTO tower_attacker (att_org_name, att_faction, att_char_id, " + "att_char_name, att_level, att_ai_level, att_profession, " + "x_coord, y_coord, is_victory, tower_battle_id, created_at) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + [attacker.get("org_name", ""), attacker.get("faction", ""), attacker.get("char_id", 0), + attacker.get("name", ""), attacker.get("level", 0), + attacker.get("ai_level", 0), attacker.get("profession", ""), 0, 0, 0, battle_id, t]) + else: + is_victory = 1 + self.db.exec("UPDATE tower_attacker SET is_victory = ? WHERE id = ?", [is_victory, row.attack_id]) + + is_finished = 1 + self.db.exec("UPDATE tower_battle SET is_finished = ?, last_updated = ? WHERE id = ?", + [is_finished, t, row.battle_id]) + + elif event_data.type == "terminated": + site_number = 0 + is_finished = 1 + + row = self.find_similar_attacks(event_data.loser.faction, event_data.loser.org_name, + event_data.location.playfield.id, t, finished=0) + if row: + self.db.exec("UPDATE tower_battle SET is_finished = ?, last_updated = ? WHERE id = ?", + [is_finished, t, row.battle_id]) + else: + self.db.exec( + "INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, " + "is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)", + [event_data.location.playfield.id, site_number, event_data.loser.org_name, event_data.loser.faction, + is_finished, event_data.type, t]) + else: + raise Exception("Unknown victory event type: '%s'" % event_data.type) + + def find_closest_site_number(self, playfield_id, x_coord, y_coord): + # noinspection SqlUnused + sql = """ + SELECT + site_number, + ((x_distance * x_distance) + (y_distance * y_distance)) radius + FROM + (SELECT + playfield_id, + site_number, + min_ql, + max_ql, + x_coord, + y_coord, + site_name, + (x_coord - ?) as x_distance, + (y_coord - ?) as y_distance + FROM + tower_sites + WHERE + playfield_id = ?) t + ORDER BY + radius + LIMIT 1""" + + row = self.db.query_single(sql, [x_coord, y_coord, playfield_id]) + if row: + return row.site_number + else: + return 0 + + def find_or_create_battle(self, playfield_id, site_number, org_name, faction, battle_type, t): + last_updated = t - (8 * 3600) + is_finished = 0 + + sql = """ + SELECT + * + FROM + tower_battle + WHERE + playfield_id = ? + AND site_number = ? + AND is_finished = ? + AND def_org_name = ? + AND def_faction = ? + AND last_updated >= ? + """ + + battle = self.db.query_single(sql, [playfield_id, site_number, is_finished, org_name, faction, last_updated]) + + if battle: + self.db.exec("UPDATE tower_battle SET last_updated = ? WHERE id = ?", [t, battle.id]) + return battle + else: + self.db.exec( + "INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, " + "is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)", + [playfield_id, site_number, org_name, faction, is_finished, battle_type, t]) + return self.db.query_single(sql, [playfield_id, site_number, is_finished, org_name, faction, last_updated]) + + def get_last_attack(self, att_faction, att_org_name, def_faction, def_org_name, playfield_id, t, is_finished=None): + last_updated = t - (8 * 3600) + + sql = f""" + SELECT + b.id AS battle_id, + a.id AS attack_id, + b.playfield_id as pf_id, + b.site_number as site + FROM + tower_battle b + JOIN tower_attacker a ON + a.tower_battle_id = b.id + WHERE + a.att_faction = ? + AND a.att_org_name = ? + AND b.def_faction = ? + AND b.def_org_name = ? + AND b.playfield_id = ? + {'AND b.is_finished = ?' if is_finished is not None else ''} + AND b.last_updated >= ? + ORDER BY + last_updated DESC + LIMIT 1""" + + return self.db.query_single(sql, + [att_faction, att_org_name, def_faction, def_org_name, playfield_id, is_finished, + last_updated] + if is_finished is not None else + [att_faction, att_org_name, def_faction, def_org_name, playfield_id, last_updated]) + + def find_similar_attacks(self, def_faction, def_org_name, playfield_id, t, finished=None): + last_updated = t - (8 * 3600) + + sql = f""" + SELECT + b.id AS battle_id, + a.id AS attack_id, + b.playfield_id as pf_id, + b.site_number as site + + FROM + tower_battle b + JOIN tower_attacker a ON + a.tower_battle_id = b.id + WHERE + b.def_faction = ? + AND b.def_org_name = ? + AND b.playfield_id = ? + {'AND b.is_finished = ?' if finished else ''} + AND b.last_updated >= ? + ORDER BY + last_updated DESC + """ + + return self.db.query_single(sql, + [def_faction, def_org_name, playfield_id, finished, + last_updated] if finished else + [def_faction, def_org_name, playfield_id, + last_updated]) diff --git a/modules/standard/tower/tower_controller.py b/modules/standard/tower/tower_controller.py new file mode 100644 index 0000000..77516b1 --- /dev/null +++ b/modules/standard/tower/tower_controller.py @@ -0,0 +1,170 @@ +import time + +from core.chat_blob import ChatBlob +from core.command_param_types import Const, Int, NamedParameters, Any +from core.decorators import instance, command +from core.igncore import IgnCore +from core.logger import Logger +from core.text import Text +from core.util import Util +from modules.standard.helpbot.playfield_controller import PlayfieldController + + +@instance() +class TowerController: + def __init__(self): + self.logger = Logger(__name__) + + def inject(self, registry): + self.bot: IgnCore = registry.get_instance("bot") + self.db = registry.get_instance("db") + self.text: Text = registry.get_instance("text") + self.util: Util = registry.get_instance("util") + self.tower: TowerController = registry.get_instance("tower_controller") + self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller") + self.public_channel_service = registry.get_instance("public_channel_service") + + @command(command="attacks", params=[Const("battle"), Int("battle_id")], access_level="member", + description="Show battle info for a specific battle") + def attacks_battle_cmd(self, _, _1, battle_id): + battle = self.db.query_single( + "SELECT b.*, p.short_name FROM tower_battle b " + "LEFT JOIN playfields p ON p.id = b.playfield_id WHERE b.id = ?", + [battle_id]) + if not battle: + return "Could not find battle with ID %d." % battle_id + + t = int(time.time()) + attackers = self.db.query("SELECT * FROM tower_attacker WHERE tower_battle_id = ? ORDER BY created_at DESC", + [battle_id]) + + first_activity = attackers[-1].created_at if len(attackers) > 0 else battle.last_updated + + blob = "" + blob += self.format_battle_info(battle, t) + blob += f"Duration: " \ + f"{self.util.time_to_readable(battle.last_updated - first_activity)}\n\n" + blob += "Attackers:\n" + + for row in attackers: + blob += "" + self.format_attacker(row) + blob += " " + self.format_timestamp(row.created_at, t) + blob += "\n" + + return ChatBlob(f"Battle Info {battle_id}", blob) + + @command(command="attacks", params=[NamedParameters(["page"])], access_level="member", + description="Show recent tower attacks and victories") + def attacks_cmd(self, _, named_params): + page = int(named_params.page or "1") + + page_size = 30 + offset = (page - 1) * page_size + + data = self.get_recent_attacks(offset, page_size) + t = int(time.time()) + return self.display(page, data, time.time()) + + @command(command="attacks", + params=[Any("playfield"), Any("site_number", is_optional=True), NamedParameters(["page"])], + access_level="member", + description="Show recent tower attacks and victories") + def cmd_attacks_pf_site(self, _, pf, site, named_params): + page = int(named_params.page or "1") + + page_size = 30 + offset = (page - 1) * page_size + playfield = self.playfield_controller.get_playfield_by_name_or_id(pf) + if not playfield: + return f"Could not find Playfield {pf}." + pf = playfield.id + data = self.get_recent_attacks_by_lca(offset, page_size, pf, site) + blob = self.display(page, data, time.time()) + if site: + blob.page_postfix = f" in {playfield.short_name} on x{site}" + + else: + blob.page_postfix = f" in {playfield.short_name}" + return blob + + def display(self, page, data, t): + blob = "" + + if page > 1: + blob += " " + self.text.make_chatcmd(f"<< Page {page - 1:d}", self.get_chat_command(page - 1)) + if len(data) > 0: + blob += " Page " + str(page) + blob += " " + self.text.make_chatcmd(f"Page {page + 1:d} >>", self.get_chat_command(page + 1)) + blob += "\n" + + current_battle_id = -1 + for row in data: + if current_battle_id != row.battle_id: + blob += "\n" + current_battle_id = row.battle_id + blob += self.format_battle_info(row, t) + blob += self.text.make_tellcmd("More Info", f"attacks battle {row.battle_id:d}") + "\n" + blob += "Attackers:\n" + + blob += "" + self.format_attacker(row) + "\n" + blob = ChatBlob(f"Tower Attacks", blob) + + return blob + + def format_attacker(self, row): + level = f"{row.att_level}/{row.att_ai_level}" if row.att_ai_level > 0 else f"{row.att_level}" + org = row.att_org_name + " " if row.att_org_name else "" + victor = " - Winner!" if row.is_victory else "" + return f"{row.att_char_name or 'Unknown attacker'} ({level} {row.att_profession})" \ + f" {org}({row.att_faction}){victor}" + + def format_battle_info(self, row, t): + blob = "" + defeated = " - Defeated!" if row.is_finished else "" + blob += f"Site: {row.short_name} {row.site_number or '?'}\n" + blob += f"Defender: {row.def_org_name} ({row.def_faction}){defeated}\n" + blob += f"Last Activity: {self.format_timestamp(row.last_updated, t)}\n" + return blob + + def format_timestamp(self, t, current_t): + return f"{self.util.format_datetime(t)} " \ + f"({self.util.time_to_readable(current_t - t)} ago)" + + def get_chat_command(self, page): + return f"/tell attacks --page={page}" + + def get_recent_attacks(self, offset, page_size): + return self.db.query("SELECT b.*, a.*, " + "COALESCE(a.att_level, 0) AS att_level, " + "COALESCE(a.att_ai_level, 0) AS att_ai_level, " + "p.short_name, " + "b.id AS battle_id " + "FROM tower_battle b " + "LEFT JOIN tower_attacker a ON a.tower_battle_id = b.id " + "LEFT JOIN playfields p ON p.id = b.playfield_id " + "ORDER BY b.last_updated DESC, a.created_at DESC " + "LIMIT ?, ?", [offset, page_size]) + + def get_recent_attacks_by_lca(self, offset, page_size, playfield, site_number=None): + if not site_number: + return self.db.query("SELECT b.*, a.*, " + "COALESCE(a.att_level, 0) AS att_level, " + "COALESCE(a.att_ai_level, 0) AS att_ai_level, " + "p.short_name, " + "b.id AS battle_id " + "FROM tower_battle b " + "LEFT JOIN tower_attacker a ON a.tower_battle_id = b.id " + "LEFT JOIN playfields p ON p.id = b.playfield_id " + "WHERE p.id =? ORDER BY b.last_updated DESC, a.created_at DESC " + "LIMIT ?, ?", [playfield, offset, page_size]) + else: + return self.db.query("SELECT b.*, a.*, " + "COALESCE(a.att_level, 0) AS att_level, " + "COALESCE(a.att_ai_level, 0) AS att_ai_level, " + "p.short_name, " + "b.id AS battle_id " + "FROM tower_battle b " + "LEFT JOIN tower_attacker a ON a.tower_battle_id = b.id " + "LEFT JOIN playfields p ON p.id = b.playfield_id " + "WHERE p.id = ? AND b.site_number = ? ORDER BY b.last_updated DESC, a.created_at DESC " + "LIMIT ?, ?", [playfield, site_number, offset, page_size]) diff --git a/modules/raidbot/tower/tower_controller.py b/modules/standard/tower/tower_events.py similarity index 85% rename from modules/raidbot/tower/tower_controller.py rename to modules/standard/tower/tower_events.py index 149477d..7f5c903 100644 --- a/modules/raidbot/tower/tower_controller.py +++ b/modules/standard/tower/tower_events.py @@ -6,16 +6,16 @@ from core.db import DB from core.decorators import instance, event from core.dict_object import DictObject from core.event_service import EventService +from core.igncore import IgnCore from core.logger import Logger from core.lookup.pork_service import PorkService from core.public_channel_service import PublicChannelService from core.text import Text -from core.igncore import IgnCore from modules.standard.helpbot.playfield_controller import PlayfieldController @instance() -class TowerController: +class TowerEventController: TOWER_ATTACK_EVENT = "tower_attack" TOWER_VICTORY_EVENT = "tower_victory" @@ -49,26 +49,15 @@ class TowerController: self.event_service.register_event_type(self.TOWER_ATTACK_EVENT) self.event_service.register_event_type(self.TOWER_VICTORY_EVENT) self.bot.register_packet_handler(server_packets.PublicChannelMessage.id, self.handle_public_channel_message) - self.db.load_sql_file(self.module_dir + "/" + "tower_site.sql", pre_optimized=True) - self.db.create_view("tower_site") + self.db.load_sql_file(self.module_dir + "/" + "tower_sites.sql", pre_optimized=True) + self.db.create_view("tower_sites") - @event(event_type="connect", description="Check if All Towers channel is available", is_hidden=True) + @event(event_type="connect", description="Check if All Towers channel is available") def handle_connect_event(self, _, _1): if self.public_channel_service.org_id and not self.public_channel_service.get_channel_id("All Towers"): self.logger.warning("This bot is a member of an org but does not have access to 'All Towers' channel and " "therefore will not receive tower attack messages") - def format_site_info(self, row): - blob = f"Short name: {row.short_name} {row.site_number:d}\n" - blob += f"Long name: {row.site_name}, {row.long_name}\n" - blob += f"Level range: {row.min_ql:d}-{row.max_ql:d}\n" - blob += "Coordinates: %s\n" % self.text.make_chatcmd(f"{row.x_coord:d}x{row.y_coord:d}", - f"/waypoint {row.x_coord:d} " - f"{row.y_coord:d} " - f"{row.playfield_id:d}") - - return blob - def handle_public_channel_message(self, conn: Conn, packet: server_packets.PublicChannelMessage): if conn.id != "main": return @@ -77,20 +66,21 @@ class TowerController: victory = self.get_victory_event(packet) if victory: - # self.logger.debug("tower victory packet: %s" % str(packet)) + self.logger.info("tower victory packet: %s" % str(packet)) # lookup playfield playfield_name = victory.location.playfield.long_name victory.location.playfield = self.playfield_controller.get_playfield_by_name(playfield_name) or \ DictObject() victory.location.playfield.long_name = playfield_name - # print(victory) + # print("VICTORY", victory) + # VICTORY {'type': 'attack', 'winner': {'faction': 'Clan', 'org_name': 'Komodites'}, 'loser': {'faction': 'Neutral', 'org_name': 'Deez Neuts'}, 'location': {'playfield': {'id': 791, 'long_name': 'Holes in the Wall', 'short_name': 'HITW', 'dungeon': 0}}} self.event_service.fire_event(self.TOWER_VICTORY_EVENT, victory) elif packet.channel_id == self.ALL_TOWERS_ID: attack = self.get_attack_event(packet) if attack: - # self.logger.debug("tower attack packet: %s" % str(packet)) + self.logger.info("tower attack packet: %s" % str(packet)) # lookup playfield playfield_name = attack.location.playfield.long_name @@ -107,10 +97,15 @@ class TowerController: attack.attacker.name = name attack.attacker.faction = faction or attack.attacker.get("faction", "Unknown") attack.attacker.org_name = org_name + # print("ATTK", attack) self.event_service.fire_event(self.TOWER_ATTACK_EVENT, attack) def get_attack_event(self, packet: server_packets.PublicChannelMessage): + + # The %s organization %s just entered a state of war! + # %s attacked the %s organization %s's tower in %s at location (%d,%d). + ATTACK_1 = [506, 12753364] if packet.extended_message and \ [packet.extended_message.category_id, packet.extended_message.instance_id] == self.ATTACK_1: params = packet.extended_message.params @@ -133,6 +128,9 @@ class TowerController: } }) else: + + ATTACK_2 = re.compile(r"^(.+) just attacked the (clan|neutral|omni) organization (.+)'s tower in (.+) " + r"at location \((\d+), (\d+)\).\n$") match = self.ATTACK_2.match(packet.message) if match: return DictObject({ @@ -159,10 +157,13 @@ class TowerController: return None def get_victory_event(self, packet: server_packets.PublicChannelMessage): + # Does not contain any relevant data. match = self.VICTORY_1.match(packet.message) if match: return None + VICTORY_2 = re.compile(r"^The (Clan|Neutral|Omni) organization (.+) attacked the (Clan|Neutral|Omni) (.+) " + r"at their base in (.+). The attackers won!!$") match = self.VICTORY_2.match(packet.message) if match: return DictObject({ @@ -188,8 +189,8 @@ class TowerController: return DictObject({ "type": "terminated", "winner": { - "faction": params[0].capitalize(), - "org_name": params[1] + "faction": "", # params[0].capitalize(), + "org_name": "", # params[1] }, "loser": { "faction": params[0].capitalize(), diff --git a/modules/standard/tower/tower_sites.sql b/modules/standard/tower/tower_sites.sql new file mode 100644 index 0000000..1748205 --- /dev/null +++ b/modules/standard/tower/tower_sites.sql @@ -0,0 +1,282 @@ +DROP TABLE IF EXISTS `tower_sites`; +CREATE TABLE IF NOT EXISTS `tower_sites` +( + `playfield_id` int(11) NOT NULL, + `site_number` smallint(6) NOT NULL, + `min_ql` smallint(6) NOT NULL, + `max_ql` smallint(6) NOT NULL, + `x_coord` smallint(6) NOT NULL, + `y_coord` smallint(6) NOT NULL, + `site_name` varchar(50) NOT NULL, + `timing` int(11) NOT NULL, + `enabled` int(11) NOT NULL, + PRIMARY KEY (`playfield_id`, `site_number`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; +INSERT INTO `tower_sites` (`playfield_id`, `site_number`, `min_ql`, `max_ql`, `x_coord`, `y_coord`, `site_name`, + `timing`, `enabled`) +VALUES (505, 1, 60, 90, 2740, 4260, 'Griffon Frontier', 1, 1), + (505, 2, 80, 110, 540, 4180, 'Draught', 1, 1), + (505, 3, 70, 95, 1740, 3460, 'Dreadfire Volcano', 1, 1), + (505, 4, 80, 120, 2780, 3420, 'Northeast Barren Lands', 1, 1), + (505, 5, 60, 90, 580, 3140, 'Western Desert', 1, 1), + (505, 6, 50, 75, 2420, 1900, 'Waylander Mines', 1, 1), + (505, 7, 70, 100, 1860, 1700, 'North of Main Omni Base', 1, 1), + (505, 8, 61, 82, 460, 1380, 'Dome Ore', 1, 1), + (505, 9, 100, 150, 2700, 620, 'Crystal Forge Volcano', 1, 1), + (505, 10, 100, 150, 660, 460, 'SW Low Plateau', 1, 1), + (550, 1, 10, 20, 2660, 2020, 'Sifter Beach', 2, 1), + (550, 2, 20, 30, 1780, 1780, 'Academy Ore', 2, 1), + (550, 3, 15, 25, 1980, 1340, 'Athen Fault', 2, 1), + (550, 4, 10, 20, 2660, 820, 'Grindmoore', 2, 1), + (550, 5, 15, 23, 1380, 380, 'Gladius Grove', 2, 1), + (551, 1, 40, 90, 1700, 3700, 'Styx Magma', 0, 1), + (551, 2, 35, 50, 2220, 3340, 'Carbon Grove', 0, 1), + (551, 3, 26, 50, 980, 3140, 'Between the Craters', 0, 1), + (551, 4, 25, 35, 340, 2420, 'Powdered Dunes', 0, 1), + (551, 5, 32, 45, 2540, 2060, 'Dust Bank', 0, 1), + (551, 6, 20, 30, 580, 1740, 'Charred Groove', 0, 1), + (551, 7, 12, 45, 940, 1540, 'West of Perdition', 0, 1), + (551, 8, 15, 30, 660, 900, 'North of Yuttos', 0, 1), + (560, 1, 100, 170, 1500, 3420, 'Terraform Edge', 1, 1), + (560, 2, 170, 250, 3060, 3020, 'West Spirals', 1, 1), + (560, 3, 170, 250, 3500, 2980, 'East Spirals', 1, 1), + (560, 4, 130, 170, 1220, 2220, 'Middle Mort Desert', 1, 1), + (560, 5, 1, 100, 900, 1460, 'Green Crater', 1, 1), + (560, 6, 110, 160, 3100, 1460, 'Oasis Ore', 1, 1), + (560, 7, 150, 200, 2740, 700, 'South East Craterwall', 1, 1), + (560, 8, 100, 150, 540, 540, 'South West Craterwall', 1, 1), + (560, 9, 160, 210, 2780, 540, 'Stormshelter', 1, 1), + (565, 1, 25, 40, 2940, 2900, 'Rich Desert Ridge', 2, 1), + (565, 2, 30, 45, 1980, 2580, 'East of Meetmedere', 2, 1), + (565, 3, 50, 75, 540, 2020, 'Middle of Western Desert', 2, 1), + (565, 4, 40, 60, 2580, 1940, 'North of Rhino Village', 2, 1), + (565, 5, 40, 60, 2700, 1260, 'South of Rhino Village', 2, 0), + (567, 1, 12, 20, 1220, 1060, 'In the Newland Desert', 1, 1), + (567, 2, 15, 25, 540, 460, 'West of Newland Lake', 1, 1), + (570, 1, 200, 300, 3220, 3020, 'North of Cyborg Hideout', 1, 1), + (570, 2, 191, 250, 3780, 2540, 'Middle of Liberty', 1, 1), + (570, 3, 120, 180, 980, 2060, 'South of Sabulum', 1, 1), + (570, 4, 190, 230, 3940, 2060, 'Cyborg Border', 1, 1), + (570, 5, 200, 300, 2820, 1820, 'Middle of Perpetual Wastelands', 1, 1), + (570, 6, 200, 300, 3740, 1700, 'South of Cyborg Hideout', 1, 1), + (570, 7, 100, 150, 1500, 1340, 'Lower Plateu Zone', 1, 1), + (570, 8, 100, 150, 2100, 1380, 'The Mid Canyon Crossing', 1, 1), + (570, 9, 120, 180, 3020, 1220, 'Plains of dust', 1, 1), + (570, 10, 100, 150, 900, 1060, 'West of Canyon', 1, 1), + (570, 11, 100, 150, 3180, 940, 'The Canyon Mines', 1, 1), + (570, 12, 190, 230, 2300, 780, 'South of Canyon', 1, 1), + (585, 1, 40, 60, 1220, 2740, 'Northern Wastelands', 1, 1), + (585, 2, 11, 16, 2180, 2580, 'West Wastelands', 1, 1), + (585, 3, 40, 55, 1020, 2460, 'Mid Wastelands', 1, 1), + (585, 4, 30, 45, 2140, 1660, 'Giant Green River Bank North', 1, 1), + (585, 5, 11, 16, 1180, 1340, 'West of the Dead Forest', 1, 1), + (585, 6, 30, 45, 2100, 1340, 'Giant Green River Bank South', 1, 1), + (585, 7, 15, 22, 1420, 1020, 'Canyon East', 1, 1), + (585, 8, 25, 35, 820, 780, 'Canyon South', 1, 1), + (585, 9, 35, 50, 900, 460, 'By the River', 1, 1), + (590, 1, 140, 200, 1740, 3100, 'By the Fisher Village', 2, 1), + (590, 2, 140, 200, 2100, 3060, 'Fisher Village Approach', 2, 1), + (590, 3, 100, 170, 2900, 2820, 'North Forest', 2, 1), + (590, 4, 90, 130, 3340, 2700, 'North-east Forest', 2, 1), + (590, 5, 130, 170, 860, 1220, 'North-west of Lava Ditches', 2, 1), + (590, 6, 100, 150, 3100, 980, 'Mid Clutching Forest', 2, 1), + (590, 7, 130, 170, 860, 780, 'South-west of Lava Ditches', 2, 1), + (590, 8, 100, 150, 3180, 620, 'South Clutching Forest', 2, 1), + (595, 1, 100, 150, 1140, 3380, 'Old ruins', 0, 1), + (595, 2, 100, 150, 3180, 2900, 'Plains of defense', 0, 1), + (595, 3, 130, 180, 1740, 2300, 'The haunted forest outskirt', 0, 1), + (595, 4, 130, 180, 900, 2220, 'Forest of Xzawkaz', 0, 1), + (595, 5, 200, 300, 2260, 1860, 'In the Swamp of Horrors', 0, 1), + (595, 6, 130, 180, 1420, 1500, 'Island of Control', 0, 1), + (595, 7, 130, 180, 1340, 1140, 'The swamp of hope', 0, 1), + (595, 8, 200, 300, 2900, 1100, 'South of the Medusa', 0, 1), + (595, 9, 140, 210, 2140, 780, 'Middle of the Foul Forest', 0, 1), + (595, 10, 200, 300, 540, 540, 'Southern Forest of Xzawkaz', 0, 1), + (600, 1, 30, 45, 2420, 2980, 'By the Rivers Edge', 1, 1), + (600, 2, 50, 75, 620, 2900, 'North Forest Road', 1, 1), + (600, 3, 25, 50, 1300, 2660, 'Along the Rivers Edge', 1, 1), + (600, 4, 30, 45, 3740, 2500, 'East Forest', 1, 1), + (600, 5, 25, 50, 3140, 2020, 'Rhino Hills', 1, 1), + (600, 6, 50, 75, 580, 1700, 'West Forest', 1, 1), + (600, 7, 60, 90, 1940, 1620, 'Crossroads', 1, 1), + (600, 8, 60, 90, 1140, 1500, 'Forestdawn', 1, 1), + (600, 9, 50, 75, 3220, 1140, 'East of Crater', 1, 1), + (605, 1, 160, 200, 2940, 2820, 'Forest Waters', 2, 1), + (605, 2, 110, 120, 1100, 2620, 'Muddy Pools', 2, 1), + (605, 3, 100, 150, 1700, 2300, 'West of Wine', 2, 1), + (605, 4, 120, 180, 2940, 2260, 'East of Wine', 2, 1), + (605, 5, 130, 195, 1900, 1740, 'Central Belial Forest', 2, 1), + (605, 6, 130, 190, 2500, 1660, 'River Delta', 2, 1), + (605, 7, 160, 200, 2540, 1220, 'Junction Forest', 2, 1), + (605, 8, 100, 150, 2340, 860, 'Borderline', 2, 1), + (605, 9, 120, 180, 2020, 420, 'Southern belial Mine', 2, 1), + (605, 10, 140, 200, 620, 380, 'Southwest Belial Mining District', 2, 1), + (610, 1, 60, 90, 1380, 2780, 'Tetlies Land control area', 1, 1), + (610, 2, 80, 120, 2900, 2660, 'East of the Great Marsh', 1, 1), + (610, 3, 60, 90, 660, 2460, 'West of outpost 10-3', 1, 1), + (610, 4, 100, 150, 2300, 2020, 'Defense of Geholva', 1, 1), + (610, 5, 106, 143, 2740, 1180, 'South of Forest of Geholva', 1, 1), + (610, 6, 120, 180, 860, 900, 'Avid Crater', 1, 1), + (610, 7, 120, 180, 1540, 900, 'East of Avid Crater', 1, 1), + (610, 8, 100, 150, 2460, 540, 'Bendelham forest Defense', 1, 1), + (615, 1, 60, 100, 1900, 3020, 'North of Lenne', 1, 1), + (615, 2, 100, 150, 860, 2820, 'Little Hawaii Defense', 1, 1), + (615, 3, 90, 120, 2620, 2660, 'Defense of Zoto', 1, 1), + (615, 4, 60, 100, 900, 2100, 'By the Ocean', 1, 1), + (615, 5, 61, 100, 2300, 1180, 'Birm', 1, 0), + (615, 6, 120, 180, 2700, 660, 'SFH Defense', 1, 1), + (615, 7, 100, 150, 1860, 500, 'South in Nightplain', 1, 1), + (620, 1, 150, 200, 2700, 3860, 'Krud the Lost Valley Defense', 2, 1), + (620, 2, 150, 225, 1900, 3180, 'Pranade', 2, 1), + (620, 3, 120, 180, 620, 2980, 'Plains of Jarga Defense', 2, 1), + (620, 4, 200, 300, 2460, 2260, 'Old Plains', 2, 1), + (620, 5, 200, 300, 1540, 1780, 'Middle of Easter Fouls Plains', 2, 1), + (620, 6, 130, 200, 1540, 1140, 'Clefre Defense', 2, 1), + (620, 7, 100, 150, 2020, 860, 'Central Sharewood', 2, 1), + (620, 8, 200, 300, 820, 540, 'Pegradul', 2, 1), + (625, 1, 90, 130, 1460, 1940, 'The Resilient Forest - North', 0, 1), + (625, 2, 90, 120, 1900, 1540, 'The Resilient Forest - East', 0, 1), + (625, 3, 125, 170, 2780, 1380, 'Central Prowler Waste', 0, 1), + (625, 4, 100, 125, 1380, 1180, 'Central Resilient Forest', 0, 1), + (625, 5, 125, 170, 2860, 1020, 'Southern Prowler Waste', 0, 1), + (625, 6, 100, 150, 4020, 980, 'The Barren Hills', 0, 1), + (625, 7, 100, 125, 1740, 860, 'The Resilient Forest - South', 0, 1), + (625, 8, 50, 75, 2460, 540, 'The Silent Woods - East', 0, 1), + (630, 1, 40, 60, 1540, 2660, 'Pleasant Range Offense Hill', 0, 1), + (630, 2, 60, 90, 2380, 2500, 'Central Pleasant Range', 0, 1), + (630, 3, 50, 75, 580, 2420, 'West of 20K', 0, 1), + (630, 4, 30, 70, 3220, 2220, 'Pleasant Range Defense', 0, 1), + (630, 5, 60, 90, 3220, 1980, 'Pleasant River Defense', 0, 1), + (630, 6, 60, 90, 3260, 1500, 'Pleasant River Offense', 0, 1), + (630, 7, 40, 60, 2260, 1140, 'Central Pleasant Plains', 0, 1), + (630, 8, 30, 70, 3020, 1020, 'East Pleasant Plains', 0, 1), + (630, 9, 30, 45, 740, 460, 'West of Versailles Tower', 0, 1), + (635, 1, 55, 70, 700, 2420, 'Northern River Bank', 2, 0), + (635, 2, 60, 90, 1780, 2460, 'Hawker Trench', 2, 1), + (635, 3, 70, 105, 1460, 1740, 'Klapam Forest Defense', 2, 1), + (635, 4, 55, 70, 2020, 1740, 'Klompfot Defense', 2, 1), + (635, 5, 70, 105, 1900, 1220, 'South of Trench', 2, 1), + (635, 6, 80, 120, 1140, 940, 'Nile Hills', 2, 1), + (635, 7, 55, 70, 1780, 700, 'Aprils Rock Offense', 2, 1), + (635, 8, 80, 150, 820, 420, 'Southern Lower River Bank', 2, 1), + (635, 9, 80, 150, 1700, 340, 'Aprils Rock Defense', 2, 1), + (646, 1, 10, 15, 460, 1300, 'Great W. Forest Vein', 1, 1), + (646, 2, 10, 15, 2940, 980, 'The Hidden Notum Canal', 1, 1), + (646, 3, 20, 30, 3220, 620, 'Mountain Areas', 1, 1), + (646, 4, 10, 15, 580, 580, 'Great W. Forest Dorsal', 1, 1), + (646, 5, 10, 15, 1500, 460, 'Western Mountain Areas', 1, 1), + (647, 1, 90, 135, 1100, 3100, 'The Mineral Mine', 1, 1), + (647, 2, 20, 30, 2900, 2940, 'NE Desert Aperient', 1, 1), + (647, 3, 37, 64, 1900, 2700, 'SurroundingTemple of Three Winds', 1, 1), + (647, 4, 25, 40, 2220, 1900, 'Piercing Thundertube', 1, 1), + (647, 5, 30, 45, 2820, 1940, 'Central Striking Ant', 1, 1), + (647, 6, 25, 40, 620, 1660, 'Tir Prairie', 1, 1), + (647, 7, 25, 40, 1180, 1700, 'Crater Swamp', 1, 1), + (650, 1, 50, 75, 540, 2820, 'West Pass', 2, 1), + (650, 2, 65, 75, 900, 2300, 'Crowning Shallows', 2, 1), + (650, 3, 100, 150, 1660, 2180, 'Haven Notum Crematorium', 2, 1), + (650, 4, 70, 140, 2020, 1740, 'Stret Vale Deux Drilling Field', 2, 1), + (650, 5, 120, 180, 1340, 1620, 'The Flooded Bottomland', 2, 1), + (650, 6, 75, 90, 1820, 740, 'Stret Woods', 2, 1), + (650, 7, 60, 90, 940, 420, 'Greenslopes', 2, 1), + (655, 1, 30, 45, 420, 2700, 'Skop Notum Mine', 1, 1), + (655, 2, 30, 80, 2820, 2340, 'Klor', 1, 1), + (655, 3, 60, 80, 2820, 1660, 'Harstad', 1, 1), + (655, 4, 40, 90, 540, 1580, 'Ubleo', 1, 1), + (655, 5, 40, 60, 1420, 1580, 'Flubu Notum Mine', 1, 1), + (655, 6, 40, 70, 4340, 900, 'Plago', 1, 1), + (655, 7, 60, 80, 2260, 380, 'jucha', 1, 1), + (655, 8, 70, 105, 4380, 380, 'Mune', 1, 1), + (655, 9, 30, 60, 820, 340, 'Mocnuf Notum Mine', 1, 1), + (665, 1, 80, 150, 940, 4820, 'Central Desert north', 2, 1), + (665, 2, 45, 75, 1260, 3860, 'Notum Disruption Mountain', 2, 1), + (665, 3, 75, 110, 1940, 3860, 'The Notum Plains', 2, 1), + (665, 4, 100, 150, 940, 3380, 'Near Clan Outpost', 2, 1), + (665, 5, 45, 80, 1300, 3060, 'Central Mountains', 2, 1), + (665, 6, 55, 150, 380, 2300, 'Surrounding Evil', 2, 1), + (665, 7, 45, 60, 1260, 2140, 'Notum Mountain', 2, 1), + (665, 8, 55, 100, 2020, 1980, 'Near Omni-Tek Outpost', 2, 1), + (665, 9, 100, 150, 420, 820, 'Shores Notum Vein', 2, 1), + (670, 1, 30, 45, 1100, 4340, 'Yukon Source', 2, 1), + (670, 2, 35, 50, 1460, 2540, 'Frisko', 2, 1), + (670, 3, 30, 45, 2140, 2420, 'Round Hills', 2, 1), + (670, 4, 50, 75, 2140, 1900, 'Dense Drewen', 2, 1), + (670, 5, 35, 50, 1260, 1820, 'Borrowed Hill', 2, 1), + (670, 6, 35, 50, 1340, 1340, 'Narrow Lune', 2, 1), + (670, 7, 10, 15, 2500, 1220, 'Micron Slopes Notum Mine', 2, 1), + (670, 8, 50, 75, 2100, 540, 'High Juniper', 2, 1), + (670, 9, 50, 75, 2300, 460, 'High Juniper Notum Vein', 2, 1), + (685, 1, 35, 50, 2140, 2620, 'Nature Reverve - East', 2, 1), + (685, 2, 35, 50, 1900, 2580, 'Nature Reverve - West', 2, 1), + (685, 3, 50, 75, 1300, 1900, 'Poole - West', 2, 1), + (685, 4, 50, 75, 1580, 1820, 'Poole - East', 2, 1), + (685, 5, 15, 25, 1140, 1100, 'V-Hill', 2, 1), + (685, 6, 20, 30, 1580, 700, 'Lunder Hills - North', 2, 1), + (685, 7, 25, 40, 2740, 460, 'Galway hills', 2, 1), + (685, 8, 20, 30, 1220, 380, 'Lunder Hills', 2, 1), + (685, 9, 25, 40, 2260, 380, 'South-east Woods', 2, 1), + (687, 1, 10, 15, 500, 1900, 'Blossom Valley', 2, 1), + (687, 2, 10, 15, 380, 1300, 'Konty Passage Plains', 2, 1), + (687, 3, 17, 28, 900, 1220, 'Vas\' Pass', 2, 1), + (687, 4, 15, 25, 780, 900, 'Arthur\'s Pass', 2, 1), + (687, 5, 10, 15, 380, 580, 'Kontys Sixth Passage - West', 2, 1), + (687, 6, 10, 15, 620, 540, 'Kontys Sixth Passage - East', 2, 1), + (695, 1, 30, 45, 940, 3260, 'North West Lush Fields', 0, 1), + (695, 2, 20, 30, 2420, 3180, 'North East Lush Fields', 0, 1), + (695, 3, 10, 40, 3460, 2940, 'Stret River Island', 0, 1), + (695, 4, 40, 60, 1260, 2460, 'West of Outpost', 0, 1), + (695, 5, 35, 60, 1740, 2460, 'East of Outpost', 0, 1), + (695, 6, 20, 30, 1780, 1820, 'Central Lush Fields', 0, 1), + (695, 7, 10, 15, 2860, 420, 'South East Lush Fields', 0, 1), + (695, 8, 30, 45, 980, 380, 'South West Lush Fields', 0, 1), + (696, 1, 15, 25, 780, 1420, 'Mutant Domain North', 2, 1), + (696, 2, 20, 30, 500, 860, 'Mutant Domain Central', 2, 1), + (696, 3, 25, 40, 780, 460, 'Mutant Domain South', 2, 1), + (716, 1, 20, 35, 500, 3220, 'Northern Grassland', 0, 1), + (716, 2, 15, 30, 980, 3020, 'Moderate Grassland', 0, 1), + (716, 3, 10, 20, 460, 2180, 'Dungeon Hilltop', 0, 1), + (716, 4, 10, 15, 700, 2180, 'Rocky Upsurge', 0, 1), + (716, 5, 15, 25, 340, 1420, 'Northern Easy Swamps Notum Field', 0, 1), + (716, 6, 15, 26, 460, 820, 'Ocean Inlet', 0, 1), + (717, 1, 30, 45, 1620, 2660, 'Greater Omni Forest Swamps', 1, 1), + (717, 2, 15, 25, 1180, 2460, 'Dragonback Ridge', 1, 1), + (717, 3, 30, 45, 1900, 1820, 'Mountainous Regions', 1, 1), + (717, 4, 20, 35, 1860, 1340, 'Waterfall Swamp', 1, 1), + (717, 5, 10, 15, 1500, 1300, 'Greater Omni Forest South', 1, 1), + (717, 6, 25, 40, 900, 1220, 'Northern Semi-Barren Area', 1, 1), + (717, 7, 10, 25, 1940, 900, 'Ring Mountain Range', 1, 1), + (717, 8, 14, 25, 940, 460, 'Southern Isle', 1, 1), + (760, 1, 60, 90, 1580, 2380, 'Notum Ore in Buttu', 2, 1), + (760, 2, 35, 50, 940, 2020, 'Mountain of Fourtyone', 2, 1), + (760, 3, 35, 50, 1300, 1980, 'Mountain in 4Holes', 2, 1), + (760, 4, 45, 60, 1660, 1740, 'South of Ahenus', 2, 1), + (760, 5, 70, 100, 1820, 1340, 'Ibreri Woods North', 2, 1), + (760, 6, 35, 50, 1460, 1260, 'Mountain of Fourtytwo', 2, 1), + (760, 7, 100, 150, 1740, 1060, 'Ibreri Woods', 2, 1), + (760, 8, 45, 70, 1180, 500, 'Ibreri', 2, 1), + (760, 9, 45, 70, 460, 420, 'Jall Mountain', 2, 1), + (790, 1, 20, 30, 1700, 3100, 'Hells Courtyard', 2, 1), + (790, 2, 15, 25, 2300, 2860, 'Pondus Beach', 2, 1), + (790, 3, 15, 30, 1700, 2780, 'Hound Land', 2, 1), + (790, 4, 20, 40, 1980, 2780, 'Hound Notum Field', 2, 1), + (790, 5, 20, 30, 1700, 1940, 'East Mutie', 2, 1), + (790, 6, 12, 30, 1340, 1220, 'Omni Outpost', 2, 1), + (790, 7, 20, 30, 660, 1180, 'South Mutie', 2, 1), + (790, 8, 30, 60, 2260, 1140, 'The Beach', 2, 1), + (791, 1, 15, 26, 420, 2020, 'Populous Mountain', 2, 1), + (791, 2, 12, 22, 660, 1500, 'Hound Land Mining', 2, 1), + (791, 3, 12, 20, 220, 1060, 'Stret West Notum Ore', 2, 1), + (791, 4, 10, 15, 740, 820, 'Snake Mountain', 2, 1), + (791, 5, 20, 40, 780, 460, 'Southern Empty Wastes and Roads', 2, 1), + (791, 6, 10, 20, 380, 340, 'Transit Valley Ore', 2, 1), + (795, 1, 40, 60, 4220, 1580, 'Illuminati', 1, 1), + (795, 2, 100, 150, 500, 1540, 'Northern Forest of Illuminations', 1, 0), + (795, 3, 25, 50, 3420, 1540, 'Fate Notum Field', 1, 1), + (795, 4, 71, 120, 580, 820, 'Pegrama', 1, 1), + (795, 5, 84, 120, 1220, 700, 'Grazeland Notum Field', 1, 1), + (795, 6, 50, 75, 4020, 620, 'Winterbottom', 1, 1), + (795, 7, 90, 120, 540, 500, 'Southern Forest of Illuminations', 1, 1), + (795, 8, 60, 90, 2900, 500, 'Summer', 1, 0); + + diff --git a/modules/standard/tower/tower_spam_controller.py b/modules/standard/tower/tower_spam_controller.py new file mode 100644 index 0000000..6b25354 --- /dev/null +++ b/modules/standard/tower/tower_spam_controller.py @@ -0,0 +1,97 @@ +import time + +from core.decorators import instance, event +from core.igncore import IgnCore +from core.logger import Logger +from core.message_hub_service import MessageHubService +from core.text import Text, MLStripper +from core.util import Util +from modules.standard.helpbot.playfield_controller import PlayfieldController +from modules.standard.tower.tower_attack_controller import TowerAttackController +from modules.standard.tower.tower_controller import TowerController +from modules.standard.tower.tower_events import TowerEventController + + +@instance() +class TowerSpamController: + SOURCE = "TowerInfo" + + def __init__(self): + self.logger = Logger(__name__) + + def inject(self, registry): + self.bot: IgnCore = registry.get_instance("bot") + self.db = registry.get_instance("db") + self.text: Text = registry.get_instance("text") + self.util: Util = registry.get_instance("util") + self.tower: TowerEventController = registry.get_instance("tower_controller") + self.tc: TowerController = registry.get_instance("tower_controller") + self.tac: TowerAttackController = registry.get_instance("tower_attack_controller") + self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller") + self.msg_hub: MessageHubService = registry.get_instance("message_hub_service") + + def pre_start(self): + self.msg_hub.register_message_source(self.SOURCE) + + @event(event_type=TowerEventController.TOWER_ATTACK_EVENT, description="NW Warnings, for attacks") + def tower_attack_event(self, _, event_data): + t = int(time.time()) + site_number = self.tac.find_closest_site_number(event_data.location.playfield.id, event_data.location.x_coord, + event_data.location.y_coord) + + # self.logger.info("ATTK ") + # self.logger.info(event_data) + # self.logger.info(" site: ") + # [11.11.2021 04:35:58] INFO :: modules.standard.tower.tower_spam_controller -> ATTK | + # [11.11.2021 04:35:58] INFO :: modules.standard.tower.tower_spam_controller -> {'attacker': {'char_id': 984112, 'name': 'Flexiblex', 'first_name': '', 'last_name': '', 'leve| + # l': 117, 'breed': 'Opifex', 'gender': 'Female', 'faction': 'Omni', 'profession': 'Meta-Physicist', 'profession_title': 'ArchPriest', 'ai_rank': 'Adept', 'ai_level': 13, 'or| + # g_id': 9655, 'org_name': 'Northern Star', 'org_rank_name': 'Unit Leader', 'org_rank_id': 4, 'dimension': 5, 'head_id': 40240, 'pvp_rating': 1503, 'pvp_title': 'Rookie', 'so| + # urce': 'people.anarchy-online.com', 'last_updated': 1636601503, 'cache_age': 3855}, 'defender': {'faction': 'Omni', 'org_name': 'Weyland Yutani'}, 'location': {'playfield':| + # {'id': 795, 'long_name': 'The Longest Road', 'short_name': 'TLR', 'dungeon': 0}, 'x_coord': 526, 'y_coord': 538}} | + # [11.11.2021 04:35:58] INFO :: modules.standard.tower.tower_spam_controller -> site: | + # [11.11.2021 04:35:58] INFO :: modules.standard.tower.tower_spam_controller -> 7 + # self.logger.info(site_number) + self.send_nw( + f"[NW] {self.text.get_formatted_faction(event_data.attacker.faction, event_data.attacker.org_name)} " + f"[{self.text.get_formatted_faction(event_data.attacker.faction, event_data.attacker.name)} ({event_data.attacker.level}/{event_data.attacker.ai_level})" + f" -> {self.util.get_profession(event_data.attacker.profession)}] " + f"attacked {self.text.get_formatted_faction(event_data.defender.faction, event_data.defender.org_name)} " + f"at {event_data.location.playfield.short_name} x{site_number}") + + @event(event_type=TowerEventController.TOWER_VICTORY_EVENT, description="Send NW warnings") + def tower_victory_event(self, _, event_data): + t = int(time.time()) + row = None + if event_data.type == "attack": + row = self.tac.get_last_attack(event_data.winner.faction, event_data.winner.org_name, + event_data.loser.faction, + event_data.loser.org_name, event_data.location.playfield.id, t) + # self.logger.info("ATTK_VICT ") + # self.logger.info(event_data) + # self.logger.info("ATTK_VICT ") + # self.logger.info(row) + + self.send_nw( + f"[NW] {self.text.get_formatted_faction(event_data.winner.faction, event_data.winner.org_name)} " + f"won against {self.text.get_formatted_faction(event_data.loser.faction, event_data.loser.org_name)} at {event_data.location.playfield.short_name} x{row.site}") + elif event_data.type == "terminated": + row = self.tac.find_similar_attacks(event_data.loser.faction, event_data.loser.org_name, + event_data.location.playfield.id, t) + # VICTORY {'type': 'terminated', 'winner': {'faction': '', 'org_name': ''}, 'loser': {'faction': 'Omni', 'org_name': 'Do you smell the scared boxes'}, 'location': {'playfield + # ': {'id': 795, 'long_name': 'The Longest Road', 'short_name': 'TLR', 'dungeon': 0}}} + if row: + # self.logger.info("ATTK_TERM ") + # self.logger.info(event_data) + # self.logger.info("TERMINATED ") + # self.logger.info(row) + + self.send_nw(f"[NW] Site terminated in {event_data.location.playfield.long_name}: " + f"{self.text.get_formatted_faction(event_data.loser.faction, event_data.loser.org_name)} at {event_data.location.playfield.short_name} x{row.site}") + else: + raise Exception("Unknown victory event type: '%s'" % event_data.type) + + def send_nw(self, message): + stripper = MLStripper() + stripper.feed(message) + clean = stripper.get_data() + self.msg_hub.send_message(self.SOURCE, None, clean, message) diff --git a/modules/standard/track/track_controller.py b/modules/standard/track/track_controller.py index bff2022..21ec62d 100644 --- a/modules/standard/track/track_controller.py +++ b/modules/standard/track/track_controller.py @@ -9,6 +9,7 @@ from core.db import DB from core.decorators import instance, command, event from core.dict_object import DictObject from core.event_service import EventService +from core.igncore import IgnCore from core.job_scheduler import JobScheduler from core.lookup.pork_service import PorkService from core.message_hub_service import MessageHubService @@ -16,10 +17,9 @@ from core.private_channel_service import PrivateChannelService from core.setting_service import SettingService from core.setting_types import TextSettingType, ColorSettingType from core.text import Text -from core.igncore import IgnCore from core.util import Util from modules.core.accounting.services.account_service import AccountService -from modules.raidbot.tower.tower_controller import TowerController +from modules.standard.tower.tower_events import TowerEventController # noinspection DuplicatedCode,SqlCaseVsIf @@ -41,7 +41,7 @@ class TrackController(BaseModule): self.account_service: AccountService = registry.get_instance("account_service") self.db: DB = registry.get_instance("db") self.priv: PrivateChannelService = registry.get_instance("private_channel_service") - self.tower: TowerController = registry.get_instance("tower_controller") + self.tower: TowerEventController = registry.get_instance("tower_controller") self.message_hub_service: MessageHubService = registry.get_instance("message_hub_service") def pre_start(self): @@ -53,7 +53,7 @@ class TrackController(BaseModule): "Color for Track logoff") self.setting_service.register(self.module_name, "autotrack", 'none', TextSettingType(['omni', 'clan', 'neutral', "none"]), - "Autotrack all players initiating tower attacks towards this faction:") + "Autotrack all players initiating tower attacks towards this faction") self.db.exec( "CREATE TABLE IF NOT EXISTS track(" "char_id int not null primary key, " @@ -103,7 +103,7 @@ class TrackController(BaseModule): color = self.setting_service.get("track_off_color").format_text("OFF") self.send_t_warn(0, f'{color} :: {self.text.format_char_info(user)}') - @event(event_type=TowerController.TOWER_ATTACK_EVENT, description="Autottrack players attacking our faction") + @event(event_type=TowerEventController.TOWER_ATTACK_EVENT, description="Autottrack players attacking our faction") def tower_attack_event(self, _, event_data): attacker = event_data.attacker if event_data.defender.faction.lower() == self.setting_service.get_value("autotrack"): diff --git a/modules/standard/wants/wants_controller.py b/modules/standard/wants/wants_controller.py index 941b8d5..a41d858 100644 --- a/modules/standard/wants/wants_controller.py +++ b/modules/standard/wants/wants_controller.py @@ -98,6 +98,7 @@ class WantsController: LEFT JOIN account a ON w.char_id = a.char_id \ LEFT JOIN account a2 ON (a2.main = a.main) \ LEFT JOIN player p ON p.char_id = COALESCE(a2.char_id, w.char_id) \ + WHERE a2.char_id = a2.main \ ORDER BY p.name" data = self.db.query(sql) diff --git a/modules/standard/whois/org_list_controller.py b/modules/standard/whois/org_list_controller.py index 42d8dc7..dfbb0b2 100644 --- a/modules/standard/whois/org_list_controller.py +++ b/modules/standard/whois/org_list_controller.py @@ -116,7 +116,7 @@ class OrgListController: for org in orgs: if Registry.get_instance('org_alias_controller', is_optional=True): blob += f'[{self.text.make_chatcmd("Add", f"/tell orgs add {org.org_id}")}]' - blob += f'[{self.text.make_chatcmd("More", f"/tell org info {org.org_id}")}]' + blob += f'[{self.text.make_chatcmd("More", f"/tell orgs info {org.org_id}")}]' blob += f' {org.org_name} ' \ f'({org.org_id}) <{org.faction.lower()}>{org.faction} ' \ f'[{org.member_count} members]
'