Release 0.9.0

This commit is contained in:
2025-11-21 07:26:02 +01:00
committed by ecv
commit 472f0812e7
240 changed files with 20033 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
from bot.module import Module
from bot import loaded_modules_dict
from time import time
class Environment(Module):
templates = object
def __init__(self):
setattr(self, "default_options", {
"module_name": self.get_module_identifier()[7:],
"dom_element_root": [],
"dom_element_select_root": ["id"],
"run_observer_interval": 3
})
setattr(self, "required_modules", [
"module_dom",
"module_dom_management",
"module_telnet",
"module_webserver"
])
self.next_cycle = 0
Module.__init__(self)
@staticmethod
def get_module_identifier():
return "module_game_environment"
def on_socket_connect(self, steamid):
Module.on_socket_connect(self, steamid)
def on_socket_disconnect(self, steamid):
Module.on_socket_disconnect(self, steamid)
# region Standard module stuff
def setup(self, options=dict):
Module.setup(self, options)
self.dom_element_root = self.options.get(
"dom_element_root", self.default_options.get("dom_element_root", None)
)
self.dom_element_select_root = self.options.get(
"dom_element_select_root", self.default_options.get("dom_element_select_root", None)
)
self.run_observer_interval = self.options.get(
"run_observer_interval", self.default_options.get("run_observer_interval", None)
)
# endregion
def get_last_recorded_gametime_dict(self):
active_dataset = self.dom.data.get("module_game_environment", {}).get("active_dataset", None)
if active_dataset is None:
return None
return (
self.dom.data
.get("module_game_environment", {})
.get(active_dataset, {})
.get("last_recorded_gametime", {})
)
def get_last_recorded_gametime_string(self):
last_recorded_gametime_dict = self.get_last_recorded_gametime_dict()
if last_recorded_gametime_dict is None:
return "Day {day}, {hour}:{minute}".format(
day="n/a",
hour="n/a",
minute="n/a"
)
return "Day {day}, {hour}:{minute}".format(
day=last_recorded_gametime_dict.get("day", "n/a"),
hour=last_recorded_gametime_dict.get("hour", "n/a"),
minute=last_recorded_gametime_dict.get("minute", "n/a")
)
def run(self):
while not self.stopped.wait(self.next_cycle):
profile_start = time()
self.trigger_action_hook(self, event_data=["getgameprefs", {
"disable_after_success": True
}])
# requires getgameprefs to be successful
self.trigger_action_hook(self, event_data=["getgamestats", {
"disable_after_success": True
}])
self.trigger_action_hook(self, event_data=["gettime", {}])
self.trigger_action_hook(self, event_data=["getentities", {}])
self.execute_telnet_triggers()
self.last_execution_time = time() - profile_start
self.next_cycle = self.run_observer_interval - self.last_execution_time
loaded_modules_dict[Environment().get_module_identifier()] = Environment()

View File

@@ -0,0 +1,49 @@
from bot import loaded_modules_dict
from os import path, pardir
from time import time
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
action_name = path.basename(path.abspath(__file__))[:-3]
def main_function(module, event_data, dispatchers_steamid):
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
module.dom.data.upsert({
"module_game_environment": {
active_dataset: {
"cancel_shutdown": True,
"shutdown_in_seconds": None,
"force_shutdown": False
}
}
})
event_data = ['say_to_all', {
'message': (
'a [FF6666]scheduled shutdown[-] has been cancelled.'
)
}]
module.trigger_action_hook(module, event_data=event_data)
""" stop the timer """
def callback_success(module, event_data, dispatchers_steamid, match=None):
pass
def callback_fail(module, event_data, dispatchers_steamid):
pass
action_meta = {
"description": "Set the (active) shutdown procedure to be cancelled",
"main_function": main_function,
"callback_success": callback_success,
"callback_fail": callback_fail,
"requires_telnet_connection": True,
"enabled": True
}
loaded_modules_dict["module_" + module_name].register_action(action_name, action_meta)

View File

@@ -0,0 +1,52 @@
from bot import loaded_modules_dict
from os import path, pardir
from time import time
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
action_name = path.basename(path.abspath(__file__))[:-3]
def main_function(module, event_data, dispatchers_steamid):
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
module.dom.data.upsert({
"module_game_environment": {
active_dataset: {
"cancel_shutdown": False,
"shutdown_in_seconds": None,
"force_shutdown": True
}
}
})
event_data = ['say_to_all', {
'message': (
'[FF6666]FORCED SHUTDOWN INITIATED[-]'
)
}]
module.trigger_action_hook(module, event_data=event_data)
""" stop the timer """
event_data = ['shutdown', {}]
module.trigger_action_hook(module, event_data=event_data)
def callback_success(module, event_data, dispatchers_steamid, match=None):
pass
def callback_fail(module, event_data, dispatchers_steamid):
pass
action_meta = {
"description": "Set the (active) shutdown procedure to be force-completed!",
"main_function": main_function,
"callback_success": callback_success,
"callback_fail": callback_fail,
"requires_telnet_connection": True,
"enabled": True
}
loaded_modules_dict["module_" + module_name].register_action(action_name, action_meta)

View File

@@ -0,0 +1,136 @@
from bot import loaded_modules_dict
from bot.constants import TELNET_TIMEOUT_NORMAL
from os import path, pardir
from time import sleep, time
import re
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
action_name = path.basename(path.abspath(__file__))[:-3]
def main_function(module, event_data, dispatchers_steamid=None):
timeout = TELNET_TIMEOUT_NORMAL
timeout_start = time()
event_data[1]["action_identifier"] = action_name
if not module.telnet.add_telnet_command_to_queue("listents"):
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
return
poll_is_finished = False
# Modern format - matches both empty and populated entity lists
regex = (
r"Executing\scommand\s\'listents\'\sby\sTelnet\sfrom\s(?P<called_by>.*?)\r?\n"
r"(?P<raw_entity_data>[\s\S]*?)"
r"Total\sof\s(?P<entity_count>\d{1,3})\sin\sthe\sgame"
)
while not poll_is_finished and (time() < timeout_start + timeout):
sleep(0.25)
match = False
for match in re.finditer(regex, module.telnet.telnet_buffer):
poll_is_finished = True
if match:
module.callback_success(callback_success, module, event_data, dispatchers_steamid, match)
return
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
def callback_success(module, event_data, dispatchers_steamid, match=None):
raw_entity_data = match.group("raw_entity_data").lstrip()
if len(raw_entity_data) >= 1:
regex = (
r"\d{1,2}. id=(?P<id>\d+), \["
r"type=(?P<type>.+), "
r"name=(?P<name>.*), "
r"id=(\d+)"
r"\], "
r"pos=\((?P<pos_x>.?\d+.\d), (?P<pos_y>.?\d+.\d), (?P<pos_z>.?\d+.\d)\), "
r"rot=\((?P<rot_x>.?\d+.\d), (?P<rot_y>.?\d+.\d), (?P<rot_z>.?\d+.\d)\), "
r"lifetime=(?P<lifetime>.+), "
r"remote=(?P<remote>.+), "
r"dead=(?P<dead>.+), "
r"health=(?P<health>\d+)"
r"\r\n"
)
entities_to_update_dict = {}
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
if active_dataset is None:
return
for m in re.finditer(regex, raw_entity_data):
last_seen_gametime_string = module.game_environment.get_last_recorded_gametime_string()
entity_dict = {
"id": m.group("id"),
"owner": m.group("id"),
"identifier": m.group("id"),
"type": str(m.group("type")),
"name": str(m.group("name")),
"pos": {
"x": int(float(m.group("pos_x"))),
"y": int(float(m.group("pos_y"))),
"z": int(float(m.group("pos_z"))),
},
"rot": {
"x": int(float(m.group("rot_x"))),
"y": int(float(m.group("rot_y"))),
"z": int(float(m.group("rot_z"))),
},
"lifetime": str(m.group("lifetime")),
"remote": bool(m.group("remote")),
"dead": bool(m.group("dead")),
"health": int(m.group("health")),
"dataset": active_dataset,
"last_seen_gametime": last_seen_gametime_string
}
entities_to_update_dict[m.group("id")] = entity_dict
if len(entities_to_update_dict) >= 1:
module.dom.data.upsert({
module.get_module_identifier(): {
"elements": {
active_dataset: entities_to_update_dict
}
}
})
stuff_to_delete = []
for path, dom_element_key, dom_element in module.dom.get_dom_element_by_query(
target_module=module.get_module_identifier(),
query="id"
):
# Delete entities that are no longer in the update or have health <= 0 (dead)
entity_data = entities_to_update_dict.get(dom_element, {})
health = entity_data.get("health", 0)
if dom_element not in entities_to_update_dict or health <= 0:
stuff_to_delete.append([module.get_module_identifier(), "elements"] + path)
for dom_element_to_delete in stuff_to_delete:
module.dom.data.remove_key_by_path(
dom_element_to_delete,
dispatchers_steamid=dispatchers_steamid
)
def callback_fail(module, event_data, dispatchers_steamid):
pass
def skip_it(module, event_data, dispatchers_steamid=None):
pass
action_meta = {
"description": "get game entities",
"main_function": main_function,
"callback_success": callback_success,
"callback_fail": callback_fail,
"skip_it": skip_it,
"requires_telnet_connection": True,
"enabled": True
}
loaded_modules_dict["module_" + module_name].register_action(action_name, action_meta)

View File

@@ -0,0 +1,108 @@
from bot import loaded_modules_dict
from bot.constants import TELNET_TIMEOUT_NORMAL
from bot.logger import get_logger
from os import path, pardir
from time import sleep, time
import re
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
action_name = path.basename(path.abspath(__file__))[:-3]
logger = get_logger("game_environment.getgameprefs")
def main_function(module, event_data, dispatchers_steamid=None):
timeout = TELNET_TIMEOUT_NORMAL
timeout_start = time()
event_data[1]["action_identifier"] = action_name
if not module.telnet.add_telnet_command_to_queue("getgamepref"):
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
return
# Modern format: timestamps ARE present in "Executing command" lines
# Format: 2025-11-18T20:21:02 4854.528 INF Executing command 'getgamepref'...
regex = (
r"(?P<datetime>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})\s(?P<stardate>[-+]?\d*\.\d+|\d+)\s"
r"INF Executing\scommand\s\'getgamepref\'\sby\sTelnet\sfrom\s(?P<called_by>.*?)\r?\n"
r"(?P<raw_gameprefs>(?:GamePref\..*?\r?\n)+)"
)
match = None
match_found = False
poll_is_finished = False
while not poll_is_finished and (time() < timeout_start + timeout):
sleep(0.25)
match = False
for match in re.finditer(regex, module.telnet.telnet_buffer, re.MULTILINE):
poll_is_finished = True
match_found = True
if match_found:
module.callback_success(callback_success, module, event_data, dispatchers_steamid, match)
return
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
def validate_settings(regex, raw_gameprefs):
gameprefs_dict = {}
all_required_settings_are_available = False
for m in re.finditer(regex, raw_gameprefs, re.MULTILINE):
stripped_gameprefs = m.group("gamepref_value").rstrip()
if all([
len(stripped_gameprefs) >= 1, # we have settings
m.group("gamepref_name") == "GameName" # the GameName setting is available!
]):
all_required_settings_are_available = True
gameprefs_dict[m.group("gamepref_name")] = stripped_gameprefs
if all_required_settings_are_available:
return gameprefs_dict
else:
return False
def callback_success(module, event_data, dispatchers_steamid, match=None):
regex = (
r"GamePref\.(?P<gamepref_name>.*)\s\=\s(?P<gamepref_value>.*)\s"
)
raw_gameprefs = match.group("raw_gameprefs")
gameprefs_dict = validate_settings(regex, raw_gameprefs)
if isinstance(gameprefs_dict, dict):
current_game_name = gameprefs_dict.get("GameName", None)
module.dom.data.upsert({
module.get_module_identifier(): {
current_game_name: {
"gameprefs": gameprefs_dict
}
}
})
module.dom.data.upsert({
module.get_module_identifier(): {
"active_dataset": current_game_name
}
})
logger.info("active_dataset_set", dataset=current_game_name)
else:
logger.error("gameprefs_validation_failed", reason="required_settings_missing")
def callback_fail(module, event_data, dispatchers_steamid):
pass
action_meta = {
"description": "gets a list of all current game-preferences",
"main_function": main_function,
"callback_success": callback_success,
"callback_fail": callback_fail,
"requires_telnet_connection": True,
"enabled": True
}
loaded_modules_dict["module_" + module_name].register_action(action_name, action_meta)

View File

@@ -0,0 +1,88 @@
from bot import loaded_modules_dict
from bot.constants import TELNET_TIMEOUT_NORMAL
from os import path, pardir
from time import sleep, time
import re
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
action_name = path.basename(path.abspath(__file__))[:-3]
def main_function(module, event_data, dispatchers_steamid=None):
# we can't save the gamestats without knowing the game-name, as each game can have different stats.
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
if active_dataset is None:
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
timeout = TELNET_TIMEOUT_NORMAL
timeout_start = time()
event_data[1]["action_identifier"] = action_name
if not module.telnet.add_telnet_command_to_queue("getgamestat"):
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
return
# Modern format: timestamps ARE present in "Executing command" lines
# Format: 2025-11-18T20:21:02 4854.528 INF Executing command 'getgamestat'...
regex = (
r"(?P<datetime>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})\s(?P<stardate>[-+]?\d*\.\d+|\d+)\s"
r"INF Executing\scommand\s\'getgamestat\'\sby\sTelnet\sfrom\s(?P<called_by>.*?)\r?\n"
r"(?P<raw_gamestats>(?:GameStat\..*?\r?\n)+)"
)
match = None
match_found = False
poll_is_finished = False
while not poll_is_finished and (time() < timeout_start + timeout):
sleep(0.25) # give the telnet a little time to respond so we have a chance to get the data at first try
match = False
for match in re.finditer(regex, module.telnet.telnet_buffer, re.MULTILINE):
poll_is_finished = True
match_found = True
if match_found:
module.callback_success(callback_success, module, event_data, dispatchers_steamid, match)
return
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
def callback_success(module, event_data, dispatchers_steamid, match=None):
regex = (
r"GameStat\.(?P<gamestat_name>.*)\s\=\s(?P<gamestat_value>.*)\s"
)
raw_gamestats = match.group("raw_gamestats")
gamestats_dict = {}
for m in re.finditer(regex, raw_gamestats, re.MULTILINE):
gamestats_dict[m.group("gamestat_name")] = m.group("gamestat_value").rstrip()
active_dataset = (
module.dom.data
.get(module.get_module_identifier())
.get("active_dataset", None)
)
module.dom.data.upsert({
module.get_module_identifier(): {
active_dataset: {
"gamestats": gamestats_dict
}
}
})
def callback_fail(module, event_data, dispatchers_steamid):
pass
action_meta = {
"description": "gets a list of all current game-stats",
"main_function": main_function,
"callback_success": callback_success,
"callback_fail": callback_fail,
"requires_telnet_connection": True,
"enabled": True
}
loaded_modules_dict["module_" + module_name].register_action(action_name, action_meta)

View File

@@ -0,0 +1,151 @@
from bot import loaded_modules_dict
from bot.constants import TELNET_TIMEOUT_NORMAL
from os import path, pardir
from time import sleep, time
import re
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
action_name = path.basename(path.abspath(__file__))[:-3]
def get_weekday_string(server_days_passed: int) -> str:
days_of_the_week = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
]
current_day_index = int(float(server_days_passed) % 7)
if 0 <= current_day_index <= 6:
return days_of_the_week[current_day_index]
else:
return ""
def is_currently_bloodmoon(module: object, day: int, hour: int = -1) -> bool:
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
next_bloodmoon_date = int(
module.dom.data
.get("module_game_environment", {})
.get(active_dataset, {})
.get("gamestats", {})
.get("BloodMoonDay", None)
)
daylight_length = int(
module.dom.data
.get("module_game_environment", {})
.get(active_dataset, {})
.get("gamestats", {})
.get("DayLightLength", None)
)
night_length = (24 - daylight_length)
morning_length = (night_length - 2)
if hour >= 0: # we want the exact bloodmoon
if next_bloodmoon_date == day and 23 >= hour >= 22:
return True
if (next_bloodmoon_date + 1) == day and 0 <= hour <= morning_length:
return True
else: # we only want the day
if next_bloodmoon_date == day:
return True
return False
def main_function(module, event_data, dispatchers_steamid=None):
timeout = TELNET_TIMEOUT_NORMAL
timeout_start = time()
event_data[1]["action_identifier"] = action_name
if not module.telnet.add_telnet_command_to_queue("gettime"):
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
return
poll_is_finished = False
# Modern format: simple "Day 447, 00:44" response
regex = r"Day\s(?P<day>\d{1,5}),\s(?P<hour>\d{1,2}):(?P<minute>\d{1,2})"
while not poll_is_finished and (time() < timeout_start + timeout):
sleep(0.25)
match = False
for match in re.finditer(regex, module.telnet.telnet_buffer):
poll_is_finished = True
if match:
module.callback_success(callback_success, module, event_data, dispatchers_steamid, match)
return
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
def callback_success(module, event_data, dispatchers_steamid, match=None):
active_dataset = (
module.dom.data
.get(module.get_module_identifier(), {})
.get("active_dataset", None)
)
# we can't save the gametime without knowing the game-name, as each game can have different stats.
if active_dataset is None:
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
matched_day = int(match.group("day"))
matched_hour = match.group("hour")
matched_minute = match.group("minute")
is_bloodmoon = is_currently_bloodmoon(module, matched_day, int(matched_hour))
is_bloodday = is_currently_bloodmoon(module, matched_day)
weekday_string = get_weekday_string(matched_day)
module.dom.data.upsert({
module.get_module_identifier(): {
active_dataset: {
"last_recorded_gametime": {
"day": matched_day,
"hour": matched_hour,
"minute": matched_minute,
"weekday": weekday_string,
"is_bloodmoon": is_bloodmoon,
"is_bloodday": is_bloodday
}
}
}
})
# Update last_recorded_servertime for webserver status widget
# Since modern 7D2D servers don't include timestamps in telnet output,
# we use system time to track when data was last received
from datetime import datetime
module.dom.data.upsert({
"module_telnet": {
"last_recorded_servertime": datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
}
})
def callback_fail(module, event_data, dispatchers_steamid):
pass
def skip_it(module, event_data, dispatchers_steamid=None):
pass
action_meta = {
"description": "gets the current gettime readout",
"main_function": main_function,
"callback_success": callback_success,
"callback_fail": callback_fail,
"skip_it": skip_it,
"requires_telnet_connection": True,
"enabled": True
}
loaded_modules_dict["module_" + module_name].register_action(action_name, action_meta)

View File

@@ -0,0 +1,102 @@
from bot import loaded_modules_dict
from bot.constants import TELNET_TIMEOUT_EXTENDED
from os import path, pardir
from time import time, sleep
import random
import re
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
action_name = path.basename(path.abspath(__file__))[:-3]
def kill_entity(module, event_data, dispatchers_steamid=None):
timeout = TELNET_TIMEOUT_EXTENDED
timeout_start = time()
event_data[1]["action_identifier"] = action_name
entity_to_be_killed = event_data[1].get("entity_id", None)
command = "kill {}".format(entity_to_be_killed)
if not module.telnet.add_telnet_command_to_queue(command):
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
return
poll_is_finished = False
# Modern format - no datetime/stardate prefix
regex = (
r"Entity\s(?P<zombie_name>.*)\s" + str(entity_to_be_killed) + r"\skilled"
)
number_of_attempts = 0
while not poll_is_finished and (time() < timeout_start + timeout):
number_of_attempts += 1
telnet_buffer_copy = (module.telnet.telnet_buffer + '.')[:-1]
for match in re.finditer(regex, telnet_buffer_copy, re.DOTALL):
return match
sleep(1)
return False
def main_function(module, event_data, dispatchers_steamid):
action = event_data[1].get("action", None)
dataset = event_data[1].get("dataset")
entity_id = event_data[1].get("entity_id")
entity_name = event_data[1].get("entity_name")
if action is not None:
if action == "kill":
match = kill_entity(module, event_data, dispatchers_steamid)
if match is not False:
if entity_name == "zombieScreamer":
module.callback_success(callback_success, module, event_data, dispatchers_steamid, match)
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
def callback_success(module, event_data, dispatchers_steamid, match=None):
action = event_data[1].get("action", None)
entity_id = event_data[1].get("entity_id")
entity_name = event_data[1].get("entity_name")
if all([
action is not None
]):
if entity_name == "zombieScreamer":
possible_maybes = [
"hopefully",
"probably dead, yes!",
"i think",
"i'm almost certain",
"yeah. definitely!!"
]
event_data = ['say_to_all', {
'message': '[CCFFCC]Screamer ([FFFFFF]{entity_id}[CCFFCC]) killed[-], [FFFFFF]{maybe}[-]...'.format(
entity_id=entity_id,
maybe=random.choice(possible_maybes)
)
}]
module.trigger_action_hook(module, event_data=event_data)
else:
event_data = ['say_to_all', {
'message': '[CCFFCC]entity ([FFFFFF]{entity_id}[CCFFCC]) killed[-]'.format(
entity_id=entity_id
)
}]
module.trigger_action_hook(module, event_data=event_data)
def callback_fail(module, event_data, dispatchers_steamid):
pass
action_meta = {
"description": "manages entity entries",
"main_function": main_function,
"callback_success": callback_success,
"callback_fail": callback_fail,
"requires_telnet_connection": False,
"enabled": True
}
loaded_modules_dict["module_" + module_name].register_action(action_name, action_meta)

View File

@@ -0,0 +1,59 @@
from bot import loaded_modules_dict, telnet_prefixes
from os import path, pardir
from time import sleep, time
import re
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
action_name = path.basename(path.abspath(__file__))[:-3]
def main_function(module, event_data, dispatchers_steamid=None):
timeout = 5 # [seconds]
timeout_start = time()
event_data[1]["action_identifier"] = action_name
message = event_data[1].get("message", None)
command = "say \"{}\"".format(message)
if not module.telnet.add_telnet_command_to_queue(command):
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
return
poll_is_finished = False
# Modern format: timestamps ARE present in "Executing command" lines
regex = (
telnet_prefixes["telnet_log"]["timestamp"] +
r"Executing\scommand\s\'" + command + r"\'\sby\sTelnet\sfrom\s(?P<called_by>.*)"
)
while not poll_is_finished and (time() < timeout_start + timeout):
sleep(0.25)
match = False
for match in re.finditer(regex, module.telnet.telnet_buffer, re.DOTALL):
poll_is_finished = True
if match:
module.callback_success(callback_success, module, event_data, dispatchers_steamid, match)
return
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
def callback_success(module, event_data, dispatchers_steamid, match=None):
pass
def callback_fail(module, event_data, dispatchers_steamid):
pass
action_meta = {
"description": "sends a message to any player",
"main_function": main_function,
"callback_success": callback_success,
"callback_fail": callback_fail,
"requires_telnet_connection": True,
"enabled": True
}
loaded_modules_dict["module_" + module_name].register_action(action_name, action_meta)

View File

@@ -0,0 +1,50 @@
from bot import loaded_modules_dict
from os import path, pardir
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
action_name = path.basename(path.abspath(__file__))[:-3]
def main_function(module, event_data, dispatchers_steamid):
shutdown_in_seconds = int(event_data[1]["shutdown_in_seconds"])
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
module.dom.data.upsert({
"module_game_environment": {
active_dataset: {
"cancel_shutdown": False,
"shutdown_in_seconds": shutdown_in_seconds,
"force_shutdown": False
}
}
})
event_data = ['say_to_all', {
'message': (
'a [FF6666]scheduled shutdown[-] is about to take place!'
'shutdown in {seconds} seconds'.format(
seconds=shutdown_in_seconds
)
)
}]
module.trigger_action_hook(module, event_data=event_data)
def callback_success(module, event_data, dispatchers_steamid, match=None):
pass
def callback_fail(module, event_data, dispatchers_steamid):
pass
action_meta = {
"description": "Sets the schedule for a shutdown",
"main_function": main_function,
"callback_success": callback_success,
"callback_fail": callback_fail,
"requires_telnet_connection": True,
"enabled": True
}
loaded_modules_dict["module_" + module_name].register_action(action_name, action_meta)

View File

@@ -0,0 +1,70 @@
from bot import loaded_modules_dict
from os import path, pardir
from time import sleep, time
import re
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
action_name = path.basename(path.abspath(__file__))[:-3]
def main_function(module, event_data, dispatchers_steamid):
timeout = 10
if not module.telnet.add_telnet_command_to_queue("shutdown"):
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
return
poll_is_finished = False
# Modern format - no datetime/stardate prefix, just look for "Disconnect"
regex = r"Disconnect.*"
timeout_start = time()
while not poll_is_finished and (time() < timeout_start + timeout):
sleep(0.5)
match = False
for match in re.finditer(regex, module.telnet.telnet_buffer):
poll_is_finished = True
if match:
module.callback_success(callback_success, module, event_data, dispatchers_steamid, match)
return
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
def callback_success(module, event_data, dispatchers_steamid, match=None):
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
module.dom.data.upsert({
"module_game_environment": {
active_dataset: {
"cancel_shutdown": False,
"shutdown_in_seconds": None,
"force_shutdown": False
}
}
})
def callback_fail(module, event_data, dispatchers_steamid):
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
module.dom.data.upsert({
"module_game_environment": {
active_dataset: {
"cancel_shutdown": False,
"shutdown_in_seconds": None,
"force_shutdown": False
}
}
})
action_meta = {
"description": "Cleanly shuts down the server",
"main_function": main_function,
"callback_success": callback_success,
"callback_fail": callback_fail,
"requires_telnet_connection": True,
"enabled": True
}
loaded_modules_dict["module_" + module_name].register_action(action_name, action_meta)

View File

@@ -0,0 +1,46 @@
from bot import loaded_modules_dict
from os import path, pardir
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
action_name = path.basename(path.abspath(__file__))[:-3]
def main_function(module, event_data, dispatchers_steamid):
action = event_data[1].get("action", None)
event_data[1]["action_identifier"] = action_name
if action == "show_options":
current_view = "options"
current_view_steamid = None
elif action == "show_frontend":
current_view = "frontend"
current_view_steamid = None
else:
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
return
module.set_current_view(dispatchers_steamid, {
"current_view": current_view,
"current_view_steamid": current_view_steamid
})
module.callback_success(callback_success, module, event_data, dispatchers_steamid)
def callback_success(module, event_data, dispatchers_steamid, match=None):
pass
def callback_fail(module, event_data, dispatchers_steamid):
pass
action_meta = {
"description": "manages entity table stuff",
"main_function": main_function,
"callback_success": callback_success,
"callback_fail": callback_fail,
"requires_telnet_connection": False,
"enabled": True
}
loaded_modules_dict["module_" + module_name].register_action(action_name, action_meta)

View File

@@ -0,0 +1,51 @@
from bot import loaded_modules_dict
from os import path, pardir
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
action_name = path.basename(path.abspath(__file__))[:-3]
def main_function(module, event_data, dispatchers_steamid=None):
event_data[1]["action_identifier"] = action_name
next_bloodmoon_date = event_data[1].get("blood_moon_date", None)
if next_bloodmoon_date is not None:
module.callback_success(callback_success, module, event_data, dispatchers_steamid)
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
def callback_success(module, event_data, dispatchers_steamid, match=None):
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
next_bloodmoon_date = event_data[1].get("blood_moon_date", None)
module.dom.data.upsert({
module.get_module_identifier(): {
active_dataset: {
"gamestats": {
"BloodMoonDay": next_bloodmoon_date
}
}
}
})
def callback_fail(module, event_data, dispatchers_steamid):
pass
def skip_it(module, event_data, dispatchers_steamid=None):
pass
action_meta = {
"description": "updates bloodmoon date",
"main_function": main_function,
"callback_success": callback_success,
"callback_fail": callback_fail,
"skip_it": skip_it,
"requires_telnet_connection": True,
"enabled": True
}
loaded_modules_dict["module_" + module_name].register_action(action_name, action_meta)

View File

@@ -0,0 +1,55 @@
from bot import loaded_modules_dict
from bot import telnet_prefixes
from os import path, pardir
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
trigger_name = path.basename(path.abspath(__file__))[:-3]
def main_function(origin_module, module, regex_result):
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
next_bloodmoon_date = (
module.dom.data
.get("module_game_environment", {})
.get(active_dataset, {})
.get("gamestats", {})
.get("BloodMoonDay", None)
)
event_data = ['say_to_all', {
'message': 'Next [FFCCCC]hordenight[FFFFFF] will be on day {day}[-]'.format(
day=next_bloodmoon_date
)
}]
module.trigger_action_hook(module, event_data=event_data)
triggers = {
"when is hordenight": r"\'(?P<player_name>.*)\'\:\s(?P<command>\/when\sis\shordenight)"
}
trigger_meta = {
"description": "tells the player when the next bloodmoon will hit",
"main_function": main_function,
"triggers": [
{
"identifier": "when is hordenight (Allocs)",
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
telnet_prefixes["Allocs"]["chat"] +
triggers["when is hordenight"]
),
"callback": main_function
},
{
"identifier": "when is hordenight (BCM)",
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
telnet_prefixes["BCM"]["chat"] +
triggers["when is hordenight"]
),
"callback": main_function
}
]
}
loaded_modules_dict["module_" + module_name].register_trigger(trigger_name, trigger_meta)

View File

@@ -0,0 +1,33 @@
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
{# set this to 'false' for debug purposes, to see the offline state, set it to 'server_is_online' or remove the line
for regular use #}
{%- set server_is_online = server_is_online -%}
{%- set online_status_string = "online" if server_is_online else "offline" -%}
<div class="{%- if server_is_online == true -%}active{%- else -%}inactive{%- endif -%}">
<span>The server is <strong>{{ online_status_string }}</strong>
{% if shutdown_in_seconds == none -%}
{{ construct_toggle_link(
server_is_online,
"shutdown", ['widget_event', ['game_environment', ['schedule_shutdown', {
"action": "schedule_shutdown",
"shutdown_in_seconds": '900'
}]]]
)}}
{%- else -%}
{{ construct_toggle_link(
server_is_online,
"cancel", ['widget_event', ['game_environment', ['cancel_shutdown', {
"action": "cancel_shutdown"
}]]]
)}}
{{ shutdown_in_seconds }} seconds to
{{ construct_toggle_link(
server_is_online,
"shutdown", ['widget_event', ['game_environment', ['force_shutdown', {
"action": "force_shutdown"
}]]]
)}}
{%- endif -%}
</span>
</div>

View File

@@ -0,0 +1,19 @@
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
{% set current_day = last_recorded_gametime["day"] %}
{% set current_hour = last_recorded_gametime["hour"] %}
{% set current_minute = last_recorded_gametime["minute"] %}
{%
set current_weekday =
"BloodDay"
if last_recorded_gametime["is_bloodday"] == true else
last_recorded_gametime["weekday"]
%}
{% set bloodmoon_modifier = "is_bloodmoon" if last_recorded_gametime["is_bloodmoon"] == true else "regular_gametime" %}
{% set bloodday_modifier = "is_bloodday" if last_recorded_gametime["is_bloodday"] == true else "regular_day" %}
<div class="{{ bloodmoon_modifier }} {{ bloodday_modifier }}">
<span>
Day <span class="day">{{ current_day }}/{{ next_bloodmoon_date }}</span>,
<span class="time">{{ current_hour }}:{{ current_minute }}</span>
({{ current_weekday }})
</span>
</div>

View File

@@ -0,0 +1,20 @@
{%- macro construct_toggle_link(bool, active_text, deactivate_event, inactive_text, activate_event) -%}
{%- set bool = bool|default(false) -%}
{%- set active_text = active_text|default(none) -%}
{%- set deactivate_event = deactivate_event|default(none) -%}
{%- set inactive_text = inactive_text|default(none) -%}
{%- set activate_event = activate_event|default(none) -%}
{%- if bool == true -%}
{%- if deactivate_event != none and activate_event != none -%}
<span class="active"><a href="#" onclick="window.socket.emit('{{ deactivate_event[0] }}', {{ deactivate_event[1] }}); return false;">{{ active_text }}</a></span>
{%- elif deactivate_event != none and activate_event == none -%}
<span class="active"><a href="#" onclick="window.socket.emit('{{ deactivate_event[0] }}', {{ deactivate_event[1] }}); return false;">{{ active_text }}</a></span>
{%- endif -%}
{%- else -%}
{%- if deactivate_event != none and activate_event != none -%}
<span class="inactive"><a href="#" onclick="window.socket.emit('{{ activate_event[0] }}', {{ activate_event[1] }}); return false;">{{ inactive_text }}</a></span>
{%- elif deactivate_event != none and activate_event == none -%}
<span class="inactive"><a href="#" onclick="window.socket.emit('{{ deactivate_event[0] }}', {{ deactivate_event[1] }}); return false;">{{ active_text }}</a></span>
{%- endif -%}
{%- endif -%}
{%- endmacro -%}

View File

@@ -0,0 +1,9 @@
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
<div>
{{ construct_toggle_link(
options_view_toggle,
"options", ['widget_event', ['game_environment', ['toggle_entities_widget_view', {'steamid': steamid, "action": "show_options"}]]],
"back", ['widget_event', ['game_environment', ['toggle_entities_widget_view', {'steamid': steamid, "action": "show_frontend"}]]]
)}}
</div>

View File

@@ -0,0 +1,3 @@
<div id="entity_table_widget_options_toggle" class="pull_out right">
{{ control_switch_options_view }}
</div>

View File

@@ -0,0 +1,7 @@
<tr>
<td colspan="8">
<div>
{{ action_delete_button }}
</div>
</td>
</tr>

View File

@@ -0,0 +1,10 @@
<tr>
<th>*</th>
<th onclick="window.sorting(this, entity_table, 1)">actions</th>
<th onclick="window.sorting(this, entity_table, 2)">name</th>
<th onclick="window.sorting(this, entity_table, 3)">type</th>
<th onclick="window.sorting(this, entity_table, 4)">pos</th>
<th onclick="window.sorting(this, entity_table, 5)">id</th>
<th onclick="window.sorting(this, entity_table, 6)">health</th>
<th onclick="window.sorting(this, entity_table, 7)" class="right">gametime</th>
</tr>

View File

@@ -0,0 +1,25 @@
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
<tr id="entity_table_row_{{ entity.dataset }}_{{ entity.id }}"{%- if css_class %} class="{{ css_class }}"{%- endif -%}>
<td>
<span id="entity_table_row_{{ entity.dataset }}_{{ entity.id }}_control_select_link" class="select_button">{{ control_select_link }}</span>
</td>
<td class="nobr" id="entity_table_row_{{ entity.dataset }}_{{ entity.id }}_actions">&nbsp;</td>
<td id="entity_table_row_{{ entity.dataset }}_{{ entity.id }}_name" onclick="$(this).selectText();">{{ entity.name }}</td>
<td id="entity_table_row_{{ entity.dataset }}_{{ entity.id }}_type" onclick="$(this).selectText();">{{ entity.type }}</td>
<td class="position right" onclick="$(this).selectText();">
<span id="entity_table_row_{{ entity.dataset }}_{{ entity.id }}_pos_x">
{{ ((entity | default({})).pos | default({}) ).x | default('0') }}
</span>
<span id="entity_table_row_{{ entity.dataset }}_{{ entity.id }}_pos_y">
{{ ((entity | default({})).pos | default({}) ).y | default('0') }}
</span>
<span id="entity_table_row_{{ entity.dataset }}_{{ entity.id }}_pos_z">
{{ ((entity | default({})).pos | default({}) ).z | default('0') }}
</span>
</td>
<td id="entity_table_row_{{ entity.dataset }}_{{ entity.id }}_id" onclick="$(this).selectText();">{{ entity.id }}</td>
<td class="center" id="entity_table_row_{{ entity.dataset }}_{{ entity.id }}_health">{{ entity.health }}</td>
<td class="nobr right" id="entity_table_row_{{ entity.dataset }}_{{ entity.id }}_last_seen_gametime">
{{ entity.last_seen_gametime }}
</td>
</tr>

View File

@@ -0,0 +1,29 @@
<header>
<div>
<span>Entities</span>
</div>
</header>
<aside>
{{ options_toggle }}
</aside>
<main>
<table class="data_table">
<caption>
<span>obey</span>
</caption>
<thead>
{{ table_header }}
</thead>
<tbody id="entity_table">
{{ table_rows }}
</tbody>
<tfoot>
{{ table_footer }}
</tfoot>
</table>
<div class="dialog">
<div id="manage_entities_widget_modal" class="modal-content">
<p>this is the text inside the modal</p>
</div>
</div>
</main>

View File

@@ -0,0 +1,27 @@
<header>
<div>
<span>Entities</span>
</div>
</header>
<aside>
{{ options_toggle }}
</aside>
<main>
<table>
<thead>
<tr>
<th colspan="2">Entity Module Options</th>
</tr>
</thead>
<tbody>
<tr>
<th colspan="2"><span>widget-options</span></th>
</tr>
{% for key, value in widget_options.items() %}
<tr>
<td><span>{{key}}</span></td><td>{{value}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</main>

View File

@@ -0,0 +1,35 @@
from bot import loaded_modules_dict
from bot import telnet_prefixes
from os import path, pardir
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
trigger_name = path.basename(path.abspath(__file__))[:-3]
def main_function(origin_module, module, regex_result):
next_blood_moon = regex_result.group("next_BloodMoon")
event_data = ['update_bloodmoon_date', {
'blood_moon_date': next_blood_moon,
}]
module.trigger_action_hook(origin_module, event_data=event_data)
triggers = {
"BloodMoon SetDay": r"BloodMoon\sSetDay:\sday\s(?P<next_BloodMoon>\d+)"
}
trigger_meta = {
"description": "reacts to updated BloodMoon date in the telnet-stream",
"main_function": main_function,
"triggers": [
{
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
triggers["BloodMoon SetDay"]
),
"callback": main_function
}
]
}
loaded_modules_dict["module_" + module_name].register_trigger(trigger_name, trigger_meta)

View File

@@ -0,0 +1,33 @@
from bot import loaded_modules_dict
from bot import telnet_prefixes
from bot.logger import get_logger
from os import path, pardir
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
trigger_name = path.basename(path.abspath(__file__))[:-3]
logger = get_logger("game_environment.shutdown_handler")
def main_function(*args, **kwargs):
module = args[0]
updated_values_dict = kwargs.get("updated_values_dict", {})
cancel_shutdown = updated_values_dict.get("cancel_shutdown", None)
force_shutdown = updated_values_dict.get("force_shutdown", None)
if cancel_shutdown:
logger.info("shutdown_cancelled")
if force_shutdown:
logger.info("shutdown_forced")
trigger_meta = {
"description": "reacts to changes in the shutdown procedure",
"main_function": main_function,
"handlers": {
"module_game_environment/%map_identifier%/cancel_shutdown": main_function,
"module_game_environment/%map_identifier%/force_shutdown": main_function
}
}
loaded_modules_dict["module_" + module_name].register_trigger(trigger_name, trigger_meta)

View File

@@ -0,0 +1,88 @@
from bot import loaded_modules_dict
from os import path, pardir
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
widget_name = path.basename(path.abspath(__file__))[:-3]
def main_widget(*args, **kwargs):
module = args[0]
dispatchers_steamid = kwargs.get("dispatchers_steamid", None)
template_frontend = module.templates.get_template('gameserver_status_widget/view_frontend.html')
server_is_online = module.dom.data.get("module_telnet", {}).get("server_is_online", True)
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
shutdown_in_seconds = (
module.dom.data
.get("module_game_environment", {})
.get(active_dataset, {})
.get("shutdown_in_seconds", None)
)
data_to_emit = module.template_render_hook(
module,
template=template_frontend,
server_is_online=server_is_online,
shutdown_in_seconds=shutdown_in_seconds
)
module.webserver.send_data_to_client_hook(
module,
payload=data_to_emit,
data_type="widget_content",
clients=[dispatchers_steamid],
target_element={
"id": "gameserver_status_widget",
"type": "div",
"selector": "body > header > div > div"
}
)
def update_widget(*args, **kwargs):
module = args[0]
updated_values_dict = kwargs.get("updated_values_dict", None)
template_frontend = module.templates.get_template('gameserver_status_widget/view_frontend.html')
server_is_online = module.dom.get_updated_or_default_value(
"module_telnet", "server_is_online", updated_values_dict, True
)
shutdown_in_seconds = module.dom.get_updated_or_default_value(
"module_game_environment", "shutdown_in_seconds", updated_values_dict, None
)
data_to_emit = module.template_render_hook(
module,
template=template_frontend,
server_is_online=server_is_online,
shutdown_in_seconds=shutdown_in_seconds
)
module.webserver.send_data_to_client_hook(
module,
payload=data_to_emit,
data_type="widget_content",
clients=module.webserver.connected_clients.keys(),
target_element={
"id": "gameserver_status_widget",
"type": "div",
"selector": "body > header > div > div"
}
)
widget_meta = {
"description": "shows gameserver status, shut it down. or don't ^^",
"main_widget": main_widget,
"handlers": {
"module_telnet/server_is_online": update_widget,
"module_game_environment/%map_identifier%/shutdown_in_seconds": update_widget,
"module_game_environment/%map_identifier%/cancel_shutdown": update_widget,
"module_game_environment/%map_identifier%/force_shutdown": update_widget
},
"enabled": True
}
loaded_modules_dict["module_" + module_name].register_widget(widget_name, widget_meta)

View File

@@ -0,0 +1,103 @@
from bot import loaded_modules_dict
from os import path, pardir
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
widget_name = path.basename(path.abspath(__file__))[:-3]
def main_widget(*args, **kwargs):
module = args[0]
dispatchers_steamid = kwargs.get("dispatchers_steamid", None)
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
template_frontend = module.templates.get_template('gametime_widget/view_frontend.html')
gametime = module.game_environment.get_last_recorded_gametime_dict()
gametime.update({
"is_bloodmoon": "",
"is_bloodday": ""
})
next_bloodmoon_date = (
module.dom.data
.get("module_game_environment", {})
.get(active_dataset, {})
.get("gamestats", {})
.get("BloodMoonDay", None)
)
data_to_emit = module.template_render_hook(
module,
template=template_frontend,
last_recorded_gametime=gametime,
next_bloodmoon_date=next_bloodmoon_date
)
module.webserver.send_data_to_client_hook(
module,
payload=data_to_emit,
data_type="widget_content",
clients=[dispatchers_steamid],
target_element={
"id": "gametime_widget",
"type": "div",
"selector": "body > header > div > div"
}
)
def update_widget(*args, **kwargs):
module = args[0]
updated_values_dict = kwargs.get("updated_values_dict", None)
original_values_dict = kwargs.get("original_values_dict", None)
gametime = updated_values_dict.get("last_recorded_gametime", None)
old_gametime = original_values_dict.get("last_recorded_gametime", None)
if gametime is None:
module.trigger_action_hook(module, event_data=["gettime", {}])
return False
if gametime == old_gametime:
pass
# return
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
next_bloodmoon_date = (
module.dom.data
.get("module_game_environment", {})
.get(active_dataset, {})
.get("gamestats", {})
.get("BloodMoonDay", None)
)
template_frontend = module.templates.get_template('gametime_widget/view_frontend.html')
data_to_emit = module.template_render_hook(
module,
template=template_frontend,
last_recorded_gametime=gametime,
next_bloodmoon_date=next_bloodmoon_date
)
module.webserver.send_data_to_client_hook(
module,
payload=data_to_emit,
data_type="widget_content",
clients=module.webserver.connected_clients.keys(),
target_element={
"id": "gametime_widget",
"type": "div",
"selector": "body > header > div > div"
}
)
return gametime
widget_meta = {
"description": "displays the in-game time and day",
"main_widget": main_widget,
"handlers": {
"module_game_environment/%map_identifier%/last_recorded_gametime": update_widget
},
"enabled": True
}
loaded_modules_dict["module_" + module_name].register_widget(widget_name, widget_meta)

View File

@@ -0,0 +1,94 @@
from bot import loaded_modules_dict
from os import path, pardir
from bot.logger import get_logger
logger = get_logger(__name__)
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
widget_name = path.basename(path.abspath(__file__))[:-3]
def announce_location_change(*args, **kwargs):
"""
Handler that announces location edits in the map chat.
This demonstrates that the callback_dict system works - multiple modules can
subscribe to the same DOM path changes without modifying the locations module.
"""
module = args[0]
method = kwargs.get("method", None)
updated_values_dict = kwargs.get("updated_values_dict", {})
# DEBUG: Log what we received
logger.info(
"location_change_debug",
method=method,
updated_values_dict_type=type(updated_values_dict).__name__,
updated_values_dict_repr=repr(updated_values_dict)[:200]
)
# Only announce on update, not insert or remove
if method != "update":
return
if not isinstance(updated_values_dict, dict):
return
# Get active dataset (map identifier)
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
if active_dataset is None:
return
# updated_values_dict structure at this depth (callback on depth 4):
# {location_identifier: {location_data}}
# location_data includes "owner" field
for location_identifier, location_dict in updated_values_dict.items():
if not isinstance(location_dict, dict):
continue
# Get owner directly from location_dict
owner_steamid = location_dict.get("owner")
if owner_steamid is None:
logger.warning(
"location_owner_missing",
location_identifier=location_identifier
)
continue
# Get player name from DOM
player_name = (
module.dom.data
.get("module_players", {})
.get("elements", {})
.get(active_dataset, {})
.get(owner_steamid, {})
.get("name", "Unknown Player")
)
# Get location name
location_name = location_dict.get("name", location_identifier)
# Send chat message via say_to_all
event_data = ['say_to_all', {
'message': (
'[FFAA00]Location Update:[-] {player} edited location [00FFFF]{location}[-]'
.format(
player=player_name,
location=location_name
)
)
}]
module.trigger_action_hook(module, event_data=event_data)
widget_meta = {
"description": "Announces location changes in map chat (test for callback_dict system)",
"main_widget": None, # No UI widget, just a handler
"handlers": {
# Subscribe to location changes - any module can do this!
"module_locations/elements/%map_identifier%/%owner_steamid%/%element_identifier%": announce_location_change
},
"enabled": True
}
loaded_modules_dict["module_" + module_name].register_widget(widget_name, widget_meta)

View File

@@ -0,0 +1,406 @@
from bot import loaded_modules_dict
from os import path, pardir
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
widget_name = path.basename(path.abspath(__file__))[:-3]
def get_entity_table_row_css_class(entity_dict):
css_classes = []
return " ".join(css_classes)
def select_view(*args, **kwargs):
module = args[0]
dispatchers_steamid = kwargs.get('dispatchers_steamid', None)
current_view = module.get_current_view(dispatchers_steamid)
if current_view == "options":
options_view(module, dispatchers_steamid=dispatchers_steamid)
elif current_view == "delete-modal":
frontend_view(module, dispatchers_steamid=dispatchers_steamid)
delete_modal_view(module, dispatchers_steamid=dispatchers_steamid)
else:
frontend_view(module, dispatchers_steamid=dispatchers_steamid)
def delete_modal_view(*args, **kwargs):
module = args[0]
dispatchers_steamid = kwargs.get('dispatchers_steamid', None)
all_available_entity_dicts = module.dom.data.get(module.get_module_identifier(), {}).get("elements", {})
all_selected_elements_count = 0
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
for map_identifier, entity_dicts in all_available_entity_dicts.items():
if active_dataset == map_identifier:
for entity_id, entity_dict in entity_dicts.items():
entity_is_selected_by = entity_dict.get("selected_by", [])
if dispatchers_steamid in entity_is_selected_by:
all_selected_elements_count += 1
modal_confirm_delete = module.dom_management.get_delete_confirm_modal(
module,
count=all_selected_elements_count,
target_module="module_game_environment",
dom_element_id="entity_table_modal_action_delete_button",
dom_action="delete_selected_dom_elements",
dom_element_root=module.dom_element_root,
dom_element_select_root=module.dom_element_select_root,
confirmed="True"
)
data_to_emit = modal_confirm_delete
module.webserver.send_data_to_client_hook(
module,
payload=data_to_emit,
data_type="modal_content",
clients=[dispatchers_steamid],
target_element={
"id": "manage_entities_widget_modal",
"type": "div",
"selector": "body > main > div"
}
)
def frontend_view(*args, **kwargs):
module = args[0]
dispatchers_steamid = kwargs.get('dispatchers_steamid', None)
template_frontend = module.templates.get_template('manage_entities_widget/view_frontend.html')
template_table_rows = module.templates.get_template('manage_entities_widget/table_row.html')
template_table_header = module.templates.get_template('manage_entities_widget/table_header.html')
template_table_footer = module.templates.get_template('manage_entities_widget/table_footer.html')
template_options_toggle = module.templates.get_template('manage_entities_widget/control_switch_view.html')
template_options_toggle_view = module.templates.get_template('manage_entities_widget/control_switch_options_view.html')
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
all_available_entity_dicts = module.dom.data.get(module.get_module_identifier(), {}).get("elements", {})
table_rows = ""
all_selected_elements_count = 0
for map_identifier, entity_dicts in all_available_entity_dicts.items():
if active_dataset == map_identifier:
for entity_id, entity_dict in entity_dicts.items():
entity_is_selected_by = entity_dict.get("selected_by", [])
entity_entry_selected = False
if dispatchers_steamid in entity_is_selected_by:
entity_entry_selected = True
all_selected_elements_count += 1
control_select_link = module.dom_management.get_selection_dom_element(
module,
target_module="module_game_environment",
dom_element_select_root=["selected_by"],
dom_element=entity_dict,
dom_element_entry_selected=entity_entry_selected,
dom_action_inactive="select_dom_element",
dom_action_active="deselect_dom_element"
)
# Sanitize dataset for HTML ID (replace spaces with underscores, lowercase)
sanitized_dataset = module.dom_management.sanitize_for_html_id(entity_dict.get("dataset", ""))
sanitized_entity_id = str(entity_id)
# Update entity_dict with sanitized values for template
entity_dict_for_template = entity_dict.copy()
entity_dict_for_template["dataset"] = sanitized_dataset
entity_dict_for_template["dataset_original"] = entity_dict.get("dataset", "")
table_rows += module.template_render_hook(
module,
template=template_table_rows,
entity=entity_dict_for_template,
css_class=get_entity_table_row_css_class(entity_dict),
control_select_link=control_select_link
)
current_view = module.get_current_view(dispatchers_steamid)
options_toggle = module.template_render_hook(
module,
template=template_options_toggle,
control_switch_options_view=module.template_render_hook(
module,
template=template_options_toggle_view,
options_view_toggle=(current_view in ["frontend", "delete-modal"]),
steamid=dispatchers_steamid
)
)
dom_element_delete_button = module.dom_management.get_delete_button_dom_element(
module,
count=all_selected_elements_count,
target_module="module_game_environment",
dom_element_id="entity_table_widget_action_delete_button",
dom_action="delete_selected_dom_elements",
dom_element_root=module.dom_element_root,
dom_element_select_root=module.dom_element_select_root
)
data_to_emit = module.template_render_hook(
module,
template=template_frontend,
options_toggle=options_toggle,
table_header=module.template_render_hook(
module,
template=template_table_header
),
table_rows=table_rows,
table_footer=module.template_render_hook(
module,
template=template_table_footer,
action_delete_button=dom_element_delete_button
)
)
module.webserver.send_data_to_client_hook(
module,
payload=data_to_emit,
data_type="widget_content",
clients=[dispatchers_steamid],
method="update",
target_element={
"id": "manage_entities_widget",
"type": "table",
"selector": "body > main > div"
}
)
def options_view(*args, **kwargs):
module = args[0]
dispatchers_steamid = kwargs.get('dispatchers_steamid', None)
template_frontend = module.templates.get_template('manage_entities_widget/view_options.html')
template_options_toggle = module.templates.get_template('manage_entities_widget/control_switch_view.html')
template_options_toggle_view = module.templates.get_template('manage_entities_widget/control_switch_options_view.html')
options_toggle = module.template_render_hook(
module,
template=template_options_toggle,
control_switch_options_view=module.template_render_hook(
module,
template=template_options_toggle_view,
options_view_toggle=False,
steamid=dispatchers_steamid
)
)
data_to_emit = module.template_render_hook(
module,
template=template_frontend,
options_toggle=options_toggle,
widget_options=module.options
)
module.webserver.send_data_to_client_hook(
module,
payload=data_to_emit,
data_type="widget_content",
clients=[dispatchers_steamid],
method="update",
target_element={
"id": "manage_entities_widget",
"type": "table",
"selector": "body > main > div"
}
)
def table_rows(*args, ** kwargs):
module = args[0]
updated_values_dict = kwargs.get("updated_values_dict", None)
method = kwargs.get("method", None)
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
if method in ["upsert", "edit", "insert"]:
for clientid in module.webserver.connected_clients.keys():
current_view = module.get_current_view(clientid)
if current_view == "frontend":
template_table_rows = module.templates.get_template('manage_entities_widget/table_row.html')
for entity_id, entity_dict in updated_values_dict.items():
try:
# Sanitize dataset for HTML ID (replace spaces with underscores, lowercase)
sanitized_dataset = module.dom_management.sanitize_for_html_id(entity_dict["dataset"])
table_row_id = "entity_table_row_{}_{}".format(
sanitized_dataset,
str(entity_id)
)
# Update entity_dict with sanitized dataset for template
entity_dict = entity_dict.copy()
entity_dict["dataset"] = sanitized_dataset
entity_dict["dataset_original"] = updated_values_dict[entity_id].get("dataset", "")
except KeyError:
table_row_id = "manage_entities_widget"
selected_entity_entries = (
module.dom.data
.get("module_game_environment", {})
.get("elements", {})
.get(active_dataset, {})
.get(entity_id, {})
.get("selected_by", [])
)
entity_entry_selected = False
if clientid in selected_entity_entries:
entity_entry_selected = True
control_select_link = module.dom_management.get_selection_dom_element(
module,
target_module="module_game_environment",
dom_element_select_root=["selected_by"],
dom_element=entity_dict,
dom_element_entry_selected=entity_entry_selected,
dom_action_inactive="select_dom_element",
dom_action_active="deselect_dom_element"
)
table_row = module.template_render_hook(
module,
template=template_table_rows,
entity=entity_dict,
css_class=get_entity_table_row_css_class(entity_dict),
control_select_link=control_select_link
)
module.webserver.send_data_to_client_hook(
module,
payload=table_row,
data_type="table_row",
clients=[clientid],
target_element={
"id": table_row_id,
"type": "tr",
"class": get_entity_table_row_css_class(entity_dict),
"selector": "body > main > div > div#manage_entities_widget > main > table > tbody"
}
)
elif method == "remove":
entity_origin = updated_values_dict[2]
entity_id = updated_values_dict[3]
# Sanitize dataset for HTML ID (replace spaces with underscores, lowercase)
sanitized_origin = module.dom_management.sanitize_for_html_id(entity_origin)
module.webserver.send_data_to_client_hook(
module,
data_type="remove_table_row",
clients="all",
target_element={
"id": "entity_table_row_{}_{}".format(
sanitized_origin,
str(entity_id)
)
}
)
update_delete_button_status(module, *args, **kwargs)
def update_widget(*args, **kwargs):
module = args[0]
updated_values_dict = kwargs.get("updated_values_dict", None)
method = kwargs.get("method", None)
if method in ["update"]:
entity_dict = updated_values_dict
player_clients_to_update = list(module.webserver.connected_clients.keys())
for clientid in player_clients_to_update:
try:
current_view = module.get_current_view(clientid)
# Sanitize dataset for HTML ID (replace spaces with underscores, lowercase)
sanitized_dataset = module.dom_management.sanitize_for_html_id(entity_dict.get("dataset", ""))
table_row_id = "entity_table_row_{}_{}".format(
sanitized_dataset,
str(entity_dict.get("id", None))
)
# Update entity_dict with sanitized dataset
original_dataset = entity_dict.get("dataset", "")
entity_dict = entity_dict.copy()
entity_dict["dataset"] = sanitized_dataset
entity_dict["dataset_original"] = original_dataset
if current_view == "frontend":
module.webserver.send_data_to_client_hook(
module,
payload=entity_dict,
data_type="table_row_content",
clients="all",
method="update",
target_element={
"id": table_row_id,
"parent_id": "manage_entities_widget",
"module": "game_environment",
"type": "tr",
"selector": "body > main > div > div#manage_entities_widget",
"class": get_entity_table_row_css_class(entity_dict),
}
)
except AttributeError as error:
pass
except KeyError as error:
pass
def update_selection_status(*args, **kwargs):
module = args[0]
updated_values_dict = kwargs.get("updated_values_dict", None)
# Sanitize dataset for HTML ID (replace spaces with underscores, lowercase)
sanitized_dataset = module.dom_management.sanitize_for_html_id(updated_values_dict["dataset"])
module.dom_management.update_selection_status(
*args, **kwargs,
target_module=module,
dom_action_active="deselect_dom_element",
dom_action_inactive="select_dom_element",
dom_element_id={
"id": "entity_table_row_{}_{}_control_select_link".format(
sanitized_dataset,
updated_values_dict["identifier"]
)
}
)
update_delete_button_status(module, *args, **kwargs)
def update_delete_button_status(*args, **kwargs):
module = args[0]
module.dom_management.update_delete_button_status(
*args, **kwargs,
dom_element_root=module.dom_element_root,
dom_element_select_root=module.dom_element_select_root,
target_module=module,
dom_action="delete_selected_dom_elements",
dom_element_id={
"id": "entity_table_widget_action_delete_button"
}
)
widget_meta = {
"description": "sends and updates a table of all currently known entities",
"main_widget": select_view,
"handlers": {
"module_game_environment/visibility/%steamid%/current_view":
select_view,
"module_game_environment/elements/%map_identifier%/%id%":
table_rows,
"module_game_environment/elements/%map_identifier%/%id%/pos":
update_widget,
"module_game_environment/elements/%map_identifier%/%id%/selected_by":
update_selection_status,
},
"enabled": True
}
loaded_modules_dict["module_" + module_name].register_widget(widget_name, widget_meta)