-> !wants
-> !orgs info
-> special cmd's
-> !assist
-> "afk" for players without active account
-> !loot add <item_ref> <count> => nolonger breaks !account
Changes:
-> grouped !tara, !gaunt, .. into !wb
-> Display the most recent news entry on logon (default: enabled)
-> improved grouping of !items
-> Added the option to authentificate WS connections (Datanet module). This is used in special cases, where the Websocket Server requires the clien tto authentificate itself. (Server sends "#auth", client responds with the auth string)
-> Add main name to relaying (priv <-> org) [default: disabled]
-> Added logon/logoff messages back
-> restricted default access to "dangerous" commands to moderator
-> Added optional logging (Private Channel, Org Channel, Tells, ... disabled by default)

Rewrite of the Tower Module.
-> More verbosity, if enabled in config. by default, GAS and Hot timer only.
-> !hot displays currently hot (and in penalty) sites, and these which go hot in < 60 minutes
-> !attacks filterable by PF and Site
-> display current contract QL's grouped by org: !contracts (requires managed cache)
This commit is contained in:
2021-11-25 14:09:43 +01:00
parent 2d7ecf4883
commit 17c776faec
44 changed files with 1669 additions and 1249 deletions
+12 -7
View File
@@ -6,7 +6,7 @@ from core.decorators import instance, timerevent
from core.event_service import EventService
from core.logger import Logger
from core.setting_service import SettingService
from core.setting_types import TextSettingType
from core.setting_types import TextSettingType, HiddenSettingType
from modules.standard.datanet.ws_worker import WebsocketRelayWorker
@@ -37,10 +37,16 @@ class WebsocketRelayController(BaseModule):
def pre_start(self):
self.event_service.register_event_type(self.WS_RELAY)
self.setting_service.register(self.module_name,
'relay_address',
'ws://localhost:25500',
'relay_address',
'ws://localhost:25500',
TextSettingType([], allow_empty=True),
"relay for timers, tower info, ...")
"relay for timers, tower info, ...")
self.setting_service.register(self.module_name,
'datanet_auth',
'',
HiddenSettingType([], allow_empty=True),
"Auth Challenge response")
@timerevent(budatime="1s", description="Relay messages from Data relay to the internal message hub",
is_hidden=True)
@@ -48,8 +54,6 @@ class WebsocketRelayController(BaseModule):
while self.queue:
obj = self.queue.pop(0)
self.event_service.fire_event(self.WS_RELAY, obj)
if obj.type == "connected":
self.send_relay_message('join', f"{self.bot.name}")
@timerevent(budatime="1m", description="Ensure the bot is connected to Data relay", is_hidden=True,
run_at_startup=True)
@@ -68,7 +72,8 @@ class WebsocketRelayController(BaseModule):
def connect(self):
self.disconnect()
self.worker = WebsocketRelayWorker(self.queue, self.setting_service.get_value("relay_address"), False)
self.worker = WebsocketRelayWorker(self.queue, self.setting_service.get_value("relay_address"),
self.setting_service.get_value("datanet_auth"))
self.dthread = threading.Thread(target=self.worker.run, daemon=True)
self.dthread.start()
+12 -5
View File
@@ -1,16 +1,17 @@
import json
from websocket import create_connection
from websocket import create_connection, WebSocketConnectionClosedException
from core.dict_object import DictObject
from core.logger import Logger
class WebsocketRelayWorker:
def __init__(self, inbound_queue, url, proxy):
def __init__(self, inbound_queue, url, auth):
self.logger = Logger(__name__)
self.inbound_queue = inbound_queue
self.url = url
self.auth = auth
self.ws = None
def run(self):
@@ -21,13 +22,19 @@ class WebsocketRelayWorker:
result = self.ws.recv()
while result:
obj = DictObject(json.loads(result))
self.inbound_queue.append(obj)
if result == "#auth":
self.ws.send(self.auth)
else:
obj = DictObject(json.loads(result))
self.inbound_queue.append(obj)
result = self.ws.recv()
self.ws.close()
except ConnectionRefusedError:
pass
except ConnectionResetError:
pass
except WebSocketConnectionClosedException:
pass
def send_message(self, message):
if self.ws:
@@ -29,7 +29,7 @@ class PlayfieldController:
blob = ""
for row in data:
blob += "[<highlight>%d</highlight>] %s (%s)\n" % (row.id, row.long_name, row.short_name)
blob += f"[<highlight>{row.id:d}</highlight>] {row.long_name} ({row.short_name})\n"
return ChatBlob("Playfields", blob)
@@ -86,6 +86,13 @@ class PlayfieldController:
"OR short_name LIKE ? "
"LIMIT 1", [name, name])
def get_playfield_by_name_or_id(self, name):
return self.db.query_single("SELECT * FROM playfields "
"WHERE long_name LIKE ? "
"OR short_name LIKE ? "
"OR id LIKE ? "
"LIMIT 1", [name, name, name])
def get_playfield_by_id(self, playfield_id):
return self.db.query_single("SELECT * FROM playfields "
"WHERE id = ?", [playfield_id])
+12 -13
View File
@@ -67,20 +67,19 @@ class ItemsController:
blob += f"Search: <highlight>QL {ql:d} {search}</highlight>\n"
else:
blob += f"Search: <highlight>{search}</highlight>\n"
blob += "\n"
head_foot = ""
if page > 1:
blob += " " + self.text.make_chatcmd(f"<< Page {page - 1:d}",
self.get_chat_command(ql, search, page - 1))
head_foot += " " + self.text.make_chatcmd(f"«« Page {page - 1:d}",
self.get_chat_command(ql, search, page - 1))
if offset + self.PAGE_SIZE < len(all_items):
blob += " Page " + str(page)
blob += " " + self.text.make_chatcmd(f"Page {page + 1:d} >>",
self.get_chat_command(ql, search, page + 1))
head_foot += " Page " + str(page)
head_foot += " " + self.text.make_chatcmd(f"Page {page + 1:d} »»",
self.get_chat_command(ql, search, page + 1))
if self.PAGE_SIZE < len(all_items):
blob += "\n"
blob += "\n"
blob += head_foot + "\n\n"
blob += self.format_items(items, ql)
blob += head_foot
# noinspection LongLine
blob += f"\nItem DB rips created using the {self.text.make_chatcmd('Budabot Items Extractor', '/start https://github.com/Budabot/ItemsExtractor')} tool."
@@ -102,7 +101,7 @@ class ItemsController:
msg = ""
msg += item_group[0].name
for item in reversed(item_group):
for item in item_group:
if ql:
if item.lowql != item.highql:
msg += f" {self.text.make_item(item.lowid, item.highid, ql, ql)}"
@@ -134,7 +133,7 @@ class ItemsController:
params.append(ql)
params.append(ql)
sql += " ORDER BY name ASC, highql DESC"
sql += " ORDER BY name, highid, highql DESC"
return self.db.query(sql, params, extended_like=True)
@@ -223,10 +222,10 @@ class ItemIter:
item = self.items[self.current_index]
if item.name != current_item.name \
or item.icon != current_item.icon \
or item.highql == current_item.highql:
or item.highql == current_item.highql \
or item.highql < current_item.highql:
break
current_item = item
grouped.append(item)
self.current_index += 1
return grouped
+10 -3
View File
@@ -8,9 +8,9 @@ from core.command_alias_service import CommandAliasService
from core.command_param_types import Const, Int, Any
from core.db import DB
from core.decorators import instance, command, timerevent
from core.igncore import IgnCore
from core.setting_service import SettingService
from core.text import Text
from core.igncore import IgnCore
from modules.core.accounting.services.account_service import AccountService
from modules.raidbot.raid.raidbot_controller import Raider
from modules.standard.items.items_controller import ItemsController
@@ -336,9 +336,16 @@ class LootController:
loot += item
self.add_item_to_loot(item)
else:
loot += item
self.add_item_to_loot(item, item_count=item_count)
out = re.match(r"(([^<]+)?<a href=[\"\']itemref://(\d+)/(\d+)/(\d+)[\"\']>([^<]+)</a>([^<]+)?)", item)
if out:
# print(out.groups())
loot += item
item = self.text.make_item(int(out[3]), int(out[4]), int(out[5]), out[6])
self.add_item_to_loot((out[2] or "") + item + (out[7] or ""), item_count=item_count)
else:
loot += item
self.add_item_to_loot(item, item_count=item_count)
self.bot.send_private_channel_message(f"<highlight>{loot}<end> was added to loot list.")
@timerevent(budatime="1h",
+63 -10
View File
@@ -8,14 +8,15 @@ from core.buddy_service import BuddyService
from core.chat_blob import ChatBlob
from core.command_param_types import Options, Int, Const, Any
from core.db import DB
from core.decorators import instance, event, command
from core.decorators import instance, event, command, setting
from core.dict_object import DictObject
from core.igncore import IgnCore
from core.job_scheduler import JobScheduler
from core.logger import Logger
from core.lookup.pork_service import PorkService
from core.setting_service import SettingService
from core.setting_types import BooleanSettingType
from core.text import Text
from core.igncore import IgnCore
from core.util import Util
from modules.core.accounting.preference_controller import PreferenceController
from modules.core.accounting.services.account_service import AccountService
@@ -76,6 +77,7 @@ hh:mm - DD.MM.YYYY
"added_by int not null, "
"added_at timestamp, "
"headline int not null default 0)")
self.db.exec("CREATE TABLE IF NOT EXISTS news_read(main INT NOT NULL PRIMARY KEY, post_id INT NOT NULL)")
def start(self):
self.commands = f" [{self.text.make_chatcmd('raids', '/tell <myname> raids')}] " \
@@ -89,7 +91,8 @@ hh:mm - DD.MM.YYYY
def logon_event(self, _, data):
if not self.bot.is_ready():
return
if "member" in self.buddy_service.get_buddy(data.packet.char_id)["types"]:
if "member" in self.buddy_service.get_buddy(data.packet.char_id)["types"] \
or "org_member" in self.buddy_service.get_buddy(data.packet.char_id)["types"]:
account = data.account
# Apply standard checks. (User Banned, Account disabled, ...)
if not self.account_service.simple_checks(data.account):
@@ -101,18 +104,21 @@ hh:mm - DD.MM.YYYY
discord = f"Your Account is <highlight>not</highlight> connected to Discord " \
f"[{self.text.make_chatcmd('Join', '/tell <myname> discord invite')}]"
else:
prefix = ""
if self.setting_service.get_value('is_alliance_bot') == '1':
prefix = f"[{self.alias_controller.get_alias(account.org_id)}] "
discord = f"Your Account <highlight>is</highlight> connected to Discord as " \
f"<highlight>[{self.alias_controller.get_alias(account.org_id)}] " \
f"{account.name}</highlight>"
f"<highlight>{prefix}{account.name}</highlight>"
user = self.pork.get_character_info(data.packet.char_id)
self.job_schedule.delayed_job(self.send_news, 15, user, self.account_service.get_alts(account.main),
discord,
self.preferences.get_pref_view_small(account))
@command(command="raids", params=[], description="Show the Raids", access_level="member")
def show_raids(self, _):
with open("data/latest_raids.txt", "r") as f:
return self.text.format_page("Raidschedule", f.read())
# @command(command="raids", params=[], description="Show the Raids", access_level="member")
# def show_raids(self, _):
# with open("data/latest_raids.txt", "r") as f:
# return self.text.format_page("Raidschedule", f.read())
def get_timers(self):
events = [self.weekly.get_next_bs(),
@@ -140,12 +146,21 @@ hh:mm - DD.MM.YYYY
self.weekly.get_next_dio()]
return sorted(events, key=lambda timer: timer.start)[0]
@setting(name="preview_recent", value="true",
description="Sends the newest News entry to the user directly, without wrapping it into a blob")
def preview_recent(self):
return BooleanSettingType()
def send_news(self, _, sender, alts, discord, prefs, auto=True):
if auto:
if self.buddy_service.get_buddy(sender.char_id)["online"] == 0:
return
timers = self.get_timers()
next_event = self.get_next_event()
last_updated: datetime = (self.db.query_single("SELECT added_at FROM news order by ID desc") or {}).get(
"added_at", 0)
if type(last_updated) != int:
last_updated = last_updated.astimezone().timestamp()
news = self.layout.format(time=datetime.now(timezone.utc).strftime("%H:%M - %d.%m.%Y") + " [UTC-0]",
main=alts[0].name,
alts_show=self.text.make_chatcmd(len(alts), "/tell <myname> alts"),
@@ -155,15 +170,38 @@ hh:mm - DD.MM.YYYY
prefs=prefs,
discord=discord)
name = self.weekly.get_long_name(next_event.type)
if last_updated > 0:
last_updated = f"[{self.util.time_to_readable(datetime.utcfromtimestamp(time.time()).timestamp() - last_updated)} ago]"
else:
last_updated = ""
blob = f"<yellow>{random.choice(self.greetings)} {sender.name} :: " \
f"{self.text.format_page('Your News', textwrap.dedent(news))} </yellow> " \
f"{self.text.format_page('Your News', textwrap.dedent(news))} </yellow>{last_updated} " \
f"{f'<yellow>::</yellow> {name} running' if next_event.is_running() else ''}"
self.bot.send_mass_message(sender.char_id, blob)
if self.preview_recent().get_value() and auto:
entry = self.get_newest_entry()
if not entry:
return
if entry.id > self.get_read_id(sender.char_id):
data = f":: <yellow>Your most recent unread news</yellow> :: \n" \
f"{entry.text}\n" \
f"{self.text.format_page('Mark as read', self.text.make_tellcmd('Mark as read', f'news read {entry.id}'))}"
self.bot.send_mass_message(sender.char_id, data)
#####################
# News Management #
#####################
@command(command="news", params=[Const("read"), Int("ID", is_optional=True)],
description="Marks All news until [ID] as read, or all of none specified.",
access_level="member",
extended_description="It is also possible to move the read marker back, i.e. from ID 5 -> 1.\nThis has no effect if the preview_recent setting is off.")
def cmd_news_read(self, sender, _, entry_id):
if not entry_id:
entry_id = (self.db.query_single("SELECT id from news ORDER BY id desc limit 1") or {}).get("id", 0)
self.mark_as_read(sender.sender.char_id, entry_id)
return f"Your News Read marker has been moved to ID <highlight>{entry_id}</highlight>."
@command(command="news", params=[], description="Show the news", access_level="member")
def show_news(self, sender):
user = self.db.query_single("SELECT * FROM player where char_id=?", [sender.sender.char_id])
@@ -243,6 +281,8 @@ hh:mm - DD.MM.YYYY
normal = ""
headline = ""
for entry in data:
if len(self.text.strip_html_tags(headline + normal) or []) > limit:
break
if entry.headline == 1:
if len(self.text.strip_html_tags(headline) or []) > limit / 2:
continue
@@ -268,3 +308,16 @@ hh:mm - DD.MM.YYYY
def add_entry(self, text, sender):
self.db.exec("INSERT INTO news(text, added_by, added_at) VALUES(?, ?, ?)",
[text, sender.char_id, datetime.utcfromtimestamp(time.time())])
def get_newest_entry(self):
return self.db.query_single("SELECT * from news order by id desc")
def get_read_id(self, char_id):
return (self.db.query_single(
"SELECT post_id from news_read where main = (SELECT main from account where char_id=?)",
[char_id]) or {}).get('post_id', 0)
def mark_as_read(self, char_id, post_id):
self.db.exec(
"INSERT INTO news_read(main, post_id) VALUES((SELECT main from account where char_id=? LIMIT 1), ?) ON DUPLICATE KEY UPDATE post_id=?",
[char_id, post_id, post_id])
+67 -45
View File
@@ -1,31 +1,30 @@
import time
from core.chat_blob import ChatBlob
from core.command_alias_service import CommandAliasService
from core.command_param_types import Any
from core.decorators import instance, command, event
from core.dict_object import DictObject
from core.igncore import IgnCore
from core.job_scheduler import JobScheduler
from core.logger import Logger
from core.setting_service import SettingService
from core.setting_types import BooleanSettingType
from core.text import Text
from core.igncore import IgnCore
from core.util import Util
from modules.standard.datanet.ws_controller import WebsocketRelayController
@instance()
class WorldBossController:
# Timers are provided through an local websocket relay, which gets fed by an external API;
# If you intend to take advantage of this module,
# you **will** need to contact the API host of your choice to whitelist
# The IP Addresses with which you intend to access the API.
# Timers are provided through a local websocket relay, which gets fed by an external API.
# example timer data:
# [{"name":"Tarasque","time": <mortal time>},
# {"name":"Vizaresh","time": <mortal time>}]
timer_data = []
alerts = [480 * 60, 360 * 60, 240 * 60, 120 * 60, 60 * 60, 60 * 15,
60 * 5, 60 * 3, 60 * 2, 60, 30, 15, 10, 5, 4, 3, 2, 1, 0]
60 * 5, 60 * 3, 60 * 2, 60, 30, 15, 5, 0]
jobs = []
def inject(self, registry):
@@ -38,25 +37,46 @@ class WorldBossController:
self.setting_service: SettingService = registry.get_instance("setting_service")
def pre_start(self):
self.setting_service.register(self.module_name, 'timer_spam', True, BooleanSettingType(),
"should timers be spammed")
self.setting_service.register(self.module_name, 'timer_spam', False, BooleanSettingType(),
"should timers be spammed")
self.command_alias_service.add_alias("tara", "wb tara")
self.command_alias_service.add_alias("gaunt", "wb gaunt")
self.command_alias_service.add_alias("loren", "wb loren")
self.command_alias_service.add_alias("reaper", "wb reaper")
@event(WebsocketRelayController.WS_RELAY, "save most current timers")
@event(WebsocketRelayController.WS_RELAY, "save most current local_timers")
def get_timer(self, _, data):
if data.type == "timer":
self.timer_data = data.payload
def test(test_data):
if test_data:
if data.type != "timer":
return
local_timers = {}
spam = True if self.setting_service.get_value("timer_spam") == "1" else False
for x in self.timer_data:
local_timers[x['name']] = x['time']
for row in data.payload:
spawn = self.get_spawn(row)
if not spawn:
continue
if row['name'] not in local_timers:
self.timer_data.append(row)
if spam:
for x in self.jobs:
if x['name'] == row['name']:
self.job_scheduler.cancel_job(x['id'])
self.jobs.append(
{'name': row['name'], 'id': self.job_scheduler.delayed_job(self.timer_alert, 2, spawn)})
continue
elif (local_timers[row['name']] + 1) < row['time']:
for timer in self.timer_data:
if timer['name'] == row['name']:
timer['time'] = row['time']
if spam:
for job in self.jobs:
if job["name"] == test_data.name:
return
self.job_scheduler.delayed_job(self.timer_alert, 2, test_data)
if self.setting_service.get_value("timer_spam") == "1":
for row in self.timer_data:
data = self.get_spawn(row)
test(data)
if job['name'] == row['name']:
self.job_scheduler.cancel_job(job['id'])
alert_duration = self.get_next_alert(spawn.at - time.time())
job_id = self.job_scheduler.scheduled_job(self.timer_alert, 2,
spawn)
job['id'] = job_id
def get_spawn(self, timer):
timer = DictObject(timer)
@@ -108,26 +128,26 @@ class WorldBossController:
return duration - alert - 1
return duration
@command(command="gaunt", params=[], description="Displays the next Vizaresh pop time", access_level="member")
def show_gaunt(self, _):
for timer in self.timer_data:
if timer['name'] == "Vizaresh":
return self.show_user(timer)
return "Timer not found"
@command(command="loren", params=[], description="Displays the next Loren Warr pop time", access_level="member")
def show_loren(self, _):
for timer in self.timer_data:
if timer['name'] == "Loren Warr":
return self.show_user(timer)
return "Timer not found"
@command(command="tara", params=[], description="Displays the next Tara pop time", access_level="member")
def show_tara(self, _):
for timer in self.timer_data:
if timer['name'] == "Tarasque":
return self.show_user(timer)
return "Timer not found"
@command(command="wb", params=[Any("worldboss", is_optional=True)],
description="Displays the next worldboss spawns", access_level="member")
def show_worldboss(self, request, boss: str):
if boss:
boss = boss.lower()
if boss in ["tara", "tarasque"]:
boss = "Tarasque"
elif boss in ["viza", "vizaresh", "gaunt", "gauntlet"]:
boss = "Vizaresh"
elif boss in ["loren", "loren warr", "loren war", "lw"]:
boss = "Loren Warr"
elif boss in ["thr", "the hollow", "the hollow reaper", "reaper"]:
boss = "The Hollow Reaper"
for x in self.timer_data:
if x['name'] == boss:
return self.show_user(x)
else:
blob = "\n".join([self.show_user(x) for x in self.timer_data if
self.show_user(x) != "No timers cached; please try again later."])
return ChatBlob("Next Worldboss spawns", blob)
def show_user(self, timer):
timer = self.get_spawn(timer)
@@ -144,7 +164,9 @@ class WorldBossController:
for row in self.timer_data:
if row["name"] == timer.name:
timer = self.get_spawn(row)
alert_duration = self.get_next_alert(timer.at - t)
if not timer.at:
return
alert_duration = self.get_next_alert(timer.at - time.time())
if timer.at - time.time() < 1:
if timer.type == "mortal":
self.send_warn(f"<highlight>{timer.name}</highlight> :: is now mortal")
@@ -152,7 +174,7 @@ class WorldBossController:
elif timer.type == "spawn":
self.send_warn(f"<highlight>{timer.name}</highlight> :: has just spawned")
self.jobs = [x for x in self.jobs if x['name'] != timer.name]
else: # timer.at > time.time():
else:
if timer.type == "mortal":
self.send_warn(f"<highlight>{timer.name}</highlight> :: mortal in {self.util.format_time(timer.time)}")
elif timer.type == "spawn":
@@ -162,7 +184,7 @@ class WorldBossController:
for row in self.timer_data:
if row["name"] == timer.name:
timer = self.get_spawn(row)
job_id = self.job_scheduler.scheduled_job(self.timer_alert, t + alert_duration, timer)
job_id = self.job_scheduler.scheduled_job(self.timer_alert, time.time() + alert_duration, timer)
for job in self.jobs:
if job['name'] == timer.name:
job['id'] = job_id
+5 -2
View File
@@ -10,13 +10,13 @@ from core.db import DB
from core.decorators import instance, event, command
from core.dict_object import DictObject
from core.fifo_queue import FifoQueue
from core.igncore import IgnCore
from core.logger import Logger
from core.lookup.character_service import CharacterService
from core.private_channel_service import PrivateChannelService
from core.public_channel_service import PublicChannelService
from core.setting_service import SettingService
from core.text import Text
from core.igncore import IgnCore
from core.util import Util
from modules.core.accounting.services.account_service import AccountService
from modules.standard.online.online_display import OnlineDisplay
@@ -163,6 +163,8 @@ class OnlineController:
alts = self.account_service.get_alts(char_id)
for alt in alts:
self.afk_list[alt.char_id] = DictObject({"message": message, "time": time.time()})
# Dirty fix for players without account
self.afk_list[char_id] = DictObject({"message": message, "time": time.time()})
elif char_id in self.afk_list.keys():
# TODO handle multiple rows
@@ -173,7 +175,8 @@ class OnlineController:
if data:
continue
data = out
if not alts:
data = self.afk_list.pop(char_id)
char_name = self.character_service.resolve_char_to_name(char_id)
time_string = self.util.time_to_readable(int(time.time()) - data.time)
channel_reply(f"<highlight>{char_name}</highlight> is back after {time_string}.")
+15 -4
View File
@@ -55,6 +55,9 @@ class OnlineDisplay:
org, priv, notify = 0, 0, 0
previous = DictObject({'char_id': 0})
for player in players:
if player.faction == "":
player.faction = "unknown"
rank = ""
if 'rank' in player:
@@ -74,7 +77,8 @@ class OnlineDisplay:
if main_id != player.main_id:
main_id = player.main_id
afk = "" if not afk else f" [<notice>{afk.message} - since {self.util.time_to_readable(int(time.time() - afk.time))}</notice>]"
blob += f"\n<highlight>{player.main_name}</highlight>{rank}:{afk}\n"
style = "style='text-decoration:none'"
blob += f"\n<highlight>{self.text.make_tellcmd(player.main_name, f'alts {player.main_name}', style=style)}</highlight>{rank}:{afk}\n"
if channel_id == 1:
org += 1
elif channel_id == 2:
@@ -99,6 +103,8 @@ class OnlineDisplay:
org, priv, notify = 0, 0, 0
previous = DictObject({'char_id': 0})
for player in players:
if player.faction == "":
player.faction = "unknown"
rank = ""
if player.char_id in in_org_priv:
continue
@@ -139,6 +145,8 @@ class OnlineDisplay:
}
last = 0
for user in query:
if user.faction == "":
user.faction = "unknown"
if last == user.char_id:
continue
last = user.char_id
@@ -168,23 +176,26 @@ class OnlineDisplay:
def format_org(self, player, rank="", afk="", main_order=False):
main = f"[{self.text.make_tellcmd(player.main_name, f'alts {player.main_name}')}]" if main_order else ""
org = f"({player.org_rank_name}) " if player.org_rank_name else "(Applicant)"
return f" {self.util.get_prof_icon(player.profession)} {rank}" \
f"{self.text.zfill(player.level, 220)}/<green>{self.text.zfill(player.ai_level, 30)}</green> " \
f"<{player.faction.lower()}>{player.name}</{player.faction.lower()}> ({player.org_rank_name}){afk} {main}\n"
f"<{player.faction.lower()}>{player.name}</{player.faction.lower()}> {org}{afk} {main}\n"
def format_priv(self, player, rank="", afk="", main_order=False):
main = f"[{self.text.make_tellcmd(player.main_name, f'alts {player.main_name}')}]" if main_order else ""
org = f"({player.org_name}|{player.org_rank_name}) " if player.org_name else ""
return f" {self.util.get_prof_icon(player.profession)} {rank}" \
f"{self.text.zfill(player.level, 220)}/<green>{self.text.zfill(player.ai_level, 30)}</green> " \
f"<{player.faction.lower()}>{player.name}</{player.faction.lower()}> " \
f"({player.org_name}|{player.org_rank_name}){afk} {main}\n"
f"{org}{afk} {main}\n"
def format_notify(self, player, rank="", afk="", main_order=False):
main = f"[{self.text.make_tellcmd(player.main_name, f'alts {player.main_name}')}]" if main_order else ""
org = f"({player.org_name}|{player.org_rank_name}) " if player.org_name else ""
return f" {self.util.get_prof_icon(player.profession)} {rank}" \
f"{self.text.zfill(player.level, 220)}/<green>{self.text.zfill(player.ai_level, 30)}</green> " \
f"<{player.faction.lower()}>{player.name}</{player.faction.lower()}> " \
f"({player.org_name}|{player.org_rank_name}){afk} {main}\n"
f"{org}{afk} {main}\n"
def count_prof(self, query, params, filters):
if filters:
+1 -2
View File
@@ -6,7 +6,6 @@ from core.command_param_types import Any, Const, Character, Options
from core.decorators import instance, command, event, timerevent
from core.igncore import IgnCore
from core.text import Text
from modules.core.config.alias_controller import AliasController
from modules.standard.raid.leader_controller import LeaderController
@@ -32,7 +31,7 @@ class AssistController:
blob = ""
for caller in self.assist:
blob += caller.capitalize()
blob += f" - [{self.text.make_chatcmd('assist', f'assist {caller}')}]"
blob += f" - [{self.text.make_chatcmd('assist', f'/assist {caller}')}]"
blob += f" [{self.text.make_tellcmd('REM', f'assist del {caller}')}]<br>"
blob += self.get_assist_output()
self.last_mod = time.time()
@@ -189,14 +189,14 @@ class SpecialsController:
blob = f"Attack: <highlight>{weapon_attack:.2f} secs</highlight>\n"
blob += f"Recharge: <highlight>{weapon_recharge:.2f} secs</highlight>\n"
blob += f"Full Auto Recharge: <highlight>{full_auto_recharge:d}</highlight>\n"
blob += f"Full Auto Skill: <highlight>{full_auto_skill:d}</highlight>\n\n"
blob += f"Full Auto Recharge: <highlight>{full_auto_recharge:.2f}</highlight>\n"
blob += f"Full Auto Skill: <highlight>{full_auto_skill:.2f}</highlight>\n\n"
blob += f"Full Auto Recharge: <highlight>{full_auto_info.recharge:d} secs</highlight>\n"
blob += f"Max Number of Bullets: <highlight>{full_auto_info.max_bullets:d}</highlight>\n\n"
blob += f"Full Auto Recharge: <highlight>{full_auto_info.recharge:.2f} secs</highlight>\n"
blob += f"Max Number of Bullets: <highlight>{full_auto_info.max_bullets:.2f}</highlight>\n\n"
blob += f"You need <highlight>{full_auto_info.skill_cap:d}</highlight> Full Auto Skill " \
f"to cap your recharge at <highlight>{full_auto_info.hard_cap:d} secs</highlight>.\n\n"
blob += f"You need <highlight>{full_auto_info.skill_cap:.2f}</highlight> Full Auto Skill " \
f"to cap your recharge at <highlight>{full_auto_info.hard_cap:.2f} secs</highlight>.\n\n"
blob += "From <highlight>0 to 10K</highlight> damage, the bullet damage is unchanged.\n"
blob += "From <highlight>10K to 11.5K</highlight> damage, each bullet damage is halved.\n"
@@ -235,7 +235,7 @@ class SpecialsController:
nano_cast_info = self.get_nano_cast_info(nano_cast_init, nano_attack_time)
blob = f"Attack: <highlight>{nano_attack_time:.2f} secs</highlight>\n"
blob += f"Nano Cast Init: <highlight>{nano_cast_init:d}</highlight>\n\n"
blob += f"Nano Cast Init: <highlight>{nano_cast_init}</highlight>\n\n"
blob += f"Cast Time Reduction: <highlight>{nano_cast_info.cast_time_reduction:.2f}</highlight>\n"
blob += f"Effective Cast Time: <highlight>{nano_cast_info.effective_cast_time:.2f}</highlight>\n\n"
@@ -247,13 +247,13 @@ class SpecialsController:
f"to instacast this nano.\n\n"
blob += f"NanoC. Init needed to instacast at Full Agg (100%): " \
f"<highlight>{nano_cast_info.instacast_full_agg:d}</highlight>\n"
f"<highlight>{nano_cast_info.instacast_full_agg:.2f}</highlight>\n"
blob += f"NanoC. Init needed to instacast at Neutral (87.5%): " \
f"<highlight>{nano_cast_info.instacast_neutral:d}</highlight>\n"
f"<highlight>{nano_cast_info.instacast_neutral:.2f}</highlight>\n"
blob += f"NanoC. Init needed to instacast at Half (50%): " \
f"<highlight>{nano_cast_info.instacast_half:d}</highlight>\n"
f"<highlight>{nano_cast_info.instacast_half:.2f}</highlight>\n"
blob += f"NanoC. Init needed to instacast at Full Def (0%): " \
f"<highlight>{nano_cast_info.instacast_full_def:d}</highlight>\n\n"
f"<highlight>{nano_cast_info.instacast_full_def:.2f}</highlight>\n\n"
blob += f"Cast time at Full Agg (100%): <highlight>{nano_cast_info.cast_time_full_agg:.2f}</highlight>\n"
blob += f"Cast time at Neutral (87.5%): <highlight>{nano_cast_info.cast_time_neutral:.2f}</highlight>\n"
+7 -7
View File
@@ -2,7 +2,7 @@ import time
from core.chat_blob import ChatBlob
from core.command_param_types import Any, Const, Time, Options
from core.decorators import instance, command, event
from core.decorators import instance, command
from core.igncore import IgnCore
from core.registry import Registry
from modules.standard.news.worldboss_controller import WorldBossController
@@ -130,18 +130,18 @@ class TimerController:
else:
return f"Error! Insufficient access level to remove timer <highlight>{timer.name}</highlight>."
@event("connect", description="reload timers on restart")
def reload_timers(self, _, _1):
timers = self.db.query("SELECT * from timer")
for timer in timers:
self.timer_alert(time.time(), timer.name)
# @event("connect", description="reload timers on restart")
# def reload_timers(self, _, _1):
# timers = self.db.query("SELECT * from timer")
# for timer in timers:
# self.timer_alert(time.time(), timer.name)
@command(command="rtimer",
params=[Const("add", is_optional=True),
TimerTime("start_time"),
TimerTime("repeating_time"),
Any("name", is_optional=True)],
access_level="member",
access_level="moderator",
description="Add a timer")
def rtimer_add_cmd(self, request, _, start_time, repeating_time, timer_name):
timer_name = timer_name or self.get_timer_name(request.sender.name)
@@ -0,0 +1,67 @@
import math
from core.aochat.BaseModule import BaseModule
from core.chat_blob import ChatBlob
from core.command_param_types import Int, NamedParameters
from core.db import DB
from core.decorators import instance, command
from core.igncore import IgnCore
from core.text import Text
# noinspection DuplicatedCode
@instance()
class ContractController(BaseModule):
PAGE_SIZE = 40
# noinspection DuplicatedCode
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")
@command(command="contracts",
params=[Int('mininum', is_optional=True), NamedParameters(['page'])],
access_level="member",
description="Shows contracts")
def cotracts(self, _, min_ql, named_params):
if not min_ql:
min_ql = 200
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]
return self.format_pagination(data, offset, page, f"Tower Contracts ({len(data)})",
f"There are no orgs with more than "
f"<highlight>{min_ql}</highlight> contract points.",
f'contracts {min_ql}')
def format_pagination(self, data, offset, page, title, nullmsg, cmd):
selected = data[offset:offset + self.PAGE_SIZE]
count = len(selected)
pages = ""
if 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(f"Page {page + 1:d} »»", f'{cmd} --page={page + 1}')
pages += "\n"
if count == 0:
return nullmsg
else:
blob = ""
blob += "" + pages + "\n"
index = offset
for entry in selected:
index += 1
blob += self.row_formatter(entry, index, data)
blob += "\n" + pages
blob += "</font>"
return ChatBlob(title, blob)
def row_formatter(self, entry, index, data):
return f"{self.text.zfill(index, len(data))}. " \
f"<white>{self.text.zfill(entry.contracts, data[0].contracts)}</white> " \
f"<{entry.faction.lower()}>{entry.org_name}</{entry.faction.lower()}>\n"
+166
View File
@@ -0,0 +1,166 @@
import time
from core.aochat.BaseModule import BaseModule
from core.command_alias_service import CommandAliasService
from core.command_param_types import Options, Int, Any, NamedParameters
from core.db import DB
from core.decorators import instance, command, event
from core.dict_object import DictObject
from core.event_service import EventService
from core.igncore import IgnCore
from core.lookup.pork_service import PorkService
from core.public_channel_service import PublicChannelService
from core.text import Text
from core.util import Util
from modules.standard.helpbot.playfield_controller import PlayfieldController
from modules.standard.tower.tower_events import TowerEventController
@instance()
class TowerHotController(BaseModule):
PAGE_SIZE = 30
# noinspection DuplicatedCode
def inject(self, registry):
self.bot: IgnCore = registry.get_instance("bot")
self.db: DB = registry.get_instance("db")
self.util: Util = registry.get_instance("util")
self.text: Text = registry.get_instance("text")
self.event_service: EventService = registry.get_instance("event_service")
self.pork_service: PorkService = registry.get_instance("pork_service")
self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller")
self.public_channel_service: PublicChannelService = registry.get_instance("public_channel_service")
self.command_alias_service: CommandAliasService = registry.get_instance("command_alias_service")
@event(event_type=TowerEventController.TOWER_ATTACK_EVENT, description="Mark Sites in penalty as in penalty",
is_enabled=False)
def tower_victory_event(self, _, event_data):
if event_data.attacker.org_id:
self.db.exec("UPDATE towers SET penalty_until=? where org_id=?",
[time.time() + 60 * 60, event_data.attacker.org_id])
@command(command="hot",
params=[Options(['tl1', 'tl2', 'tl3', 'tl4', 'tl5', 'tl6', 'tl7']), Any('faction', is_optional=True),
NamedParameters(["page"])],
access_level="member",
description="Shows hot playfields")
def hot_tl(self, _, tl, faction: str, named_params):
if faction:
if faction.startswith("--page="):
named_params = DictObject({'page': faction[7:]})
faction = None
if faction is not None and faction.lower() not in ['omni', 'clan', 'neut', 'neutral']:
return f"Unknown faction: {faction}"
tl = tl[2:]
page = int(named_params.page or "1")
offset = (page - 1) * self.PAGE_SIZE
towers = self.get_hot_sites_tl(int(tl), faction)
return self.text.format_pagination(towers, offset, page, self.formatter, f"Hot Sites TL{tl} ({len(towers)})",
f"There are no hot sites for TL <highlight>{tl}</highlight>.",
f'hot tl{tl} {faction or ""}', self.PAGE_SIZE)
def formatter(self, row, index, data):
d = {}
if index > 1:
d = data[index - 2]
status = ""
if row.status_time <= 3600:
status += f"<red>5%</red> (closes in {self.util.time_to_readable(row.status_time)})"
elif row.status_time <= (3600 * 6):
status += f"<orange>25%</orange> (closes in {self.util.time_to_readable(row.status_time)})"
else:
status += f"<green>75%</green> (opens in {self.util.time_to_readable(row.status_time - (3600 * 6))})"
if row.penalty_until > time.time():
status += f" <red>In Penalty for: {self.util.time_to_readable(row.penalty_until - time.time())}</red>"
blob = ""
if self.get_ct_type(d.get("ql", 0)) < (tl := self.get_ct_type(row.ql)):
blob += f"<notice>TL{tl}</notice><br>"
space = f"{row.short_name} x{row.site_number}"
place = "_" * (7 - len(space))
return blob + "<tab>" + self.text.make_tellcmd(space,
f'lc {row.short_name} {row.site_number}') + \
f"<black>{place}</black> QL {row.min_ql}/<highlight>{row.ql}</highlight>/{row.max_ql} - " \
f"{self.text.get_formatted_faction(row.faction, row.org_name)}, {status}\n"
@command(command="hot",
params=[Int('level', is_optional=True), Any('faction', is_optional=True), NamedParameters(["page"])],
access_level="member",
description="Shows hot playfields by level")
def hot_level(self, _, level, faction, named_params):
if faction:
if faction.startswith("--page="):
named_params = DictObject({'page': faction[7:]})
faction = None
if faction is not None and faction.lower() not in ['omni', 'clan', 'neut', 'neutral']:
return f"Unknown faction: {faction}"
if level:
if level < 0 | level > 220:
return f"Level out of range: {level}"
page = int(named_params.page or "1")
offset = (page - 1) * self.PAGE_SIZE
towers = self.get_hot_sites(level, faction)
level = f"{level}" if level else ""
faction = f"{faction} " if faction else ""
return self.text.format_pagination(towers, offset, page, self.formatter, f"Hot Towersites ({len(towers)})",
f"There are no hot sites.", f'hot {level}{faction}', self.PAGE_SIZE)
def get_hot_sites(self, level=None, faction=None):
where = ""
now = time.time() % 86400
params = [now, now, now]
if level:
where += " AND l.pvp_min <=? and pvp_max >= ? "
params.append(level)
params.append(level)
if faction:
where += " AND c.faction LIKE ? "
params.append(faction.capitalize())
data = self.db.query("SELECT a.*, f.short_name, c.org_name, b.min_ql, b.max_ql, "
"(CASE WHEN (a.close_time-?) < 0 THEN a.close_time-? +86400 ELSE a.close_time-? END) "
" AS status_time FROM tower_sites b "
"LEFT JOIN towers a ON a.pf_id = b.playfield_id AND a.site_number = b.site_number "
"LEFT JOIN level l on a.ql = l.level "
"LEFT JOIN playfields f on a.pf_id = f.id "
f"LEFT JOIN all_orgs c ON a.org_id = c.org_id WHERE close_time IS NOT NULL {where} ORDER BY a.ql",
params)
return [x for x in data if x.status_time - (3600 * 6) < 60 * 60 or x.penalty_until > time.time()]
def get_hot_sites_tl(self, tl=7, faction=None):
min_ql, max_ql = self.util.get_level_range_tl(tl)
where = ""
now = time.time() % 86400
params = [now, now, now]
where += " AND ql between ? and ? "
params.append(min_ql)
params.append(max_ql)
if faction:
where += " AND c.faction LIKE "
params.append("%" + faction.capitalize() + "%")
data = self.db.query("SELECT a.*, f.short_name, c.org_name, b.min_ql, b.max_ql, "
"(CASE WHEN (a.close_time-?) < 0 THEN a.close_time-? +86400 ELSE a.close_time-? END) "
" AS status_time FROM tower_sites b "
"LEFT JOIN towers a ON a.pf_id = b.playfield_id AND a.site_number = b.site_number "
"LEFT JOIN level l on a.ql = l.level "
"LEFT JOIN playfields f on a.pf_id = f.id "
f"LEFT JOIN all_orgs c ON a.org_id = c.org_id WHERE close_time IS NOT NULL {where} ORDER BY a.ql",
params)
return [x for x in data if x.status_time - (3600 * 6) < 60 * 60 or x.penalty_until > time.time()]
def get_ct_type(self, ql):
if ql == 0:
return 0
elif ql < 34:
return 1
elif ql < 82:
return 2
elif ql < 129:
return 3
elif ql < 177:
return 4
elif ql < 201:
return 5
elif ql < 226:
return 6
else:
return 7
+279
View File
@@ -0,0 +1,279 @@
import time
from datetime import datetime
import pytz
from core.chat_blob import ChatBlob
from core.command_alias_service import CommandAliasService
from core.command_param_types import Any, Int, Const
from core.db import DB
from core.decorators import instance, command, event, setting
from core.event_service import EventService
from core.igncore import IgnCore
from core.lookup.pork_service import PorkService
from core.public_channel_service import PublicChannelService
from core.setting_types import BooleanSettingType
from core.text import Text
from core.util import Util
from modules.standard.helpbot.playfield_controller import PlayfieldController
# legacy(0), EU - friendly(1) or US - friendly(2) => timing
from modules.standard.tower.tower_attack_controller import TowerAttackController
from modules.standard.tower.tower_events import TowerEventController
FIXED_TIMES = {1: 4,
2: 20}
@instance()
class LandController:
def inject(self, registry):
self.bot: IgnCore = registry.get_instance("bot")
self.db: DB = registry.get_instance("db")
self.text: Text = registry.get_instance("text")
self.util: Util = registry.get_instance("util")
self.event_service: EventService = registry.get_instance("event_service")
self.pork_service: PorkService = registry.get_instance("pork_service")
self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller")
self.public_channel_service: PublicChannelService = registry.get_instance("public_channel_service")
self.command_alias_service: CommandAliasService = registry.get_instance("command_alias_service")
self.tac: TowerAttackController = registry.get_instance("tower_attack_controller")
def pre_start(self):
self.command_alias_service.add_alias('towers', "lc")
self.command_alias_service.add_alias('tower', "lc")
self.command_alias_service.add_alias('lca', "lc")
self.db.load_sql_file(self.module_dir + "/" + "tower_sites.sql", pre_optimized=True)
self.db.shared.exec("CREATE TABLE IF NOT EXISTS `towers` ("
"`tower_id` INT(11) NOT NULL,"
"`pf_id` INT(11) NOT NULL,"
"`site_number` INT(11) NOT NULL,"
"`x_coord` INT(11) NOT NULL,"
"`y_coord` INT(11) NOT NULL,"
"`high_id` INT(11) NOT NULL,"
"`ql` INT(11) NOT NULL,"
"`org_id` INT(11),"
"`faction` VARCHAR(11),"
"`planted` INT(11),"
"`close_time` INT(11),"
"`penalty_until` INT(11),"
"PRIMARY KEY (`tower_id`) USING HASH,"
"INDEX `site` (`pf_id`, `site_number`, `org_id`, `close_time`, `penalty_until`) USING HASH,"
"INDEX `tower` (`planted`, `ql`, `x_coord`, `y_coord`) USING HASH) "
"COLLATE='utf8mb4_general_ci' ENGINE=MEMORY;")
self.db.create_view("towers")
@setting(name="lc_cmd_full", value="false", description="Toggle the verbosity of !lc commands")
def lc_full(self) -> BooleanSettingType:
return BooleanSettingType()
@event(event_type=TowerEventController.TOWER_VICTORY_EVENT, description="Purge sites which got wiped from DB",
is_enabled=False)
def tower_victory_event(self, _, event_data):
row = self.tac.get_last_attack(event_data.winner.faction, event_data.winner.org_name, event_data.loser.faction,
event_data.loser.org_name, event_data.location.playfield.id, time.time())
if row:
self.db.exec("DELETE FROM towers where pf_id=? and site_number=?", [row.pf_id, row.site])
@command(command="lc", params=[], access_level="member",
description="See a list of playfields containing land control tower sites")
def lc_list_cmd(self, request):
data = self.db.query(
"SELECT id, long_name, short_name FROM playfields "
"WHERE id IN (SELECT DISTINCT playfield_id FROM tower_sites) ORDER BY short_name")
blob = ""
for row in data:
blob += f"[{row.id:d}] {self.text.make_tellcmd(row.long_name, f'lc {row.short_name}')} <highlight>{row.short_name}</highlight>\n"
return ChatBlob(f"Land Control Playfields ({len(data):d})", blob)
@command(command="lc", params=[Const("org"), Any("search")], access_level="member",
description="See a list of land control tower sites in a particular playfield")
def sites_org_cmd(self, request, _, search):
if search.isdigit():
org_id = int(search)
result = search
else:
orgs = self.find_orgs(search)
num_orgs = len(orgs)
if num_orgs == 0:
char_info = self.pork_service.get_character_info(search)
if char_info:
if not char_info.org_id:
return f"<highlight>{search.capitalize()}</highlight> does not appear to belong to an org."
else:
org_id = char_info.org_id
result = char_info.org_name
else:
return f"Character or org <highlight>{search}</highlight> does not own any sites."
elif num_orgs == 1:
result = orgs[0].org_name
org_id = orgs[0].org_id
else:
blob = ""
for org in orgs:
blob += self.text.make_tellcmd(f"{org.org_name} ({org.org_id})",
f"lc org {org.org_id}") + "\n"
return ChatBlob(f"Orgs matching your search criteria ({num_orgs})", blob)
data = self.get_towers_by_org(org_id)
blob = ""
ql = 0
for x in data:
blob += f"<pagebreak>{self.format_site_info(x, time.time(), len(data))}"
blob += f"<tab>Dist: <highlight>{x.guard}</highlight> Conductors and <highlight>{x.turrets}</highlight> Turrets planted\n\n"
ql += x.ql
blob += f"Stats: QL<highlight>{ql}</highlight>, contracts up to QL<highlight>{ql * 2}</highlight>"
return ChatBlob(f"Sites owned by {result}", blob)
@command(command="lc", params=[Any("playfield"), Int("site_number", is_optional=True)], access_level="member",
description="See a list of land control tower sites in a particular playfield")
def lc_playfield_cmd(self, request, playfield_name, site_number):
playfield = self.playfield_controller.get_playfield_by_name_or_id(playfield_name)
if not playfield:
return f"Could not find playfield <highlight>{playfield_name}</highlight>."
data = self.get_towers(playfield.id, site_number)
blob = ""
t = int(time.time())
if site_number:
data = self.get_towers(playfield.id, site_number)
blob += "<pagebreak>" + self.format_site_info(data, t)
else:
for row in data:
blob += f"<pagebreak>{self.format_site_info(row, t, len(data))}\n"
if site_number:
title = f"Tower Info: {playfield.long_name} x{site_number}"
else:
title = f"Tower Info: {playfield.long_name} ({len(data)})"
return ChatBlob(title, blob)
@command(command="free", params=[],
access_level="member",
description="Shows potentially free towerfields")
def free(self, _, ):
blob = ""
data = self.get_free()
for row in data:
blob += f"<pagebreak>{self.format_site_info(row, time.time(), len(data))}\n"
return ChatBlob(f"FREE Towersites ({len(data)})", blob) if blob else f"No free towersites found."
def format_site_info(self, row, t, count=0):
data = row
if count == 0 and data:
row = data[0]
blob = f"<highlight>{row.short_name} x{row.site_number}</highlight> ({row.site_name})\n"
blob += f"<tab>Level Range: <white>{row.min_ql} - {row.max_ql}</white> "
if row.timing == 0:
blob += f"[<grey>Legacy</grey>]\n"
if row.timing == 1:
blob += f"[<grey><black>0</black>4 UTC</grey>]\n"
if row.timing == 2:
blob += f"[<grey>20 UTC</grey>]\n"
blob += f"<tab>Coordinates: "
blob += self.text.make_chatcmd(f"{row.x_coord:d}x{row.y_coord:d}",
f"/waypoint {row.x_coord:d} {row.y_coord:d} {row.pf_id:d}") + "\n"
if row.get("org_name", None):
current_day_time = t % 86400
if row.timing > 0:
row.close_time = FIXED_TIMES[row.timing] * 3600 + row.planted % 3600
value = datetime.fromtimestamp(row.close_time, tz=pytz.UTC)
current_status_time = row.close_time - current_day_time
if current_status_time < 0:
current_status_time += 86400
status = ""
if current_status_time <= 3600:
status += f"<red>5%</red> (closes in {self.util.time_to_readable(current_status_time)})"
elif current_status_time <= (3600 * 6):
status += f"<orange>25%</orange> (closes in {self.util.time_to_readable(current_status_time)})"
else:
status += f"<green>75%</green> (opens in {self.util.time_to_readable(current_status_time - (3600 * 6))})"
if row.penalty_until > t:
status += f" <red>Penalty</red> (for {self.util.time_to_readable(row.penalty_until - t)})"
blob += f"<tab>CT: QL<highlight>{row.ql}</highlight> ({self.text.get_formatted_faction(row.faction, row.org_name)}) T{self.get_ct_type(row.ql)} - Planted {self.util.time_to_readable(t - row.planted)} ago\n"
blob += f"<tab>Gas: {status}\n"
if self.lc_full().get_value():
towers = ""
cond, turret = 0, 0
if count == 0:
for tower in data:
if tower.name.__contains__("Turret"):
turret += 1
elif tower.name.__contains__("Conductor"):
cond += 1
towers += f" - QL<highlight>{self.text.zfill(tower.ql, 220)}</highlight> {tower.name}\n"
blob += f"<tab>Dist: <highlight>{cond}</highlight> Conductors and <highlight>{turret}</highlight> Turrets\n"
blob += "\n Towers:\n"
blob += towers
else:
if not row.enabled:
blob += "<red>Disabled</red>\n"
else:
blob += "<tab><red>This site is potentially unplanted</red>\n"
return blob
def get_ct_type(self, ql):
if ql < 34:
return 1
elif ql < 82:
return 2
elif ql < 129:
return 3
elif ql < 177:
return 4
elif ql < 201:
return 5
elif ql < 226:
return 6
else:
return 7
def get_free(self):
return self.db.query("""SELECT d.playfield_id AS pf_id,d.site_number, d.site_name, d.min_ql, d.max_ql, d.x_coord, d.y_coord, d.timing, d.enabled, a.tower_id, a.ql, a.close_time, a.penalty_until, a.planted, b.*, c.*, e.* FROM tower_sites d
LEFT JOIN towers a on a.pf_id = d.playfield_id and a.site_number = d.site_number
LEFT JOIN aodb b ON a.high_id = b.highid
LEFT JOIN playfields c on d.playfield_id = c.id
LEFT JOIN all_orgs e on a.org_id = e.org_id
WHERE a.org_id IS NULL AND d.enabled = 1 GROUP BY d.playfield_id, d.site_number """)
def get_towers(self, pf, site=None):
if site:
return self.db.query("""SELECT d.playfield_id AS pf_id,d.site_number, d.site_name, d.min_ql, d.max_ql, d.x_coord, d.y_coord, d.timing, d.enabled, a.tower_id, a.ql, a.close_time, a.penalty_until, a.planted, b.*, c.*, e.* FROM tower_sites d
LEFT JOIN towers a on a.pf_id = d.playfield_id and a.site_number = d.site_number
LEFT JOIN aodb b ON a.high_id = b.highid
LEFT JOIN playfields c on d.playfield_id = c.id
LEFT JOIN all_orgs e on a.org_id = e.org_id
WHERE playfield_id=? AND d.site_number=? ORDER BY close_time IS NULL, ql desc""",
[pf, site])
else:
return self.db.query("""SELECT d.playfield_id AS pf_id, d.site_number, d.site_name, d.min_ql, d.max_ql, d.x_coord, d.y_coord, d.timing, d.enabled, a.tower_id, a.ql, a.close_time, a.penalty_until, a.planted, b.*, c.*, e.* FROM tower_sites d
LEFT JOIN towers a on a.pf_id = d.playfield_id and a.site_number = d.site_number
LEFT JOIN aodb b ON a.high_id = b.highid
LEFT JOIN playfields c on d.playfield_id = c.id
LEFT JOIN all_orgs e on a.org_id = e.org_id
WHERE playfield_id=?
GROUP BY playfield_id, site_number
ORDER BY site_number, ql DESC
""", [pf])
def get_towers_by_org(self, org_id):
return self.db.query(
"SELECT COUNT(CASE WHEN name LIKE '%Turret%' THEN 1 WHEN name LIKE '%SAM Battery%' THEN 1 END) turrets, "
"COUNT(CASE WHEN name LIKE '%Guard%' THEN 1 END) guard, "
"a.*, b.*, c.*, d.site_name, d.min_ql, d.max_ql, d.timing, d.enabled, e.* FROM towers a "
"LEFT JOIN aodb b ON a.high_id = b.highid "
"LEFT JOIN playfields c on a.pf_id = c.id "
"LEFT JOIN tower_sites d on a.pf_id = d.playfield_id and a.site_number = d.site_number "
"LEFT JOIN all_orgs e on a.org_id = e.org_id "
"WHERE a.org_id=? GROUP BY a.pf_id, a.site_number ORDER BY ql, close_time IS NOT NULL", [org_id])
def find_orgs(self, search):
return self.db.query("SELECT DISTINCT a.org_name, a.org_id FROM all_orgs a "
"LEFT JOIN towers b ON a.org_id = b.org_id WHERE org_name <EXTENDED_LIKE=0> ? AND b.org_id IS NOT NULL",
[search], extended_like=True)
@@ -0,0 +1,247 @@
import time
from core.decorators import instance, event
from core.igncore import IgnCore
from core.logger import Logger
from core.text import Text
from modules.standard.helpbot.playfield_controller import PlayfieldController
from modules.standard.tower.tower_events import TowerEventController
@instance()
class TowerAttackController:
def __init__(self):
self.logger = Logger(__name__)
def inject(self, registry):
self.bot: IgnCore = registry.get_instance("bot")
self.db = registry.get_instance("db")
self.text: Text = registry.get_instance("text")
self.tower: TowerEventController = registry.get_instance("tower_controller")
self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller")
def start(self):
self.db.shared.exec(
"CREATE TABLE IF NOT EXISTS tower_attacker ("
"id INT PRIMARY KEY AUTO_INCREMENT, "
"att_org_name VARCHAR(50) NOT NULL, "
"att_faction VARCHAR(10) NOT NULL, "
"att_char_id INT, att_char_name VARCHAR(20) NOT NULL, "
"att_level INT NOT NULL, "
"att_ai_level INT NOT NULL, "
"att_profession VARCHAR(15) NOT NULL, "
"x_coord INT NOT NULL, "
"y_coord INT NOT NULL, "
"is_victory SMALLINT NOT NULL, "
"tower_battle_id INT NOT NULL, "
"created_at INT NOT NULL)")
self.db.shared.exec(
"CREATE TABLE IF NOT EXISTS tower_battle ("
"id INT PRIMARY KEY AUTO_INCREMENT, "
"playfield_id INT NOT NULL, "
"site_number INT NOT NULL, "
"def_org_name VARCHAR(50) NOT NULL, "
"def_faction VARCHAR(10) NOT NULL, "
"is_finished INT NOT NULL, "
"battle_type VARCHAR(20) NOT NULL, "
"last_updated INT NOT NULL)"
"")
self.db.create_view("tower_battle")
self.db.create_view("tower_attacker")
@event(event_type=TowerEventController.TOWER_ATTACK_EVENT, description="Create logentries for tower attacks",
is_enabled=False)
def tower_attack_event(self, _, event_data):
t = int(time.time())
site_number = self.find_closest_site_number(event_data.location.playfield.id, event_data.location.x_coord,
event_data.location.y_coord)
attacker = event_data.attacker or {}
defender = event_data.defender
battle = self.find_or_create_battle(event_data.location.playfield.id, site_number, defender.org_name,
defender.faction, "attack", t)
self.db.exec(
"INSERT INTO tower_attacker (att_org_name, att_faction, att_char_id, att_char_name, "
"att_level, att_ai_level, att_profession, "
"x_coord, y_coord, is_victory, tower_battle_id, created_at) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
[attacker.get("org_name", ""), attacker.get("faction", ""), attacker.get("char_id", 0),
attacker.get("name", ""), attacker.get("level", 0),
attacker.get("ai_level", 0), attacker.get("profession", ""), event_data.location.x_coord,
event_data.location.y_coord, 0, battle.id, t])
@event(event_type=TowerEventController.TOWER_VICTORY_EVENT, description="Record tower victories", is_enabled=False)
def tower_victory_event(self, _, event_data):
t = int(time.time())
if event_data.type == "attack":
row = self.get_last_attack(event_data.winner.faction, event_data.winner.org_name, event_data.loser.faction,
event_data.loser.org_name, event_data.location.playfield.id, t, is_finished=0)
if not row:
site_number = 0
is_finished = 1
self.db.exec(
"INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, "
"is_finished, battle_type, last_updated) "
"VALUES (?, ?, ?, ?, ?, ?, ?)",
[event_data.location.playfield.id, site_number, event_data.loser.org_name, event_data.loser.faction,
is_finished, event_data.type, t])
battle_id = self.db.last_insert_id()
attacker = event_data.winner or {}
self.db.exec(
"INSERT INTO tower_attacker (att_org_name, att_faction, att_char_id, "
"att_char_name, att_level, att_ai_level, att_profession, "
"x_coord, y_coord, is_victory, tower_battle_id, created_at) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
[attacker.get("org_name", ""), attacker.get("faction", ""), attacker.get("char_id", 0),
attacker.get("name", ""), attacker.get("level", 0),
attacker.get("ai_level", 0), attacker.get("profession", ""), 0, 0, 0, battle_id, t])
else:
is_victory = 1
self.db.exec("UPDATE tower_attacker SET is_victory = ? WHERE id = ?", [is_victory, row.attack_id])
is_finished = 1
self.db.exec("UPDATE tower_battle SET is_finished = ?, last_updated = ? WHERE id = ?",
[is_finished, t, row.battle_id])
elif event_data.type == "terminated":
site_number = 0
is_finished = 1
row = self.find_similar_attacks(event_data.loser.faction, event_data.loser.org_name,
event_data.location.playfield.id, t, finished=0)
if row:
self.db.exec("UPDATE tower_battle SET is_finished = ?, last_updated = ? WHERE id = ?",
[is_finished, t, row.battle_id])
else:
self.db.exec(
"INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, "
"is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)",
[event_data.location.playfield.id, site_number, event_data.loser.org_name, event_data.loser.faction,
is_finished, event_data.type, t])
else:
raise Exception("Unknown victory event type: '%s'" % event_data.type)
def find_closest_site_number(self, playfield_id, x_coord, y_coord):
# noinspection SqlUnused
sql = """
SELECT
site_number,
((x_distance * x_distance) + (y_distance * y_distance)) radius
FROM
(SELECT
playfield_id,
site_number,
min_ql,
max_ql,
x_coord,
y_coord,
site_name,
(x_coord - ?) as x_distance,
(y_coord - ?) as y_distance
FROM
tower_sites
WHERE
playfield_id = ?) t
ORDER BY
radius
LIMIT 1"""
row = self.db.query_single(sql, [x_coord, y_coord, playfield_id])
if row:
return row.site_number
else:
return 0
def find_or_create_battle(self, playfield_id, site_number, org_name, faction, battle_type, t):
last_updated = t - (8 * 3600)
is_finished = 0
sql = """
SELECT
*
FROM
tower_battle
WHERE
playfield_id = ?
AND site_number = ?
AND is_finished = ?
AND def_org_name = ?
AND def_faction = ?
AND last_updated >= ?
"""
battle = self.db.query_single(sql, [playfield_id, site_number, is_finished, org_name, faction, last_updated])
if battle:
self.db.exec("UPDATE tower_battle SET last_updated = ? WHERE id = ?", [t, battle.id])
return battle
else:
self.db.exec(
"INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, "
"is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)",
[playfield_id, site_number, org_name, faction, is_finished, battle_type, t])
return self.db.query_single(sql, [playfield_id, site_number, is_finished, org_name, faction, last_updated])
def get_last_attack(self, att_faction, att_org_name, def_faction, def_org_name, playfield_id, t, is_finished=None):
last_updated = t - (8 * 3600)
sql = f"""
SELECT
b.id AS battle_id,
a.id AS attack_id,
b.playfield_id as pf_id,
b.site_number as site
FROM
tower_battle b
JOIN tower_attacker a ON
a.tower_battle_id = b.id
WHERE
a.att_faction = ?
AND a.att_org_name = ?
AND b.def_faction = ?
AND b.def_org_name = ?
AND b.playfield_id = ?
{'AND b.is_finished = ?' if is_finished is not None else ''}
AND b.last_updated >= ?
ORDER BY
last_updated DESC
LIMIT 1"""
return self.db.query_single(sql,
[att_faction, att_org_name, def_faction, def_org_name, playfield_id, is_finished,
last_updated]
if is_finished is not None else
[att_faction, att_org_name, def_faction, def_org_name, playfield_id, last_updated])
def find_similar_attacks(self, def_faction, def_org_name, playfield_id, t, finished=None):
last_updated = t - (8 * 3600)
sql = f"""
SELECT
b.id AS battle_id,
a.id AS attack_id,
b.playfield_id as pf_id,
b.site_number as site
FROM
tower_battle b
JOIN tower_attacker a ON
a.tower_battle_id = b.id
WHERE
b.def_faction = ?
AND b.def_org_name = ?
AND b.playfield_id = ?
{'AND b.is_finished = ?' if finished else ''}
AND b.last_updated >= ?
ORDER BY
last_updated DESC
"""
return self.db.query_single(sql,
[def_faction, def_org_name, playfield_id, finished,
last_updated] if finished else
[def_faction, def_org_name, playfield_id,
last_updated])
+170
View File
@@ -0,0 +1,170 @@
import time
from core.chat_blob import ChatBlob
from core.command_param_types import Const, Int, NamedParameters, Any
from core.decorators import instance, command
from core.igncore import IgnCore
from core.logger import Logger
from core.text import Text
from core.util import Util
from modules.standard.helpbot.playfield_controller import PlayfieldController
@instance()
class TowerController:
def __init__(self):
self.logger = Logger(__name__)
def inject(self, registry):
self.bot: IgnCore = registry.get_instance("bot")
self.db = registry.get_instance("db")
self.text: Text = registry.get_instance("text")
self.util: Util = registry.get_instance("util")
self.tower: TowerController = registry.get_instance("tower_controller")
self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller")
self.public_channel_service = registry.get_instance("public_channel_service")
@command(command="attacks", params=[Const("battle"), Int("battle_id")], access_level="member",
description="Show battle info for a specific battle")
def attacks_battle_cmd(self, _, _1, battle_id):
battle = self.db.query_single(
"SELECT b.*, p.short_name FROM tower_battle b "
"LEFT JOIN playfields p ON p.id = b.playfield_id WHERE b.id = ?",
[battle_id])
if not battle:
return "Could not find battle with ID <highlight>%d</highlight>." % battle_id
t = int(time.time())
attackers = self.db.query("SELECT * FROM tower_attacker WHERE tower_battle_id = ? ORDER BY created_at DESC",
[battle_id])
first_activity = attackers[-1].created_at if len(attackers) > 0 else battle.last_updated
blob = ""
blob += self.format_battle_info(battle, t)
blob += f"Duration: <highlight>" \
f"{self.util.time_to_readable(battle.last_updated - first_activity)}</highlight>\n\n"
blob += "<header2>Attackers:</header2>\n"
for row in attackers:
blob += "<tab>" + self.format_attacker(row)
blob += " " + self.format_timestamp(row.created_at, t)
blob += "\n"
return ChatBlob(f"Battle Info {battle_id}", blob)
@command(command="attacks", params=[NamedParameters(["page"])], access_level="member",
description="Show recent tower attacks and victories")
def attacks_cmd(self, _, named_params):
page = int(named_params.page or "1")
page_size = 30
offset = (page - 1) * page_size
data = self.get_recent_attacks(offset, page_size)
t = int(time.time())
return self.display(page, data, time.time())
@command(command="attacks",
params=[Any("playfield"), Any("site_number", is_optional=True), NamedParameters(["page"])],
access_level="member",
description="Show recent tower attacks and victories")
def cmd_attacks_pf_site(self, _, pf, site, named_params):
page = int(named_params.page or "1")
page_size = 30
offset = (page - 1) * page_size
playfield = self.playfield_controller.get_playfield_by_name_or_id(pf)
if not playfield:
return f"Could not find Playfield <highlight>{pf}</highlight>."
pf = playfield.id
data = self.get_recent_attacks_by_lca(offset, page_size, pf, site)
blob = self.display(page, data, time.time())
if site:
blob.page_postfix = f" in {playfield.short_name} on x{site}"
else:
blob.page_postfix = f" in {playfield.short_name}"
return blob
def display(self, page, data, t):
blob = ""
if page > 1:
blob += " " + self.text.make_chatcmd(f"<< Page {page - 1:d}", self.get_chat_command(page - 1))
if len(data) > 0:
blob += " Page " + str(page)
blob += " " + self.text.make_chatcmd(f"Page {page + 1:d} >>", self.get_chat_command(page + 1))
blob += "\n"
current_battle_id = -1
for row in data:
if current_battle_id != row.battle_id:
blob += "\n<pagebreak>"
current_battle_id = row.battle_id
blob += self.format_battle_info(row, t)
blob += self.text.make_tellcmd("More Info", f"attacks battle {row.battle_id:d}") + "\n"
blob += "<header2>Attackers:</header2>\n"
blob += "<tab>" + self.format_attacker(row) + "\n"
blob = ChatBlob(f"Tower Attacks", blob)
return blob
def format_attacker(self, row):
level = f"{row.att_level}/<green>{row.att_ai_level}</green>" if row.att_ai_level > 0 else f"{row.att_level}"
org = row.att_org_name + " " if row.att_org_name else ""
victor = " - <notice>Winner!</notice>" if row.is_victory else ""
return f"{row.att_char_name or 'Unknown attacker'} ({level} {row.att_profession})" \
f" {org}({row.att_faction}){victor}"
def format_battle_info(self, row, t):
blob = ""
defeated = " - <notice>Defeated!</notice>" if row.is_finished else ""
blob += f"Site: <highlight>{row.short_name} {row.site_number or '?'}</highlight>\n"
blob += f"Defender: <highlight>{row.def_org_name}</highlight> ({row.def_faction}){defeated}\n"
blob += f"Last Activity: {self.format_timestamp(row.last_updated, t)}\n"
return blob
def format_timestamp(self, t, current_t):
return f"<highlight>{self.util.format_datetime(t)}</highlight> " \
f"({self.util.time_to_readable(current_t - t)} ago)"
def get_chat_command(self, page):
return f"/tell <myname> attacks --page={page}"
def get_recent_attacks(self, offset, page_size):
return self.db.query("SELECT b.*, a.*, "
"COALESCE(a.att_level, 0) AS att_level, "
"COALESCE(a.att_ai_level, 0) AS att_ai_level, "
"p.short_name, "
"b.id AS battle_id "
"FROM tower_battle b "
"LEFT JOIN tower_attacker a ON a.tower_battle_id = b.id "
"LEFT JOIN playfields p ON p.id = b.playfield_id "
"ORDER BY b.last_updated DESC, a.created_at DESC "
"LIMIT ?, ?", [offset, page_size])
def get_recent_attacks_by_lca(self, offset, page_size, playfield, site_number=None):
if not site_number:
return self.db.query("SELECT b.*, a.*, "
"COALESCE(a.att_level, 0) AS att_level, "
"COALESCE(a.att_ai_level, 0) AS att_ai_level, "
"p.short_name, "
"b.id AS battle_id "
"FROM tower_battle b "
"LEFT JOIN tower_attacker a ON a.tower_battle_id = b.id "
"LEFT JOIN playfields p ON p.id = b.playfield_id "
"WHERE p.id =? ORDER BY b.last_updated DESC, a.created_at DESC "
"LIMIT ?, ?", [playfield, offset, page_size])
else:
return self.db.query("SELECT b.*, a.*, "
"COALESCE(a.att_level, 0) AS att_level, "
"COALESCE(a.att_ai_level, 0) AS att_ai_level, "
"p.short_name, "
"b.id AS battle_id "
"FROM tower_battle b "
"LEFT JOIN tower_attacker a ON a.tower_battle_id = b.id "
"LEFT JOIN playfields p ON p.id = b.playfield_id "
"WHERE p.id = ? AND b.site_number = ? ORDER BY b.last_updated DESC, a.created_at DESC "
"LIMIT ?, ?", [playfield, site_number, offset, page_size])
+208
View File
@@ -0,0 +1,208 @@
import re
from core.aochat import server_packets
from core.conn import Conn
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 modules.standard.helpbot.playfield_controller import PlayfieldController
@instance()
class TowerEventController:
TOWER_ATTACK_EVENT = "tower_attack"
TOWER_VICTORY_EVENT = "tower_victory"
TOWER_BATTLE_OUTCOME_ID = 42949672962
ALL_TOWERS_ID = 42949672960
# 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]
ATTACK_2 = re.compile(r"^(.+) just attacked the (clan|neutral|omni) organization (.+)'s tower in (.+) "
r"at location \((\d+), (\d+)\).\n$")
VICTORY_1 = re.compile(r"^Notum Wars Update: Victory to the (Clan|Neutral|Omni)s!!!$")
VICTORY_2 = re.compile(r"^The (Clan|Neutral|Omni) organization (.+) attacked the (Clan|Neutral|Omni) (.+) "
r"at their base in (.+). The attackers won!!$")
VICTORY_3 = [506, 147506468] # 'Notum Wars Update: The %s organization %s lost their base in %s.'
def __init__(self):
self.logger = Logger(__name__)
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.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")
def pre_start(self):
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_sites.sql", pre_optimized=True)
self.db.create_view("tower_sites")
@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 handle_public_channel_message(self, conn: Conn, packet: server_packets.PublicChannelMessage):
if conn.id != "main":
return
if packet.channel_id == self.TOWER_BATTLE_OUTCOME_ID:
victory = self.get_victory_event(packet)
if victory:
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", 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.info("tower attack packet: %s" % str(packet))
# lookup playfield
playfield_name = attack.location.playfield.long_name
attack.location.playfield = self.playfield_controller.get_playfield_by_name(playfield_name) or \
DictObject()
attack.location.playfield.long_name = playfield_name
# lookup attacker
name = attack.attacker.name
faction = attack.attacker.faction
org_name = attack.attacker.org_name
char_info = self.pork_service.get_character_info(name)
attack.attacker = char_info or DictObject()
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
return DictObject({
"attacker": {
"name": params[2],
"faction": params[0].capitalize(),
"org_name": params[1]
},
"defender": {
"faction": params[3].capitalize(),
"org_name": params[4]
},
"location": {
"playfield": {
"long_name": params[5]
},
"x_coord": params[6],
"y_coord": params[7]
}
})
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({
"attacker": {
"name": match.group(1),
"faction": "",
"org_name": ""
},
"defender": {
"faction": match.group(2).capitalize(),
"org_name": match.group(3)
},
"location": {
"playfield": {
"long_name": match.group(4)
},
"x_coord": match.group(5),
"y_coord": match.group(6)
}
})
# Unknown attack
self.logger.warning("Unknown tower attack: " + str(packet))
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({
"type": "attack",
"winner": {
"faction": match.group(1).capitalize(),
"org_name": match.group(2)
},
"loser": {
"faction": match.group(3).capitalize(),
"org_name": match.group(4)
},
"location": {
"playfield": {
"long_name": match.group(5)
}
}
})
if packet.extended_message and \
[packet.extended_message.category_id, packet.extended_message.instance_id] == self.VICTORY_3:
params = packet.extended_message.params
return DictObject({
"type": "terminated",
"winner": {
"faction": "", # params[0].capitalize(),
"org_name": "", # params[1]
},
"loser": {
"faction": params[0].capitalize(),
"org_name": params[1]
},
"location": {
"playfield": {
"long_name": params[2]
}
}
})
# Unknown victory
self.logger.warning("Unknown tower victory: " + str(packet))
return None
+282
View File
@@ -0,0 +1,282 @@
DROP TABLE IF EXISTS `tower_sites`;
CREATE TABLE IF NOT EXISTS `tower_sites`
(
`playfield_id` int(11) NOT NULL,
`site_number` smallint(6) NOT NULL,
`min_ql` smallint(6) NOT NULL,
`max_ql` smallint(6) NOT NULL,
`x_coord` smallint(6) NOT NULL,
`y_coord` smallint(6) NOT NULL,
`site_name` varchar(50) NOT NULL,
`timing` int(11) NOT NULL,
`enabled` int(11) NOT NULL,
PRIMARY KEY (`playfield_id`, `site_number`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
INSERT INTO `tower_sites` (`playfield_id`, `site_number`, `min_ql`, `max_ql`, `x_coord`, `y_coord`, `site_name`,
`timing`, `enabled`)
VALUES (505, 1, 60, 90, 2740, 4260, 'Griffon Frontier', 1, 1),
(505, 2, 80, 110, 540, 4180, 'Draught', 1, 1),
(505, 3, 70, 95, 1740, 3460, 'Dreadfire Volcano', 1, 1),
(505, 4, 80, 120, 2780, 3420, 'Northeast Barren Lands', 1, 1),
(505, 5, 60, 90, 580, 3140, 'Western Desert', 1, 1),
(505, 6, 50, 75, 2420, 1900, 'Waylander Mines', 1, 1),
(505, 7, 70, 100, 1860, 1700, 'North of Main Omni Base', 1, 1),
(505, 8, 61, 82, 460, 1380, 'Dome Ore', 1, 1),
(505, 9, 100, 150, 2700, 620, 'Crystal Forge Volcano', 1, 1),
(505, 10, 100, 150, 660, 460, 'SW Low Plateau', 1, 1),
(550, 1, 10, 20, 2660, 2020, 'Sifter Beach', 2, 1),
(550, 2, 20, 30, 1780, 1780, 'Academy Ore', 2, 1),
(550, 3, 15, 25, 1980, 1340, 'Athen Fault', 2, 1),
(550, 4, 10, 20, 2660, 820, 'Grindmoore', 2, 1),
(550, 5, 15, 23, 1380, 380, 'Gladius Grove', 2, 1),
(551, 1, 40, 90, 1700, 3700, 'Styx Magma', 0, 1),
(551, 2, 35, 50, 2220, 3340, 'Carbon Grove', 0, 1),
(551, 3, 26, 50, 980, 3140, 'Between the Craters', 0, 1),
(551, 4, 25, 35, 340, 2420, 'Powdered Dunes', 0, 1),
(551, 5, 32, 45, 2540, 2060, 'Dust Bank', 0, 1),
(551, 6, 20, 30, 580, 1740, 'Charred Groove', 0, 1),
(551, 7, 12, 45, 940, 1540, 'West of Perdition', 0, 1),
(551, 8, 15, 30, 660, 900, 'North of Yuttos', 0, 1),
(560, 1, 100, 170, 1500, 3420, 'Terraform Edge', 1, 1),
(560, 2, 170, 250, 3060, 3020, 'West Spirals', 1, 1),
(560, 3, 170, 250, 3500, 2980, 'East Spirals', 1, 1),
(560, 4, 130, 170, 1220, 2220, 'Middle Mort Desert', 1, 1),
(560, 5, 1, 100, 900, 1460, 'Green Crater', 1, 1),
(560, 6, 110, 160, 3100, 1460, 'Oasis Ore', 1, 1),
(560, 7, 150, 200, 2740, 700, 'South East Craterwall', 1, 1),
(560, 8, 100, 150, 540, 540, 'South West Craterwall', 1, 1),
(560, 9, 160, 210, 2780, 540, 'Stormshelter', 1, 1),
(565, 1, 25, 40, 2940, 2900, 'Rich Desert Ridge', 2, 1),
(565, 2, 30, 45, 1980, 2580, 'East of Meetmedere', 2, 1),
(565, 3, 50, 75, 540, 2020, 'Middle of Western Desert', 2, 1),
(565, 4, 40, 60, 2580, 1940, 'North of Rhino Village', 2, 1),
(565, 5, 40, 60, 2700, 1260, 'South of Rhino Village', 2, 0),
(567, 1, 12, 20, 1220, 1060, 'In the Newland Desert', 1, 1),
(567, 2, 15, 25, 540, 460, 'West of Newland Lake', 1, 1),
(570, 1, 200, 300, 3220, 3020, 'North of Cyborg Hideout', 1, 1),
(570, 2, 191, 250, 3780, 2540, 'Middle of Liberty', 1, 1),
(570, 3, 120, 180, 980, 2060, 'South of Sabulum', 1, 1),
(570, 4, 190, 230, 3940, 2060, 'Cyborg Border', 1, 1),
(570, 5, 200, 300, 2820, 1820, 'Middle of Perpetual Wastelands', 1, 1),
(570, 6, 200, 300, 3740, 1700, 'South of Cyborg Hideout', 1, 1),
(570, 7, 100, 150, 1500, 1340, 'Lower Plateu Zone', 1, 1),
(570, 8, 100, 150, 2100, 1380, 'The Mid Canyon Crossing', 1, 1),
(570, 9, 120, 180, 3020, 1220, 'Plains of dust', 1, 1),
(570, 10, 100, 150, 900, 1060, 'West of Canyon', 1, 1),
(570, 11, 100, 150, 3180, 940, 'The Canyon Mines', 1, 1),
(570, 12, 190, 230, 2300, 780, 'South of Canyon', 1, 1),
(585, 1, 40, 60, 1220, 2740, 'Northern Wastelands', 1, 1),
(585, 2, 11, 16, 2180, 2580, 'West Wastelands', 1, 1),
(585, 3, 40, 55, 1020, 2460, 'Mid Wastelands', 1, 1),
(585, 4, 30, 45, 2140, 1660, 'Giant Green River Bank North', 1, 1),
(585, 5, 11, 16, 1180, 1340, 'West of the Dead Forest', 1, 1),
(585, 6, 30, 45, 2100, 1340, 'Giant Green River Bank South', 1, 1),
(585, 7, 15, 22, 1420, 1020, 'Canyon East', 1, 1),
(585, 8, 25, 35, 820, 780, 'Canyon South', 1, 1),
(585, 9, 35, 50, 900, 460, 'By the River', 1, 1),
(590, 1, 140, 200, 1740, 3100, 'By the Fisher Village', 2, 1),
(590, 2, 140, 200, 2100, 3060, 'Fisher Village Approach', 2, 1),
(590, 3, 100, 170, 2900, 2820, 'North Forest', 2, 1),
(590, 4, 90, 130, 3340, 2700, 'North-east Forest', 2, 1),
(590, 5, 130, 170, 860, 1220, 'North-west of Lava Ditches', 2, 1),
(590, 6, 100, 150, 3100, 980, 'Mid Clutching Forest', 2, 1),
(590, 7, 130, 170, 860, 780, 'South-west of Lava Ditches', 2, 1),
(590, 8, 100, 150, 3180, 620, 'South Clutching Forest', 2, 1),
(595, 1, 100, 150, 1140, 3380, 'Old ruins', 0, 1),
(595, 2, 100, 150, 3180, 2900, 'Plains of defense', 0, 1),
(595, 3, 130, 180, 1740, 2300, 'The haunted forest outskirt', 0, 1),
(595, 4, 130, 180, 900, 2220, 'Forest of Xzawkaz', 0, 1),
(595, 5, 200, 300, 2260, 1860, 'In the Swamp of Horrors', 0, 1),
(595, 6, 130, 180, 1420, 1500, 'Island of Control', 0, 1),
(595, 7, 130, 180, 1340, 1140, 'The swamp of hope', 0, 1),
(595, 8, 200, 300, 2900, 1100, 'South of the Medusa', 0, 1),
(595, 9, 140, 210, 2140, 780, 'Middle of the Foul Forest', 0, 1),
(595, 10, 200, 300, 540, 540, 'Southern Forest of Xzawkaz', 0, 1),
(600, 1, 30, 45, 2420, 2980, 'By the Rivers Edge', 1, 1),
(600, 2, 50, 75, 620, 2900, 'North Forest Road', 1, 1),
(600, 3, 25, 50, 1300, 2660, 'Along the Rivers Edge', 1, 1),
(600, 4, 30, 45, 3740, 2500, 'East Forest', 1, 1),
(600, 5, 25, 50, 3140, 2020, 'Rhino Hills', 1, 1),
(600, 6, 50, 75, 580, 1700, 'West Forest', 1, 1),
(600, 7, 60, 90, 1940, 1620, 'Crossroads', 1, 1),
(600, 8, 60, 90, 1140, 1500, 'Forestdawn', 1, 1),
(600, 9, 50, 75, 3220, 1140, 'East of Crater', 1, 1),
(605, 1, 160, 200, 2940, 2820, 'Forest Waters', 2, 1),
(605, 2, 110, 120, 1100, 2620, 'Muddy Pools', 2, 1),
(605, 3, 100, 150, 1700, 2300, 'West of Wine', 2, 1),
(605, 4, 120, 180, 2940, 2260, 'East of Wine', 2, 1),
(605, 5, 130, 195, 1900, 1740, 'Central Belial Forest', 2, 1),
(605, 6, 130, 190, 2500, 1660, 'River Delta', 2, 1),
(605, 7, 160, 200, 2540, 1220, 'Junction Forest', 2, 1),
(605, 8, 100, 150, 2340, 860, 'Borderline', 2, 1),
(605, 9, 120, 180, 2020, 420, 'Southern belial Mine', 2, 1),
(605, 10, 140, 200, 620, 380, 'Southwest Belial Mining District', 2, 1),
(610, 1, 60, 90, 1380, 2780, 'Tetlies Land control area', 1, 1),
(610, 2, 80, 120, 2900, 2660, 'East of the Great Marsh', 1, 1),
(610, 3, 60, 90, 660, 2460, 'West of outpost 10-3', 1, 1),
(610, 4, 100, 150, 2300, 2020, 'Defense of Geholva', 1, 1),
(610, 5, 106, 143, 2740, 1180, 'South of Forest of Geholva', 1, 1),
(610, 6, 120, 180, 860, 900, 'Avid Crater', 1, 1),
(610, 7, 120, 180, 1540, 900, 'East of Avid Crater', 1, 1),
(610, 8, 100, 150, 2460, 540, 'Bendelham forest Defense', 1, 1),
(615, 1, 60, 100, 1900, 3020, 'North of Lenne', 1, 1),
(615, 2, 100, 150, 860, 2820, 'Little Hawaii Defense', 1, 1),
(615, 3, 90, 120, 2620, 2660, 'Defense of Zoto', 1, 1),
(615, 4, 60, 100, 900, 2100, 'By the Ocean', 1, 1),
(615, 5, 61, 100, 2300, 1180, 'Birm', 1, 0),
(615, 6, 120, 180, 2700, 660, 'SFH Defense', 1, 1),
(615, 7, 100, 150, 1860, 500, 'South in Nightplain', 1, 1),
(620, 1, 150, 200, 2700, 3860, 'Krud the Lost Valley Defense', 2, 1),
(620, 2, 150, 225, 1900, 3180, 'Pranade', 2, 1),
(620, 3, 120, 180, 620, 2980, 'Plains of Jarga Defense', 2, 1),
(620, 4, 200, 300, 2460, 2260, 'Old Plains', 2, 1),
(620, 5, 200, 300, 1540, 1780, 'Middle of Easter Fouls Plains', 2, 1),
(620, 6, 130, 200, 1540, 1140, 'Clefre Defense', 2, 1),
(620, 7, 100, 150, 2020, 860, 'Central Sharewood', 2, 1),
(620, 8, 200, 300, 820, 540, 'Pegradul', 2, 1),
(625, 1, 90, 130, 1460, 1940, 'The Resilient Forest - North', 0, 1),
(625, 2, 90, 120, 1900, 1540, 'The Resilient Forest - East', 0, 1),
(625, 3, 125, 170, 2780, 1380, 'Central Prowler Waste', 0, 1),
(625, 4, 100, 125, 1380, 1180, 'Central Resilient Forest', 0, 1),
(625, 5, 125, 170, 2860, 1020, 'Southern Prowler Waste', 0, 1),
(625, 6, 100, 150, 4020, 980, 'The Barren Hills', 0, 1),
(625, 7, 100, 125, 1740, 860, 'The Resilient Forest - South', 0, 1),
(625, 8, 50, 75, 2460, 540, 'The Silent Woods - East', 0, 1),
(630, 1, 40, 60, 1540, 2660, 'Pleasant Range Offense Hill', 0, 1),
(630, 2, 60, 90, 2380, 2500, 'Central Pleasant Range', 0, 1),
(630, 3, 50, 75, 580, 2420, 'West of 20K', 0, 1),
(630, 4, 30, 70, 3220, 2220, 'Pleasant Range Defense', 0, 1),
(630, 5, 60, 90, 3220, 1980, 'Pleasant River Defense', 0, 1),
(630, 6, 60, 90, 3260, 1500, 'Pleasant River Offense', 0, 1),
(630, 7, 40, 60, 2260, 1140, 'Central Pleasant Plains', 0, 1),
(630, 8, 30, 70, 3020, 1020, 'East Pleasant Plains', 0, 1),
(630, 9, 30, 45, 740, 460, 'West of Versailles Tower', 0, 1),
(635, 1, 55, 70, 700, 2420, 'Northern River Bank', 2, 0),
(635, 2, 60, 90, 1780, 2460, 'Hawker Trench', 2, 1),
(635, 3, 70, 105, 1460, 1740, 'Klapam Forest Defense', 2, 1),
(635, 4, 55, 70, 2020, 1740, 'Klompfot Defense', 2, 1),
(635, 5, 70, 105, 1900, 1220, 'South of Trench', 2, 1),
(635, 6, 80, 120, 1140, 940, 'Nile Hills', 2, 1),
(635, 7, 55, 70, 1780, 700, 'Aprils Rock Offense', 2, 1),
(635, 8, 80, 150, 820, 420, 'Southern Lower River Bank', 2, 1),
(635, 9, 80, 150, 1700, 340, 'Aprils Rock Defense', 2, 1),
(646, 1, 10, 15, 460, 1300, 'Great W. Forest Vein', 1, 1),
(646, 2, 10, 15, 2940, 980, 'The Hidden Notum Canal', 1, 1),
(646, 3, 20, 30, 3220, 620, 'Mountain Areas', 1, 1),
(646, 4, 10, 15, 580, 580, 'Great W. Forest Dorsal', 1, 1),
(646, 5, 10, 15, 1500, 460, 'Western Mountain Areas', 1, 1),
(647, 1, 90, 135, 1100, 3100, 'The Mineral Mine', 1, 1),
(647, 2, 20, 30, 2900, 2940, 'NE Desert Aperient', 1, 1),
(647, 3, 37, 64, 1900, 2700, 'SurroundingTemple of Three Winds', 1, 1),
(647, 4, 25, 40, 2220, 1900, 'Piercing Thundertube', 1, 1),
(647, 5, 30, 45, 2820, 1940, 'Central Striking Ant', 1, 1),
(647, 6, 25, 40, 620, 1660, 'Tir Prairie', 1, 1),
(647, 7, 25, 40, 1180, 1700, 'Crater Swamp', 1, 1),
(650, 1, 50, 75, 540, 2820, 'West Pass', 2, 1),
(650, 2, 65, 75, 900, 2300, 'Crowning Shallows', 2, 1),
(650, 3, 100, 150, 1660, 2180, 'Haven Notum Crematorium', 2, 1),
(650, 4, 70, 140, 2020, 1740, 'Stret Vale Deux Drilling Field', 2, 1),
(650, 5, 120, 180, 1340, 1620, 'The Flooded Bottomland', 2, 1),
(650, 6, 75, 90, 1820, 740, 'Stret Woods', 2, 1),
(650, 7, 60, 90, 940, 420, 'Greenslopes', 2, 1),
(655, 1, 30, 45, 420, 2700, 'Skop Notum Mine', 1, 1),
(655, 2, 30, 80, 2820, 2340, 'Klor', 1, 1),
(655, 3, 60, 80, 2820, 1660, 'Harstad', 1, 1),
(655, 4, 40, 90, 540, 1580, 'Ubleo', 1, 1),
(655, 5, 40, 60, 1420, 1580, 'Flubu Notum Mine', 1, 1),
(655, 6, 40, 70, 4340, 900, 'Plago', 1, 1),
(655, 7, 60, 80, 2260, 380, 'jucha', 1, 1),
(655, 8, 70, 105, 4380, 380, 'Mune', 1, 1),
(655, 9, 30, 60, 820, 340, 'Mocnuf Notum Mine', 1, 1),
(665, 1, 80, 150, 940, 4820, 'Central Desert north', 2, 1),
(665, 2, 45, 75, 1260, 3860, 'Notum Disruption Mountain', 2, 1),
(665, 3, 75, 110, 1940, 3860, 'The Notum Plains', 2, 1),
(665, 4, 100, 150, 940, 3380, 'Near Clan Outpost', 2, 1),
(665, 5, 45, 80, 1300, 3060, 'Central Mountains', 2, 1),
(665, 6, 55, 150, 380, 2300, 'Surrounding Evil', 2, 1),
(665, 7, 45, 60, 1260, 2140, 'Notum Mountain', 2, 1),
(665, 8, 55, 100, 2020, 1980, 'Near Omni-Tek Outpost', 2, 1),
(665, 9, 100, 150, 420, 820, 'Shores Notum Vein', 2, 1),
(670, 1, 30, 45, 1100, 4340, 'Yukon Source', 2, 1),
(670, 2, 35, 50, 1460, 2540, 'Frisko', 2, 1),
(670, 3, 30, 45, 2140, 2420, 'Round Hills', 2, 1),
(670, 4, 50, 75, 2140, 1900, 'Dense Drewen', 2, 1),
(670, 5, 35, 50, 1260, 1820, 'Borrowed Hill', 2, 1),
(670, 6, 35, 50, 1340, 1340, 'Narrow Lune', 2, 1),
(670, 7, 10, 15, 2500, 1220, 'Micron Slopes Notum Mine', 2, 1),
(670, 8, 50, 75, 2100, 540, 'High Juniper', 2, 1),
(670, 9, 50, 75, 2300, 460, 'High Juniper Notum Vein', 2, 1),
(685, 1, 35, 50, 2140, 2620, 'Nature Reverve - East', 2, 1),
(685, 2, 35, 50, 1900, 2580, 'Nature Reverve - West', 2, 1),
(685, 3, 50, 75, 1300, 1900, 'Poole - West', 2, 1),
(685, 4, 50, 75, 1580, 1820, 'Poole - East', 2, 1),
(685, 5, 15, 25, 1140, 1100, 'V-Hill', 2, 1),
(685, 6, 20, 30, 1580, 700, 'Lunder Hills - North', 2, 1),
(685, 7, 25, 40, 2740, 460, 'Galway hills', 2, 1),
(685, 8, 20, 30, 1220, 380, 'Lunder Hills', 2, 1),
(685, 9, 25, 40, 2260, 380, 'South-east Woods', 2, 1),
(687, 1, 10, 15, 500, 1900, 'Blossom Valley', 2, 1),
(687, 2, 10, 15, 380, 1300, 'Konty Passage Plains', 2, 1),
(687, 3, 17, 28, 900, 1220, 'Vas\' Pass', 2, 1),
(687, 4, 15, 25, 780, 900, 'Arthur\'s Pass', 2, 1),
(687, 5, 10, 15, 380, 580, 'Kontys Sixth Passage - West', 2, 1),
(687, 6, 10, 15, 620, 540, 'Kontys Sixth Passage - East', 2, 1),
(695, 1, 30, 45, 940, 3260, 'North West Lush Fields', 0, 1),
(695, 2, 20, 30, 2420, 3180, 'North East Lush Fields', 0, 1),
(695, 3, 10, 40, 3460, 2940, 'Stret River Island', 0, 1),
(695, 4, 40, 60, 1260, 2460, 'West of Outpost', 0, 1),
(695, 5, 35, 60, 1740, 2460, 'East of Outpost', 0, 1),
(695, 6, 20, 30, 1780, 1820, 'Central Lush Fields', 0, 1),
(695, 7, 10, 15, 2860, 420, 'South East Lush Fields', 0, 1),
(695, 8, 30, 45, 980, 380, 'South West Lush Fields', 0, 1),
(696, 1, 15, 25, 780, 1420, 'Mutant Domain North', 2, 1),
(696, 2, 20, 30, 500, 860, 'Mutant Domain Central', 2, 1),
(696, 3, 25, 40, 780, 460, 'Mutant Domain South', 2, 1),
(716, 1, 20, 35, 500, 3220, 'Northern Grassland', 0, 1),
(716, 2, 15, 30, 980, 3020, 'Moderate Grassland', 0, 1),
(716, 3, 10, 20, 460, 2180, 'Dungeon Hilltop', 0, 1),
(716, 4, 10, 15, 700, 2180, 'Rocky Upsurge', 0, 1),
(716, 5, 15, 25, 340, 1420, 'Northern Easy Swamps Notum Field', 0, 1),
(716, 6, 15, 26, 460, 820, 'Ocean Inlet', 0, 1),
(717, 1, 30, 45, 1620, 2660, 'Greater Omni Forest Swamps', 1, 1),
(717, 2, 15, 25, 1180, 2460, 'Dragonback Ridge', 1, 1),
(717, 3, 30, 45, 1900, 1820, 'Mountainous Regions', 1, 1),
(717, 4, 20, 35, 1860, 1340, 'Waterfall Swamp', 1, 1),
(717, 5, 10, 15, 1500, 1300, 'Greater Omni Forest South', 1, 1),
(717, 6, 25, 40, 900, 1220, 'Northern Semi-Barren Area', 1, 1),
(717, 7, 10, 25, 1940, 900, 'Ring Mountain Range', 1, 1),
(717, 8, 14, 25, 940, 460, 'Southern Isle', 1, 1),
(760, 1, 60, 90, 1580, 2380, 'Notum Ore in Buttu', 2, 1),
(760, 2, 35, 50, 940, 2020, 'Mountain of Fourtyone', 2, 1),
(760, 3, 35, 50, 1300, 1980, 'Mountain in 4Holes', 2, 1),
(760, 4, 45, 60, 1660, 1740, 'South of Ahenus', 2, 1),
(760, 5, 70, 100, 1820, 1340, 'Ibreri Woods North', 2, 1),
(760, 6, 35, 50, 1460, 1260, 'Mountain of Fourtytwo', 2, 1),
(760, 7, 100, 150, 1740, 1060, 'Ibreri Woods', 2, 1),
(760, 8, 45, 70, 1180, 500, 'Ibreri', 2, 1),
(760, 9, 45, 70, 460, 420, 'Jall Mountain', 2, 1),
(790, 1, 20, 30, 1700, 3100, 'Hells Courtyard', 2, 1),
(790, 2, 15, 25, 2300, 2860, 'Pondus Beach', 2, 1),
(790, 3, 15, 30, 1700, 2780, 'Hound Land', 2, 1),
(790, 4, 20, 40, 1980, 2780, 'Hound Notum Field', 2, 1),
(790, 5, 20, 30, 1700, 1940, 'East Mutie', 2, 1),
(790, 6, 12, 30, 1340, 1220, 'Omni Outpost', 2, 1),
(790, 7, 20, 30, 660, 1180, 'South Mutie', 2, 1),
(790, 8, 30, 60, 2260, 1140, 'The Beach', 2, 1),
(791, 1, 15, 26, 420, 2020, 'Populous Mountain', 2, 1),
(791, 2, 12, 22, 660, 1500, 'Hound Land Mining', 2, 1),
(791, 3, 12, 20, 220, 1060, 'Stret West Notum Ore', 2, 1),
(791, 4, 10, 15, 740, 820, 'Snake Mountain', 2, 1),
(791, 5, 20, 40, 780, 460, 'Southern Empty Wastes and Roads', 2, 1),
(791, 6, 10, 20, 380, 340, 'Transit Valley Ore', 2, 1),
(795, 1, 40, 60, 4220, 1580, 'Illuminati', 1, 1),
(795, 2, 100, 150, 500, 1540, 'Northern Forest of Illuminations', 1, 0),
(795, 3, 25, 50, 3420, 1540, 'Fate Notum Field', 1, 1),
(795, 4, 71, 120, 580, 820, 'Pegrama', 1, 1),
(795, 5, 84, 120, 1220, 700, 'Grazeland Notum Field', 1, 1),
(795, 6, 50, 75, 4020, 620, 'Winterbottom', 1, 1),
(795, 7, 90, 120, 540, 500, 'Southern Forest of Illuminations', 1, 1),
(795, 8, 60, 90, 2900, 500, 'Summer', 1, 0);
@@ -0,0 +1,97 @@
import time
from core.decorators import instance, event
from core.igncore import IgnCore
from core.logger import Logger
from core.message_hub_service import MessageHubService
from core.text import Text, MLStripper
from core.util import Util
from modules.standard.helpbot.playfield_controller import PlayfieldController
from modules.standard.tower.tower_attack_controller import TowerAttackController
from modules.standard.tower.tower_controller import TowerController
from modules.standard.tower.tower_events import TowerEventController
@instance()
class TowerSpamController:
SOURCE = "TowerInfo"
def __init__(self):
self.logger = Logger(__name__)
def inject(self, registry):
self.bot: IgnCore = registry.get_instance("bot")
self.db = registry.get_instance("db")
self.text: Text = registry.get_instance("text")
self.util: Util = registry.get_instance("util")
self.tower: TowerEventController = registry.get_instance("tower_controller")
self.tc: TowerController = registry.get_instance("tower_controller")
self.tac: TowerAttackController = registry.get_instance("tower_attack_controller")
self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller")
self.msg_hub: MessageHubService = registry.get_instance("message_hub_service")
def pre_start(self):
self.msg_hub.register_message_source(self.SOURCE)
@event(event_type=TowerEventController.TOWER_ATTACK_EVENT, description="NW Warnings, for attacks")
def tower_attack_event(self, _, event_data):
t = int(time.time())
site_number = self.tac.find_closest_site_number(event_data.location.playfield.id, event_data.location.x_coord,
event_data.location.y_coord)
# self.logger.info("ATTK ")
# self.logger.info(event_data)
# self.logger.info(" site: ")
# [11.11.2021 04:35:58] INFO :: modules.standard.tower.tower_spam_controller -> ATTK |
# [11.11.2021 04:35:58] INFO :: modules.standard.tower.tower_spam_controller -> {'attacker': {'char_id': 984112, 'name': 'Flexiblex', 'first_name': '', 'last_name': '', 'leve|
# l': 117, 'breed': 'Opifex', 'gender': 'Female', 'faction': 'Omni', 'profession': 'Meta-Physicist', 'profession_title': 'ArchPriest', 'ai_rank': 'Adept', 'ai_level': 13, 'or|
# g_id': 9655, 'org_name': 'Northern Star', 'org_rank_name': 'Unit Leader', 'org_rank_id': 4, 'dimension': 5, 'head_id': 40240, 'pvp_rating': 1503, 'pvp_title': 'Rookie', 'so|
# urce': 'people.anarchy-online.com', 'last_updated': 1636601503, 'cache_age': 3855}, 'defender': {'faction': 'Omni', 'org_name': 'Weyland Yutani'}, 'location': {'playfield':|
# {'id': 795, 'long_name': 'The Longest Road', 'short_name': 'TLR', 'dungeon': 0}, 'x_coord': 526, 'y_coord': 538}} |
# [11.11.2021 04:35:58] INFO :: modules.standard.tower.tower_spam_controller -> site: |
# [11.11.2021 04:35:58] INFO :: modules.standard.tower.tower_spam_controller -> 7
# self.logger.info(site_number)
self.send_nw(
f"[<cyan>NW</cyan>] {self.text.get_formatted_faction(event_data.attacker.faction, event_data.attacker.org_name)} "
f"[{self.text.get_formatted_faction(event_data.attacker.faction, event_data.attacker.name)} ({event_data.attacker.level}/{event_data.attacker.ai_level})"
f" -> {self.util.get_profession(event_data.attacker.profession)}] "
f"attacked {self.text.get_formatted_faction(event_data.defender.faction, event_data.defender.org_name)} "
f"at {event_data.location.playfield.short_name} x{site_number}")
@event(event_type=TowerEventController.TOWER_VICTORY_EVENT, description="Send NW warnings")
def tower_victory_event(self, _, event_data):
t = int(time.time())
row = None
if event_data.type == "attack":
row = self.tac.get_last_attack(event_data.winner.faction, event_data.winner.org_name,
event_data.loser.faction,
event_data.loser.org_name, event_data.location.playfield.id, t)
# self.logger.info("ATTK_VICT ")
# self.logger.info(event_data)
# self.logger.info("ATTK_VICT ")
# self.logger.info(row)
self.send_nw(
f"[<cyan>NW</cyan>] {self.text.get_formatted_faction(event_data.winner.faction, event_data.winner.org_name)} "
f"won against {self.text.get_formatted_faction(event_data.loser.faction, event_data.loser.org_name)} at {event_data.location.playfield.short_name} x{row.site}")
elif event_data.type == "terminated":
row = self.tac.find_similar_attacks(event_data.loser.faction, event_data.loser.org_name,
event_data.location.playfield.id, t)
# VICTORY {'type': 'terminated', 'winner': {'faction': '', 'org_name': ''}, 'loser': {'faction': 'Omni', 'org_name': 'Do you smell the scared boxes'}, 'location': {'playfield
# ': {'id': 795, 'long_name': 'The Longest Road', 'short_name': 'TLR', 'dungeon': 0}}}
if row:
# self.logger.info("ATTK_TERM ")
# self.logger.info(event_data)
# self.logger.info("TERMINATED ")
# self.logger.info(row)
self.send_nw(f"[<cyan>NW</cyan>] Site terminated in {event_data.location.playfield.long_name}: "
f"{self.text.get_formatted_faction(event_data.loser.faction, event_data.loser.org_name)} at {event_data.location.playfield.short_name} x{row.site}")
else:
raise Exception("Unknown victory event type: '%s'" % event_data.type)
def send_nw(self, message):
stripper = MLStripper()
stripper.feed(message)
clean = stripper.get_data()
self.msg_hub.send_message(self.SOURCE, None, clean, message)
+5 -5
View File
@@ -9,6 +9,7 @@ from core.db import DB
from core.decorators import instance, command, event
from core.dict_object import DictObject
from core.event_service import EventService
from core.igncore import IgnCore
from core.job_scheduler import JobScheduler
from core.lookup.pork_service import PorkService
from core.message_hub_service import MessageHubService
@@ -16,10 +17,9 @@ from core.private_channel_service import PrivateChannelService
from core.setting_service import SettingService
from core.setting_types import TextSettingType, ColorSettingType
from core.text import Text
from core.igncore import IgnCore
from core.util import Util
from modules.core.accounting.services.account_service import AccountService
from modules.raidbot.tower.tower_controller import TowerController
from modules.standard.tower.tower_events import TowerEventController
# noinspection DuplicatedCode,SqlCaseVsIf
@@ -41,7 +41,7 @@ class TrackController(BaseModule):
self.account_service: AccountService = registry.get_instance("account_service")
self.db: DB = registry.get_instance("db")
self.priv: PrivateChannelService = registry.get_instance("private_channel_service")
self.tower: TowerController = registry.get_instance("tower_controller")
self.tower: TowerEventController = registry.get_instance("tower_controller")
self.message_hub_service: MessageHubService = registry.get_instance("message_hub_service")
def pre_start(self):
@@ -53,7 +53,7 @@ class TrackController(BaseModule):
"Color for Track logoff")
self.setting_service.register(self.module_name, "autotrack", 'none',
TextSettingType(['omni', 'clan', 'neutral', "none"]),
"Autotrack all players initiating tower attacks towards this faction:")
"Autotrack all players initiating tower attacks towards this faction")
self.db.exec(
"CREATE TABLE IF NOT EXISTS track("
"char_id int not null primary key, "
@@ -103,7 +103,7 @@ class TrackController(BaseModule):
color = self.setting_service.get("track_off_color").format_text("OFF")
self.send_t_warn(0, f'{color} :: {self.text.format_char_info(user)}')
@event(event_type=TowerController.TOWER_ATTACK_EVENT, description="Autottrack players attacking our faction")
@event(event_type=TowerEventController.TOWER_ATTACK_EVENT, description="Autottrack players attacking our faction")
def tower_attack_event(self, _, event_data):
attacker = event_data.attacker
if event_data.defender.faction.lower() == self.setting_service.get_value("autotrack"):
@@ -98,6 +98,7 @@ class WantsController:
LEFT JOIN account a ON w.char_id = a.char_id \
LEFT JOIN account a2 ON (a2.main = a.main) \
LEFT JOIN player p ON p.char_id = COALESCE(a2.char_id, w.char_id) \
WHERE a2.char_id = a2.main \
ORDER BY p.name"
data = self.db.query(sql)
@@ -116,7 +116,7 @@ class OrgListController:
for org in orgs:
if Registry.get_instance('org_alias_controller', is_optional=True):
blob += f'[{self.text.make_chatcmd("Add", f"/tell <myname> orgs add {org.org_id}")}]'
blob += f'[{self.text.make_chatcmd("More", f"/tell <myname> org info {org.org_id}")}]'
blob += f'[{self.text.make_chatcmd("More", f"/tell <myname> orgs info {org.org_id}")}]'
blob += f' <highlight>{org.org_name}<end> ' \
f'(<highlight>{org.org_id}<end>) <{org.faction.lower()}>{org.faction}<end> ' \
f'[<highlight>{org.member_count}<end> members]<br><pagebreak>'