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

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)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,202 @@
from bot.module import Module
from bot import loaded_modules_dict
from bot.logger import get_logger
from bot.constants import (
PERMISSION_LEVEL_DEFAULT,
PERMISSION_LEVEL_BUILDER,
PERMISSION_LEVEL_PLAYER,
is_moderator_or_higher,
is_builder_or_higher
)
logger = get_logger("permissions")
class Permissions(Module):
def __init__(self):
setattr(self, "default_options", {
"module_name": self.get_module_identifier()[7:],
"default_player_password": None
})
setattr(self, "required_modules", [
'module_dom',
'module_players',
'module_locations',
'module_webserver'
])
self.next_cycle = 0
self.run_observer_interval = 5
self.all_available_actions_dict = {}
self.all_available_widgets_dict = {}
Module.__init__(self)
@staticmethod
def get_module_identifier():
return "module_permissions"
# region Standard module stuff
def setup(self, options=dict):
Module.setup(self, options)
# endregion
def start(self):
""" all modules have been loaded and initialized by now. we can bend the rules here."""
self.set_permission_hooks()
self.all_available_actions_dict = self.get_all_available_actions_dict()
self.all_available_widgets_dict = self.get_all_available_widgets_dict()
Module.start(self)
# endregion
# ==================== Permission Check Helpers ====================
@staticmethod
def _is_owner(steamid: str, event_data: list) -> bool:
"""Check if user is the owner of the element being modified."""
return str(steamid) == event_data[1].get("dom_element_owner", "")
@staticmethod
def _check_toggle_flag_permission(permission_level: int, steamid: str, event_data: list) -> bool:
"""Check permission for toggle_enabled_flag action."""
if event_data[0] != "toggle_enabled_flag":
return False
# Builders and below can only edit their own elements
if permission_level >= PERMISSION_LEVEL_BUILDER:
return not Permissions._is_owner(steamid, event_data)
return False
@staticmethod
def _check_widget_options_permission(permission_level: int, event_data: list) -> bool:
"""Check permission for widget options view."""
if not (event_data[0].startswith("toggle_") and event_data[0].endswith("_widget_view")):
return False
if event_data[1].get("action") == "show_options":
# Only moderators and admins can see options
return not is_moderator_or_higher(permission_level)
return False
@staticmethod
def _check_dom_management_permission(permission_level: int, steamid: str, event_data: list) -> bool:
"""Check permissions for DOM management actions."""
action_name = event_data[0]
sub_action = event_data[1].get("action", "")
if action_name not in ["delete", "select"]:
return False
# Select/deselect: builders and below can only modify their own elements
if sub_action in ["select_dom_element", "deselect_dom_element"]:
if permission_level >= PERMISSION_LEVEL_BUILDER:
return not Permissions._is_owner(steamid, event_data)
return False
# Delete: only moderators and admins
if sub_action == "delete_selected_dom_elements":
return permission_level >= PERMISSION_LEVEL_BUILDER
return False
@staticmethod
def _check_players_permission(permission_level: int, event_data: list) -> bool:
"""Check permissions for player management actions."""
if event_data[0] == "kick_player":
# Only builder and above can kick players
return permission_level > PERMISSION_LEVEL_BUILDER
return False
@staticmethod
def _check_locations_permission(permission_level: int, steamid: str, event_data: list) -> bool:
"""Check permissions for location management actions."""
action_name = event_data[0]
sub_action = event_data[1].get("action", "")
if action_name not in ["edit_location", "management_tools", "toggle_locations_widget_view"]:
return False
# Edit/enable/disable: builders and below can only modify their own locations
if sub_action in ["edit_location_entry", "enable_location_entry", "disable_location_entry"]:
if permission_level >= PERMISSION_LEVEL_BUILDER:
return not Permissions._is_owner(steamid, event_data)
return False
# Create new: only players and above
if sub_action == "show_create_new":
return permission_level > PERMISSION_LEVEL_PLAYER
return False
@staticmethod
def _check_telnet_permission(permission_level: int, event_data: list) -> bool:
"""Check permissions for telnet actions."""
if event_data[0] == "shutdown":
# Only moderators and admins can shutdown server
return permission_level >= PERMISSION_LEVEL_BUILDER
return False
# ==================== Main Permission Check ====================
@staticmethod
def trigger_action_with_permission(*args, **kwargs):
"""
Check permissions before triggering an action.
Permissions default to allowed if no specific rule matches.
Module-specific permission checks are delegated to helper methods.
"""
module = args[0]
event_data = kwargs.get("event_data", [])
dispatchers_steamid = kwargs.get("dispatchers_steamid", None)
# Default to allowing action
permission_denied = False
if dispatchers_steamid is not None:
# Get user's permission level
permission_level = int(
module.dom.data.get("module_players", {}).get("admins", {}).get(
dispatchers_steamid, PERMISSION_LEVEL_DEFAULT
)
)
module_identifier = module.get_module_identifier()
# Run permission checks based on action and module
permission_denied = (
Permissions._check_toggle_flag_permission(permission_level, dispatchers_steamid, event_data) or
Permissions._check_widget_options_permission(permission_level, event_data) or
(module_identifier == "module_dom_management" and
Permissions._check_dom_management_permission(permission_level, dispatchers_steamid, event_data)) or
(module_identifier == "module_players" and
Permissions._check_players_permission(permission_level, event_data)) or
(module_identifier == "module_locations" and
Permissions._check_locations_permission(permission_level, dispatchers_steamid, event_data)) or
(module_identifier == "module_telnet" and
Permissions._check_telnet_permission(permission_level, event_data))
)
if permission_denied:
logger.warn("permission_denied",
action=event_data[0],
user=dispatchers_steamid,
permission_level=permission_level)
event_data[1]["has_permission"] = not permission_denied
# Execute the action
return module.trigger_action(module, event_data=event_data, dispatchers_steamid=dispatchers_steamid)
@staticmethod
def template_render_hook_with_permission(*args, **kwargs):
module = args[0]
return module.template_render(*args, **kwargs)
def set_permission_hooks(self):
for identifier, module in loaded_modules_dict.items():
module.trigger_action_hook = self.trigger_action_with_permission
module.template_render_hook = self.template_render_hook_with_permission
loaded_modules_dict[Permissions().get_module_identifier()] = Permissions()

View File

@@ -0,0 +1,75 @@
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
player_steamid = event_data[1].get("player_steamid", None)
dataset = event_data[1].get("dataset", None)
entered_password = event_data[1].get("entered_password", None)
default_player_password = module.default_options.get("default_player_password", None)
if all([
dataset is not None,
player_steamid is not None,
entered_password is not None,
default_player_password is not None
]):
if default_player_password == entered_password:
is_authenticated = True
else:
is_authenticated = False
module.dom.data.upsert({
"module_players": {
"elements": {
dataset: {
player_steamid: {
"is_authenticated": is_authenticated
}
}
}
}
}, dispatchers_steamid=player_steamid)
if is_authenticated is True:
module.callback_success(callback_success, module, event_data, dispatchers_steamid)
return
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
def callback_success(module, event_data, dispatchers_steamid, match=None):
player_steamid = event_data[1].get("player_steamid", None)
if player_steamid is not None:
event_data = ['say_to_player', {
'steamid': player_steamid,
'message': '[66FF66]Thank you for playing along[-][FFFFFF], you may now leave the Lobby-area[-]'
}]
module.trigger_action_hook(module.players, event_data=event_data)
def callback_fail(module, event_data, dispatchers_steamid):
player_steamid = event_data[1].get("player_steamid", None)
if player_steamid is not None:
event_data = ['say_to_player', {
'steamid': player_steamid,
'message': '[FF6666]Could not authenticate[-][FFFFFF], wrong password perhaps?[-]'
}]
module.trigger_action_hook(module.players, event_data=event_data)
pass
action_meta = {
"description": "sets a players authenticated flag",
"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,89 @@
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
player_steamid = event_data[1].get("player_steamid", None)
active_dataset = event_data[1].get("dataset", None)
flag_player_to_be_muted = event_data[1].get("is_muted", None)
if all([
active_dataset is not None,
player_steamid is not None,
flag_player_to_be_muted is not None,
]):
player_dict = (
module.dom.data
.get("module_players", {})
.get("elements", {})
.get(active_dataset, {})
.get(player_steamid, {})
)
player_is_currently_muted = player_dict.get("is_muted", False)
if not flag_player_to_be_muted:
default_player_password = module.default_options.get("default_player_password", None)
if player_is_currently_muted or default_player_password is None:
module.callback_success(callback_success, module, event_data, dispatchers_steamid)
return
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
def callback_success(module, event_data, dispatchers_steamid, match=None):
player_steamid = event_data[1].get("player_steamid", None)
active_dataset = event_data[1].get("dataset", None)
if player_steamid is not None:
default_player_password = module.default_options.get("default_player_password", None)
if default_player_password is not None:
event_data = ['say_to_player', {
'steamid': player_steamid,
'message': '[66FF66]Free speech[-][FFFFFF], you may now chat. Say hello ^^[-]'
}]
module.trigger_action_hook(module.players, event_data=event_data)
event_data = ['toggle_player_mute', {
'steamid': player_steamid,
'mute_status': False,
'dataset': active_dataset
}]
module.trigger_action_hook(module.players, event_data=event_data)
def callback_fail(module, event_data, dispatchers_steamid):
player_steamid = event_data[1].get("player_steamid", None)
active_dataset = event_data[1].get("dataset", None)
if player_steamid is not None:
default_player_password = module.default_options.get("default_player_password", None)
if default_player_password is not None:
event_data = ['say_to_player', {
'steamid': player_steamid,
'message': '[FF6666]You have been automatically muted[-][FFFFFF], until you have authenticated![-]'
}]
module.trigger_action_hook(module.players, event_data=event_data)
event_data = ['toggle_player_mute', {
'steamid': player_steamid,
'mute_status': True,
'dataset': active_dataset
}]
module.trigger_action_hook(module.players, event_data=event_data)
action_meta = {
"description": "tools to help managing a players ability to chat (and speak?)",
"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,57 @@
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"^.*password\s(?P<password>.*)", command)
if result:
entered_password = result.group("password")
else:
return
event_data = ['set_authentication', {
'dataset': module.dom.data.get("module_game_environment", {}).get("active_dataset", None),
'player_steamid': steamid,
'entered_password': entered_password
}]
module.trigger_action_hook(origin_module, event_data=event_data)
triggers = {
"password": r"\'(?P<player_name>.*)\'\:\s(?P<command>\/password.*)"
}
trigger_meta = {
"description": "validates a players password",
"main_function": main_function,
"triggers": [
{
"identifier": "password (Alloc)",
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
telnet_prefixes["Allocs"]["chat"] +
triggers["password"]
),
"callback": main_function
},
{
"identifier": "password (BCM)",
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
telnet_prefixes["BCM"]["chat"] +
triggers["password"]
),
"callback": main_function
}
]
}
loaded_modules_dict["module_" + module_name].register_trigger(trigger_name, trigger_meta)

View File

@@ -0,0 +1,68 @@
from bot import loaded_modules_dict
from bot import telnet_prefixes
from os import path, pardir
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
trigger_name = path.basename(path.abspath(__file__))[:-3]
def main_function(origin_module, module, regex_result):
command = regex_result.group("command")
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
player_steamid = regex_result.group("player_steamid")
existing_player_dict = (
module.dom.data
.get("module_players", {})
.get("elements", {})
.get(active_dataset, {})
.get(player_steamid, None)
)
if command == "connected":
# we want to mute completely new players and players that are not authenticated on login.
if existing_player_dict is not None:
default_player_password = module.default_options.get("default_player_password", None)
player_is_authenticated = existing_player_dict.get("is_authenticated", False)
if not player_is_authenticated and default_player_password is not None:
is_muted = True
else:
is_muted = False
event_data = ['set_player_mute', {
'dataset': module.dom.data.get("module_game_environment", {}).get("active_dataset", None),
'player_steamid': player_steamid,
'is_muted': is_muted
}]
module.trigger_action_hook(origin_module, event_data=event_data)
trigger_meta = {
"description": "reacts to telnets player discovery messages for real time responses!",
"main_function": main_function,
"triggers": [
{
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
r"\[Steamworks.NET\]\s"
r"(?P<command>.*)\s"
r"player:\s(?P<player_name>.*)\s"
r"SteamId:\s(?P<player_steamid>\d+)\s(.*)"
),
"callback": main_function
}, {
"regex": (
telnet_prefixes["telnet_log"]["timestamp"] +
r"Player\s"
r"(?P<command>.*), "
r"entityid=(?P<entity_id>.*), "
r"name=(?P<player_name>.*), "
r"steamid=(?P<player_steamid>.*), "
r"steamOwner=(?P<owner_id>.*), "
r"ip=(?P<player_ip>.*)"
),
"callback": main_function
}
]
}
loaded_modules_dict["module_" + module_name].register_trigger(trigger_name, trigger_meta)

View File

@@ -0,0 +1,43 @@
from bot import loaded_modules_dict
from os import path, pardir
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
trigger_name = path.basename(path.abspath(__file__))[:-3]
def main_function(*args, **kwargs):
module = args[0]
updated_values_dict = kwargs.get("updated_values_dict", {})
player_steamid = kwargs.get("dispatchers_steamid", None)
is_authenticated = updated_values_dict.get("is_authenticated", None)
try:
if all([
is_authenticated is not None,
player_steamid is not None
]):
event_data = ['set_player_mute', {
'dataset': module.dom.data.get("module_game_environment", {}).get("active_dataset", None),
'player_steamid': player_steamid
}]
if is_authenticated:
event_data[1]["is_muted"] = False
else:
event_data[1]["is_muted"] = True
module.trigger_action_hook(module, event_data=event_data)
except AttributeError:
pass
trigger_meta = {
"description": "reacts to a players authentication change",
"main_function": main_function,
"handlers": {
"module_players/elements/%map_identifier%/%steamid%/is_authenticated": main_function,
}
}
loaded_modules_dict["module_" + module_name].register_trigger(trigger_name, trigger_meta)

View File

@@ -0,0 +1,62 @@
from bot import loaded_modules_dict
from os import path, pardir
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
trigger_name = path.basename(path.abspath(__file__))[:-3]
def main_function(*args, **kwargs):
module = args[0]
original_values_dict = kwargs.get("original_values_dict", {})
updated_values_dict = kwargs.get("updated_values_dict", {})
dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset", None)
found_lobby = False
for lobby in module.locations.get_elements_by_type("is_lobby"):
lobby_dict = lobby
found_lobby = True
if found_lobby is False:
return
# only dive into this when not authenticated
if original_values_dict.get("is_authenticated", False) is False and any([
original_values_dict.get("pos", {}).get("x") != updated_values_dict.get("pos", {}).get("x"),
original_values_dict.get("pos", {}).get("y") != updated_values_dict.get("pos", {}).get("y"),
original_values_dict.get("pos", {}).get("z") != updated_values_dict.get("pos", {}).get("z")
]):
on_the_move_player_dict = (
module.dom.data
.get("module_players", {})
.get("elements", {})
.get(dataset, {})
.get(updated_values_dict.get("steamid"), {})
)
pos_is_inside_coordinates = module.locations.position_is_inside_boundary(updated_values_dict, lobby_dict)
if pos_is_inside_coordinates is True:
# nothing to do, we are inside the lobby
return
# no early exits, seems like the player is outside an active lobby without any authentication!
# seems like we should port ^^
event_data = ['teleport_to_coordinates', {
'location_coordinates': {
"x": lobby_dict["coordinates"]["x"],
"y": lobby_dict["coordinates"]["y"],
"z": lobby_dict["coordinates"]["z"]
},
'steamid': on_the_move_player_dict.get("steamid")
}]
module.trigger_action_hook(module.locations, event_data=event_data)
trigger_meta = {
"description": "reacts to every players move!",
"main_function": main_function,
"handlers": {
"module_players/elements/%map_identifier%/%steamid%/pos": main_function,
}
}
loaded_modules_dict["module_" + module_name].register_trigger(trigger_name, trigger_meta)

View File

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

View File

@@ -0,0 +1,79 @@
from bot import loaded_modules_dict
from bot.constants import TELNET_TIMEOUT_VERY_SHORT
from os import path, pardir
from time import sleep, time
import re
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
action_name = path.basename(path.abspath(__file__))[:-3]
def main_function(module, event_data, dispatchers_steamid=None):
timeout = TELNET_TIMEOUT_VERY_SHORT
timeout_start = time()
event_data[1]["action_identifier"] = action_name
event_data[1]["fail_reason"] = []
if module.telnet.add_telnet_command_to_queue("admin list"):
poll_is_finished = False
# Updated regex for modern 7D2D server format (V 2.x+)
# New format: "Defined User Permissions:" and SteamIDs have "Steam_" prefix
# Timestamps are still present in "Executing command" lines
regex = (
r"(?P<datetime>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})\s(?P<stardate>[-+]?\d*\.\d+|\d+)\s"
r"INF Executing\scommand\s\'admin list\'\sby\sTelnet\sfrom\s(?P<called_by>.*?)\r?\n"
r"(?P<raw_adminlist>Defined User Permissions:[\s\S]*?(?=Defined Group Permissions:))"
)
while not poll_is_finished and (time() < timeout_start + timeout):
sleep(0.25)
match = False
for match in re.finditer(regex, module.telnet.telnet_buffer, re.DOTALL):
poll_is_finished = True
if match:
module.callback_success(callback_success, module, event_data, dispatchers_steamid, match)
return
event_data[1]["fail_reason"].append("action timed out")
else:
event_data[1]["fail_reason"].append("action already queued up")
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
def callback_success(module, event_data, dispatchers_steamid, match=None):
# Updated regex for modern format with "Steam_" prefix
# Example: " 0: Steam_76561198040658370 (stored name: MOTKU)"
regex = (
r"(?:^\s{0,7})(?P<level>\d{1,2})\:\s+Steam_(?P<steamid>\d{17})"
)
raw_adminlist = match.group("raw_adminlist")
admin_dict = {}
for m in re.finditer(regex, raw_adminlist, re.MULTILINE):
admin_dict[m.group("steamid")] = m.group("level")
module.dom.data.upsert({
module.get_module_identifier(): {
"admins": admin_dict
}
})
disable_after_success = event_data[1]["disable_after_success"]
if disable_after_success:
module.disable_action(action_name)
def callback_fail(module, event_data, dispatchers_steamid):
pass
action_meta = {
"description": "gets a list of all admins and mods",
"main_function": main_function,
"callback_success": callback_success,
"callback_fail": callback_fail,
"requires_telnet_connection": True,
"enabled": True
}
loaded_modules_dict["module_" + module_name].register_action(action_name, action_meta)

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