810c2c8c4d
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)]
368 lines
17 KiB
Python
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 ""
|