# From SND scripts to maintainable Dalamud plugins

Updated: 2026-08-11

Audience: plugin authors, maintainers, and software agents translating a Something Need Doing (SND) Lua workflow into a standalone Dalamud plugin.

This is an architecture guide, not a promise that any specific automation remains safe after a game or Dalamud update. Confirm current SND and Dalamud APIs from their upstream documentation before implementation.

## Scope

Something Need Doing is a Dalamud plugin that expands the native macro system and includes a Lua engine. It is useful for quickly expressing sequential workflows. A standalone Dalamud plugin has a different lifecycle: it owns long-lived services, frame-driven state, cancellation, configuration, UI, packaging, and cleanup.

Use SND as an executable behavior specification. Do not mechanically translate every `yield` into a blocking sleep.

## Choose the smallest suitable form

| Need | Prefer |
| --- | --- |
| Personal or experimental sequential workflow | SND Lua script |
| A few configurable steps with an intentional SND dependency | SND script with documented modules or IPC |
| Reusable UI, persistent state, release packaging, or many users | Standalone Dalamud plugin |
| Long-running workflow that must cancel, recover, and report failures | Standalone plugin with a task runner |

## Target architecture

Separate the port into four layers:

1. **Shared command layer** - chat commands, key input, addon interaction, IPC, navigation, and other reusable operations.
2. **Task runner** - ordered, nonblocking steps with enter actions, completion predicates, timeouts, retries, cancellation, and diagnostics.
3. **Workflow layer** - task-specific decisions and composition. It calls shared operations instead of duplicating them.
4. **Presentation layer** - configuration, status, controls, and error messages. It observes the workflow and does not contain the automation itself.

This separation lets a maintainer test a command once, reuse it in several workflows, and reason about failures without reading one large script.

## Translate sequential waits into state

SND-style pseudocode:

```lua
yield("/command destination")
yield("/wait 1")

while IsBusy() do
    yield("/wait 0.1")
end
```

Plugin-style pseudocode:

```csharp
runner.Enqueue(new TaskStep(
    name: "Issue command",
    onEnter: () => commands.Execute("destination"),
    isComplete: () => true,
    timeout: TimeSpan.FromSeconds(2)));

runner.Enqueue(new TaskStep(
    name: "Observe transition start",
    isComplete: () => gameState.IsBusy,
    timeout: TimeSpan.FromSeconds(10)));

runner.Enqueue(new TaskStep(
    name: "Wait for transition end",
    isComplete: () => !gameState.IsBusy,
    timeout: TimeSpan.FromSeconds(45),
    maxRetries: 1));
```

The runner evaluates `isComplete` from the framework/update loop. It must not block the game thread. Treat the code above as a shape, not a drop-in API.

## Readiness is a gate, not a delay

A fixed delay is only a settling aid. It is not proof that the game is ready.

After a relog, teleport, or zone change, require several consecutive readiness observations before continuing. A useful default is three passes:

```text
command issued
  -> short initialization window
  -> transition/busy state observed
  -> transition complete
  -> readiness true on 3 consecutive checks
  -> small settle delay
  -> next action
```

Reset the consecutive-pass counter whenever readiness becomes false. The readiness predicate should be narrow and workflow-specific, for example: local player available, expected territory loaded, not between areas, required addon present, and no occupied condition that invalidates the next action.

## Step contract

Each task step should define:

- a stable name for logs and UI;
- an optional `OnEnter` action that runs once per attempt;
- an `IsComplete` predicate that is safe to poll;
- a finite timeout;
- a retry count only when repeating the action is safe;
- cancellation behavior;
- a failure message with the observed state.

Polling code should handle transient unavailable state and return `false`. It should not turn a normal zone transition into an unhandled exception. Permanent configuration or invariant errors should fail clearly instead of retrying forever.

## Porting workflow

1. Inventory every command, condition, wait, loop, external plugin call, mutable variable, and exit path in the SND script.
2. Mark which operations already belong in a shared service. Do not copy command helpers into the new workflow.
3. Draw the state transitions, including busy-start and busy-end observations.
4. Convert each blocking wait or loop into a named, pollable step.
5. Add finite timeouts and cancellation before adding retries.
6. Add consecutive readiness checks after relog, teleport, zoning, and addon reconstruction.
7. Persist only configuration and resumable state. Do not silently resume an unsafe half-completed action after reload.
8. Expose current step, elapsed time, retry count, and last failure in the UI or logs.
9. Test shared operations individually, then small sequences, then the full workflow.
10. Build and package with the current Dalamud SDK contract; inspect the output archive and manifest before release.

## Validation checklist

- The framework/update callback stays responsive; no `Thread.Sleep`, busy loop, or synchronous long wait is used.
- Stop and dispose paths cancel the active sequence and unregister events exactly once.
- Every step either completes, times out, is cancelled, or reports a specific failure.
- Retries cannot repeat destructive or non-idempotent work without an explicit guard.
- Relog and zone transitions require observed readiness, not time alone.
- Missing player, territory, addon, IPC provider, or external plugin state is handled.
- Debug and Release builds pass with the current project toolchain.
- The packaged archive contains the expected DLL, manifest, and approved assets only.
- Runtime validation is recorded separately from build success.

## Current upstream starting points

- SND repository: <https://github.com/Jaksuhn/SomethingNeedDoing>
- Official Dalamud plugin-development guide: <https://dalamud.dev/plugin-development/getting-started/>
- Official SamplePlugin template: <https://github.com/goatcorp/SamplePlugin>
- Aethertek FFXIV builder skill: </skills/library/ffxiv-dalamud-plugin-builder/SKILL.md>
- Aethertek regression skill: </skills/library/runtests/SKILL.md>

## Machine-use notes

- This page is a public-safe synthesis. It intentionally excludes private helper libraries, local paths, exact automation targets, and internal application code.
- Pseudocode is illustrative and is not a promise of exact API names.
- Prefer the current upstream repository and official Dalamud docs over cached examples.
- When reporting completion, distinguish source review, compile/package success, and live in-game validation.
