Files
igncore/modules/raidbot/tower/tower_attack_controller.py
T
Minidodo 810c2c8c4d Fix for mrelay - some bots are sending malformed messages, caused by wrong usage of their modules.
Accessing an external service through the bot for gathering tower data is nolonger supported; and should be done via external scripts.
Fix for callers, and missing alias'es for loot tables.
!accounts will only show mains now.
the character order of all alt lists has been reversed: [main] high => low instead of [main] low => high
!account add <name> also marks accounts as type 0, if an account gets re-enabled. might cause strange behaviour with member-logs, if used in onlinebots.
Member type is being displayed in !account now. [Member (X)]
2021-09-19 14:09:44 +02:00

368 lines
17 KiB
Python

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<pagebreak>"
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 += "<header2>Attackers:</header2>\n"
blob += "<tab>" + 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 <highlight>%d</highlight>." % 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: <highlight>" \
f"{self.util.time_to_readable(battle.last_updated - first_activity)}</highlight>\n\n"
blob += "<header2>Attackers:</header2>\n"
for row in attackers:
blob += "<tab>" + 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 = " :: <red>He's a <myname> Raider!</red>"
self.bot.send_private_channel_message(
f"[<cyan>NW</cyan>] "
f"<{event_data.defender.faction.lower()}>"
f"{event_data.defender.org_name}"
f"</{event_data.defender.faction.lower()}> "
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}/<green>{row.att_ai_level}</green>" if row.att_ai_level > 0 else f"{row.att_level}"
org = row.att_org_name + " " if row.att_org_name else ""
victor = " - <notice>Winner!</notice>" 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 = " - <notice>Defeated!</notice>" if row.is_finished else ""
blob += f"Site: <highlight>{row.short_name} {row.site_number or '?'}</highlight>\n"
blob += f"Defender: <highlight>{row.def_org_name}</highlight> ({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"<highlight>{self.util.format_datetime(t)}</highlight> " \
f"({self.util.time_to_readable(current_t - t)} ago)"
def get_chat_command(self, page):
return f"/tell <myname> 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 " \
"<symbol>attacks command to work correctly.\n\n"
else:
return ""