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

337
DOM_HELPER_MIGRATION.md Normal file
View File

@@ -0,0 +1,337 @@
# DOM Helper Migration Plan
## Step 1: Add Helpers to Module Base Class
**File:** `bot/module.py`
Add two methods to Module class:
```python
def get_element_from_dom(self, module_name, dataset, owner, identifier=None):
"""
Get full element from DOM.
Args:
module_name: e.g. "module_locations"
dataset: active dataset name
owner: owner steamid
identifier: element identifier (optional for owner-level access)
Returns:
Full element dict from DOM
"""
path_data = (
self.dom.data
.get(module_name, {})
.get("elements", {})
.get(dataset, {})
.get(owner, {})
)
if identifier:
return path_data.get(identifier, {})
return path_data
def update_element(self, module_name, dataset, owner, identifier, updates, dispatchers_steamid=None):
"""
Update element fields in DOM.
Args:
module_name: e.g. "module_locations"
dataset: active dataset name
owner: owner steamid
identifier: element identifier
updates: dict with fields to update
dispatchers_steamid: who triggered update
Example:
module.update_element("module_locations", dataset, owner, "Lobby",
{"is_enabled": True})
"""
# Get current element
element = self.get_element_from_dom(module_name, dataset, owner, identifier)
# Merge updates
element.update(updates)
# Write back full element
self.dom.data.upsert({
module_name: {
"elements": {
dataset: {
owner: {
identifier: element
}
}
}
}
}, dispatchers_steamid=dispatchers_steamid, min_callback_level=4, max_callback_level=5)
def get_element_from_callback(self, updated_values_dict, matched_path):
"""
Get full element from DOM based on callback data.
Args:
updated_values_dict: Dict from callback kwargs
matched_path: Pattern with wildcards like "module_x/elements/%dataset%/%owner%/%id%"
Returns:
Full element dict from DOM
Notes:
- matched_path contains wildcards (e.g., %owner_steamid%)
- Wildcards are placeholders, not actual values
- Actual values come from updated_values_dict structure
- Works with any depth (4, 5, 6+)
"""
parts = matched_path.split('/')
if 'elements' not in parts or len(parts) < 4:
return {}
active_dataset = self.dom.data.get("module_game_environment", {}).get("active_dataset")
module_name = parts[0]
# Depth 5+: module/elements/dataset/owner/identifier[/property]
# Structure: {identifier: {data}}
if len(parts) >= 5:
identifier = list(updated_values_dict.keys())[0]
element_dict = updated_values_dict[identifier]
owner = element_dict.get("owner")
# Get full element (ignoring property path if depth > 5)
return self.get_element_from_dom(module_name, active_dataset, owner, identifier)
# Depth 4: module/elements/dataset/owner
# Structure: {owner: {...}}
elif len(parts) == 4:
owner = list(updated_values_dict.keys())[0]
return self.get_element_from_dom(module_name, active_dataset, owner)
return {}
```
## Step 2: Update All Actions
**Find all actions:**
```bash
grep -r "module.dom.data.upsert" bot/modules/*/actions/*.py
```
**Pattern - OLD:**
```python
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)
```
**Pattern - NEW:**
```python
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset")
module.update_element(
"module_locations",
active_dataset,
location_owner,
location_identifier,
{"is_enabled": element_is_enabled},
dispatchers_steamid=dispatchers_steamid
)
```
**Actions checklist:**
- [ ] `locations/actions/toggle_enabled_flag.py`
- [ ] `locations/actions/edit_location.py` (keep as-is, already writes full element)
- [ ] `players/actions/update_player_permission_level.py`
- [ ] `players/actions/toggle_player_mute.py`
- [ ] `dom_management/actions/select.py`
- [ ] All other actions with partial upserts
## Step 3: Update All Handlers
**Find all handlers:**
```bash
python3 scripts/analyze_callback_usage.py
```
**Pattern - OLD:**
```python
def handler(*args, **kwargs):
module = args[0]
updated_values_dict = kwargs.get("updated_values_dict", {})
for identifier, data in updated_values_dict.items():
owner = data.get("owner")
# Complex DOM lookups...
full_element = module.dom.data.get(...)...
```
**Pattern - NEW:**
```python
def handler(*args, **kwargs):
module = args[0]
updated_values_dict = kwargs.get("updated_values_dict", {})
matched_path = kwargs.get("matched_path", "")
for identifier, data in updated_values_dict.items():
# Get full element
element = module.get_element_from_callback(
{identifier: data},
matched_path
)
# Use directly
owner = element.get("owner")
name = element.get("name")
# ...
```
**Handlers checklist:**
- [ ] `locations/widgets/manage_locations_widget.py::table_row`
- [ ] `locations/widgets/manage_locations_widget.py::update_location_on_map`
- [ ] `locations/widgets/manage_locations_widget.py::update_selection_status`
- [ ] `locations/widgets/manage_locations_widget.py::update_enabled_flag`
- [ ] `players/widgets/manage_players_widget.py::table_rows`
- [ ] `players/widgets/manage_players_widget.py::update_widget`
- [ ] `game_environment/widgets/location_change_announcer_widget.py::announce_location_change`
## Step 4: Testing
**For each file:**
1. Update code
2. Restart bot
3. Trigger action/handler
4. Check logs for errors
5. Verify UI updates
**Automated check:**
```bash
# No more complex upserts:
grep -r "\"elements\":" bot/modules/*/actions/*.py | wc -l
# Should be 0 or very few (only edit_location type actions)
# No more DOM fallback in handlers:
grep -r "\.get.*\.get.*\.get.*\.get" bot/modules/*/widgets/*.py | wc -l
# Should be much lower
```
## Step 5: Validate Wildcards and Browser Updates
**Critical check - wildcards MUST work:**
Handler registration uses wildcards:
```python
"module_locations/elements/%map_identifier%/%owner_steamid%/%element_identifier%": handler
```
Callback system passes `matched_path` with wildcards intact.
Helper `get_element_from_callback()` MUST parse this correctly.
**Test each wildcard pattern:**
```bash
# Test location handler (depth 5):
# Pattern: module_locations/elements/%map%/%owner%/%id%
# Edit location → check handler receives correct element
# Test player handler (depth 4):
# Pattern: module_players/elements/%map%/%steamid%
# Update player → check handler receives correct element
# Test enabled flag (depth 6):
# Pattern: module_locations/elements/%map%/%owner%/%id%/is_enabled
# Toggle enabled → check handler receives correct element
```
**Verify browser updates:**
For EACH handler that uses `send_data_to_client_hook()`:
1. Check handler uses `get_element_from_callback()` to get full element
2. Check payload sent to browser is complete
3. Verify browser JavaScript receives `data.payload.{element}` with all fields
4. Test in browser: trigger action → verify immediate update without page refresh
**Handlers sending to browser:**
- [ ] `update_location_on_map` → sends `location_update`
- [ ] `table_row` → sends `table_row`
- [ ] `update_enabled_flag` → sends `element_content`
- [ ] `table_rows` (players) → sends `table_row`
- [ ] `update_widget` (players) → sends `table_row_content`
All must send complete elements.
## Step 6: Example for Future
**New trigger - warn player on low health:**
```python
def low_health_warning(*args, **kwargs):
module = args[0]
updated_values_dict = kwargs.get("updated_values_dict", {})
matched_path = kwargs.get("matched_path", "")
# Get full player element
for steamid, data in updated_values_dict.items():
player = module.get_element_from_callback(
{steamid: data},
matched_path
)
# Check health
if player.get("health", 100) < 60:
module.trigger_action_hook(module, event_data=[
'say_to_player',
{'steamid': steamid, 'message': 'Low health!'}
])
# Register at health property level (depth 5):
widget_meta = {
"handlers": {
"module_players/elements/%map_identifier%/%steamid%/health": low_health_warning
}
}
# Handler receives full player element, not just health value!
# player = {
# "steamid": "12345",
# "name": "Player",
# "health": 45, ← changed field
# "pos": {...},
# ... ← all other fields
# }
```
**New action - set player flag:**
```python
def main_function(module, event_data, dispatchers_steamid):
steamid = event_data[1].get("steamid")
flag_value = event_data[1].get("flag_value")
active_dataset = module.dom.data.get("module_game_environment", {}).get("active_dataset")
# Update element
module.update_element(
"module_players",
active_dataset,
steamid,
None, # player has no sub-identifier
{"custom_flag": flag_value},
dispatchers_steamid=dispatchers_steamid
)
```
Done. Clear pattern for everything.