174 lines
8.5 KiB
Markdown
174 lines
8.5 KiB
Markdown
# Action System
|
|
|
|
This is the most complex subsystem and the one we've already broken once. Read carefully before editing anything that touches actions.
|
|
|
|
## The three registration paths
|
|
|
|
`[verified]` from `Source/Scripts/SkyrimNetApi.psc:27-31` (Papyrus path), `SkyrimNet.log:14123-14266` (CustomActionManager loader), `CppAPI/PublicAPI.h:93-97` (C++ path), and on-disk YAML inspection.
|
|
|
|
### 1. Papyrus registration
|
|
|
|
A mod's Papyrus script calls:
|
|
|
|
```papyrus
|
|
SkyrimNetApi.RegisterAction(
|
|
string actionName, ; UPPERCASED in the registry — "OpenTrade" → "OPENTRADE"
|
|
string actionDescription,
|
|
string eligibilityScriptName,
|
|
string eligibilityFunctionName,
|
|
string executionScriptName,
|
|
string executionFunctionName,
|
|
string triggeringEventTypesCsv,
|
|
string actionType, ; "PAPYRUS" or "PAPYRUS_CUSTOM"
|
|
int priority,
|
|
string parameterSchemaJson,
|
|
string customCategory,
|
|
string tagsCsv
|
|
)
|
|
```
|
|
|
|
The C++ DLL keeps a registry of these. When the LLM emits `ACTION: OPENTRADE PARAMS: {...}`, the DLL:
|
|
1. Looks up `OPENTRADE` in the registry.
|
|
2. Calls the eligibility function via Papyrus (`Action_IsEligible` style), waits up to `dialogue.eligibilityCheckTimeoutMs` (= 2500ms).
|
|
3. If eligible, calls the execution function with the parameters.
|
|
|
|
**Shipped Papyrus actions** (from `Source/Scripts/skynet_Library.psc:130-315`):
|
|
`OpenTrade`, `AccompanyTarget`, `StopAccompanying`, `WaitHere`, `Gesture` (with anim enum), `RentRoom`, `CompanionFollow`, `CompanionWait`, `CompanionInventory`, `CompanionGiveTask`.
|
|
|
|
`[verified]` `SkyrimNet.log:16393` shows: `ActionLibrary::RegisterPapyrusAction for action: OPENTRADE`.
|
|
|
|
### 2. YAML registration
|
|
|
|
A mod can drop YAML files into `SKSE/Plugins/SkyrimNet/config/actions/*.yaml` *in its own mod folder*. SkyrimNet's `CustomActionManager::Initialize` scans across all installed mods at load.
|
|
|
|
**YAML actions preserve case** in the registry (unlike Papyrus actions). So `OpenTrade` stays `OpenTrade`.
|
|
|
|
**Schema** — sample from `SeverActions - SkyrimNet Action Pack/.../config/actions/attacktarget.yaml:1-37`:
|
|
```yaml
|
|
customCategory: Combat
|
|
name: AttackTarget
|
|
description: ...
|
|
questEditorId: ... # the Skyrim Quest holding the function
|
|
scriptName: ... # Papyrus script attached to the quest
|
|
executionFunctionName: ...
|
|
parameterMapping:
|
|
- parameterName: target
|
|
decorator: get_nearby_actor # or other decorator → Skyrim object
|
|
enabled: true
|
|
defaultPriority: 50
|
|
eligibilityRules:
|
|
- decorator: is_in_combat
|
|
operator: equals
|
|
value: true
|
|
# AND/OR combinations supported
|
|
```
|
|
|
|
**Contributing mods at last load** `[verified]` from `SkyrimNet.log`:
|
|
- **`SeverActions - SkyrimNet Action Pack`** — ~80 generic actions (Combat/Travel/Economy/etc.). The big one.
|
|
- **`IntelEngine`** — 15 actions (likely intelligence/observation-themed).
|
|
- **`OStimNet_v0.9.2`** — ~10 actions, prefixed `tton_*` (sex/affection — adult mod).
|
|
|
|
### 3. C++ registration
|
|
|
|
External SKSE plugins can call `PublicRegisterCPPAction()` from `CppAPI/PublicAPI.h:93-97` to register C++-implemented actions directly. Avoids the Papyrus VM round-trip; useful for performance-critical or low-level actions.
|
|
|
|
`[unknown]` whether any currently-loaded mod uses this path in this install.
|
|
|
|
---
|
|
|
|
## Action categories (the wrapper layer)
|
|
|
|
Actions can be grouped under categories defined in `cat_*.yaml` files. Categories let the LLM "intent-pick" without seeing all leaf actions.
|
|
|
|
**Known categories** `[verified]` from logs:
|
|
`Combat`, `Communication`, `Travel`, `Economy`, `Items`, `Magic`, `Outfit`, `Crafting`, `Scheduling`, `Command` (companion), `Arrest`.
|
|
|
|
**Two-stage flow:**
|
|
1. GM (or native action selector) emits `ACTION: Communication PARAMS: {"intent": "express gratitude..."}` — picks the *category* and states the intent in natural language.
|
|
2. C++ orchestrator fires a second LLM call using `prompts/native_action_selector_drilldown.prompt`, which sees only the leaf actions under that category plus the intent. Picks the specific leaf.
|
|
|
|
This pattern reduces cognitive load: instead of choosing among ~105 actions, the model picks one of ~10 categories then one of ~5-15 leaves.
|
|
|
|
---
|
|
|
|
## The `ACTION:` parser grammar
|
|
|
|
`[verified]` from log behavior at `ActionManager.cpp:1783` and `:1814`.
|
|
|
|
**Required form (start of a line):**
|
|
```
|
|
ACTION: ActionName
|
|
ACTION: ActionName PARAMS: {"param": "value", ...}
|
|
ACTION: None
|
|
```
|
|
|
|
**Strict requirements:**
|
|
- The literal **`ACTION: `** prefix (with the colon and space) at the start of a line.
|
|
- Action name must match a registered action **exactly** (case-sensitive, except Papyrus actions are uppercase in the registry).
|
|
- `PARAMS:` if present must be valid JSON.
|
|
- The action line must be on its own line, after any dialogue text (with `embed_actions_in_dialogue: true`).
|
|
|
|
**No leniency:**
|
|
- Bare action names (e.g. `OPENTRADE` alone) are NOT recognized. They fall through to dialogue text.
|
|
- No fuzzy matching. No alternate prefixes (`Action:`, `ACT:`, `[ACTION]`).
|
|
- Logged when not found: `[ActionManager.cpp:1783] ParseEmbeddedAction: No ACTION: line found in response`.
|
|
- Logged when found: `[ActionManager.cpp:1814] ParseEmbeddedAction: Successfully parsed action 'X' with params: {...}`.
|
|
|
|
**Strip behavior:** `FilterActionLines` (`ActionManager.cpp:1840`) removes the recognized `ACTION:` line from dialogue text *before* TTS, so it isn't spoken aloud. **It only strips lines it parsed successfully** — malformed lines (the bug #2 case) stay in the text and get spoken.
|
|
|
|
---
|
|
|
|
## The two action selectors — how they relate
|
|
|
|
| Aspect | `gamemaster_action_selector` | `native_action_selector` |
|
|
|---|---|---|
|
|
| **Scope** | scene-level orchestration | per-NPC, post-dialogue attribution |
|
|
| **When** | GM polling tick, player input | after Dialogue agent produces text |
|
|
| **Picks from** | StartConversation / ContinueConversation / Narrate / None / native actions | the eligible-actions registry, two-stage |
|
|
| **Variant** | `gamemaster_evaluation` | `action_evaluation` |
|
|
| **Model** | claude-sonnet (this install) | eva (this install) |
|
|
| **`max_tokens`** | 256 | 500 |
|
|
| **Output** | one ACTION line | one ACTION line (after drilldown: two LLM calls total) |
|
|
|
|
They are **complementary, not competitive**:
|
|
- The GM directs *who speaks and what about*.
|
|
- The native action selector classifies *what physically/mechanically happens* once an NPC has spoken.
|
|
|
|
**With `embed_actions_in_dialogue: true`:** the Dialogue agent itself can emit an `ACTION:` line inline, which **bypasses** the native action selector entirely. Faster, but more prone to format errors → bug #2 territory.
|
|
|
|
---
|
|
|
|
## Where custom actions actually live (resolved)
|
|
|
|
We initially looked for them in `mods/SkyrimNet/` itself and didn't find them. They live in **other mods' folders**:
|
|
|
|
```
|
|
/home/dafit/Games/Skyrim/nimmersky/mods/SeverActions - SkyrimNet Action Pack/SKSE/Plugins/SkyrimNet/config/actions/*.yaml
|
|
/home/dafit/Games/Skyrim/nimmersky/mods/IntelEngine/SKSE/Plugins/SkyrimNet/config/actions/*.yaml
|
|
/home/dafit/Games/Skyrim/nimmersky/mods/OStimNet_v0.9.2/SKSE/Plugins/SkyrimNet/config/actions/*.yaml
|
|
```
|
|
|
|
`[hypothesis]` — paths inferred from the contributing-mod names in the log; the agent didn't enumerate the actual files. Confirm by `find /home/dafit/Games/Skyrim/nimmersky/mods/ -name "*.yaml" -path "*/config/actions/*"`.
|
|
|
|
This is also why "export from web UI" doesn't work cleanly — the UI sees actions in its registry but doesn't know they came from neighbor mods' folders, and can't reach back across mod boundaries to dump them.
|
|
|
|
**Path forward for git-tracking:** symlink each contributing mod's `config/actions/` into a `nimmersky/skyrimnet/contributed-actions/{modname}/` archive, or write a one-shot collator script that copies them out.
|
|
|
|
---
|
|
|
|
## The malformed-marker problem (bug #2)
|
|
|
|
The Dialogue model occasionally emits `OPENTRADE` (bare uppercase token) instead of `ACTION: OpenTrade`. The parser ignores the bare token, doesn't strip it, and it gets TTS'd as plain text — Arcadia speaks "OPENTRADE" out loud.
|
|
|
|
**Why it happens:** `submodules/user_final_instructions/0750_embedded_actions.prompt:6-9` lists actions to the LLM as bullets:
|
|
```
|
|
**Available Actions:**
|
|
- `OPENTRADE` — Use ONLY if ...
|
|
- `OFFERQUEST` — ...
|
|
```
|
|
The model sometimes emits the bare bullet name instead of the full `ACTION: OPENTRADE` line. The bullet list format encourages the mistake.
|
|
|
|
**Why it became visible only after fixing bug #1:** the over-firing GM loop gave the Dialogue model 3-5 retries per scene; bug #2 was statistically masked. Fixing the loop removed the retries, exposing bug #2.
|
|
|
|
See [`bugs-and-fixes.md`](bugs-and-fixes.md) for fix candidates.
|