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 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<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 += "Site: <highlight>%s %s</highlight>\n" % (row.short_name, row.site_number or "?")
|
|
blob += "Defender: <highlight>%s</highlight> (%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 "<highlight>%s</highlight> (%s ago)" % (
|
|
self.util.format_datetime(t), self.util.time_to_readable(current_t - t))
|
|
|
|
def get_chat_command(self, page):
|
|
return "/tell <myname> 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 " \
|
|
"<symbol>attacks command to work correctly.\n\n"
|
|
else:
|
|
return ""
|