64kramsystem / 64kramsystem/ghidra-vice-connector

Watch expressions: evaluate and display named memory/register values on each stop

Open
#33 0 comments 0 reactions 0 assignees View on GitHub
priority: low
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.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.