Initial Release of IGNCore version 2.5

This commit is contained in:
2021-08-09 13:18:56 +02:00
commit a83d98c47e
910 changed files with 224171 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
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 command, instance
from core.lookup.character_service import CharacterService
from core.setting_service import SettingService
from core.text import Text
from core.tyrbot import Tyrbot
from core.util import Util
from modules.core.accounting.services.account_service import AccountService
@instance()
class PresetController:
def __init__(self):
pass
def inject(self, registry):
self.bot: Tyrbot = registry.get_instance("bot")
self.db: DB = registry.get_instance("db")
self.text: Text = registry.get_instance("text")
self.character_service: CharacterService = registry.get_instance("character_service")
self.util: Util = registry.get_instance("util")
self.setting_service: SettingService = registry.get_instance("setting_service")
self.command_alias_service: CommandAliasService = registry.get_instance("command_alias_service")
self.account_service: AccountService = registry.get_instance("account_service")
def start(self):
self.command_alias_service.add_alias("startauc", "auction start")
self.command_alias_service.add_alias("bid", "auction bid")
if self.db.query_single("SELECT COUNT(*) AS count FROM points_presets").count < 1:
# Populate with pre-made presets if empty
sql = "INSERT INTO points_presets (name, points) VALUES " \
"('Killed Sideboss', 1), " \
"('Killed TNH', 2), " \
"('Killed Beast', 3), " \
"('Alien Playfield', 3), " \
"('Wipe: Clan', 1), " \
"('Wipe: Omni', 1), " \
"('Bonus', 1), " \
"('Tara', 2), " \
"('S42/Boss', 2), " \
"('S42/Endboss', 4)"
self.db.exec(sql, [])
@command(command="presets", params=[Const("add"), Any("name"), Int("points")], access_level="superadmin",
description="Add new points preset", sub_command="mod")
def presets_add_cmd(self, _1, _2, name: str, points: int):
count = self.db.query_single("SELECT COUNT(*) AS count FROM points_presets WHERE name = ?", [name]).count
if count > 0:
return "A preset already exists with the name <highlight>%s<end>." % name
sql = "INSERT INTO points_presets (name, points) VALUES (?,?)"
if self.db.exec(sql, [name, points]) > 0:
return f"A preset with the name <highlight>{name}<end> was added, worth <green>{points:d}<end> points."
return "Failed to insert new preset in DB."
@command(command="presets", params=[Const("rem"), Int("preset_id")], access_level="superadmin",
description="Delete preset", sub_command="mod")
def presets_rem_cmd(self, _1, _2, preset_id: int):
if self.db.exec("DELETE FROM points_presets WHERE preset_id = ?", [preset_id]) > 0:
return "Successfully removed preset with ID <highlight>%d<end>." % preset_id
return "No preset with given ID <highlight>%d<end>." % preset_id
@command(command="presets",
params=[Const("alter"), Int("preset_id"), Int("new_points")],
access_level="superadmin",
description="Alter the points dished out by given preset", sub_command="mod")
def presets_alter_cmd(self, _1, _2, preset_id: int, new_points: int):
preset = self.db.query_single("SELECT * FROM points_presets WHERE preset_id = ?", [preset_id])
if preset:
if self.db.exec("UPDATE points_presets SET points = ? WHERE preset_id = ?", [new_points, preset_id]) > 0:
return f"Successfully updated the preset, <highlight>{preset.name}<end>, " \
f"to dish out <green>{new_points:d}<end> points instead of <red>{preset.points:d}<end>."
return "Failed to update preset with ID <highlight>%d<end>." % preset_id
@command(command="presets", params=[], access_level="admin",
description="See list of points presets")
def presets_cmd(self, _):
return ChatBlob("Points presets", self.build_preset_list)
@property
def build_preset_list(self):
presets = self.db.query("SELECT * FROM points_presets ORDER BY name, points DESC")
if presets:
blob = ""
for preset in presets:
add_points_link = self.text.make_chatcmd("Add pts", f"/tell <myname> raid addpts {preset.name}")
delete_link = self.text.make_chatcmd("Delete", f"/tell <myname> presets rem {preset.preset_id:d}")
blob += f"<highlight>{preset.name}<end> worth <green>{preset.points:d}<end> points " \
f"[id: {preset.preset_id:d}]\n | [{add_points_link}] [{delete_link}]\n\n"
return blob
return "No presets available. " \
"To add new presets use <highlight><symbol>presets add preset_name preset_points<end>."
+592
View File
@@ -0,0 +1,592 @@
import time
from typing import Union
from core.aochat import server_packets
from core.aochat.BaseModule import BaseModule
from core.buddy_service import BuddyService
from core.chat_blob import ChatBlob
from core.command_param_types import Const, Int, Any, Options, Character
from core.command_request import CommandRequest
from core.db import DB
from core.decorators import instance, command, timerevent, event
from core.event_service import EventService
from core.lookup.character_service import CharacterService
from core.lookup.pork_service import PorkService
from core.private_channel_service import PrivateChannelService
from core.sender_obj import SenderObj
from core.setting_service import SettingService
from core.text import Text
from core.tyrbot import Tyrbot
from core.util import Util
from modules.core.accounting.services.account_service import AccountService
from modules.raidbot.raid.preset_controller import PresetController
from modules.standard.raid.leader_controller import LeaderController
class Raider:
def __init__(self, alts, active):
self.main_id = alts[0].char_id
self.alts = alts
self.active_id = active
self.accumulated_points = 0
self.is_active = True
self.left_raid = None
self.was_kicked = None
self.was_kicked_reason = None
def get_active_char(self):
for alt in self.alts:
if self.active_id == alt.char_id:
return alt
return None
class Raid:
def __init__(self, raid_name, started_by, raiders=None):
self.raid_name = raid_name
self.desc = raid_name
self.started_at = int(time.time())
self.started_by = started_by
self.raiders = raiders or []
self.is_open = True
self.raid_orders = None
self.level = 180
@instance()
class RaidbotController(BaseModule):
NO_RAID_RUNNING_RESPONSE = "No raid is running."
def __init__(self):
self.raid = None
def inject(self, registry):
self.bot: Tyrbot = registry.get_instance("bot")
self.db: DB = registry.get_instance("db")
self.text: Text = registry.get_instance("text")
self.setting_service: SettingService = registry.get_instance("setting_service")
self.character_service: CharacterService = registry.get_instance("character_service")
self.preset_controller: PresetController = registry.get_instance("preset_controller")
self.util: Util = registry.get_instance("util")
self.event_service: EventService = registry.get_instance("event_service")
self.leader: LeaderController = registry.get_instance("leader_controller")
self.getresp = registry.get_instance("translation_service").get_response
self.pork: PorkService = registry.get_instance("pork_service")
self.private_channel_service: PrivateChannelService = registry.get_instance("private_channel_service")
self.account_service: AccountService = registry.get_instance("account_service")
self.buddy_service: BuddyService = registry.get_instance("buddy_service")
def pre_start(self):
self.event_service.register_event_type("RAID_END")
@event("connect", "Adds all raiders to buddylist")
def connect(self, _, _1):
query = self.db.query("SELECT char_id, member from account where member != -1 and disabled = 0")
if query:
for player in query:
self.buddy_service.add_buddy(player.char_id, "member")
@command(command="raid", params=[], access_level="member",
description="Show the current raid status")
def raid_cmd(self, _):
if not self.raid:
return self.NO_RAID_RUNNING_RESPONSE
t = int(time.time())
blob = ""
blob += f"Name: <highlight>{self.raid.desc}<end>\n"
blob += f"Minimum Level: <highlight>{self.raid.level}</highlight>\n"
blob += f"Started By: <highlight>{self.raid.started_by.name}<end>\n"
blob += f"Raidleader: " \
f"<highlight>" \
f"{self.leader.leader.name if self.leader.leader else '<red>not set - use !leader set<end>'}" \
f"<end>"
blob += f"Started At: <highlight>{self.util.format_datetime(self.raid.started_at)}<end> " \
f"({self.util.time_to_readable(t - self.raid.started_at)} ago)\n"
if self.raid.is_open:
blob += f"Status: <green>Open<end> [{self.text.make_chatcmd('Join', '/tell <myname> raid join')}] " \
f"<red>[{self.text.make_chatcmd('Lock', '/tell <myname> raid lock')}]<end>"
else:
blob += f"Status: <red>Closed<end> " \
f"<green>[{self.text.make_chatcmd('Unlock', '/tell <myname> raid open')}]<end>"
blob += "\n\n"
if self.raid.raid_orders:
blob += "<header2>Orders<end>\n"
blob += self.raid.raid_orders + "\n\n"
blob += "<header2>Raiders<end>\n"
for raider in self.raid.raiders:
if raider.is_active:
blob += self.format_row(raider.get_active_char())
return ChatBlob("Raid Status", blob)
def format_row(self, user):
return f" {self.util.get_prof_icon(user.profession)} " \
f"<{user.faction.lower()}>{user.name}</{user.faction.lower()}>" \
f" ({user.level}/<green>{user.ai_level}</green>) " \
f"[<{user.faction.lower()}>{user.org_name}</{user.faction.lower()}>|<cyan>{user.org_rank_name}</cyan>]\n"
@event(event_type="private_channel_left", description="Autokick inactive characters")
def left_channel(self, _1, event_data: server_packets.PrivateChannelClientLeft):
self.raid_kick_cmd(None, None, self.pork.get_character_info(event_data.char_id), "Left private channel")
@command(command="raid", params=[Const("start"), Any("raid_name")],
description="Start new raid", access_level="leader", sub_command="manage")
def raid_start_cmd(self, request: CommandRequest, _, raid_name: str):
if self.raid:
return "The <highlight>%s<end> raid is already running." % self.raid.raid_name
self.raid = Raid(raid_name, request.sender)
leader_alts = self.account_service.get_alts(request.sender.char_id)
self.raid.raiders.append(Raider(leader_alts, request.sender.char_id))
join_link = self.get_raid_join_blob("Click here")
msg = "\n" \
f"<highlight>────────[ Raid starting ]────────<end>\n" \
f"Initiator: <highlight>{request.sender.name}<end>\n" \
f"Raid Name: <highlight>{raid_name}<end>\n" \
f"{join_link} to join\n" \
f"<highlight>────────[ Raid starting ]────────<end>"
self.leader.leader = None
self.leader.set_raid_leader(request.sender, request.sender)
# self.bot.send_org_message(msg)
self.bot.send_private_channel_message(msg)
self.account_service.add_log(leader_alts[0].char_id, "raid",
f"[<notice>STARTED</notice>] Raid with desc: <highlight>{raid_name}</highlight>",
request.sender.char_id)
if request.channel == "msg":
return "You have been set as the <notice>active<end> Raidleader. " \
"Set another Raidleader by using <notice>!leader set &lt;name&gt;<end>"
else:
return f"{request.sender.name} has been set as the <notice>active<end> Raidleader automatically."
@command(command="raid",
params=[Const("desc"), Any("description")],
description="Change the raid description",
access_level="leader", sub_command="manage")
def raid_desc_cmd(self, _1, _2, desc):
if not self.raid:
return self.NO_RAID_RUNNING_RESPONSE
else:
self.raid.desc = desc
return f"Changed raid description to <highlight>{desc}</highlight> successfully."
@command(command="raid",
params=[Const("level"), Int("level")],
description="Change the minimum raider level",
access_level="leader",
sub_command="manage")
def raid_level_cmd(self, _1, _2, level):
if not self.raid:
return self.NO_RAID_RUNNING_RESPONSE
else:
self.raid.level = level
return f"Changed minimum raider level to <highlight>{level}</highlight> successfully."
@command(command="raid",
params=[Const("add"), Character("character_name")],
description="add an player to the raid",
access_level="leader", sub_command="manage")
def raid_add_cmd(self, request, _, char: SenderObj):
if not self.raid:
return self.NO_RAID_RUNNING_RESPONSE
if not char.char_id:
return self.getresp("global", "char_not_found", {"char": char.name})
if not self.private_channel_service.in_private_channel(char.char_id):
return f"The player <highlight>{char.name}</highlight> is not in my private channel. Could not add him."
main_id = self.account_service.get_main(char.char_id).char_id
in_raid = self.is_in_raid(main_id)
if in_raid is not None:
if in_raid.active_id == char.char_id:
if in_raid.is_active:
return f"The Character <highlight>{char.name}<end> is already participating in the Raid."
else:
in_raid.is_active = True
in_raid.was_kicked = None
in_raid.was_kicked_reason = None
in_raid.left_raid = None
self.account_service.add_log(main_id, "raid",
f"[<notice>ADD</notice>] Added to the raid by "
f"<highlight>{request.sender.name}</highlight>",
request.sender.char_id)
self.send_raid_msg("<green>READD</green>",
f"<highlight>{char.name}</highlight> by "
f"<highlight>{request.sender.name}</highlight>")
elif in_raid.is_active:
former_active_name = self.character_service.resolve_char_to_name(in_raid.active_id)
return f"<highlight>{char.name}<end> is already participating in the raid with the char " \
f"<highlight>{former_active_name}<end>"
else:
alts = self.account_service.get_alts(char.char_id)
self.raid.raiders.append(Raider(alts, char.char_id))
self.account_service.add_log(main_id, "raid", f"[<notice>ADD</notice>] Added to the raid by "
f"<highlight>{request.sender.name}</highlight>",
request.sender.char_id)
self.send_raid_msg("<green>ADD</green>", f"<highlight>{char.name}</highlight> by "
f"<highlight>{request.sender.name}</highlight>")
@command(command="raid", params=[Const("join")], description="Join the ongoing raid", access_level="member")
def raid_join_cmd(self, request, _):
if not self.raid:
return self.NO_RAID_RUNNING_RESPONSE
if not self.private_channel_service.in_private_channel(request.sender.char_id):
return "You are not in my private channel. you cant join the raid."
main_id = self.account_service.get_main(request.sender.char_id).char_id
in_raid = self.is_in_raid(main_id)
user = self.pork.get_character_info(request.sender.char_id)
if user.level < self.raid.level:
return f"You need to be at least level <highlight>{self.raid.level}</highlight> " \
f"to participate in this raid."
if in_raid is not None:
if in_raid.active_id == request.sender.char_id:
if in_raid.is_active:
return "You are already participating in the raid."
else:
if not self.raid.is_open:
return "Raid is closed."
in_raid.is_active = True
in_raid.was_kicked = None
in_raid.was_kicked_reason = None
in_raid.left_raid = None
self.account_service.add_log(main_id, "raid", f"[<notice>JOIN</notice>] Raid: "
f"<highlight>{self.raid.raid_name}</highlight>",
request.sender.char_id)
self.send_raid_msg("<green>REJOIN</green>", f"<green>{request.sender.name}</green>")
elif in_raid.is_active:
former_active_name = self.character_service.resolve_char_to_name(in_raid.active_id)
in_raid.active_id = request.sender.char_id
self.account_service.add_log(main_id, "raid", f"[<notice>JOIN</notice>] Raid: "
f"<highlight>{self.raid.raid_name}</highlight>",
request.sender.char_id)
self.send_raid_msg("<green>ALT</green>", f"<highlight>{request.sender.name}</highlight> "
f"[before: <highlight>{former_active_name}</highlight>]")
elif not in_raid.is_active:
if not self.raid.is_open:
return "Raid is closed."
former_active_name = self.character_service.resolve_char_to_name(in_raid.active_id)
in_raid.active_id = request.sender.char_id
in_raid.was_kicked = None
in_raid.was_kicked_reason = None
in_raid.left_raid = None
self.account_service.add_log(main_id, "raid",
f"[<notice>JOIN</notice>] Raid: "
f"<highlight>{self.raid.raid_name}</highlight>",
request.sender.char_id)
self.send_raid_msg("<green>ALT</green>", f"<highlight>{request.sender.name}</highlight> "
f"[before: <highlight>{former_active_name}</highlight>]")
elif self.raid.is_open:
alts = self.account_service.get_alts(request.sender.char_id)
self.raid.raiders.append(Raider(alts, request.sender.char_id))
self.account_service.add_log(main_id, "raid",
f"[<notice>JOIN</notice>] Raid: <highlight>{self.raid.raid_name}</highlight>",
request.sender.char_id)
self.send_raid_msg("<green>JOIN</green>", f"<green>{request.sender.name}</green>")
# self.bot.send_private_channel_message("<highlight>%s<end> joined the raid." % request.sender.name)
else:
return "Raid is closed."
@command(command="raid", params=[Const("leave")], description="Leave the ongoing raid", access_level="member")
def raid_leave_cmd(self, request, _):
main = self.account_service.get_main(request.sender.char_id).char_id
in_raid = self.is_in_raid(main)
if in_raid:
if not in_raid.is_active:
return "You are not active in the raid."
in_raid.is_active = False
in_raid.left_raid = int(time.time())
self.account_service.add_log(main, "raid",
f"[<notice>LEAVE</notice>] Raid: <highlight>{self.raid.raid_name}</highlight>",
request.sender.char_id)
self.send_raid_msg("<red>LEAVE</red>", f"<red>{request.sender.name}</red>")
# self.bot.send_private_channel_message("<highlight>%s<end> left the raid." % request.sender.name)
else:
return "You are not in the raid."
@command(command="raid",
params=[Const("addpts"), Any("name")],
description="Add points to all active participants",
access_level="leader",
sub_command="manage")
def points_add_cmd(self, request, _, name: str):
if not self.raid:
return self.NO_RAID_RUNNING_RESPONSE
preset = self.db.query_single("SELECT * FROM points_presets WHERE name = ?", [name])
if not preset:
return ChatBlob("No such preset - see list of presets", self.preset_controller.build_preset_list)
count = 0
for raider in self.raid.raiders:
current_points = self.account_service.get_account(raider.main_id)
if raider.is_active:
if current_points and current_points.disabled == 0:
self.account_service.add_pts(raider.main_id,
preset.points,
f"{preset.name}",
request.sender.char_id)
raider.accumulated_points += preset.points
count += 1
self.bot.send_private_channel_message(f"<highlight>{preset.points}</highlight> points added "
f"to all active raiders (<highlight>{count}</highlight>).")
@command(command="raid", params=[Const("check")], description="Get a list of raiders to do active check",
access_level="leader", sub_command="manage")
def raid_active_cmd(self, _1, _):
if not self.raid:
return self.NO_RAID_RUNNING_RESPONSE
blob = ""
raider_names = []
for raider in self.raid.raiders:
if not raider.is_active:
continue
raider_name = self.character_service.resolve_char_to_name(raider.active_id)
akick_link = self.text.make_chatcmd("Active kick", f"/tell <myname> raid kick {raider.main_id} inactive")
warn_link = self.text.make_chatcmd("Warn",
f"/tell <myname> cmd {raider_name} missed active check, "
f"please give notice.")
blob += "<highlight>%s<end> [%s] [%s]\n" % (raider_name, akick_link, warn_link)
raider_names.append(raider_name)
active_check_names = "/assist "
active_check_names += "\\n /assist ".join(raider_names)
blob += "[<a href='chatcmd://%s'>Active check</a>]\n\n" % active_check_names
raider_names.clear()
return ChatBlob("Active check", blob)
@command(command="raid", params=[Const("kick"), Character("char"), Any("reason")],
description="Set raider as kicked with a reason", access_level="leader", sub_command="manage")
def raid_kick_cmd(self, request, _2, char: SenderObj, reason: str):
if self.raid is None:
return self.NO_RAID_RUNNING_RESPONSE
if not char.char_id:
return self.getresp("global", "char_not_found", {"char": char.name})
main_id = self.account_service.get_main(char.char_id).char_id
in_raid = self.is_in_raid(main_id)
if in_raid is not None:
if not in_raid.is_active:
return "<highlight>%s<end> is not an active participant of the raid." % char.name
in_raid.is_active = False
in_raid.was_kicked = int(time.time())
in_raid.was_kicked_reason = reason
name = self.character_service.resolve_char_to_name(in_raid.active_id)
if request:
self.account_service.add_log(main_id, "raid", f"[<notice>KICKED</notice>] by "
f"<highlight>{request.sender.name}</highlight> "
f"for {reason}",
request.sender.char_id)
self.account_service.add_log(self.account_service.get_main(request.sender.char_id).char_id, "raid",
f"[<notice>KICKED</notice>] <highlight>{char.name}</highlight> "
f"for {reason}",
request.sender.char_id)
else:
self.account_service.add_log(main_id, "raid",
f"[<notice>KICKED</notice>] by "
f"<highlight>{self.bot.get_char_name()}</highlight> for {reason}",
self.bot.get_char_id())
self.send_raid_msg("KICKED", f"<highlight>{name}</highlight> for: <notice>{reason}</notice>")
else:
return "<highlight>%s<end> is not participating." % char.name
@command(command="raid",
params=[Options(["unlock", "open", "lock", "close"])],
description="Open/close raid for new participants",
access_level="leader",
sub_command="manage")
def raid_open_close_cmd(self, request, action):
if not self.raid:
return self.NO_RAID_RUNNING_RESPONSE
if action in ["unlock", "open"]:
if self.raid.is_open:
return "Raid is already open."
self.raid.is_open = True
self.send_raid_msg("<green>unlocked</green>", f"by <highlight>{request.sender.name}</highlight>")
return
elif action in ["lock", "close"]:
if self.raid.is_open:
self.raid.is_open = False
self.send_raid_msg("<red>locked</red>", f"by <highlight>{request.sender.name}</highlight>")
return
return "Raid is already closed."
@command(command="raid",
params=[Options(["save", "end", "stop"])],
description="Save and log running raid",
access_level="leader",
sub_command="manage")
def raid_save_cmd(self, _1, _2):
if not self.raid:
return self.NO_RAID_RUNNING_RESPONSE
sql = "INSERT INTO raid_log (raid_name, started_by, raid_start, raid_end) VALUES (?,?,?,?)"
num_rows = self.db.exec(sql, [self.raid.raid_name,
self.raid.started_by.char_id,
self.raid.started_at,
int(time.time())])
if num_rows > 0:
raid_id = self.db.query_single("SELECT raid_id FROM raid_log ORDER BY raid_id DESC LIMIT 1").raid_id
for raider in self.raid.raiders:
sql = "INSERT INTO raid_log_participants (" \
"raid_id, raider_id, accumulated_points, " \
"left_raid, was_kicked, was_kicked_reason" \
") " \
"VALUES (?,?,?,?,?,?)"
self.db.exec(sql, [raid_id, raider.active_id,
raider.accumulated_points,
raider.left_raid,
raider.was_kicked,
raider.was_kicked_reason])
self.raid = None
self.leader.set_raid_leader(self.leader.leader, None)
self.event_service.fire_event("RAID_ENDED")
return "Raid has ended, logs got saved. <notice>Raidleader cleared.<end>"
else:
return "Failed to end raid. Try again."
@command(command="raid", params=[Const("logentry"), Int("raid_id"), Character("char", is_optional=True)],
description="Show log entry for raid, with possibility of narrowing down the log for character in raid",
access_level="member")
def raid_log_entry_cmd(self, _1, _2, raid_id: int, char: SenderObj):
log_entry_spec = None
if char:
sql = "SELECT * FROM raid_log r " \
"LEFT JOIN raid_log_participants p ON r.raid_id = p.raid_id " \
"WHERE r.raid_id = ? AND p.raider_id = ?"
log_entry_spec = self.db.query_single(sql, [raid_id, char.char_id])
sql = "SELECT * FROM raid_log r " \
"LEFT JOIN raid_log_participants p ON r.raid_id = p.raid_id " \
"WHERE r.raid_id = ? ORDER BY p.accumulated_points DESC"
log_entry = self.db.query(sql, [raid_id])
pts_sum = self.db.query_single("SELECT SUM(p.accumulated_points) AS sum FROM raid_log_participants p "
"WHERE p.raid_id = ?", [raid_id]).sum
if not log_entry:
return "No such log entry."
blob = f"Raid name: <highlight>{log_entry[0].raid_name}<end>\n"
blob += f"Started by: <highlight>{self.character_service.resolve_char_to_name(log_entry[0].started_by)}<end>\n"
blob += f"Start time: <highlight>{self.util.format_datetime(log_entry[0].raid_start)}<end>\n"
blob += f"End time: <highlight>{self.util.format_datetime(log_entry[0].raid_end)}<end>\n"
blob += f"Run time: " \
f"<highlight>{self.util.time_to_readable(log_entry[0].raid_end - log_entry[0].raid_start)}<end>\n"
blob += f"Total points: <highlight>{pts_sum:d}<end>\n\n"
if char and log_entry_spec:
raider_name = self.character_service.resolve_char_to_name(log_entry_spec.raider_id)
main_info = self.account_service.get_main(log_entry_spec.raider_id)
alt_link = "Alt of %s" % main_info.name if main_info.char_id != log_entry_spec.raider_id else "Alts"
alt_link = self.text.make_chatcmd(alt_link, "/tell <myname> alts %s" % main_info.name)
blob += "<header2>Log entry for %s<end>\n" % raider_name
blob += "Raider: <highlight>%s<end> [%s]\n" % (raider_name, alt_link)
blob += "Left raid: %s\n" % ("n/a"
if log_entry_spec.left_raid is None
else self.util.format_datetime(log_entry_spec.left_raid))
blob += "Was kicked: %s\n" % ("No"
if log_entry_spec.was_kicked is None
else "Yes [%s]" % (self.util.format_datetime(log_entry_spec.was_kicked)))
blob += "Kick reason: %s\n\n" % ("n/a"
if log_entry_spec.was_kicked_reason is None
else log_entry_spec.was_kicked_reason)
blob += "<header2>Participants<end>\n"
for raider in log_entry:
raider_name = self.character_service.resolve_char_to_name(raider.raider_id)
main_info = self.account_service.get_main(raider.raider_id)
alt_link = "Alt of %s" % main_info.name if main_info.char_id != raider.raider_id else "Alts"
alt_link = self.text.make_chatcmd(alt_link, "/tell <myname> alts %s" % main_info.name)
log_link = self.text.make_chatcmd("Log", "/tell <myname> raid logentry %d %s" % (raid_id, raider_name))
account_link = self.text.make_chatcmd("Account", "/tell <myname> account %s" % raider_name)
blob += f"{raider_name} - {raider.accumulated_points:d} points earned " \
f"[{log_link}] [{account_link}] [{alt_link}]\n"
log_entry_reference = "the raid %s" % log_entry[0].raid_name \
if char is None \
else "%s in raid %s" \
% (self.character_service.resolve_char_to_name(char.char_id), log_entry[0].raid_name)
return ChatBlob("Log entry for %s" % log_entry_reference, blob)
@command(command="raid", params=[Const("history")], description="Show a list of recent raids",
access_level="member")
def raid_history_cmd(self, _1, _2):
sql = "SELECT * FROM raid_log ORDER BY raid_end DESC LIMIT 30"
raids = self.db.query(sql)
blob = ""
for raid in raids:
participant_link = self.text.make_chatcmd("Log", "/tell <myname> raid logentry %d" % raid.raid_id)
timestamp = self.util.format_datetime(raid.raid_start)
leader_name = self.character_service.resolve_char_to_name(raid.started_by)
blob += f"[{raid.raid_id:d}] [{timestamp}] <highlight>{raid.raid_name}<end> " \
f"started by <highlight>{leader_name}<end> [{participant_link}]\n"
return ChatBlob("Raid history", blob)
def is_in_raid(self, main_id: int) -> Union[bool, Raider]:
if self.raid is None:
return True
for raider in self.raid.raiders:
if raider.main_id == main_id:
return raider
def send_raid_msg(self, msg_type, msg):
self.bot.send_private_channel_message(f"[<notice>Raid: {msg_type}</notice>] {msg}")
def get_raid_join_blob(self, link_txt: str):
blob = "<header2>1. Join the raid<end>\n" \
"To join the current raid <highlight>%s<end>, send the following tell to <myname>\n" \
"<tab><tab><a href='chatcmd:///tell <myname> <symbol>raid join'>/tell <myname> raid join</a>\n" \
"\n" \
"<header2>2. Enable LFT<end>\n" \
"When you have joined the raid, go lft with \"<myname>\" as description\n" \
"<tab><tab><a href='chatcmd:///lft <font color=#FFFF00>» <myname><end>'>/lft <myname></a>\n" \
"\n" \
"<header2>3. Announce<end>\n" \
"You could announce to the raid leader, that you have enabled LFT\n" \
"<tab><tab><a href='chatcmd:///group <myname> I am on lft'>Announce</a> that you have enabled lft\n" \
"\n" \
"<header2>4. Rally with yer mateys<end>\n" \
"Finally, move towards the starting location of the raid.\n" \
"<highlight>Ask for help<end> if you're in doubt of where to go." % self.raid.raid_name
return self.text.paginate_single(ChatBlob(link_txt, blob))
@timerevent(budatime="5m", description="announce raid")
def announce_raid(self, event_type, event_data):
if self.raid:
if self.raid.is_open:
self.bot.send_private_channel_message("<highlight>Raid Running<end> "
"<yellow>::<end> <green>%s<end> <yellow>::<end> %s" %
(self.raid.desc,
f"<a href=\"text://{self.raid.raid_name}\">"
f"{self.get_raid_join_blob('Click to Join')}"
f"</a>"))
else:
self.bot.send_private_channel_message("<highlight>Raid Running<end> <yellow>::<end> <green>%s<end> "
"<yellow>::<end> <red>Closed<end>" % self.raid.desc)
@timerevent(budatime="1m", description="No leader reminder")
def leader_auto_remove(self, _1, _2):
if self.raid:
leader = self.leader.leader
if not leader:
self.bot.send_private_channel_message(f"<red>::</red> No Leader set <red>::</red> "
f"use <highlight>!leader set</highlight>")
return
if not self.is_in_raid(leader.char_id):
self.bot.send_private_channel_message(f"<red>::</red> Raidleader <highlight>{leader.name}</highlight> "
f"is not in raid <red>::</red>")
@@ -0,0 +1,124 @@
from core.aochat.BaseModule import BaseModule
from core.chat_blob import ChatBlob
from core.command_param_types import Any
from core.db import DB
from core.decorators import instance, command
from core.lookup.character_service import CharacterService
from core.lookup.pork_service import PorkService
from core.private_channel_service import PrivateChannelService
from core.setting_service import SettingService
from core.text import Text
from core.tyrbot import Tyrbot
from core.util import Util
from modules.core.accounting.services.account_service import AccountService
@instance()
class MassinviteController(BaseModule):
NO_RAID_RUNNING_RESPONSE = "No raid is running."
def __init__(self):
self.count = 0
def inject(self, registry):
self.bot: Tyrbot = registry.get_instance("bot")
self.db: DB = registry.get_instance("db")
self.text: Text = registry.get_instance("text")
self.setting_service: SettingService = registry.get_instance("setting_service")
self.character_service: CharacterService = registry.get_instance("character_service")
self.util: Util = registry.get_instance("util")
self.pork: PorkService = registry.get_instance("pork_service")
self.private_channel_service: PrivateChannelService = registry.get_instance("private_channel_service")
self.account_service: AccountService = registry.get_instance("account_service")
@command(command="massinvite",
params=[Any("text")],
description="Send a Massmessage combined with a massinvite",
access_level="leader")
def mass_invite(self, request, msg):
if not self.raid:
return self.NO_RAID_RUNNING_RESPONSE
main = ""
blob = ""
spam = [self.format_subtile_spam(
f"<highlight>{request.sender.name}</highlight> invited you to join a raid: <yellow>{msg}</yellow>"),
self.format_spam(f"<highlight>{request.sender.name}</highlight> invited you to join a raid:\n"
f"<red>{msg}</red>")]
self.bot.send_private_channel_message(spam[1])
invites = self.account_service.get_all_members(online_only=True)
request.reply(f"Attempting to send {len(invites)} Raid invites....")
for player in invites:
if player.main != main:
main = player.main
blob += f"\n<notice>{self.character_service.resolve_char_to_name(main)}<notice> as \n"
blob += self.handle_player(player, spam, True)
msg = ChatBlob("Info", blob)
msg.page_prefix = f"{self.count} Massinvites sent! ["
msg.page_postfix = "]"
self.count = 0
if request.channel == "msg":
self.bot.send_mass_message(request.sender.char_id, msg)
else:
return msg
@command(command="massmessage", params=[Any("text")], description="Send a Massmessage to all online players",
access_level="leader")
def mass_message(self, request, msg):
# if not self.raid:
# return self.NO_RAID_RUNNING_RESPONSE
main = ""
blob = ""
spam = [self.format_subtile_spam(
f"<highlight>{request.sender.name}</highlight> sent you a message: <yellow>{msg}</yellow>"),
self.format_spam(f"<highlight>{request.sender.name}</highlight> sent you a message:\n"
f"<red>{msg}</red>")]
self.bot.send_private_channel_message(spam[1])
invites = self.account_service.get_all_members(online_only=True)
request.reply(f"Attempting to send {len(invites)} Massmessage's...")
for player in invites:
if player.main != main:
main = player.main
blob += f"\n<notice>{self.character_service.resolve_char_to_name(main)}<notice> as \n"
blob += self.handle_player(player, spam, False)
msg = ChatBlob("Info", blob)
msg.page_prefix = f"{self.count} Massmessage's have been sent! ["
msg.page_postfix = "]"
self.count = 0
if request.channel == "msg":
self.bot.send_mass_message(request.sender.char_id, msg)
else:
return msg
def handle_player(self, user, msg, invite=False):
pg = " [<green>PG</green>]" if self.private_channel_service.in_private_channel(
user.char_id) else " [<red>MSG sent</red>]"
if not self.private_channel_service.in_private_channel(user.char_id):
count = 0
if user.raid_spam == 1:
if user.subtile_spam == 1:
self.bot.send_mass_message(user.char_id, msg[0])
else:
self.bot.send_mass_message(user.char_id, msg[1])
count += 1
if user.raid_invite == 1 and invite:
self.private_channel_service.invite(user.char_id)
count += 1
if count == 0:
return ""
self.count += 1
return f"<tab>{self.util.get_prof_icon(user.profession)} " \
f"({self.text.zfill(user.level, 220)}/<green>{self.text.zfill(user.ai_level, 30)}</green>) " \
f"<{user.faction.lower()}>{user.name}</{user.faction.lower()}> " \
f"[<{user.faction.lower()}>{user.org_name}</{user.faction.lower()}> - " \
f"<highlight>{user.org_rank_id + 1}</highlight>]{pg}\n"
def format_spam(self, msg):
return f"\n" \
f"<yellow>────────[ Raid invite ]────────</yellow>\n" \
f"{msg}\n" \
f"<yellow>────────[ Raid invite ]────────</yellow>"
def format_subtile_spam(self, msg):
return f"[ Raid invite ] {msg}"
@@ -0,0 +1,81 @@
import math
from core.aochat.BaseModule import BaseModule
from core.chat_blob import ChatBlob
from core.command_alias_service import CommandAliasService
from core.command_param_types import Int, NamedParameters
from core.db import DB
from core.decorators import instance, command
from core.event_service import EventService
from core.lookup.pork_service import PorkService
from core.public_channel_service import PublicChannelService
from core.text import Text
from core.tyrbot import Tyrbot
from core.util import Util
from modules.raidbot.tower.tower_service import TowerService
from modules.standard.helpbot.playfield_controller import PlayfieldController
# noinspection DuplicatedCode
@instance()
class ContractController(BaseModule):
PAGE_SIZE = 40
# noinspection DuplicatedCode
def inject(self, registry):
self.bot: Tyrbot = registry.get_instance("bot")
self.db: DB = registry.get_instance("db")
self.util: Util = registry.get_instance("util")
self.text: Text = registry.get_instance("text")
self.event_service: EventService = registry.get_instance("event_service")
self.pork_service: PorkService = registry.get_instance("pork_service")
self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller")
self.public_channel_service: PublicChannelService = registry.get_instance("public_channel_service")
self.towercache: TowerService = registry.get_instance("tower_service")
self.command_alias_service: CommandAliasService = registry.get_instance("command_alias_service")
@command(command="contracts",
params=[Int('mininum', is_optional=True), NamedParameters(['page'])],
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 CAST(SUM(ql)*2 AS INTEGER) AS contracts, "
"COUNT(*) as sites, org_name, org_id, faction FROM towers "
"where org_name IS NOT NULL GROUP BY org_name ORDER BY contracts desc", [])
page = int(named_params.page or "1")
offset = (page - 1) * self.PAGE_SIZE
data = [x for x in data if 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("«« Page %d" % (page - 1), f'{cmd} --page={page - 1}')
if offset + self.PAGE_SIZE < len(data):
pages += f" Page {page}/{math.ceil(len(data) / self.PAGE_SIZE)}"
pages += " " + self.text.make_tellcmd("Page %d »»" % (page + 1), f'{cmd} --page={page + 1}')
pages += "\n"
if count == 0:
return nullmsg
else:
blob = "<font color=CCInfoText>"
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"
+76
View File
@@ -0,0 +1,76 @@
import time
from core.aochat.BaseModule import BaseModule
from core.chat_blob import ChatBlob
from core.command_alias_service import CommandAliasService
from core.db import DB
from core.decorators import instance, command
from core.event_service import EventService
from core.lookup.pork_service import PorkService
from core.public_channel_service import PublicChannelService
from core.text import Text
from core.tyrbot import Tyrbot
from core.util import Util
from modules.raidbot.tower.tower_service import TowerService
from modules.standard.helpbot.playfield_controller import PlayfieldController
@instance()
class PlantController(BaseModule):
# noinspection DuplicatedCode
def inject(self, registry):
self.bot: Tyrbot = registry.get_instance("bot")
self.db: DB = registry.get_instance("db")
self.util: Util = registry.get_instance("util")
self.text: Text = registry.get_instance("text")
self.event_service: EventService = registry.get_instance("event_service")
self.pork_service: PorkService = registry.get_instance("pork_service")
self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller")
self.public_channel_service: PublicChannelService = registry.get_instance("public_channel_service")
self.towercache: TowerService = registry.get_instance("tower_service")
self.command_alias_service: CommandAliasService = registry.get_instance("command_alias_service")
@command(command="penalty", params=[], access_level="member", description="Shows planttimers")
def penalty(self, _):
blob = "<font color=CCInfoText>"
self.towercache.attack_hot.sort(key=lambda k: k['org_name'])
rem = []
for key, value in enumerate(sorted(self.towercache.attack_hot, key=lambda k: k['org_name'])):
if value["hot"] < time.time():
rem.append(key)
continue
lca = self.towercache.get_towers_by_org_name(value["org_name"])
for site in lca:
blob += self.towercache.format_entry(site, 10)
blob += "</font>"
for i in reversed(rem):
self.towercache.attack_hot.pop(i)
if len(self.towercache.attack_hot) == 0:
blob = ""
return ChatBlob(f"Warhot tower sites", blob.strip("\n")) if blob != "" else f"There are no orgs in penalty."
@command(command="plant", params=[], access_level="member", description="Shows planttimers")
def plant(self, _):
blob = "<font color=CCInfoText>"
rem = []
for key, value in enumerate(sorted(self.towercache.plant, key=lambda k: k['pf'])):
print(key, value)
if value["plant"] < time.time():
rem.append(key)
continue
lca = self.playfield_controller.get_playfield_by_id(value['pf'])
blob += f"[<cyan>{lca.short_name}</cyan>] <cyan>x{value['site']}</cyan> in " \
f"<white>{self.util.format_time(value['plant'] - time.time())}</white>\n"
blob += "</font>"
for i in reversed(rem):
self.towercache.plant.pop(i)
if len(self.towercache.plant) == 0:
blob = ""
if blob != "":
return ChatBlob(f"Awaiting plants ({len(self.towercache.plant)})", blob.strip("\n"))
else:
return f"There are no sites awaiting plants."
@@ -0,0 +1,358 @@
import time
from core.chat_blob import ChatBlob
from core.command_param_types import Const, Int, NamedParameters
from core.decorators import instance, command, event
from core.job_scheduler import JobScheduler
from core.logger import Logger
from core.public_channel_service import PublicChannelService
from core.setting_service import SettingService
from core.text import Text
from core.tyrbot import Tyrbot
from modules.core.accounting.services.account_service import AccountService
from modules.raidbot.tower.tower_controller import TowerController
from modules.raidbot.tower.tower_service import TowerService
from modules.standard.helpbot.playfield_controller import PlayfieldController
@instance()
class TowerAttackController:
def __init__(self):
self.logger = Logger(__name__)
def inject(self, registry):
self.bot: Tyrbot = registry.get_instance("bot")
self.db = registry.get_instance("db")
self.text: Text = registry.get_instance("text")
self.settings: SettingService = registry.get_instance("setting_service")
self.util = registry.get_instance("util")
self.tower: TowerController = registry.get_instance("tower_controller")
self.towerservice: TowerService = registry.get_instance("tower_service")
self.event_service = registry.get_instance("event_service")
self.command_alias_service = registry.get_instance("command_alias_service")
self.public_channel_service: PublicChannelService = registry.get_instance("public_channel_service")
self.job_scheduler: JobScheduler = registry.get_instance("job_scheduler")
self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller")
self.account_service: AccountService = registry.get_instance("account_service")
def start(self):
self.db.exec(
"CREATE TABLE IF NOT EXISTS tower_attacker ("
"id INT PRIMARY KEY AUTO_INCREMENT, "
"att_org_name VARCHAR(50) NOT NULL, "
"att_faction VARCHAR(10) NOT NULL, "
"att_char_id INT, att_char_name VARCHAR(20) NOT NULL, "
"att_level INT NOT NULL, "
"att_ai_level INT NOT NULL, "
"att_profession VARCHAR(15) NOT NULL, "
"x_coord INT NOT NULL, "
"y_coord INT NOT NULL, "
"is_victory SMALLINT NOT NULL, "
"tower_battle_id INT NOT NULL, "
"created_at INT NOT NULL)")
self.db.exec(
"CREATE TABLE IF NOT EXISTS tower_battle ("
"id INT PRIMARY KEY AUTO_INCREMENT, "
"playfield_id INT NOT NULL, "
"site_number INT NOT NULL, "
"def_org_name VARCHAR(50) NOT NULL, "
"def_faction VARCHAR(10) NOT NULL, "
"is_finished INT NOT NULL, "
"battle_type VARCHAR(20) NOT NULL, "
"last_updated INT NOT NULL)")
self.command_alias_service.add_alias("victory", "attacks")
@command(command="attacks", params=[NamedParameters(["page"])], access_level="member",
description="Show recent tower attacks and victories")
def attacks_cmd(self, _, named_params):
page = int(named_params.page or "1")
page_size = 30
offset = (page - 1) * page_size
sql = """
SELECT
b.*,
a.*,
COALESCE(a.att_level, 0) AS att_level,
COALESCE(a.att_ai_level, 0) AS att_ai_level,
p.short_name,
b.id AS battle_id
FROM
tower_battle b
LEFT JOIN tower_attacker a ON
a.tower_battle_id = b.id
LEFT JOIN playfields p ON
p.id = b.playfield_id
ORDER BY
b.last_updated DESC,
a.created_at DESC
LIMIT %d, %d
""" % (offset, page_size)
data = self.db.query(sql)
t = int(time.time())
blob = self.check_for_all_towers_channel()
if page > 1:
blob += " " + self.text.make_chatcmd("<< Page %d" % (page - 1), self.get_chat_command(page - 1))
if len(data) > 0:
blob += " Page " + str(page)
blob += " " + self.text.make_chatcmd("Page %d >>" % (page + 1), self.get_chat_command(page + 1))
blob += "\n"
current_battle_id = -1
for row in data:
if current_battle_id != row.battle_id:
blob += "\n<pagebreak>"
current_battle_id = row.battle_id
blob += self.format_battle_info(row, t)
blob += self.text.make_tellcmd("More Info", "attacks battle %d" % row.battle_id) + "\n"
blob += "<header2>Attackers:</header2>\n"
blob += "<tab>" + self.format_attacker(row) + "\n"
return ChatBlob("Tower Attacks", blob)
@command(command="attacks", params=[Const("battle"), Int("battle_id")], access_level="member",
description="Show battle info for a specific battle")
def attacks_battle_cmd(self, _, _1, battle_id):
battle = self.db.query_single(
"SELECT b.*, p.short_name FROM tower_battle b "
"LEFT JOIN playfields p ON p.id = b.playfield_id WHERE b.id = ?",
[battle_id])
if not battle:
return "Could not find battle with ID <highlight>%d</highlight>." % battle_id
t = int(time.time())
attackers = self.db.query("SELECT * FROM tower_attacker WHERE tower_battle_id = ? ORDER BY created_at DESC",
[battle_id])
first_activity = attackers[-1].created_at if len(attackers) > 0 else battle.last_updated
blob = self.check_for_all_towers_channel()
blob += self.format_battle_info(battle, t)
blob += f"Duration: <highlight>" \
f"{self.util.time_to_readable(battle.last_updated - first_activity)}</highlight>\n\n"
blob += "<header2>Attackers:</header2>\n"
for row in attackers:
blob += "<tab>" + self.format_attacker(row)
blob += " " + self.format_timestamp(row.created_at, t)
blob += "\n"
return ChatBlob("Battle Info %d" % battle_id, blob)
@event(event_type=TowerController.TOWER_ATTACK_EVENT, description="Warn on Tower attacks", is_hidden=True)
def tower_attack_event(self, _, event_data):
t = int(time.time())
site_number = self.find_closest_site_number(event_data.location.playfield.id, event_data.location.x_coord,
event_data.location.y_coord)
attacker = event_data.attacker or {}
defender = event_data.defender
battle = self.find_or_create_battle(event_data.location.playfield.id, site_number, defender.org_name,
defender.faction, "attack", t)
# print(battle)
self.db.exec(
"INSERT INTO tower_attacker (att_org_name, att_faction, att_char_id, att_char_name, "
"att_level, att_ai_level, att_profession, "
"x_coord, y_coord, is_victory, tower_battle_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
[attacker.get("org_name", ""), attacker.get("faction", ""), attacker.get("char_id", 0),
attacker.get("name", ""), attacker.get("level", 0),
attacker.get("ai_level", 0), attacker.get("profession", ""), event_data.location.x_coord,
event_data.location.y_coord, 0, battle.id, t])
@event(event_type=TowerController.TOWER_ATTACK_EVENT, description="Notify Tower attacks on own org")
def tower_def_event(self, _, event_data):
if event_data.attacker.get("name", None) is not None:
field_id = self.find_closest_site_number(event_data.location.playfield.id, event_data.location.x_coord,
event_data.location.y_coord)
row = self.db.query_single(
"SELECT t.*, p.short_name, p.long_name FROM tower_site t "
"JOIN playfields p ON t.playfield_id = p.id WHERE t.playfield_id = ? AND site_number = ?",
[event_data.location.playfield.id, field_id])
lca = self.text.format_page("%s - %d" % (event_data.location.playfield.long_name, field_id),
self.tower.format_site_info(row))
attacker = self.text.format_char_info(event_data.attacker)
add = ""
if account := self.account_service.get_account(event_data.attacker.char_id):
if account.disabled == 0 and account.member != 0:
add = " :: <red>He's a <myname> Raider!</red>"
self.bot.send_private_channel_message(
f"[<cyan>NW</cyan>] "
f"<{event_data.defender.faction.lower()}>"
f"{event_data.defender.org_name}"
f"</{event_data.defender.faction.lower()}> "
f"attacked by {attacker} in {lca}{add}")
@event(event_type=TowerController.TOWER_VICTORY_EVENT, description="Record tower victories", is_hidden=True)
def tower_victory_event(self, _, event_data):
t = int(time.time())
if event_data.type == "attack":
row = self.get_last_attack(event_data.winner.faction, event_data.winner.org_name, event_data.loser.faction,
event_data.loser.org_name, event_data.location.playfield.id, t)
if not row:
site_number = 0
is_finished = 1
self.db.exec(
"INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, "
"is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)",
[event_data.location.playfield.id, site_number, event_data.loser.org_name, event_data.loser.faction,
is_finished, event_data.type, t])
battle_id = self.db.last_insert_id()
attacker = event_data.winner or {}
self.db.exec(
"INSERT INTO tower_attacker (att_org_name, att_faction, att_char_id, "
"att_char_name, att_level, att_ai_level, att_profession, "
"x_coord, y_coord, is_victory, tower_battle_id, created_at) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
[attacker.get("org_name", ""), attacker.get("faction", ""), attacker.get("char_id", 0),
attacker.get("name", ""), attacker.get("level", 0),
attacker.get("ai_level", 0), attacker.get("profession", ""), 0, 0, 0, battle_id, t])
else:
is_victory = 1
self.db.exec("UPDATE tower_attacker SET is_victory = ? WHERE id = ?", [is_victory, row.attack_id])
is_finished = 1
self.db.exec("UPDATE tower_battle SET is_finished = ?, last_updated = ? WHERE id = ?",
[is_finished, t, row.battle_id])
elif event_data.type == "terminated":
site_number = 0
is_finished = 1
self.db.exec(
"INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, "
"is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)",
[event_data.location.playfield.id, site_number, event_data.loser.org_name, event_data.loser.faction,
is_finished, event_data.type, t])
else:
raise Exception("Unknown victory event type: '%s'" % event_data.type)
def format_attacker(self, row):
level = ("%d/<green>%d</green>" % (
row.att_level, row.att_ai_level)) if row.att_ai_level > 0 else "%d" % row.att_level
org = row.att_org_name + " " if row.att_org_name else ""
victor = " - <notice>Winner!</notice>" if row.is_victory else ""
return "%s (%s %s) %s(%s)%s" % (
row.att_char_name or "Unknown attacker", level, row.att_profession, org, row.att_faction, victor)
def find_closest_site_number(self, playfield_id, x_coord, y_coord):
# noinspection SqlUnused
sql = """
SELECT
site_number,
((x_distance * x_distance) + (y_distance * y_distance)) radius
FROM
(SELECT
playfield_id,
site_number,
min_ql,
max_ql,
x_coord,
y_coord,
site_name,
(x_coord - ?) as x_distance,
(y_coord - ?) as y_distance
FROM
tower_site
WHERE
playfield_id = ?) t
ORDER BY
radius
LIMIT 1"""
row = self.db.query_single(sql, [x_coord, y_coord, playfield_id])
if row:
return row.site_number
else:
return 0
def find_or_create_battle(self, playfield_id, site_number, org_name, faction, battle_type, t):
last_updated = t - (8 * 3600)
is_finished = 0
sql = """
SELECT
*
FROM
tower_battle
WHERE
playfield_id = ?
AND site_number = ?
AND is_finished = ?
AND def_org_name = ?
AND def_faction = ?
AND last_updated >= ?
"""
battle = self.db.query_single(sql, [playfield_id, site_number, is_finished, org_name, faction, last_updated])
if battle:
self.db.exec("UPDATE tower_battle SET last_updated = ? WHERE id = ?", [t, battle.id])
return battle
else:
self.db.exec(
"INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, "
"is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)",
[playfield_id, site_number, org_name, faction, is_finished, battle_type, t])
time.sleep(0.5)
return self.db.query_single("SELECT * FROM tower_battle WHERE id = ?", [self.db.last_insert_id()])
def get_last_attack(self, att_faction, att_org_name, def_faction, def_org_name, playfield_id, t):
last_updated = t - (8 * 3600)
is_finished = 0
sql = """
SELECT
b.id AS battle_id,
a.id AS attack_id,
b.playfield_id as pf_id,
b.site_number as site
FROM
tower_battle b
JOIN tower_attacker a ON
a.tower_battle_id = b.id
WHERE
a.att_faction = ?
AND a.att_org_name = ?
AND b.def_faction = ?
AND b.def_org_name = ?
AND b.playfield_id = ?
AND b.is_finished = ?
AND b.last_updated >= ?
ORDER BY
last_updated DESC
LIMIT 1"""
return self.db.query_single(sql,
[att_faction, att_org_name, def_faction, def_org_name, playfield_id, is_finished,
last_updated])
def format_battle_info(self, row, t):
blob = ""
defeated = " - <notice>Defeated!</notice>" if row.is_finished else ""
blob += "Site: <highlight>%s %s</highlight>\n" % (row.short_name, row.site_number or "?")
blob += "Defender: <highlight>%s</highlight> (%s)%s\n" % (row.def_org_name, row.def_faction, defeated)
blob += "Last Activity: %s\n" % self.format_timestamp(row.last_updated, t)
return blob
def format_timestamp(self, t, current_t):
return "<highlight>%s</highlight> (%s ago)" % (
self.util.format_datetime(t), self.util.time_to_readable(current_t - t))
def get_chat_command(self, page):
return "/tell <myname> attacks --page=%d" % page
def check_for_all_towers_channel(self):
if not self.public_channel_service.get_channel_name(TowerController.ALL_TOWERS_ID):
return "Notice: The bot must belong to an org and be promoted to a rank that is high enough " \
"to have the All Towers channel (e.g., Squad Commander) in order for the " \
"<symbol>attacks command to work correctly.\n\n"
else:
return ""
+207
View File
@@ -0,0 +1,207 @@
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.logger import Logger
from core.lookup.pork_service import PorkService
from core.public_channel_service import PublicChannelService
from core.text import Text
from core.tyrbot import Tyrbot
from modules.standard.helpbot.playfield_controller import PlayfieldController
@instance()
class TowerController:
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: Tyrbot = 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_site.sql", pre_optimized=True)
self.db.create_view("tower_site")
@event(event_type="connect", description="Check if All Towers channel is available", is_hidden=True)
def handle_connect_event(self, _, _1):
if self.public_channel_service.org_id and not self.public_channel_service.get_channel_id("All Towers"):
self.logger.warning("This bot is a member of an org but does not have access to 'All Towers' channel and "
"therefore will not receive tower attack messages")
def format_site_info(self, row):
blob = f"Short name: <highlight>{row.short_name} {row.site_number:d}</highlight>\n"
blob += f"Long name: <highlight>{row.site_name}, {row.long_name}</highlight>\n"
blob += f"Level range: <highlight>{row.min_ql:d}-{row.max_ql:d}</highlight>\n"
blob += "Coordinates: %s\n" % self.text.make_chatcmd(f"{row.x_coord:d}x{row.y_coord:d}",
f"/waypoint {row.x_coord:d} "
f"{row.y_coord:d} "
f"{row.playfield_id:d}")
return blob
def handle_public_channel_message(self, conn: Conn, packet: server_packets.PublicChannelMessage):
if conn.id != "main":
return
if packet.channel_id == self.TOWER_BATTLE_OUTCOME_ID:
victory = self.get_victory_event(packet)
if victory:
# self.logger.debug("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)
self.event_service.fire_event(self.TOWER_VICTORY_EVENT, victory)
elif packet.channel_id == self.ALL_TOWERS_ID:
attack = self.get_attack_event(packet)
if attack:
# self.logger.debug("tower attack packet: %s" % str(packet))
# 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
self.event_service.fire_event(self.TOWER_ATTACK_EVENT, attack)
def get_attack_event(self, packet: server_packets.PublicChannelMessage):
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:
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):
match = self.VICTORY_1.match(packet.message)
if match:
return None
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
@@ -0,0 +1,204 @@
from core.aochat.BaseModule import BaseModule
from core.chat_blob import ChatBlob
from core.command_alias_service import CommandAliasService
from core.command_param_types import Options, Int, Any, Const, NamedParameters
from core.db import DB
from core.decorators import instance, command
from core.dict_object import DictObject
from core.event_service import EventService
from core.lookup.pork_service import PorkService
from core.public_channel_service import PublicChannelService
from core.text import Text
from core.tyrbot import Tyrbot
from core.util import Util
from modules.raidbot.tower.tower_service import TowerService
from modules.standard.helpbot.playfield_controller import PlayfieldController
@instance()
class TowerHotController(BaseModule):
PAGE_SIZE = 9
# noinspection DuplicatedCode
def inject(self, registry):
self.bot: Tyrbot = registry.get_instance("bot")
self.db: DB = registry.get_instance("db")
self.util: Util = registry.get_instance("util")
self.text: Text = registry.get_instance("text")
self.event_service: EventService = registry.get_instance("event_service")
self.pork_service: PorkService = registry.get_instance("pork_service")
self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller")
self.public_channel_service: PublicChannelService = registry.get_instance("public_channel_service")
self.towercache: TowerService = registry.get_instance("tower_service")
self.command_alias_service: CommandAliasService = registry.get_instance("command_alias_service")
def pre_start(self):
self.command_alias_service.add_alias('towers', "lc")
self.command_alias_service.add_alias('tower', "lc")
@command(command="hot",
params=[Options(['tl1', 'tl2', 'tl3', 'tl4', 'tl5', 'tl6', 'tl7']), Any('faction', is_optional=True),
NamedParameters(["page"])],
access_level="member",
description="Shows hot playfields")
def hot_tl(self, _, tl, faction: str, named_params):
if faction:
if faction.startswith("--page="):
named_params = DictObject({'page': faction[7:]})
faction = None
if faction is not None and faction.lower() not in ['omni', 'clan', 'neut', 'neutral']:
return f"Unknown faction: {faction}"
tl = tl[2:]
page = int(named_params.page or "1")
offset = (page - 1) * self.PAGE_SIZE
towers = self.towercache.get_towers_hot_tl(int(tl), faction)
return self.text.format_pagination(towers, offset, page, self.formatter, f"Hot Sites TL{tl} ({len(towers)})",
f"There are no hot sites for TL <highlight>{tl}</highlight>.",
f'hot tl{tl} {faction or ""}', 9)
def formatter(self, row, _, data):
return self.towercache.format_entry(row, len(data))
@command(command="hot",
params=[Int('level', is_optional=True), Any('faction', is_optional=True), NamedParameters(["page"])],
access_level="member",
description="Shows hot playfields by level")
def hot_level(self, _, level, faction, named_params):
if faction:
if faction.startswith("--page="):
named_params = DictObject({'page': faction[7:]})
faction = None
if faction is not None and faction.lower() not in ['omni', 'clan', 'neut', 'neutral']:
return f"Unknown faction: {faction}"
if level:
if level < 0 | level > 220:
return f"Level out of range: {level}"
page = int(named_params.page or "1")
offset = (page - 1) * self.PAGE_SIZE
towers = self.towercache.get_towers_hot_level(level, faction)
level = f"{level}" if level else ""
faction = f"{faction} " if faction else ""
return self.text.format_pagination(towers, offset, page, self.formatter, f"Hot Towersites ({len(towers)})",
f"There are no hot sites.", f'hot {level}{faction}', 9)
@command(command="free", params=[],
access_level="member",
description="Shows hot playfields by level")
def free(self, _, ):
blob = ""
towers = [x for x in self.towercache.get_free() if x.short_name not in ['AND', 'GTC']]
for row in towers:
blob += self.towercache.format_entry(row, len(towers))
return ChatBlob(f"FREE Towersites ({len(towers)})", blob) if blob else f"No free towersites found."
@command(command="lc", params=[], access_level="member",
description="See a list of land control tower sites in a particular playfield")
def lc(self, _):
hot, cold, unplanted = 0, 0, 0
clan, omni, neut = 0, 0, 0
last_pf = 0
previous = {}
blob = ""
def number(numb):
if numb < 10:
return f"<black>0</black>{numb}"
return numb
for tower in self.towercache.get_towers_all():
if tower.id != last_pf:
if last_pf == 0:
previous = tower
last_pf = tower.id
continue
blob += f"<red>{number(hot)}</red> <cyan>{number(cold)}</cyan> <grey>{number(unplanted)}</grey> :: " \
f"<clan>{number(clan)}</clan> <neutral>{number(neut)}</neutral> <omni>{number(omni)}</omni> " \
f"[{self.text.make_tellcmd(previous.short_name, f'lc {previous.long_name}')}] " \
f"{previous.long_name}\n"
hot, cold, unplanted = 0, 0, 0
clan, omni, neut = 0, 0, 0
previous = tower
last_pf = tower.id
site = self.towercache.is_hot(tower)
if site == 0:
cold += 1
elif site == -1:
unplanted += 1
elif site == 1:
hot += 1
faction = tower.get('faction', None)
if faction:
faction = faction.lower
if faction == "omni":
omni += 1
elif faction == "clan":
clan += 1
else:
neut += 1
return ChatBlob('All Tower Sites', blob)
@command(command="lc", params=[Const("org"), Any("org_name", is_optional=True)], access_level="member",
description="See a list of land control tower sites in a particular playfield")
def lc_org(self, _, _1, org):
towers = []
try:
org = int(org)
except ValueError:
pass
if type(org) == str:
orgs = self.db.query(
"SELECT * from all_orgs where org_name LIKE ? and org_id in "
"(SELECT org_id from towers group by org_id)",
[f"%{org.replace(' ', '%')}%"])
if len(orgs) == 1:
towers = self.towercache.get_towers_by_org(orgs[0].org_id)
elif len(orgs) == 0:
return "Your search returned no orgs."
else:
blob = "Your search had multiple results; please pick an org:<br>"
for org in orgs:
blob += "[%s] <highlight>%s<end> (<highlight>%s<end>) <%s>%s<end> [<highlight>%s<end> " \
"members]<br><pagebreak>" \
% (self.text.make_chatcmd("Towers", "/tell <myname> lc org %s" % org.org_id),
org.org_name, org.org_id, org.faction.lower(), org.faction, org.member_count)
return ChatBlob("Pick an Org", blob)
elif type(org) == int:
if len(self.db.query("SELECT org_id from all_orgs where org_id=?", [int(org)])) == 0:
return "Your search returned no orgs."
else:
towers = self.towercache.get_towers_by_org(org)
title = f"Towersites of the org {org}"
blob = ""
for tower in towers:
blob += self.towercache.format_entry(tower, len(towers))
return ChatBlob(title, blob)
@command(command="lc", params=[Any("playfield"), Int("site_number", is_optional=True)], access_level="member",
description="See a list of land control tower sites in a particular playfield")
def lc_playfield(self, _, playfield_name, site_number):
playfield = self.playfield_controller.get_playfield_by_name(playfield_name)
if not playfield:
return "Could not find playfield <highlight>%s</highlight>." % playfield_name
if site_number:
title = f"Tower site x{site_number} in {playfield.long_name}"
towers = self.towercache.get_towers_by_pf_site(playfield.id, site_number)
else:
title = f"Tower sites in {playfield.long_name}"
towers = self.towercache.get_towers_by_pf(playfield.id)
blob = ""
for tower in towers:
blob += self.towercache.format_entry(tower, len(towers))
if site_number:
blob += "More to come... stay tuned."
return ChatBlob(title, blob)
+342
View File
@@ -0,0 +1,342 @@
import sys
import time
from mysql.connector.cursor import CursorBase
from requests import Session
from conf.config import BotConfig
from core.aochat.BaseModule import BaseModule
from core.db import DB
from core.decorators import instance, timerevent, event
from core.job_scheduler import JobScheduler
from core.text import Text
from core.tyrbot import Tyrbot
from core.util import Util
from modules.raidbot.tower.tower_controller import TowerController
from modules.standard.helpbot.playfield_controller import PlayfieldController
@instance()
class TowerService(BaseModule):
# [{'org_name': "Dragons", 'hot': time.time() + 60 * 60}, {'org_name': "Most Wanted", 'hot': time.time() + 60 * 60}]
attack_hot = []
plant = []
def inject(self, registry):
self.bot: Tyrbot = registry.get_instance("bot")
self.db: DB = registry.get_instance("db")
self.util: Util = registry.get_instance("util")
self.text: Text = registry.get_instance("text")
self.playfield_controller: PlayfieldController = registry.get_instance("playfield_controller")
self.job_scheduler: JobScheduler = registry.get_instance("job_scheduler")
mod = __import__(f'conf.{sys.argv[1]}', fromlist=['BotConfig'])
config: BotConfig = getattr(mod, 'BotConfig')
if hasattr(config, "tower_url"):
self.tower_url = config.tower_url
else:
self.tower_url = None
def pre_start(self):
self.db.shared.exec("CREATE TABLE IF NOT EXISTS towers("
"pf_id int not null, "
"site_number int not null,"
"ql int,"
"x_coord int not null,"
"y_coord int not null,"
"org_id int,"
"org_name varchar(255), "
"faction varchar(32), "
"close_time int,"
"planted int,"
"enabled tinyint, "
"PRIMARY KEY (pf_id, site_number), "
"INDEX ql(ql), INDEX close(close_time), "
"INDEX planted(planted), INDEX enabled(enabled)) ENGINE MEMORY")
self.db.create_view("towers")
@event(event_type=TowerController.TOWER_ATTACK_EVENT, description="Track planthot", is_hidden=True)
def tower_attack(self, _, event_data):
if event_data.attacker.get("org_id", None):
self.attack_hot.append({'org_name': event_data.attacker.org_name, 'hot': time.time() + 60 * 60})
@event(event_type=TowerController.TOWER_VICTORY_EVENT, description="Send NW warnings")
def victory(self, _, event_data):
t = int(time.time())
if event_data.type == "attack":
row = self.get_last_attack(event_data.winner.faction, event_data.winner.org_name, event_data.loser.faction,
event_data.loser.org_name, event_data.location.playfield.id, t)
self.send_nw_warn(0,
f'<{event_data.loser.faction.lower()}>'
f'{event_data.loser.org_name}'
f'</{event_data.loser.faction.lower()}>'
f' Lost their Site at {event_data.location.playfield.short_name} x{row.site} - '
f'<{event_data.winner.faction.lower()}>'
f'{event_data.winner.org_name}'
f'</{event_data.winner.faction.lower()}> <highlight>won!!</highlight>')
self.plant.append({'pf': row.pf_id, 'site': row.site, 'plant': time.time() + 20 * 60 - 1})
self.prepare_nw_warn(row.pf_id, row.site)
elif event_data.type == "terminated":
field = self.db.query("SELECT * FROM towers t where t.org_name=? and t.pf_id=?",
[event_data.loser.org_name, event_data.playfield.id])
if len(field) == 1:
field = field[0]
self.plant.append({'pf': event_data.location.playfield.id,
'site': field.site_number,
'plant': time.time() + 20 * 60 - 1})
self.send_nw_warn(0, f'<{event_data.loser.faction.lower()}>'
f'{event_data.loser.org_name}'
f'</{event_data.loser.faction.lower()}> Lost their Site at '
f'{event_data.location.playfield.short_name} x{field.site_number}')
self.prepare_nw_warn(event_data.location.playfield.id, field.site_number)
return
self.plant.append({'pf': event_data.location.playfield.id,
'site': f"(<red>UKN</red>) PO: {event_data.loser.org_name}|{event_data.loser.faction}",
'plant': time.time() + 20 * 60 - 1})
self.send_nw_warn(0,
f'<{event_data.loser.faction.lower()}>'
f'{event_data.loser.org_name}'
f'</{event_data.loser.faction.lower()}> '
f'Lost their Site in {event_data.location.playfield.long_name}')
self.prepare_nw_warn(event_data.playfield.id, "(<red>UKN</red>)",
f"(<red>UKN</red>) PO: {event_data.loser.org_name}|{event_data.loser.faction}")
@timerevent(budatime="4h", description="fetch the towerAPI cache", is_enabled=False)
def fetch_tower_update(self, _1, _2):
if self.tower_url:
#
# ONLY Access the API's via Tor... Tyrence is shadow-banning every IP Subnet
# accessing it multiple times/hour whenever the limit parameter is being used....
# ?limit is a parameter not documented anywhere, but allows pulling the whole list
# On other API implementations this parameter has no effect,
# as the server always responds with the full list.
from torpy.http.requests import TorRequests
with TorRequests() as tor_request:
with tor_request.get_session(1) as session:
session: Session
r = session.get(self.tower_url)
if not r:
return
if data := r.json()["results"]:
blob = []
for row in data:
blob.append((row["playfield_id"], row["site_number"], row["ql"],
row["x_coord"], row["y_coord"],
row["org_id"], row['org_name'], row['faction'],
row["close_time"], row["created_at"],
row["enabled"]))
with self.db.lock:
with self.db.pool.get_connection() as conn:
with conn.cursor(dictionary=True) as cur:
cur: CursorBase
cur.executemany(
"INSERT INTO towers (pf_id, site_number, "
"ql, x_coord, y_coord, org_id, org_name, "
"faction, close_time, planted, enabled) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE "
"org_id=VALUE(org_id), "
"org_name=VALUE(org_name), "
"faction=VALUE(faction), "
"close_time=VALUE(close_time),"
"ql=VALUE(ql),"
"planted=VALUE(planted) ", blob)
rem = []
for key, value in enumerate(sorted(self.attack_hot, key=lambda k: k['hot'])):
if value['hot'] < time.time():
rem.append(key)
for key in reversed(rem):
self.attack_hot.pop(key)
def day_time(self, day_t):
if day_t > 86400:
day_t -= 86400
elif day_t < 0:
day_t += 86400
return day_t
def get(self, where_order="", param=None):
if param is None:
param = []
return self.db.query(f"SELECT a.org_id, a.planted, a.close_time, a.ql, a.org_name, a.faction, "
f"b.min_ql, b.max_ql, b.site_name, b.site_number, b.x_coord, b.y_coord, "
f"c.id, c.long_name, c.short_name, "
f"e.pvp_min, e.pvp_max "
f"FROM towers a "
f"INNER JOIN tower_site b ON a.site_number=b.site_number "
f"LEFT JOIN playfields c ON a.pf_id = c.id "
f"LEFT JOIN level e ON e.level = a.ql "
f"WHERE a.pf_id = b.playfield_id and enabled = 1 "
f"{where_order}", param)
def get_towers_by_tl(self, tl, faction=None):
min_ql, max_ql = self.util.get_level_range_tl(tl)
if faction:
return self.get("and ql between ? and ? and faction LIKE ? order by c.short_name",
[min_ql, max_ql, faction])
return self.get("and ql between ? and ? order by c.short_name", [min_ql, max_ql])
def get_towers_all(self):
return self.get("order by c.long_name", [])
def get_towers_by_pf(self, pf):
return self.get("and a.pf_id=? order by a.site_number", [pf])
def get_towers_by_pf_site(self, pf, site):
return self.get("and a.pf_id=? and a.site_number=?", [pf, site])
def get_towers_by_org(self, org):
return self.get("and a.org_id"
"=? order by c.long_name", [org])
def get_towers_by_org_name(self, org):
return self.get("and a.org_name=? order by c.long_name", [org])
def get_free(self):
return self.get("and a.org_id IS NULL order by a.site_number", [])
def get_towers_hot_tl(self, tl, faction=None):
towers = self.get_towers_by_tl(tl, faction)
out = []
for tower in towers:
if self.is_hot(tower) in [1, 2]:
out.append(tower)
return out
def get_towers_hot_level(self, level, faction=None):
out = []
if level:
if faction:
towers = self.get("and pvp_min <= ? and pvp_max >= ? and faction LIKE ?", [level, level, faction])
else:
towers = self.get("and pvp_min <= ? and pvp_max >= ?", [level, level])
else:
if faction:
towers = self.get("and faction LIKE ?", [faction])
else:
towers = self.get()
for tower in towers:
if self.is_hot(tower) in [1, 2]:
out.append(tower)
return out
def format_entry(self, entry, _):
h3 = ""
now = self.day_time(int(time.time()) % 86400)
row0 = f"<font color=CCInfoText>Site: <blue>{entry.short_name}</blue> <red>x{entry.site_number}</red> " \
f"[R:<red>{entry.min_ql} - {entry.max_ql}</red>] " \
f"[{self.text.make_tellcmd('More', f'lc {entry.short_name} {entry.site_number}')}]\n"
row1 = f'<grey>UKN</grey> :: <red>No Owner -> Unplanted</red>\n'
row2 = ""
row3 = "</font>\n"
hot = self.is_hot(entry)
if hot != -1:
h1 = "<cyan>COLD</cyan>"
if hot == 2:
h1 = "<red>WARHOT</red>"
for org in self.attack_hot:
if org['org_name'] == entry.org_name:
hot_normal = self.is_hot(entry, False)
# print(hot, hot_normal, org['hot'] - now)
if hot_normal == 1:
h3 = f"<cyan>COLD</cyan> in " \
f"{self.util.format_time(self.day_time(int(entry.close_time - now)))}"
if hot_normal == 0:
h3 = f"<cyan>COLD</cyan> in {self.util.format_time(org['hot'] - now)}"
elif hot == 1:
h1 = '<red>HOT</red>'
h3 = f"<cyan>COLD</cyan> in {self.util.format_time(self.day_time(int(entry.close_time - now)))}"
else:
hot_time = self.day_time(entry.close_time - 6 * 60 * 60)
h3 = f"<red>HOT</red> in " \
f"{self.util.format_time((18 * 80 * 60 - hot_time) if hot_time > 18 * 60 * 60 else hot_time)}"
org = f"<{entry.faction.lower()}>{entry.org_name}</{entry.faction.lower()}> " \
f"[{self.text.make_tellcmd('View org', f'lc org {entry.org_name}')}]"
row1 = f"{h1} :: {h3} :: {org} :: \n"
row3 = f" » Planted: <grey>{self.util.format_datetime(entry.planted)}</grey></font>\n\n"
if entry.pvp_min:
pvp = f"[<red>{entry.pvp_min} - {entry.pvp_max}</red>]"
else:
pvp = f"[<red>175 - 220</red>]"
# noinspection LongLine
row2 = f" » QL: <red>{entry.ql}</red> PvP: {pvp} " \
f"[{self.text.make_chatcmd(f'{entry.x_coord} x {entry.y_coord}', f'/waypoint {entry.x_coord} {entry.y_coord} {entry.id}')}]\n"
return row0 + row1 + row2 + row3 + "<pagebreak>"
def is_hot(self, entry, with_war=True) -> int:
if entry.get("close_time", None):
now = self.day_time((time.time()) % 86400)
self.attack_hot.sort(key=lambda k: k['org_name'])
rem = []
inside = False
for index, i in enumerate(self.attack_hot):
if i['hot'] < time.time():
rem.append(index)
continue
if i['org_name'] == entry.org_name:
inside = True
for index in reversed(rem):
self.attack_hot.pop(index)
if inside and with_war:
return 2
if self.day_time(entry.close_time - int(now)) > 6 * 60 * 60:
return 0
return 1
return -1
def get_last_attack(self, att_faction, att_org_name, def_faction, def_org_name, playfield_id, t):
last_updated = t - (8 * 3600)
is_finished = 1
sql = """
SELECT
b.id AS battle_id,
a.id AS attack_id,
b.playfield_id as pf_id,
b.site_number as site
FROM
tower_battle b
JOIN tower_attacker a ON
a.tower_battle_id = b.id
WHERE
a.att_faction = ?
AND a.att_org_name = ?
AND b.def_faction = ?
AND b.def_org_name = ?
AND b.playfield_id = ?
AND b.is_finished = ?
AND b.last_updated >= ?
ORDER BY
last_updated DESC
LIMIT 1"""
return self.db.query_single(sql,
[att_faction, att_org_name, def_faction, def_org_name, playfield_id, is_finished,
last_updated])
def prepare_nw_warn(self, pf_id, site, bonus=""):
pf = self.playfield_controller.get_playfield_by_id(pf_id)
site = f"{pf.short_name} <cyan>»</cyan> x{site}" + bonus
self.job_scheduler.delayed_job(self.send_nw_warn, 0, f"{site} plantable in 20 minutes!")
self.job_scheduler.delayed_job(self.send_nw_warn, 10 * 60 - 1, f"{site} plantable in 10 minutes!")
self.job_scheduler.delayed_job(self.send_nw_warn, 15 * 60 - 1, f"{site} plantable in 5 minutes!")
self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 - 1, f"{site} plantable in 1 minute!")
self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 + 30 - 1, f"{site} plantable in 30 seconds!")
self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 + 45 - 1, f"{site} plantable in 15 seconds!")
self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 + 50 - 1, f"{site} plantable in 10 seconds!")
self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 + 55 - 1, f"{site} plantable in 5 seconds!")
self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 + 56 - 1, f"{site} plantable in 4 seconds!")
self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 + 57 - 1, f"{site} plantable in 3 seconds!")
self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 + 58 - 1, f"{site} plantable in 2 seconds!")
self.job_scheduler.delayed_job(self.send_nw_warn, 19 * 60 + 59 - 1, f"{site} plantable in 1 second!")
self.job_scheduler.delayed_job(self.send_nw_warn, 20 * 60 - 1, f"{site} plantable <green>NOW</green>!")
def send_nw_warn(self, _, msg):
self.bot.send_private_channel_message(f"[<cyan>NW</cyan>] {msg}")
+278
View File
@@ -0,0 +1,278 @@
# noinspection LongLineForFile
DROP TABLE IF EXISTS tower_site;
CREATE TABLE tower_site
(
playfield_id INT NOT NULL,
site_number SMALLINT NOT NULL,
min_ql SMALLINT NOT NULL,
max_ql SMALLINT NOT NULL,
x_coord SMALLINT NOT NULL,
y_coord SMALLINT NOT NULL,
site_name varchar(50) NOT NULL,
PRIMARY KEY (playfield_id, site_number)
);
INSERT INTO tower_site (playfield_id, site_number, min_ql, max_ql, x_coord, y_coord, site_name)
VALUES (505, 1, 60, 90, 2740, 4260, 'Griffon Frontier'),
(505, 2, 80, 110, 540, 4180, 'Draught'),
(505, 3, 70, 95, 1740, 3460, 'Dreadfire Volcano'),
(505, 4, 80, 120, 2780, 3420, 'Northeast Barren Lands'),
(505, 5, 60, 90, 580, 3140, 'Western Desert'),
(505, 6, 50, 75, 2420, 1900, 'Waylander Mines'),
(505, 7, 70, 100, 1860, 1700, 'North of Main Omni Base'),
(505, 8, 61, 82, 460, 1380, 'Dome Ore'),
(505, 9, 100, 150, 2700, 620, 'Crystal Forge Volcano'),
(505, 10, 100, 150, 660, 460, 'SW Low Plateau'),
(550, 1, 10, 20, 2660, 2020, 'Sifter Beach'),
(550, 2, 20, 30, 1780, 1780, 'Academy Ore'),
(550, 3, 15, 25, 1980, 1340, 'Athen Fault'),
(550, 4, 10, 20, 2660, 820, 'Grindmoore'),
(550, 5, 15, 23, 1380, 380, 'Gladius Grove'),
(551, 1, 40, 90, 1700, 3700, 'Styx Magma'),
(551, 2, 35, 50, 2220, 3340, 'Carbon Grove'),
(551, 3, 26, 50, 980, 3140, 'Between the Craters'),
(551, 4, 25, 35, 340, 2420, 'Powdered Dunes'),
(551, 5, 32, 45, 2540, 2060, 'Dust Bank'),
(551, 6, 20, 30, 580, 1740, 'Charred Groove'),
(551, 7, 12, 45, 940, 1540, 'West of Perdition'),
(551, 8, 15, 30, 660, 900, 'North of Yuttos'),
(560, 1, 100, 170, 1500, 3420, 'Terraform Edge'),
(560, 2, 170, 250, 3060, 3020, 'West Spirals'),
(560, 3, 170, 250, 3500, 2980, 'East Spirals'),
(560, 4, 130, 170, 1220, 2220, 'Middle Mort Desert'),
(560, 5, 1, 100, 900, 1460, 'Green Crater'),
(560, 6, 110, 160, 3100, 1460, 'Oasis Ore'),
(560, 7, 150, 200, 2740, 700, 'South East Craterwall'),
(560, 8, 100, 150, 540, 540, 'South West Craterwall'),
(560, 9, 160, 210, 2780, 540, 'Stormshelter'),
(565, 1, 25, 40, 2940, 2900, 'Rich Desert Ridge'),
(565, 2, 30, 45, 1980, 2580, 'East of Meetmedere'),
(565, 3, 50, 75, 540, 2020, 'Middle of Western Desert'),
(565, 4, 40, 60, 2580, 1940, 'North of Rhino Village'),
(565, 5, 40, 60, 2700, 1260, 'South of Rhino Village'),
(567, 1, 12, 20, 1220, 1060, 'In the Newland Desert'),
(567, 2, 15, 25, 540, 460, 'West of Newland Lake'),
(570, 1, 200, 300, 3220, 3020, 'North of Cyborg Hideout'),
(570, 2, 191, 250, 3780, 2540, 'Middle of Liberty'),
(570, 3, 120, 180, 980, 2060, 'South of Sabulum'),
(570, 4, 190, 230, 3940, 2060, 'Cyborg Border'),
(570, 5, 200, 300, 2820, 1820, 'Middle of Perpetual Wastelands'),
(570, 6, 200, 300, 3740, 1700, 'South of Cyborg Hideout'),
(570, 7, 100, 150, 1500, 1340, 'Lower Plateu Zone'),
(570, 8, 100, 150, 2100, 1380, 'The Mid Canyon Crossing'),
(570, 9, 120, 180, 3020, 1220, 'Plains of dust'),
(570, 10, 100, 150, 900, 1060, 'West of Canyon'),
(570, 11, 100, 150, 3180, 940, 'The Canyon Mines'),
(570, 12, 190, 230, 2300, 780, 'South of Canyon'),
(585, 1, 40, 60, 1220, 2740, 'Northern Wastelands'),
(585, 2, 11, 16, 2180, 2580, 'West Wastelands'),
(585, 3, 40, 55, 1020, 2460, 'Mid Wastelands'),
(585, 4, 30, 45, 2140, 1660, 'Giant Green River Bank North'),
(585, 5, 11, 16, 1180, 1340, 'West of the Dead Forest'),
(585, 6, 30, 45, 2100, 1340, 'Giant Green River Bank South'),
(585, 7, 15, 22, 1420, 1020, 'Canyon East'),
(585, 8, 25, 35, 820, 780, 'Canyon South'),
(585, 9, 35, 50, 900, 460, 'By the River'),
(590, 1, 140, 200, 1740, 3100, 'By the Fisher Village'),
(590, 2, 140, 200, 2100, 3060, 'Fisher Village Approach'),
(590, 3, 100, 170, 2900, 2820, 'North Forest'),
(590, 4, 90, 130, 3340, 2700, 'North-east Forest'),
(590, 5, 130, 170, 860, 1220, 'North-west of Lava Ditches'),
(590, 6, 100, 150, 3100, 980, 'Mid Clutching Forest'),
(590, 7, 130, 170, 860, 780, 'South-west of Lava Ditches'),
(590, 8, 100, 150, 3180, 620, 'South Clutching Forest'),
(595, 1, 100, 150, 1140, 3380, 'Old ruins'),
(595, 2, 100, 150, 3180, 2900, 'Plains of defense'),
(595, 3, 130, 180, 1740, 2300, 'The haunted forest outskirt'),
(595, 4, 130, 180, 900, 2220, 'Forest of Xzawkaz'),
(595, 5, 200, 300, 2260, 1860, 'In the Swamp of Horrors'),
(595, 6, 130, 180, 1420, 1500, 'Island of Control'),
(595, 7, 130, 180, 1340, 1140, 'The swamp of hope'),
(595, 8, 200, 300, 2900, 1100, 'South of the Medusa'),
(595, 9, 140, 210, 2140, 780, 'Middle of the Foul Forest'),
(595, 10, 200, 300, 540, 540, 'Southern Forest of Xzawkaz'),
(600, 1, 30, 45, 2420, 2980, 'By the Rivers Edge'),
(600, 2, 50, 75, 620, 2900, 'North Forest Road'),
(600, 3, 25, 50, 1300, 2660, 'Along the Rivers Edge'),
(600, 4, 30, 45, 3740, 2500, 'East Forest'),
(600, 5, 25, 50, 3140, 2020, 'Rhino Hills'),
(600, 6, 50, 75, 580, 1700, 'West Forest'),
(600, 7, 60, 90, 1940, 1620, 'Crossroads'),
(600, 8, 60, 90, 1140, 1500, 'Forestdawn'),
(600, 9, 50, 75, 3220, 1140, 'East of Crater'),
(605, 1, 160, 200, 2940, 2820, 'Forest Waters'),
(605, 2, 110, 120, 1100, 2620, 'Muddy Pools'),
(605, 3, 100, 150, 1700, 2300, 'West of Wine'),
(605, 4, 120, 180, 2940, 2260, 'East of Wine'),
(605, 5, 130, 195, 1900, 1740, 'Central Belial Forest'),
(605, 6, 130, 190, 2500, 1660, 'River Delta'),
(605, 7, 160, 200, 2540, 1220, 'Junction Forest'),
(605, 8, 100, 150, 2340, 860, 'Borderline'),
(605, 9, 120, 180, 2020, 420, 'Southern belial Mine'),
(605, 10, 140, 200, 620, 380, 'Southwest Belial Mining District'),
(610, 1, 60, 90, 1380, 2780, 'Tetlies Land control area'),
(610, 2, 80, 120, 2900, 2660, 'East of the Great Marsh'),
(610, 3, 60, 90, 660, 2460, 'West of outpost 10-3'),
(610, 4, 100, 150, 2300, 2020, 'Defense of Geholva'),
(610, 5, 106, 143, 2740, 1180, 'South of Forest of Geholva'),
(610, 6, 120, 180, 860, 900, 'Avid Crater'),
(610, 7, 120, 180, 1540, 900, 'East of Avid Crater'),
(610, 8, 100, 150, 2460, 540, 'Bendelham forest Defense'),
(615, 1, 60, 100, 1900, 3020, 'North of Lenne'),
(615, 2, 100, 150, 860, 2820, 'Little Hawaii Defense'),
(615, 3, 90, 120, 2620, 2660, 'Defense of Zoto'),
(615, 4, 60, 100, 900, 2100, 'By the Ocean'),
(615, 5, 61, 100, 2300, 1180, 'Birm'),
(615, 6, 120, 180, 2700, 660, 'SFH Defense'),
(615, 7, 100, 150, 1860, 500, 'South in Nightplain'),
(620, 1, 150, 200, 2700, 3860, 'Krud the Lost Valley Defense'),
(620, 2, 150, 225, 1900, 3180, 'Pranade'),
(620, 3, 120, 180, 620, 2980, 'Plains of Jarga Defense'),
(620, 4, 200, 300, 2460, 2260, 'Old Plains'),
(620, 5, 200, 300, 1540, 1780, 'Middle of Easter Fouls Plains'),
(620, 6, 130, 200, 1540, 1140, 'Clefre Defense'),
(620, 7, 100, 150, 2020, 860, 'Central Sharewood'),
(620, 8, 200, 300, 820, 540, 'Pegradul'),
(625, 1, 90, 130, 1460, 1940, 'The Resilient Forest - North'),
(625, 2, 90, 120, 1900, 1540, 'The Resilient Forest - East'),
(625, 3, 125, 170, 2780, 1380, 'Central Prowler Waste'),
(625, 4, 100, 125, 1380, 1180, 'Central Resilient Forest'),
(625, 5, 125, 170, 2860, 1020, 'Southern Prowler Waste'),
(625, 6, 100, 150, 4020, 980, 'The Barren Hills'),
(625, 7, 100, 125, 1740, 860, 'The Resilient Forest - South'),
(625, 8, 50, 75, 2460, 540, 'The Silent Woods - East'),
(630, 1, 40, 60, 1540, 2660, 'Pleasant Range Offense Hill'),
(630, 2, 60, 90, 2380, 2500, 'Central Pleasant Range'),
(630, 3, 50, 75, 580, 2420, 'West of 20K'),
(630, 4, 30, 70, 3220, 2220, 'Pleasant Range Defense'),
(630, 5, 60, 90, 3220, 1980, 'Pleasant River Defense'),
(630, 6, 60, 90, 3260, 1500, 'Pleasant River Offense'),
(630, 7, 40, 60, 2260, 1140, 'Central Pleasant Plains'),
(630, 8, 30, 70, 3020, 1020, 'East Pleasant Plains'),
(630, 9, 30, 45, 740, 460, 'West of Versailles Tower'),
(635, 1, 55, 70, 700, 2420, 'Northern River Bank'),
(635, 2, 60, 90, 1780, 2460, 'Hawker Trench'),
(635, 3, 70, 105, 1460, 1740, 'Klapam Forest Defense'),
(635, 4, 55, 70, 2020, 1740, 'Klompfot Defense'),
(635, 5, 70, 105, 1900, 1220, 'South of Trench'),
(635, 6, 80, 120, 1140, 940, 'Nile Hills'),
(635, 7, 55, 70, 1780, 700, 'Aprils Rock Offense'),
(635, 8, 80, 150, 820, 420, 'Southern Lower River Bank'),
(635, 9, 80, 150, 1700, 340, 'Aprils Rock Defense'),
(646, 1, 10, 15, 460, 1300, 'Great W. Forest Vein'),
(646, 2, 10, 15, 2940, 980, 'The Hidden Notum Canal'),
(646, 3, 20, 30, 3220, 620, 'Mountain Areas'),
(646, 4, 10, 15, 580, 580, 'Great W. Forest Dorsal'),
(646, 5, 10, 15, 1500, 460, 'Western Mountain Areas'),
(647, 1, 90, 135, 1100, 3100, 'The Mineral Mine'),
(647, 2, 20, 30, 2900, 2940, 'NE Desert Aperient'),
(647, 3, 37, 64, 1900, 2700, 'SurroundingTemple of Three Winds'),
(647, 4, 25, 40, 2220, 1900, 'Piercing Thundertube'),
(647, 5, 30, 45, 2820, 1940, 'Central Striking Ant'),
(647, 6, 25, 40, 620, 1660, 'Tir Prairie'),
(647, 7, 25, 40, 1180, 1700, 'Crater Swamp'),
(650, 1, 50, 75, 540, 2820, 'West Pass'),
(650, 2, 65, 75, 900, 2300, 'Crowning Shallows'),
(650, 3, 100, 150, 1660, 2180, 'Haven Notum Crematorium'),
(650, 4, 70, 140, 2020, 1740, 'Stret Vale Deux Drilling Field'),
(650, 5, 120, 180, 1340, 1620, 'The Flooded Bottomland'),
(650, 6, 75, 90, 1820, 740, 'Stret Woods'),
(650, 7, 60, 90, 940, 420, 'Greenslopes'),
(655, 1, 30, 45, 420, 2700, 'Skop Notum Mine'),
(655, 2, 30, 80, 2820, 2340, 'Klor'),
(655, 3, 60, 80, 2820, 1660, 'Harstad'),
(655, 4, 40, 90, 540, 1580, 'Ubleo'),
(655, 5, 40, 60, 1420, 1580, 'Flubu Notum Mine'),
(655, 6, 40, 70, 4340, 900, 'Plago'),
(655, 7, 60, 80, 2260, 380, 'jucha'),
(655, 8, 70, 105, 4380, 380, 'Mune'),
(655, 9, 30, 60, 820, 340, 'Mocnuf Notum Mine'),
(665, 1, 80, 150, 940, 4820, 'Central Desert north'),
(665, 2, 45, 75, 1260, 3860, 'Notum Disruption Mountain'),
(665, 3, 75, 110, 1940, 3860, 'The Notum Plains'),
(665, 4, 100, 150, 940, 3380, 'Near Clan Outpost'),
(665, 5, 45, 80, 1300, 3060, 'Central Mountains'),
(665, 6, 55, 150, 380, 2300, 'Surrounding Evil'),
(665, 7, 45, 60, 1260, 2140, 'Notum Mountain'),
(665, 8, 55, 100, 2020, 1980, 'Near Omni-Tek Outpost'),
(665, 9, 100, 150, 420, 820, 'Shores Notum Vein'),
(670, 1, 30, 45, 1100, 4340, 'Yukon Source'),
(670, 2, 35, 50, 1460, 2540, 'Frisko'),
(670, 3, 30, 45, 2140, 2420, 'Round Hills'),
(670, 4, 50, 75, 2140, 1900, 'Dense Drewen'),
(670, 5, 35, 50, 1260, 1820, 'Borrowed Hill'),
(670, 6, 35, 50, 1340, 1340, 'Narrow Lune'),
(670, 7, 10, 15, 2500, 1220, 'Micron Slopes Notum Mine'),
(670, 8, 50, 75, 2100, 540, 'High Juniper'),
(670, 9, 50, 75, 2300, 460, 'High Juniper Notum Vein'),
(685, 1, 35, 50, 2140, 2620, 'Nature Reverve - East'),
(685, 2, 35, 50, 1900, 2580, 'Nature Reverve - West'),
(685, 3, 50, 75, 1300, 1900, 'Poole - West'),
(685, 4, 50, 75, 1580, 1820, 'Poole - East'),
(685, 5, 15, 25, 1140, 1100, 'V-Hill'),
(685, 6, 20, 30, 1580, 700, 'Lunder Hills - North'),
(685, 7, 25, 40, 2740, 460, 'Galway hills'),
(685, 8, 20, 30, 1220, 380, 'Lunder Hills'),
(685, 9, 25, 40, 2260, 380, 'South-east Woods'),
(687, 1, 10, 15, 500, 1900, 'Blossom Valley'),
(687, 2, 10, 15, 380, 1300, 'Konty Passage Plains'),
(687, 3, 17, 28, 900, 1220, 'Vas'' Pass'),
(687, 4, 15, 25, 780, 900, 'Arthur''s Pass'),
(687, 5, 10, 15, 380, 580, 'Kontys Sixth Passage - West'),
(687, 6, 10, 15, 620, 540, 'Kontys Sixth Passage - East'),
(695, 1, 30, 45, 940, 3260, 'North West Lush Fields'),
(695, 2, 20, 30, 2420, 3180, 'North East Lush Fields'),
(695, 3, 10, 40, 3460, 2940, 'Stret River Island'),
(695, 4, 40, 60, 1260, 2460, 'West of Outpost'),
(695, 5, 35, 60, 1740, 2460, 'East of Outpost'),
(695, 6, 20, 30, 1780, 1820, 'Central Lush Fields'),
(695, 7, 10, 15, 2860, 420, 'South East Lush Fields'),
(695, 8, 30, 45, 980, 380, 'South West Lush Fields'),
(696, 1, 15, 25, 780, 1420, 'Mutant Domain North'),
(696, 2, 20, 30, 500, 860, 'Mutant Domain Central'),
(696, 3, 25, 40, 780, 460, 'Mutant Domain South'),
(716, 1, 20, 35, 500, 3220, 'Northern Grassland'),
(716, 2, 15, 30, 980, 3020, 'Moderate Grassland'),
(716, 3, 10, 20, 460, 2180, 'Dungeon Hilltop'),
(716, 4, 10, 15, 700, 2180, 'Rocky Upsurge'),
(716, 5, 15, 25, 340, 1420, 'Northern Easy Swamps Notum Field'),
(716, 6, 15, 26, 460, 820, 'Ocean Inlet'),
(717, 1, 30, 45, 1620, 2660, 'Greater Omni Forest Swamps'),
(717, 2, 15, 25, 1180, 2460, 'Dragonback Ridge'),
(717, 3, 30, 45, 1900, 1820, 'Mountainous Regions'),
(717, 4, 20, 35, 1860, 1340, 'Waterfall Swamp'),
(717, 5, 10, 15, 1500, 1300, 'Greater Omni Forest South'),
(717, 6, 25, 40, 900, 1220, 'Northern Semi-Barren Area'),
(717, 7, 10, 25, 1940, 900, 'Ring Mountain Range'),
(717, 8, 14, 25, 940, 460, 'Southern Isle'),
(760, 1, 60, 90, 1580, 2380, 'Notum Ore in Buttu'),
(760, 2, 35, 50, 940, 2020, 'Mountain of Fourtyone'),
(760, 3, 35, 50, 1300, 1980, 'Mountain in 4Holes'),
(760, 4, 45, 60, 1660, 1740, 'South of Ahenus'),
(760, 5, 70, 100, 1820, 1340, 'Ibreri Woods North'),
(760, 6, 35, 50, 1460, 1260, 'Mountain of Fourtytwo'),
(760, 7, 100, 150, 1740, 1060, 'Ibreri Woods'),
(760, 8, 45, 70, 1180, 500, 'Ibreri'),
(760, 9, 45, 70, 460, 420, 'Jall Mountain'),
(790, 1, 20, 30, 1700, 3100, 'Hells Courtyard'),
(790, 2, 15, 25, 2300, 2860, 'Pondus Beach'),
(790, 3, 15, 30, 1700, 2780, 'Hound Land'),
(790, 4, 20, 40, 1980, 2780, 'Hound Notum Field'),
(790, 5, 20, 30, 1700, 1940, 'East Mutie'),
(790, 6, 12, 30, 1340, 1220, 'Omni Outpost'),
(790, 7, 20, 30, 660, 1180, 'South Mutie'),
(790, 8, 30, 60, 2260, 1140, 'The Beach'),
(791, 1, 15, 26, 420, 2020, 'Populous Mountain'),
(791, 2, 12, 22, 660, 1500, 'Hound Land Mining'),
(791, 3, 12, 20, 220, 1060, 'Stret West Notum Ore'),
(791, 4, 10, 15, 740, 820, 'Snake Mountain'),
(791, 5, 20, 40, 780, 460, 'Southern Empty Wastes and Roads'),
(791, 6, 10, 20, 380, 340, 'Transit Valley Ore'),
(795, 1, 40, 60, 4220, 1580, 'Illuminati'),
(795, 2, 100, 150, 500, 1540, 'Northern Forest of Illuminations'),
(795, 3, 25, 50, 3420, 1540, 'Fate Notum Field'),
(795, 4, 71, 120, 580, 820, 'Pegrama'),
(795, 5, 84, 120, 1220, 700, 'Grazeland Notum Field'),
(795, 6, 50, 75, 4020, 620, 'Winterbottom'),
(795, 7, 90, 120, 540, 500, 'Southern Forest of Illuminations'),
(795, 8, 60, 90, 2900, 500, 'Summer');