138 lines
7.1 KiB
Markdown
138 lines
7.1 KiB
Markdown
# Avatar Engine foundation
|
|
|
|
`avatar_base` owns renderer-independent avatar definitions and preset state. It has no
|
|
runtime dependency on dialogs, party characters, UI engine mods or Live2D. A visual novel,
|
|
a character preview or another UI can use the same logical instance.
|
|
|
|
This first version implements resource loading, validated preset selections, deterministic
|
|
composition and serializable state. `IAvatarRenderer` defines the presentation boundary;
|
|
The `avatar_layers` mod implements the `layers` renderer and is an optional dependency of content
|
|
that uses it. Live2D adaptation, scenario actions, a visual editor and automatic save-system
|
|
integration are not implemented yet. Existing `first=` / `second=` dialog portraits are unaffected.
|
|
|
|
## Resource format
|
|
|
|
Content mods declare `depends: ["avatar_base>=1"]` (or `avatar_layers>=1` for layered images) and supply normal entity files, for example
|
|
`avatars/marao.json`. `Kind: avatar` is registered by `AvatarDefinition`. Definitions use the
|
|
standard entity resolver: folder/ZIP providers, overrides, `Extends` and patches work through
|
|
that pipeline; the catalog does not parse files or maintain a second resource cache.
|
|
|
|
```json
|
|
{
|
|
"Kind": "avatar",
|
|
"Items": [{
|
|
"Id": "my_game/marao",
|
|
"Renderer": "layers",
|
|
"BaseValues": {
|
|
"layers/body": "marao/body.png",
|
|
"layers/eyes": "marao/eyes/opened.png"
|
|
},
|
|
"Groups": {
|
|
"emotion": {
|
|
"Default": "neutral",
|
|
"Priority": 0,
|
|
"Presets": {
|
|
"neutral": {},
|
|
"angry": {"Values": {"layers/eyes": "marao/eyes/narrowed.png"}}
|
|
}
|
|
},
|
|
"eyes_state": {
|
|
"Default": "auto",
|
|
"Priority": 100,
|
|
"Presets": {
|
|
"auto": {},
|
|
"opened": {"Values": {"layers/eyes": "marao/eyes/opened.png"}},
|
|
"closed": {"Values": {"layers/eyes": "marao/eyes/closed.png"}}
|
|
}
|
|
}
|
|
}
|
|
}]
|
|
}
|
|
```
|
|
|
|
Group and preset names are author-defined, case-sensitive and not a fixed engine vocabulary.
|
|
Each group has one selected preset; `Default` must name an existing preset. Definitions with
|
|
no groups are valid static avatars. `auto` is an ordinary empty preset here, not a reserved word:
|
|
it contributes nothing, leaving the baseline and earlier groups in control.
|
|
|
|
`Values` is a dictionary of opaque JSON targets. The layers adapter reads PNG paths;
|
|
a Live2D adapter can read numeric values such as `parameters/ParamEyeLOpen: 1`. The base does
|
|
not invent a universal numeric range, interpret binding names or load assets. Binding value
|
|
types, required bindings, compatibility of poses/layers and asset existence are the adapter's
|
|
responsibility. Values can also be objects or arrays for an adapter's structured configuration.
|
|
|
|
## Applying presets
|
|
|
|
```csharp
|
|
IAvatar avatar = AvatarCatalog.Create("my_game/marao");
|
|
avatar.ApplyPresets(new Dictionary<string, string> {
|
|
["emotion"] = "angry",
|
|
["eyes_state"] = "opened"
|
|
});
|
|
AvatarFrame target = avatar.Resolve();
|
|
using IAvatarRenderer renderer = AvatarRenderers.Create(avatar);
|
|
// Attach renderer.View to the host. After later changes:
|
|
renderer.Apply(target, transitionSeconds: 0.15);
|
|
AvatarState saved = avatar.CaptureState();
|
|
IAvatar restored = AvatarCatalog.Create(saved.AvatarId, saved);
|
|
```
|
|
|
|
`ApplyPresets` is atomic: every supplied group/preset is checked before changing any selection.
|
|
Unmentioned groups retain their choices. Null, misspellings and unknown presets throw
|
|
`ArgumentException`; there is no silent fallback. `AvatarCatalog.Create` throws
|
|
`KeyNotFoundException` for an unknown avatar. Select the group's default explicitly to reset it.
|
|
|
|
Every `Resolve` starts from `BaseValues` and applies all selected presets by ascending group
|
|
`Priority`, then ordinal group name. Later contributions replace earlier values for the same
|
|
binding. The order of scenario commands and JSON properties never determines precedence.
|
|
An individual JSON binding is replaced whole, not deep merged; null is a literal target value.
|
|
Thus explicit `eyes_state` at priority 100 overrides the eye contribution of `emotion` at 0.
|
|
Selecting `auto` delegates control back to the emotion/baseline on the next composition.
|
|
|
|
The output is a complete immutable snapshot, not a delta. Leaving a preset removes its
|
|
contributions; adapters must reset bindings absent from a later target rather than retaining
|
|
stale values. Define adapter-required reset targets in `BaseValues`. There is no automatic
|
|
cross-group compatibility solver in this foundation.
|
|
|
|
## Ownership and persistence
|
|
|
|
An `AvatarInstance` validates and snapshots its definition. It never changes cached entity DTOs.
|
|
Two instances of the same definition have independent selections. Frames and captured states
|
|
are detached; editing the returned save DTO cannot alter a live instance. Active instances keep
|
|
their definition snapshot after resource changes; the owning host recreates them on reload.
|
|
|
|
`AvatarState` stores the avatar ID and selected preset names. JSON round-trip it in the owning
|
|
subsystem's save data. Missing groups on restore use defaults; a mismatched avatar ID or unknown
|
|
selection is rejected. Content migrations must explicitly handle removed/renamed presets.
|
|
There is no global avatar roster or automatic registration with the game's save system.
|
|
|
|
`IAvatarRenderer` exposes a renderer-owned `Control` and accepts complete targets with an optional
|
|
transition duration. Rendering mods implement it and decide how presets become images or model
|
|
parameters. They must validate compatible targets and finite nonnegative durations. Zero snaps;
|
|
a discrete adapter may also snap for positive duration. A new animated target interrupts the
|
|
previous transition from the currently displayed state. The host attaches `View` to its UI and
|
|
disposes the adapter before freeing that container. The adapter frees owned nodes/animations,
|
|
never shared cache textures. All visual operations run on the Godot main thread.
|
|
|
|
The base owns no Godot objects or update loop. Pure logical instances do not need disposal.
|
|
`AvatarRenderers` discovers public, parameterless `IAvatarRendererFactory` implementations via
|
|
`ModTypeConsumer`, including normal unload notifications. Factories declare a unique case-sensitive
|
|
renderer ID and create an owned adapter. Duplicate IDs are rejected. `AvatarRenderers.Create(avatar)`
|
|
resolves the backend and applies the initial state, disposing the adapter if initialization fails.
|
|
The host explicitly applies subsequent `avatar.Resolve()` targets; there is no hidden subscription.
|
|
The host must dispose active adapters before their mod/scene unloads. Missing backends throw
|
|
`KeyNotFoundException`, making a missing content dependency visible.
|
|
|
|
## Verification
|
|
|
|
Tests use the engine's runtime mod compiler, not a separate project referencing mod sources:
|
|
|
|
```text
|
|
godot --headless --path . -- --frontend --with-mods=avatar_base --run-tests --mod=avatar_base
|
|
godot --headless --path . -- --frontend --with-mods=avatar_base --itest=avatar_contracts --run-tests --mod=avatar_base__contracts
|
|
```
|
|
|
|
In this workspace run these through the shared Windows job bridge. The opt-in `contracts` test
|
|
suite supplies a sample `Kind: avatar` resource and checks the real catalog and independent
|
|
instances. Production mods contain no sample character or fake renderer implementation.
|