Update avatar_base implementation, assets and documentation
This commit is contained in:
commit
1ac123dd1b
24 files changed
+862
No files matched your search
+137
@@ -0,0 +1,137 @@
|
||||
# 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.
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
# Система аватаров: основа
|
||||
|
||||
`avatar_base` реализует независимое состояние аватара: группы, именованные пресеты,
|
||||
композицию целевых значений и JSON-состояние для сохранений. Зависимости от диалогов,
|
||||
боевых персонажей и Live2D нет. Полный формат и контракт: [en.md](en.md).
|
||||
|
||||
## Управление
|
||||
|
||||
```csharp
|
||||
IAvatar avatar = AvatarCatalog.Create("my_game/marao");
|
||||
avatar.ApplyPresets(new Dictionary<string, string> {
|
||||
["emotion"] = "angry",
|
||||
["eyes_state"] = "opened"
|
||||
});
|
||||
AvatarFrame target = avatar.Resolve();
|
||||
AvatarState saved = avatar.CaptureState();
|
||||
```
|
||||
|
||||
Контентный мод зависит от `avatar_base>=1` и объявляет сущности `Kind: avatar`.
|
||||
У определения есть `Id`, `Renderer`, `BaseValues` и `Groups`. У группы — `Default`,
|
||||
`Priority`, `Presets`; у пресета — `Values`. Имена групп и пресетов произвольные,
|
||||
регистрозависимые. Значения пресетов — целевые привязки для конкретного рендера,
|
||||
а не универсальные числа от 0 до 1.
|
||||
|
||||
Каждая группа выбирает один пресет. Изменение группы сохраняет остальные группы.
|
||||
Все изменения проверяются до применения: опечатка не оставляет частично изменённое
|
||||
состояние. Неизвестные значения отклоняются, автоматических подмен нет.
|
||||
|
||||
Композиция начинается с `BaseValues`, затем применяет группы по возрастанию
|
||||
`Priority`, при равенстве — по имени (ordinal). Более поздняя группа перекрывает
|
||||
одноимённую привязку целиком. Порядок команд ничего не меняет. Например, эмоция
|
||||
имеет приоритет 0, явное состояние глаз — 100. Пустой пресет `auto` не вносит
|
||||
привязок и тем самым возвращает управление эмоции. Это обычное имя, не ключевое слово.
|
||||
|
||||
`Resolve()` возвращает полный неизменяемый снимок, без оставшихся значений прошлого
|
||||
пресета. `CaptureState()` — отдельный DTO с ID аватара и выбором пресетов. При
|
||||
восстановлении отсутствующие группы получают значения по умолчанию, неизвестные
|
||||
группы/пресеты и чужой ID отклоняются. Изменения ресурсов не меняют уже созданный
|
||||
экземпляр: владелец пересоздаёт его при перезагрузке.
|
||||
|
||||
## Граница реализации
|
||||
|
||||
`IAvatarRenderer` задаёт контракт визуального адаптера: корневой `Control`, применение
|
||||
полного целевого состояния, время перехода и освобождение принадлежащих ему ресурсов.
|
||||
Мод `avatar_layers` уже выбирает и отображает PNG-слои. Будущий адаптер Live2D будет
|
||||
выбирать параметры модели. `AvatarRenderers.Create(avatar)` находит фабрику по `Renderer`
|
||||
и применяет начальное состояние. После изменений владелец явно вызывает
|
||||
`renderer.Apply(avatar.Resolve())`. Фабрики автоматически регистрируются и удаляются
|
||||
вместе с модами через стандартный `ModTypeConsumer`.
|
||||
|
||||
В этой версии нет адаптера Live2D, сценарных команд, редактора,
|
||||
проверки совместимости картинок и автоматической интеграции с сохранениями.
|
||||
Существующие диалоговые аватарки работают как прежде. JSON-пример и правила владения
|
||||
ресурсами приведены в английской документации. Тесты проверяют независимость
|
||||
экземпляров, приоритеты, атомарность, сериализацию и загрузку настоящего ресурса.
|
||||
Reference in new issue
Block a user