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,67 @@
from bot.module import Module
from bot import loaded_modules_dict
from time import time
class Players(Module):
dom_element_root = list
dom_element_select_root = list
def __init__(self):
setattr(self, "default_options", {
"module_name": self.get_module_identifier()[7:],
"run_observer_interval": 3,
"dom_element_root": [],
"dom_element_select_root": ["selected_by"]
})
setattr(self, "required_modules", [
"module_webserver",
"module_dom",
"module_dom_management",
"module_game_environment",
"module_telnet"
])
self.next_cycle = 0
Module.__init__(self)
@staticmethod
def get_module_identifier():
return "module_players"
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.run_observer_interval = self.options.get(
"run_observer_interval", self.default_options.get("run_observer_interval", None)
)
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)
)
# endregion
def run(self):
while not self.stopped.wait(self.next_cycle):
profile_start = time()
self.trigger_action_hook(self, event_data=["getadmins", {
"disable_after_success": True
}])
self.trigger_action_hook(self, event_data=["getplayers", {}])
self.last_execution_time = time() - profile_start
self.next_cycle = self.run_observer_interval - self.last_execution_time
loaded_modules_dict[Players().get_module_identifier()] = Players()

View File

@@ -0,0 +1,79 @@
from bot import loaded_modules_dict
from bot.constants import TELNET_TIMEOUT_VERY_SHORT
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_VERY_SHORT
timeout_start = time()
event_data[1]["action_identifier"] = action_name
event_data[1]["fail_reason"] = []
if module.telnet.add_telnet_command_to_queue("admin list"):
poll_is_finished = False
# Updated regex for modern 7D2D server format (V 2.x+)
# New format: "Defined User Permissions:" and SteamIDs have "Steam_" prefix
# Timestamps are still present in "Executing command" lines
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\'admin list\'\sby\sTelnet\sfrom\s(?P<called_by>.*?)\r?\n"
r"(?P<raw_adminlist>Defined User Permissions:[\s\S]*?(?=Defined Group Permissions:))"
)
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
event_data[1]["fail_reason"].append("action timed out")
else:
event_data[1]["fail_reason"].append("action already queued up")
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
def callback_success(module, event_data, dispatchers_steamid, match=None):
# Updated regex for modern format with "Steam_" prefix
# Example: " 0: Steam_76561198040658370 (stored name: MOTKU)"
regex = (
r"(?:^\s{0,7})(?P<level>\d{1,2})\:\s+Steam_(?P<steamid>\d{17})"
)
raw_adminlist = match.group("raw_adminlist")
admin_dict = {}
for m in re.finditer(regex, raw_adminlist, re.MULTILINE):
admin_dict[m.group("steamid")] = m.group("level")
module.dom.data.upsert({
module.get_module_identifier(): {
"admins": admin_dict
}
})
disable_after_success = event_data[1]["disable_after_success"]
if disable_after_success:
module.disable_action(action_name)
def callback_fail(module, event_data, dispatchers_steamid):
pass
action_meta = {
"description": "gets a list of all admins and mods",
"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,246 @@
from bot import loaded_modules_dict
from bot.constants import TELNET_TIMEOUT_SHORT
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 _set_players_offline(players_dict):
"""
Helper function to mark all players in a dictionary as offline.
Creates a new dictionary with all players set to is_online=False and
is_initialized=False. This is used when telnet commands fail or timeout.
Args:
players_dict: Dictionary of player data keyed by steam_id
Returns:
Dictionary with same players but marked as offline
"""
modified_players = {}
for steam_id, player_data in players_dict.items():
# Create a copy of the player dict
updated_player = player_data.copy()
updated_player["is_online"] = False
updated_player["is_initialized"] = False
modified_players[steam_id] = updated_player
return modified_players
def main_function(module, event_data, dispatchers_steamid=None):
timeout = TELNET_TIMEOUT_SHORT
timeout_start = time()
event_data[1]["action_identifier"] = action_name
event_data[1]["fail_reason"] = []
if module.telnet.add_telnet_command_to_queue("lp"):
poll_is_finished = False
# Modern format - matches both empty and populated player lists
regex = (
r"Executing\scommand\s\'lp\'\sby\sTelnet\sfrom\s"
r"(?P<called_by>.*?)\r?\n"
r"(?P<raw_playerdata>[\s\S]*?)"
r"Total\sof\s(?P<player_count>\d{1,2})\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
event_data[1]["fail_reason"].append("timed out waiting for response")
else:
event_data[1]["fail_reason"].append("action already queued up")
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
def callback_success(module, event_data, dispatchers_steamid, match=None):
""" without a place to store this, why bother """
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
player_count = int(match.group("player_count"))
if all([
active_dataset is None,
player_count <= 0
]):
return False
""" get some basic stuff needed later """
last_seen_gametime_string = module.game_environment.get_last_recorded_gametime_string()
""" lets extract all data the game provides!! """
# Note: Modern regex doesn't capture datetime, using current time instead
from datetime import datetime
telnet_datetime = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
raw_playerdata = match.group("raw_playerdata").lstrip()
# Modern 7D2D format includes pltfmid (platform ID) and crossid (Epic cross-platform ID)
# Format: pltfmid=Steam_76561198040658370, crossid=EOS_..., ip=..., ping=...
regex = (
r"\d{1,2}\. id=(?P<id>\d+), (?P<name>[^,]+), "
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"remote=(?P<remote>\w+), "
r"health=(?P<health>\d+), "
r"deaths=(?P<deaths>\d+), "
r"zombies=(?P<zombies>\d+), "
r"players=(?P<players>\d+), "
r"score=(?P<score>\d+), "
r"level=(?P<level>\d+), "
r"pltfmid=Steam_(?P<steamid>\d+), crossid=(?P<crossid>[\w_]+), "
r"ip=(?P<ip>[^,]+), "
r"ping=(?P<ping>\d+)"
r"\r\n"
)
all_players_dict = (
module.dom.data.get(module.get_module_identifier(), {})
.get("elements", {})
.get(active_dataset, {})
)
players_to_update_dict = {}
for m in re.finditer(regex, raw_playerdata):
in_limbo = int(m.group("health")) == 0
player_dict = {
# data the game provides
"id": m.group("id"),
"name": str(m.group("name")),
"remote": bool(m.group("remote")),
"health": int(m.group("health")),
"deaths": int(m.group("deaths")),
"zombies": int(m.group("zombies")),
"players": int(m.group("players")),
"score": int(m.group("score")),
"level": int(m.group("level")),
"steamid": m.group("steamid"),
"ip": str(m.group("ip")),
"ping": int(float(m.group("ping"))),
"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")))
},
# data invented by the bot
"dataset": active_dataset,
"in_limbo": in_limbo,
"is_online": True,
"is_initialized": True,
"last_updated_servertime": telnet_datetime,
"last_seen_gametime": last_seen_gametime_string,
"owner": m.group("steamid")
}
players_to_update_dict[m.group("steamid")] = player_dict
""" players_to_update_dict now holds all game-data for all online players plus a few generated ones like last seen
and is_initialized. Otherwise it's empty """
# set all players not currently online to offline
online_players_list = list(players_to_update_dict.keys())
for steamid, existing_player_dict in all_players_dict.items():
if existing_player_dict["is_initialized"] is False:
continue
if steamid not in online_players_list and existing_player_dict["is_online"] is True:
# Create offline version of player using copy
updated_player = existing_player_dict.copy()
updated_player["is_online"] = False
updated_player["is_initialized"] = False
players_to_update_dict[steamid] = updated_player
if len(players_to_update_dict) >= 1:
module.dom.data.upsert({
module.get_module_identifier(): {
"elements": {
active_dataset: players_to_update_dict
}
}
})
if online_players_list != module.dom.data.get(module.get_module_identifier(), {}).get("online_players"):
module.dom.data.upsert({
module.get_module_identifier(): {
"online_players": online_players_list
}
})
def callback_fail(module, event_data, dispatchers_steamid):
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
if active_dataset is None:
return
all_existing_players_dict = (
module.dom.data
.get(module.get_module_identifier(), {})
.get("elements", {})
.get(active_dataset, {})
)
# Mark all existing players as offline using helper function
all_modified_players_dict = _set_players_offline(all_existing_players_dict)
module.dom.data.upsert({
module.get_module_identifier(): {
"elements": {
active_dataset: all_modified_players_dict
}
}
})
module.dom.data.upsert({
module.get_module_identifier(): {
"online_players": []
}
})
def skip_it(module, event_data, dispatchers_steamid=None):
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
if active_dataset is None:
return
all_existing_players_dict = (
module.dom.data
.get(module.get_module_identifier(), {})
.get("elements", {})
.get(active_dataset, {})
)
# Mark all existing players as offline using helper function
all_modified_players_dict = _set_players_offline(all_existing_players_dict)
module.dom.data.upsert({
module.get_module_identifier(): {
"elements": {
active_dataset: all_modified_players_dict
}
}
})
action_meta = {
"description": "gets a list of all currently logged in players and sets status-flags",
"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,107 @@
from bot import loaded_modules_dict
from bot import telnet_prefixes
from bot.logger import get_logger
from os import path, pardir
from time import time, sleep
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("players.kick_player")
def main_function(module, event_data, dispatchers_steamid):
action = event_data[1].get("action", None)
event_data[1]["action_identifier"] = action_name
action_is_confirmed = event_data[1].get("confirmed", "False")
player_to_be_kicked = event_data[1].get("steamid", None)
if action == "kick_player":
if action_is_confirmed == "True":
timeout = 5 # [seconds]
timeout_start = time()
reason = event_data[1].get("reason")
# Get player entity ID - game requires entity ID instead of steamid
dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
player_dict = (
module.dom.data
.get("module_players", {})
.get("elements", {})
.get(dataset, {})
.get(player_to_be_kicked, {})
)
player_entity_id = player_dict.get("id")
if not player_entity_id:
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
return
command = "kick {} \"{}\"".format(player_entity_id, reason)
"""
i was trying re.escape, string replacements... the only thing that seems to work is all of them together
Had some big trouble filtering out stuff like ^ and " and whatnot
"""
regex = (
telnet_prefixes["telnet_log"]["timestamp"] +
r"Executing\scommand\s\'" + re.escape(command) + r"\'\s"
r"by\sTelnet\s"
r"from\s(?P<called_by>.*)"
).replace('"', '\\"')
logger.debug("kick_command_prepared",
command=command,
user=dispatchers_steamid,
target=player_to_be_kicked,
reason=reason)
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
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
else:
module.set_current_view(dispatchers_steamid, {
"current_view": "kick-modal",
"current_view_steamid": player_to_be_kicked
})
return
elif action == "cancel_kick_player":
module.callback_success(callback_success, module, event_data, dispatchers_steamid)
return
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
def callback_success(module, event_data, dispatchers_steamid, match=None):
module.set_current_view(dispatchers_steamid, {
"current_view": "frontend",
})
def callback_fail(module, event_data, dispatchers_steamid):
pass
action_meta = {
"description": "kicks a 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,74 @@
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
target_player_steamid = event_data[1].get("steamid", None)
message = event_data[1].get("message", None)
# Get player entity ID - game requires entity ID instead of steamid
dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
player_dict = (
module.dom.data
.get("module_players", {})
.get("elements", {})
.get(dataset, {})
.get(target_player_steamid, {})
)
player_entity_id = player_dict.get("id")
if not player_entity_id:
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
return
command = "sayplayer {} \"{}\"".format(player_entity_id, 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,114 @@
from bot import loaded_modules_dict
from bot import telnet_prefixes
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("players.teleport_player")
def main_function(module, event_data, dispatchers_steamid=None):
timeout = 6 # [seconds]
timeout_start = time()
event_data[1]["action_identifier"] = action_name
target_coordinates = event_data[1].get("coordinates", None)
player_to_be_teleported_steamid = event_data[1].get("steamid", None)
dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
player_to_be_teleported_dict = (
module.dom.data
.get("module_players", {})
.get("elements", {})
.get(dataset, {})
.get(player_to_be_teleported_steamid, {})
)
player_coordinates = player_to_be_teleported_dict.get("pos", None)
if all([
dataset is not None,
target_coordinates is not None,
player_coordinates is not None
]) and all([
# no sense in porting a player to a place they are already standing on ^^
target_coordinates != player_coordinates
]):
# Use entity ID instead of steamid - game requires entity ID now
player_entity_id = player_to_be_teleported_dict.get("id")
if not player_entity_id:
event_data[1]["fail_reason"] = "player entity ID not found"
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
return
command = (
"teleportplayer {player_to_be_teleported} {pos_x} {pos_y} {pos_z}"
).format(
player_to_be_teleported=player_entity_id,
pos_x=int(float(target_coordinates["x"])),
pos_y=int(float(target_coordinates["y"])),
pos_z=int(float(target_coordinates["z"]))
)
if not module.telnet.add_telnet_command_to_queue(command):
event_data[1]["fail_reason"] = "duplicate command"
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
return
poll_is_finished = False
regex = (
telnet_prefixes["telnet_log"]["timestamp"] +
r"PlayerSpawnedInWorld\s"
r"\("
r"reason: (?P<command>.+?),\s"
r"position: (?P<pos_x>.*),\s(?P<pos_y>.*),\s(?P<pos_z>.*)"
r"\):\s"
r"EntityID={entity_id},\s".format(entity_id=player_to_be_teleported_dict.get("id")) +
r"PlayerID='{player_to_be_teleported}',\s".format(player_to_be_teleported=player_to_be_teleported_steamid) +
r"OwnerID='{player_to_be_teleported}',\s".format(player_to_be_teleported=player_to_be_teleported_steamid) +
r"PlayerName='(?P<player_name>.*)'"
)
while not poll_is_finished and (time() < timeout_start + timeout):
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
sleep(0.25)
event_data[1]["fail_reason"] = "action timed out"
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
return
event_data[1]["fail_reason"] = "insufficient data for execution"
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
return
def callback_success(module, event_data, dispatchers_steamid, match=None):
player_to_be_teleported = event_data[1].get("steamid", None)
dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
def callback_fail(module, event_data, dispatchers_steamid):
logger.error("teleport_failed",
user=dispatchers_steamid,
target=event_data[1].get("steamid"),
reason=event_data[1].get("fail_reason", "no reason known"))
action_meta = {
"description": "teleports a 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=None):
event_data[1]["action_identifier"] = action_name
target_player_steamid = event_data[1].get("steamid", None)
auth_status = event_data[1].get("auth_status", None)
active_dataset = event_data[1].get("dataset", None)
if all([target_player_steamid, auth_status is not None, active_dataset]):
module.dom.data.upsert({
"module_players": {
"elements": {
active_dataset: {
target_player_steamid: {
"is_authenticated": auth_status
}
}
}
}
})
module.callback_success(callback_success, module, event_data, dispatchers_steamid)
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": "toggles a player's authentication status",
"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,91 @@
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
target_player_steamid = event_data[1].get("steamid", None)
mute_status = event_data[1].get("mute_status", None)
# Get player entity ID - game requires entity ID instead of steamid
dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
player_dict = (
module.dom.data
.get("module_players", {})
.get("elements", {})
.get(dataset, {})
.get(target_player_steamid, {})
)
player_entity_id = player_dict.get("id")
if not player_entity_id:
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
return
command = "bc-mute {} {}".format(player_entity_id, mute_status)
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):
target_player_steamid = event_data[1].get("steamid", None)
flag_player_to_be_muted = event_data[1].get("mute_status", None)
active_dataset = event_data[1].get("dataset", None)
module.dom.data.upsert({
"module_players": {
"elements": {
active_dataset: {
target_player_steamid: {
"is_muted": flag_player_to_be_muted
}
}
}
}
})
pass
def callback_fail(module, event_data, dispatchers_steamid):
pass
action_meta = {
"description": "mutes or unmutes a given 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):
action = event_data[1].get("action", None)
event_data[1]["action_identifier"] = action_name
player_steamid = event_data[1].get("steamid", None)
if action == "show_options":
current_view = "options"
current_view_steamid = None
elif action == "show_frontend":
current_view = "frontend"
current_view_steamid = None
elif action == "show_info_view":
current_view = "info"
current_view_steamid = player_steamid
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 player 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,64 @@
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):
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
player_to_be_updated_steamid = event_data[1].get("steamid", None)
permission_level = event_data[1].get("level", 1000)
event_data[1]["action_identifier"] = action_name
event_data[1]["fail_reason"] = []
player_to_be_updated = (
module.dom.data
.get("module_players", {})
.get("elements", {})
.get(active_dataset, {})
.get(player_to_be_updated_steamid, None)
)
if player_to_be_updated is not None:
module.dom.data.upsert({
"module_players": {
"elements": {
active_dataset: {
player_to_be_updated_steamid: {
"permission_level": permission_level
}
}
}
}
})
module.callback_success(callback_success, module, event_data, dispatchers_steamid)
return
else:
event_data[1]["fail_reason"].append(
"player does not exist on this server / has not logged in yet t create a file"
)
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": "updates a players profiles permission data",
"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,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,10 @@
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
<span id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_control_info_link" class="info">
{{- construct_toggle_link(
True,
"i", ['widget_event', ['players', ['toggle_players_widget_view', {
'steamid': player.steamid,
'action': 'show_info_view'
}]]]
)-}}
</span>

View File

@@ -0,0 +1,13 @@
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
{%- if player.is_initialized == true -%}
<span id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_control_kick_link" class="info">
{{- construct_toggle_link(
player.is_initialized,
"kick", ['widget_event', ['players', ['kick_player', {
'action': 'kick_player',
'steamid': player.steamid,
'confirmed': 'False'
}]]]
)-}}
</span>
{%- endif -%}

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', ['players', ['toggle_players_widget_view', {'steamid': steamid, "action": "show_options"}]]],
"back", ['widget_event', ['players', ['toggle_players_widget_view', {'steamid': steamid, "action": "show_frontend"}]]]
)}}
</div>

View File

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

View File

@@ -0,0 +1,43 @@
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
<table class="delete_modal">
<tr>
<td colspan="2">
<p>Are you sure you want to kick player {{ steamid }}?</p>
</td>
</tr>
<tr>
<td>
<p>By clicking [confirm] you will continue to proceed kicking the dude.</p>
</td>
<td>
<p>Clicking [cancel] will abort the kicking.</p>
</td>
</tr>
<tr>
<td colspan="2">
<div id="kick_player_reason">
<input name="kick_player_reason" value="{{ reason }}" />
</div>
</td>
</tr>
<tr>
<td>
<div id="kick_player_confirm" class="modal_delete_button">
<span class="active">
{% include "manage_players_widget/modal_confirm_kick_send_data.html" %}
</span>
</div>
</td>
<td>
<div id="kick_player_cancel" class="modal_cancel_button">
{{ construct_toggle_link(
True,
"cancel", ['widget_event', ['players', ['kick_player', {
'action': "cancel_kick_player",
'steamid': 'steamid'
}]]]
) }}
</div>
</td>
</tr>
</table>

View File

@@ -0,0 +1,15 @@
<script>
function send_kick_data_to_bot() {
let kick_reason = $("input[name='kick_player_reason']").val();
window.socket.emit(
'widget_event',
['players', ['kick_player', {
'action': 'kick_player',
'steamid': '{{ steamid }}',
'reason': kick_reason,
'confirmed': 'True'
}]]
);
}
</script>
<a href="#" onclick="send_kick_data_to_bot(); return false;">confirm</a>

View File

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

View File

@@ -0,0 +1,13 @@
<tr>
<th>*</th>
<th onclick="window.sorting(this, player_table, 1)">actions</th>
<th onclick="window.sorting(this, player_table, 2)">level</th>
<th onclick="window.sorting(this, player_table, 3)">name</th>
<th onclick="window.sorting(this, player_table, 4)">health</th>
<th onclick="window.sorting(this, player_table, 5)">id</th>
<th onclick="window.sorting(this, player_table, 6)">steamid</th>
<th onclick="window.sorting(this, player_table, 7)">pos</th>
<th onclick="window.sorting(this, player_table, 8)">zombies</th>
<th onclick="window.sorting(this, player_table, 9)" class="right">last seen</th>
<th onclick="window.sorting(this, player_table, 10)" class="right">gametime</th>
</tr>

View File

@@ -0,0 +1,30 @@
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
<tr id="player_table_row_{{ player.dataset }}_{{ player.steamid }}"{%- if css_class %} class="{{ css_class }}"{%- endif -%}>
<td>
<span id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_control_select_link" class="select_button">{{ control_select_link }}</span>
</td>
<td class="nobr" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_actions">{{ control_info_link }}{{ control_kick_link }}</td>
<td class="center" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_level">{{ player.level }}</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_name" onclick="$(this).selectText();">{{ player.name }}</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_health">{{ player.health }}</td>
<td class="right" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_id" onclick="$(this).selectText();">{{ player.id }}</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_steamid" onclick="$(this).selectText();">{{ player.steamid }}</td>
<td class="position right" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_pos" onclick="$(this).selectText();">
<span id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_pos_x">
{{ ((player | default({})).pos | default({}) ).x | default('0') }}
</span>
<span id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_pos_y">
{{ ((player | default({})).pos | default({}) ).y | default('0') }}
</span>
<span id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_pos_z">
{{ ((player | default({})).pos | default({}) ).z | default('0') }}
</span>
</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_zombies">{{ player.zombies }}</td>
<td class="nobr right" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_last_updated_servertime">
{{ player.last_updated_servertime }}
</td>
<td class="nobr right" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_last_seen_gametime">
{{ player.last_seen_gametime }}
</td>
</tr>

View File

@@ -0,0 +1,33 @@
<header>
<div>
<span>Players</span>
</div>
</header>
<aside>
{{ options_toggle }}
</aside>
<main>
<table class="data_table">
<caption>
<span>offline</span>
<span class="in_limbo">offline and dead</span>
<span class="is_online in_limbo">logging in</span>
<span class="is_online is_initialized">online</span>
<span class="in_limbo is_online is_initialized">online and dead</span>
</caption>
<thead>
{{ table_header }}
</thead>
<tbody id="player_table">
{{ table_rows }}
</tbody>
<tfoot>
{{ table_footer }}
</tfoot>
</table>
<div class="dialog">
<div id="manage_players_widget_modal" class="modal-content">
<p>this is the text inside the modal</p>
</div>
</div>
</main>

View File

@@ -0,0 +1,155 @@
<header>
<div>
<span>Players</span>
</div>
</header>
<aside>
{{ options_toggle }}
</aside>
<main>
<table class="info_sheet">
<thead>
<tr>
<th colspan="3">player-info ({{ player.steamid }})</th>
</tr>
</thead>
<tbody id="player_table_row_{{ player.dataset }}_{{ player.steamid }}">
<tr>
<td>Name</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_name" onclick="$(this).selectText();">{{ player.name }}</td>
<td>the players steam-name</td>
</tr>
<tr>
<td>ID</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_id" onclick="$(this).selectText();">{{ player.id }}</td>
<td></td>
</tr>
<tr>
<td>SteamID</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_steamid" onclick="$(this).selectText();">{{ player.steamid }}</td>
<td></td>
</tr>
<tr>
<td>Health</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_health">{{ player.health }}</td>
<td></td>
</tr>
<tr>
<td>Position</td>
<td class="position" onclick="$(this).selectText();">
<span id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_pos_x">{{ ((player | default({})).pos | default({}) ).x | default('0') }}</span>
<span id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_pos_y">{{ ((player | default({})).pos | default({}) ).y | default('0') }}</span>
<span id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_pos_z">{{ ((player | default({})).pos | default({}) ).z | default('0') }}</span>
</td>
<td></td>
</tr>
<tr>
<td>Rotation</td>
<td class="position">
<span id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_rot_x">{{ ((player | default({})).rot | default({}) ).x | default('0') }}</span>
<span id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_rot_y">{{ ((player | default({})).rot | default({}) ).y | default('0') }}</span>
<span id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_rot_z">{{ ((player | default({})).rot | default({}) ).z | default('0') }}</span>
</td>
<td></td>
</tr>
<tr>
<td>Level</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_level">{{ player.level }}</td>
<td></td>
</tr>
<tr>
<td>IP-Address</td>
<td onclick="$(this).selectText();" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_ip">{{ player.ip }}</td>
<td></td>
</tr>
<tr>
<td>Ping</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_ping">{{ player.ping }}</td>
<td></td>
</tr>
<tr>
<td>Deaths</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_deaths">{{ player.deaths }}</td>
<td></td>
</tr>
<tr>
<td>Zombie-Kills</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_zombies">{{ player.zombies }}</td>
<td></td>
</tr>
<tr>
<td>Player-Kills</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_players">{{ player.players }}</td>
<td></td>
</tr>
<tr>
<td>Score</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_score">{{ player.score }}</td>
<td></td>
</tr>
<tr>
<td>Last seen</td>
<td class="nobr" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_last_updated_servertime">
{{ player.last_updated_servertime }}
</td>
<td></td>
</tr>
<tr>
<td>First seen (gametime)</td>
<td class="nobr" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_first_seen_gametime">
{{ player.first_seen_gametime }}
</td>
<td></td>
</tr>
<tr>
<td>Last seen (gametime)</td>
<td class="nobr" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_last_seen_gametime">
{{ player.last_seen_gametime }}
</td>
<td></td>
</tr>
<tr>
<td>dataset</td>
<td class="nobr" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_origin">
{{ player.dataset }}
</td>
<td>the server-instance this entry is from</td>
</tr>
<tr>
<td>is_authenticated</td>
<td class="nobr" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_is_authenticated">
{{ player.is_authenticated | default("False") }}
</td>
<td>has authenticated with the bot</td>
</tr>
<tr>
<td>in_limbo</td>
<td class="nobr" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_in_limbo">
{{ player.in_limbo | default("False") }}
</td>
<td>hasn't got any health, is dead(ish)!</td>
</tr>
<tr>
<td>is_initialized</td>
<td class="nobr" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_is_initialized">
{{ player.is_initialized | default("False") }}
</td>
<td>player is online, has health, is ready to go!</td>
</tr>
<tr>
<td>is_online</td>
<td class="nobr" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_is_online">
{{ player.is_online | default("False") }}
</td>
<td>we can see you!</td>
</tr>
<tr>
<td>is_muted</td>
<td class="nobr" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_is_muted">
{{ player.is_muted | default("False") }}
</td>
<td>come again?</td>
</tr>
</tbody>
</table>
</main>

View File

@@ -0,0 +1,27 @@
<header>
<div>
<span>Players</span>
</div>
</header>
<aside>
{{ options_toggle }}
</aside>
<main>
<table class="options_table">
<thead>
<tr>
<th colspan="2">player table widget 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,168 @@
// ========================================
// Player Popup Actions
// ========================================
// Kick player from map popup
window.kickPlayerFromMap = function(steamid, playerName) {
const reason = prompt('Kick reason for ' + playerName + ':', 'Admin action');
if (reason === null) {
return; // User cancelled
}
window.socket.emit(
'widget_event',
['players',
['kick_player', {
'action': 'kick_player',
'steamid': steamid,
'reason': reason || 'No reason provided',
'confirmed': 'True'
}]]
);
console.log('[MAP] Kicking player:', steamid, 'Reason:', reason);
};
// Send message to player from map popup
window.messagePlayerFromMap = function(steamid, playerName) {
const message = prompt('Message to ' + playerName + ':', '');
if (!message) {
return; // User cancelled or empty message
}
window.socket.emit(
'widget_event',
['players',
['say_to_player', {
'steamid': steamid,
'message': message
}]]
);
console.log('[MAP] Sending message to player:', steamid, 'Message:', message);
};
// Toggle player mute status from map popup
window.togglePlayerMuteFromMap = function(steamid, dataset, isMuted) {
window.socket.emit(
'widget_event',
['players',
['toggle_player_mute', {
'steamid': steamid,
'dataset': dataset,
'mute_status': isMuted
}]]
);
console.log('[MAP] Toggling player mute status:', steamid, 'to', isMuted);
};
// Toggle player authentication status from map popup
window.togglePlayerAuthFromMap = function(steamid, dataset, isAuth) {
window.socket.emit(
'widget_event',
['players',
['toggle_player_authentication', {
'steamid': steamid,
'dataset': dataset,
'auth_status': isAuth
}]]
);
console.log('[MAP] Toggling player auth status:', steamid, 'to', isAuth);
};
// Teleport player from map popup - click to select destination
window.teleportPlayerFromMap = function(steamid, playerName) {
// Close any open popups
map.closePopup();
// Create info message
const infoDiv = L.DomUtil.create('div', 'coordinates-display');
infoDiv.style.bottom = '50px';
infoDiv.style.background = 'rgba(255, 204, 0, 0.95)';
infoDiv.style.borderColor = 'var(--lcars-golden-tanoi)';
infoDiv.style.color = '#000';
infoDiv.style.fontWeight = 'bold';
infoDiv.innerHTML = '🎯 Click destination for ' + playerName + ' (Locations will use their TP entry)';
document.getElementById('map').appendChild(infoDiv);
// Change cursor
map.getContainer().style.cursor = 'crosshair';
// Wait for click
map.once('click', function(e) {
const clickCoords = e.latlng;
let targetX = Math.round(clickCoords.lat);
let targetY = 0; // Default Y, will be adjusted
let targetZ = Math.round(clickCoords.lng);
// Check if click is inside any location with teleport_entry
let foundLocationTeleport = false;
for (const locationId in locations) {
const loc = locations[locationId];
const teleportEntry = loc.teleport_entry || {};
const hasTeleport = teleportEntry.x !== undefined &&
teleportEntry.y !== undefined &&
teleportEntry.z !== undefined;
if (hasTeleport) {
// Check if click is inside this location's bounds
if (isInsideLocation(clickCoords.lat, clickCoords.lng, loc)) {
// Use location's teleport entry
targetX = Math.round(parseFloat(teleportEntry.x));
targetY = Math.round(parseFloat(teleportEntry.y));
targetZ = Math.round(parseFloat(teleportEntry.z));
foundLocationTeleport = true;
console.log('[MAP] Using location teleport entry:', loc.name, 'at', targetX, targetY, targetZ);
break;
}
}
}
// If no location teleport found, use default Y (ground level)
if (!foundLocationTeleport) {
targetY = -1; // Standard ground level
}
// Call teleport_player action
window.socket.emit(
'widget_event',
['players',
['teleport_player', {
'steamid': steamid,
'coordinates': {
'x': targetX.toString(),
'y': targetY.toString(),
'z': targetZ.toString()
}
}]]
);
console.log('[MAP] Teleporting player:', steamid, 'to', targetX, targetY, targetZ);
// Cleanup
map.getContainer().style.cursor = '';
document.getElementById('map').removeChild(infoDiv);
});
};
// Helper function to check if coordinates are inside a location
function isInsideLocation(x, z, loc) {
const coords = loc.coordinates;
const dims = loc.dimensions;
const shape = loc.shape;
if (shape === 'circle' || shape === 'spherical') {
const radius = parseFloat(dims.radius || 0);
const distance = Math.sqrt(
Math.pow(x - coords.x, 2) +
Math.pow(z - coords.z, 2)
);
return distance <= radius;
} else if (shape === 'rectangle' || shape === 'box') {
const width = parseFloat(dims.width || 0);
const length = parseFloat(dims.length || 0);
return (
x >= coords.x - width && x <= coords.x + width &&
z >= coords.z - length && z <= coords.z + length
);
}
return false;
}

View File

@@ -0,0 +1,3 @@
// Player markers are now loaded dynamically via Socket.IO
// Initial loading is handled by player_position_update events
// See player_update_handler.html for marker creation logic

View File

@@ -0,0 +1,64 @@
// Helper function to create player popup content
function createPlayerPopup(steamid, player) {
const healthMax = 150;
const healthPercent = Math.round((player.health / healthMax) * 100);
// Status badge
let statusBadge = '🟢 Online';
let statusColor = '#66ff66';
if (player.in_limbo) {
statusBadge = '💀 Dead';
statusColor = '#ff6666';
} else if (!player.is_initialized) {
statusBadge = '🔄 Spawning';
statusColor = '#ffaa66';
}
// Permission badge
let permissionBadge = '';
if (player.permission_level === 0) {
permissionBadge = '<span style="background: #ff0000; color: white; padding: 2px 6px; border-radius: 3px; font-size: 0.85em; font-weight: bold;">🛡️ ADMIN</span>';
}
// Use template literal for clean HTML
return `
<div style="min-width: 280px; font-family: monospace;">
<b style="font-size: 1.1em;">${player.name}</b>
${permissionBadge ? '<br>' + permissionBadge : ''}
<br><span style="font-size: 0.9em; color: ${statusColor};">${statusBadge}</span>
<br><hr style="margin: 5px 0; border-color: #333;">
<b>❤️ Health:</b> ${player.health}/${healthMax} (${healthPercent}%)
<br><b>⭐ Level:</b> ${player.level} | <b>🎯 Score:</b> ${player.score}
<br><b>🧟 Zombies:</b> ${player.zombies} | <b>💀 Deaths:</b> ${player.deaths}
<br><b>👥 Players:</b> ${player.players} | <b>📡 Ping:</b> ${player.ping}ms
<br><hr style="margin: 8px 0; border-color: #333;">
<div style="display: flex; gap: 5px; margin-bottom: 5px;">
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer; flex: 1;">
<input type="checkbox"
${player.is_muted ? 'checked' : ''}
onchange="togglePlayerMuteFromMap('${steamid}', this.checked)"
style="cursor: pointer; width: 16px; height: 16px;" />
<span style="font-weight: bold; font-size: 0.9em;">🔇 Muted</span>
</label>
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer; flex: 1;">
<input type="checkbox"
${player.is_authenticated ? 'checked' : ''}
onchange="togglePlayerAuthFromMap('${steamid}', this.checked)"
style="cursor: pointer; width: 16px; height: 16px;" />
<span style="font-weight: bold; font-size: 0.9em;">✅ Auth</span>
</label>
</div>
<div style="display: flex; gap: 5px; margin-bottom: 5px;">
<button onclick="messagePlayerFromMap('${steamid}', '${player.name.replace(/'/g, "\\'")}', event)"
style="flex: 1; padding: 6px 10px; background: var(--lcars-anakiwa); color: #000; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 0.9em;">
💬 Message</button>
<button onclick="kickPlayerFromMap('${steamid}', '${player.name.replace(/'/g, "\\'")}', event)"
style="flex: 1; padding: 6px 10px; background: var(--lcars-hopbush); color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 0.9em;">
👢 Kick</button>
</div>
<button onclick="teleportPlayerFromMap('${steamid}', '${player.name.replace(/'/g, "\\'")}', event)"
style="width: 100%; padding: 8px 10px; background: var(--lcars-golden-tanoi); color: #000; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 0.95em;">
🎯 Teleport Player - Click Map</button>
</div>
`;
}

View File

@@ -0,0 +1,58 @@
// Handle player position updates
if (data.data_type === 'player_position_update' && data.payload) {
const playerData = data.payload;
const steamid = playerData.steamid;
const pos = playerData.position;
console.log('[MAP] Processing player update for:', steamid, pos);
if (!steamid || !pos) {
console.warn('[MAP] Invalid player update data:', playerData);
return;
}
// Update or create player data in players object
if (!players[steamid]) {
players[steamid] = {};
}
// Update all player data
players[steamid].pos = pos;
players[steamid].name = playerData.name || players[steamid].name || 'Player';
players[steamid].level = playerData.level !== undefined ? playerData.level : (players[steamid].level || 0);
players[steamid].health = playerData.health !== undefined ? playerData.health : (players[steamid].health || 0);
players[steamid].zombies = playerData.zombies !== undefined ? playerData.zombies : (players[steamid].zombies || 0);
players[steamid].deaths = playerData.deaths !== undefined ? playerData.deaths : (players[steamid].deaths || 0);
players[steamid].players = playerData.players !== undefined ? playerData.players : (players[steamid].players || 0);
players[steamid].score = playerData.score !== undefined ? playerData.score : (players[steamid].score || 0);
players[steamid].ping = playerData.ping !== undefined ? playerData.ping : (players[steamid].ping || 0);
players[steamid].is_muted = playerData.is_muted !== undefined ? playerData.is_muted : (players[steamid].is_muted || false);
players[steamid].is_authenticated = playerData.is_authenticated !== undefined ? playerData.is_authenticated : (players[steamid].is_authenticated || false);
players[steamid].in_limbo = playerData.in_limbo !== undefined ? playerData.in_limbo : (players[steamid].in_limbo || false);
players[steamid].is_initialized = playerData.is_initialized !== undefined ? playerData.is_initialized : (players[steamid].is_initialized || false);
players[steamid].permission_level = playerData.permission_level || players[steamid].permission_level || null;
players[steamid].dataset = playerData.dataset || players[steamid].dataset || '';
// Update or create marker
if (playerMarkers[steamid]) {
// Update existing marker position and popup
playerMarkers[steamid].setLatLng([pos.x, pos.z]);
playerMarkers[steamid].setPopupContent(createPlayerPopup(steamid, players[steamid]));
console.log('[MAP] Updated player marker:', steamid);
} else {
// Create new marker
const marker = L.circleMarker([pos.x, pos.z], {
radius: 5,
fillColor: '#66ccff',
color: '#0099ff',
weight: 2,
opacity: 1,
fillOpacity: 0.9,
className: 'player-marker'
}).addTo(map);
marker.bindPopup(createPlayerPopup(steamid, players[steamid]));
playerMarkers[steamid] = marker;
console.log('[MAP] Created new player marker:', steamid);
}
}

View File

@@ -0,0 +1,36 @@
from .discord_webhook import DiscordWebhook
from bot import loaded_modules_dict
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(*args, **kwargs):
module = args[0]
permission_levels = (
module.dom.data
.get(module.get_module_identifier(), {})
.get("admins", {})
)
for steamid, level in permission_levels.items():
event_data = ['update_player_permission_level', {
'steamid': steamid,
'level': level
}]
module.trigger_action_hook(module.players, event_data=event_data)
trigger_meta = {
"description": (
"Will call the update_player_permission_level action after permissions have been retrieved from the game"
),
"main_function": main_function,
"handlers": {
"module_players/admins": main_function,
}
}
loaded_modules_dict["module_" + module_name].register_trigger(trigger_name, trigger_meta)

View File

@@ -0,0 +1,4 @@
__all__ = ["DiscordWebhook",
"DiscordEmbed"]
from .webhook import (DiscordWebhook, DiscordEmbed)

View File

@@ -0,0 +1,284 @@
import requests
import time
import datetime
import logging
import json
logger = logging.getLogger(__name__)
class DiscordWebhook:
"""
Webhook for Discord
"""
def __init__(self, url, **kwargs):
"""
Init Webhook for Discord
:param url: discord_webhook webhook url
:keyword content: the message contents
:keyword username: override the default username of the webhook
:keyword avatar_url: ooverride the default avatar of the webhook
:keyword tts: true if this is a TTS message
:keyword file: file contents
:keyword filename: file name
:keyword embeds: list of embedded rich content
:keyword proxies: dict of proxies
"""
self.url = url
self.content = kwargs.get('content')
self.username = kwargs.get('username')
self.avatar_url = kwargs.get('avatar_url')
self.tts = kwargs.get('tts', False)
self.files = kwargs.get('files', dict())
self.embeds = kwargs.get('embeds', [])
self.proxies = kwargs.get('proxies', None)
def add_file(self, file, filename):
"""
add file to webhook
:param file: file content
:param filename: filename
:return:
"""
self.files['_{}'.format(filename)] = (filename, file)
def add_embed(self, embed):
"""
add embedded rich content
:param embed: embed object or dict
"""
self.embeds.append(embed.__dict__ if isinstance(embed, DiscordEmbed) else embed)
def remove_embed(self, index):
"""
remove embedded rich content from `self.embeds`
:param index: index of embed in `self.embeds`
"""
self.embeds.pop(index)
def get_embeds(self):
"""
get all `self.embeds` as list
:return: `self.embeds`
"""
return self.embeds
def set_proxies(self, proxies):
"""
set proxies
:param proxies: dict of proxies
"""
self.proxies = proxies
@property
def json(self):
"""
convert webhook data to json
:return webhook data as json:
"""
data = dict()
embeds = self.embeds
self.embeds = list()
# convert DiscordEmbed to dict
for embed in embeds:
self.add_embed(embed)
for key, value in self.__dict__.items():
if value and key not in ['url', 'files', 'filename']:
data[key] = value
embeds_empty = all(not embed for embed in data["embeds"]) if 'embeds' in data else True
if embeds_empty and 'content' not in data and bool(self.files) is False:
logger.error('webhook message is empty! set content or embed data')
return data
def execute(self):
"""
execute Webhook
:return:
"""
if bool(self.files) is False:
response = requests.post(self.url, json=self.json, proxies=self.proxies)
else:
self.files['payload_json'] = (None, json.dumps(self.json))
response = requests.post(self.url, files=self.files, proxies=self.proxies)
if response.status_code in [200, 204]:
logger.debug("Webhook executed")
else:
logger.error('status code %s: %s' % (response.status_code, response.content.decode("utf-8")))
class DiscordEmbed:
"""
Discord Embed
"""
def __init__(self, **kwargs):
"""
Init Discord Embed
:keyword title: title of embed
:keyword description: description of embed
:keyword url: url of embed
:keyword timestamp: timestamp of embed content
:keyword color: color code of the embed as int
:keyword footer: footer information
:keyword image: image information
:keyword thumbnail: thumbnail information
:keyword video: video information
:keyword provider: provider information
:keyword author: author information
:keyword fields: fields information
"""
self.title = kwargs.get('title')
self.description = kwargs.get('description')
self.url = kwargs.get('url')
self.timestamp = kwargs.get('timestamp')
self.color = kwargs.get('color')
self.footer = kwargs.get('footer')
self.image = kwargs.get('image')
self.thumbnail = kwargs.get('thumbnail')
self.video = kwargs.get('video')
self.provider = kwargs.get('provider')
self.author = kwargs.get('author')
self.fields = kwargs.get('fields', [])
def set_title(self, title):
"""
set title of embed
:param title: title of embed
"""
self.title = title
def set_description(self, description):
"""
set description of embed
:param description: description of embed
"""
self.description = description
def set_url(self, url):
"""
set url of embed
:param url: url of embed
"""
self.url = url
def set_timestamp(self, timestamp=str(datetime.datetime.utcfromtimestamp(time.time()))):
"""
set timestamp of embed content
:param timestamp: (optional) timestamp of embed content
"""
self.timestamp = timestamp
def set_color(self, color):
"""
set color code of the embed as int
:param color: color code of the embed as int
"""
self.color = color
def set_footer(self, **kwargs):
"""
set footer information of embed
:keyword text: footer text
:keyword icon_url: url of footer icon (only supports http(s) and attachments)
:keyword proxy_icon_url: a proxied url of footer icon
"""
self.footer = {
'text': kwargs.get('text'),
'icon_url': kwargs.get('icon_url'),
'proxy_icon_url': kwargs.get('proxy_icon_url')
}
def set_image(self, **kwargs):
"""
set image of embed
:keyword url: source url of image (only supports http(s) and attachments)
:keyword proxy_url: a proxied url of the image
:keyword height: height of image
:keyword width: width of image
"""
self.image = {
'url': kwargs.get('url'),
'proxy_url': kwargs.get('proxy_url'),
'height': kwargs.get('height'),
'width': kwargs.get('width'),
}
def set_thumbnail(self, **kwargs):
"""
set thumbnail of embed
:keyword url: source url of thumbnail (only supports http(s) and attachments)
:keyword proxy_url: a proxied thumbnail of the image
:keyword height: height of thumbnail
:keyword width: width of thumbnail
"""
self.thumbnail = {
'url': kwargs.get('url'),
'proxy_url': kwargs.get('proxy_url'),
'height': kwargs.get('height'),
'width': kwargs.get('width'),
}
def set_video(self, **kwargs):
"""
set video of embed
:keyword url: source url of video
:keyword height: height of video
:keyword width: width of video
"""
self.video = {
'url': kwargs.get('url'),
'height': kwargs.get('height'),
'width': kwargs.get('width'),
}
def set_provider(self, **kwargs):
"""
set provider of embed
:keyword name: name of provider
:keyword url: url of provider
"""
self.provider = {
'name': kwargs.get('name'),
'url': kwargs.get('url'),
}
def set_author(self, **kwargs):
"""
set author of embed
:keyword name: name of author
:keyword url: url of author
:keyword icon_url: url of author icon (only supports http(s) and attachments)
:keyword proxy_icon_url: a proxied url of author icon
"""
self.author = {
'name': kwargs.get('name'),
'url': kwargs.get('url'),
'icon_url': kwargs.get('icon_url'),
'proxy_icon_url': kwargs.get('proxy_icon_url'),
}
def add_embed_field(self, **kwargs):
"""
set field of embed
:keyword name: name of the field
:keyword value: value of the field
:keyword inline: (optional) whether or not this field should display inline
"""
self.fields.append({
'name': kwargs.get('name'),
'value': kwargs.get('value'),
'inline': kwargs.get('inline', True)
})
def del_embed_field(self, index):
"""
remove field from `self.fields`
:param index: index of field in `self.fields`
"""
self.fields.pop(index)
def get_embed_fields(self):
"""
get all `self.fields` as list
:return: `self.fields`
"""
return self.fields

View File

@@ -0,0 +1,115 @@
from .discord_webhook import DiscordWebhook
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):
command = regex_result.group("command")
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
player_steamid = regex_result.group("player_steamid")
existing_player_dict = (
module.dom.data
.get("module_players", {})
.get("elements", {})
.get(active_dataset, {})
.get(player_steamid, None)
)
last_seen_gametime_string = module.game_environment.get_last_recorded_gametime_string()
executed_trigger = False
player_dict = {}
if command in ["Authenticating", "connected"]:
if existing_player_dict is None:
player_dict = {
"name": regex_result.group("player_name"),
"steamid": player_steamid,
"pos": {
"x": 0,
"y": 0,
"z": 0,
},
"dataset": active_dataset,
"owner": player_steamid,
"last_seen_gametime": last_seen_gametime_string
}
else:
player_dict.update(existing_player_dict)
player_dict.update({
"is_online": True,
"in_limbo": True,
"is_initialized": False,
})
if command == "connected":
player_dict.update({
"id": regex_result.group("entity_id"),
"ip": regex_result.group("player_ip"),
"steamid": player_steamid,
"owner": player_steamid
})
player_name = player_dict.get("name", regex_result.group("player_name"))
payload = '{} is logging into {} at {}'.format(player_name, active_dataset, last_seen_gametime_string)
discord_payload_url = origin_module.options.get("discord_webhook", None)
if discord_payload_url is not None:
webhook = DiscordWebhook(
url=discord_payload_url,
content=payload
)
webhook.execute()
executed_trigger = True
if all([
executed_trigger is True,
active_dataset is not None,
player_steamid is not None,
len(player_dict) >= 1
]):
module.dom.data.upsert({
"module_players": {
"elements": {
active_dataset: {
player_steamid: player_dict
}
}
}
})
trigger_meta = {
"description": "reacts to telnets player discovery messages for real time responses!",
"main_function": main_function,
"triggers": [
{
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
r"\[Steamworks.NET\]\s"
r"(?P<command>.*)\s"
r"player:\s(?P<player_name>.*)\s"
r"SteamId:\s(?P<player_steamid>\d+)\s(.*)"
),
"callback": main_function
}, {
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
r"Player\s"
r"(?P<command>.*), "
r"entityid=(?P<entity_id>.*), "
r"name=(?P<player_name>.*), "
r"steamid=(?P<player_steamid>.*), "
r"steamOwner=(?P<owner_id>.*), "
r"ip=(?P<player_ip>.*)"
),
"callback": main_function
}
]
}
loaded_modules_dict["module_" + module_name].register_trigger(trigger_name, trigger_meta)

View File

@@ -0,0 +1,78 @@
from .discord_webhook import DiscordWebhook
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):
command = regex_result.group("command")
executed_trigger = False
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
if command == "disconnected":
player_steamid = regex_result.group("player_steamid")
existing_player_dict = (
module.dom.data
.get("module_players", {})
.get("elements", {})
.get(active_dataset, {})
.get(player_steamid, {})
)
player_dict = {}
player_dict.update(existing_player_dict)
player_dict.update({
"is_online": False,
"is_initialized": False
})
player_name = player_dict.get("name", regex_result.group("player_name"))
last_seen_gametime_string = module.game_environment.get_last_recorded_gametime_string()
payload = '{} left {} at {}'.format(player_name, active_dataset, last_seen_gametime_string)
discord_payload_url = origin_module.options.get("discord_webhook", None)
if discord_payload_url is not None:
webhook = DiscordWebhook(
url=discord_payload_url,
content=payload
)
webhook.execute()
executed_trigger = True
if executed_trigger is True:
module.dom.data.upsert({
"module_players": {
"elements": {
active_dataset: {
player_steamid: player_dict
}
}
}
})
trigger_meta = {
"description": "reacts to telnets player disconnected message for real time responses!",
"main_function": main_function,
"triggers": [
{
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
r"Player\s"
r"(?P<command>.*):\s"
r"EntityID=(?P<entity_id>.*),\s"
r"PlayerID='(?P<player_steamid>\d{17})',\s"
r"OwnerID='(?P<owner_id>\d{17})',\s"
r"PlayerName='(?P<player_name>.*)'"
),
"callback": main_function
}
]
}
loaded_modules_dict["module_" + module_name].register_trigger(trigger_name, trigger_meta)

View File

@@ -0,0 +1,108 @@
from bot import loaded_modules_dict
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 send_player_update_to_map(*args, **kwargs):
"""Send player updates to map view via socket.io"""
module = args[0]
updated_values_dict = kwargs.get("updated_values_dict", {})
if updated_values_dict is None:
return
# Get steamid and dataset
steamid = updated_values_dict.get("steamid")
dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
if not all([dataset, steamid]):
return
# Get full player data from DOM
player_dict = (
module.dom.data
.get("module_players", {})
.get("elements", {})
.get(dataset, {})
.get(steamid, {})
)
if not player_dict:
return
# Check which clients are viewing the map
locations_module = loaded_modules_dict.get("module_locations")
if not locations_module:
return
for clientid in module.webserver.connected_clients.keys():
# Check if client is viewing the map in the locations widget
current_view = (
locations_module.dom.data
.get("module_locations", {})
.get("visibility", {})
.get(clientid, {})
.get("current_view", None)
)
if current_view != "map":
continue
# Prepare player update data
pos = player_dict.get("pos", {})
if not pos:
continue
player_update_data = {
"steamid": steamid,
"name": player_dict.get("name", "Player"),
"level": player_dict.get("level", 0),
"health": player_dict.get("health", 0),
"zombies": player_dict.get("zombies", 0),
"deaths": player_dict.get("deaths", 0),
"players": player_dict.get("players", 0),
"score": player_dict.get("score", 0),
"ping": player_dict.get("ping", 0),
"is_muted": player_dict.get("is_muted", False),
"is_authenticated": player_dict.get("is_authenticated", False),
"in_limbo": player_dict.get("in_limbo", False),
"is_initialized": player_dict.get("is_initialized", False),
"permission_level": player_dict.get("permission_level", None),
"dataset": dataset,
"position": {
"x": float(pos.get("x", 0)),
"y": float(pos.get("y", 0)),
"z": float(pos.get("z", 0))
}
}
module.webserver.send_data_to_client_hook(
module,
payload=player_update_data,
data_type="player_position_update",
clients=[clientid]
)
trigger_meta = {
"description": "sends player updates to webmap clients",
"main_function": send_player_update_to_map,
"handlers": {
# Listen to all player field updates that are relevant for the map
"module_players/elements/%map_identifier%/%steamid%/pos": send_player_update_to_map,
"module_players/elements/%map_identifier%/%steamid%/health": send_player_update_to_map,
"module_players/elements/%map_identifier%/%steamid%/level": send_player_update_to_map,
"module_players/elements/%map_identifier%/%steamid%/zombies": send_player_update_to_map,
"module_players/elements/%map_identifier%/%steamid%/deaths": send_player_update_to_map,
"module_players/elements/%map_identifier%/%steamid%/players": send_player_update_to_map,
"module_players/elements/%map_identifier%/%steamid%/score": send_player_update_to_map,
"module_players/elements/%map_identifier%/%steamid%/ping": send_player_update_to_map,
"module_players/elements/%map_identifier%/%steamid%/is_muted": send_player_update_to_map,
"module_players/elements/%map_identifier%/%steamid%/is_authenticated": send_player_update_to_map,
"module_players/elements/%map_identifier%/%steamid%/in_limbo": send_player_update_to_map,
"module_players/elements/%map_identifier%/%steamid%/is_initialized": send_player_update_to_map,
}
}
loaded_modules_dict["module_" + module_name].register_trigger(trigger_name, trigger_meta)

View File

@@ -0,0 +1,138 @@
from .discord_webhook import DiscordWebhook
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):
command = regex_result.group("command")
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
update_player_pos = False
last_recorded_gametime_string = module.game_environment.get_last_recorded_gametime_string()
if command == "joined the game":
player_name = regex_result.group("player_name")
payload = '{} joined {} at {}'.format(player_name, active_dataset, last_recorded_gametime_string)
discord_payload_url = origin_module.options.get("discord_webhook", None)
if discord_payload_url is not None:
webhook = DiscordWebhook(
url=discord_payload_url,
content=payload
)
webhook.execute()
elif any([
command == "EnterMultiplayer",
command == "JoinMultiplayer"
]):
steamid = regex_result.group("player_steamid")
player_name = regex_result.group("player_name")
existing_player_dict = (
module.dom.data.get("module_players", {})
.get("elements", {})
.get(active_dataset, {})
.get(steamid, {})
)
player_dict = {
"name": player_name
}
player_dict.update(existing_player_dict)
if command == "EnterMultiplayer":
player_dict["first_seen_gametime"] = last_recorded_gametime_string
module.dom.data.upsert({
"module_players": {
"elements": {
active_dataset: {
steamid: player_dict
}
}
}
})
elif command == "JoinMultiplayer":
default_player_password = module.default_options.get("default_player_password", None)
if player_dict.get("is_authenticated", False) is True or default_player_password is None:
message = "[66FF66]Welcome back[-] [FFFFFF]{}[-]".format(player_name)
else:
message = (
"[66FF66]Welcome to the server[-] [FFFFFF]{player_name}[-]"
).format(
player_name=player_name
)
if default_player_password is not None:
message += ", [FF6666]please authenticate[-] [FFFFFF]and make yourself at home[-]"
event_data = ['say_to_player', {
'steamid': steamid,
'message': message
}]
module.trigger_action_hook(origin_module.players, event_data=event_data)
elif command == "Teleport":
update_player_pos = True
if update_player_pos:
player_to_be_updated = regex_result.group("player_steamid")
pos_after_teleport = {
"pos": {
"x": regex_result.group("pos_x"),
"y": regex_result.group("pos_y"),
"z": regex_result.group("pos_z"),
}
}
# update the players location data with the teleport ones
module.dom.data.upsert({
"module_players": {
"elements": {
active_dataset: {
player_to_be_updated: pos_after_teleport
}
}
}
})
trigger_meta = {
"description": "reacts to telnets playerspawn messages for real time responses!",
"main_function": main_function,
"triggers": [
{
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
r"PlayerSpawnedInWorld\s"
r"\("
r"reason: (?P<command>.+?),\s"
r"position: (?P<pos_x>.*),\s(?P<pos_y>.*),\s(?P<pos_z>.*)"
r"\):\s"
r"EntityID=(?P<entity_id>.*),\s"
r"PlayerID='(?P<player_steamid>.*)',\s"
r"OwnerID='(?P<owner_steamid>.*)',\s"
r"PlayerName='(?P<player_name>.*)'"
),
"callback": main_function
}, {
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
r"Player (?P<command>.*): "
r"EntityID=(?P<entity_id>.*), "
r"PlayerID=\'(?P<player_steamid>.*)\', "
r"OwnerID=\'(?P<owner_id>.*)\', "
r"PlayerName='(?P<player_name>.*)\'$"
),
"callback": main_function
}, {
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
telnet_prefixes["GMSG"]["command"]
),
"callback": main_function
}
]
}
loaded_modules_dict["module_" + module_name].register_trigger(trigger_name, trigger_meta)

View File

@@ -0,0 +1,628 @@
from bot import loaded_modules_dict
from os import path, pardir
from collections import OrderedDict
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
widget_name = path.basename(path.abspath(__file__))[:-3]
def get_player_table_row_css_class(player_dict):
css_classes = []
if player_dict.get("is_online", False):
css_classes.append("is_online")
if player_dict.get("in_limbo", False):
css_classes.append("in_limbo")
if int(player_dict.get("health", 0)) > 0:
css_classes.append("has_health")
if player_dict.get("is_initialized", False):
css_classes.append("is_initialized")
permission_level = player_dict.get("permission_level", False)
if permission_level:
css_classes.append("has_level_" + permission_level.zfill(4))
else:
css_classes.append("has_no_level")
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 == "info":
show_info_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)
elif current_view == "kick-modal":
frontend_view(module, dispatchers_steamid=dispatchers_steamid)
kick_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_player_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, player_dicts in all_available_player_dicts.items():
if active_dataset == map_identifier:
for player_steamid, player_dict in player_dicts.items():
player_is_selected_by = player_dict.get("selected_by", [])
if dispatchers_steamid in player_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_players",
dom_element_id="player_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_players_widget_modal",
"type": "div",
"selector": "body > main > div"
}
)
def kick_modal_view(*args, **kwargs):
module = args[0]
dispatchers_steamid = kwargs.get('dispatchers_steamid', None)
current_view_steamid = (
module.dom.data
.get("module_players", {})
.get("visibility", {})
.get(dispatchers_steamid, {})
.get("current_view_steamid", None)
)
modal_confirm_kick = module.template_render_hook(
module,
template=module.templates.get_template('manage_players_widget/modal_confirm_kick.html'),
confirmed=kwargs.get("confirmed", "False"),
reason=(
"no reason provided, "
"try again in a few minutes and check if perhaps a bloodmoon is in progress ^^ "
"or something ^^"
),
steamid=current_view_steamid
)
module.webserver.send_data_to_client_hook(
module,
payload=modal_confirm_kick,
data_type="modal_content",
clients=[dispatchers_steamid],
target_element={
"id": "manage_players_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_players_widget/view_frontend.html')
template_table_rows = module.templates.get_template('manage_players_widget/table_row.html')
template_table_header = module.templates.get_template('manage_players_widget/table_header.html')
template_table_footer = module.templates.get_template('manage_players_widget/table_footer.html')
control_info_link = module.templates.get_template('manage_players_widget/control_info_link.html')
control_kick_link = module.templates.get_template('manage_players_widget/control_kick_link.html')
template_options_toggle = module.templates.get_template('manage_players_widget/control_switch_view.html')
template_options_toggle_view = module.templates.get_template(
'manage_players_widget/control_switch_options_view.html'
)
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
all_available_player_dicts = module.dom.data.get(module.get_module_identifier(), {}).get("elements", {})
# Build table rows efficiently using list + join
table_rows_list = []
all_selected_elements_count = 0
for map_identifier, player_dicts in all_available_player_dicts.items():
if active_dataset == map_identifier:
# have the recently online players displayed first initially!
ordered_player_dicts = OrderedDict(
sorted(
player_dicts.items(),
key=lambda x: x[1].get('last_updated_servertime', ""),
reverse=True
)
)
for player_steamid, player_dict in ordered_player_dicts.items():
player_is_selected_by = player_dict.get("selected_by", [])
player_entry_selected = False
if dispatchers_steamid in player_is_selected_by:
player_entry_selected = True
all_selected_elements_count += 1
# Sanitize dataset for HTML ID (replace spaces with underscores, lowercase)
player_dict_for_template = player_dict.copy()
player_dict_for_template["dataset"] = module.dom_management.sanitize_for_html_id(player_dict.get("dataset", ""))
player_dict_for_template["dataset_original"] = player_dict.get("dataset", "")
control_select_link = module.dom_management.get_selection_dom_element(
module,
target_module="module_players",
dom_element=player_dict_for_template,
dom_element_select_root=["selected_by"],
dom_element_entry_selected=player_entry_selected,
dom_action_active="deselect_dom_element",
dom_action_inactive="select_dom_element"
)
table_rows_list.append(module.template_render_hook(
module,
template=template_table_rows,
player=player_dict_for_template,
css_class=get_player_table_row_css_class(player_dict),
control_info_link=module.template_render_hook(
module,
template=control_info_link,
player=player_dict_for_template
),
control_kick_link=module.template_render_hook(
module,
template=control_kick_link,
player=player_dict_for_template,
),
control_select_link=control_select_link
))
rendered_table_rows = ''.join(table_rows_list)
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_players",
dom_element_id="player_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=rendered_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_players_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_players_widget/view_options.html')
template_options_toggle = module.templates.get_template('manage_players_widget/control_switch_view.html')
template_options_toggle_view = module.templates.get_template('manage_players_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_players_widget",
"type": "table",
"selector": "body > main > div"
}
)
def show_info_view(*args, **kwargs):
module = args[0]
dispatchers_steamid = kwargs.get('dispatchers_steamid', None)
template_frontend = module.templates.get_template('manage_players_widget/view_info.html')
template_options_toggle = module.templates.get_template('manage_players_widget/control_switch_view.html')
template_options_toggle_view = module.templates.get_template(
'manage_players_widget/control_switch_options_view.html'
)
current_view_steamid = (
module.dom.data
.get("module_players", {})
.get("visibility", {})
.get(dispatchers_steamid, {})
.get("current_view_steamid", "frontend")
)
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
)
)
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
player_dict = (
module.dom.data
.get("module_players", {})
.get("elements", {})
.get(active_dataset, {})
.get(current_view_steamid, None)
)
data_to_emit = module.template_render_hook(
module,
template=template_frontend,
options_toggle=options_toggle,
player=player_dict
)
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_players_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_players_widget/table_row.html')
control_info_link = module.templates.get_template('manage_players_widget/control_info_link.html')
control_kick_link = module.templates.get_template('manage_players_widget/control_kick_link.html')
for player_steamid, player_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(player_dict["dataset"])
table_row_id = "player_table_row_{}_{}".format(
sanitized_dataset,
str(player_steamid)
)
# Update player_dict with sanitized dataset for template
player_dict = player_dict.copy()
player_dict["dataset"] = sanitized_dataset
player_dict["dataset_original"] = updated_values_dict[player_steamid].get("dataset", "")
except KeyError:
table_row_id = "manage_players_widget"
player_entry_selected = False
if clientid in player_dict.get("selected_by", []):
player_entry_selected = True
control_select_link = module.dom_management.get_selection_dom_element(
module,
target_module="module_players",
dom_element_select_root=["selected_by"],
dom_element=player_dict,
dom_element_entry_selected=player_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,
player=player_dict,
css_class=get_player_table_row_css_class(player_dict),
control_info_link=module.template_render_hook(
module,
template=control_info_link,
player=player_dict
),
control_kick_link=module.template_render_hook(
module,
template=control_kick_link,
player=player_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_player_table_row_css_class(player_dict),
"selector": "body > main > div > div#manage_players_widget > main > table > tbody"
}
)
elif method == "remove":
player_origin = updated_values_dict[2]
player_steamid = updated_values_dict[3]
# Sanitize dataset for HTML ID (replace spaces with underscores, lowercase)
sanitized_origin = module.dom_management.sanitize_for_html_id(player_origin)
module.webserver.send_data_to_client_hook(
module,
data_type="remove_table_row",
clients="all",
target_element={
"id": "player_table_row_{}_{}".format(
sanitized_origin,
str(player_steamid)
)
}
)
update_delete_button_status(module, *args, **kwargs)
def update_widget(*args, **kwargs):
module = args[0]
updated_values_dict = kwargs.get("updated_values_dict", None)
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
method = kwargs.get("method", None)
if method in ["update"]:
original_player_dict = (
module.dom.data
.get("module_players", {})
.get("elements", {})
.get(active_dataset, {})
.get(updated_values_dict.get("steamid", None), {})
)
player_clients_to_update = list(module.webserver.connected_clients.keys())
for clientid in player_clients_to_update:
try:
module_players = module.dom.data.get("module_players", {})
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(original_player_dict.get("dataset", ""))
table_row_id = "player_table_row_{}_{}".format(
sanitized_dataset,
str(original_player_dict.get("steamid", None))
)
# Update dicts with sanitized dataset
original_dataset = original_player_dict.get("dataset", "")
updated_values_dict_sanitized = updated_values_dict.copy()
updated_values_dict_sanitized["dataset"] = sanitized_dataset
updated_values_dict_sanitized["dataset_original"] = original_dataset
original_player_dict_sanitized = original_player_dict.copy()
original_player_dict_sanitized["dataset"] = sanitized_dataset
original_player_dict_sanitized["dataset_original"] = original_dataset
if current_view == "frontend":
module.webserver.send_data_to_client_hook(
module,
payload=updated_values_dict_sanitized,
data_type="table_row_content",
clients=[clientid],
method="update",
target_element={
"id": table_row_id,
"parent_id": "manage_players_widget",
"module": "players",
"type": "tr",
"selector": "body > main > div > div#manage_players_widget",
"class": get_player_table_row_css_class(original_player_dict_sanitized),
}
)
elif current_view == "info":
module.webserver.send_data_to_client_hook(
module,
payload=updated_values_dict_sanitized,
data_type="table_row_content",
clients=[clientid],
method="update",
target_element={
"id": table_row_id,
"parent_id": "manage_players_widget",
"module": "players",
"type": "tr",
"selector": "body > main > div > div#manage_players_widget",
"class": get_player_table_row_css_class(updated_values_dict_sanitized),
}
)
except AttributeError as error:
# probably dealing with a player_dict here, not the players dict
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": "player_table_row_{}_{}_control_select_link".format(
sanitized_dataset,
updated_values_dict["owner"]
)
}
)
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": "player_table_widget_action_delete_button"
}
)
def update_actions_status(*args, **kwargs):
""" we want to update the action status here
not all actions can work all the time, some depend on a player being online for example"""
module = args[0]
control_info_link = module.templates.get_template('manage_players_widget/control_info_link.html')
control_kick_link = module.templates.get_template('manage_players_widget/control_kick_link.html')
player_dict = kwargs.get("updated_values_dict", None)
# Sanitize dataset for HTML ID (replace spaces with underscores, lowercase)
original_dataset = player_dict.get("dataset", "")
player_dict_sanitized = player_dict.copy()
player_dict_sanitized["dataset"] = module.dom_management.sanitize_for_html_id(original_dataset)
player_dict_sanitized["dataset_original"] = original_dataset
rendered_control_info_link = module.template_render_hook(
module,
template=control_info_link,
player=player_dict_sanitized
)
rendered_control_kick_link = module.template_render_hook(
module,
template=control_kick_link,
player=player_dict_sanitized
)
payload = rendered_control_info_link + rendered_control_kick_link
module.webserver.send_data_to_client_hook(
module,
payload=payload,
data_type="element_content",
clients="all",
method="update",
target_element={
"id": "player_table_row_{}_{}_actions".format(
player_dict_sanitized.get("dataset"),
player_dict_sanitized.get("steamid")
)
}
)
widget_meta = {
"description": "sends and updates a table of all currently known players",
"main_widget": select_view,
"handlers": {
# the %abc% placeholders can contain any text at all, it has no effect on anything but code-readability
# the third line could just as well read
# "module_players/elements/%x%/%x%/%x%/selected_by": update_selection_status
# and would still function the same as
# "module_players/elements/%map_identifier%/%steamid%/%element_identifier%/selected_by":
# update_selection_status
"module_players/visibility/%steamid%/current_view":
select_view,
"module_players/elements/%map_identifier%/%steamid%":
table_rows,
"module_players/elements/%map_identifier%/%steamid%/pos":
update_widget,
"module_players/elements/%map_identifier%/%steamid%/selected_by":
update_selection_status,
"module_players/elements/%map_identifier%/%steamid%/is_initialized":
update_actions_status
# "module_players/elements/%map_identifier%/%steamid%/%element_identifier%/is_enabled":
# update_enabled_flag,
},
"enabled": True
}
loaded_modules_dict["module_" + module_name].register_widget(widget_name, widget_meta)