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

105
bot/__init__.py Normal file
View File

@@ -0,0 +1,105 @@
""" some IDE's will throw 'PEP 8' warnings for imports, but this has to happen early, I think """
from gevent import monkey
monkey.patch_all()
""" standard imports """
from importlib import import_module
from os import path, chdir, walk
import json
from collections import deque
from bot.logger import get_logger
root_dir = path.dirname(path.abspath(__file__))
logger = get_logger("init")
chdir(root_dir)
loaded_modules_dict = {} # this will be populated by the imports done next:
telnet_prefixes = {
"telnet_log": {
# Modern 7D2D servers still include timestamp/stardate/INF in "Executing command" lines
# Format: 2025-11-18T20:20:59 4851.528 INF Executing command...
"timestamp": r"(?P<datetime>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})\s(?P<stardate>[-+]?\d*\.\d+|\d+)\sINF\s"
},
"GMSG": {
"command": (
r"GMSG:\s"
r"Player\s\'"
r"(?P<player_name>.*)"
r"\'\s"
r"(?P<command>.*)$"
)
},
"BCM": {
"chat": (
r"Chat\shandled\sby\smod\s\'(?P<used_mod>.*?)\':\s"
r"Chat\s\(from\s\'(?P<player_steamid>.*?)\',\sentity\sid\s\'(?P<entity_id>.*?)\',\s"
r"to\s\'(?P<target_room>.*)\'\)\:\s"
)
},
"Allocs": {
"chat": (
r"Chat\s\(from \'(?P<player_steamid>.*)\',\sentity\sid\s\'(?P<entity_id>.*)\',\s"
r"to \'(?P<target_room>.*)\'\)\:\s"
)
}
}
modules_to_start_list = deque()
module_loading_order = []
started_modules_dict = {}
available_modules_list = next(walk(path.join('modules', '.')))[1]
for module in available_modules_list:
""" at the bottom of each module, the loaded_modules_list will be updated
modules may not do any stuff in their __init__, apart from setting variables
and calling static methods, unless you know what you are doing """
import_module("bot.modules." + module)
def batch_setup_modules(modules_list):
def get_options_dict(module_name):
try:
options_dir = "{}/{}".format(root_dir, "options")
with open(path.join(options_dir, module_name + ".json")) as open_file:
return json.load(open_file)
except FileNotFoundError:
return dict
if len(module_loading_order) >= 1:
for module_to_setup in module_loading_order:
module_options_dict = get_options_dict(module_to_setup)
loaded_modules_dict[module_to_setup].setup(module_options_dict)
modules_to_start_list.append(loaded_modules_dict[module_to_setup])
else:
""" this should load all module in an order they can work with
Make absolutely SURE there's no circular dependencies, because I won't :) """
for module_to_setup in modules_list:
try:
if isinstance(loaded_modules_dict[module_to_setup].required_modules, list): # has dependencies, load those first!
batch_setup_modules(loaded_modules_dict[module_to_setup].required_modules)
raise AttributeError
except AttributeError: # raised by isinstance = has no dependencies, load right away
if loaded_modules_dict[module_to_setup] not in modules_to_start_list:
module_options_dict = get_options_dict(module_to_setup)
loaded_modules_dict[module_to_setup].setup(module_options_dict)
modules_to_start_list.append(loaded_modules_dict[module_to_setup])
def setup_modules():
loaded_modules_identifier_list = []
for loaded_module_identifier, loaded_module in loaded_modules_dict.items():
loaded_modules_identifier_list.append(loaded_module.get_module_identifier())
batch_setup_modules(loaded_modules_identifier_list)
def start_modules():
for module_to_start in modules_to_start_list:
module_to_start.start()
started_modules_dict[module_to_start.get_module_identifier()] = module_to_start
if len(loaded_modules_dict) == len(started_modules_dict):
logger.info("modules_started", count=len(started_modules_dict), modules=list(started_modules_dict.keys()))

98
bot/constants.py Normal file
View File

@@ -0,0 +1,98 @@
"""
Constants for chrani-bot-tng
This module contains all constants used throughout the bot to avoid
magic numbers and improve maintainability.
"""
# =============================================================================
# Permission Levels
# =============================================================================
# Permission levels for player access control
PERMISSION_LEVEL_ADMIN = 0 # Full access
PERMISSION_LEVEL_MODERATOR = 1 # Moderate players
PERMISSION_LEVEL_BUILDER = 2 # Build permissions
PERMISSION_LEVEL_PLAYER = 4 # Regular player
PERMISSION_LEVEL_DEFAULT = 2000 # Default for new/unknown players
# =============================================================================
# Telnet Command Timeouts (seconds)
# =============================================================================
TELNET_TIMEOUT_VERY_SHORT = 1.5 # Quick polls
TELNET_TIMEOUT_SHORT = 2 # Standard commands (lp, gettime)
TELNET_TIMEOUT_NORMAL = 3 # Most commands (listents)
TELNET_TIMEOUT_EXTENDED = 8 # Complex commands (manage entities)
TELNET_TIMEOUT_RECONNECT = 10 # Wait before reconnect attempt
# =============================================================================
# Telnet Buffer Limits
# =============================================================================
TELNET_BUFFER_MAX_SIZE = 12288 # Maximum telnet buffer size (bytes)
TELNET_LINES_MAX_HISTORY = 150 # Maximum telnet lines to keep in history
# =============================================================================
# Server Settings
# =============================================================================
DEFAULT_SERVER_PORT = 5000 # Default webserver port
DEFAULT_OBSERVER_INTERVAL = 0.1 # Default module polling interval (seconds)
# =============================================================================
# WebSocket Settings
# =============================================================================
WEBSOCKET_PING_TIMEOUT = 15 # WebSocket ping timeout (seconds)
WEBSOCKET_PING_INTERVAL = 5 # WebSocket ping interval (seconds)
# =============================================================================
# Thread Pool Settings
# =============================================================================
CALLBACK_THREAD_POOL_SIZE = 10 # Max concurrent callback threads
# =============================================================================
# String/Token Generation
# =============================================================================
DEFAULT_TOKEN_LENGTH = 20 # Default length for random tokens
# =============================================================================
# Helper Functions
# =============================================================================
def get_permission_level_name(level: int) -> str:
"""
Get human-readable name for a permission level.
Args:
level: Permission level integer
Returns:
String name of the permission level
"""
permission_names = {
PERMISSION_LEVEL_ADMIN: "Admin",
PERMISSION_LEVEL_MODERATOR: "Moderator",
PERMISSION_LEVEL_BUILDER: "Builder",
PERMISSION_LEVEL_PLAYER: "Player",
PERMISSION_LEVEL_DEFAULT: "Default/Unknown"
}
return permission_names.get(level, f"Custom ({level})")
def is_admin(permission_level: int) -> bool:
"""Check if permission level is admin."""
return permission_level == PERMISSION_LEVEL_ADMIN
def is_moderator_or_higher(permission_level: int) -> bool:
"""Check if permission level is moderator or higher (lower number = higher privilege)."""
return permission_level <= PERMISSION_LEVEL_MODERATOR
def is_builder_or_higher(permission_level: int) -> bool:
"""Check if permission level is builder or higher."""
return permission_level <= PERMISSION_LEVEL_BUILDER

31
bot/git.txt Normal file
View File

@@ -0,0 +1,31 @@
IntelliJ Git Workflow für Claude-Branches
Teil 1: Branch auschecken und Updates pullen
Erstmaliges Auschecken eines neuen Claude-Branches:
Git → Fetch (holt alle Remote-Branches)
Git → Branches... (oder rechts unten auf den aktuellen Branch-Namen klicken)
In der Branch-Liste unter "Remote Branches" → "origin" den Branch claude/add-map-legend-... finden
Rechtsklick auf den Branch → "Checkout"
IntelliJ fragt "Checkout as new local branch?" → "Checkout" klicken
Updates vom gleichen Branch pullen (nach meinen Änderungen):
Git → Pull... (Strg+T)
Im Dialog ist bereits der richtige Branch vorausgewählt
Einfach "Pull" klicken
Teil 2: Branch in Master integrieren und pushen
Wenn alle Tests erfolgreich sind und der Branch fertig ist:
Git → Branches...
"master" auswählen → "Checkout" (du wechselst auf master)
Git → Merge...
Den claude/add-map-legend-... Branch auswählen → "Merge" klicken
Git → Push... (Strg+Shift+K)
"Push" klicken
Fertig. Der Code ist jetzt in master und auf dem Remote-Server.
Das war's - kein Terminal, keine Alternativen, nur diese Schritte.

311
bot/logger.py Normal file
View File

@@ -0,0 +1,311 @@
"""
Structured logging system for chrani-bot-tng
Provides context-aware logging with consistent formatting across the entire application.
Replaces print() statements with structured, grep-able log output.
Usage:
from bot.logger import get_logger
logger = get_logger("webserver")
# Error logging (always shown)
logger.error("tile_fetch_failed", user="steamid123", z=4, x=-2, y=1, status=404)
# Warning logging (always shown)
logger.warn("auth_missing_sid", user="steamid456", action="tile_request")
# Info logging (startup only, can be disabled)
logger.info("module_loaded", module="webserver", version="1.0")
# Debug logging (opt-in via config)
logger.debug("action_trace", action="select_dom_element", path="/map/owner/id")
Output format:
[ERROR] [2025-01-19 12:34:56.123] tile_fetch_failed | user=steamid123 z=4 x=-2 y=1 status=404
"""
import sys
from datetime import datetime
from typing import Any, Dict, Optional
class Colors:
"""ANSI color codes for terminal output"""
# Foreground colors (bright versions for better visibility)
RED = "\033[91m" # Errors
YELLOW = "\033[93m" # Warnings
GREEN = "\033[92m" # Info
GRAY = "\033[90m" # Debug
LIGHT_GRAY = "\033[37m" # Timestamps
WHITE = "\033[97m" # Event/context text
RESET = "\033[0m" # Reset to default
# Optional: Bold variants
BOLD = "\033[1m"
class LogLevel:
"""Log level constants"""
ERROR = "ERROR"
WARN = "WARN"
INFO = "INFO"
DEBUG = "DEBUG"
class LogConfig:
"""Global logging configuration"""
# Which levels to actually output
enabled_levels = {
LogLevel.ERROR, # Always enabled
LogLevel.WARN, # Always enabled
LogLevel.INFO, # Enabled (for now, can be disabled after testing)
# LogLevel.DEBUG # Disabled by default, uncomment to enable
}
# Format settings
show_timestamps = True
timestamp_format = "%Y-%m-%d %H:%M:%S.%f" # Includes microseconds
use_colors = True # Enable colored output
@classmethod
def enable_debug(cls):
"""Enable debug logging (call this from config or command line)"""
cls.enabled_levels.add(LogLevel.DEBUG)
@classmethod
def disable_info(cls):
"""Disable info logging (for production)"""
cls.enabled_levels.discard(LogLevel.INFO)
@classmethod
def disable_colors(cls):
"""Disable colored output (for log files or incompatible terminals)"""
cls.use_colors = False
class ContextLogger:
"""
Structured logger with context support
Provides consistent, grep-able logging across the application.
Each logger instance is bound to a specific module/component.
"""
def __init__(self, module_name: str):
"""
Create a logger for a specific module
Args:
module_name: Name of the module (e.g., "webserver", "dom_management")
"""
self.module_name = module_name
def _format_context(self, **context) -> str:
"""
Format context dict as key=value pairs
Args:
**context: Arbitrary key-value pairs
Returns:
Formatted string: "key1=value1 key2=value2"
"""
if not context:
return ""
# Sort keys for consistent output
pairs = []
for key in sorted(context.keys()):
value = context[key]
# Format value appropriately
if value is None:
formatted_value = "null"
elif isinstance(value, str):
# Escape spaces and special chars if needed
if " " in value or "=" in value:
formatted_value = f'"{value}"'
else:
formatted_value = value
elif isinstance(value, bool):
formatted_value = str(value).lower()
elif isinstance(value, (list, dict)):
# Complex types: just show type and length
formatted_value = f"{type(value).__name__}[{len(value)}]"
else:
formatted_value = str(value)
pairs.append(f"{key}={formatted_value}")
return " ".join(pairs)
def _log(self, level: str, event: str, **context):
"""
Internal logging method
Args:
level: Log level (ERROR, WARN, INFO, DEBUG)
event: Event name (snake_case, descriptive)
**context: Additional context as key=value pairs
"""
# Check if this level is enabled
if level not in LogConfig.enabled_levels:
return
# Always include module in context
context["module"] = self.module_name
# Build timestamp
timestamp = ""
timestamp_color = ""
if LogConfig.show_timestamps:
now = datetime.now()
# Truncate microseconds to milliseconds
timestamp_str = now.strftime(LogConfig.timestamp_format)[:-3]
if LogConfig.use_colors:
timestamp_color = Colors.LIGHT_GRAY
timestamp = f"{timestamp_color}[{timestamp_str}]{Colors.RESET} "
else:
timestamp = f"[{timestamp_str}] "
# Build context string
context_str = self._format_context(**context)
# Get color for this level
level_color = ""
text_color = ""
reset = ""
if LogConfig.use_colors:
if level == LogLevel.ERROR:
level_color = Colors.RED
elif level == LogLevel.WARN:
level_color = Colors.YELLOW
elif level == LogLevel.INFO:
level_color = Colors.GREEN
elif level == LogLevel.DEBUG:
level_color = Colors.GRAY
text_color = Colors.WHITE
reset = Colors.RESET
# Format: [LEVEL] [TIMESTAMP] event | context
# Only the [LEVEL] tag is colored, rest is white text with gray timestamp
if context_str:
message = f"{level_color}[{level:<5}]{reset} {timestamp}{text_color}{event} | {context_str}{reset}"
else:
message = f"{level_color}[{level:<5}]{reset} {timestamp}{text_color}{event}{reset}"
# Output to stderr (standard for logs, keeps stdout clean)
print(message, file=sys.stderr, flush=True)
def error(self, event: str, **context):
"""
Log an error - always shown, indicates something failed
Use for:
- Action failures
- Network errors
- Parse errors
- Validation failures
- Unexpected exceptions
Args:
event: Error event name (e.g., "tile_fetch_failed")
**context: Additional context (user, action, error message, etc.)
"""
self._log(LogLevel.ERROR, event, **context)
def warn(self, event: str, **context):
"""
Log a warning - always shown, indicates something unexpected but handled
Use for:
- Fallback behavior activated
- Deprecated feature used
- Rate limiting triggered
- Auth retry needed
- Missing optional config
Args:
event: Warning event name (e.g., "auth_missing_sid")
**context: Additional context
"""
self._log(LogLevel.WARN, event, **context)
def info(self, event: str, **context):
"""
Log informational message - shown during startup, can be disabled
Use for:
- Module loaded
- Server started
- Configuration summary
- Connection established
Args:
event: Info event name (e.g., "module_loaded")
**context: Additional context
"""
self._log(LogLevel.INFO, event, **context)
def debug(self, event: str, **context):
"""
Log debug information - disabled by default, opt-in
Use for:
- Action execution traces
- Data transformations
- Flow control decisions
- Detailed state changes
Enable with: LogConfig.enable_debug()
Args:
event: Debug event name (e.g., "action_trace")
**context: Additional context
"""
self._log(LogLevel.DEBUG, event, **context)
# Global logger registry
_loggers: Dict[str, ContextLogger] = {}
def get_logger(module_name: str) -> ContextLogger:
"""
Get or create a logger for a specific module
Args:
module_name: Module identifier (e.g., "webserver", "dom_management")
Returns:
ContextLogger instance for this module
Example:
logger = get_logger("webserver")
logger.error("connection_failed", host="localhost", port=8080)
"""
if module_name not in _loggers:
_loggers[module_name] = ContextLogger(module_name)
return _loggers[module_name]
# Convenience function for quick usage
def log_error(module: str, event: str, **context):
"""Quick error logging without getting logger instance"""
get_logger(module).error(event, **context)
def log_warn(module: str, event: str, **context):
"""Quick warning logging without getting logger instance"""
get_logger(module).warn(event, **context)
def log_info(module: str, event: str, **context):
"""Quick info logging without getting logger instance"""
get_logger(module).info(event, **context)
def log_debug(module: str, event: str, **context):
"""Quick debug logging without getting logger instance"""
get_logger(module).debug(event, **context)

1
bot/mixins/__init__.py Normal file
View File

@@ -0,0 +1 @@
__all__ = ["action", "template", "trigger", "widget"]

106
bot/mixins/action.py Normal file
View File

@@ -0,0 +1,106 @@
from os import path, listdir, pardir
from importlib import import_module
from threading import Thread
from bot import loaded_modules_dict
import string
import random
class Action(object):
available_actions_dict = dict
trigger_action_hook = object
def __init__(self):
self.available_actions_dict = {}
self.trigger_action_hook = self.trigger_action
def register_action(self, identifier, action_dict):
self.available_actions_dict[identifier] = action_dict
def enable_action(self, identifier):
self.available_actions_dict[identifier]["enabled"] = True
def disable_action(self, identifier):
self.available_actions_dict[identifier]["enabled"] = False
@staticmethod
def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
return ''.join(random.choice(chars) for _ in range(size))
@staticmethod
def get_all_available_actions_dict():
all_available_actions_dict = {}
for loaded_module_identifier, loaded_module in loaded_modules_dict.items():
if len(loaded_module.available_actions_dict) >= 1:
all_available_actions_dict[loaded_module_identifier] = loaded_module.available_actions_dict
return all_available_actions_dict
def import_actions(self):
modules_root_dir = path.join(path.dirname(path.abspath(__file__)), pardir, "modules")
module_actions_root_dir = path.join(modules_root_dir, self.options['module_name'], "actions")
try:
for module_action in listdir(module_actions_root_dir):
if module_action == 'common.py' or module_action == '__init__.py' or module_action[-3:] != '.py':
continue
import_module("bot.modules." + self.options['module_name'] + ".actions." + module_action[:-3])
except FileNotFoundError as error:
# module does not have actions
pass
@staticmethod
def trigger_action(target_module, event_data=None, dispatchers_steamid=None):
if event_data is None:
event_data = []
action_identifier = event_data[0]
if action_identifier in target_module.available_actions_dict:
server_is_online = target_module.dom.data.get("module_telnet", {}).get("server_is_online", False)
active_action = target_module.available_actions_dict[action_identifier]
action_requires_server_to_be_online = active_action.get(
"requires_telnet_connection", False
)
action_is_enabled = active_action.get("enabled", False)
user_has_permission = event_data[1].get("has_permission", None)
# permission is None = no status has been set, so it's allowed (default)
# permission is True = Permission has been set by some other process
# permission is False = permission has not been granted by any module
if dispatchers_steamid is not None:
# none would be a system-call
pass
if action_is_enabled:
event_data[1]["module"] = target_module.getName()
event_data[1]["uuid4"] = target_module.id_generator(22)
if server_is_online is True or action_requires_server_to_be_online is not True:
if any([
user_has_permission is None,
user_has_permission is True
]):
Thread(
target=active_action.get("main_function"),
args=(target_module, event_data, dispatchers_steamid)
).start()
else:
# in case we don't have permission, we call the fail callback. it then can determine what to do
# next
fail_callback = active_action.get("callback_fail")
Thread(
target=target_module.callback_fail(
fail_callback,
target_module,
event_data,
dispatchers_steamid
),
args=(target_module, event_data, dispatchers_steamid)
).start()
else:
try:
skip_it_callback = active_action.get("skip_it")
Thread(
target=skip_it_callback,
args=(target_module, event_data)
).start()
except KeyError:
pass

15
bot/mixins/template.py Normal file
View File

@@ -0,0 +1,15 @@
from os import path, pardir
import jinja2
class Template(object):
templates = object
def __init__(self):
pass
def import_templates(self):
modules_root_dir = path.join(path.dirname(path.abspath(__file__)), pardir, "modules")
modules_template_dir = path.join(modules_root_dir, self.options['module_name'], 'templates')
file_loader = jinja2.FileSystemLoader(modules_template_dir)
self.templates = jinja2.Environment(loader=file_loader)

62
bot/mixins/trigger.py Normal file
View File

@@ -0,0 +1,62 @@
from bot import loaded_modules_dict
from os import path, listdir, pardir
from importlib import import_module
import re
class Trigger(object):
available_triggers_dict = dict
def __init__(self):
self.available_triggers_dict = {}
def start(self):
try:
for name, triggers in self.available_triggers_dict.items():
try:
for trigger, handler in triggers["handlers"].items():
self.dom.data.register_callback(self, trigger, handler)
except KeyError:
pass
except KeyError as error:
pass
def register_trigger(self, identifier, trigger_dict):
self.available_triggers_dict[identifier] = trigger_dict
def import_triggers(self):
modules_root_dir = path.join(path.dirname(path.abspath(__file__)), pardir, "modules")
module_triggers_root_dir = path.join(modules_root_dir, self.options['module_name'])
try:
for module_trigger in listdir(path.join(module_triggers_root_dir, "triggers")):
if module_trigger == 'common.py' or module_trigger == '__init__.py' or module_trigger[-3:] != '.py':
continue
import_module("bot.modules." + self.options['module_name'] + ".triggers." + module_trigger[:-3])
for module_trigger in listdir(path.join(module_triggers_root_dir, "commands")):
if module_trigger == 'common.py' or module_trigger == '__init__.py' or module_trigger[-3:] != '.py':
continue
import_module("bot.modules." + self.options['module_name'] + ".commands." + module_trigger[:-3])
except FileNotFoundError:
pass
except ModuleNotFoundError:
pass
def execute_telnet_triggers(self):
telnet_lines_to_process = self.telnet.get_a_bunch_of_lines_from_queue(25)
for telnet_line in telnet_lines_to_process:
for loaded_module in loaded_modules_dict.values():
for trigger_name, trigger_group in loaded_module.available_triggers_dict.items():
try:
for trigger in trigger_group["triggers"]:
regex_results = re.search(trigger["regex"], telnet_line)
if regex_results:
trigger["callback"](loaded_module, self, regex_results)
# TODO: add method to append log, or create a new one
# TODO: this needs to weed out triggers being called too often
except KeyError:
pass

87
bot/mixins/widget.py Normal file
View File

@@ -0,0 +1,87 @@
from os import path, listdir, pardir
from importlib import import_module
from bot import loaded_modules_dict
class Widget(object):
available_widgets_dict = dict
template_render_hook = object
def __init__(self):
self.available_widgets_dict = {}
self.template_render_hook = self.template_render
def on_socket_connect(self, steamid):
if isinstance(self.available_widgets_dict, dict) and len(self.available_widgets_dict) >= 1:
for name, widget in self.available_widgets_dict.items():
if widget["main_widget"] is not None:
widget["main_widget"](self, dispatchers_steamid=steamid)
def on_socket_disconnect(self, steamid):
if isinstance(self.available_widgets_dict, dict) and len(self.available_widgets_dict) >= 1:
for name, widget in self.available_widgets_dict.items():
if widget["main_widget"] is not None:
widget["main_widget"](self, dispatchers_steamid=steamid)
def on_socket_event(self, event_data, dispatchers_steamid):
pass
@staticmethod
def template_render(*args, **kwargs):
try:
template = kwargs.get("template", None)
rendered_template = template.render(**kwargs)
except AttributeError as error:
rendered_template = ""
return rendered_template
@staticmethod
def get_all_available_widgets_dict():
all_available_widgets_dict = {}
for loaded_module_identifier, loaded_module in loaded_modules_dict.items():
if len(loaded_module.available_widgets_dict) >= 1:
all_available_widgets_dict[loaded_module_identifier] = loaded_module.available_widgets_dict
return all_available_widgets_dict
def start(self):
if isinstance(self.available_widgets_dict, dict) and len(self.available_widgets_dict) >= 1:
for name, widget in self.available_widgets_dict.items():
for trigger, handler in widget["handlers"].items():
self.dom.data.register_callback(self, trigger, handler)
def register_widget(self, identifier, widget_dict):
if widget_dict.get("enabled", True):
self.available_widgets_dict[identifier] = widget_dict
def import_widgets(self):
modules_root_dir = path.join(path.dirname(path.abspath(__file__)), pardir, "modules")
module_widgets_root_dir = path.join(modules_root_dir, self.options['module_name'], "widgets")
try:
for module_widget in listdir(module_widgets_root_dir):
if module_widget == 'common.py' or module_widget == '__init__.py' or module_widget[-3:] != '.py':
continue
import_module("bot.modules." + self.options['module_name'] + ".widgets." + module_widget[:-3])
except FileNotFoundError as error:
# module does not have widgets
pass
def get_current_view(self, dispatchers_steamid):
return (
self.dom.data
.get(self.get_module_identifier(), {})
.get("visibility", {})
.get(dispatchers_steamid, {})
.get("current_view", "frontend")
)
def set_current_view(self, dispatchers_steamid, options):
self.dom.data.upsert({
self.get_module_identifier(): {
"visibility": {
dispatchers_steamid: options
}
}
}, dispatchers_steamid=dispatchers_steamid)

103
bot/module.py Normal file
View File

@@ -0,0 +1,103 @@
from threading import Thread, Event
from bot import started_modules_dict
from bot.logger import get_logger
from bot.mixins.trigger import Trigger
from bot.mixins.action import Action
from bot.mixins.template import Template
from bot.mixins.widget import Widget
logger = get_logger("module")
class Module(Thread, Action, Trigger, Template, Widget):
""" This class may ONLY be used to extend a module, it is not meant to be instantiated on it's own """
options = dict
stopped = object
run_observer_interval = int
run_observer_interval_idle = int
last_execution_time = float
def __init__(self):
if type(self) is Module:
raise NotImplementedError("You may not instantiate this class on it's own")
self.stopped = Event()
Action.__init__(self)
Trigger.__init__(self)
Template.__init__(self)
Widget.__init__(self)
Thread.__init__(self)
def setup(self, provided_options=dict):
self.options = self.default_options
options_filename = "module_" + self.options['module_name'] + ".json"
if isinstance(provided_options, dict):
self.options.update(provided_options)
logger.debug("module_options_loaded",
module=self.options['module_name'],
options_file=options_filename)
else:
logger.debug("module_options_defaults",
module=self.default_options["module_name"],
options_file=options_filename)
self.import_triggers()
self.import_actions()
self.import_templates()
self.import_widgets()
self.name = self.options['module_name']
return self
def start(self):
for required_module in self.required_modules:
setattr(self, required_module[7:], started_modules_dict[required_module])
setattr(self, self.name, self) # add self to dynamic module list to unify calls from actions
self.setDaemon(daemonic=True)
Thread.start(self)
Widget.start(self)
Trigger.start(self)
return self
def on_socket_connect(self, dispatchers_steamid):
Widget.on_socket_connect(self, dispatchers_steamid)
def on_socket_disconnect(self, dispatchers_steamid):
Widget.on_socket_disconnect(self, dispatchers_steamid)
def on_socket_event(self, event_data, dispatchers_steamid):
self.trigger_action_hook(self, event_data=event_data, dispatchers_steamid=dispatchers_steamid)
self.emit_event_status(self, event_data, dispatchers_steamid)
Widget.on_socket_event(self, event_data, dispatchers_steamid)
def emit_event_status(self, module, event_data, recipient_steamid, status=None):
# recipient_steamid can be None, "all" or [list_of_steamid's]
if recipient_steamid is not None and status is not None:
recipient_steamid = [recipient_steamid]
self.webserver.emit_event_status(module, event_data, recipient_steamid, status)
@staticmethod
def callback_success(callback, module, event_data, dispatchers_steamid, match=None):
event_data[1]["status"] = "success"
action_identifier = event_data[1]["action_identifier"]
if event_data[1].get("disable_after_success"):
module.disable_action(action_identifier)
module.emit_event_status(module, event_data, dispatchers_steamid, event_data[1])
callback(module, event_data, dispatchers_steamid, match)
@staticmethod
def callback_fail(callback, module, event_data, dispatchers_steamid):
event_data[1]["status"] = "fail"
logger.error("action_failed",
module=module.getName(),
action=event_data[0],
reason=event_data[1].get("fail_reason", "unknown"),
user=dispatchers_steamid)
module.emit_event_status(module, event_data, dispatchers_steamid, event_data[1])
callback(module, event_data, dispatchers_steamid)

11
bot/modules/__init__.py Normal file
View File

@@ -0,0 +1,11 @@
__all__ = [
'dom',
'webserver',
'dom_management',
'telnet',
'game_environment',
'players',
'locations',
'permissions',
'storage'
]

View File

@@ -0,0 +1,73 @@
import json
from bot.module import Module
from bot import loaded_modules_dict
from .callback_dict import CallbackDict
class Dom(Module):
data = CallbackDict
def __init__(self):
setattr(self, "default_options", {
"module_name": self.get_module_identifier()[7:]
})
setattr(self, "required_modules", [])
self.data = CallbackDict()
self.run_observer_interval = 5
Module.__init__(self)
@staticmethod
def get_module_identifier():
return "module_dom"
# region Standard module stuff
def setup(self, options=dict):
Module.setup(self, options)
# endregion
def get_updated_or_default_value(self, module_identifier, identifier, updated_values_dict, default_value):
try:
updated_or_default_value = updated_values_dict.get(
identifier, self.data.get(module_identifier).get(identifier, default_value)
)
except AttributeError as error:
updated_or_default_value = default_value
return updated_or_default_value
""" method to retrieve any dom elements based on their name or key """
def get_dom_element_by_query(
self,
dictionary=None,
target_module="module_dom",
query="",
current_layer=0,
path=None
):
starting_layer = len(loaded_modules_dict[target_module].dom_element_root)
if path is None:
path = []
if dictionary is None:
dictionary = self.data.get(target_module, {}).get("elements", {})
for key, value in dictionary.items():
if type(value) is dict:
yield from self.get_dom_element_by_query(
dictionary=value,
target_module=target_module,
query=query,
current_layer=current_layer + 1,
path=path + [key]
)
else:
if current_layer >= starting_layer and key == query:
yield path, key, value
@staticmethod
def pretty_print_dict(dict_to_print=dict):
print(json.dumps(dict_to_print, sort_keys=True, indent=4))
loaded_modules_dict[Dom().get_module_identifier()] = Dom()

View File

@@ -0,0 +1,514 @@
"""
Reactive Dictionary with Callbacks
A dictionary implementation that triggers callbacks when values are modified.
Similar to React's state management but for Python dictionaries.
Features:
- Monitors nested dictionary changes (insert, update, delete, append)
- Path-based callback registration with wildcard support
- Thread-safe callback execution
- Efficient path matching using pre-compiled patterns
"""
from typing import Dict, List, Any, Optional, Callable, Tuple
from collections.abc import Mapping
from collections import deque
from threading import Thread
from concurrent.futures import ThreadPoolExecutor
from copy import deepcopy
from functools import reduce
import operator
import re
from bot import loaded_modules_dict
from bot.constants import CALLBACK_THREAD_POOL_SIZE
from bot.logger import get_logger
logger = get_logger("telnet")
class CallbackDict(dict):
"""
A dictionary that triggers registered callbacks when its values change.
Callbacks can be registered for specific paths (e.g., "players/76561198012345678/name")
or with wildcards (e.g., "players/%steamid%/name").
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Structure: {depth: {path_pattern: [callback_info, ...]}}
self._callbacks: Dict[int, Dict[str, List[Dict]]] = {}
# Compiled regex patterns for fast matching: {path_pattern: compiled_regex}
self._compiled_patterns: Dict[str, re.Pattern] = {}
# Thread pool for callback execution (reuse threads instead of creating new ones)
self._executor = ThreadPoolExecutor(max_workers=CALLBACK_THREAD_POOL_SIZE, thread_name_prefix="callback")
# ==================== Path Utilities ====================
@staticmethod
def _split_path(path: str) -> List[str]:
"""Split a path string into components."""
return path.split("/") if path else []
@staticmethod
def _join_path(components: List[str]) -> str:
"""Join path components into a string."""
return "/".join(components)
@staticmethod
def _get_nested_value(data: dict, keys: List[str]) -> Any:
"""Get a value from a nested dictionary using a list of keys."""
if not keys:
return data
return reduce(operator.getitem, keys, data)
def _calculate_depth(self, d: Any) -> int:
"""Calculate the maximum depth of a nested dictionary."""
if not isinstance(d, dict) or not d:
return 0
return 1 + max((self._calculate_depth(v) for v in d.values()), default=0)
# ==================== Pattern Matching ====================
def _compile_pattern(self, path_pattern: str) -> re.Pattern:
"""
Compile a path pattern into a regex for efficient matching.
Wildcards like %steamid% become regex capture groups.
Example: "players/%steamid%/name" -> r"^players/([^/]+)/name$"
"""
if path_pattern in self._compiled_patterns:
return self._compiled_patterns[path_pattern]
# Escape special regex characters except our wildcard markers
escaped = re.escape(path_pattern)
# Convert %wildcard% to regex capture group
regex_pattern = re.sub(r'%[^%]+%', r'([^/]+)', escaped)
# Anchor the pattern
regex_pattern = f"^{regex_pattern}$"
compiled = re.compile(regex_pattern)
self._compiled_patterns[path_pattern] = compiled
return compiled
def _match_path(self, path: str, depth: int) -> List[str]:
"""
Find all registered callback patterns that match the given path.
Returns list of matching pattern strings.
"""
if depth not in self._callbacks:
return []
matching_patterns = []
for pattern in self._callbacks[depth].keys():
compiled_pattern = self._compile_pattern(pattern)
if compiled_pattern.match(path):
matching_patterns.append(pattern)
return matching_patterns
# ==================== Callback Management ====================
def register_callback(
self,
module: Any,
path_pattern: str,
callback: Callable
) -> None:
"""
Register a callback for a specific path pattern.
Args:
module: The module that owns this callback
path_pattern: Path to monitor (e.g., "players/%steamid%/name")
callback: Function to call when path changes
"""
depth = path_pattern.count('/')
# Initialize depth level if needed
if depth not in self._callbacks:
self._callbacks[depth] = {}
# Initialize pattern list if needed
if path_pattern not in self._callbacks[depth]:
self._callbacks[depth][path_pattern] = []
# Add callback info
self._callbacks[depth][path_pattern].append({
"callback": callback,
"module": module
})
def _collect_callbacks(
self,
path: str,
method: str,
updated_values: Any,
original_values: Any,
dispatchers_steamid: Optional[str],
min_depth: int = 0,
max_depth: Optional[int] = None
) -> List[Dict]:
"""
Collect all callbacks that should be triggered for a path change.
Returns list of callback packages ready for execution.
"""
depth = path.count('/')
# Check depth constraints
if max_depth is not None and depth > max_depth:
return []
if depth < min_depth:
return []
# Find matching patterns
matching_patterns = self._match_path(path, depth)
if not matching_patterns:
return []
# Build callback packages
packages = []
for pattern in matching_patterns:
for callback_info in self._callbacks[depth][pattern]:
packages.append({
"target": callback_info["callback"],
"args": (callback_info["module"],),
"kwargs": {
"updated_values_dict": updated_values,
"original_values_dict": original_values,
"dispatchers_steamid": dispatchers_steamid,
"method": method,
"matched_path": pattern
}
})
return packages
def _execute_callbacks(self, callback_packages: List[Dict]) -> None:
"""Execute a list of callback packages in separate threads."""
for package in callback_packages:
self._executor.submit(
self._safe_callback_wrapper,
package
)
def _safe_callback_wrapper(self, package: Dict) -> None:
"""
Wrapper that safely executes a callback with error handling.
This prevents callback errors from breaking the system and provides
visibility into what's failing.
"""
try:
package["target"](
*package["args"],
**package["kwargs"]
)
except Exception as error:
# Extract useful context for debugging
module = package["args"][0] if package["args"] else None
module_name = module.getName() if hasattr(module, 'getName') else 'unknown'
matched_path = package["kwargs"].get("matched_path", "unknown")
logger.error(
"callback_execution_failed",
module=module_name,
path=matched_path,
error=str(error),
error_type=type(error).__name__
)
# ==================== Dictionary Operations ====================
def upsert(
self,
updates: Dict,
dispatchers_steamid: Optional[str] = None,
min_callback_level: int = 0,
max_callback_level: Optional[int] = None
) -> None:
"""
Update or insert values into the dictionary.
This is the main method for modifying the dictionary. It handles nested
updates intelligently and triggers appropriate callbacks.
Args:
updates: Dictionary of values to upsert
dispatchers_steamid: ID of the user who triggered this change
min_callback_level: Minimum depth level for callbacks
max_callback_level: Maximum depth level for callbacks
"""
if not isinstance(updates, Mapping) or not updates:
return
# Make a snapshot of current state before any changes
original_state = deepcopy(dict(self))
# Determine max depth if not specified
if max_callback_level is None:
max_callback_level = self._calculate_depth(updates)
# Collect all callbacks that will be triggered
all_callbacks = []
# Process updates recursively
self._upsert_recursive(
current_dict=self,
updates=updates,
original_state=original_state,
path_components=[],
callbacks_accumulator=all_callbacks,
dispatchers_steamid=dispatchers_steamid,
min_depth=min_callback_level,
max_depth=max_callback_level
)
# Execute all collected callbacks
self._execute_callbacks(all_callbacks)
def _upsert_recursive(
self,
current_dict: dict,
updates: Dict,
original_state: Dict,
path_components: List[str],
callbacks_accumulator: List[Dict],
dispatchers_steamid: Optional[str],
min_depth: int,
max_depth: int
) -> None:
"""Recursive helper for upsert operation."""
current_depth = len(path_components)
for key, new_value in updates.items():
# Build the full path for this key
full_path_components = path_components + [key]
full_path = self._join_path(full_path_components)
# Determine the operation type
key_exists = key in current_dict
old_value = current_dict.get(key)
if key_exists:
# Update case
if isinstance(old_value, dict) and isinstance(new_value, dict):
# Both are dicts - recurse deeper
method = "update"
self._upsert_recursive(
current_dict=current_dict[key],
updates=new_value,
original_state=original_state.get(key, {}),
path_components=full_path_components,
callbacks_accumulator=callbacks_accumulator,
dispatchers_steamid=dispatchers_steamid,
min_depth=min_depth,
max_depth=max_depth
)
elif old_value == new_value:
# Value unchanged - skip callbacks
method = "unchanged"
else:
# Value changed - update it
method = "update"
current_dict[key] = new_value
else:
# Insert case
method = "insert"
current_dict[key] = new_value
# If inserted value is a dict, recurse through it
if isinstance(new_value, dict):
self._upsert_recursive(
current_dict=current_dict[key],
updates=new_value,
original_state={},
path_components=full_path_components,
callbacks_accumulator=callbacks_accumulator,
dispatchers_steamid=dispatchers_steamid,
min_depth=min_depth,
max_depth=max_depth
)
# Collect callbacks for this change (skip if unchanged)
if method != "unchanged":
callbacks = self._collect_callbacks(
path=full_path,
method=method,
updated_values=updates,
original_values=original_state,
dispatchers_steamid=dispatchers_steamid,
min_depth=min_depth,
max_depth=max_depth
)
callbacks_accumulator.extend(callbacks)
def append(
self,
updates: Dict,
dispatchers_steamid: Optional[str] = None,
maxlen: Optional[int] = None,
min_callback_level: int = 0,
max_callback_level: Optional[int] = None
) -> None:
"""
Append values to list entries in the dictionary.
If the target key doesn't exist, creates a new list.
If maxlen is specified, creates a deque with that maxlen.
Args:
updates: Dictionary mapping paths to values to append
dispatchers_steamid: ID of user who triggered this
maxlen: Maximum length for created lists (uses deque)
min_callback_level: Minimum depth for callbacks
max_callback_level: Maximum depth for callbacks
"""
if not isinstance(updates, Mapping) or not updates:
return
original_state = deepcopy(dict(self))
if max_callback_level is None:
max_callback_level = self._calculate_depth(updates)
all_callbacks = []
self._append_recursive(
current_dict=self,
updates=updates,
original_state=original_state,
path_components=[],
callbacks_accumulator=all_callbacks,
dispatchers_steamid=dispatchers_steamid,
maxlen=maxlen,
min_depth=min_callback_level,
max_depth=max_callback_level
)
self._execute_callbacks(all_callbacks)
def _append_recursive(
self,
current_dict: dict,
updates: Dict,
original_state: Dict,
path_components: List[str],
callbacks_accumulator: List[Dict],
dispatchers_steamid: Optional[str],
maxlen: Optional[int],
min_depth: int,
max_depth: int
) -> None:
"""Recursive helper for append operation."""
current_depth = len(path_components)
for key, value in updates.items():
full_path_components = path_components + [key]
full_path = self._join_path(full_path_components)
# Collect callbacks before making changes
callbacks = self._collect_callbacks(
path=full_path,
method="append",
updated_values=updates,
original_values=original_state,
dispatchers_steamid=dispatchers_steamid,
min_depth=min_depth,
max_depth=max_depth
)
callbacks_accumulator.extend(callbacks)
# Perform the append operation
if key in current_dict:
try:
current_dict[key].append(value)
except AttributeError:
# Not a list/deque, can't append
pass
else:
# Create new list or deque
if maxlen is not None:
current_dict[key] = deque(maxlen=maxlen)
else:
current_dict[key] = []
current_dict[key].append(value)
# If we found callbacks, don't recurse deeper (callback is at this level)
if callbacks:
return
# Otherwise, recurse if both are dicts
old_value = current_dict.get(key)
if isinstance(value, Mapping) and isinstance(old_value, Mapping):
self._append_recursive(
current_dict=old_value,
updates=value,
original_state=original_state.get(key, {}),
path_components=full_path_components,
callbacks_accumulator=callbacks_accumulator,
dispatchers_steamid=dispatchers_steamid,
maxlen=maxlen,
min_depth=min_depth,
max_depth=max_depth
)
def remove_key_by_path(
self,
key_path: List[str],
dispatchers_steamid: Optional[str] = None
) -> None:
"""
Remove a key from the dictionary by its path.
Args:
key_path: List of keys representing the path (e.g., ['players', '12345', 'name'])
dispatchers_steamid: ID of user who triggered this
"""
if not key_path:
return
# Get module's delete root to determine how much of the path to use
try:
module = loaded_modules_dict[key_path[0]]
delete_root = getattr(module, 'dom_element_select_root', [])
keys_to_ignore = len(delete_root) - 1 if delete_root else 0
except (KeyError, AttributeError):
keys_to_ignore = 0
# Build the path for callback matching
if keys_to_ignore >= 1:
path_for_callbacks = self._join_path(key_path[:-keys_to_ignore])
else:
path_for_callbacks = self._join_path(key_path)
# Collect callbacks
callbacks = self._collect_callbacks(
path=path_for_callbacks,
method="remove",
updated_values=key_path,
original_values={},
dispatchers_steamid=dispatchers_steamid,
min_depth=len(key_path),
max_depth=len(key_path)
)
# Perform the deletion
try:
parent = self._get_nested_value(self, key_path[:-1])
del parent[key_path[-1]]
except (KeyError, TypeError, IndexError):
# Key doesn't exist or path is invalid
pass
# Execute callbacks
self._execute_callbacks(callbacks)
def __del__(self):
"""Cleanup: shutdown the thread pool when the object is destroyed."""
try:
self._executor.shutdown(wait=False)
except:
pass

View File

@@ -0,0 +1,208 @@
from bot.module import Module
from bot import loaded_modules_dict
class DomManagement(Module):
# region Standard module stuff
def __init__(self):
setattr(self, "default_options", {
"module_name": self.get_module_identifier()[7:]
})
setattr(self, "required_modules", [
"module_dom",
"module_webserver"
])
self.run_observer_interval = 5
Module.__init__(self)
@staticmethod
def get_module_identifier():
return "module_dom_management"
def setup(self, options=dict):
Module.setup(self, options)
# endregion
# region Tools and workers
@staticmethod
def sanitize_for_html_id(value):
"""
Sanitize a string for use in HTML IDs.
Replaces spaces with underscores and converts to lowercase.
Args:
value: String to sanitize
Returns:
Sanitized string safe for HTML IDs
"""
return str(value).replace(" ", "_").lower()
def occurrences_of_key_in_nested_mapping(self, key, value):
for k, v in value.items():
if k == key:
yield v
elif isinstance(v, dict):
for result in self.occurrences_of_key_in_nested_mapping(key, v):
yield result
def get_dict_element_by_path(self, d, l):
if len(l) == 1:
return d.get(l[0], [])
return self.get_dict_element_by_path(d.get(l[0], {}), l[1:])
# endregion
# region Template functions
@staticmethod
def get_selection_dom_element(*args, **kwargs):
module = args[0]
return module.template_render_hook(
module,
template=module.dom_management.templates.get_template('control_select_link.html'),
dom_element_select_root=kwargs.get("dom_element_select_root"),
target_module=kwargs.get("target_module"),
dom_element_entry_selected=kwargs.get("dom_element_entry_selected"),
dom_element=kwargs.get("dom_element"),
dom_action_inactive=kwargs.get("dom_action_inactive"),
dom_action_active=kwargs.get("dom_action_active")
)
@staticmethod
def get_delete_button_dom_element(*args, **kwargs):
module = args[0]
return module.template_render_hook(
module,
template=module.dom_management.templates.get_template('control_action_delete_button.html'),
count=kwargs.get("count"),
target_module=kwargs.get("target_module"),
dom_element_root=kwargs.get("dom_element_root"),
dom_element_select_root=kwargs.get("dom_element_select_root"),
dom_action=kwargs.get("dom_action"),
delete_selected_entries_active=kwargs.get("count") >= 1,
dom_element_id=kwargs.get("dom_element_id"),
confirmed=kwargs.get("confirmed", "False")
)
@staticmethod
def get_delete_confirm_modal(*args, **kwargs):
module = args[0]
return module.template_render_hook(
module,
template=module.dom_management.templates.get_template('modal_confirm_delete.html'),
count=kwargs.get("count"),
target_module=kwargs.get("target_module"),
dom_element_root=kwargs.get("dom_element_root"),
dom_element_select_root=kwargs.get("dom_element_select_root"),
dom_action=kwargs.get("dom_action"),
delete_selected_entries_active=kwargs.get("count") >= 1,
dom_element_id=kwargs.get("dom_element_id"),
confirmed=kwargs.get("confirmed", "False")
)
@staticmethod
def update_selection_status(*args, **kwargs):
module = args[0]
updated_values_dict = kwargs.get("updated_values_dict", None)
target_module = kwargs.get("target_module", None)
dom_element_root = kwargs.get("dom_element_root", [])
dom_action_active = kwargs.get("dom_action_active", None)
dom_action_inactive = kwargs.get("dom_action_inactive", None)
dom_element_select_root = kwargs.get("dom_element_select_root", ["selected_by"])
dom_element_id = kwargs.get("dom_element_id", None)
# Use unsanitized dataset_original for DOM lookups (if available)
dom_element_origin = updated_values_dict.get("dataset_original", updated_values_dict.get("dataset"))
dom_element_owner = updated_values_dict["owner"]
dispatchers_steamid = kwargs.get("dispatchers_steamid", None)
# getting the base root for all elements. it's always this path if the module wants to use these built
# in functions
dom_element = (
module.dom.data
.get(target_module.get_module_identifier(), {})
.get("elements", {})
.get(dom_element_origin, {})
.get(dom_element_owner, {})
)
# get the individual element path, as provided by the module
for sub_dict in dom_element_root:
dom_element = dom_element.get(sub_dict)
dom_element_is_selected_by = dom_element.get("selected_by", [])
dom_element_entry_selected = False
if dispatchers_steamid in dom_element_is_selected_by:
dom_element_entry_selected = True
control_select_link = module.dom_management.get_selection_dom_element(
module,
target_module=target_module.get_module_identifier(),
dom_element_select_root=dom_element_select_root,
dom_element=dom_element,
dom_element_entry_selected=dom_element_entry_selected,
dom_action_inactive=dom_action_inactive,
dom_action_active=dom_action_active
)
module.webserver.send_data_to_client_hook(
module,
payload=control_select_link,
data_type="element_content",
clients=[dispatchers_steamid],
method="update",
target_element=dom_element_id
)
def update_delete_button_status(self, *args, **kwargs):
module = args[0]
target_module = kwargs.get("target_module", None)
dom_action = kwargs.get("dom_action", None)
dom_element_id = kwargs.get("dom_element_id", None)
template_action_delete_button = module.dom_management.templates.get_template('control_action_delete_button.html')
all_available_elements = (
module.dom.data
.get(target_module.get_module_identifier(), {})
.get("elements", {})
)
for clientid in module.webserver.connected_clients.keys():
all_selected_elements = 0
for dom_element_is_selected_by in self.occurrences_of_key_in_nested_mapping(
"selected_by",
all_available_elements
):
if clientid in dom_element_is_selected_by:
all_selected_elements += 1
data_to_emit = module.template_render_hook(
module,
template=template_action_delete_button,
dom_action=dom_action,
dom_element_root=kwargs.get("dom_element_root", []),
dom_element_select_root=kwargs.get("dom_element_select_root", []),
target_module=target_module.get_module_identifier(),
count=all_selected_elements,
delete_selected_entries_active=all_selected_elements >= 1,
dom_element_id=dom_element_id["id"],
confirmed=kwargs.get("confirmed", "False")
)
module.webserver.send_data_to_client_hook(
module,
payload=data_to_emit,
data_type="element_content",
clients=[clientid],
method="replace",
target_element=dom_element_id
)
# endregion
loaded_modules_dict[DomManagement().get_module_identifier()] = DomManagement()

View File

@@ -0,0 +1,66 @@
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)
target_module = event_data[1].get("target_module", None)
action_is_confirmed = event_data[1].get("confirmed", "False")
event_data[1]["action_identifier"] = action_name
if action == "delete_selected_dom_elements":
if action_is_confirmed == "True":
stuff_to_delete = []
for path, dom_element_key, dom_element in module.dom.get_dom_element_by_query(
target_module=target_module,
query="selected_by"
):
if dispatchers_steamid in dom_element:
stuff_to_delete.append([target_module, "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
)
module.callback_success(callback_success, module, event_data, dispatchers_steamid)
return
else:
loaded_modules_dict[target_module].set_current_view(dispatchers_steamid, {
"current_view": "delete-modal"
})
return
elif action == "cancel_delete_selected_dom_elements":
module.callback_success(callback_success, module, event_data, dispatchers_steamid)
return
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
return
def callback_success(module, event_data, dispatchers_steamid, match=None):
target_module = event_data[1].get("target_module", None)
loaded_modules_dict[target_module].set_current_view(dispatchers_steamid, {
"current_view": "frontend",
})
def callback_fail(module, event_data, dispatchers_steamid):
pass
action_meta = {
"description": "tools to help managing dom elements in the webinterface",
"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,120 @@
from bot import loaded_modules_dict
from bot.logger import get_logger
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]
logger = get_logger("dom_management.select")
def main_function(module, event_data, dispatchers_steamid):
action = event_data[1].get("action", None)
target_module = event_data[1].get("target_module", None)
event_data[1]["action_identifier"] = action_name
event_data[1]["fail_reason"] = []
dom_element_select_root = event_data[1].get("dom_element_select_root", ["selected_by"])
dom_element_origin = event_data[1].get("dom_element_origin", None)
dom_element_owner = event_data[1].get("dom_element_owner", None)
dom_element_identifier = event_data[1].get("dom_element_identifier", None)
if all([
action is not None,
dom_element_origin is not None,
dom_element_owner is not None,
dom_element_identifier is not None
]):
if action in [ # only proceed with known commands
"select_dom_element",
"deselect_dom_element"
]:
general_root = [target_module, "elements", dom_element_origin, dom_element_owner]
full_root = general_root + dom_element_select_root
selected_by_dict_element = module.dom_management.get_dict_element_by_path(module.dom.data, full_root)
# CRITICAL: Make a COPY of the list! Otherwise we modify the original list,
# and then upsert sees old_value == new_value (both are references to the same list)
# This would cause the callback to NOT fire because method="unchanged"
selected_by_dict_element = list(selected_by_dict_element)
try:
if action == "select_dom_element":
if dispatchers_steamid not in selected_by_dict_element:
selected_by_dict_element.append(dispatchers_steamid)
elif action == "deselect_dom_element":
if dispatchers_steamid in selected_by_dict_element:
selected_by_dict_element.remove(dispatchers_steamid)
except ValueError as error:
logger.error("select_list_manipulation_failed",
user=dispatchers_steamid,
action=action,
target_module=target_module,
origin=dom_element_origin,
owner=dom_element_owner,
identifier=dom_element_identifier,
error=str(error))
# Build data payload
data_payload = {
"selected_by": selected_by_dict_element,
"dataset": dom_element_origin,
"dataset_original": dom_element_origin,
"owner": dom_element_owner,
"identifier": dom_element_identifier
}
# Build nested structure dynamically based on dom_element_select_root
# All keys except the last one (which is "selected_by") define nesting levels
nested_keys = dom_element_select_root[:-1] # Remove "selected_by"
# Build nested dict from inside out
nested_data = data_payload
for key in reversed(nested_keys):
nested_data = {key: nested_data}
module.dom.data.upsert({
target_module: {
"elements": {
dom_element_origin: {
dom_element_owner: nested_data
}
}
}
}, dispatchers_steamid=dispatchers_steamid, min_callback_level=4)
module.callback_success(callback_success, module, event_data, dispatchers_steamid)
return
else:
event_data[1]["fail_reason"].append("unknown action")
else:
event_data[1]["fail_reason"].append("required options not set")
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
return
""" these will not be called directly. Always call the modules_callback and that will call this local function:
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": "handles selecting or deselecting an element in the dom for further actions",
"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,13 @@
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
{% set count_string = count|string %}
<div id="{{ dom_element_id }}" class="delete_button">
{{ construct_toggle_link(
delete_selected_entries_active,
label|default("delete") + " (" + count_string + ")", ['widget_event', ['dom_management', ['delete', {
"target_module": target_module,
"dom_element_root": dom_element_root,
'action': dom_action,
'confirmed': confirmed
}]]]
) }}
</div>

View File

@@ -0,0 +1,24 @@
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
{% set owner = dom_element.owner|default("null") %}
{% set identifier = dom_element.identifier|default("null") %}
{% set dataset = dom_element.dataset|default("null") %}
{% set dataset_original = dom_element.dataset_original|default(dataset) %}
{{ construct_toggle_link(
dom_element_entry_selected,
"&#9745;", ['widget_event', ['dom_management', ['select', {
"dom_element_select_root": dom_element_select_root,
"target_module": target_module,
"dom_element_owner": owner,
"dom_element_identifier": identifier,
"dom_element_origin": dataset_original,
'action': dom_action_active
}]]],
"&#9744;", ['widget_event', ['dom_management', ['select', {
"dom_element_select_root": dom_element_select_root,
"target_module": target_module,
"dom_element_owner": owner,
"dom_element_identifier": identifier,
"dom_element_origin": dataset_original,
"action": dom_action_inactive
}]]]
) }}

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,75 @@
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
{% set count_string = count|string %}
<div class="delete_modal">
<header>
<p>Make sure you've got the right stuff selected! Deletions can not be undone.</p>
</header>
<div>
<p>You have <span class="selected_dom_elements">{{ count_string }} elements</span> selected for deletion:</p>
</div>
<div class="dynamic_content_size">
<p>This will soon be an actual list of actual elements to delete</p>
<ul>
<li>Element 1</li>
<li>Element 2</li>
<li>Element 3</li>
<li>Element 4</li>
<li>Element 5</li>
<li>Element 6</li>
<li>Element 7</li>
<li>Element 8</li>
<li>Element 9</li>
<li>Element 10</li>
<li>Element 11</li>
<li>Element 12</li>
<li>Element 13</li>
<li>Element 14</li>
<li>Element 15</li>
<li>Element 16</li>
<li>Element 17</li>
<li>Element 18</li>
<li>Element 19</li>
<li>Element 20</li>
<li>Element 21</li>
<li>Element 22</li>
<li>Element 23</li>
<li>Element 24</li>
<li>Element 25</li>
<li>Element 26</li>
<li>Element 27</li>
<li>Element 28</li>
<li>Element 29</li>
<li>Element 30</li>
<li>Element 31</li>
<li>Element 32</li>
<li>Element 33</li>
<li>Element 34</li>
<li>Element 35</li>
<li>Element 36</li>
<li>Element 37</li>
<li>Element 38</li>
<li>Element 39</li>
<li>Element 40</li>
</ul>
</div>
<div>
<section>
<p>
By clicking [confirm] you will continue to proceed deleting
<span class="selected_dom_elements">{{ count_string }} elements</span>.
</p>
<p>
{% include "modal_confirm_delete_confirm_button.html" %}
</p>
</section>
<section>
<p>
Clicking [cancel] will abort the deletion process,
it will keep the selection intact.
</p>
<p>
{% include "modal_confirm_delete_cancel_button.html" %}
</p>
</section>
</div>
</div>

View File

@@ -0,0 +1,11 @@
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
<div id="{{ dom_element_id }}_cancel" class="modal_cancel_button">
{{ construct_toggle_link(
delete_selected_entries_active,
label|default("cancel"), ['widget_event', ['dom_management', ['delete', {
"target_module": target_module,
"dom_element_root": dom_element_root,
'action': "cancel_delete_selected_dom_elements"
}]]]
) }}
</div>

View File

@@ -0,0 +1,12 @@
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
<div id="{{ dom_element_id }}_confirm" class="modal_delete_button">
{{ construct_toggle_link(
delete_selected_entries_active,
label|default("confirm"), ['widget_event', ['dom_management', ['delete', {
"target_module": target_module,
"dom_element_root": dom_element_root,
'action': dom_action,
'confirmed': confirmed
}]]]
) }}
</div>

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)

View File

@@ -0,0 +1,252 @@
from bot.module import Module
from bot import loaded_modules_dict
from time import time
import math
class Locations(Module):
dom_element_root = list
dom_element_select_root = list
default_max_locations = int
standard_location_shape = str
def __init__(self):
setattr(self, "default_options", {
"module_name": self.get_module_identifier()[7:],
"dom_element_root": ["%dom_element_identifier%"],
"dom_element_select_root": ["%dom_element_identifier%", "selected_by"],
"default_max_locations": 3,
"standard_location_shape": "rectangular",
"run_observer_interval": 3,
"run_observer_interval_idle": 10
})
setattr(self, "required_modules", [
'module_dom',
'module_dom_management',
'module_game_environment',
'module_players',
'module_telnet',
'module_webserver'
])
self.next_cycle = 0
self.all_available_actions_dict = {}
self.all_available_widgets_dict = {}
Module.__init__(self)
@staticmethod
def get_module_identifier():
return "module_locations"
# 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.run_observer_interval_idle = self.options.get(
"run_observer_interval_idle", self.default_options.get("run_observer_interval_idle", 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)
)
self.default_max_locations = self.options.get(
"default_max_locations", self.default_options.get("default_max_locations", None)
)
self.standard_location_shape = self.options.get(
"standard_location_shape", self.default_options.get("standard_location_shape", None)
)
# endregion
def start(self):
""" all modules have been loaded and initialized by now. we can bend the rules here."""
Module.start(self)
# endregion
def get_elements_by_type(self, location_type: str, var=None):
if var is None:
active_dataset = self.dom.data.get("module_game_environment", {}).get("active_dataset", None)
var = (
self.dom.data
.get("module_locations", {})
.get("elements", {})
.get(active_dataset, {})
)
if hasattr(var, 'items'):
for k, v in var.items():
if k == "type":
if location_type in v:
yield var
if isinstance(v, dict):
for result in self.get_elements_by_type(location_type, v):
yield result
elif isinstance(v, list):
for d in v:
for result in self.get_elements_by_type(location_type, d):
yield result
@staticmethod
def get_location_volume(location_dict):
"""
Calculate the 3D volume coordinates for a box-shaped location.
Handles different map quadrants (SW, SE, NE, NW) with appropriate
coordinate adjustments for the game's coordinate system.
Args:
location_dict: Dictionary containing shape, dimensions, and coordinates
Returns:
Dictionary with pos_x, pos_y, pos_z, pos_x2, pos_y2, pos_z2
or None if not a box shape
"""
shape = location_dict.get("shape", None)
if shape != "box":
return None
dimensions = location_dict.get("dimensions", None)
coords = location_dict.get("coordinates", None)
# Convert coordinates to integers
x = int(float(coords["x"]))
y = int(float(coords["y"]))
z = int(float(coords["z"]))
width = float(dimensions["width"])
height = float(dimensions["height"])
length = float(dimensions["length"])
# Determine quadrant and calculate adjustments
is_west = x < 0 # West quadrants (SW, NW)
is_south = z < 0 # South quadrants (SW, SE)
# Base coordinates
pos_x = x - 1 if is_west else x
pos_z = z - 1
# Z2 adjustment depends on quadrant
z2_offset = -2 if is_south else -1
return {
"pos_x": pos_x,
"pos_y": y,
"pos_z": pos_z,
"pos_x2": int(x - width),
"pos_y2": int(y + height - 1),
"pos_z2": int(z + length + z2_offset)
}
@staticmethod
def _is_inside_sphere(player_pos, center, radius):
"""Check if position is inside a 3D sphere."""
distance = math.sqrt(
(player_pos['x'] - center['x']) ** 2 +
(player_pos['y'] - center['y']) ** 2 +
(player_pos['z'] - center['z']) ** 2
)
return distance <= radius
@staticmethod
def _is_inside_circle(player_pos, center, radius):
"""Check if position is inside a 2D circle (ignores Y axis)."""
distance = math.sqrt(
(player_pos['x'] - center['x']) ** 2 +
(player_pos['z'] - center['z']) ** 2
)
return distance <= radius
@staticmethod
def _is_inside_box(player_pos, corner, dimensions):
"""Check if position is inside a 3D box."""
return all([
player_pos['x'] - dimensions['width'] <= corner['x'] <= player_pos['x'] + dimensions['width'],
player_pos['y'] - dimensions['height'] <= corner['y'] <= player_pos['y'] + dimensions['height'],
player_pos['z'] - dimensions['length'] <= corner['z'] <= player_pos['z'] + dimensions['length']
])
@staticmethod
def _is_inside_rectangle(player_pos, corner, dimensions):
"""Check if position is inside a 2D rectangle (ignores Y axis)."""
return all([
player_pos['x'] - dimensions['width'] <= corner['x'] <= player_pos['x'] + dimensions['width'],
player_pos['z'] - dimensions['length'] <= corner['z'] <= player_pos['z'] + dimensions['length']
])
@staticmethod
def position_is_inside_boundary(position_dict=None, boundary_dict=None):
"""
Check if a position is inside a boundary shape.
Supports multiple shape types: spherical, circle, box, rectangular.
Args:
position_dict: Dictionary with 'pos' containing x, y, z coordinates
boundary_dict: Dictionary with 'shape', 'dimensions', 'coordinates'
Returns:
bool: True if position is inside boundary, False otherwise
"""
if not all([position_dict, boundary_dict]):
return False
shape = boundary_dict.get("shape")
dimensions = boundary_dict.get("dimensions")
coordinates = boundary_dict.get("coordinates")
if not all([shape, dimensions, coordinates]):
return False
# Extract player position
player_pos = {
'x': float(position_dict.get("pos", {}).get("x", 0)),
'y': float(position_dict.get("pos", {}).get("y", 0)),
'z': float(position_dict.get("pos", {}).get("z", 0))
}
# Extract boundary center/corner coordinates
boundary_coords = {
'x': float(coordinates.get("x", 0)),
'y': float(coordinates.get("y", 0)),
'z': float(coordinates.get("z", 0))
}
# Check shape type and delegate to appropriate helper
if shape == "spherical":
radius = float(dimensions.get("radius", 0))
return Locations._is_inside_sphere(player_pos, boundary_coords, radius)
elif shape == "circle":
radius = float(dimensions.get("radius", 0))
return Locations._is_inside_circle(player_pos, boundary_coords, radius)
elif shape == "box":
dims = {
'width': float(dimensions.get("width", 0)),
'height': float(dimensions.get("height", 0)),
'length': float(dimensions.get("length", 0))
}
return Locations._is_inside_box(player_pos, boundary_coords, dims)
elif shape == "rectangular":
dims = {
'width': float(dimensions.get("width", 0)),
'length': float(dimensions.get("length", 0))
}
return Locations._is_inside_rectangle(player_pos, boundary_coords, dims)
return False
def run(self):
while not self.stopped.wait(self.next_cycle):
profile_start = time()
self.last_execution_time = time() - profile_start
self.next_cycle = self.run_observer_interval - self.last_execution_time
loaded_modules_dict[Locations().get_module_identifier()] = Locations()

View File

@@ -0,0 +1,55 @@
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):
event_data[1]["action_identifier"] = action_name
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
location_identifier = event_data[1].get("location_identifier")
location_dict = (
module.dom.data.get("module_locations", {})
.get("elements", {})
.get(active_dataset, {})
.get(dispatchers_steamid, {})
.get(location_identifier, None)
)
coordinates = module.get_location_volume(location_dict)
if coordinates is not None:
command = (
"bc-export {location_to_be_exported} {pos_x} {pos_y} {pos_z} {pos_x2} {pos_y2} {pos_z2}"
).format(
location_to_be_exported=location_dict.get("identifier"),
**coordinates
)
module.telnet.add_telnet_command_to_queue(command)
module.callback_success(callback_success, module, event_data, dispatchers_steamid)
return
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
return
def callback_success(module, event_data, dispatchers_steamid, match=None):
pass
def callback_fail(module, event_data, dispatchers_steamid):
pass
action_meta = {
"description": "Will export everything inside the locations Volume. Will only work for 'box' locations",
"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,97 @@
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 fix_coordinates_for_bc_import(location_dict: dict, coordinates: dict, player_dict=None) -> bool:
shape = location_dict.get("shape", None)
dimensions = location_dict.get("dimensions", None)
location_coordinates = location_dict.get("coordinates", None)
if shape == "box":
if player_dict is None:
if int(float(location_coordinates["x"])) < 0: # W Half
coordinates["pos_x"] = int(float(coordinates["pos_x"]) - float(dimensions["width"]) + 1)
if int(float(location_coordinates["x"])) >= 0: # E Half
coordinates["pos_x"] = int(float(coordinates["pos_x"]) - float(dimensions["width"]))
else:
if int(float(player_dict.get("pos", {}).get("x"))) < 0: # W Half
coordinates["pos_x"] = int(float(player_dict.get("pos", {}).get("x")) - float(dimensions["width"]) - 1)
if int(float(player_dict.get("pos", {}).get("x"))) >= 0: # E Half
coordinates["pos_x"] = int(float(player_dict.get("pos", {}).get("x")) - float(dimensions["width"]))
coordinates["pos_y"] = int(float(player_dict.get("pos", {}).get("y")))
if int(float(player_dict.get("pos", {}).get("z"))) < 0: # S Half
coordinates["pos_z"] = int(float(player_dict.get("pos", {}).get("z")) - 1)
if int(float(player_dict.get("pos", {}).get("z"))) >= 0: # N Half
coordinates["pos_z"] = int(float(player_dict.get("pos", {}).get("z")))
return True
else:
return False
def main_function(module, event_data, dispatchers_steamid):
event_data[1]["action_identifier"] = action_name
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
location_identifier = event_data[1].get("location_identifier")
spawn_in_place = event_data[1].get("spawn_in_place")
location_dict = (
module.dom.data.get("module_locations", {})
.get("elements", {})
.get(active_dataset, {})
.get(dispatchers_steamid, {})
.get(location_identifier, None)
)
coordinates = module.get_location_volume(location_dict)
if coordinates is not None:
if spawn_in_place:
player_dict = (
module.dom.data.get("module_players", {})
.get("elements", {})
.get(active_dataset, {})
.get(dispatchers_steamid, {})
)
fix_coordinates_for_bc_import(location_dict, coordinates, player_dict)
else:
fix_coordinates_for_bc_import(location_dict, coordinates)
command = (
"bc-import {location_to_be_imported} {pos_x} {pos_y} {pos_z}"
).format(
location_to_be_imported=location_dict.get("identifier"),
**coordinates
)
module.telnet.add_telnet_command_to_queue(command)
module.callback_success(callback_success, module, event_data, dispatchers_steamid)
return
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
return
def callback_success(module, event_data, dispatchers_steamid, match=None):
pass
def callback_fail(module, event_data, dispatchers_steamid):
pass
action_meta = {
"description": "Imports a saved prefab. Needs to have a location first!",
"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,82 @@
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):
location_identifier = event_data[1].get("location_identifier", None)
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
event_data[1]["action_identifier"] = action_name
event_data[1]["fail_reason"] = []
location_owner = event_data[1].get("location_owner", dispatchers_steamid)
location_name = event_data[1].get("location_name", None)
if location_identifier is None or location_identifier == "":
location_identifier = ''.join(e for e in location_name if e.isalnum())
location_shape = event_data[1].get("location_shape", module.default_options.get("standard_location_shape", None))
location_types = event_data[1].get("location_type", [])
location_coordinates = event_data[1].get("location_coordinates", {})
location_teleport_entry = event_data[1].get("location_teleport_entry", {})
location_dimensions = event_data[1].get("location_dimensions", {})
location_enabled = event_data[1].get("is_enabled", False)
last_changed = event_data[1].get("last_changed", False)
if all([
location_name is not None and len(location_name) >= 3,
location_identifier is not None,
location_shape is not None,
active_dataset is not None,
location_owner is not None
]):
module.dom.data.upsert({
module.get_module_identifier(): {
"elements": {
active_dataset: {
str(location_owner): {
location_identifier: {
"name": location_name,
"identifier": location_identifier,
"dataset": active_dataset,
"shape": location_shape,
"type": location_types,
"coordinates": location_coordinates,
"teleport_entry": location_teleport_entry,
"dimensions": location_dimensions,
"owner": str(location_owner),
"is_enabled": location_enabled,
"selected_by": [],
"last_changed": last_changed
}
}
}
}
}
}, dispatchers_steamid=dispatchers_steamid, max_callback_level=4)
module.callback_success(callback_success, module, event_data, dispatchers_steamid)
return
event_data[1]["fail_reason"].append("not all conditions met!")
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": "manages location 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,48 @@
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):
action = event_data[1].get("action", None)
event_data[1]["action_identifier"] = action_name
location_identifier = event_data[1].get("location_identifier", None)
if action == "start onslaught":
event_data = ['say_to_player', {
'steamid': dispatchers_steamid,
'message': '[66FF66]Onslaught[-][FFFFFF] Started for location [66FF66]{}[-]'.format(location_identifier)
}]
module.trigger_action_hook(module.players, event_data=event_data)
elif action == "stop onslaught":
event_data = ['say_to_player', {
'steamid': dispatchers_steamid,
'message': '[66FF66]Onslaught[-][FFFFFF] in location [66FF66]{}[-] Ended[-]'.format(location_identifier)
}]
module.trigger_action_hook(module.players, event_data=event_data)
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 the onslaught event on dedicated locations",
"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,42 @@
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):
event_data[1]["action_identifier"] = action_name
location_coordinates = event_data[1].get("location_coordinates", {})
player_steamid = event_data[1].get("steamid", dispatchers_steamid)
if location_coordinates:
module.trigger_action_hook(
module.players, event_data=["teleport_player", {
"steamid": player_steamid,
"coordinates": location_coordinates
}])
module.callback_success(callback_success, module, event_data, dispatchers_steamid)
return
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
return
def callback_success(module, event_data, dispatchers_steamid, match=None):
pass
def callback_fail(module, event_data, dispatchers_steamid):
pass
action_meta = {
"description": "Teleports a player to a set of coordinates",
"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
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):
event_data[1]["action_identifier"] = action_name
action = event_data[1].get("action", None)
location_origin = event_data[1].get("dom_element_origin", None)
location_owner = event_data[1].get("dom_element_owner", None)
location_identifier = event_data[1].get("dom_element_identifier", None)
if all([
action is not None
]):
if action == "enable_location_entry" or action == "disable_location_entry":
element_is_enabled = action == "enable_location_entry"
module.dom.data.upsert({
"module_locations": {
"elements": {
location_origin: {
location_owner: {
location_identifier: {
"is_enabled": element_is_enabled
}
}
}
}
}
}, dispatchers_steamid=dispatchers_steamid, min_callback_level=4)
module.callback_success(callback_success, module, event_data, dispatchers_steamid)
return
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
return
def callback_success(module, event_data, dispatchers_steamid, match=None):
pass
def callback_fail(module, event_data, dispatchers_steamid):
pass
action_meta = {
"description": "Sets or removes the enabled flag of a location",
"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,72 @@
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):
event_data[1]["action_identifier"] = action_name
action = event_data[1].get("action", None)
location_owner = event_data[1].get("dom_element_owner", None)
location_identifier = event_data[1].get("dom_element_identifier", None)
location_origin = event_data[1].get("dom_element_origin", None)
# Support for prefilled coordinates from map
prefill_x = event_data[1].get("prefill_x", None)
prefill_y = event_data[1].get("prefill_y", None)
prefill_z = event_data[1].get("prefill_z", None)
if action == "show_options":
current_view = "options"
elif action == "show_frontend":
current_view = "frontend"
elif action == "show_create_new":
current_view = "create_new"
elif action == "edit_location_entry":
current_view = "edit_location_entry"
elif action == "show_special_locations":
current_view = "special_locations"
elif action == "show_map":
current_view = "map"
else:
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
return
view_data = {
"current_view": current_view,
"location_owner": location_owner,
"location_identifier": location_identifier,
"location_origin": location_origin
}
# Add prefill data if creating new location
if current_view == "create_new" and any([prefill_x, prefill_y, prefill_z]):
view_data["prefill_coordinates"] = {
"x": prefill_x,
"y": prefill_y,
"z": prefill_z
}
module.set_current_view(dispatchers_steamid, view_data)
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 location 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,64 @@
from bot import loaded_modules_dict
from bot import telnet_prefixes
from os import path, pardir
import re
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")
steamid = regex_result.group("player_steamid")
result = re.match(r"^.*add\slocation\s(?P<location_name>.*)", command)
if result:
location_name = result.group("location_name")
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(steamid, {})
if len(player_dict) >= 1 and result:
event_data = ['edit_location', {
'location_coordinates': {
"x": player_dict["pos"]["x"],
"y": player_dict["pos"]["y"],
"z": player_dict["pos"]["z"]
},
'location_name': location_name,
'action': 'create_new',
'last_changed': module.game_environment.get_last_recorded_gametime_string()
}]
module.trigger_action_hook(origin_module, event_data=event_data, dispatchers_steamid=steamid)
triggers = {
"add location": r"\'(?P<player_name>.*)\'\:\s(?P<command>\/add location.*)"
}
trigger_meta = {
"description": "catches location commands from the players chat and then adds them to the database",
"main_function": main_function,
"triggers": [
{
"identifier": "add location (Alloc)",
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
telnet_prefixes["Allocs"]["chat"] +
triggers["add location"]
),
"callback": main_function
},
{
"identifier": "add location (BCM)",
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
telnet_prefixes["BCM"]["chat"] +
triggers["add location"]
),
"callback": main_function
}
]
}
loaded_modules_dict["module_" + module_name].register_trigger(trigger_name, trigger_meta)

View File

@@ -0,0 +1,77 @@
from bot import loaded_modules_dict
from bot import telnet_prefixes
from os import path, pardir
import re
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")
player_steamid = regex_result.group("player_steamid")
location_dict = None
result = re.match(r"^.*export\slocation\s(?P<location_identifier>.*)(?:\s)?(?P<spawn_in_place>.*)?", command)
if result:
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
location_identifier = result.group("location_identifier")
location_dict = (
module.dom.data.get("module_locations", {})
.get("elements", {})
.get(active_dataset, {})
.get(player_steamid, {})
.get(location_identifier, None)
)
location_name = location_dict.get("name")
else:
location_name = "None Provided"
location_identifier = "None"
if location_dict is not None:
event_data = ['bc-export', {
"location_identifier": location_identifier
}]
module.trigger_action_hook(origin_module.locations, event_data=event_data, dispatchers_steamid=player_steamid)
else:
event_data = ['say_to_player', {
'steamid': player_steamid,
'message': '[FFFFFF]Could not find [66FF66]{location_name} ({location_identifier})[-]'.format(
location_name=location_name,
location_identifier=location_identifier
)
}]
module.trigger_action_hook(origin_module.players, event_data=event_data)
triggers = {
"export location": r"\'(?P<player_name>.*)\'\:\s(?P<command>\/export location.*)"
}
trigger_meta = {
"description": "will issue the BCM mods bc-export command on the specified location",
"main_function": main_function,
"triggers": [
{
"identifier": "export location",
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
telnet_prefixes["Allocs"]["chat"] +
triggers["export location"]
),
"callback": main_function
},
{
"identifier": "export location",
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
telnet_prefixes["BCM"]["chat"] +
triggers["export location"]
),
"callback": main_function
}
]
}
loaded_modules_dict["module_" + module_name].register_trigger(trigger_name, trigger_meta)

View File

@@ -0,0 +1,78 @@
from bot import loaded_modules_dict
from bot import telnet_prefixes
from os import path, pardir
import re
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")
player_steamid = regex_result.group("player_steamid")
location_dict = None
spawn_in_place = False
result = re.match(r"^.*import\slocation\s(?P<location_identifier>\S+)(?:\s)?(?P<spawn_in_place>here)?", command)
if result:
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
location_identifier = result.group("location_identifier")
location_dict = (
module.dom.data
.get("module_locations", {})
.get("elements", {})
.get(active_dataset, {})
.get(player_steamid, {})
.get(location_identifier, None)
)
spawn_in_place = result.group("spawn_in_place") == "here"
if location_dict is not None:
event_data = ['bc-import', {
"location_identifier": location_identifier,
"spawn_in_place": spawn_in_place
}]
module.trigger_action_hook(origin_module.locations, event_data=event_data, dispatchers_steamid=player_steamid)
else:
event_data = ['say_to_player', {
'steamid': player_steamid,
'message': '[FFFFFF]Could not find [66FF66]{location_name} ({location_identifier})[-]'.format(
location_name="None Provided",
location_identifier="None"
)
}]
module.trigger_action_hook(origin_module.players, event_data=event_data)
triggers = {
"import location": r"\'(?P<player_name>.*)\'\:\s(?P<command>\/import location.*)"
}
trigger_meta = {
"description": "will issue the BCM mods bc-import command on the specified location",
"main_function": main_function,
"triggers": [
{
"identifier": "import location (Allocs)",
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
telnet_prefixes["Allocs"]["chat"] +
triggers["import location"]
),
"callback": main_function
},
{
"identifier": "import location (BCM)",
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
telnet_prefixes["BCM"]["chat"] +
triggers["import location"]
),
"callback": main_function
}
]
}
loaded_modules_dict["module_" + module_name].register_trigger(trigger_name, trigger_meta)

View File

@@ -0,0 +1,59 @@
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):
player_steamid = regex_result.group("player_steamid")
found_home = False
location_dict = {}
for home in origin_module.get_elements_by_type("is_home"):
if home.get("owner") == player_steamid:
location_dict = home
found_home = True
if found_home is True and len(location_dict) >= 1:
event_data = ['teleport_to_coordinates', {
'location_coordinates': {
"x": location_dict.get("teleport_entry", {}).get("x", location_dict["coordinates"]["x"]),
"y": location_dict.get("teleport_entry", {}).get("y", location_dict["coordinates"]["y"]),
"z": location_dict.get("teleport_entry", {}).get("z", location_dict["coordinates"]["z"])
}
}]
module.trigger_action_hook(origin_module, event_data=event_data, dispatchers_steamid=player_steamid)
triggers = {
"send me home": r"\'(?P<player_name>.*)\'\:\s(?P<command>\/send\sme\shome)"
}
trigger_meta = {
"description": "sends the player to his home, if available",
"main_function": main_function,
"triggers": [
{
"identifier": "send me home",
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
telnet_prefixes["Allocs"]["chat"] +
triggers["send me home"]
),
"callback": main_function
},
{
"identifier": "send me home",
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
telnet_prefixes["BCM"]["chat"] +
triggers["send me home"]
),
"callback": main_function
}
]
}
loaded_modules_dict["module_" + module_name].register_trigger(trigger_name, trigger_meta)

View File

@@ -0,0 +1,102 @@
from bot import loaded_modules_dict
from bot import telnet_prefixes
from os import path, pardir
import re
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")
player_steamid = regex_result.group("player_steamid")
result = re.match(r"^.*send\sme\sto\slocation\s(?P<location_identifier>.*)", command)
if result:
location_identifier = result.group("location_identifier")
else:
return
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
location_dict = (
module.dom.data.get("module_locations", {})
.get("elements", {})
.get(active_dataset, {})
.get(player_steamid, {})
.get(location_identifier, {})
)
player_dict = (
module.dom.data.get("module_players", {})
.get("elements", {})
.get(active_dataset, {})
.get(player_steamid, {})
)
if len(player_dict) >= 1 and len(location_dict) >= 1:
teleport_entry = location_dict.get("teleport_entry", {})
teleport_entry_x = teleport_entry.get("x", None)
teleport_entry_y = teleport_entry.get("y", None)
teleport_entry_z = teleport_entry.get("z", None)
if any([
teleport_entry_x is None,
teleport_entry_y is None,
teleport_entry_z is None,
all([
int(teleport_entry_x) == 0,
int(teleport_entry_y) == 0,
int(teleport_entry_z) == 0,
])
]):
location_coordinates = {
"x": location_dict["coordinates"]["x"],
"y": location_dict["coordinates"]["y"],
"z": location_dict["coordinates"]["z"]
}
else:
location_coordinates = {
"x": teleport_entry_x,
"y": teleport_entry_y,
"z": teleport_entry_z
}
event_data = ['teleport_to_coordinates', {
'location_coordinates': location_coordinates
}]
module.trigger_action_hook(origin_module, event_data=event_data, dispatchers_steamid=player_steamid)
triggers = {
"send me to location": r"\'(?P<player_name>.*)\'\:\s(?P<command>\/send\sme\sto\slocation.*)"
}
trigger_meta = {
"description": (
"sends player to the location of their choosing, will use the teleport_entry coordinates if available"
),
"main_function": main_function,
"triggers": [
{
"identifier": "add location (Alloc)",
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
telnet_prefixes["Allocs"]["chat"] +
triggers["send me to location"]
),
"callback": main_function
},
{
"identifier": "add location (BCM)",
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
telnet_prefixes["BCM"]["chat"] +
triggers["send me to location"]
),
"callback": main_function
}
]
}
loaded_modules_dict["module_" + module_name].register_trigger(trigger_name, trigger_meta)

View File

@@ -0,0 +1,116 @@
from bot import loaded_modules_dict
from bot import telnet_prefixes
from os import path, pardir
import re
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")
steamid = regex_result.group("player_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(steamid, {})
)
if len(player_dict) < 1:
return False
result = re.match(r"^.*st.*\sonslaught\s(?P<onslaught_options>.*)", command)
if result:
onslaught_options = result.group("onslaught_options")
else:
""" no options provided
might later chose the location one is standing in and owns, or let some other stuff happen
"""
onslaught_options = None
if command.startswith("/start onslaught"):
""" check if the player is inside a location which allows onslaught to be enabled """
# let's iterate through all suitable locations
for onslaught_location in origin_module.get_elements_by_type("is_onslaught"):
# only proceed with the player is inside a dedicated location
if any([
onslaught_options in ["everywhere", onslaught_location["identifier"]],
origin_module.position_is_inside_boundary(player_dict, onslaught_location)
]):
# fire onslaught in all selected locations
event_data = ['onslaught', {
'onslaught_options': onslaught_options,
'location_owner': onslaught_location['owner'],
'location_identifier': onslaught_location['identifier'],
'action': 'start onslaught'
}]
module.trigger_action_hook(origin_module, event_data=event_data, dispatchers_steamid=steamid)
elif command.startswith("/stop onslaught"):
for onslaught_location in origin_module.get_elements_by_type("is_onslaught"):
# only proceed with the player is inside a dedicated location
if any([
onslaught_options in ["everywhere", onslaught_location["identifier"]],
origin_module.position_is_inside_boundary(player_dict, onslaught_location)
]):
# fire onslaught in all selected locations
event_data = ['onslaught', {
'location_owner': onslaught_location['owner'],
'location_identifier': onslaught_location['identifier'],
'action': 'stop onslaught'
}]
module.trigger_action_hook(origin_module, event_data=event_data, dispatchers_steamid=steamid)
triggers = {
"start onslaught": r"\'(?P<player_name>.*)\'\:\s(?P<command>\/start\sonslaught.*)",
"stop onslaught": r"\'(?P<player_name>.*)\'\:\s(?P<command>\/stop\sonslaught.*)"
}
trigger_meta = {
"description": "will start the onslaught event in a specified location",
"main_function": main_function,
"triggers": [
{
"identifier": "start onslaught (Alloc)",
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
telnet_prefixes["Allocs"]["chat"] +
triggers["start onslaught"]
),
"callback": main_function
},
{
"identifier": "stop onslaught (Alloc)",
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
telnet_prefixes["Allocs"]["chat"] +
triggers["stop onslaught"]
),
"callback": main_function
},
{
"identifier": "start onslaught (BCM)",
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
telnet_prefixes["BCM"]["chat"] +
triggers["start onslaught"]
),
"callback": main_function
},
{
"identifier": "stop onslaught (BCM)",
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
telnet_prefixes["BCM"]["chat"] +
triggers["stop onslaught"]
),
"callback": main_function
}
]
}
loaded_modules_dict["module_" + module_name].register_trigger(trigger_name, trigger_meta)

View File

@@ -0,0 +1,64 @@
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):
location_identifier = "PlaceofDeath"
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
steamid = regex_result.group("player_steamid")
player_dict = module.dom.data.get("module_players", {}).get("elements", {}).get(active_dataset, {}).get(steamid, {})
location_dict = (
module.dom.data.get("module_locations", {})
.get("elements", {})
.get(active_dataset, {})
.get(steamid, {})
.get(location_identifier, {})
)
if len(player_dict) >= 1 and len(location_dict) >= 1:
event_data = ['teleport_to_coordinates', {
'location_coordinates': {
"x": location_dict["coordinates"]["x"],
"y": location_dict["coordinates"]["y"],
"z": location_dict["coordinates"]["z"]
}
}]
module.trigger_action_hook(origin_module, event_data=event_data, dispatchers_steamid=steamid)
triggers = {
"take me to my grave": r"\'(?P<player_name>.*)\'\:\s(?P<command>\/take me to my grave)"
}
trigger_meta = {
"description": "sends the player to his final resting place, if available",
"main_function": main_function,
"triggers": [
{
"identifier": "take me to my grave (Allocs)",
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
telnet_prefixes["Allocs"]["chat"] +
triggers["take me to my grave"]
),
"callback": main_function
},
{
"identifier": "take me to my grave (BCM)",
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
telnet_prefixes["BCM"]["chat"] +
triggers["take me to my grave"]
),
"callback": main_function
}
]
}
loaded_modules_dict["module_" + module_name].register_trigger(trigger_name, trigger_meta)

View File

@@ -0,0 +1,100 @@
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 find_by_key(data, target):
for key, value in data.items():
if isinstance(value, dict):
yield from find_by_key(value, target)
elif key == target:
yield value
def main_function(origin_module, module, regex_result):
player_steamid = regex_result.group("player_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(player_steamid, {})
)
all_locations_dict = (
module.dom.data.get("module_locations", {})
.get("elements", {})
.get(active_dataset, {})
)
occupied_locations = []
for locations_by_owner in all_locations_dict:
for location_identifier, location_dict in all_locations_dict[locations_by_owner].items():
if origin_module.position_is_inside_boundary(player_dict, location_dict):
occupied_locations.append(location_dict)
if len(occupied_locations) > 0:
event_data = ['say_to_player', {
'steamid': player_steamid,
'message': '[FFFFFF]You are inside the following locations:[-]'
}]
module.trigger_action_hook(origin_module.players, event_data=event_data)
for location_dict in occupied_locations:
location_owner_dict = (
module.dom.data.get("module_players", {})
.get("elements", {})
.get(active_dataset, {})
.get(location_dict.get("owner"), {})
)
event_data = ['say_to_player', {
'steamid': player_steamid,
'message': '[FFFFFF]{name} [66FF66]({owner})[-]'.format(
name=location_dict.get("name", "n/a"),
owner=location_owner_dict.get("name", "n/a")
)
}]
module.trigger_action_hook(origin_module.players, event_data=event_data)
else:
event_data = ['say_to_player', {
'steamid': player_steamid,
'message': '[FFFFFF]You do not seem to be in any designated location[-]'
}]
module.trigger_action_hook(origin_module.players, event_data=event_data)
triggers = {
"where am i": r"\'(?P<player_name>.*)\'\:\s(?P<command>\/where am i)"
}
trigger_meta = {
"description": "prints out a list of locations a player currently occupies",
"main_function": main_function,
"triggers": [
{
"identifier": "where am i (Allocs)",
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
telnet_prefixes["Allocs"]["chat"] +
triggers["where am i"]
),
"callback": main_function
},
{
"identifier": "where am i (BCM)",
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
telnet_prefixes["BCM"]["chat"] +
triggers["where am i"]
),
"callback": main_function
}
]
}
loaded_modules_dict["module_" + module_name].register_trigger(trigger_name, trigger_meta)

View File

@@ -0,0 +1,56 @@
{%- 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 -%}
{%- macro construct_view_menu(views, current_view, module_name, steamid, default_view='frontend') -%}
{#
Dynamically construct a navigation menu for widget views.
Parameters:
views: Dict of view configurations
Example: {
'frontend': {'label_active': 'back', 'label_inactive': 'main', 'action': 'show_frontend'},
'options': {'label_active': 'back', 'label_inactive': 'options', 'action': 'show_options'}
}
current_view: Current active view name (string)
module_name: Module name for socket.io event (e.g., 'locations')
steamid: User's steamid for action parameters
default_view: View to return to when deactivating (default: 'frontend')
#}
{%- for view_id, config in views.items() -%}
{%- if config.get('include_in_menu', True) -%}
{%- set is_active = (current_view == view_id) -%}
{%- set label_active = config.get('label_active', config.get('label', 'back')) -%}
{%- set label_inactive = config.get('label_inactive', config.get('label', view_id)) -%}
{%- set action = config.get('action', 'show_' ~ view_id) -%}
{%- set default_action = config.get('default_action', 'show_' ~ default_view) -%}
<div>
{{ construct_toggle_link(
is_active,
label_active,
['widget_event', [module_name, ['toggle_' ~ module_name ~ '_widget_view', {'steamid': steamid, 'action': default_action}]]],
label_inactive,
['widget_event', [module_name, ['toggle_' ~ module_name ~ '_widget_view', {'steamid': steamid, 'action': action}]]]
)}}
</div>
{%- endif -%}
{%- endfor -%}
{%- endmacro -%}

View File

@@ -0,0 +1,12 @@
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
<span id="location_table_row_{{ location.dataset }}_{{ location.owner}}_{{ location.identifier }}_control_edit_link" class="toggle_button">
{{- construct_toggle_link(
True,
"edit", ['widget_event', ['locations', ['toggle_locations_widget_view', {
"dom_element_owner": location.owner,
"dom_element_identifier": location.identifier,
"dom_element_origin": location.dataset_original,
"action": "edit_location_entry"
}]]]
) -}}
</span>

View File

@@ -0,0 +1,18 @@
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
<span id="location_table_row_{{ location.dataset }}_{{ location.owner}}_{{ location.identifier }}_control_enabled_link" class="toggle_button">
{{- construct_toggle_link(
location.is_enabled,
"enabled", ['widget_event', ['locations', ['toggle_enabled_flag', {
"dom_element_owner": location.owner,
"dom_element_identifier": location.identifier,
"dom_element_origin": location.dataset_original,
"action": "disable_location_entry"
}]]],
"enabled", ['widget_event', ['locations', ['toggle_enabled_flag', {
"dom_element_owner": location.owner,
"dom_element_identifier": location.identifier,
"dom_element_origin": location.dataset_original,
"action": "enable_location_entry"
}]]]
) -}}
</span>

View File

@@ -0,0 +1,10 @@
<div class="bottom" id="current_player_pos">
<span class="info">
<div>
<span>you are here:</span>
<div>x: <span id="location_coordinates_x">{{ pos_x }}</span></div>
<div>y: <span id="location_coordinates_y">{{ pos_y }}</span></div>
<div>z: <span id="location_coordinates_z">{{ pos_z }}</span></div>
</div>
</span>
</div>

View File

@@ -0,0 +1,9 @@
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
<div>
{{ construct_toggle_link(
create_new_view_toggle,
"create new location", ['widget_event', ['locations', ['toggle_locations_widget_view', {'steamid': steamid, "action": "show_create_new"}]]],
"back", ['widget_event', ['locations', ['toggle_locations_widget_view', {'steamid': steamid, "action": "show_frontend"}]]]
)}}
</div>

View File

@@ -0,0 +1,12 @@
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
<div>
{{ construct_toggle_link(
map_view_toggle,
"hide map", ['widget_event', ['locations', ['toggle_locations_widget_view', {
"action": 'show_frontend'
}]]],
"show map", ['widget_event', ['locations', ['toggle_locations_widget_view', {
"action": 'show_map'
}]]]
)}}
</div>

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

View File

@@ -0,0 +1,9 @@
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
<div>
{{ construct_toggle_link(
special_locations_view_toggle,
"special locations", ['widget_event', ['locations', ['toggle_locations_widget_view', {'steamid': steamid, "action": "show_special_locations"}]]],
"back", ['widget_event', ['locations', ['toggle_locations_widget_view', {'steamid': steamid, "action": "show_frontend"}]]]
)}}
</div>

View File

@@ -0,0 +1,7 @@
<div id="locations_widget_options_toggle" class="pull_out right">
{{ control_switch_options_view }}
{{ control_switch_create_new_view }}
{{ control_switch_special_locations_view }}
{{ control_switch_map_view }}
{{ control_player_location_view }}
</div>

View File

@@ -0,0 +1,11 @@
{%- from 'jinja2_macros.html' import construct_view_menu with context -%}
<div id="locations_widget_options_toggle" class="pull_out right">
{{ construct_view_menu(
views=views,
current_view=current_view,
module_name='locations',
steamid=steamid,
default_view='frontend'
)}}
{{ control_player_location_view }}
</div>

View File

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

View File

@@ -0,0 +1,9 @@
<tr>
<th>*</th>
<th onclick="window.sorting(this, location_table, 1)">actions</th>
<th onclick="window.sorting(this, location_table, 2)">name</th>
<th onclick="window.sorting(this, location_table, 3)">owner name</th>
<th onclick="window.sorting(this, location_table, 4)">identifier</th>
<th onclick="window.sorting(this, location_table, 5)">coordinates</th>
<th onclick="window.sorting(this, location_table, 6)">last changed</th>
</tr>

View File

@@ -0,0 +1,18 @@
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
{%- set location_is_enabled = location.is_enabled|default(false) -%}
{%- set location_entry_selected = false -%}
<tr id="location_table_row_{{ location.dataset }}_{{ location.owner}}_{{ location.identifier }}"{%- if css_class %} class="{{ css_class }}"{%- endif -%}>
<td>
<span id="location_table_row_{{ location.dataset }}_{{ location.owner}}_{{ location.identifier }}_control_select_link" class="select_button">{{ control_select_link }}</span>
</td>
<td class="right" id="location_table_row_{{ location.dataset }}_{{ location.owner}}_{{ location.identifier }}_actions">{{ control_edit_link }}{{ control_enabled_link }}</td>
<td id="location_table_row_{{ location.dataset }}_{{ location.owner}}_{{ location.identifier }}_name" onclick="$(this).selectText();">{{ location.name }}</td>
<td id="location_table_row_{{ location.dataset }}_{{ location.owner}}_{{ location.identifier }}_owner_name" onclick="$(this).selectText();">{{ player_dict.name }}</td>
<td id="location_table_row_{{ location.dataset }}_{{ location.owner}}_{{ location.identifier }}_id" onclick="$(this).selectText();">{{ location.identifier }}</td>
<td class="right" id="location_table_row_{{ location.dataset }}_{{ location.owner}}_{{ location.identifier }}_coordinates" onclick="$(this).selectText();">
{{ ((location | default({})).coordinates | default({}) ).x }}
{{ ((location | default({})).coordinates | default({}) ).y }}
{{ ((location | default({})).coordinates | default({}) ).z }}
</td>
<td class="right" id="location_table_row_{{ location.dataset }}_{{ location.owner}}_{{ location.identifier }}_last_changed">{{ location.last_changed }}</td>
</tr>

View File

@@ -0,0 +1,34 @@
{% set is_edit = location_to_edit_dict | length > 1 %}
<header>
<div>
<span>Locations</span>
</div>
</header>
<aside>
{{ options_toggle }}
</aside>
<main>
<table class="form_table">
<thead>
<tr>
<th>Create a new location!</th>
</tr>
</thead>
<tbody>
<tr>
<td>
{% with location_dict=location_to_edit_dict, is_edit=is_edit %}
{% include "manage_locations_widget/view_create_new_set_name.html" %}
{% include "manage_locations_widget/view_create_new_identifier.html" %}
{% include "manage_locations_widget/view_create_new_select_shape.html" %}
{% include "manage_locations_widget/view_create_new_set_dimensions.html" %}
{% include "manage_locations_widget/view_create_new_set_coordinates.html" %}
{% include "manage_locations_widget/view_create_new_set_teleport_coordinates.html" %}
{% include "manage_locations_widget/view_create_new_select_type.html" %}
{% include "manage_locations_widget/view_create_new_control_send_data.html" %}
{% endwith %}
</td>
</tr>
</tbody>
</table>
</main>

View File

@@ -0,0 +1,57 @@
{% set action = "edit" if is_edit else "create_new" %}
<!-- This logic is for the UX experience only, all values are checked for validity in the bots backend //-->
<script>
function send_location_data_to_bot() {
let location_shape = $("input[name='location_shape']:checked").val();
let checked_location_types = new Array();
$.each($("input[name='location_type[]']:checked"), function() {
checked_location_types.push($(this).val());
});
let location_coordinates_x = $("input[name='location_coordinates_x']").val();
let location_coordinates_y = $("input[name='location_coordinates_y']").val();
let location_coordinates_z = $("input[name='location_coordinates_z']").val();
let location_coordinates = {
'x': location_coordinates_x,
'y': location_coordinates_y,
'z': location_coordinates_z
};
let location_teleport_entry_x = $("input[name='location_teleport_entry_x']").val();
let location_teleport_entry_y = $("input[name='location_teleport_entry_y']").val();
let location_teleport_entry_z = $("input[name='location_teleport_entry_z']").val();
let location_teleport_entry = {
'x': location_teleport_entry_x,
'y': location_teleport_entry_y,
'z': location_teleport_entry_z
};
let location_dimensions_radius = $("input[name='location_dimensions_radius']").val();
let location_dimensions_width = $("input[name='location_dimensions_width']").val();
let location_dimensions_length = $("input[name='location_dimensions_length']").val();
let location_dimensions_height = $("input[name='location_dimensions_height']").val();
let location_dimensions = {
'radius': location_dimensions_radius,
'width': location_dimensions_width,
'length': location_dimensions_length,
'height': location_dimensions_height,
};
let location_name = $("input[name='location_name']").val();
let location_identifier = $("input[name='location_identifier']").val();
window.socket.emit(
'widget_event',
['locations', ['edit_location', {
'location_identifier': location_identifier,
'location_shape': location_shape,
'location_type': checked_location_types,
'location_coordinates': location_coordinates,
'location_teleport_entry': location_teleport_entry,
'location_dimensions': location_dimensions,
'location_name': location_name,
'location_owner': "{{ location_dict.owner }}",
'is_enabled': "{{ location_dict.is_enabled }}",
'action': "{{ action }}"
}]]
);
}
</script>
<a href="#" onclick="send_location_data_to_bot(); return false;">save</a>

View File

@@ -0,0 +1,35 @@
<table class="box_input">
<tbody>
<tr>
<td>
<div>{%- if not is_edit %}
<script>
$("input[name='location_name']").on('input', function() {
let location_name = $("input[name='location_name']").val();
let location_identifier = location_name.replace(/[^a-zA-Z0-9]/g, '');
$("input[name='location_identifier']").val(location_identifier);
});
</script>{% endif %}
<label>
<span>
<input type="text" name="location_identifier" value="{{ location_dict.identifier }}" disabled="disabled" />
location Identifier
</span>
</label>
</div>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td>
<div>
This is the Identifier of your location<br />
It is used to handle the location with in-game chat commands<br />
It has to be unique for each users locations.<br />
This setting can not be changed later on!<br />
</div>
</td>
</tr>
</tfoot>
</table>

View File

@@ -0,0 +1,83 @@
{% set checked_string = ' checked="checked"' %}
{% set location_shape_not_found = true %}
{% if location_dict["shape"] == "circle" %}
{% set location_shape_circle_checked = checked_string %}
{% set location_shape_not_found = false %}
{% endif %}
{% if location_dict["shape"] == "spherical" %}
{% set location_shape_spherical_checked = checked_string %}
{% set location_shape_not_found = false %}
{% endif %}
{% if location_dict["shape"] == "rectangular" %}
{% set location_shape_rectangular_checked = checked_string %}
{% set location_shape_not_found = false %}
{% endif %}
{% if location_dict["shape"] == "box" %}
{% set location_shape_box_checked = checked_string %}
{% set location_shape_not_found = false %}
{% endif %}
{% if location_shape_not_found %}
{% set location_shape_rectangular_checked = checked_string %}
{% endif %}
<table class="box_select">
<thead>
<tr>
<th colspan="2">
Choose the shape of your location
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="box">
<label><span>
<input type="radio" name="location_shape" value="circle" {{ location_shape_circle_checked }}/>
circle (2d)</span>
</label>
</div>
</td>
<td>
<div class="box">
<label>
<span><input type="radio" name="location_shape" value="spherical" {{ location_shape_spherical_checked }}/>
spherical (3d)</span>
</label>
</div>
</td>
</tr>
<tr>
<td>
<div class="box">
<label><span>
<input type="radio" name="location_shape" value="rectangular" {{ location_shape_rectangular_checked }} />
rectangular (2d)</span>
</label>
</div>
</td>
<td>
<div class="box">
<label><span>
<input type="radio" name="location_shape" value="box" {{ location_shape_box_checked }} />
box (3d)</span>
</label>
</div>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="2">
<div>
locations can have several different shapes.<br />
you can have either a square (default, just like the LCB) or a round base, depending on your build-style<br />
You can also limit the height of your locations, so you can build, for example,
an underground base without affecting the ground above at all.<br />
It is also useful for building multi-story builds with different locations per level,
or even rooms on the same one.<br />
This setting can be changed later on, at any time!
</div>
</td>
</tr>
</tfoot>
</table>

View File

@@ -0,0 +1,106 @@
{% set checked_string = ' checked="checked"' %}
{% set location_type_not_found = true %}
{% if "is_village" in location_dict["type"] %}
{% set location_type_village_checked = checked_string %}
{% set location_type_not_found = false %}
{% endif %}
{% if "is_screamerfree" in location_dict["type"] %}
{% set location_type_screamerfree_checked = checked_string %}
{% set location_type_not_found = false %}
{% endif %}
{% if "is_home" in location_dict["type"] %}
{% set location_type_home_checked = checked_string %}
{% set location_type_not_found = false %}
{% endif %}
{% if "is_lobby" in location_dict["type"] %}
{% set location_type_lobby_checked = checked_string %}
{% set location_type_not_found = false %}
{% endif %}
{% if "is_onslaught" in location_dict["type"] %}
{% set location_type_onslaught_checked = checked_string %}
{% set location_type_not_found = false %}
{% endif %}
{% if "is_hunting_resort" in location_dict["type"] %}
{% set location_type_hunting_resort_checked = checked_string %}
{% set location_type_not_found = false %}
{% endif %}
{% if location_type_not_found %}{% endif %}
<table class="box_select">
<thead>
<tr>
<th colspan="2">
Choose the type of your location
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="box">
<label class="slider">Village
<input type="checkbox" name="location_type[]" value="is_village" {{ location_type_village_checked }}/>
<span>This location is a village and will have all village-functions enabled</span>
</label>
</div>
</td>
<td>
<div class="box">
<label class="slider">Screamer-free
<input type="checkbox" name="location_type[]" value="is_screamerfree" {{ location_type_screamerfree_checked }}/>
<span>Screamers will be killed on arrival</span>
</label>
</div>
</td>
</tr>
<tr>
<td>
<div class="box">
<label class="slider">Onslaught
<input type="checkbox" name="location_type[]" value="is_onslaught" {{ location_type_onslaught_checked }} />
<span>Endless streams of zombies. Until you turn 'em off.</span>
</label>
</div>
</td>
<td>
<div class="box">
<label class="slider">Hunting Resort
<input type="checkbox" name="location_type[]" value="is_hunting_resort" {{ location_type_hunting_resort_checked }} />
<span>They are there, promise!!</span>
</label>
</div>
</td>
</tr>
<tr>
<td>
<div class="box">
<label class="slider">Home
<input type="checkbox" name="location_type[]" value="is_home" {{ location_type_home_checked }} />
<span>a player will be able to teleport to this under special circunstances</span>
</label>
</div>
</td>
<td>
<div class="box">
<label class="slider">Lobby
<input type="checkbox" name="location_type[]" value="is_lobby" {{ location_type_lobby_checked }} />
<span>No way out. Unless you know the password.<br />
What's the word?<br />
B-b-b-b-b-b-b-b-bird bird bird b-b-bird is the word :)</span>
</label>
</div>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="2">
<div>
locations can have a bunch of types<br />
Types will add to one another, so you could create a home-lobby that is screamer-free and also it's own village :)<br />
uncheck all to make it a simple location without any features attached. What for you ask?<br />
Well, you could make your Builds available via locations, then people might find them easier.
</div>
</td>
</tr>
</tfoot>
</table>

View File

@@ -0,0 +1,55 @@
{% set coordinates = location_dict.coordinates|default({}) %}
{% set pos_x = coordinates.x|default("0") %}
{% set pos_y = coordinates.y|default("0") %}
{% set pos_z = coordinates.z|default("0") %}
<table class="box_input">
<thead>
<tr>
<th colspan="4">
Choose the coordinates of your location
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div>
<label>x: <input size="6" name="location_coordinates_x" type="text" value="{{ pos_x }}" /></label>
</div>
</td>
<td>
<div>
<label>y: <input size="6" name="location_coordinates_y" type="text" value="{{ pos_y }}" /></label>
</div>
</td>
<td>
<div>
<label>z: <input size="6" name="location_coordinates_z" type="text" value="{{ pos_z }}" /></label>
</div>
</td>
<td>
<div>
<a href="#" onclick="use_current_coordinates2(); return false;">use current position</a>
</div>
</td>
<script>
function use_current_coordinates2() {
$("input[name='location_coordinates_x']").val($("#location_coordinates_x").text());
$("input[name='location_coordinates_y']").val($("#location_coordinates_y").text());
$("input[name='location_coordinates_z']").val($("#location_coordinates_z").text());
}
</script>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="4">
<div>
Sets the coordinates of your location<br />
This will be the S/E corner of your location area. For round shapes, imagine a rectangle wrapped around
it and use it's S/E corner
</div>
</td>
</tr>
</tfoot>
</table>

View File

@@ -0,0 +1,89 @@
{% set dimensions = location_dict.dimensions|default({}) %}
{% set location_dimensions_radius_value = dimensions.radius|default("0") %}
{% set location_dimensions_width_value = dimensions.width|default("0") %}
{% set location_dimensions_length_value = dimensions.length|default("0") %}
{% set location_dimensions_height_value = dimensions.height|default("0") %}
<table class="box_input">
<thead>
<tr>
<th>
Choose the size and dimensions of your location
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div>
<label><span>
<input type="text" name="location_dimensions_radius" value="{{ location_dimensions_radius_value }}"/>
Radius (current position is the center)</span>
</label>
</div>
<div>
<label><span>
<input type="text" name="location_dimensions_width" value="{{ location_dimensions_width_value }}"/>
Width (from the S/E corner going WEST)</span>
</label>
</div>
<div>
<label><span>
<input type="text" name="location_dimensions_length" value="{{ location_dimensions_length_value }}"/>
Length (from the S/E corner going NORTH)</span>
</label>
</div>
<div>
<label><span>
<input type="text" name="location_dimensions_height" value="{{ location_dimensions_height_value }}"/>
Height (from the S/E corner going UP)</span>
</label>
</div>
<script>
function set_shape_control_status() {
let location_shape = $("input[name='location_shape']:checked").val();
if (location_shape === "circle" || location_shape === "spherical") {
$("input[name='location_dimensions_radius']").prop("disabled", false);
$("input[name='location_dimensions_width']").prop("disabled", true);
$("input[name='location_dimensions_length']").prop("disabled", true);
$("input[name='location_dimensions_height']").prop("disabled", true);
}
if (location_shape === "square" || location_shape === "cubical") {
$("input[name='location_dimensions_radius']").prop("disabled", true);
$("input[name='location_dimensions_width']").prop("disabled", false);
$("input[name='location_dimensions_length']").prop("disabled", true);
$("input[name='location_dimensions_height']").prop("disabled", true);
}
if (location_shape === "rectangular") {
$("input[name='location_dimensions_radius']").prop("disabled", true);
$("input[name='location_dimensions_width']").prop("disabled", false);
$("input[name='location_dimensions_length']").prop("disabled", false);
$("input[name='location_dimensions_height']").prop("disabled", true);
}
if (location_shape === "box") {
$("input[name='location_dimensions_radius']").prop("disabled", true);
$("input[name='location_dimensions_width']").prop("disabled", false);
$("input[name='location_dimensions_length']").prop("disabled", false);
$("input[name='location_dimensions_height']").prop("disabled", false);
}
}
set_shape_control_status();
$("input[name='location_shape']").on('change', function() {
set_shape_control_status()
});
</script>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td>
<div>
Sets the Dimensions of your location<br />
Depending on it's shape, it's either by radius or by defining up to three connecting side-lengths<br />
The radius is set from the position the player is standing on, rectangular shapes start from
the S/E corner going N/W and UP.<br />
</div>
</td>
</tr>
</tfoot>
</table>

View File

@@ -0,0 +1,32 @@
<table class="box_input">
<thead>
<tr>
<th>
Choose the name of your location
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div>
<label><span>
<input type="text" name="location_name" value="{{ location_dict.name }}"/>
location name</span>
</label>
</div>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td>
<div>
Sets the name of your location<br />
this is only for visual purposes, it has no bearing on it's function<br />
This setting can be changed later on, at any time!
</div>
</td>
</tr>
</tfoot>
</table>

View File

@@ -0,0 +1,54 @@
{% set teleport_entry = location_dict.teleport_entry|default({}) %}
{% set pos_x = teleport_entry.x|default("0") %}
{% set pos_y = teleport_entry.y|default("0") %}
{% set pos_z = teleport_entry.z|default("0") %}
<table class="box_input">
<thead>
<tr>
<th colspan="4">
Choose the teleport-entry for this location
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div>
<label>x: <input size="6" name="location_teleport_entry_x" type="text" value="{{ pos_x }}" /></label>
</div>
</td>
<td>
<div>
<label>y: <input size="6" name="location_teleport_entry_y" type="text" value="{{ pos_y }}" /></label>
</div>
</td>
<td>
<div>
<label>z: <input size="6" name="location_teleport_entry_z" type="text" value="{{ pos_z }}" /></label>
</div>
</td>
<td>
<div>
<a href="#" onclick="use_current_coordinates(); return false;">use current position</a>
</div>
</td>
<script>
function use_current_coordinates() {
$("input[name='location_teleport_entry_x']").val($("#location_coordinates_x").text());
$("input[name='location_teleport_entry_y']").val($("#location_coordinates_y").text());
$("input[name='location_teleport_entry_z']").val($("#location_coordinates_z").text());
}
</script>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="4">
<div>
Sets the teleport-entry point for your location<br />
Players using the bots travel commands will emerge in this spot and not in the default S/E corner
</div>
</td>
</tr>
</tfoot>
</table>

View File

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

View File

@@ -0,0 +1,474 @@
<!-- Leaflet CSS -->
<link rel="stylesheet" href="/static/leaflet.css"/>
<style>
#map {
width: 100%;
height: 100%;
min-height: 400px;
background: #1a1a1a;
border: 2px solid var(--lcars-hopbush);
border-radius: 8px;
box-sizing: border-box;
}
main {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
.leaflet-container {
background: #1a1a1a;
}
.location-marker {
background-color: var(--lcars-golden-tanoi);
border: 2px solid var(--lcars-tanoi);
border-radius: 50%;
width: 12px;
height: 12px;
}
.player-marker {
background-color: var(--lcars-anakiwa);
border: 2px solid var(--lcars-mariner);
border-radius: 50%;
width: 10px;
height: 10px;
}
.map-controls {
margin-bottom: 10px;
padding: 10px;
background: rgba(0, 0, 0, 0.3);
border-radius: 4px;
}
/* Custom Legend Styling */
.map-legend {
background: rgba(26, 26, 26, 0.95);
border: 2px solid var(--lcars-hopbush);
border-radius: 8px;
padding: 12px;
font-family: monospace;
font-size: 12px;
line-height: 1.6;
color: var(--lcars-golden-tanoi);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.5);
min-width: 200px;
}
.map-legend h4 {
margin: 0 0 8px 0;
color: var(--lcars-hopbush);
font-size: 14px;
font-weight: bold;
border-bottom: 1px solid var(--lcars-hopbush);
padding-bottom: 4px;
}
.map-legend .legend-item {
margin: 4px 0;
display: flex;
justify-content: space-between;
gap: 10px;
}
.map-legend .legend-label {
color: var(--lcars-anakiwa);
font-weight: bold;
}
.map-legend .legend-value {
color: var(--lcars-golden-tanoi);
text-align: right;
}
/* Coordinates Display */
.coordinates-display {
position: absolute;
bottom: 10px;
right: 10px;
background: rgba(26, 26, 26, 0.95);
border: 2px solid var(--lcars-anakiwa);
border-radius: 6px;
padding: 8px 12px;
font-family: monospace;
font-size: 13px;
color: var(--lcars-anakiwa);
z-index: 1000;
pointer-events: none;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
.coordinates-display .coord-label {
font-weight: bold;
color: var(--lcars-hopbush);
}
/* Create Location Button */
.create-location-btn {
background: var(--lcars-hopbush);
border: 2px solid var(--lcars-hopbush);
color: white;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
font-size: 13px;
transition: all 0.3s ease;
}
.create-location-btn:hover {
background: var(--lcars-golden-tanoi);
border-color: var(--lcars-golden-tanoi);
color: #000;
}
.create-location-btn.active {
background: var(--lcars-tanoi);
border-color: var(--lcars-tanoi);
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
</style>
<header>
<div>
<span>Locations Map</span>
</div>
</header>
<aside>
{{ control_switch_view }}
</aside>
<main>
<div id="map"></div>
<!-- Leaflet JS -->
<script src="/static/leaflet.js"></script>
<script>
// Wait for Leaflet to load
(function initMap() {
if (typeof L === 'undefined') {
setTimeout(initMap, 100);
return;
}
// Check if map container exists
const mapContainer = document.getElementById('map');
if (!mapContainer) {
console.error('[MAP] Map container #map not found!');
return;
}
// 7D2D Projection (from Alloc's mod)
const SDTD_Projection = {
project: function (latlng) {
return new L.Point(
(latlng.lat) / Math.pow(2, 4),
(latlng.lng) / Math.pow(2, 4)
);
},
unproject: function (point) {
return new L.LatLng(
point.x * Math.pow(2, 4),
point.y * Math.pow(2, 4)
);
}
};
// 7D2D CRS (from Alloc's mod)
const SDTD_CRS = L.extend({}, L.CRS.Simple, {
projection: SDTD_Projection,
transformation: new L.Transformation(1, 0, -1, 0),
scale: function (zoom) {
return Math.pow(2, zoom);
}
});
// Initialize map with 7D2D CRS
const map = L.map('map', {
crs: SDTD_CRS,
center: [0, 0],
zoom: 3,
minZoom: -1,
maxZoom: 7,
attributionControl: false
});
// Create tile layer (Y-axis flipping handled by backend)
const tileLayer = L.tileLayer('/map_tiles/{z}/{x}/{y}.png', {
tileSize: 128,
minNativeZoom: 0,
minZoom: -1,
maxNativeZoom: 4,
maxZoom: 7
}).addTo(map);
// Storage for markers and shapes
const locationShapes = {};
const playerMarkers = {};
// Data variables for locations and players (initially empty, loaded via socket.io)
const locations = {};
const players = {};
// ========================================
// Location Shape Creation Function
// ========================================
{{ webmap_templates.location_shapes|safe }}
// ========================================
// Player Popup Creation
// ========================================
{{ webmap_templates.player_popup|safe }}
// ========================================
// Real-time Update Handlers
// ========================================
// Remove old map handler if it exists from previous view load
if (window.mapDataHandler) {
window.socket.off('data', window.mapDataHandler);
}
// Create NEW handler function that captures current local scope
// This must be recreated each time to access current locationShapes/playerMarkers
window.mapDataHandler = function(data) {
console.log('[MAP] Received socket.io data:', data.data_type, data);
// Map metadata (gameprefs, dataset info)
if (data.data_type === 'map_metadata' && data.payload) {
gameprefs = data.payload.gameprefs || {};
activeDataset = data.payload.active_dataset || 'Unknown';
updateLegend();
console.log('[MAP] Updated map metadata:', gameprefs, activeDataset);
}
// Player position updates
{{ webmap_templates.player_update_handler|safe }}
// Location updates and removals
{{ webmap_templates.location_update_handler|safe }}
// Update legend counts after any changes
if (data.data_type === 'player_position_update' || data.data_type === 'location_update' || data.data_type === 'location_remove') {
updateLegend();
}
};
// Register the new handler
window.socket.on('data', window.mapDataHandler);
// Fit map to show all markers and shapes on load
const allMapObjects = Object.values(locationShapes).concat(Object.values(playerMarkers));
if (allMapObjects.length > 0) {
const group = L.featureGroup(allMapObjects);
map.fitBounds(group.getBounds().pad(0.1));
}
// Fix map size after a short delay (in case container was hidden initially)
setTimeout(function() {
map.invalidateSize();
}, 250);
// ========================================
// FEATURE 1: Map Legend with Metadata (loaded via Socket.IO)
// ========================================
// Map metadata variables (loaded via socket.io)
let gameprefs = {};
let activeDataset = 'Loading...';
// Create custom legend control
const legend = L.control({position: 'topleft'});
let legendDiv = null;
legend.onAdd = function (mapInstance) {
legendDiv = L.DomUtil.create('div', 'map-legend');
updateLegend();
return legendDiv;
};
// Function to update legend content
function updateLegend() {
if (!legendDiv) return;
const gameName = gameprefs['GameName'] || activeDataset;
const gameWorld = gameprefs['GameWorld'] || 'N/A';
const worldGenSeed = gameprefs['WorldGenSeed'] || 'N/A';
const worldGenSize = gameprefs['WorldGenSize'] || 'N/A';
const locationCount = Object.keys(locations).length;
const playerCount = Object.keys(players).length;
legendDiv.innerHTML = `
<h4>🗺️ Map Info</h4>
<div class="legend-item">
<span class="legend-label">World:</span>
<span class="legend-value">${gameName}</span>
</div>
<div class="legend-item">
<span class="legend-label">Type:</span>
<span class="legend-value">${gameWorld}</span>
</div>
<div class="legend-item">
<span class="legend-label">Size:</span>
<span class="legend-value">${worldGenSize}</span>
</div>
<div class="legend-item">
<span class="legend-label">Seed:</span>
<span class="legend-value">${worldGenSeed}</span>
</div>
<div class="legend-item">
<span class="legend-label">Locations:</span>
<span class="legend-value" id="legend-location-count">${locationCount}</span>
</div>
<div class="legend-item">
<span class="legend-label">Players:</span>
<span class="legend-value" id="legend-player-count">${playerCount}</span>
</div>
`;
}
legend.addTo(map);
// ========================================
// FEATURE 2: Coordinates Under Mouse Cursor
// ========================================
// Create coordinates display element
const coordsDiv = L.DomUtil.create('div', 'coordinates-display');
coordsDiv.innerHTML = '<span class="coord-label">X:</span> <span id="coord-x">0</span> | ' +
'<span class="coord-label">Z:</span> <span id="coord-z">0</span>';
document.getElementById('map').appendChild(coordsDiv);
// Update coordinates on mouse move
map.on('mousemove', function(e) {
const coords = e.latlng;
// In 7D2D, lat corresponds to X and lng to Z
document.getElementById('coord-x').textContent = Math.round(coords.lat);
document.getElementById('coord-z').textContent = Math.round(coords.lng);
});
// ========================================
// FEATURE 3: Create Location UI
// ========================================
// Store tempMarker globally so it can be accessed by confirm/cancel functions
let tempMarker = null;
// Create custom control for location creation
const createLocationControl = L.control({position: 'topright'});
createLocationControl.onAdd = function (mapInstance) {
const btn = L.DomUtil.create('button', 'create-location-btn');
btn.innerHTML = '📍 Create Location';
btn.title = 'Click to enable location creation mode';
// Prevent map interactions when clicking the button
L.DomEvent.disableClickPropagation(btn);
let creationMode = false;
btn.onclick = function() {
creationMode = !creationMode;
if (creationMode) {
btn.classList.add('active');
btn.innerHTML = '✖️ Cancel';
map.getContainer().style.cursor = 'crosshair';
// Add click handler for location creation
map.once('click', function(e) {
const coords = e.latlng;
const x = Math.round(coords.lat);
const z = Math.round(coords.lng);
// Create temporary marker
tempMarker = L.circleMarker([coords.lat, coords.lng], {
radius: 8,
fillColor: '#ff00ff',
color: '#ff00ff',
weight: 3,
opacity: 1,
fillOpacity: 0.6
}).addTo(map);
tempMarker.bindPopup(
'<div style="text-align: center;">' +
'<b>New Location</b><br>' +
'X: ' + x + ', Z: ' + z + '<br>' +
'<button onclick="confirmLocation(' + x + ', ' + z + ')" ' +
'style="margin: 5px; padding: 5px 10px; background: var(--lcars-hopbush); color: white; border: none; border-radius: 4px; cursor: pointer;">' +
'Create Here</button>' +
'<button onclick="cancelLocation()" ' +
'style="margin: 5px; padding: 5px 10px; background: #666; color: white; border: none; border-radius: 4px; cursor: pointer;">' +
'Cancel</button>' +
'</div>'
).openPopup();
// Reset button state
btn.classList.remove('active');
btn.innerHTML = '📍 Create Location';
map.getContainer().style.cursor = '';
creationMode = false;
});
} else {
btn.classList.remove('active');
btn.innerHTML = '📍 Create Location';
map.getContainer().style.cursor = '';
// Remove temp marker if exists
if (tempMarker) {
map.removeLayer(tempMarker);
tempMarker = null;
}
}
};
return btn;
};
createLocationControl.addTo(map);
// Global functions for location creation confirmation
window.confirmLocation = function(x, z) {
// Navigate to create_new view with pre-filled coordinates using WebSocket
// This follows the exact same pattern as all other action calls in the system
window.socket.emit(
'widget_event', // Socket event name
['locations', // Module identifier
['toggle_locations_widget_view', // Action identifier
{ // Action parameters
'action': 'show_create_new',
'prefill_x': x,
'prefill_z': z,
'prefill_y': 0 // Default Y coordinate
}]]
);
console.log('[MAP] Navigating to create location form with coordinates:', x, z);
// Remove temp marker
if (tempMarker) {
map.removeLayer(tempMarker);
tempMarker = null;
}
};
window.cancelLocation = function() {
// Remove temp marker
if (tempMarker) {
map.removeLayer(tempMarker);
tempMarker = null;
}
};
// ========================================
// Location Actions (Edit, Enable, Move, Set Teleport)
// ========================================
{{ webmap_templates.location_actions|safe }}
// ========================================
// Player Actions (Kick, Message, Mute, Auth, Teleport)
// ========================================
{{ webmap_templates.player_actions|safe }}
})();
</script>
</main>

View File

@@ -0,0 +1,27 @@
<header>
<div>
<span>Locations</span>
</div>
</header>
<aside>
{{ options_toggle }}
</aside>
<main>
<table class="options_table">
<thead>
<tr>
<th colspan="2">location widget options</th>
</tr>
</thead>
<tbody>
<tr>
<th colspan="2"><span>location-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,172 @@
// ========================================
// Location Popup Actions
// ========================================
// Edit location from map popup
window.editLocationFromMap = function(dataset, owner, identifier) {
// This follows the exact same pattern as control_edit_link.html
window.socket.emit(
'widget_event',
['locations',
['toggle_locations_widget_view', {
'dom_element_owner': owner,
'dom_element_identifier': identifier,
'dom_element_origin': dataset,
'action': 'edit_location_entry'
}]]
);
console.log('[MAP] Opening edit view for location:', identifier);
};
// Toggle enabled status from map popup
window.toggleLocationEnabled = function(dataset, owner, identifier, isChecked) {
// This follows the exact same pattern as control_enabled_link.html
const action = isChecked ? 'enable_location_entry' : 'disable_location_entry';
window.socket.emit(
'widget_event',
['locations',
['toggle_enabled_flag', {
'dom_element_owner': owner,
'dom_element_identifier': identifier,
'dom_element_origin': dataset,
'action': action
}]]
);
console.log('[MAP] Toggling location enabled status:', identifier, 'to', isChecked);
};
// Move location to new position (relative teleport)
window.moveLocationFromMap = function(locationId) {
const loc = locations[locationId];
if (!loc) {
console.error('[MAP] Location not found:', locationId);
return;
}
// 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(102, 204, 255, 0.95)';
infoDiv.style.borderColor = 'var(--lcars-anakiwa)';
infoDiv.style.color = '#000';
infoDiv.style.fontWeight = 'bold';
infoDiv.innerHTML = '📍 Click new location position on map';
document.getElementById('map').appendChild(infoDiv);
// Change cursor
map.getContainer().style.cursor = 'crosshair';
// Wait for click
map.once('click', function(e) {
const newCoords = e.latlng;
const newX = Math.round(newCoords.lat);
const newZ = Math.round(newCoords.lng);
// Calculate offset
const offsetX = newX - loc.coordinates.x;
const offsetZ = newZ - loc.coordinates.z;
// Move teleport_entry relatively if it exists
let newTeleportEntry = loc.teleport_entry || {};
if (newTeleportEntry.x !== undefined && newTeleportEntry.y !== undefined && newTeleportEntry.z !== undefined) {
newTeleportEntry = {
x: (parseFloat(newTeleportEntry.x) || 0) + offsetX,
y: parseFloat(newTeleportEntry.y) || 0, // Y stays same
z: (parseFloat(newTeleportEntry.z) || 0) + offsetZ
};
}
// Call edit_location with ALL fields
window.socket.emit(
'widget_event',
['locations',
['edit_location', {
'location_identifier': loc.identifier,
'location_name': loc.name,
'location_shape': loc.shape,
'location_type': loc.type || [],
'location_coordinates': {
'x': newX,
'y': loc.coordinates.y,
'z': newZ
},
'location_teleport_entry': newTeleportEntry,
'location_dimensions': loc.dimensions || {},
'location_owner': loc.owner,
'is_enabled': loc.is_enabled
}]]
);
console.log('[MAP] Moved location to:', newX, newZ, 'Offset:', offsetX, offsetZ);
// Cleanup
map.getContainer().style.cursor = '';
document.getElementById('map').removeChild(infoDiv);
});
};
// Set teleport coordinates
window.setTeleportFromMap = function(locationId) {
const loc = locations[locationId];
if (!loc) {
console.error('[MAP] Location not found:', locationId);
return;
}
// 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, 153, 0, 0.95)';
infoDiv.style.borderColor = 'var(--lcars-golden-tanoi)';
infoDiv.style.color = '#000';
infoDiv.style.fontWeight = 'bold';
infoDiv.innerHTML = '🎯 Click teleport destination on map';
document.getElementById('map').appendChild(infoDiv);
// Change cursor
map.getContainer().style.cursor = 'crosshair';
// Wait for click
map.once('click', function(e) {
const teleportCoords = e.latlng;
const tpX = Math.round(teleportCoords.lat);
const tpZ = Math.round(teleportCoords.lng);
// Use current Y coordinate or default to location Y
const tpY = (loc.teleport_entry && loc.teleport_entry.y) || loc.coordinates.y;
// Call edit_location with ALL fields
window.socket.emit(
'widget_event',
['locations',
['edit_location', {
'location_identifier': loc.identifier,
'location_name': loc.name,
'location_shape': loc.shape,
'location_type': loc.type || [],
'location_coordinates': loc.coordinates,
'location_teleport_entry': {
'x': tpX,
'y': tpY,
'z': tpZ
},
'location_dimensions': loc.dimensions || {},
'location_owner': loc.owner,
'is_enabled': loc.is_enabled
}]]
);
console.log('[MAP] Set teleport to:', tpX, tpY, tpZ);
// Cleanup
map.getContainer().style.cursor = '';
document.getElementById('map').removeChild(infoDiv);
});
};

View File

@@ -0,0 +1,143 @@
// Helper function to create location shape based on type
function createLocationShape(locationId, loc) {
const coords = loc.coordinates;
const centerLatLng = [coords.x, coords.z]; // 7D2D coordinates (x, z)
const dims = loc.dimensions || {};
const shape = loc.shape || 'circle';
const isEnabled = loc.is_enabled;
const is3D = (shape === 'box' || shape === 'spherical');
// Color scheme
const fillColor = isEnabled ? '#ff9900' : '#666666';
const strokeColor = isEnabled ? '#ffcc00' : '#999999';
const fillOpacity = isEnabled ? 0.3 : 0.15;
let leafletShape;
if (shape === 'circle') {
const radius = parseFloat(dims.radius || 10);
leafletShape = L.circle(centerLatLng, {
radius: radius,
fillColor: fillColor,
color: strokeColor,
weight: 2,
opacity: 0.8,
fillOpacity: fillOpacity
});
} else if (shape === 'spherical') {
const radius = parseFloat(dims.radius || 10);
leafletShape = L.circle(centerLatLng, {
radius: radius,
fillColor: fillColor,
color: strokeColor,
weight: 2,
opacity: 0.8,
fillOpacity: fillOpacity,
dashArray: '5, 5' // Dashed to indicate 3D
});
} else if (shape === 'rectangular') {
const width = parseFloat(dims.width || 10);
const length = parseFloat(dims.length || 10);
// Rectangle bounds: from center, extend width/length in both directions
const bounds = [
[coords.x - width, coords.z - length],
[coords.x + width, coords.z + length]
];
leafletShape = L.rectangle(bounds, {
fillColor: fillColor,
color: strokeColor,
weight: 2,
opacity: 0.8,
fillOpacity: fillOpacity
});
} else if (shape === 'box') {
const width = parseFloat(dims.width || 10);
const length = parseFloat(dims.length || 10);
const bounds = [
[coords.x - width, coords.z - length],
[coords.x + width, coords.z + length]
];
leafletShape = L.rectangle(bounds, {
fillColor: fillColor,
color: strokeColor,
weight: 2,
opacity: 0.8,
fillOpacity: fillOpacity,
dashArray: '5, 5' // Dashed to indicate 3D
});
} else {
// Fallback to circle
leafletShape = L.circle(centerLatLng, {
radius: 10,
fillColor: fillColor,
color: strokeColor,
weight: 2,
opacity: 0.8,
fillOpacity: fillOpacity
});
}
// Build popup content
const dimensionText = shape === 'circle' || shape === 'spherical'
? `Radius: ${dims.radius || 'N/A'}`
: `Width: ${dims.width || 'N/A'}, Length: ${dims.length || 'N/A'}${shape === 'box' ? ', Height: ' + (dims.height || 'N/A') : ''}`;
// Parse locationId to extract components
// Format: {dataset}_{owner}_{identifier}
const locationIdParts = locationId.split('_');
const dataset = locationIdParts.slice(0, -2).join('_'); // Handle datasets with underscores
const owner = locationIdParts[locationIdParts.length - 2];
const identifier = locationIdParts[locationIdParts.length - 1];
const teleportEntry = loc.teleport_entry || {};
const hasTeleport = teleportEntry.x !== undefined && teleportEntry.y !== undefined && teleportEntry.z !== undefined;
const teleportText = hasTeleport
? `TP: ${parseFloat(teleportEntry.x || 0).toFixed(0)}, ${parseFloat(teleportEntry.y || 0).toFixed(0)}, ${parseFloat(teleportEntry.z || 0).toFixed(0)}`
: 'TP: Not set';
// Use template literal for clean HTML
const popupContent = `
<div style="min-width: 250px; font-family: monospace;">
<b style="font-size: 1.1em;">${loc.name}</b>
<br><span style="font-size: 0.9em; color: #888;">${is3D ? '🎲 3D' : '⬜ 2D'} - ${shape}</span>
<br><hr style="margin: 5px 0; border-color: #333;">
<b>Type:</b> ${loc.type && loc.type.length > 0 ? loc.type.join(', ') : 'None'}
<br><b>Owner:</b> ${loc.owner}
<br><b>Status:</b> ${isEnabled ? '✅ Enabled' : '❌ Disabled'}
<br><b>Position:</b> ${coords.x.toFixed(0)}, ${coords.y.toFixed(0)}, ${coords.z.toFixed(0)}
<br><b>Dimensions:</b> ${dimensionText}
<br><b>${teleportText}</b>
<br><hr style="margin: 8px 0; border-color: #333;">
<div style="display: flex; gap: 5px; justify-content: space-between; align-items: center; margin-bottom: 5px;">
<button onclick="editLocationFromMap('${dataset}', '${owner}', '${identifier}')"
style="flex: 1; padding: 6px 12px; background: var(--lcars-hopbush); color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">
✏️ Edit</button>
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer; white-space: nowrap;">
<input type="checkbox"
id="enable_${locationId}"
${isEnabled ? 'checked' : ''}
onchange="toggleLocationEnabled('${dataset}', '${owner}', '${identifier}', this.checked)"
style="cursor: pointer; width: 18px; height: 18px;" />
<span style="font-weight: bold;">Enabled</span>
</label>
</div>
<div style="display: flex; gap: 5px;">
<button onclick="moveLocationFromMap('${locationId}')"
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;">
📍 Move</button>
<button onclick="setTeleportFromMap('${locationId}')"
style="flex: 1; padding: 6px 10px; background: var(--lcars-golden-tanoi); color: #000; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 0.9em;">
🎯 Set TP</button>
</div>
</div>
`;
leafletShape.bindPopup(popupContent);
leafletShape.addTo(map);
return leafletShape;
}
// Location shapes are now loaded dynamically via Socket.IO
// Initial loading is handled by location_update events
// See location_update_handler.html for shape creation logic

View File

@@ -0,0 +1,32 @@
// Listen for location updates/additions
if (data.data_type === 'location_update' && data.payload && data.payload.location) {
const locationId = data.payload.location_id;
const loc = data.payload.location;
// Update locations dictionary with new data
locations[locationId] = loc;
// Remove old shape if exists
if (locationShapes[locationId]) {
map.removeLayer(locationShapes[locationId]);
delete locationShapes[locationId];
}
// Create new shape
try {
const shape = createLocationShape(locationId, loc);
locationShapes[locationId] = shape;
} catch (error) {
console.error('[MAP] Error updating location shape:', error);
}
}
// Listen for location removals
if (data.data_type === 'location_remove' && data.payload && data.payload.location_id) {
const locationId = data.payload.location_id;
if (locationShapes[locationId]) {
map.removeLayer(locationShapes[locationId]);
delete locationShapes[locationId];
}
}

View File

@@ -0,0 +1,113 @@
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_location_update_to_map(*args, **kwargs):
"""Send location updates to map view via socket.io"""
module = args[0]
method = kwargs.get("method", None)
updated_values_dict = kwargs.get("updated_values_dict", None)
if updated_values_dict is None:
return
# Check which clients are viewing the map
for clientid in module.webserver.connected_clients.keys():
current_view = module.get_current_view(clientid)
if current_view != "map":
continue
if method in ["upsert", "update", "edit"]:
# Send location update for each changed location
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
# updated_values_dict structure at callback depth 4:
# {location_identifier: {location_data}}
# location_data includes "owner" field
for 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:
continue
location_id = f"{active_dataset}_{owner_steamid}_{identifier}"
# Get full location data from DOM if fields are missing in updated_values_dict
# (e.g., when only is_enabled is updated)
full_location_dict = (
module.dom.data
.get("module_locations", {})
.get("elements", {})
.get(active_dataset, {})
.get(owner_steamid, {})
.get(identifier, {})
)
coordinates = location_dict.get("coordinates")
if coordinates is None:
coordinates = full_location_dict.get("coordinates", {})
dimensions = location_dict.get("dimensions")
if dimensions is None:
dimensions = full_location_dict.get("dimensions", {})
location_data = {
"name": location_dict.get("name", full_location_dict.get("name", "Unknown")),
"identifier": identifier,
"owner": owner_steamid,
"shape": location_dict.get("shape", full_location_dict.get("shape", "circle")),
"coordinates": {
"x": float(coordinates.get("x", 0)),
"y": float(coordinates.get("y", 0)),
"z": float(coordinates.get("z", 0))
},
"dimensions": dimensions,
"teleport_entry": location_dict.get("teleport_entry", full_location_dict.get("teleport_entry", {})),
"type": location_dict.get("type", full_location_dict.get("type", [])),
"is_enabled": location_dict.get("is_enabled", full_location_dict.get("is_enabled", False))
}
module.webserver.send_data_to_client_hook(
module,
payload={
"location_id": location_id,
"location": location_data
},
data_type="location_update",
clients=[clientid]
)
elif method in ["remove"]:
# Send location removal
location_origin = updated_values_dict[2]
owner_steamid = updated_values_dict[3]
location_identifier = updated_values_dict[-1]
location_id = f"{location_origin}_{owner_steamid}_{location_identifier}"
module.webserver.send_data_to_client_hook(
module,
payload={
"location_id": location_id
},
data_type="location_remove",
clients=[clientid]
)
trigger_meta = {
"description": "sends location updates to webmap clients",
"main_function": send_location_update_to_map,
"handlers": {
"module_locations/elements/%map_identifier%/%owner_steamid%/%element_identifier%":
send_location_update_to_map,
}
}
loaded_modules_dict["module_" + module_name].register_trigger(trigger_name, trigger_meta)

View File

@@ -0,0 +1,61 @@
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):
player_name = regex_result.group("player_name")
command = regex_result.group("command")
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
all_players_dict = (
module.dom.data
.get("module_players", {})
.get("elements", {})
.get(active_dataset)
)
steamid = None
servertime_player_died = "n/A"
for player_steamid, player_dict in all_players_dict.items():
if player_dict["name"] == player_name:
steamid = player_steamid
servertime_player_died = player_dict.get("last_seen_gametime", servertime_player_died)
break
if steamid is None:
return
if command == 'died':
event_data = ['edit_location', {
'location_coordinates': {
"x": player_dict["pos"]["x"],
"y": player_dict["pos"]["y"],
"z": player_dict["pos"]["z"]
},
'location_name': "Place of Death",
'action': 'edit',
'location_enabled': True,
'last_changed': servertime_player_died
}]
module.trigger_action_hook(origin_module, event_data=event_data, dispatchers_steamid=steamid)
trigger_meta = {
"description": "reacts to telnets player dying message",
"main_function": main_function,
"triggers": [
{
"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,63 @@
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")
if command != "EnterMultiplayer":
return
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
player_dict = {
"pos": {
"x": regex_result.group("pos_x"),
"y": regex_result.group("pos_y"),
"z": regex_result.group("pos_z")
}
}
player_steamid = regex_result.group("player_steamid")
first_seen_gametime_string = module.game_environment.get_last_recorded_gametime_string()
event_data = ['edit_location', {
'location_owner': player_steamid,
'location_coordinates': {
"x": player_dict["pos"]["x"],
"y": player_dict["pos"]["y"],
"z": player_dict["pos"]["z"]
},
'location_name': "Initial Spawn",
'action': 'create_new',
'location_enabled': True,
'last_changed': first_seen_gametime_string
}]
module.trigger_action_hook(origin_module, event_data=event_data)
trigger_meta = {
"description": "reacts to any initial playerspawn",
"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
}
]
}
loaded_modules_dict["module_" + module_name].register_trigger(trigger_name, trigger_meta)

View File

@@ -0,0 +1,86 @@
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):
position_dict = {
"pos": {
"x": regex_result.group("pos_x"),
"y": regex_result.group("pos_y"),
"z": regex_result.group("pos_z")
}
}
zombie_id = regex_result.group("entity_id")
zombie_name = regex_result.group("zombie_name")
screamer_safe_locations = []
found_screamer_safe_location = False
for screamer_safe_location in origin_module.get_elements_by_type("is_screamerfree"):
screamer_safe_locations.append(screamer_safe_location)
found_screamer_safe_location = True
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
if zombie_name == "zombieScreamer":
for location_dict in screamer_safe_locations:
if origin_module.locations.position_is_inside_boundary(position_dict, location_dict):
event_data = ['manage_entities', {
'dataset': active_dataset,
'entity_id': zombie_id,
'entity_name': zombie_name,
'action': 'kill'
}]
# no steamid cause it's a system_call
module.trigger_action_hook(origin_module.game_environment, event_data=event_data)
event_data = ['say_to_all', {
'message': (
'[FF6666]Screamer ([FFFFFF]{entity_id}[FF6666]) spawned[-] '
'[FFFFFF]inside [CCCCFF]{location_name}[FFFFFF].[-]'.format(
entity_id=zombie_id,
location_name=location_dict.get("name")
)
)
}]
module.trigger_action_hook(origin_module.game_environment, event_data=event_data)
# we only need to match one location. even though a screamer can be in multiple locations at once,
# we still only have to kill it once :)
break
else:
if found_screamer_safe_location:
event_data = ['say_to_all', {
'message': (
'[FF6666]Screamer ([FFFFFF]{entity_id}[FF6666]) spawned[-] '
'[CCCCFF]somewhere[FFFFFF]...[-]'.format(
entity_id=zombie_id
)
)
}]
module.trigger_action_hook(origin_module.game_environment, event_data=event_data)
trigger_meta = {
"description": "reacts to spawning zombies (screamers, mostly)",
"main_function": main_function,
"triggers": [
{
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
r"(?P<command>.+?)\s"
r"\["
r"type=(.*),\s"
r"name=(?P<zombie_name>.+?),\s"
r"id=(?P<entity_id>.*)\]\sat\s\((?P<pos_x>.*),\s(?P<pos_y>.*),\s(?P<pos_z>.*)\)\s"
r"Day=(\d.*)\s"
r"TotalInWave=(\d.*)\s"
r"CurrentWave=(\d.*)"
),
"callback": main_function
}
]
}
loaded_modules_dict["module_" + module_name].register_trigger(trigger_name, trigger_meta)

Some files were not shown because too many files have changed in this diff Show More