Fixed:
-> !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:
@@ -257,6 +257,8 @@ class Character(Any):
|
||||
if char_id is None:
|
||||
return SenderObj(char_id, val.capitalize(), None)
|
||||
else:
|
||||
if int(char_id) >= 4294967290:
|
||||
return SenderObj(None, val.capitalize(), None)
|
||||
return SenderObj(char_id, val.capitalize(), access_service.get_access_level(char_id))
|
||||
|
||||
|
||||
|
||||
+30
-7
@@ -17,6 +17,7 @@ from core.logger import Logger
|
||||
from core.lookup.character_service import CharacterService
|
||||
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 modules.core.accounting.services.access_service import AccessService
|
||||
|
||||
@@ -41,8 +42,8 @@ class IgnCore:
|
||||
self.dimension = None
|
||||
self.last_timer_event = 0
|
||||
self.start_time = int(time.time())
|
||||
self.major_version = "IGNCore v2.7"
|
||||
self.minor_version = "2"
|
||||
self.major_version = "IGNCore v2.8"
|
||||
self.minor_version = "0"
|
||||
self.incoming_queue = FifoQueue()
|
||||
self.mass_message_queue = None
|
||||
self.conns = DictObject()
|
||||
@@ -151,6 +152,15 @@ class IgnCore:
|
||||
|
||||
def start(self):
|
||||
self.register_packet_handler(server_packets.PrivateMessage.id, self.handle_private_message, priority=40)
|
||||
self.setting_service.register("core.logging", "log_tells", "false",
|
||||
BooleanSettingType(),
|
||||
"Should tells get logged to file")
|
||||
self.setting_service.register("core.logging", "log_priv", "false",
|
||||
BooleanSettingType(),
|
||||
"Should the private channel get logged to file")
|
||||
self.setting_service.register("core.logging", "log_org", "false",
|
||||
BooleanSettingType(),
|
||||
"Should the org channel get logged to file")
|
||||
|
||||
def connect(self, config):
|
||||
conn = self.create_conn("main")
|
||||
@@ -352,6 +362,13 @@ class IgnCore:
|
||||
else:
|
||||
color = self.setting_service.get("private_message_color").get_font_color() if add_color else ""
|
||||
pages = self.get_text_pages(msg, self.setting_service.get("private_message_max_page_length").get_value())
|
||||
if self.setting_service.get_value("log_tells") == "1":
|
||||
if type(msg) == ChatBlob:
|
||||
self.logger.log_tell('spam', '->', self.character_service.get_char_name(char_id),
|
||||
f"[link]{msg.title}[/link]")
|
||||
|
||||
else:
|
||||
self.logger.log_tell('spam', '->', self.character_service.get_char_name(char_id), msg)
|
||||
for page in pages:
|
||||
# self.logger.log_tell(conn_id, "To", self.character_service.get_char_name(char_id), page)
|
||||
packet = client_packets.PrivateMessage(char_id, color + page, "\0")
|
||||
@@ -376,18 +393,23 @@ class IgnCore:
|
||||
DictObject({"private_channel_id": private_channel_id, "message": msg}))
|
||||
|
||||
def send_mass_message(self, char_id, msg, add_color=True, log_message=False):
|
||||
# self.logger.log_tell('spam', 'To', self.character_service.get_char_name(char_id), msg)
|
||||
if not char_id:
|
||||
self.logger.warning("Could not send message to empty char_id")
|
||||
if len(self.conns.items()) == 1:
|
||||
self.send_private_message(char_id, msg, add_color, log_message)
|
||||
else:
|
||||
if self.setting_service.get_value("log_tells") == "1":
|
||||
if type(msg) == ChatBlob:
|
||||
self.logger.log_tell('spam', '->', self.character_service.get_char_name(char_id),
|
||||
f"[link]{msg.title}[/link]")
|
||||
|
||||
else:
|
||||
self.logger.log_tell('spam', '->', self.character_service.get_char_name(char_id), msg)
|
||||
color = self.setting_service.get("private_message_color").get_font_color() if add_color else ""
|
||||
pages = self.get_text_pages(msg, self.setting_service.get("private_message_max_page_length").get_value())
|
||||
for page in pages:
|
||||
if log_message:
|
||||
self.logger.log_tell("spam", "To", self.character_service.get_char_name(char_id), page)
|
||||
|
||||
# if self.log_mass_tell().get_value():
|
||||
# self.logger.log_tell("spam", "->", self.character_service.get_char_name(char_id), page)
|
||||
if self.mass_message_queue:
|
||||
packet = client_packets.PrivateMessage(char_id, color + page, "\0")
|
||||
self.mass_message_queue.put(packet)
|
||||
@@ -396,7 +418,8 @@ class IgnCore:
|
||||
self.conns["main"].send_packet(packet)
|
||||
|
||||
def handle_private_message(self, conn: Conn, packet: server_packets.PrivateMessage):
|
||||
# self.logger.log_tell(conn.id, "From", self.character_service.get_char_name(packet.char_id), packet.message)
|
||||
if self.setting_service.get_value("log_tells") == "1":
|
||||
self.logger.log_tell(conn.id, "<-", self.character_service.get_char_name(packet.char_id), packet.message)
|
||||
self.event_service.fire_event(self.PRIVATE_MSG_EVENT, packet)
|
||||
|
||||
def get_text_pages(self, msg, max_page_length):
|
||||
|
||||
@@ -2,6 +2,7 @@ from core.aochat import server_packets, client_packets
|
||||
from core.conn import Conn
|
||||
from core.decorators import instance
|
||||
from core.logger import Logger
|
||||
from core.setting_service import SettingService
|
||||
|
||||
|
||||
@instance()
|
||||
@@ -19,6 +20,7 @@ class PrivateChannelService:
|
||||
self.event_service = registry.get_instance("event_service")
|
||||
self.character_service = registry.get_instance("character_service")
|
||||
self.access_service = registry.get_instance("access_service")
|
||||
self.setting_service: SettingService = registry.get_instance("setting_service")
|
||||
|
||||
def pre_start(self):
|
||||
self.event_service.register_event_type(self.JOINED_PRIVATE_CHANNEL_EVENT)
|
||||
@@ -38,15 +40,19 @@ class PrivateChannelService:
|
||||
def handle_private_channel_message(self, conn: Conn, packet: server_packets.PrivateChannelMessage):
|
||||
if conn.id != "main":
|
||||
return
|
||||
|
||||
if self.setting_service.get_value("log_priv") == "1":
|
||||
char_name = self.character_service.get_char_name(packet.char_id)
|
||||
self.logger.log_chat(conn, "Private Channel", char_name, packet.message)
|
||||
if packet.private_channel_id == self.bot.get_char_id():
|
||||
self.event_service.fire_event(self.PRIVATE_CHANNEL_MESSAGE_EVENT, packet)
|
||||
|
||||
def handle_private_channel_client_joined(self, conn: Conn, packet: server_packets.PrivateChannelClientJoined):
|
||||
if conn.id != "main":
|
||||
return
|
||||
|
||||
if packet.private_channel_id == self.bot.get_char_id():
|
||||
if self.setting_service.get_value("log_priv") == "1":
|
||||
char_name = self.character_service.get_char_name(packet.char_id)
|
||||
self.logger.log_chat(conn, "Private Channel", None, f"{char_name} joined the channel.")
|
||||
self.private_channel_chars[packet.char_id] = packet
|
||||
self.event_service.fire_event(self.JOINED_PRIVATE_CHANNEL_EVENT, packet)
|
||||
|
||||
@@ -55,6 +61,9 @@ class PrivateChannelService:
|
||||
return
|
||||
|
||||
if packet.private_channel_id == self.bot.get_char_id():
|
||||
if self.setting_service.get_value("log_priv") == "1":
|
||||
char_name = self.character_service.get_char_name(packet.char_id)
|
||||
self.logger.log_chat(conn, "Private Channel", None, f"{char_name} left the channel.")
|
||||
del self.private_channel_chars[packet.char_id]
|
||||
self.event_service.fire_event(self.LEFT_PRIVATE_CHANNEL_EVENT, packet)
|
||||
|
||||
|
||||
@@ -87,20 +87,22 @@ class PublicChannelService(BaseModule):
|
||||
return
|
||||
|
||||
if self.is_org_channel_id(packet.channel_id):
|
||||
# char_name = self.character_service.get_char_name(packet.char_id)
|
||||
# if packet.extended_message:
|
||||
# message = packet.extended_message.get_message()
|
||||
# else:
|
||||
# message = packet.message
|
||||
# # self.logger.log_chat(conn.id, "Org Channel", char_name, message)
|
||||
if self.setting_service.get_value("log_org") == "1" and packet.char_id == self.bot.get_char_id():
|
||||
char_name = self.character_service.get_char_name(packet.char_id)
|
||||
if packet.extended_message:
|
||||
message = packet.extended_message.get_message()
|
||||
else:
|
||||
message = packet.message
|
||||
self.logger.log_chat(conn.id, "Org Channel", char_name, message)
|
||||
self.event_service.fire_event(self.ORG_CHANNEL_MESSAGE_EVENT, packet)
|
||||
elif packet.channel_id == self.ORG_MSG_CHANNEL_ID:
|
||||
# char_name = self.character_service.get_char_name(packet.char_id)
|
||||
# if packet.extended_message:
|
||||
# message = packet.extended_message.get_message()
|
||||
# else:
|
||||
# message = packet.message
|
||||
# self.logger.log_chat(conn.id, "Org Msg", char_name, message)
|
||||
if self.setting_service.get_value("log_org") == "1" and packet.char_id == self.bot.get_char_id():
|
||||
char_name = self.character_service.get_char_name(packet.char_id)
|
||||
if packet.extended_message:
|
||||
message = packet.extended_message.get_message()
|
||||
else:
|
||||
message = packet.message
|
||||
self.logger.log_chat(conn.id, "Org Msg", char_name, message)
|
||||
self.event_service.fire_event(self.ORG_MSG_EVENT, packet)
|
||||
|
||||
def is_org_channel_id(self, channel_id):
|
||||
|
||||
+1
-1
@@ -128,7 +128,7 @@ class Text:
|
||||
if count == 0:
|
||||
return no_data_msg
|
||||
else:
|
||||
blob = "<font color=CCInfoText>"
|
||||
blob = ""
|
||||
blob += "" + pages + "\n"
|
||||
blob += headline
|
||||
index = offset
|
||||
|
||||
@@ -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,6 +78,13 @@ class PrivateChannelController:
|
||||
return hjson.load(f)
|
||||
|
||||
def handle_incoming_relay_message(self, ctx):
|
||||
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")
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)],
|
||||
|
||||
@@ -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,10 +44,15 @@ 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):
|
||||
# {'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,
|
||||
@@ -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()
|
||||
|
||||
@@ -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]:
|
||||
self.buddy_service.remove_buddy(char_id, "member")
|
||||
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")
|
||||
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)
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -42,14 +42,18 @@ class WebsocketRelayController(BaseModule):
|
||||
TextSettingType([], allow_empty=True),
|
||||
"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)
|
||||
def handle_queue_event(self, _, _1):
|
||||
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()
|
||||
|
||||
|
||||
@@ -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:
|
||||
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])
|
||||
|
||||
@@ -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}",
|
||||
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} >>",
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -335,10 +335,17 @@ class LootController:
|
||||
else:
|
||||
loot += item
|
||||
self.add_item_to_loot(item)
|
||||
else:
|
||||
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",
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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(),
|
||||
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:
|
||||
for job in self.jobs:
|
||||
if job["name"] == test_data.name:
|
||||
if data.type != "timer":
|
||||
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)
|
||||
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'] == 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
|
||||
|
||||
@@ -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}.")
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
+7
-21
@@ -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:
|
||||
@@ -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
|
||||
@@ -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])
|
||||
@@ -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])
|
||||
+22
-21
@@ -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(),
|
||||
@@ -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)
|
||||
@@ -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>'
|
||||
|
||||
Reference in New Issue
Block a user