338 lines
9.5 KiB
Markdown
338 lines
9.5 KiB
Markdown
|
|
# 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.
|