-> !wants
-> !orgs info
-> special cmd's
-> !assist
-> "afk" for players without active account
-> !loot add <item_ref> <count> => 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)
This commit is contained in:
2021-11-25 14:09:43 +01:00
parent 2d7ecf4883
commit 17c776faec
44 changed files with 1669 additions and 1249 deletions
@@ -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 <highlight>{user.name}</highlight>.",
request.sender.char_id)
@@ -256,7 +257,7 @@ class AccountController:
response += f" Status: {'<green>Open</green>' if alts[0].disabled == 0 else '<red>Closed</red>'}\n"
response += f" Created at: <notice>{self.util.format_datetime(alts[0].created)}</notice>\n"
if last_seen:
response += f" Last seen on <notice>{last_seen.name}</notice> <highlight>{self.util.time_to_readable(time.time()-last_seen.last_seen)}</highlight> ago\n"
response += f" Last seen on <notice>{last_seen.name}</notice> <highlight>{self.util.time_to_readable(time.time() - last_seen.last_seen)}</highlight> ago\n"
response += f" Permissions: <notice>{', '.join(perms)}</notice>\n"
if alts[0].discord_joined == 1:
joined = ' (Joined server)'
@@ -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:
@@ -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} <yellow>{ctx.sender.name}</yellow>"
if account := self.account_service.get_account(ctx.sender.char_id):
if account.main != ctx.sender.char_id:
name += f" (<yellow>{account.name}</yellow>)"
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()
+3 -3
View File
@@ -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} - <highlight>{channel_id:d}</highlight>\n"
pub_channels += f"{name} - <highlight>{channel_id}</highlight>\n"
for event_type in self.event_service.get_event_types():
event_types += f"{event_type}\n"
@@ -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 <myname> 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"<header>::: Information :::</header><br><br>"
f"This message has been sent to you by:<br><br>"
f"<header2>Igncom</header2><br>"
f"<notice>{ctx.sender[1].name + '#' + ctx.sender[1].discriminator}</notice><br>"
f"<highlight>{ctx.sender[0]}</highlight> on Alliance Discord.<br><br>"
f"<notice>{ctx.sender.discord_handle}</notice><br>"
f"<highlight>{name}</highlight> on Alliance Discord.<br><br>"
f"To reply, either respond in the relay or "
f"contact them directly at the provided handles.<br><br>"
f"<header2>Have you joined The Alliance Discord yet? "
f"If not <highlight>{invite}</highlight> to receive an invite.</header2>")
self.send_message_to_alliance(plain_msg + f" <yellow>[{blob}]</yellow>")
self.send_message_to_alliance(f"{name}: {plain_msg}" + f" <yellow>[{blob}]</yellow>")
def send_message_to_alliance(self, msg):
if self.relay_channel_id:
+2 -2
View File
@@ -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 <orange>disabled</orange> but can be enabled. " \
"<highlight>%s</highlight> disabled it %s ago." % (row.name, time_str)
msg = f"The cloaking device is <orange>disabled</orange> but can be enabled. " \
f"<highlight>{row.name}</highlight> 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")
+36
View File
@@ -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: <grey>{message}</grey>"
else:
entry = self.account_service.get_entry(request.sender.char_id)
if entry.logon:
return f"Your current logon message is: <grey>{entry.logon}</grey>"
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: <grey>{message}</grey>"
else:
entry = self.account_service.get_entry(request.sender.char_id)
if entry.logon:
return f"Your current logoff message is: <grey>{entry.logon}</grey>"
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)],
+16 -7
View File
@@ -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': "[<cyan>Org</cyan>] <a href='user://Risianna'>Risianna</a>: Sooo"}
if not self.display_main().get_value() == "1" and ctx.source == "private_channel" and ctx.sender:
name = f"{OrgChannelController.ORG_CHANNEL_PREFIX} <yellow>{ctx.sender.name}</yellow>"
if account := self.account_service.get_account(ctx.sender.char_id):
if account.main != ctx.sender.char_id:
name += f" (<yellow>{account.name}</yellow>)"
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" :: <grey>{event_data.account.logon}<grey>" if event_data.account.logon else ""
logoff = f" :: <grey>{event_data.account.logoff}<grey>" if event_data.account.logoff else ""
msg = f"{char_name} logged <red>off</red>.{logoff}"
self.bot.send_org_message(msg, fire_outgoing_event=False)
self.message_hub_service.send_message(self.MESSAGE_SOURCE, None, None, "[<cyan>Org</cyan>] " + 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()
+15 -4
View File
@@ -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)
@@ -42,8 +42,7 @@ class WaveCounterController:
self.send_message("General incoming. <red>DO NOT enter the city!</red>")
self.scheduled_job_id = None
else:
self.send_message("Wave <highlight>%d</highlight> incoming. "
"<red>DO NOT enter the city!</red>" % wave_number)
self.send_message(f"Wave <highlight>{wave_number}</highlight> incoming. <red>DO NOT enter the city!</red>")
self.scheduled_job_id = self.job_scheduler.scheduled_job(self.timer_alert,
t + self.ALERT_TIMES[wave_number],
wave_number)
@@ -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"<font color=#.{6}><font color=#.{6}>Planned Raids last updated \w+ \d+\w+, \d+ \d+:\d+:: "
r"<a href=\"text://(.+)\">.+</a></font></font>",
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 = "<center><font color=#DDDD44>:::: Planned Raids ::::" \
"</font></center><br><font color=#66AA66> Es sind mir leider keine Raids bekannt.</font>"
# @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)
-76
View File
@@ -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 = "<font color=CCInfoText>"
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 += "</font>"
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 = "<font color=CCInfoText>"
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"[<cyan>{lca.short_name}</cyan>] <cyan>x{value['site']}</cyan> in " \
f"<white>{self.util.format_time(value['plant'] - time.time())}</white>\n"
blob += "</font>"
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."
@@ -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<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 = ""
# 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 = " :: <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 ""
@@ -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 <highlight>{tl}</highlight>.",
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"<black>0</black>{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"<red>{number(hot)}</red> <cyan>{number(cold)}</cyan> <grey>{number(unplanted)}</grey> :: " \
f"<clan>{number(clan)}</clan> <neutral>{number(neut)}</neutral> <omni>{number(omni)}</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:<br>"
for org in orgs:
blob += "[%s] <highlight>%s<end> (<highlight>%s<end>) <%s>%s<end> [<highlight>%s<end> " \
"members]<br><pagebreak>" \
% (self.text.make_chatcmd("Towers", "/tell <myname> 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 <highlight>%s</highlight>." % 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)
-298
View File
@@ -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'</{event_data.loser.faction.lower()}>'
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'</{event_data.winner.faction.lower()}> <highlight>won!!</highlight>')
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'</{event_data.loser.faction.lower()}> 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"(<red>UKN</red>) 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'</{event_data.loser.faction.lower()}> '
f'Lost their Site in {event_data.location.playfield.long_name}')
self.prepare_nw_warn(event_data.playfield.id, "(<red>UKN</red>)",
f"(<red>UKN</red>) 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"<font color=CCInfoText>Site: <blue>{entry.short_name}</blue> <red>x{entry.site_number}</red> " \
f"[R:<red>{entry.min_ql} - {entry.max_ql}</red>] " \
f"[{self.text.make_tellcmd('More', f'lc {entry.short_name} {entry.site_number}')}]\n"
row1 = f'<grey>UKN</grey> :: <red>No Owner -> Unplanted</red>\n'
row2 = ""
row3 = "</font>\n"
hot = self.is_hot(entry)
if hot != -1:
h1 = "<cyan>COLD</cyan>"
if hot == 2:
h1 = "<red>WARHOT</red>"
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"<cyan>COLD</cyan> in " \
f"{self.util.format_time(self.day_time(int(entry.close_time - now)))}"
if hot_normal == 0:
h3 = f"<cyan>COLD</cyan> in {self.util.format_time(org['hot'] - now)}"
elif hot == 1:
h1 = '<red>HOT</red>'
h3 = f"<cyan>COLD</cyan> 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"<red>HOT</red> 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}</{entry.faction.lower()}> " \
f"[{self.text.make_tellcmd('View org', f'lc org {entry.org_name}')}]"
row1 = f"{h1} :: {h3} :: {org} :: \n"
row3 = f" » Planted: <grey>{self.util.format_datetime(entry.planted)}</grey></font>\n\n"
if entry.pvp_min:
pvp = f"[<red>{entry.pvp_min} - {entry.pvp_max}</red>]"
else:
pvp = f"[<red>175 - 220</red>]"
# noinspection LongLine
row2 = f" » QL: <red>{entry.ql}</red> 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 + "<pagebreak>"
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} <cyan>»</cyan> 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 <green>NOW</green>!")
def send_nw_warn(self, _, msg):
self.bot.send_private_channel_message(f"[<cyan>NW</cyan>] {msg}")
File diff suppressed because one or more lines are too long
+12 -7
View File
@@ -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()
+12 -5
View File
@@ -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:
@@ -29,7 +29,7 @@ class PlayfieldController:
blob = ""
for row in data:
blob += "[<highlight>%d</highlight>] %s (%s)\n" % (row.id, row.long_name, row.short_name)
blob += f"[<highlight>{row.id:d}</highlight>] {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])
+12 -13
View File
@@ -67,20 +67,19 @@ class ItemsController:
blob += f"Search: <highlight>QL {ql:d} {search}</highlight>\n"
else:
blob += f"Search: <highlight>{search}</highlight>\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
+10 -3
View File
@@ -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"(([^<]+)?<a href=[\"\']itemref://(\d+)/(\d+)/(\d+)[\"\']>([^<]+)</a>([^<]+)?)", 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"<highlight>{loot}<end> was added to loot list.")
@timerevent(budatime="1h",
+63 -10
View File
@@ -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 <myname> 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 <highlight>not</highlight> connected to Discord " \
f"[{self.text.make_chatcmd('Join', '/tell <myname> 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 <highlight>is</highlight> connected to Discord as " \
f"<highlight>[{self.alias_controller.get_alias(account.org_id)}] " \
f"{account.name}</highlight>"
f"<highlight>{prefix}{account.name}</highlight>"
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 <myname> 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"<yellow>{random.choice(self.greetings)} {sender.name} :: " \
f"{self.text.format_page('Your News', textwrap.dedent(news))} </yellow> " \
f"{self.text.format_page('Your News', textwrap.dedent(news))} </yellow>{last_updated} " \
f"{f'<yellow>::</yellow> {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":: <yellow>Your most recent unread news</yellow> :: \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 <highlight>{entry_id}</highlight>."
@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])
+67 -45
View File
@@ -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": <mortal time>},
# {"name":"Vizaresh","time": <mortal 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"<highlight>{timer.name}</highlight> :: is now mortal")
@@ -152,7 +174,7 @@ class WorldBossController:
elif timer.type == "spawn":
self.send_warn(f"<highlight>{timer.name}</highlight> :: 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"<highlight>{timer.name}</highlight> :: 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
+5 -2
View File
@@ -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"<highlight>{char_name}</highlight> is back after {time_string}.")
+15 -4
View File
@@ -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" [<notice>{afk.message} - since {self.util.time_to_readable(int(time.time() - afk.time))}</notice>]"
blob += f"\n<highlight>{player.main_name}</highlight>{rank}:{afk}\n"
style = "style='text-decoration:none'"
blob += f"\n<highlight>{self.text.make_tellcmd(player.main_name, f'alts {player.main_name}', style=style)}</highlight>{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)}/<green>{self.text.zfill(player.ai_level, 30)}</green> " \
f"<{player.faction.lower()}>{player.name}</{player.faction.lower()}> ({player.org_rank_name}){afk} {main}\n"
f"<{player.faction.lower()}>{player.name}</{player.faction.lower()}> {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)}/<green>{self.text.zfill(player.ai_level, 30)}</green> " \
f"<{player.faction.lower()}>{player.name}</{player.faction.lower()}> " \
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)}/<green>{self.text.zfill(player.ai_level, 30)}</green> " \
f"<{player.faction.lower()}>{player.name}</{player.faction.lower()}> " \
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:
+1 -2
View File
@@ -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}')}]<br>"
blob += self.get_assist_output()
self.last_mod = time.time()
@@ -189,14 +189,14 @@ class SpecialsController:
blob = f"Attack: <highlight>{weapon_attack:.2f} secs</highlight>\n"
blob += f"Recharge: <highlight>{weapon_recharge:.2f} secs</highlight>\n"
blob += f"Full Auto Recharge: <highlight>{full_auto_recharge:d}</highlight>\n"
blob += f"Full Auto Skill: <highlight>{full_auto_skill:d}</highlight>\n\n"
blob += f"Full Auto Recharge: <highlight>{full_auto_recharge:.2f}</highlight>\n"
blob += f"Full Auto Skill: <highlight>{full_auto_skill:.2f}</highlight>\n\n"
blob += f"Full Auto Recharge: <highlight>{full_auto_info.recharge:d} secs</highlight>\n"
blob += f"Max Number of Bullets: <highlight>{full_auto_info.max_bullets:d}</highlight>\n\n"
blob += f"Full Auto Recharge: <highlight>{full_auto_info.recharge:.2f} secs</highlight>\n"
blob += f"Max Number of Bullets: <highlight>{full_auto_info.max_bullets:.2f}</highlight>\n\n"
blob += f"You need <highlight>{full_auto_info.skill_cap:d}</highlight> Full Auto Skill " \
f"to cap your recharge at <highlight>{full_auto_info.hard_cap:d} secs</highlight>.\n\n"
blob += f"You need <highlight>{full_auto_info.skill_cap:.2f}</highlight> Full Auto Skill " \
f"to cap your recharge at <highlight>{full_auto_info.hard_cap:.2f} secs</highlight>.\n\n"
blob += "From <highlight>0 to 10K</highlight> damage, the bullet damage is unchanged.\n"
blob += "From <highlight>10K to 11.5K</highlight> 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: <highlight>{nano_attack_time:.2f} secs</highlight>\n"
blob += f"Nano Cast Init: <highlight>{nano_cast_init:d}</highlight>\n\n"
blob += f"Nano Cast Init: <highlight>{nano_cast_init}</highlight>\n\n"
blob += f"Cast Time Reduction: <highlight>{nano_cast_info.cast_time_reduction:.2f}</highlight>\n"
blob += f"Effective Cast Time: <highlight>{nano_cast_info.effective_cast_time:.2f}</highlight>\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"<highlight>{nano_cast_info.instacast_full_agg:d}</highlight>\n"
f"<highlight>{nano_cast_info.instacast_full_agg:.2f}</highlight>\n"
blob += f"NanoC. Init needed to instacast at Neutral (87.5%): " \
f"<highlight>{nano_cast_info.instacast_neutral:d}</highlight>\n"
f"<highlight>{nano_cast_info.instacast_neutral:.2f}</highlight>\n"
blob += f"NanoC. Init needed to instacast at Half (50%): " \
f"<highlight>{nano_cast_info.instacast_half:d}</highlight>\n"
f"<highlight>{nano_cast_info.instacast_half:.2f}</highlight>\n"
blob += f"NanoC. Init needed to instacast at Full Def (0%): " \
f"<highlight>{nano_cast_info.instacast_full_def:d}</highlight>\n\n"
f"<highlight>{nano_cast_info.instacast_full_def:.2f}</highlight>\n\n"
blob += f"Cast time at Full Agg (100%): <highlight>{nano_cast_info.cast_time_full_agg:.2f}</highlight>\n"
blob += f"Cast time at Neutral (87.5%): <highlight>{nano_cast_info.cast_time_neutral:.2f}</highlight>\n"
+7 -7
View File
@@ -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 <highlight>{timer.name}</highlight>."
@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)
@@ -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 = "<font color=CCInfoText>"
blob = ""
blob += "" + pages + "\n"
index = offset
for entry in selected:
+166
View File
@@ -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 <highlight>{tl}</highlight>.",
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"<red>5%</red> (closes in {self.util.time_to_readable(row.status_time)})"
elif row.status_time <= (3600 * 6):
status += f"<orange>25%</orange> (closes in {self.util.time_to_readable(row.status_time)})"
else:
status += f"<green>75%</green> (opens in {self.util.time_to_readable(row.status_time - (3600 * 6))})"
if row.penalty_until > time.time():
status += f" <red>In Penalty for: {self.util.time_to_readable(row.penalty_until - time.time())}</red>"
blob = ""
if self.get_ct_type(d.get("ql", 0)) < (tl := self.get_ct_type(row.ql)):
blob += f"<notice>TL{tl}</notice><br>"
space = f"{row.short_name} x{row.site_number}"
place = "_" * (7 - len(space))
return blob + "<tab>" + self.text.make_tellcmd(space,
f'lc {row.short_name} {row.site_number}') + \
f"<black>{place}</black> QL {row.min_ql}/<highlight>{row.ql}</highlight>/{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
+279
View File
@@ -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}')} <highlight>{row.short_name}</highlight>\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"<highlight>{search.capitalize()}</highlight> 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 <highlight>{search}</highlight> 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"<pagebreak>{self.format_site_info(x, time.time(), len(data))}"
blob += f"<tab>Dist: <highlight>{x.guard}</highlight> Conductors and <highlight>{x.turrets}</highlight> Turrets planted\n\n"
ql += x.ql
blob += f"Stats: QL<highlight>{ql}</highlight>, contracts up to QL<highlight>{ql * 2}</highlight>"
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 <highlight>{playfield_name}</highlight>."
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 += "<pagebreak>" + self.format_site_info(data, t)
else:
for row in data:
blob += f"<pagebreak>{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"<pagebreak>{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"<highlight>{row.short_name} x{row.site_number}</highlight> ({row.site_name})\n"
blob += f"<tab>Level Range: <white>{row.min_ql} - {row.max_ql}</white> "
if row.timing == 0:
blob += f"[<grey>Legacy</grey>]\n"
if row.timing == 1:
blob += f"[<grey><black>0</black>4 UTC</grey>]\n"
if row.timing == 2:
blob += f"[<grey>20 UTC</grey>]\n"
blob += f"<tab>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"<red>5%</red> (closes in {self.util.time_to_readable(current_status_time)})"
elif current_status_time <= (3600 * 6):
status += f"<orange>25%</orange> (closes in {self.util.time_to_readable(current_status_time)})"
else:
status += f"<green>75%</green> (opens in {self.util.time_to_readable(current_status_time - (3600 * 6))})"
if row.penalty_until > t:
status += f" <red>Penalty</red> (for {self.util.time_to_readable(row.penalty_until - t)})"
blob += f"<tab>CT: QL<highlight>{row.ql}</highlight> ({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"<tab>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<highlight>{self.text.zfill(tower.ql, 220)}</highlight> {tower.name}\n"
blob += f"<tab>Dist: <highlight>{cond}</highlight> Conductors and <highlight>{turret}</highlight> Turrets\n"
blob += "\n Towers:\n"
blob += towers
else:
if not row.enabled:
blob += "<red>Disabled</red>\n"
else:
blob += "<tab><red>This site is potentially unplanted</red>\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 <EXTENDED_LIKE=0> ? AND b.org_id IS NOT NULL",
[search], extended_like=True)
@@ -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])
+170
View File
@@ -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 <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 = ""
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)
@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 <highlight>{pf}</highlight>."
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<pagebreak>"
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 += "<header2>Attackers:</header2>\n"
blob += "<tab>" + self.format_attacker(row) + "\n"
blob = ChatBlob(f"Tower Attacks", blob)
return blob
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 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 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])
@@ -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: <highlight>{row.short_name} {row.site_number:d}</highlight>\n"
blob += f"Long name: <highlight>{row.site_name}, {row.long_name}</highlight>\n"
blob += f"Level range: <highlight>{row.min_ql:d}-{row.max_ql:d}</highlight>\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(),
+282
View File
@@ -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);
@@ -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"[<cyan>NW</cyan>] {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"[<cyan>NW</cyan>] {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"[<cyan>NW</cyan>] 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)
+5 -5
View File
@@ -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"):
@@ -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)
@@ -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 <myname> orgs add {org.org_id}")}]'
blob += f'[{self.text.make_chatcmd("More", f"/tell <myname> org info {org.org_id}")}]'
blob += f'[{self.text.make_chatcmd("More", f"/tell <myname> orgs info {org.org_id}")}]'
blob += f' <highlight>{org.org_name}<end> ' \
f'(<highlight>{org.org_id}<end>) <{org.faction.lower()}>{org.faction}<end> ' \
f'[<highlight>{org.member_count}<end> members]<br><pagebreak>'