Skip to content

Fix consumer-reported blockers in the 0.0.1-alpha.1 preview - #2

Draft
VPDPersonal wants to merge 3 commits into
mainfrom
fix/upm-package-blockers
Draft

VPDPersonal wants to merge 3 commits into
mainfrom
fix/upm-package-blockers

Conversation

@VPDPersonal

Copy link
Copy Markdown
Owner

Acts on an audit of the published upm-preview branch (0.0.1-alpha.1) from a consumer project. Every item below was reproduced against the sources before being fixed.

Summary

  • 🐛 StateFactory.CreateState handed out a live reference to its own field, which the next call cleared and rewrote. A ChangeState issued from a state's Enter / IEnterController.OnEnter rewrote the chain the outer ChangeState was still iterating — states entered twice, duplicates left in CurrentStates. The async path was affected too, where the chain is held across await.
  • 🐛 A re-entrant ChangeState is now queued and applied once the running change completes (run-to-completion), instead of mutating the chain underneath it. Guards are re-resolved at apply time.
  • IsTransitionEnabled(source, target) — the edge-level guard every state change funnels through. IsStateEnabled only ever received the target, so edge legality could not be expressed without reading CurrentStates[^1] by hand.
  • StrictTransitions (opt-in) makes the transition registry the declaration of allowed edges: an uncovered path throws instead of transitioning silently. A partially covered path no longer counts as covered.
  • MonoStateMachine now forwards IsStateEnabled / IsControllerEnabled / IsTransitionEnabled / StrictTransitions. It composes the core rather than inheriting it, so a subclass previously could not override them at all (CS0115).
  • 🐛 ChangeStateAsync from an async enter/exit callback deadlocked — it awaited the very transition it was running inside. Now throws with an explanation.
  • 🔧 MIT license: LICENSE, package LICENSE.md, license field. The first preview shipped with none, which by default reserves all rights and blocks shipping a game built on the package.
  • 🐛 The Game Loop sample never reached the published package. package.json advertised Samples~/GameLoop, but a *~ backup pattern in a global gitignore matched the Samples~ directory itself — it was absent from every commit, and therefore from the git subtree split the release workflow publishes. The workflow was fine; the files were never there.
  • 📝 README no longer promises what UPM cannot do. UniTask was documented as "pulled in automatically as a package dependency"; UPM does not resolve git dependencies transitively, so a fresh install never compiled. It is now an explicit first install step with a pinned URL. The upm branch instructions are gone until the first stable release creates that branch.
  • ♻️ TransitionVia and the async transition entry points no longer dispatch through MethodInfo.Invoke.

Notes for review

  • 82/82 tests pass (73 pre-existing + 9 new). The two regression tests were run against the unmodified sources first and both fail there — CreateState returns a chain that shrinks from 2 to 1 after a second call, and CurrentStates ends up with 3 entries but only 2 distinct.
  • ⚠️ MonoStateMachine is not covered by the xUnit suite — it needs UnityEngine and is not in the test csproj. The forwarding was verified by compiling all 36 package sources against minimal MonoBehaviour / Time / UniTask stubs: clean build, zero warnings. Worth a look in the Editor before release.
  • ⚠️ No dependencies entry was added on purpose. A git URL there is ignored by UPM, and the registry form resolves only for consumers who have the OpenUPM scoped registry — for everyone else the install would fail outright on resolution. Documenting the step beats failing loudly.
  • ⚠️ Behaviour change: ChangeState now rejects a call made during an async transition before consulting IsStateEnabled, so an in-flight async transition throws regardless of the target. Previously a target IsStateEnabled refused returned silently.
  • 🔧 LICENSE.md ships without a .meta; Unity generates it on the next Editor open.
  • 🔧 Aspid.Core.HSM.Generators.dll was deliberately left untouched — no generator source changed, so the rebuild diff was pure noise.
  • 🔧 Default behaviour is unchanged for existing users: StrictTransitions is off, IsTransitionEnabled returns true, and the permissive partial-chain path still transitions.
🇷🇺 Описание на русском

Закрывает аудит опубликованной ветки upm-preview (0.0.1-alpha.1), присланный из проекта-потребителя. Каждый пункт перед починкой воспроизведён по исходникам.

Что сделано

  • 🐛 StateFactory.CreateState отдавал живую ссылку на собственное поле, которое следующий вызов очищал и перезаписывал. ChangeState, вызванный из Enter / IEnterController.OnEnter состояния, перезаписывал цепочку, по которой внешний ChangeState ещё шёл: состояния входили дважды, в CurrentStates оставались дубликаты. Async-путь задет тоже — там цепочка живёт через await.
  • 🐛 Реэнтерабельный ChangeState теперь ставится в очередь и применяется после завершения текущего перехода (run-to-completion), а не правит цепочку под ним. Гарды разрешаются в момент применения.
  • IsTransitionEnabled(source, target) — гард по ребру, через который проходит каждая смена состояния. IsStateEnabled получал только цель, поэтому легальность ребра нельзя было выразить, не читая CurrentStates[^1] вручную.
  • StrictTransitions (по желанию) делает реестр переходов объявлением разрешённых рёбер: непокрытый путь бросает исключение вместо молчаливого перехода. Частично покрытый путь больше не считается покрытым.
  • MonoStateMachine пробрасывает IsStateEnabled / IsControllerEnabled / IsTransitionEnabled / StrictTransitions. Он композирует ядро, а не наследует его, поэтому наследник раньше не мог их переопределить вообще (CS0115).
  • 🐛 ChangeStateAsync из async-колбэка входа/выхода уходил в дедлок — ждал тот самый переход, внутри которого выполнялся. Теперь бросает исключение с объяснением.
  • 🔧 Лицензия MIT: LICENSE, LICENSE.md в пакете, поле license. Первый превью вышел без всего этого, то есть по умолчанию all rights reserved — выпуск игры на пакете юридически заблокирован.
  • 🐛 Сэмпл Game Loop никогда не доезжал до опубликованного пакета. package.json объявлял Samples~/GameLoop, но паттерн *~ в глобальном gitignore матчил саму папку Samples~ — её не было ни в одном коммите, а значит и в git subtree split, который делает релизный workflow. Workflow был исправен, файлов просто не существовало.
  • 📝 README больше не обещает того, чего UPM не умеет. UniTask был описан как «pulled in automatically as a package dependency»; UPM не резолвит git-зависимости транзитивно, поэтому свежая установка не компилировалась. Теперь это явный первый шаг установки с пином. Инструкции про ветку upm убраны до первого стабильного релиза.
  • ♻️ TransitionVia и async-переходы больше не ходят через MethodInfo.Invoke.

На что смотреть при ревью

  • 82/82 теста зелёные (73 существовавших + 9 новых). Два регрессионных теста сначала прогнаны на неизменённых исходниках — оба падают: CreateState отдаёт цепочку, схлопывающуюся с 2 до 1 после второго вызова, а в CurrentStates оказывается 3 элемента при 2 уникальных.
  • ⚠️ MonoStateMachine не покрыт xUnit-набором — ему нужен UnityEngine, и в тестовый csproj он не включён. Проброс проверен компиляцией всех 36 исходников пакета против минимальных стабов MonoBehaviour / Time / UniTask: сборка чистая, ноль предупреждений. Перед релизом стоит открыть в редакторе.
  • ⚠️ Секция dependencies не добавлена намеренно. Git-URL там UPM игнорирует, а реестровая форма резолвится только у тех, у кого подключён scoped-registry OpenUPM — у остальных установка упала бы на резолве. Документированный шаг лучше громкого падения.
  • ⚠️ Изменение поведения: ChangeState теперь отклоняет вызов во время async-перехода до проверки IsStateEnabled, то есть активный async-переход бросает независимо от цели. Раньше цель, отклонённая IsStateEnabled, возвращалась молча.
  • 🔧 LICENSE.md идёт без .meta — Unity сгенерирует при следующем открытии редактора.
  • 🔧 Aspid.Core.HSM.Generators.dll намеренно не тронут: исходники генератора не менялись, дифф пересборки был чистым шумом.
  • 🔧 Поведение по умолчанию для существующих пользователей не изменилось: StrictTransitions выключен, IsTransitionEnabled возвращает true, частично покрытый путь по-прежнему переходит.

VPDPersonal and others added 3 commits August 10, 2026 12:55
- Declare MIT in package.json and add LICENSE / LICENSE.md. The first
  preview shipped without any license, which by default reserves all
  rights and blocks shipping a game that depends on the package.
- Re-include ~-suffixed directories in .gitignore. A global '*~' backup
  pattern matched the Samples~ directory itself, so the sample that
  package.json advertises was absent from every commit, and therefore
  from the subtree split the release workflow publishes.
- Stop claiming UniTask is "pulled in automatically as a package
  dependency". UPM does not resolve git dependencies transitively, so it
  never was; document it as an explicit first install step instead.
- Drop the upm branch install instructions until the first stable release
  creates that branch, and bump the version to 0.0.1-alpha.2.

Co-Authored-By: Claude <noreply@anthropic.com>
…anges

StateFactory.CreateState returned a live reference to its own field, which
the next call cleared and rewrote. A ChangeState issued from a state's
Enter / IEnterController.OnEnter therefore rewrote the chain the outer
ChangeState was still iterating: states were entered twice and duplicates
were left in CurrentStates. The async path was affected too, where the
chain is held across await.

- CreateState now returns a list the caller owns; the machine rents a
  separate buffer per in-flight transition.
- A re-entrant ChangeState is queued and applied once the running change
  completes (run-to-completion), with guards re-resolved at apply time.
- Add IsTransitionEnabled(source, target), the edge-level guard every state
  change funnels through, and opt-in StrictTransitions, which rejects a
  path no registered transition fully covers instead of transitioning
  silently. A partially covered path no longer counts as covered.
- Forward IsStateEnabled / IsControllerEnabled / IsTransitionEnabled /
  StrictTransitions through MonoStateMachine, which composes the core
  rather than inheriting it and so could not override them at all (CS0115).
- ChangeStateAsync called from an async enter/exit callback deadlocked on
  the transition it was running inside; it now throws instead.
- Replace the MethodInfo.Invoke dispatch with Type-based overloads.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@VPDPersonal VPDPersonal added bug Something isn't working documentation Improvements or additions to documentation labels Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant