Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
02457bd
chore: the last three files with CRLF line endings catch up
YozoraKurage Aug 11, 2026
9b742b4
feat: the transition list says which one the Animator tries first
YozoraKurage Aug 11, 2026
22af306
feat: solo stops being invisible
YozoraKurage Aug 11, 2026
d354462
fix: a condition stops losing the parameter it names
YozoraKurage Aug 11, 2026
56681f0
feat: the keys every other graph view uses work here too
YozoraKurage Aug 11, 2026
f95511f
feat: the analyzer catches two more transitions that cannot do what t…
YozoraKurage Aug 11, 2026
8d23645
fix: the hover highlight is answered once a frame, not on every event
YozoraKurage Aug 11, 2026
8978e90
fix: Ctrl+A selects the same set twice running, and hover outranks se…
YozoraKurage Aug 11, 2026
3aa64d1
feat: Select Transitions works on the whole selection
YozoraKurage Aug 11, 2026
2fb8064
feat: the mouse wheel flips a condition and moves its threshold
YozoraKurage Aug 11, 2026
fbfac1d
fix: the comparisons keep Unity's names, and the wheel steps by one
YozoraKurage Aug 11, 2026
2437d43
feat: arrow keys walk the graph, M and T wire it, and edges stop expl…
YozoraKurage Aug 11, 2026
cf6523a
refactor: one table says what every key does
YozoraKurage Aug 11, 2026
10b4731
feat: the keys are Unity's to rebind
YozoraKurage Aug 11, 2026
60a8660
feat: the shortcuts are editable on DaerD's own settings page
YozoraKurage Aug 11, 2026
afc1375
feat: the transition list answers one question at a time
YozoraKurage Aug 11, 2026
13a8cb1
fix: a sync request gives the cycle its place back
YozoraKurage Aug 11, 2026
2f7cca2
feat: a remote can tell when it has heard everything
YozoraKurage Aug 12, 2026
774576b
feat: a remote can tell when a lap came up short
YozoraKurage Aug 12, 2026
0212537
refactor: ×N is a share of the pass, not a speed
YozoraKurage Aug 12, 2026
0c9d82b
feat: values that change together arrive together
YozoraKurage Aug 12, 2026
89f381d
feat: DD DynamicAnalyze runs the controller instead of reading it
YozoraKurage Aug 12, 2026
cc56b38
feat: DD DynamicAnalyze runs the wearer's copy and somebody else's
YozoraKurage Aug 12, 2026
616c138
feat: the run is something to look at
YozoraKurage Aug 12, 2026
2dbf266
feat: the run can be pushed on while it runs, and it stopped being slow
YozoraKurage Aug 12, 2026
14971eb
refactor: the value you poke is the value you are watching
YozoraKurage Aug 12, 2026
0e91fc3
fix: two watchers that judged once and then went deaf
YozoraKurage Aug 12, 2026
f072608
feat: a run saves as a clip, and a clip drives a run
YozoraKurage Aug 12, 2026
d07b5d3
feat: what DaerD believes about the SDK is now checked against it
YozoraKurage Aug 12, 2026
0b16efe
feat: a run says what it does not promise
YozoraKurage Aug 12, 2026
275dbbe
feat: the other person can turn up late
YozoraKurage Aug 12, 2026
5081c70
feat: one ring per type, because a step can only carry one
YozoraKurage Aug 13, 2026
697cf08
feat: a type with one target is told it is not multiplexing anything
YozoraKurage Aug 13, 2026
9e1433e
feat: a weight that cannot be spaced out says so
YozoraKurage Aug 13, 2026
4ac127f
feat: any pass can carry the drift flag, because a lap is a step
YozoraKurage Aug 13, 2026
527893a
refactor: the grid is something to look at, not something to write
YozoraKurage Aug 13, 2026
4fecc5d
refactor: the wizard stops handing out weights
YozoraKurage Aug 13, 2026
76375d8
chore: the commit rules say what the good messages already did
YozoraKurage Aug 13, 2026
765d8f0
fix: the waveform's ranges follow a run that is still growing
YozoraKurage Aug 13, 2026
ad4c47f
fix: the key hints read the shortcut table, and Alt is a modifier
YozoraKurage Aug 13, 2026
ca3bd19
fix: unticking a row no longer costs it its weight
YozoraKurage Aug 13, 2026
436a1f8
fix: the sync warnings stop advising a control that is gone
YozoraKurage Aug 13, 2026
c4f86a7
chore: every generated layer is walked for transitions that can never…
YozoraKurage Aug 13, 2026
900eb9a
feat: a group under Ready commits nothing nobody sent
YozoraKurage Aug 13, 2026
5d6ec8d
chore: the two harnesses are held to the same answers
YozoraKurage Aug 13, 2026
beb6ba5
feat: DD DynamicAnalyze gets rows you can reach, a second cursor, a g…
YozoraKurage Aug 13, 2026
7cd37b0
fix: the parameters VRChat syncs for you reach the other person
YozoraKurage Aug 13, 2026
a1a2b2b
chore: 設計の「なぜ」を会話ログまで遡れるようにする(distillery 導入)
YozoraKurage Aug 14, 2026
8723179
perf: the sync editor works its answers out once instead of once per …
YozoraKurage Aug 15, 2026
ba43df4
fix: the cell you click in DD DynamicAnalyze is the cell that takes t…
YozoraKurage Aug 15, 2026
0f94916
feat: DD DynamicAnalyze's waveform is got around the way the Animatio…
YozoraKurage Aug 15, 2026
30603f6
feat: DD DynamicAnalyze's trace says which transition is firing
YozoraKurage Aug 15, 2026
982a1d8
feat: DD DynamicAnalyze reads its own trace and says what it found
YozoraKurage Aug 15, 2026
94ed03a
feat: the wire takes time, and its dice are its own
YozoraKurage Aug 15, 2026
5d8722f
feat: DD DynamicAnalyze records a layer's weight and lets you turn it
YozoraKurage Aug 15, 2026
c7b2f37
feat: DD DynamicAnalyze says what is wrong with a run before you wait…
YozoraKurage Aug 15, 2026
aef473f
feat: DD DynamicAnalyze's viewer walks one row's edges and says how f…
YozoraKurage Aug 15, 2026
57c66ae
feat: a saved DD DynamicAnalyze run carries the experiment that made it
YozoraKurage Aug 15, 2026
fbb6ffe
feat: the hand you used live in DD DynamicAnalyze becomes the experim…
YozoraKurage Aug 15, 2026
d52c35a
chore: DD DynamicAnalyze's independence is watched rather than rememb…
YozoraKurage Aug 15, 2026
38d1b09
fix: the built-ins reach the other person on the channels VRChat actu…
YozoraKurage Aug 15, 2026
617b7d8
chore: DD DynamicAnalyze's three divergences are measured instead of …
YozoraKurage Aug 15, 2026
4d7a618
fix: async sync's drift verdict lands a frame after the lap, not a se…
YozoraKurage Aug 15, 2026
0575114
fix: DD DynamicAnalyze's drivers fire again when a state is entered f…
YozoraKurage Aug 15, 2026
9065a70
feat: async sync's groups are sent as one reading instead of assemble…
YozoraKurage Aug 15, 2026
64f6247
fix: async sync's group commit hands the set over on the frame it com…
YozoraKurage Aug 15, 2026
ca0bcd7
fix: DD DynamicAnalyze stops warning about the two things it gets right
YozoraKurage Aug 15, 2026
a207970
feat: a run can now be taken off the avatar you are actually wearing
YozoraKurage Aug 15, 2026
bd0744b
feat: the window has a third mood, and it records instead of simulating
YozoraKurage Aug 15, 2026
14df860
feat: DD DynamicAnalyze's Rec knows which of the avatars in the scene…
YozoraKurage Aug 15, 2026
a1098f9
feat: DD DynamicAnalyze records the copies other people see, beside t…
YozoraKurage Aug 15, 2026
da72f7e
feat: DD DynamicAnalyze stands in the avatar's build and writes down …
YozoraKurage Aug 15, 2026
a98deb1
feat: DD DynamicAnalyze's Rec reads the avatar you built, not the con…
YozoraKurage Aug 15, 2026
3e3be55
feat: DD DynamicAnalyze takes a recording apart into the inputs you c…
YozoraKurage Aug 15, 2026
95aae72
feat: DD DynamicAnalyze's tracks can be switched off, renamed and tak…
YozoraKurage Aug 15, 2026
1773dd8
feat: DD DynamicAnalyze presses the edited inputs into the avatar you…
YozoraKurage Aug 15, 2026
4c1b581
feat: the parameter store reaches Modular Avatar by type, and finds a…
YozoraKurage Aug 15, 2026
7d93a61
feat: the sync list and the parameter rows say what the built avatar …
YozoraKurage Aug 15, 2026
4f4f008
feat: DD DynamicAnalyze's Rec panel says what the build did to the av…
YozoraKurage Aug 15, 2026
2628a73
feat: DD DynamicAnalyze runs have a format of their own, and it is no…
YozoraKurage Aug 16, 2026
5157cec
feat: DD DynamicAnalyze saves runs as .ddrun, opens both formats, and…
YozoraKurage Aug 16, 2026
1803b33
fix: DD DynamicAnalyze records the whole avatar's parameters, not one…
YozoraKurage Aug 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ temp.meta
# からローカルパッケージとして参照されるので、チルダ無しの temp/ に生成 cs を
# 置くと丸ごとコンパイル対象になってしまう。
temp~
.distillery/
57 changes: 57 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,40 @@ git の履歴に入れてはならない**。コミットされるファイル
- 漏れて入れてしまった場合: 未 push なら履歴を書き換えて除去し、バックアップ ref
(refs/original)も削除する。push 済みなら直ちにユーザーに報告して指示を仰ぐ。

## コミットメッセージは設計判断の一次資料

このリポジトリでは、設計判断の記録は doc コメントではなくコミットメッセージが持つ。
コードコメントは「今どうであるか」を、コミットメッセージは「なぜそうなったか」を
書く場所と使い分ける。後から `git log` を読む人間と AI が唯一の読者だと思って書く。

- 件名は conventional prefix(feat / fix / refactor / chore)+ 変更が使う人に
もたらす意味を一文で。本文が主役なので件名で説明し切ろうとしない。ただし
機能領域が特定できる語(analyzer、sync、graph 等)を件名か本文冒頭に一つは
入れる — 後から履歴を領域で絞れることが一次資料の条件。
- 本文には「何をしたか」ではなく**なぜこの形か**を書く: 動機になった問題、
選んだ機構、その帰結。差分を読めば分かることは繰り返さない。
- **採らなかった案と捨てた理由を書く。** 特に既存の方式を置き換える・巻き戻す
コミットは、前の方式を採用したときの論拠に答える形で、捨てた理由と
**失った能力**を必ず書く。「Replaces X」だけで理由の無いコミットを作らない。
- **トレードオフと保証の射程を書く**: 何を犠牲にしたか、この変更が保証
しないこと、意図的に対応しなかったケースとその理由。
- 本文中の数値の主張(「N スロットまで検証」「約 1/7 が該当」等)は、テストや
計測の実際の範囲と一致させる。テスト側の doc と食い違う数字を書かない。
- 互換性への影響を書く: 保存済みデータ・生成物・レシピ API・既存コントローラが
どう扱われるか。「既存には影響なし」も明示する価値のある主張。

## 設計来歴は distillery で引く(pull 型)

「なぜこの設計にしたか」「過去に何を決めたか」を問われたら、推測や差分からの再構成で
答えず、先に `.distillery/decisions/index.md` と該当 ADR を読む。コード側からは
`git blame` → コミットの `Session:` トレーラー → `.distillery/sessions/` で逆引きできる。
頼まれていないのに過去ログをコンテキストへ大量に持ち込まない(必要になってから引く)。

- `.distillery/` はローカル専用(remote 無し・pre-push 拒否)。中身、特に sessions/ の
生ログを外部へ送らない・コピーしない。共有できるのは人間が承認した ADR のみ。
- ADR の作成と状態変更(waive / deprecate 等)は人間の承認を得てから確定する。
手順は distillery スキルに従う。

## テストの走らせ方

この DevContainer には Unity 2022.3.22f1 が入っている(ベースは game-ci の
Expand Down Expand Up @@ -52,6 +86,29 @@ Editor イメージ)。EditMode テストはコンテナ内で完結する。
.devcontainer/unity/add-vpm.sh --remove com.vrchat.avatars
```

テストプロジェクトには GestureManager・Av3Emulator・NDMF・Modular Avatar も埋め込み
パッケージで入れてある(`Packages/` 直下の `vrchat.blackstartx.gesture-manager` /
`lyuma.av3emulator` / `nadena.dev.ndmf` / `nadena.dev.modular-avatar`)。DD DynamicAnalyze
が前 2 つを Rec で、NDMF を BuildCapture で型参照する(asmdef の versionDefines `DAERD_GM` /
`DAERD_AV3E` / `DAERD_NDMF` / `DAERD_VRC`)ためで、**4 つとも VRChat SDK に依存している**
(NDMF は SDK 無しでも動く設計だが、Unity.Collections などの依存を SDK 経由で解決している
ので、この構成では SDK と一緒に落ちる)。**SDK を抜くときは 4 つとも `Packages/` の外へ
先に退避する** — 順序を逆にするとツール側がコンパイルエラーになり、テストが 1 件も
走らない(終了コード 3)。MA は NDMF に依存するので単独で抜くこともできる。

```
mkdir -p /home/node/unity-tools-aside
mv /home/node/unity-testproject/Packages/vrchat.blackstartx.gesture-manager \
/home/node/unity-testproject/Packages/lyuma.av3emulator \
/home/node/unity-testproject/Packages/nadena.dev.modular-avatar \
/home/node/unity-testproject/Packages/nadena.dev.ndmf /home/node/unity-tools-aside/
.devcontainer/unity/add-vpm.sh --remove com.vrchat.avatars
# 戻すときは逆順(SDK を入れてから mv で戻す)
```

ツール不在では `#if DAERD_*` の中が丸ごと消え、asmdef の参照は未解決のまま無害に残る。
「ツールが無くても DaerD がコンパイルできる」ことは、この構成での全件実行が保証している。

**ビヘイビアやドライバに触る変更は、SDK 有りと無しの両方で走らせること。**
製品コードは型を名前で探すので、SDK があるとテスト側スタブではなく SDK の型が
実際に付く。型でキャストするテストはこの差で「SDK 有りでだけ落ちる」ようになる
Expand Down
121 changes: 116 additions & 5 deletions Editor/Authoring/AsyncSyncRecipeBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ namespace Yozolab.DaerD.Authoring
///
/// c.AsyncSync()
/// .Targets("Hue", "Outfit", "TailState")
/// .Rate("Hue", 2) // or spell the cycle out yourself:
/// .Schedule("Hue", "Outfit", "Hue", "TailState")
/// .Rate("Hue", 2) // weight: two of the pass's places. Or spell
/// .Schedule("Hue", "Outfit", "Hue", "TailState") // the cycle out yourself:
/// .FloatChannels(2).Step(0.3f);
///
/// Sends() goes one deeper still, saying what each step carries rather than which slot it
Expand Down Expand Up @@ -80,12 +80,36 @@ internal AsyncSyncRecipeBuilder(ControllerBuilder root, string baseName)
string layer = string.IsNullOrEmpty(_request.layerName)
? _request.baseName : _request.layerName;
if (!root.PostLayers.Contains(layer)) root.PostLayers.Add(layer);
// The Ready watcher is the same call's output and is rebuilt with it. Read
// back off the saved setup rather than derived from the name, which the
// builder may have had to make unique.
foreach (var watcher in WatcherLayerNames(controller, _request.baseName))
if (!root.PostLayers.Contains(watcher)) root.PostLayers.Add(watcher);

warnings.AddRange(AsyncSyncBuilder.Warnings(_request));
return warnings;
});
}

/// <summary>The layer names the setup's watchers ended up with — the builder
/// uniquifies the names it creates, so only the saved setup knows them.</summary>
static List<string> WatcherLayerNames(AnimatorController controller, string baseName)
{
var names = new List<string>();
var config = GraphFrameData.FindAsyncSync(controller, baseName);
if (config == null) return names;
var owned = new HashSet<AnimatorStateMachine>();
if (config.readyLayer != null) owned.Add(config.readyLayer);
if (config.staleLayer != null) owned.Add(config.staleLayer);
if (config.groups != null)
foreach (var group in config.groups)
if (group?.layer != null) owned.Add(group.layer);
foreach (var layer in controller.layers)
if (layer.stateMachine != null && owned.Contains(layer.stateMachine))
names.Add(layer.name);
return names;
}

/// <summary>
/// The base name an unnamed <c>AsyncSync()</c> runs under. A setup already generated
/// under the old flat "Async" default keeps it: renaming it on the next Generate would
Expand Down Expand Up @@ -128,8 +152,20 @@ public AsyncSyncRecipeBuilder Targets(params string[] parameters)
return Record("Targets", Names(parameters));
}

/// <summary>Sync this parameter <paramref name="timesPerPass"/> times per pass
/// (spread as evenly as the other slots allow). Ignored under an explicit Schedule.</summary>
/// <summary>
/// Give this parameter <paramref name="timesPerPass"/> of the pass's places, spread as
/// evenly as the other slots allow. A weight rather than a speed: the pass is however
/// many places it has, so raising one target's share lengthens the pass for everyone
/// else, and raising every target's changes nothing at all (a common factor is divided
/// out). Ignored under an explicit <see cref="Schedule"/>.
///
/// For "send this the moment it changes" reach for <see cref="Requestable"/> instead —
/// a weight buys a target places in every pass, whether or not anything moved. That is
/// why the wizard stopped offering weights and this call did not: a recipe author is
/// spelling a pass out and can mean this, while a form that hands the control to
/// everyone mostly hands out a longer pass. Setups that carry weights are still loaded,
/// built and exported unchanged.
/// </summary>
public AsyncSyncRecipeBuilder Rate(string parameter, int timesPerPass)
{
_request.rates[parameter] = timesPerPass;
Expand All @@ -148,6 +184,81 @@ public AsyncSyncRecipeBuilder Requestable(params string[] targets)
return Record("Requestable", Names(targets));
}

/// <summary>
/// Generate the remote-initialized flag: a local, unsynced Bool ("base/Ready") that
/// turns on once a client has decoded every slot at least once, and never turns off
/// again. It is what a remote has instead of a way to ask — nothing it observes can
/// reach the wearer — so read it to hold back anything that would look wrong with
/// half the values in place.
///
/// The wearer reads it as on from the start: their own values were never anywhere
/// else. Write <c>Ready &amp;&amp; !IsLocal</c> for "a remote that has finished
/// initializing" specifically.
/// </summary>
public AsyncSyncRecipeBuilder Ready()
{
_request.ready = true;
return Record("Ready");
}

/// <summary>
/// Generate the drift-suspicion flag: a local, unsynced Bool ("base/Stale") that turns
/// on when a lap did not bring every slot and off again when one does. Read it to hold
/// back anything that would look wrong on values that may have stopped arriving.
///
/// Judged when a slot the pass sends exactly once comes round, so it needs no timer
/// and no margin, and a pass stretched by a request cannot make it wrong. A pass with
/// no such slot — every slot sent more than once, or open to requests — cannot carry
/// the flag, and Generate says so instead of applying.
///
/// A remote that arrives mid-pass reads it as on for the rest of that pass; pair it
/// with <see cref="Ready"/> to tell that apart from a cycle that has started dropping.
/// </summary>
public AsyncSyncRecipeBuilder Stale()
{
_request.stale = true;
return Record("Stale");
}

/// <summary>
/// Assign these targets together, however far apart the pass sends them: the wearer
/// reads the whole set in one driver when the pass reaches the first of them and sends
/// every member from that reading, and a remote holds each one aside as it arrives and
/// copies the set into the real parameters once they are all in. One driver at each
/// end, so the values leave together and land together, and what the far side shows is
/// a set the wearer really held at one moment.
///
/// For targets that could share a step, batching already does this and does it sooner —
/// one driver sends them, so they were never going to arrive apart. Reach for a group
/// when they cannot share one: different types, or more of a type than the channels
/// carry.
///
/// The cost is freshness, both ways. A change made just after the group's reading was
/// taken is not sent until the pass comes round to it again, and a lap that loses a
/// member commits nothing and leaves the remote on the last complete set. That is the
/// trade in one sentence: a whole set that is up to a pass old, rather than a fresh
/// half of one. It costs no synced bits — a latch, a shadow and a flag per member, all
/// local, and a two-state layer per group.
///
/// Pair it with <see cref="Ready"/>. Without the flag the guarantee only starts after
/// the first full pass: a client decodes the index it arrived to — zero, because
/// nothing has reached it yet — as that slot arriving, so its first commit can carry a
/// value nobody sent. With the flag the commit waits for it, and there is no such first
/// commit to distrust.
///
/// c.AsyncSync("Zip").Targets("Hue", "Mesh", "Shape")
/// .Group("Outfit", "Hue", "Mesh", "Shape").Ready();
/// </summary>
public AsyncSyncRecipeBuilder Group(string name, params string[] members)
{
var group = new GraphFrameData.AsyncSyncConfig.SyncGroup { name = name };
group.members.AddRange(members);
_request.groups.Add(group);
var args = new List<string> { RecipeScript.S(name) };
args.AddRange(Names(members));
return Record("Group", args.ToArray());
}

/// <summary>
/// Spell the cycle out step by step, naming one target per step (a batched target
/// stands for its whole slot). Every multiplexed parameter must appear at least once
Expand All @@ -164,7 +275,7 @@ public AsyncSyncRecipeBuilder Schedule(params string[] stepsInOrder)

/// <summary>
/// Spell one step out as the set of targets it sends, and call it once per step. This
/// replaces the batching, the rates and <see cref="Schedule"/> together: the slots
/// replaces the batching, the weights and <see cref="Schedule"/> together: the slots
/// become the distinct sets, so the call says which targets share a step as well as
/// when each step comes round.
///
Expand Down
16 changes: 14 additions & 2 deletions Editor/Authoring/RecipeDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ public void Run()
EmitAsyncSync(layer.name, sync);
continue;
}
if (_asyncSyncs.supporting.Contains(layer.name))
{
_c.Script.Comment(RecipeExporter.Header("Layer: " + layer.name));
_c.Script.Comment("(regenerated by the async sync layer above)");
continue;
}
if (_gadgets.supporting.Contains(layer.name))
{
_c.Script.Comment(RecipeExporter.Header("Layer: " + layer.name));
Expand Down Expand Up @@ -176,6 +182,10 @@ void EmitAsyncSync(string layerName, AsyncSyncBuilder.Request r)
}
var requestable = AsyncSyncBuilder.RequestableTargets(r);
if (requestable.Count > 0) sync.Requestable(requestable.ToArray());
if (r.ready) sync.Ready();
if (r.stale) sync.Stale();
foreach (var group in AsyncSyncBuilder.EffectiveGroups(r))
sync.Group(group.name, group.members.ToArray());
// Before the cycle and the grid, which are the calls it makes legal: a Sends run
// that repeats a step reads as a mistake until the line above it says otherwise.
if (r.allowRepeatSteps) sync.AllowRepeats();
Expand Down Expand Up @@ -558,9 +568,11 @@ internal void EmitBehaviour(StateBuilder sb, ControllerIR.Behaviour behaviour)
break;
case 2:
if (TypeOf(entry.name) == AnimatorControllerParameterType.Bool)
sb.DrivingRandomizes(BoolOf(entry.name), entry.chance);
sb.DrivingRandomizes(BoolOf(entry.name), entry.chance,
entry.preventRepeats);
else
sb.DrivingRandomizes(Handle(entry.name, AnimatorControllerParameterType.Float), entry.min, entry.max);
sb.DrivingRandomizes(Handle(entry.name, AnimatorControllerParameterType.Float),
entry.min, entry.max, entry.preventRepeats);
break;
case 3:
if (entry.convertRange)
Expand Down
17 changes: 16 additions & 1 deletion Editor/Authoring/RecipeExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,11 @@ internal class AsyncSyncPlan
public readonly Dictionary<string, AsyncSyncBuilder.Request> layers =
new Dictionary<string, AsyncSyncBuilder.Request>();

/// <summary>Layers a planned setup also rebuilds — today the Ready watcher. They
/// have no call of their own: the AsyncSync call that owns them puts them back,
/// and emitting their states would build the machinery a second time.</summary>
public readonly HashSet<string> supporting = new HashSet<string>();

/// <summary>
/// Whether a planned setup generated this parameter — the index, the value
/// channels, the request flags, all of which live under its base name. The call
Expand Down Expand Up @@ -295,6 +300,15 @@ static AsyncSyncPlan PlanAsyncSyncs(AnimatorController controller,
continue;
}
plan.layers[layer.name] = request;
var owned = new HashSet<AnimatorStateMachine>();
if (config.readyLayer != null) owned.Add(config.readyLayer);
if (config.staleLayer != null) owned.Add(config.staleLayer);
if (config.groups != null)
foreach (var group in config.groups)
if (group?.layer != null) owned.Add(group.layer);
foreach (var other in controller.layers)
if (other.stateMachine != null && owned.Contains(other.stateMachine))
plan.supporting.Add(other.name);
}

// Same post-step caveat the gadget layers carry: this one is rebuilt at the end of
Expand Down Expand Up @@ -384,7 +398,8 @@ void Machine(ControllerIR.Machine machine)
continue;
// Same for a sync layer: its states play the controller's Empty clip, which the
// call resolves (or creates) at Generate time rather than through a field.
if (asyncSyncs.layers.ContainsKey(layer.name)) continue;
if (asyncSyncs.layers.ContainsKey(layer.name)
|| asyncSyncs.supporting.Contains(layer.name)) continue;
Register(layer.mask);
Machine(layer.machine);
foreach (var entry in layer.syncedMotions)
Expand Down
Loading
Loading