64kramsystem / 64kramsystem/ghidra-vice-connector
Watch expressions: evaluate and display named memory/register values on each stop
- Dominant language
- Python
- Stars
- 1
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
## Summary
Allow the user to define named watch expressions that are re-evaluated on every stop event and displayed in the Ghidra trace. Examples: current raster line (`$D012`), bank config (`$01`), score variable at a known address, or a dereferenced pointer. Eliminates the need to manually read memory for frequently-checked values during a game debugging session.
Source: IceBroLite Watch View; vscode-kickass-studio variable watch (confirmed in `src/vice/viceVariableInfo.ts`).
---
## Expression types to support
| Syntax | Meaning |
|--------|---------|
| `$D012` | Byte at address |
| `$DC01` | Byte at address |
| `*$FB` | Dereference: read 2-byte LE address from $FB/$FC, then read byte at that address |
| `word:$2B` | 16-bit LE word at address |
| `reg:A` | Current value of register A |
| `reg:PC` | Current PC |
---
## Implementation plan
### 1. Watch store
Add a persistent list of watch definitions to `commands.State` (`commands.py:54`):
```python
class State:
def reset_client(self):
...
self.watches: list[dict] = [] # [{name, expr}, ...]
```
### 2. Expression evaluator
New function in `commands.py`:
```python
def _eval_watch(vice, regs: dict, expr: str) -> str:
expr = expr.strip()
if expr.startswith('reg:'):
name = expr[4:].upper()
val = regs.get(arch.VICE_TO_GHIDRA_REG.get(name, name), regs.get(name))
return f'0x{val:04X}' if val is not None else '?'
word_mode = expr.startswith('word:')
deref = expr.startswith('*')
addr_str = expr.lstrip('word:').lstrip('*').strip()
addr = int(addr_str.lstrip('$'), 16)
if deref:
ptr_bytes = vice.memory_get(addr, addr + 1)
addr = ptr_bytes[0] | (ptr_bytes[1] << 8)
data = vice.memory_get(addr, addr + (1 if not word_mode else 1))
if word_mode:
return f'0x{data[0] | (data[1] << 8):04X}'
return f'0x{data[0]:02X}'
```
### 3. Populate on stop
In `commands.on_stop()` (`commands.py:387`), after `put_registers()`:
```python
if STATE.watches:
regs = vice.registers_get()
for w in STATE.watches:
try:
val = _eval_watch(vice, regs, w['expr'])
except Exception as e:
val = f'ERR: {e}'
path = f'C64.Watches[{w["name"]}]'
obj = t.create_object(path)
obj.set_value('_display', f'{w["name"]} = {val} ({w["expr"]})')
obj.set_value('value', val)
obj.insert()
```
### 4. `methods.py` — manage watches
```python
@REGISTRY.method(display='Add Watch')
def add_watch(process: C64, name: str, expr: str):
"""Add a watch expression. expr examples: '$D012', 'word:$2B', '*$FB', 'reg:A'"""
commands.STATE.watches.append({'name': name, 'expr': expr})
log.info(f'Watch added: {name} = {expr}')
@REGISTRY.method(display='Remove Watch')
def remove_watch(process: C64, name: str):
commands.STATE.watches = [w for w in commands.STATE.watches if w['name'] != name]
log.info(f'Watch removed: {name}')
@REGISTRY.method(display='List Watches')
def list_watches(process: C64):
for w in commands.STATE.watches:
log.info(f' {w["name"]:20s} {w["expr"]}')
```
### 5. `schema.xml` — watch container
Add under `C64`:
```xml
```
```xml
```
---
## Files to change
- `src/main/py/src/vice/commands.py` — add `watches` to `State`, `_eval_watch()`, populate watches in `on_stop()` and `populate_initial_state()`
- `src/main/py/src/vice/methods.py` — add `add_watch`, `remove_watch`, `list_watches`
- `src/main/py/src/vice/schema.xml` — add `Watches`, `WatchContainer`, `WatchEntry`
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.