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 Tyrbot 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: Tyrbot = 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 = "" if account := self.account_service.get_account(event_data.attacker.char_id): 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 += "Site: %s %s\n" % (row.short_name, row.site_number or "?") blob += "Defender: %s (%s)%s\n" % (row.def_org_name, row.def_faction, defeated) blob += "Last Activity: %s\n" % self.format_timestamp(row.last_updated, t) return blob def format_timestamp(self, t, current_t): return "%s (%s ago)" % ( self.util.format_datetime(t), self.util.time_to_readable(current_t - t)) def get_chat_command(self, page): return "/tell attacks --page=%d" % 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 ""