Compare commits

..
Author SHA1 Message Date
Ovid e019197f43 fic_yingli1 2026-05-19 18:33:30 +08:00
5011 changed files with 870712 additions and 294026 deletions
@@ -1,140 +0,0 @@
---
name: "source-command-split-commit"
description: "将当前未提交改动拆分为多个符合规范的原子提交"
---
# source-command-split-commit
Use this skill when the user asks to run the migrated source command `split-commit`.
## Command Template
将当前未提交改动拆分为多个原子提交。执行前会分析变更、生成分组方案,经用户确认后逐组提交。
## 执行流程
### Step 1: 前置检查
1. **清理残留文件**:若存在 `msg.txt`,先 `rm -f msg.txt`
2. **读取规范**:确认 `COMMIT_CONVENTION.md` 存在且可读
### Step 2: 保护分支检查
保护分支列表(按优先级):`AGENTS.md` > `.cursor/rules/` > `.git/config branch.protected` > 默认(`master|main|develop`)
若当前在保护分支,询问用户:
- **A** - 创建 feature 分支后继续拆分(推荐)
- **B** - 转移到新分支后压成单提交
- **C** - 取消
### Step 3: 盘点变更
执行以下命令分析:
- `git status` - 整体状态
- `git diff --stat` - 文件统计
- `git diff` - 详细改动
- `git diff --cached` - 已暂存改动
### Step 4: 规则化分组
按"意图一致"原则分组,**优先按 type 分,再按 scope 分**。
**必须拆分的情况**
- 同一组可归属多个 type
- 同文件包含多个独立目的(如功能+重构)
- 资源替换与代码逻辑混合
**可以合并的情况**(最小拆分原则):
- 同一功能的多文件改动(脚本+Prefab)
- 同一模块的多个相关 Bug 修复
### Step 5: 生成方案表格
以表格形式展示拆分方案:
| 顺序 | 文件 | 变更目的 | 提交头 | 依据 | 方式 |
|------|------|----------|--------|------|------|
| 1 | `路径` | 描述 | `type(scope): subject` | 原因 | `git add`/`git add -p` |
**Type 选择优先级**`fix` > `feat` > `art` > `audio` > `scene` > `yarn` > `perf|refactor|...`
**Scope 规则**:单模块用模块名(如 `blockpuzzle`),跨模块省略。
### Step 6: 用户确认
询问:`[Y 执行 / n 取消 / 提出调整]`
- 若调整:根据反馈修改方案,再次确认
- 若执行:进入 Step 7
- 若取消:清理并退出
### Step 7: 逐组提交
对每组执行:
1. `git add <file>``git add -p <file>`
2. 复查:`git diff --cached --stat``git diff --cached`
3. 写入 `msg.txt` 并提交:`git commit -F msg.txt`
4. 检查状态:`git status`
**失败处理**
- `git add -p` 跳过关键 hunk → 暂停,提示处理方案
- pre-commit hook 失败 → 询问 [R]重试 [S]跳过 [A]中止
### Step 8: 结果校验
输出:
1. `git log --oneline -<N>`
2. 回放清单(验证 type/scope/subject 合规性)
3. 若不合规,询问是否修正
### Step 9: 清理
`rm -f msg.txt`
---
## Unity 改动速查表
| 改动类型 | type | scope 示例 |
|----------|------|-----------|
| 脚本逻辑修复 | `fix` | `blockpuzzle`, `dialog` |
| 脚本新功能 | `feat` | `huoshan`, `clue` |
| 美术资源(贴图/模型/动画) | `art` | 对应模块或省略 |
| 音频资源 | `audio` | `audio-kit` 或省略 |
| 场景配置(灯光/镜头) | `scene` | 对应模块或省略 |
| Yarn 对话 | `yarn` | `dialog` |
| 测试代码 | `test` | 对应模块 |
---
## 规范校验清单(执行前检查)
- [ ] 格式:`<type>(<scope>): <subject>``<type>: <subject>`
- [ ] type ∈ {`feat`,`fix`,`art`,`audio`,`scene`,`yarn`,`perf`,`refactor`,`style`,`docs`,`build`,`chore`,`test`}
- [ ] subject:中文、动词开头、≤50字、无句号
- [ ] 一次提交只做一件事
---
## 中文提交方法
由于 PowerShell 中文乱码问题,统一使用文件方式:
```bash
echo "type(scope): 描述" > msg.txt
git commit -F msg.txt
rm -f msg.txt
```
---
## 关于 Pre-commit Hooks
若配置了 husky/lint-staged
- 每次提交都会触发检查
- 如需跳过(**不推荐**):`git commit -F msg.txt --no-verify`
- 跳过可能导致 CI 失败
---
开始执行请回复 **"开始"**,或告知需要的调整。
-72
View File
@@ -1,72 +0,0 @@
---
name: yarn-text-audit
description: 全量审校 Yarn 文本,生成可追溯问题台账并可导出飞书多维表格
---
对用户指定的 Yarn 目录执行全量文本审校。确定性覆盖与安全门禁由 `Tools/yarn_text_audit.py` 提供;语言和叙事判断必须保持可复核。
## 执行流程
1. 明确目标目录、参考目录、是否包含注释、是否只出报告。
2. 对所有目标目录运行:
```powershell
python Tools/yarn_text_audit.py scan `
--root <Yarn目录可重复> `
--output Temp/YarnTextAudit `
--review-memory <可选上次飞书审阅导出的 reviewed-base.json>
```
3. 检查 `Temp/YarnTextAudit/summary.json`,必须满足:
- `unknown_active_lines == 0`
- `unreviewed_records == 0`
- `active_records == ledger.jsonl` 实际行数
4. 分文件、分节点复核 `ledger.jsonl`。活动文本包括命名对白、无角色旁白、系统文本、选项和 option prompt;排除首个非空白字符为 `//` 的注释、节点元数据和 Yarn 命令。
5. 将确定性错误标为“建议修改”,语义、节奏、专名和重要信息等判断标为“需确认”;不以自动规则冒充绝对正确。
6. 交付 `issues.csv``coverage.csv`。用户要求飞书时,先运行 `python Tools/yarn_text_audit.py lark-payloads --output Temp/YarnTextAudit`,再创建 `问题清单``覆盖报告``术语与样式` 三张表;单批不超过 200 条并核对返回数量。
7. 用户完成多维表格审阅后,导出完整记录并执行:
```powershell
python Tools/yarn_text_audit.py apply-review `
--reviews Temp/YarnTextAudit/reviewed-base.json `
--manifest Temp/YarnTextAudit/manifest.json `
--report Temp/YarnTextAudit/apply-report.json
# 预览无误后才实际写入
python Tools/yarn_text_audit.py apply-review `
--reviews Temp/YarnTextAudit/reviewed-base.json `
--manifest Temp/YarnTextAudit/manifest.json `
--report Temp/YarnTextAudit/apply-report.json `
--apply
```
8. 修改后重新扫描并运行 `verify`。不得新增、重生成或修改 `#line:` 标签;不得增加文本显示行数。
## 人工反馈优先级
- 后续扫描使用 `--review-memory`;脚本会按原文或已应用后的预期文本重建稳定 ID,不重复报告已经人工决策的记录。
- “按照备注修改”高于分类和脚本建议;只执行备注明确要求的断行、颜色、文字或效果。
- 不因 `Task` 说话者、`role="Aside"` 或专有名词出现本身自动判定颜色。必须结合是否为玩家行动目标、真实内心表达及当前叙事重点逐处判断。
- 专有名词不要求每次出现都标蓝;检查/拆除/短接/装回等玩家下一步操作可以按上下文标黄。
- 笑话效果优先只包裹双关或谐音落点,不默认包裹整句。
- `#autonext` 不应一律修正为 `#auto_next`;若审阅备注要求移除,则直接移除。
- 长行断点以约 18 个全角字符的实际 UI 宽度估算自动折行数,优先在逗号处显式控制;已有断行未经批准不重排。
## AIBIS 审校规则
- 检查错别字、错误重复、缺字候选、的地得、全半角和标点、标签配对及换行舒适度。
- 换行优先考虑逗号附近;只有在不增加显示行数时才建议调整。逗号停顿只用于调整后的断点或明确节奏位置。
- `[[aside]]...[[/aside]]`:实际自言自语、念文本或小声说话;斜体本身不是充分条件。
- `[[term]]...[[/term]]`:游戏专有名词,默认蓝色 `#52B6FF`
- `[[key]]...[[/key]]`:玩家必须关注的信息,优先级高于专名。
- `[[comma-pause]]`:可配置的 0.2 秒逗号停顿。
- `[[ulm]]`FP_Peipei2 中每条活动 `ULM自训练进程:` 文本。
- `[[joke]]...[[/joke]]`:火山实际讲出的笑话,不包括讨论笑话的说明对白。
- 本地输出放入已忽略的 `Temp/YarnTextAudit`;修改前保存文件 SHA-256,应用时必须精确匹配原文与哈希。
## 自检
```powershell
python Tools/yarn_text_audit.py self-test
python Tools/yarn_text_audit.py verify --output Temp/YarnTextAudit
```
@@ -1,94 +0,0 @@
---
name: narrative-timeline-flow
description: >-
叙事 Timelineaseprite/静图改 Timeline、Addressable 登记、黑屏防穿帮、替换 show_full_screen。
Use when 加叙事 timeline、aseprite 转 timeline、英里初登场、CG 演出、替换 show_full_screen、
hide_full_screen、黑屏穿帮、fade_out 时机、wait_timeline、Day1 begin / Day2 night 天桥 CG、
或用户提到 TimelineKit 叙事接入。
---
# 叙事 Timeline 接入(Aseprite / 静图 → Yarn
API 细节见 [`Assets/Scripts/SceneManagement/TimelineKit/README_TimelineKit.md`](../../Assets/Scripts/SceneManagement/TimelineKit/README_TimelineKit.md)。本 skill 只记**本仓库约定与踩坑**。
## 1. 何时用哪种 Yarn 命令
| 场景 | 命令 |
|------|------|
| 播完再往下走 | `<<play_timeline 名>>`(阻塞) |
| 黑屏下先起播 / 与对白并行 / Loop CG | `<<start_timeline 名>>` |
| 已 `start_timeline`,要等播完 | `<<wait_timeline 名>>`(不重头播) |
| 关掉整棵 Director GO | `<<hide_timeline 名>>` |
名称与场景里 `DirectorHandler.timelineName` **完全一致**(叙事多用中文单名,内嵌 Playable)。
## 2. 资产与 Addressable
1. Aseprite → `Assets/GameContent/Scene_{场景}/animationRaw/`(或现有 `AnimationRaw/`
2. Timeline → `GameContent/Scene_{场景}/Timeline/{名}.playable`
3. Addressable Group = 对应场景线(如 `Scene_ClinicOut``Scene_Bridge`
4. Address = `Timeline/{名}`(必须带 `Timeline/` 前缀)
5. 叙事 Timeline **一般不**加 `TimelineAssetRef`(维修玩法才用)
参考范本:`医生出地铁``day2夕阳医生看佩佩侧脸``英里初登场`
## 3. 场景 GO 约定
- 根节点挂 `PlayableDirector``Play On Awake = false`+ `DirectorHandler`
- **根节点默认 Active**`Start` 里要注册到 `TimelineCenter`;关掉根节点会注册失败
- **带 Sprite 的子物体(如 `Visual`)默认 Inactive**;用 ActivationTrack 或播放时再显
- `hide_timeline` = 根 `SetActive(false)`;之后再播时 `PlayTimeline` 会重新打开
- 全屏 CG 常需调位置(例:天桥侧脸根节点 `Y = -5`);PPU / scale 按相机再调
## 4. 黑屏防穿帮(必做)
**错误**`fade_out` 揭开场景 → 再 `play_timeline`(先露底再播 CG)。
**正确**(切场景后的 intro CG):
```yarn
<<fade_in>>
<<load_scene Scene/ClinicOut>>
<<init_actor ...>>
<<start_timeline 英里初登场>>
<<wait 0.2>>
<<fade_out>>
<<wait_timeline 英里初登场>>
<<hide_timeline 英里初登场>>
<<show_actor ...>>
<<play_timeline 医生出地铁>>
```
要点:黑屏内起播并稳住首帧 → 再结束黑屏 → 再等播完。CG 盖住初登场时,角色可延后到 `hide_timeline` 后再 `show_actor`
## 5. 替换 `show_full_screen` 静图
常驻 / Loop 演出(对白期间一直播):
```yarn
<<start_timeline day2夕阳医生看佩佩侧脸>>
<<fade_out 1>>
# ... 对白 ...
<<fade_in 1>>
<<hide_timeline day2夕阳医生看佩佩侧脸>>
<<fade_out 1>>
```
不要再用 `show_full_screen` / `hide_full_screen` 播同一段。
## 6. 其它坑
- `<<load_scene>>` **只需场景名**。旧脚本第二参数(如 `1`)已兼容忽略,**新代码不要写**。
-`.yarn`**不要动**已有 `#line:`;新行不加 `#line:`
- 只改运行时实际绑定的 Yarn(如 Day1_begin → FP,不是 Fiction 草稿)。
- Aseprite 多 tag:选主演出 clipLoop CG 用循环 clip + Director `Loop` 或长轨;一次性 intro 用有限时长 + `wait_timeline`
## 7. 落地检查清单
- [ ] Timeline 在正确 `GameContent/Scene_* /Timeline/`
- [ ] Addressable`Timeline/{名}` + 正确 Group
- [ ] `timelineName` 与 Yarn 一致;`Play On Awake = false`
- [ ] Visual 默认关;进 Play 不会提前露 CG
- [ ] 切场景 intro:黑屏内 `start` → 短 wait → `fade_out``wait_timeline`
- [ ] 静图替换:`start` / `hide` 成对
- [ ] 跑一次确认无先露场景、无 `load_scene` 参数错误
+1 -2
View File
@@ -75,8 +75,7 @@
*.psd filter=lfs diff=lfs merge=lfs -text
*.psb filter=lfs diff=lfs merge=lfs -text
*.bank filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -text
# DOTween editor images - exclude from LFS (stored as regular binaries, Unity re-import causes false changes)
Assets/Plugins/Demigiant/DOTween/Editor/Imgs/*.png -filter -diff -merge
Assets/Plugins/Demigiant/DOTween/Editor/Imgs/*.jpg -filter -diff -merge
Assets/Plugins/Demigiant/DOTween/Editor/Imgs/*.jpg -filter -diff -merge
+1 -3
View File
@@ -110,6 +110,4 @@ fmod_editor.log
.history/*
**/.history/*
.fake
Keystore/aibis-release-key.keystore
Keystore/keystore.bat
.fake
+1 -65
View File
@@ -56,70 +56,6 @@
"temp/": true,
"Temp/": true
},
"files.associations": {
"**/Plugins/Android/*Template.gradle": "plaintext"
},
"dotnet.defaultSolution": "aibis-dream.sln",
"dotnet.preferCSharpExtension": true,
"cSpell.enableFiletypes": [
"yarnspinner"
],
"cSpell.languageSettings": [
{
"languageId": "yarnspinner",
"patterns": [
{
"name": "yarn-commands",
"pattern": "/<<[^>]*>>/g"
},
{
"name": "yarn-expressions",
"pattern": "/\\{[^}]*\\}/g"
},
{
"name": "yarn-variables",
"pattern": "/\\$[a-zA-Z_][a-zA-Z0-9_]*/g"
},
{
"name": "yarn-markup",
"pattern": "/\\[[^\\]]*\\]/g"
},
{
"name": "yarn-hashtags",
"pattern": "/#[a-zA-Z_][a-zA-Z0-9_:-]*/g"
},
{
"name": "yarn-node-markers",
"pattern": "/^(---|===)$/gm"
},
{
"name": "yarn-headers",
"pattern": "/^(title|tags|position|color|colorID|tracking|custom|style):.*/gm"
},
{
"name": "yarn-character-names",
"pattern": "/^\\s*[A-Za-z_][A-Za-z0-9_]*(?=:)/gm"
},
{
"name": "yarn-comments",
"pattern": "/\\/\\/.*$/gm"
}
],
"ignoreRegExpList": [
"yarn-commands",
"yarn-expressions",
"yarn-variables",
"yarn-markup",
"yarn-hashtags",
"yarn-node-markers",
"yarn-headers",
"yarn-character-names",
"yarn-comments"
]
}
],
"cSpell.words": [
"Aibis",
"Newtonsoft"
]
"dotnet.preferCSharpExtension": true
}
-152
View File
@@ -1,152 +0,0 @@
# AGENTS.md
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
## Project Overview
AIBIS Dream (also known as "AllOurBrokenParts") is a Unity-based narrative adventure game with investigative elements. The project uses Yarn Spinner for dialogue, features a custom framework with modular systems, and includes multiple mini-games and a modular repair/fix system.
**Unity Version:** 2022.3.7f1c1 (Universal Render Pipeline)
## Architecture
### Core Framework (Assets/Scripts/Framework/)
Modular framework with Kit-pattern utilities:
- **ActionKit**: Action sequencing and command pattern
- **AudioKit**: Audio management (includes FMOD integration)
- **Config / Core**: Configuration and core utilities
- **EventSystemKit**: Decoupled event system
- **LogKit**: Structured logging with categories
- **PoolKit**: Object pooling
- **ResourceKit**: Asset loading via `ResourceSystem` (Addressables-based)
- **Note**: New code must use `ResourceSystem`, never `Resources.Load`. See [Resource Loading Best Practices](Docs/ResourceLoadingBestPractices.md). Asset placement rules in [Resource Management Spec](Docs/ResourceManagementSpec.md).
- **SelectorKit**: Weighted/batch/non-repeat random selection
- **SingletonKit**: Singleton base classes
- **StateMachineKit**: State management
- **OtherKit**: Miscellaneous utilities (`CameraKit`, `AnimatorKit`, `FadeKit`, `LocalizationKit`, etc.)
- **Attribute**: Custom editor attributes
### Key Game Systems
- **Game Loop** (Scripts/Game Loop/): `GameManager` (Singleton entry), `YarnVariableStorage` (Yarn variables), `SaveRestoreOrchestrator` / `SaveSystem` (save snapshots), `SceneLoader`, `ChapterController`, `TalkSceneSO`
- **Dialog System** (Scripts/Dialog System/): Yarn Spinner integration — `DialogController`, `LineRunner`, `LocalisedLineProvider`, `BaseYarnCommand`
- **SceneManagement** (Scripts/SceneManagement/): `ActorKit/`, `AnimatorKit/`, `EnvironmentKit/`, `SpawnerKit/`, `TimelineKit/`, `SceneCenter/`, `DreamDoorSystem`
- **UI** (Scripts/UI/): `DialogUI/`, `Panel/`, `Form/`, `Components/`, `Cursor/`, `UIManager`
- **MiniGame** (Scripts/MiniGame/): `HuoShan/` (language particles, EmotionWave), `BlockPuzzle/`, `Peipei/` (Eye, CutLine, Memory)
- **FixSystem** (Scripts/FixSystem/): Legacy — HeatMap, PunchTape, Memory, screen effects
- **FixSystemNew** (Scripts/FixSystemNew/): Active repair system — `FixSystemCenter`, `BodyModule/`, `Cable/`, `WhackMole/`, `Screen System/`, `Open/`, `Clinic/`
- **AssetRefs** (Scripts/AssetRefs/): Addressable key constants (`ConstRef`, `FixAssetRef`, `TimelineAssetRef`)
### Content & Localization
- **Assets/GameContent/**: Runtime assets by scope (`Common/`, `UI/`, `Scene_{Name}/`, `Feature_{Name}/`)
- **Assets/Language/**: Unity Localization tables (runtime active: `UIText/`, `Locales/`, `Sprite/`; `Dialog/``ActorName/``Params/` currently kept as migration backups)
- **Assets/Resources/Yarn/**: Current Yarn dialogue files (target migration to `Assets/Narrative/` per spec, not yet complete)
- **Assets/StreamingAssets/Config/**: CSV-driven localized gameplay data (`character.csv` for actor names, `params.csv` for `l10n.*` substitution terms)
- **Assets/Prototype/**: HTML/JS prototypes for mechanic testing
## Critical Dependencies
### Unity Packages
- **Yarn Spinner** 3.2.2 (OpenUPM), **Addressables** 1.21.21, **Cinemachine** 2.9.7
- **URP** 14.0.8, **TextMesh Pro** 3.0.6, **Timeline** 1.7.5, **Localization** 1.4.5
- **Visual Effect Graph** 14.0.8, **Visual Scripting** 1.8.0
### Third-Party
- **FMOD** (audio engine), **DOTween** (tweening), **Shapes** (vector graphics)
- **More Mountains Feedbacks** (game feel), **Destructible 2D**
## Development Guidelines
### Code Organization
1. Each major system self-contained in its own folder under `Assets/Scripts/`
2. Kit-pattern utilities in `Framework/` — reusable across systems
3. Singleton pattern for global managers (derived from `SingletonKit`)
4. Favor composition over inheritance
5. All runtime asset loading via `ResourceSystem` — never `Resources.Load`
### Scene Structure
- Scenes in `Assets/Scenes/`; `Persistence` scene hosts global managers and Persistent Loader
- Fix scenes: `{Character}FixScene` (e.g. `HuoShanFixScene`, `PeipeiFixScene`, `ExpressFixScene`)
- Test scenes: `languageTest`, `HuoShanExpressionTest`, `wavetest`, etc.
### Addressables
- Config in `Assets/AddressableAssetsData/`; Groups: `Core`, `Scene_*`, `Feature_*`, `Shared_*`, `Localization-*`
- Key format: `Category/Path` (e.g. `Animation/Peipei`, `Timeline/BlockPuzzle/PanelOpen`)
- Scene keys use bare PascalCase names (e.g. `ClinicOut`)
- Details in [Resource Loading Best Practices](Docs/ResourceLoadingBestPractices.md)
### Yarn Dialogue Files
- Current path: `Assets/Resources/Yarn/` (migration to `Assets/Narrative/Yarn/` planned)
- Pattern: `{Character}_{Stage}.yarn`; UTF-8 encoding required for Chinese
#### Yarn Line Tag Maintenance
- **Line tags** (`#line:xxxxxxx`) must be unique across the entire file. Duplicate tags break Yarn Spinner localization.
- Duplicates typically occur when copying an existing node to create a variant (e.g. `地铁ver1``地铁ver3`).
- **Use the dedup tool** (`Tools/dedup_yarn_tags.py`) for bulk cleanup:
```bash
# Preview duplicates from a specific node onward
python Tools/dedup_yarn_tags.py Assets/Resources/Yarn/Fiction_Day1_begin/Fiction_Day1_begin.yarn --from-node 地铁ver4
# Fix duplicates from a specific line onward
python Tools/dedup_yarn_tags.py Assets/Resources/Yarn/Fiction_Day1_begin/Fiction_Day1_begin.yarn --from-line 535 --fix
# Scan an entire directory (preview only)
python Tools/dedup_yarn_tags.py Assets/Resources/Yarn/ --scan-all
```
#### Yarn Script Generation
- **Do NOT add `#line:` tags when generating or editing Yarn dialogue.** Leave all line-tag generation to the user's localization workflow (Unity Localization + Yarn Spinner import tools).
- When creating new `.yarn` files, write plain dialogue lines without any `#line:` suffixes.
### Asset Naming
- **Scripts**: PascalCase (e.g. `DialogController.cs`)
- **Addressable Keys**: `Category/Path` — no spaces, no extensions, ≤3 levels
- **Addressable Groups**: `{Type}_{PascalCaseName}`
### Git & Commits
- LFS for large binaries; ignored: Library/, Temp/, Build/, *.csproj, *.sln
- **Commit format**: `<type>(<scope>): <subject>` — subject in Chinese, type/scope in English
- Extended types: `feat`, `fix`, `art`, `audio`, `scene`, `yarn`, `perf`, `refactor`, `style`, `docs`, `build`, `chore`, `test`
- See [COMMIT_CONVENTION.md](COMMIT_CONVENTION.md) for full details
## Key Build Commands
```bash
# Windows build
"C:\Program Files\Unity\Hub\Editor\2022.3.7f1c1\Editor\Unity.exe" -batchmode -quit -projectPath . -buildWindows64Player "Build/AIBIS_Dream.exe" -logFile Build/log.txt
```
## Common Tasks
### Adding a New Mini-Game
1. Create `Assets/Scripts/MiniGame/{GameName}/`
2. Add assets to `Assets/GameContent/Feature_{GameName}/`
3. Create Addressable Group `Feature_{GameName}`
4. Create test scene in `Assets/Scenes/`
### Adding Yarn Dialogue
1. Create `.yarn` in `Assets/Resources/Yarn/{Module}/`
2. Ensure YarnProject localization assets/CSV are updated (Unity `Assets/Language/Dialog/` is backup-only during migration)
3. Reference in scene's `DialogueRunner`; test with `DialogController`
## Debugging & Performance
- `LogKit` for structured logging; Addressables Event Viewer for resource debugging
- Monitor `SceneResourceLoader.TotalHandleCount` for leak detection
- Always use PoolKit for frequent instantiation; `LoadUncachedAsync` + `EarlyRelease` for large Timeline assets
- Use `LoadAsync<GameObject>` + `Instantiate` for prefabs, never `InstantiateAsync`
## Important Notes
- Narrative-focused game — dialogue and story take precedence
- `FixSystemNew` is the active repair architecture; `FixSystem` is legacy
- `YarnVariableStorage` holds Yarn runtime variables; save/load flows through `SaveRestoreOrchestrator` and `SaveSystem`
## Reference Documents
- [Resource Loading Best Practices](Docs/ResourceLoadingBestPractices.md) — `ResourceSystem` API, Key/Group/Label naming, lifecycle
- [Resource Management Spec](Docs/ResourceManagementSpec.md) — Asset placement: `GameContent/`, `Narrative/`, `_Archive/`
- [Commit Convention](COMMIT_CONVENTION.md) — Commit message format and branch naming
- [HuoShan Docs](Docs/HuoShan/) — Narrative flow, resources, test flowcharts
---
*This document provides guidance for Codex when working with the AIBIS Dream codebase.*
@@ -15,7 +15,7 @@ MonoBehaviour:
m_DefaultGroup: 09546aca93a801441859af2a811fe53b
m_currentHash:
serializedVersion: 2
Hash: e92b9b9776d0601449874b8c1b0ed1ae
Hash: 8e8b410f1714c542808a3bfbe0724666
m_OptimizeCatalogSize: 0
m_BuildRemoteCatalog: 0
m_BundleLocalCatalog: 0
@@ -59,15 +59,13 @@ MonoBehaviour:
- {fileID: 11400000, guid: 6d259d339c6615248b893837398c1438, type: 2}
- {fileID: 11400000, guid: 8718a71c99923e247a960cbf4e4795f5, type: 2}
- {fileID: 11400000, guid: 1e521cfc13b314642b1e682e3be7ea17, type: 2}
- {fileID: 11400000, guid: aafc8258a2111e94d8339a3152cf50e0, type: 2}
- {fileID: 11400000, guid: 7e4ab0de168458b43a21dc2f9e811ff5, type: 2}
- {fileID: 11400000, guid: 724eed66b2d8eef4faf77a4bee4d2845, type: 2}
- {fileID: 11400000, guid: 62a92c74b9bd5bd439690727c1831500, type: 2}
- {fileID: 11400000, guid: b593dc98ea11a484995123de77d9bf1a, type: 2}
- {fileID: 11400000, guid: d582f9080605ab34cbcde3249c980030, type: 2}
- {fileID: 11400000, guid: 54799118a585dda439cab043d56ec68c, type: 2}
- {fileID: 11400000, guid: dc8d31ed80ea35945812ff6552efbbe5, type: 2}
- {fileID: 11400000, guid: af94af36950b08d4f9a5037546bbe040, type: 2}
- {fileID: 11400000, guid: 60fd2fd8c7094b242b01c63de65de9ea, type: 2}
- {fileID: 11400000, guid: 7e4ab0de168458b43a21dc2f9e811ff5, type: 2}
m_BuildSettings:
m_CompileScriptsInVirtualMode: 0
m_CleanupStreamingAssetsAfterBuilds: 1
@@ -114,7 +112,6 @@ MonoBehaviour:
- Locale-en
- Locale-zh-Hans
- SceneSO
- Locale-ja-JP
m_SchemaTemplates: []
m_GroupTemplateObjects:
- {fileID: 11400000, guid: 0c158052680a3e94184c450d00291514, type: 2}
@@ -1,31 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bbb281ee3bf0b054c82ac2347e9e782c, type: 3}
m_Name: Localization-Asset-Tables-Japanese (Japan) (ja-JP)
m_EditorClassIdentifier:
m_GroupName: Localization-Asset-Tables-Japanese (Japan) (ja-JP)
m_Data:
m_SerializedData: []
m_GUID: bac626c44cee44544a45a750681e92e0
m_SerializeEntries:
- m_GUID: 8b2d41616f1a75840a5800ee28d1c4ed
m_Address: sprite_ja-JP
m_ReadOnly: 1
m_SerializedLabels:
- Locale-ja-JP
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 1
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
m_SchemaSet:
m_Schemas:
- {fileID: 11400000, guid: 842c6a62aa3abdf4d9b230f249b9fd76, type: 2}
- {fileID: 11400000, guid: 1706e28ae8e595149b2ac1fbea7f8071, type: 2}
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: af94af36950b08d4f9a5037546bbe040
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -15,11 +15,17 @@ MonoBehaviour:
m_GroupName: Localization-Assets-English (en)
m_Data:
m_SerializedData: []
m_GUID: 59bf392f3aef3b24da46dd8d3fa9259f
m_SerializeEntries: []
m_GUID: 50aef5699023b21459e68677a9449d80
m_SerializeEntries:
- m_GUID: 032b1b5e09139ae45b6de51d35baae79
m_Address: discord-square-color-icon
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 1
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
m_SchemaSet:
m_Schemas:
- {fileID: 11400000, guid: fa74e99b946159549971be154369b2cb, type: 2}
- {fileID: 11400000, guid: 8e1cbbf76f0150145a39ce648fbf3985, type: 2}
- {fileID: 11400000, guid: 895c7c97ef5802446a395377ef704940, type: 2}
- {fileID: 11400000, guid: 8004f70cd10ce4a439cb9e822bb078c2, type: 2}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 60fd2fd8c7094b242b01c63de65de9ea
guid: aafc8258a2111e94d8339a3152cf50e0
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
@@ -22,6 +22,145 @@ MonoBehaviour:
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 37c291d6dce732d45afb2e2b62218683
m_Address: Assets/Language/Dialog/FP_Day1_begin/FP_Day1_begin Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c1de143c98cd0804ab3b7b9a36fcf06a
m_Address: Assets/Language/ActorName/ActorName Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 76902d55aa2d3474aa96c8e574b8212f
m_Address: Assets/Language/Dialog/FP_Peipei1/FP_Peipei1 Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0cb567219be64c04b9552ecc61f8396a
m_Address: Assets/Language/Dialog/Fiction_Day1_night/Fiction_Day1_night Shared
Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 685539ac5030ab5409faa8feccba91e2
m_Address: Assets/Language/Dialog/FP_Day1_mid/FP_Day1_mid Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 4f3df7a54674e8343b4699544a25db67
m_Address: Assets/Language/Dialog/FP_Shitou2/FP_Shitou2 Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 2c7f3692831aa5f4daa13d00e99ad77c
m_Address: Assets/Language/Dialog/FP_Shitou1/FP_Shitou1 Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 949f8d1a26133a94c83ac9cc7e6d94c7
m_Address: Assets/Language/Dialog/FP_Peipei2/FP_Peipei2 Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 59e2a0d8fc60a784993a6f6cdfeb9c7d
m_Address: Assets/Language/Dialog/FP_Day1_night/FP_Day1_night Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 831d81945a7dc8d498ccebcdd4d05044
m_Address: Assets/Language/Dialog/FP_Day2_mid/FP_Day2_mid Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 45899a60a64d58a4f81fad32d2171702
m_Address: Assets/Language/Dialog/FP_Day2_night/FP_Day2_night Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: ea388b59e37ddc14da4d8e3e9ead4133
m_Address: Assets/Language/Dialog/Test_Huoshan/Test_Huoshan Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 8f3dfd038e75c3a41a865ab8dd730809
m_Address: Assets/Language/Dialog/FP_Prologue/FP_Prologue Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 011b21a8c75449e4a982549cebc1a6ca
m_Address: Assets/Language/Params/Params Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: f6d220fec7b888b4f89463b5da038e62
m_Address: Assets/Language/Dialog/Fiction_Day1_mid/Fiction_Day1_mid Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c5f887d31e1d7bc46b8a7ab784f19b9a
m_Address: Assets/Language/Dialog/Fiction_Day1_begin/Fiction_Day1_begin Shared
Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 578cb0ae76a180d45ac70e058c676ac7
m_Address: Assets/Language/Dialog/FP_Peipei3/FP_Peipei3 Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: f5108536d82f2c14a922f0a1d3e83638
m_Address: Assets/Language/Dialog/FP_Day3_mid/FP_Day3_mid Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0ee8d3542a2395745b4ad6bf55e3b097
m_Address: Assets/Language/Dialog/Fiction_Day2_mid/Fiction_Day2_mid Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 771ed3ef48efbb645827ce5c29d63754
m_Address: Assets/Language/Dialog/Fiction_Day2_night/Fiction_Day2_night Shared
Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: aaa27d8b7dfc3fc4294bda026ae135f2
m_Address: Assets/Language/Dialog/Fiction_Day3_mid/Fiction_Day3_mid Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 3c243fd229013b84e9c88091eaa74103
m_Address: Assets/Language/Dialog/Fiction_Day3_night/Fiction_Day3_night Shared
Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: bb8e2763fb266ff4d8b9c0b0f37882e2
m_Address: Assets/Language/Dialog/Fiction_Day4_mid/Fiction_Day4_mid Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: bb6fa95706f665c469e270567be5cfe9
m_Address: Assets/Language/Dialog/Fiction_Huoshan1/New Table Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 042ab1d25056cbd4984621037ac848f0
m_Address: Assets/Language/Dialog/Fiction_Yingli1/Fiction_Yingli1 Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 70f8ff033cddcd543a618974781d0bb0
m_Address: Assets/Language/Dialog/FP_Day3_night/FP_Day3_night Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 08cb937dd06038b4486d6a3662539542
m_Address: Assets/Language/Dialog/Test_GeneralEnd/New Table Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9abdd56d318b1ec459c7e866748d6878
m_Address: Assets/Language/Sprite/sprite Shared Data.asset
m_ReadOnly: 1
@@ -33,22 +172,47 @@ MonoBehaviour:
m_SerializedLabels:
- Locale-en
- Locale-zh-Hans
- Locale-ja-JP
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 4a2d391471bd9cd439150c52659e9846
m_Address: Assets/Language/Params/Params Shared Data.asset
- m_GUID: dd4a0fe1d258fbc4384ab1db825d5ae3
m_Address: Gts1/Fiction_Gts1 Shared Data.asset/Fiction_Gts1 Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 032b1b5e09139ae45b6de51d35baae79
m_Address: discord-square-color-icon
- m_GUID: 57cfa4d941133b840b4345cf815dda66
m_Address: Assets/Language/Dialog/Fiction_Day2_begin/Fiction_Day2_begin Shared
Data.asset
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
- Locale-ja-JP
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 7ea8c68fdc624de4c86434963c06ec55
m_Address: Assets/Language/ChapterInfo/ChapterInfo Shared Data.asset
- m_GUID: c3d23bd971e02d34cb356b2029523427
m_Address: Assets/Language/Dialog/Fiction_Day1_sleep/Fiction_Day1_sleep Shared
Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: e3c75f951d9792449906a3a8010d2e74
m_Address: Assets/Language/Dialog/Fiction_Day2_sleep/Fiction_Day2_sleep Shared
Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 28396c5e50d205645b94b89018d01a2a
m_Address: Assets/Language/Dialog/Ficion_Shitou1/Fiction_Shitou1 Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 73c880a15817b7f408ad0596f07a3626
m_Address: Assets/Language/Dialog/FP_Day1_sleep/FP_Day1_sleep Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: afa9b26bd11b9d847bbb9714b8049a4b
m_Address: Assets/Language/Dialog/FP_Day2_begin/FP_Day2_begin Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 90d6cbb308df1484f9146a0ad156202e
m_Address: Assets/Language/Dialog/FP_Day2_sleep/FP_Day2_sleep Shared Data.asset
m_ReadOnly: 1
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
@@ -29,12 +29,6 @@ MonoBehaviour:
m_SerializedLabels:
- Locale
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9d3da569ceb2f3740bd6f4fd01e51c62
m_Address: Japanese (Japan) (ja-JP)
m_ReadOnly: 1
m_SerializedLabels:
- Locale
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 1
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
m_SchemaSet:
@@ -23,14 +23,206 @@ MonoBehaviour:
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: aeb318c895d57014b89d5540c6baff95
- m_GUID: c21a8fbe9bf147649b5b01320ada3f11
m_Address: FP_Day1_begin_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0b504f7f6402d8747896b0cf19674e69
m_Address: ActorName_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 247e0ac5e48cbe24ea7e228e1a039590
m_Address: FP_Peipei1_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: d6149a0fedb6cc24ba2a318aedab1de0
m_Address: FP_Day1_mid_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 8a56c6bff47205b439b07425f6f5ad94
m_Address: FP_Shitou2_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 32954c9c2dce35e42a31befcec1e861f
m_Address: FP_Shitou1_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 860bd75190298994b9d5dee30aebd910
m_Address: FP_Peipei2_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: bfc0211d0386bc9468ecace61129ecd5
m_Address: FP_Day1_night_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 204ffaeec9d0f7e4cb708476b4d459a2
m_Address: FP_Day2_mid_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 2516ada36f949234caf993f1deaf79e6
m_Address: FP_Day2_night_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 523b3962bff363849a36683b375adc06
m_Address: Test_Huoshan_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 83f5756a759af6f459aa1871eb248f5b
m_Address: FP_Prologue_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 5d4915d0761772c41902e704e2ecfd0b
m_Address: Params_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: bdf8ad79a9f707b45805e23c25ccdadb
m_Address: ChapterInfo_zh-Hans
- m_GUID: fb51befc2226dfa44bbde7203364c019
m_Address: Fiction_Day2_mid_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 6603078d8eb41524e9ad2edd5d182ccc
m_Address: Fiction_Day1_mid_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 72755a9b5a5ca5c4e92028ca44c66a6d
m_Address: FP_Peipei3_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c260e1e2604154648971c06353edad0e
m_Address: FP_Day3_mid_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: a75bb4d4a30c22f40ad5c7d70c8276d4
m_Address: Fiction_Day1_night_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: b2f0a2aa5e0b95346bf499980c2994d4
m_Address: Fiction_Day2_night_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 6c498406bf979ca45b345e5bdbd3cc4b
m_Address: Fiction_Day3_mid_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 8e5812f1d9f10a4488702f35405d197d
m_Address: Fiction_Day3_night_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 35f017da97194574da316391464204b2
m_Address: Fiction_Day4_mid_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 75ed4030af85d794f832919dbe427f51
m_Address: Fiction_Huoshan1_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 4b181f70ec2e65f4cab1545662415306
m_Address: Fiction_Yingli1_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: a525b6d2bcfeb2446b2f4d2b36b043c7
m_Address: FP_Day3_night_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 1f031d78df49e5645a4ceaafa7283f91
m_Address: Test_GeneralEnd_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9cecc6d4b999d5c489b69406176bab99
m_Address: Fiction_Gts1_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: fbfb0ced2ba1bf44e86964158ae53d72
m_Address: Fiction_Day1_sleep_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 999ab1a8772cb1a4cba0325144ff5488
m_Address: Fiction_Day1_begin_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 4d88b47f4a7e3f249b44203f411e4adf
m_Address: Fiction_Day2_sleep_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 03f5785bc02343d448436956c6991b38
m_Address: Fiction_Shitou1_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 14a941304f21e2042afd56019acfca6c
m_Address: FP_Day1_sleep_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: d9f05be3049f65b4c82800b1bde3b7c2
m_Address: FP_Day2_begin_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 09898f26fbea1454cb10f2c27e2ca907
m_Address: FP_Day2_sleep_zh-Hans
m_ReadOnly: 1
m_SerializedLabels:
- Locale-zh-Hans
@@ -23,14 +23,206 @@ MonoBehaviour:
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9248cb179e7aafa4c903f1fc54b41ef7
- m_GUID: 1e7115110ec21ec4da592999daa0c146
m_Address: FP_Day1_begin_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: e80667e8ff7367e4ab9bc4f9421271e8
m_Address: ActorName_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 08ef2a7dddda7ee49b869bcd73858404
m_Address: FP_Peipei1_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 30ab319b5e0bafe46aa622bb93381c7e
m_Address: FP_Day1_mid_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 927a25bda151fc7469749a3724bef6b0
m_Address: FP_Shitou2_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: fda6eb219437ffc40ab860aa89d9446d
m_Address: FP_Shitou1_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 13f447125bbae5e45a4a446c2c9ac397
m_Address: FP_Peipei2_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c65e073d94ffbbb4b9b6107dee05e670
m_Address: FP_Day1_night_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 399374eeba6905048a7342d0bb494f9c
m_Address: FP_Day2_mid_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: cbc3925d28fc66c4cb3293b7bd66db0b
m_Address: FP_Day2_night_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: fb0b8f197b5a607468eeb9326737d547
m_Address: Test_Huoshan_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 49a6e4d039261bf42a18782ca317fca5
m_Address: FP_Prologue_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0a9b8bbf38d109d409792ae7cc48cec7
m_Address: Params_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 8906df9235e52fb41b11fe8776a77032
m_Address: ChapterInfo_en
- m_GUID: eb9ebf027dd4b564cb035d7167ff8099
m_Address: Fiction_Day2_mid_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 705a631ebd09db04fa1bda39e20e505e
m_Address: Fiction_Day1_mid_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: fde90ade684482142af49533e4f4ed62
m_Address: FP_Peipei3_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 036c8c6ed1ecc314a8e0ae02fe15ab5e
m_Address: FP_Day3_mid_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0d2928a7d58c26a4ca78abf2f5da1373
m_Address: Fiction_Day1_night_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 59fc388134f5d884c8d7ca28f2d7017e
m_Address: Fiction_Day2_night_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 379ead226bea4f844b1c136c4f748633
m_Address: Fiction_Day3_mid_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: aaf54fb66c2fa9d4ab68626ced632a7a
m_Address: Fiction_Day3_night_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c0c8b8aea11da444ea41eece5a698e5f
m_Address: Fiction_Day4_mid_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 2a14b1d82f4aef94aac2b230392605fc
m_Address: Fiction_Huoshan1_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 79ccbc35144b91a45bb9fdf9732d238a
m_Address: Fiction_Yingli1_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0e9bb0974017aa346a379b78126f6f7b
m_Address: FP_Day3_night_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 27ad0341432ae0b46a8fe117eb5c73e2
m_Address: Test_GeneralEnd_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 1adbe96b15171134584c9a2d7d69c045
m_Address: Fiction_Gts1_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9d72f86c018628e418747a7d0a66cb21
m_Address: Fiction_Day1_sleep_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 30a0c1ae8c45da34f8e3f90692e96e11
m_Address: Fiction_Day1_begin_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 36d94b46b4a12b54b8e084d8d275e675
m_Address: Fiction_Day2_sleep_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 265c923a085f98042b44f8b18dd8389b
m_Address: Fiction_Shitou1_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: ef76bcd9daf9b6a4580eaa28d2073ebf
m_Address: FP_Day1_sleep_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: a6b685d3b3e17c64da7ca72add7b3540
m_Address: FP_Day2_begin_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: a0e634b20b52dd34abb7999e889ec772
m_Address: FP_Day2_sleep_en
m_ReadOnly: 1
m_SerializedLabels:
- Locale-en
@@ -1,43 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bbb281ee3bf0b054c82ac2347e9e782c, type: 3}
m_Name: Localization-String-Tables-Japanese (Japan) (ja-JP)
m_EditorClassIdentifier:
m_GroupName: Localization-String-Tables-Japanese (Japan) (ja-JP)
m_Data:
m_SerializedData: []
m_GUID: 62cf9265bfa710b4b9152c336336a38e
m_SerializeEntries:
- m_GUID: 7c757424270c91e43bf45198f69c983a
m_Address: UIText_ja-JP
m_ReadOnly: 1
m_SerializedLabels:
- Locale-ja-JP
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9ed23d1d910a1a147ba5c316a87cbad4
m_Address: Params_ja-JP
m_ReadOnly: 1
m_SerializedLabels:
- Locale-ja-JP
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9d8accfdeabc8034c986233920a17184
m_Address: ChapterInfo_ja-JP
m_ReadOnly: 1
m_SerializedLabels:
- Locale-ja-JP
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 1
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
m_SchemaSet:
m_Schemas:
- {fileID: 11400000, guid: 7df396a6b5136fe4e9d2d659731161d2, type: 2}
- {fileID: 11400000, guid: 545ed8780a6508047bd42b964feb82fd, type: 2}
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: dc8d31ed80ea35945812ff6552efbbe5
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -47,13 +47,8 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: f19cff809e7020b41ba8ad7da199db86
m_Address: "Timeline/day2\u5915\u9633\u533B\u751F\u770B\u4F69\u4F69\u4FA7\u8138"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: f18fc85f0eeecb44b9cfae24e8e19fa1
m_Address: "FrameAnimation/\u5929\u6865\u4F69\u4F69"
- m_GUID: ea008b0c912c0d748997bf458879cb74
m_Address: "Animation/\u5929\u6865\u4F69\u4F69"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
@@ -17,8 +17,8 @@ MonoBehaviour:
m_SerializedData: []
m_GUID: 9ae1da1b7a680244cbe735b3b4437f26
m_SerializeEntries:
- m_GUID: 01b1640d62cd5ff4686088bb3d4c8fba
m_Address: "FrameAnimation/\u8BCA\u5BA4\u5916\u4F69\u4F69"
- m_GUID: b6a14d8a19b9a3041baf76df2e8e4558
m_Address: "Animation/\u8BCA\u5BA4\u5916\u4F69\u4F69"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
@@ -72,21 +72,16 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 7760fc67933fb684d81ea5275fac0a50
m_Address: "Animation/\u8BCA\u5BA4\u5916\u706B\u5C71"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 253f47db959b2814998695fb5d1f4d97
m_Address: "Timeline/\u65CB\u8F6C\u53F6\u67C4"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 1799dbcdb4a76624bb89622707581f92
m_Address: "Timeline/\u82F1\u91CC\u521D\u767B\u573A"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 3d0198d0d6d221948a3d5ddf9cd2c34f
m_Address: "FrameAnimation/\u8BCA\u5BA4\u5916\u706B\u5C71"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 0
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
m_SchemaSet:
@@ -32,6 +32,11 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 417a7984630786f46a5ade5d2b457852
m_Address: "Animation/\u706B\u5C71"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 838bd5cc2ceffc1459d167675d96f66a
m_Address: "Timeline/\u706B\u5C71\u8868\u8FBEpanel"
m_ReadOnly: 0
@@ -82,21 +87,11 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 58d09572ecd54714a787a1fc5aec6c5f
m_Address: "Timeline/\u706B\u5C71\u5934\u75DB3"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: e69af1ed498c57f4799f936aa28e4ed4
m_Address: "Timeline/\u706B\u5C71\u5934\u75DB1"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: efa335dcb6e706347bd4acf3222331b9
m_Address: "FrameAnimation/\u706B\u5C71"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 0
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
m_SchemaSet:
@@ -32,8 +32,8 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 26105571c9644aa4194f2b981ee33a58
m_Address: "FrameAnimation/\u4F69\u4F69"
- m_GUID: 809f95af8dd580448ab804b94fbd9cb2
m_Address: "Animation/\u4F69\u4F69"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
@@ -22,8 +22,23 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: b0ee8d48dab31814da87651bba9b5041
m_Address: "FrameAnimation/\u5730\u94C1\u533B\u751F"
- m_GUID: 2d4ccf1c582289344976ab5c3af05515
m_Address: "Animation/\u5730\u94C1\u533B\u751F"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0057bf4b5fe16aa4b8c7db27d0ddfc0a
m_Address: "Animation/\u5730\u94C1\u706B\u5C71"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 40f07326ace570241bd93fcfc3decb87
m_Address: "Animation/\u5730\u94C1\u706B\u5C71\u6697"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: a63b52b3e8d9e0441aaff086b9c1ae6b
m_Address: "Animation/\u5730\u94C1\u533B\u751F\u6697"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
@@ -42,16 +57,6 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: b02d1e0ae4bc971408a0b6a667597624
m_Address: "FrameAnimation/\u5730\u94C1\u706B\u5C71"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 66be33a1bdb16ee409c6ac05b61e08a7
m_Address: "Subway/\u76F8\u9519\u5217\u8F66"
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 0
m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2}
m_SchemaSet:
@@ -1,45 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e5d17a21594effb4e9591490b009e7aa, type: 3}
m_Name: Localization-Asset-Tables-Japanese (Japan) (ja-JP)_BundledAssetGroupSchema
m_EditorClassIdentifier:
m_Group: {fileID: 11400000, guid: af94af36950b08d4f9a5037546bbe040, type: 2}
m_InternalBundleIdMode: 1
m_Compression: 1
m_IncludeAddressInCatalog: 1
m_IncludeGUIDInCatalog: 1
m_IncludeLabelsInCatalog: 1
m_InternalIdNamingMode: 0
m_CacheClearBehavior: 0
m_IncludeInBuild: 1
m_BundledAssetProviderType:
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider
m_ForceUniqueProvider: 0
m_UseAssetBundleCache: 1
m_UseAssetBundleCrc: 1
m_UseAssetBundleCrcForCachedBundles: 1
m_UseUWRForLocalBundles: 0
m_Timeout: 0
m_ChunkedTransfer: 0
m_RedirectLimit: -1
m_RetryCount: 0
m_BuildPath:
m_Id: 2ae6a7549097a9b4caefe44b61b1fcc5
m_LoadPath:
m_Id: 631ec5c677ab8084fb9be9b9ee39c6ef
m_BundleMode: 0
m_AssetBundleProviderType:
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider
m_BundleNaming: 1
m_AssetLoadMode: 0
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 842c6a62aa3abdf4d9b230f249b9fd76
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -1,16 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5834b5087d578d24c926ce20cd31e6d6, type: 3}
m_Name: Localization-Asset-Tables-Japanese (Japan) (ja-JP)_ContentUpdateGroupSchema
m_EditorClassIdentifier:
m_Group: {fileID: 11400000, guid: af94af36950b08d4f9a5037546bbe040, type: 2}
m_StaticContent: 0
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 1706e28ae8e595149b2ac1fbea7f8071
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -12,7 +12,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: e5d17a21594effb4e9591490b009e7aa, type: 3}
m_Name: Localization-Assets-English (en)_BundledAssetGroupSchema
m_EditorClassIdentifier:
m_Group: {fileID: 11400000, guid: 60fd2fd8c7094b242b01c63de65de9ea, type: 2}
m_Group: {fileID: 11400000, guid: aafc8258a2111e94d8339a3152cf50e0, type: 2}
m_InternalBundleIdMode: 1
m_Compression: 1
m_IncludeAddressInCatalog: 1
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: fa74e99b946159549971be154369b2cb
guid: 895c7c97ef5802446a395377ef704940
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
@@ -12,5 +12,5 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 5834b5087d578d24c926ce20cd31e6d6, type: 3}
m_Name: Localization-Assets-English (en)_ContentUpdateGroupSchema
m_EditorClassIdentifier:
m_Group: {fileID: 11400000, guid: 60fd2fd8c7094b242b01c63de65de9ea, type: 2}
m_Group: {fileID: 11400000, guid: aafc8258a2111e94d8339a3152cf50e0, type: 2}
m_StaticContent: 0
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 8e1cbbf76f0150145a39ce648fbf3985
guid: 8004f70cd10ce4a439cb9e822bb078c2
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
@@ -1,45 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e5d17a21594effb4e9591490b009e7aa, type: 3}
m_Name: Localization-String-Tables-Japanese (Japan) (ja-JP)_BundledAssetGroupSchema
m_EditorClassIdentifier:
m_Group: {fileID: 11400000, guid: dc8d31ed80ea35945812ff6552efbbe5, type: 2}
m_InternalBundleIdMode: 1
m_Compression: 1
m_IncludeAddressInCatalog: 1
m_IncludeGUIDInCatalog: 1
m_IncludeLabelsInCatalog: 1
m_InternalIdNamingMode: 0
m_CacheClearBehavior: 0
m_IncludeInBuild: 1
m_BundledAssetProviderType:
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider
m_ForceUniqueProvider: 0
m_UseAssetBundleCache: 1
m_UseAssetBundleCrc: 1
m_UseAssetBundleCrcForCachedBundles: 1
m_UseUWRForLocalBundles: 0
m_Timeout: 0
m_ChunkedTransfer: 0
m_RedirectLimit: -1
m_RetryCount: 0
m_BuildPath:
m_Id: 2ae6a7549097a9b4caefe44b61b1fcc5
m_LoadPath:
m_Id: 631ec5c677ab8084fb9be9b9ee39c6ef
m_BundleMode: 0
m_AssetBundleProviderType:
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider
m_BundleNaming: 1
m_AssetLoadMode: 0
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 7df396a6b5136fe4e9d2d659731161d2
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -1,16 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5834b5087d578d24c926ce20cd31e6d6, type: 3}
m_Name: Localization-String-Tables-Japanese (Japan) (ja-JP)_ContentUpdateGroupSchema
m_EditorClassIdentifier:
m_Group: {fileID: 11400000, guid: dc8d31ed80ea35945812ff6552efbbe5, type: 2}
m_StaticContent: 0
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 545ed8780a6508047bd42b964feb82fd
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -32,11 +32,6 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: b9e730c7e5824c5ca2858a253fe93c98
m_Address: Prefab/FrameAnimationActor
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: cd996317bdc6ae8478acb2de956f25d9
m_Address: Prefab/AnimaMobileObject
m_ReadOnly: 0
@@ -1,550 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-7273616238501396146
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bcae7dc92d05e1e4094e0e448b670355, type: 3}
m_Name: VolumeAsset
m_EditorClassIdentifier:
m_Template:
Weight: 1
m_Volume: {fileID: 11400000, guid: 0af8555a6447b50409db2139400d6ab7, type: 2}
--- !u!114 &-3378792728507138695
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: cd3925730e7a59446932e9c8cba28ccf, type: 3}
m_Name: Volume Track (1)
m_EditorClassIdentifier:
m_Version: 3
m_AnimClip: {fileID: 0}
m_Locked: 0
m_Muted: 0
m_CustomPlayableFullTypename:
m_Curves: {fileID: 0}
m_Parent: {fileID: 11400000}
m_Children: []
m_Clips:
- m_Version: 1
m_Start: 0
m_ClipIn: 0
m_Asset: {fileID: 7967868333749088964}
m_Duration: 0.75
m_TimeScale: 1
m_ParentTrack: {fileID: -3378792728507138695}
m_EaseInDuration: 0.05
m_EaseOutDuration: 0.28
m_BlendInDuration: -1
m_BlendOutDuration: -1
m_MixInCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_MixOutCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_BlendInCurveMode: 0
m_BlendOutCurveMode: 0
m_ExposedParameterNames: []
m_AnimationCurves: {fileID: 0}
m_Recordable: 0
m_PostExtrapolationMode: 0
m_PreExtrapolationMode: 0
m_PostExtrapolationTime: 0
m_PreExtrapolationTime: 0
m_DisplayName: VolumeAsset
m_Markers:
m_Objects: []
m_Layer: 0
m_Priority: 0
--- !u!114 &-3117513502812618692
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: cd3925730e7a59446932e9c8cba28ccf, type: 3}
m_Name: Volume Track
m_EditorClassIdentifier:
m_Version: 3
m_AnimClip: {fileID: 0}
m_Locked: 0
m_Muted: 0
m_CustomPlayableFullTypename:
m_Curves: {fileID: 0}
m_Parent: {fileID: 11400000}
m_Children: []
m_Clips:
- m_Version: 1
m_Start: 0
m_ClipIn: 0
m_Asset: {fileID: -7273616238501396146}
m_Duration: 0.75
m_TimeScale: 1
m_ParentTrack: {fileID: -3117513502812618692}
m_EaseInDuration: 0.05
m_EaseOutDuration: 0.28
m_BlendInDuration: -1
m_BlendOutDuration: -1
m_MixInCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_MixOutCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_BlendInCurveMode: 0
m_BlendOutCurveMode: 0
m_ExposedParameterNames: []
m_AnimationCurves: {fileID: 0}
m_Recordable: 0
m_PostExtrapolationMode: 0
m_PreExtrapolationMode: 0
m_PostExtrapolationTime: 0
m_PreExtrapolationTime: 0
m_DisplayName: VolumeAsset
m_Markers:
m_Objects: []
m_Layer: 0
m_Priority: 0
--- !u!114 &-2955631063214815081
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: cd3925730e7a59446932e9c8cba28ccf, type: 3}
m_Name: Volume Track (2)
m_EditorClassIdentifier:
m_Version: 3
m_AnimClip: {fileID: 0}
m_Locked: 0
m_Muted: 0
m_CustomPlayableFullTypename:
m_Curves: {fileID: 0}
m_Parent: {fileID: 11400000}
m_Children: []
m_Clips:
- m_Version: 1
m_Start: 0
m_ClipIn: 0
m_Asset: {fileID: -1361877534822022742}
m_Duration: 0.75
m_TimeScale: 1
m_ParentTrack: {fileID: -2955631063214815081}
m_EaseInDuration: 0.05
m_EaseOutDuration: 0.28
m_BlendInDuration: -1
m_BlendOutDuration: -1
m_MixInCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_MixOutCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_BlendInCurveMode: 0
m_BlendOutCurveMode: 0
m_ExposedParameterNames: []
m_AnimationCurves: {fileID: 0}
m_Recordable: 0
m_PostExtrapolationMode: 0
m_PreExtrapolationMode: 0
m_PostExtrapolationTime: 0
m_PreExtrapolationTime: 0
m_DisplayName: VolumeAsset
m_Markers:
m_Objects: []
m_Layer: 0
m_Priority: 0
--- !u!114 &-1361877534822022742
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bcae7dc92d05e1e4094e0e448b670355, type: 3}
m_Name: VolumeAsset
m_EditorClassIdentifier:
m_Template:
Weight: 1
m_Volume: {fileID: 11400000, guid: 631b3200517e20440b88f28015fc3a5c, type: 2}
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bfda56da833e2384a9677cd3c976a436, type: 3}
m_Name: "\u706B\u5C71\u5934\u75DB3"
m_EditorClassIdentifier:
m_Version: 0
m_Tracks:
- {fileID: -3117513502812618692}
- {fileID: -3378792728507138695}
- {fileID: -2955631063214815081}
- {fileID: 2112685405309563144}
m_FixedDuration: 0
m_EditorSettings:
m_Framerate: 60
m_ScenePreview: 1
m_DurationMode: 0
m_MarkerTrack: {fileID: 0}
--- !u!74 &1077202520131138910
AnimationClip:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Recorded
serializedVersion: 7
m_Legacy: 0
m_Compressed: 0
m_UseHighQualityCurve: 1
m_RotationCurves: []
m_CompressedRotationCurves: []
m_EulerCurves: []
m_PositionCurves: []
m_ScaleCurves: []
m_FloatCurves:
- serializedVersion: 2
curve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 0.07
value: 0.8
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 0.42
value: 0.8
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 0.68
value: 0
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
attribute: m_Color.a
path:
classID: 212
script: {fileID: 0}
flags: 0
m_PPtrCurves: []
m_SampleRate: 60
m_WrapMode: 0
m_Bounds:
m_Center: {x: 0, y: 0, z: 0}
m_Extent: {x: 0, y: 0, z: 0}
m_ClipBindingConstant:
genericBindings:
- serializedVersion: 2
path: 0
attribute: 304273561
script: {fileID: 0}
typeID: 212
customType: 0
isPPtrCurve: 0
isIntCurve: 0
isSerializeReferenceCurve: 0
pptrCurveMapping: []
m_AnimationClipSettings:
serializedVersion: 2
m_AdditiveReferencePoseClip: {fileID: 0}
m_AdditiveReferencePoseTime: 0
m_StartTime: 0
m_StopTime: 0.68
m_OrientationOffsetY: 0
m_Level: 0
m_CycleOffset: 0
m_HasAdditiveReferencePose: 0
m_LoopTime: 0
m_LoopBlend: 0
m_LoopBlendOrientation: 0
m_LoopBlendPositionY: 0
m_LoopBlendPositionXZ: 0
m_KeepOriginalOrientation: 0
m_KeepOriginalPositionY: 1
m_KeepOriginalPositionXZ: 0
m_HeightFromFeet: 0
m_Mirror: 0
m_EditorCurves:
- serializedVersion: 2
curve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 0.07
value: 0.8
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 0.42
value: 0.8
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 0.68
value: 0
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
attribute: m_Color.a
path:
classID: 212
script: {fileID: 0}
flags: 0
m_EulerEditorCurves: []
m_HasGenericRootTransform: 0
m_HasMotionFloatCurves: 0
m_Events: []
--- !u!114 &2112685405309563144
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d21dcc2386d650c4597f3633c75a1f98, type: 3}
m_Name: Animation Track
m_EditorClassIdentifier:
m_Version: 3
m_AnimClip: {fileID: 0}
m_Locked: 0
m_Muted: 0
m_CustomPlayableFullTypename:
m_Curves: {fileID: 0}
m_Parent: {fileID: 11400000}
m_Children: []
m_Clips: []
m_Markers:
m_Objects: []
m_InfiniteClipPreExtrapolation: 1
m_InfiniteClipPostExtrapolation: 1
m_InfiniteClipOffsetPosition: {x: 0, y: 0, z: 0}
m_InfiniteClipOffsetEulerAngles: {x: 0, y: 0, z: 0}
m_InfiniteClipTimeOffset: 0
m_InfiniteClipRemoveOffset: 0
m_InfiniteClipApplyFootIK: 1
mInfiniteClipLoop: 0
m_MatchTargetFields: 63
m_Position: {x: 0, y: 0, z: 0}
m_EulerAngles: {x: 0, y: 0, z: 0}
m_AvatarMask: {fileID: 0}
m_ApplyAvatarMask: 1
m_TrackOffset: 0
m_InfiniteClip: {fileID: 1077202520131138910}
m_OpenClipOffsetRotation: {x: 0, y: 0, z: 0, w: 1}
m_Rotation: {x: 0, y: 0, z: 0, w: 1}
m_ApplyOffsets: 0
--- !u!114 &7967868333749088964
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bcae7dc92d05e1e4094e0e448b670355, type: 3}
m_Name: VolumeAsset
m_EditorClassIdentifier:
m_Template:
Weight: 1
m_Volume: {fileID: 11400000, guid: 841479fd6ffe3ee40a88ee41bd395f09, type: 2}
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 58d09572ecd54714a787a1fc5aec6c5f
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b51c643e2acc12549a81d17c232253a5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e274bde4016eca842be36e589f6285f6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.
@@ -244,14 +244,3 @@ MonoBehaviour:
m_PreInfinity: 0
m_PostInfinity: 0
m_RotationOrder: 0
- m_From: '**ANY CAMERA**'
m_To: Dream Camera
m_Blend:
m_Style: 0
m_Time: 2
m_CustomCurve:
serializedVersion: 2
m_Curve: []
m_PreInfinity: 0
m_PostInfinity: 0
m_RotationOrder: 0
-8
View File
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 638cd3fa1c2470d42aceb69214c01786
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,260 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using AibisDream.Kit;
using AibisDream.SaveSystem;
using FMOD;
using FMOD.Studio;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
namespace AibisDream.SystemEditor.Tests
{
public sealed class AudioGlobalParameterSnapshotTests
{
[Test]
public void SetFromYarn_TracksOnlySuccessfulUniqueNames()
{
var api = new FakeFmodGlobalParameterApi();
api.SetByNameResults["missing"] = RESULT.ERR_EVENT_NOTFOUND;
var registry = new YarnGlobalParameterRegistry(api);
Assert.That(registry.SetFromYarn("reverb", 0.4f), Is.EqualTo(RESULT.OK));
Assert.That(registry.SetFromYarn("REVERB", 0.7f), Is.EqualTo(RESULT.OK));
Assert.That(
registry.SetFromYarn("missing", 1f),
Is.EqualTo(RESULT.ERR_EVENT_NOTFOUND));
Assert.That(registry.TrackedNames, Has.Count.EqualTo(1));
Assert.That(registry.TrackedNames.Single(), Is.EqualTo("reverb"));
Assert.That(api.SetByNameCalls[0].IgnoreSeekSpeed, Is.False);
Assert.That(api.SetByNameCalls[1].IgnoreSeekSpeed, Is.False);
}
[Test]
public void CaptureFinalValues_ReadsFmodFinalValueAndRetainsFailuresForRetry()
{
var api = new FakeFmodGlobalParameterApi();
api.Reads["stage"] = new ParameterRead(9f, 2.37f, RESULT.OK);
api.Reads["missing"] = new ParameterRead(0f, 0f, RESULT.ERR_EVENT_NOTFOUND);
var registry = new YarnGlobalParameterRegistry(api);
registry.SetFromYarn("stage", 9f);
registry.SetFromYarn("missing", 1f);
var warnings = new List<string>();
var snapshot = registry.CaptureFinalValues(warnings.Add);
Assert.That(snapshot, Has.Count.EqualTo(1));
Assert.That(snapshot["stage"], Is.EqualTo(2.37f));
Assert.That(snapshot["stage"], Is.Not.EqualTo(9f));
Assert.That(registry.TrackedNames, Is.EquivalentTo(new[] { "stage", "missing" }));
Assert.That(warnings, Has.Count.EqualTo(1));
StringAssert.Contains("missing", warnings[0]);
}
[Test]
public void ResetToDefaults_RemovesOnlyParametersThatFmodResetSuccessfully()
{
var goodId = new PARAMETER_ID { data1 = 1, data2 = 2 };
var failedId = new PARAMETER_ID { data1 = 3, data2 = 4 };
var api = new FakeFmodGlobalParameterApi();
api.Descriptions["good"] = new ParameterDescriptionRead(
new PARAMETER_DESCRIPTION { id = goodId, defaultvalue = 0.25f },
RESULT.OK);
api.Descriptions["failed"] = new ParameterDescriptionRead(
new PARAMETER_DESCRIPTION { id = failedId, defaultvalue = 0.75f },
RESULT.OK);
api.SetByIdResults[failedId] = RESULT.ERR_INVALID_PARAM;
var registry = new YarnGlobalParameterRegistry(api);
registry.SetFromYarn("good", 1f);
registry.SetFromYarn("failed", 1f);
var warnings = new List<string>();
registry.ResetToDefaults(warnings.Add);
Assert.That(registry.TrackedNames, Is.EquivalentTo(new[] { "failed" }));
Assert.That(api.SetByIdCalls, Has.Count.EqualTo(2));
var goodReset = api.SetByIdCalls.Single(call => call.Id.Equals(goodId));
Assert.That(goodReset.Value, Is.EqualTo(0.25f));
Assert.That(goodReset.IgnoreSeekSpeed, Is.True);
Assert.That(warnings, Has.Count.EqualTo(1));
StringAssert.Contains("failed", warnings[0]);
}
[Test]
public void RestoreFinalValues_JumpsImmediatelyThenNextYarnTargetUsesSeekSpeed()
{
var api = new FakeFmodGlobalParameterApi();
api.SetByNameResults["missing"] = RESULT.ERR_EVENT_NOTFOUND;
var registry = new YarnGlobalParameterRegistry(api);
var warnings = new List<string>();
registry.RestoreFinalValues(
new Dictionary<string, float>
{
["stage"] = 2.37f,
["missing"] = 1f
},
warnings.Add);
registry.SetFromYarn("stage", 3f);
Assert.That(api.SetByNameCalls, Has.Count.EqualTo(3));
Assert.That(api.SetByNameCalls[0].Value, Is.EqualTo(2.37f));
Assert.That(api.SetByNameCalls[0].IgnoreSeekSpeed, Is.True);
Assert.That(api.SetByNameCalls[1].Name, Is.EqualTo("missing"));
Assert.That(api.SetByNameCalls[1].IgnoreSeekSpeed, Is.True);
Assert.That(api.SetByNameCalls[2].Value, Is.EqualTo(3f));
Assert.That(api.SetByNameCalls[2].IgnoreSeekSpeed, Is.False);
Assert.That(registry.TrackedNames, Is.EquivalentTo(new[] { "stage" }));
Assert.That(warnings, Has.Count.EqualTo(1));
StringAssert.Contains("missing", warnings[0]);
}
[Test]
public void AudioSnapshotDto_RoundTripsFinalValuesWithoutChangingSchema()
{
var source = new SaveSnapshot();
source.sections[SnapshotProviderIds.Audio] = new AudioSnapshotDto
{
ambState = "Dream",
yarnGlobalParameters = new Dictionary<string, float>
{
["reverb"] = 0.42f,
["hs1LogStage"] = 2.37f
}
};
var json = JsonConvert.SerializeObject(source);
var restored = JsonConvert.DeserializeObject<SaveSnapshot>(json);
var audio = ((JObject)restored.sections[SnapshotProviderIds.Audio])
.ToObject<AudioSnapshotDto>();
Assert.That(restored.schemaVersion, Is.EqualTo(2));
Assert.That(SaveSnapshotSchema.CurrentVersion, Is.EqualTo(2));
Assert.That(audio.yarnGlobalParameters["reverb"], Is.EqualTo(0.42f));
Assert.That(audio.yarnGlobalParameters["hs1LogStage"], Is.EqualTo(2.37f));
}
private readonly struct SetByNameCall
{
internal SetByNameCall(string name, float value, bool ignoreSeekSpeed)
{
Name = name;
Value = value;
IgnoreSeekSpeed = ignoreSeekSpeed;
}
internal string Name { get; }
internal float Value { get; }
internal bool IgnoreSeekSpeed { get; }
}
private readonly struct SetByIdCall
{
internal SetByIdCall(PARAMETER_ID id, float value, bool ignoreSeekSpeed)
{
Id = id;
Value = value;
IgnoreSeekSpeed = ignoreSeekSpeed;
}
internal PARAMETER_ID Id { get; }
internal float Value { get; }
internal bool IgnoreSeekSpeed { get; }
}
private readonly struct ParameterRead
{
internal ParameterRead(float value, float finalValue, RESULT result)
{
Value = value;
FinalValue = finalValue;
Result = result;
}
internal float Value { get; }
internal float FinalValue { get; }
internal RESULT Result { get; }
}
private readonly struct ParameterDescriptionRead
{
internal ParameterDescriptionRead(
PARAMETER_DESCRIPTION description,
RESULT result)
{
Description = description;
Result = result;
}
internal PARAMETER_DESCRIPTION Description { get; }
internal RESULT Result { get; }
}
private sealed class FakeFmodGlobalParameterApi : IFmodGlobalParameterApi
{
internal readonly Dictionary<string, RESULT> SetByNameResults =
new(System.StringComparer.OrdinalIgnoreCase);
internal readonly Dictionary<string, ParameterRead> Reads =
new(System.StringComparer.OrdinalIgnoreCase);
internal readonly Dictionary<string, ParameterDescriptionRead> Descriptions =
new(System.StringComparer.OrdinalIgnoreCase);
internal readonly Dictionary<PARAMETER_ID, RESULT> SetByIdResults = new();
internal readonly List<SetByNameCall> SetByNameCalls = new();
internal readonly List<SetByIdCall> SetByIdCalls = new();
public RESULT SetParameterByName(
string name,
float value,
bool ignoreSeekSpeed)
{
SetByNameCalls.Add(new SetByNameCall(name, value, ignoreSeekSpeed));
return SetByNameResults.TryGetValue(name, out var result)
? result
: RESULT.OK;
}
public RESULT GetParameterByName(
string name,
out float value,
out float finalValue)
{
if (Reads.TryGetValue(name, out var read))
{
value = read.Value;
finalValue = read.FinalValue;
return read.Result;
}
value = 0f;
finalValue = 0f;
return RESULT.OK;
}
public RESULT GetParameterDescriptionByName(
string name,
out PARAMETER_DESCRIPTION description)
{
if (Descriptions.TryGetValue(name, out var read))
{
description = read.Description;
return read.Result;
}
description = default;
return RESULT.ERR_EVENT_NOTFOUND;
}
public RESULT SetParameterById(
PARAMETER_ID id,
float value,
bool ignoreSeekSpeed)
{
SetByIdCalls.Add(new SetByIdCall(id, value, ignoreSeekSpeed));
return SetByIdResults.TryGetValue(id, out var result)
? result
: RESULT.OK;
}
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: e120d9cfce5a4723a6e640b3cc3ce29b
-123
View File
@@ -1,123 +0,0 @@
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
using AibisDream.Utility;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using Yarn.Markup;
using Yarn.Unity;
namespace AibisDream.SystemEditor.Tests
{
public sealed class AutoNextMetadataTests
{
[Test]
public void AutoNextWithoutParameter_UsesExistingFixedDelay()
{
var lineInfo = LineInfo.Generate(CreateLine("auto_next"));
Assert.That(lineInfo.isAutoSkip, Is.True);
Assert.That(lineInfo.autoNextDelaySeconds, Is.Null);
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(ConstRef.FixedDelay));
}
[Test]
public void AutoNextWithParameter_UsesSpecifiedSeconds()
{
var lineInfo = LineInfo.Generate(CreateLine("auto_next:4.5"));
Assert.That(lineInfo.isAutoSkip, Is.True);
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(4.5f));
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(4500));
}
[Test]
public void AutoNextWithZeroDelay_IsValid()
{
var lineInfo = LineInfo.Generate(CreateLine("auto_next:0"));
Assert.That(lineInfo.isAutoSkip, Is.True);
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(0f));
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.Zero);
}
[Test]
public void AutoNextParameter_UsesInvariantCulture()
{
var originalCulture = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = new CultureInfo("de-DE");
var lineInfo = LineInfo.Generate(CreateLine("auto_next:4.5"));
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(4.5f));
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(4500));
}
finally
{
CultureInfo.CurrentCulture = originalCulture;
}
}
[TestCase("auto_next:")]
[TestCase("auto_next:abc")]
[TestCase("auto_next:-1")]
[TestCase("auto_next:NaN")]
[TestCase("auto_next:Infinity")]
[TestCase("auto_next:2147484")]
public void AutoNextWithInvalidParameter_FallsBackToExistingFixedDelay(string metadata)
{
LogAssert.Expect(LogType.Warning, new Regex("auto_next 参数无效"));
var lineInfo = LineInfo.Generate(CreateLine(metadata));
Assert.That(lineInfo.isAutoSkip, Is.True);
Assert.That(lineInfo.autoNextDelaySeconds, Is.Null);
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(ConstRef.FixedDelay));
}
[TestCase("auto_next_extra")]
[TestCase("auto_next_extra:4.5")]
[TestCase("AUTO_NEXT")]
[TestCase("AUTO_NEXT:4.5")]
public void SimilarMetadata_IsNotRecognized(string metadata)
{
var line = CreateLine(metadata);
Assert.That(line.IsAutoSkipLine(), Is.False);
Assert.That(line.TryGetAutoNextDelaySeconds(out _), Is.False);
}
[Test]
public void ParameterizedTag_TakesPrecedenceOverPlainTag()
{
var lineInfo = LineInfo.Generate(CreateLine("auto_next", "auto_next:2.5"));
Assert.That(lineInfo.isAutoSkip, Is.True);
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(2.5f));
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(2500));
}
[Test]
public void MultipleParameterizedTags_UseFirstAndWarn()
{
LogAssert.Expect(LogType.Warning, new Regex("存在多个 auto_next 参数标签"));
var lineInfo = LineInfo.Generate(CreateLine("auto_next:2.5", "auto_next:4.5"));
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(2.5f));
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(2500));
}
private static LocalizedLine CreateLine(params string[] metadata)
{
return new LocalizedLine
{
TextID = "line:auto-next-test",
Metadata = metadata,
Text = new MarkupParseResult("Test line", new List<MarkupAttribute>())
};
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: d476e493adf4855468ce46409dae6661
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
-8
View File
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 09d2581fcb827614bba6bf5c3279358e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,277 +0,0 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AibisDream;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace AibisDream.SystemEditor
{
public class ChapterGraphEditorWindow : EditorWindow
{
private ChapterGraphView _graphView;
private ChapterGraphInspector _inspector;
private IMGUIContainer _inspectorContainer;
private const string RootFolderPrefKey = "AibisDream.ChapterGraph.RootFolder";
private const string CurrentFolderPrefKey = "AibisDream.ChapterGraph.CurrentFolder";
private string _rootFolder;
private string _currentFolder;
private DropdownField _folderDropdown;
private readonly Dictionary<string, string> _displayToPath = new();
[MenuItem(AibisEditorMenus.ChapterGraphEditor)]
public static void ShowWindow()
{
var window = GetWindow<ChapterGraphEditorWindow>("Chapter Graph Editor");
window.minSize = new Vector2(800, 600);
}
private void OnEnable()
{
if (_graphView == null) return;
_graphView.LoadGraph(_currentFolder);
}
private void OnDisable()
{
if (_graphView != null)
{
_graphView.OnNodeDeleted -= OnNodeDeleted;
}
if (_inspector != null)
{
_inspector.OnColorChanged -= OnNodeColorChanged;
_inspector.Destroy();
}
}
private void CreateGUI()
{
// 根容器:纵向排列
var root = rootVisualElement;
root.style.flexDirection = FlexDirection.Column;
// 顶部工具栏
var toolbar = new Toolbar();
toolbar.Add(CreateToolbarButton("Save", OnSaveClicked));
toolbar.Add(CreateToolbarButton("Auto Layout", OnAutoLayoutClicked));
toolbar.Add(CreateToolbarButton("Validate", OnValidateClicked));
_rootFolder = EditorPrefs.GetString(RootFolderPrefKey, "Assets/ScriptableObjects/SceneSO");
_folderDropdown = new DropdownField("路径");
_folderDropdown.style.width = 160;
_folderDropdown.style.marginLeft = 8;
_folderDropdown.style.marginRight = 4;
RefreshFolderChoices();
_folderDropdown.RegisterValueChangedCallback(OnFolderChanged);
toolbar.Add(_folderDropdown);
var setRootBtn = new Button(OnSetRootFolderClicked) { text = "..." };
setRootBtn.style.width = 28;
toolbar.Add(setRootBtn);
toolbar.Add(new ToolbarSpacer() { style = { flexGrow = 1 } });
toolbar.Add(CreateToolbarButton("Create Chapter", OnCreateChapterClicked));
root.Add(toolbar);
// 主内容区:横向分栏
var mainContainer = new VisualElement();
mainContainer.style.flexGrow = 1;
mainContainer.style.flexDirection = FlexDirection.Row;
mainContainer.style.overflow = Overflow.Hidden;
// 左侧 Inspector 面板
var leftPanel = new VisualElement();
leftPanel.style.width = 320;
leftPanel.style.minWidth = 200;
leftPanel.style.maxWidth = 500;
leftPanel.style.flexShrink = 0;
leftPanel.style.flexDirection = FlexDirection.Column;
leftPanel.style.borderRightWidth = 1;
leftPanel.style.borderRightColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f));
// Inspector 标题
var header = new Label("章节属性")
{
style =
{
unityFontStyleAndWeight = FontStyle.Bold,
fontSize = 14,
paddingTop = 8,
paddingBottom = 8,
paddingLeft = 12,
paddingRight = 12,
borderBottomWidth = 1,
borderBottomColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f))
}
};
leftPanel.Add(header);
// IMGUI Inspector
_inspector = new ChapterGraphInspector();
_inspectorContainer = new IMGUIContainer(() => _inspector.OnGUI());
_inspectorContainer.style.flexGrow = 1;
leftPanel.Add(_inspectorContainer);
mainContainer.Add(leftPanel);
// 拖拽条
var resizer = new VisualElement();
resizer.style.width = 5;
resizer.style.backgroundColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f));
mainContainer.Add(resizer);
float startWidth = 0;
float startMouseX = 0;
resizer.RegisterCallback<MouseDownEvent>(evt =>
{
startWidth = leftPanel.resolvedStyle.width;
startMouseX = evt.mousePosition.x;
resizer.CaptureMouse();
evt.StopPropagation();
});
resizer.RegisterCallback<MouseMoveEvent>(evt =>
{
if (!resizer.HasMouseCapture()) return;
var delta = evt.mousePosition.x - startMouseX;
var newWidth = Mathf.Clamp(startWidth + delta, 200, 600);
leftPanel.style.width = newWidth;
});
resizer.RegisterCallback<MouseUpEvent>(evt =>
{
if (resizer.HasMouseCapture())
resizer.ReleaseMouse();
});
// 右侧 GraphView
// 套一层容器:GraphView 与左侧 Inspector 并列放在 Flex Row 中时,
// 内置 RectangleSelector 绘制的框选矩形会出现坐标偏移(已知 Unity 问题)。
// 将 GraphView 放在独立的 Flex 子容器中可使其局部原点与父容器对齐,规避该偏移。
var graphViewWrapper = new VisualElement();
graphViewWrapper.style.flexGrow = 1;
graphViewWrapper.style.flexDirection = FlexDirection.Column;
graphViewWrapper.style.overflow = Overflow.Hidden;
_graphView = new ChapterGraphView();
_graphView.style.flexGrow = 1;
graphViewWrapper.Add(_graphView);
mainContainer.Add(graphViewWrapper);
root.Add(mainContainer);
// 加载图数据
_graphView.LoadGraph(_currentFolder);
// 选中节点同步到 Inspector
_graphView.OnNodeSelected += OnNodeSelected;
_graphView.OnNodeDeleted += OnNodeDeleted;
_inspector.OnColorChanged += OnNodeColorChanged;
}
private void OnNodeDeleted(TalkSceneSO so)
{
_inspector?.SetTarget(null);
_inspectorContainer?.MarkDirtyRepaint();
}
private void OnNodeColorChanged(TalkSceneSO so)
{
_graphView?.UpdateNodeColor(so);
}
private Button CreateToolbarButton(string text, System.Action onClick)
{
var btn = new Button(onClick) { text = text };
return btn;
}
private void OnSaveClicked()
{
AssetDatabase.SaveAssets();
EditorUtility.DisplayDialog("保存", "所有资源已保存。", "确定");
}
private void OnAutoLayoutClicked()
{
_graphView?.AutoLayout();
}
private void OnValidateClicked()
{
_graphView?.ValidateGraph();
}
private void OnCreateChapterClicked()
{
var center = new Vector2(position.width / 2f, position.height / 2f);
var localPos = _graphView.contentViewContainer.WorldToLocal(center);
var defaultFolder = _currentFolder ?? EditorPrefs.GetString(ChapterGraphView.DefaultFolderPrefKey, _rootFolder);
_graphView?.CreateChapterNodeAt(localPos, defaultFolder);
}
private void OnNodeSelected(TalkSceneSO so)
{
_inspector?.SetTarget(so);
_inspectorContainer?.MarkDirtyRepaint();
}
private void RefreshFolderChoices()
{
_displayToPath.Clear();
_displayToPath["All"] = null;
var subFolders = AssetDatabase.GetSubFolders(_rootFolder);
foreach (var folder in subFolders)
{
var name = Path.GetFileName(folder);
_displayToPath[name] = folder;
}
var rootHasSO = AssetDatabase.FindAssets("t:TalkSceneSO", new[] { _rootFolder })
.Select(g => AssetDatabase.GUIDToAssetPath(g))
.Any(path => Path.GetDirectoryName(path)?.Replace('\\', '/') == _rootFolder);
if (rootHasSO)
{
_displayToPath["(Root)"] = _rootFolder;
}
_folderDropdown.choices = _displayToPath.Keys.ToList();
var saved = EditorPrefs.GetString(CurrentFolderPrefKey, "All");
_folderDropdown.value = _displayToPath.ContainsKey(saved) ? saved : "All";
_currentFolder = _displayToPath.TryGetValue(_folderDropdown.value, out var path) ? path : null;
}
private void OnFolderChanged(ChangeEvent<string> evt)
{
_currentFolder = _displayToPath.TryGetValue(evt.newValue, out var path) ? path : null;
EditorPrefs.SetString(CurrentFolderPrefKey, evt.newValue);
_graphView?.LoadGraph(_currentFolder);
}
private void OnSetRootFolderClicked()
{
var path = EditorUtility.OpenFolderPanel("选择章节根目录", _rootFolder, "");
if (string.IsNullOrEmpty(path)) return;
var absoluteRoot = Application.dataPath.Replace('\\', '/');
var selected = path.Replace('\\', '/');
if (!selected.StartsWith(absoluteRoot))
{
EditorUtility.DisplayDialog("错误", "请选择项目 Assets 目录内的文件夹。", "确定");
return;
}
_rootFolder = "Assets" + selected.Substring(absoluteRoot.Length);
EditorPrefs.SetString(RootFolderPrefKey, _rootFolder);
RefreshFolderChoices();
_graphView?.LoadGraph(_currentFolder);
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: c30bb2152169f544cb672c29ac8cb5d1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,70 +0,0 @@
using AibisDream;
using UnityEditor;
using UnityEngine;
namespace AibisDream.SystemEditor
{
public class ChapterGraphInspector
{
private Editor _editor;
private TalkSceneSO _target;
private Vector2 _scrollPosition;
public event System.Action<TalkSceneSO> OnColorChanged;
public void SetTarget(TalkSceneSO so)
{
// 不能用 ==,Unity 重载了 ==,已销毁的对象 == null 为 true
if (ReferenceEquals(_target, so)) return;
_target = so;
if (_editor != null)
{
Object.DestroyImmediate(_editor);
_editor = null;
}
if (so != null)
{
_editor = Editor.CreateEditor(so);
}
}
public void OnGUI()
{
if (_editor == null)
{
EditorGUILayout.HelpBox("请在图中选中一个章节节点以编辑属性", MessageType.Info);
return;
}
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
EditorGUI.BeginChangeCheck();
var newColor = EditorGUILayout.ColorField("节点颜色", _target.nodeColor);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(_target, "Change Node Color");
_target.nodeColor = newColor;
EditorUtility.SetDirty(_target);
OnColorChanged?.Invoke(_target);
}
EditorGUILayout.Space(6);
_editor.OnInspectorGUI();
EditorGUILayout.EndScrollView();
}
public void Destroy()
{
if (_editor != null)
{
Object.DestroyImmediate(_editor);
_editor = null;
}
_target = null;
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 9b122b6f926732f46b067d9b26eb191b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,503 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AibisDream;
using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;
namespace AibisDream.SystemEditor
{
public class ChapterGraphView : GraphView
{
public event Action<TalkSceneSO> OnNodeSelected;
public event Action<TalkSceneSO> OnNodeDeleted;
private const float HorizontalSpacing = 420f;
private const float VerticalSpacing = 220f;
public const string DefaultFolderPrefKey = "AibisDream.ChapterGraph.DefaultFolder";
public const string DefaultFolder = "Assets/ScriptableObjects/SceneSO";
public ChapterGraphView()
{
style.flexGrow = 1;
SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);
this.AddManipulator(new ContentDragger());
this.AddManipulator(new SelectionDragger());
this.AddManipulator(new RectangleSelector());
var grid = new GridBackground();
Insert(0, grid);
grid.StretchToParentSize();
graphViewChanged += OnGraphViewChanged;
}
public void UpdateNodeColor(TalkSceneSO so)
{
foreach (var node in graphElements.OfType<ChapterNode>())
{
if (node.SceneSo == so)
{
node.UpdateColor();
break;
}
}
}
public void LoadGraph(string folderPath = null)
{
ClearGraph();
var guids = AssetDatabase.FindAssets("t:TalkSceneSO", folderPath != null ? new[] { folderPath } : null);
var allSos = guids.Select(g => AssetDatabase.LoadAssetAtPath<TalkSceneSO>(AssetDatabase.GUIDToAssetPath(g))).Where(so => so != null).ToList();
var nodeMap = new Dictionary<TalkSceneSO, ChapterNode>();
// 创建节点
foreach (var so in allSos)
{
var node = new ChapterNode(so);
AddElement(node);
nodeMap[so] = node;
}
// 创建连线
foreach (var so in allSos)
{
if (so.exits == null) continue;
if (!nodeMap.TryGetValue(so, out var sourceNode)) continue;
for (int i = 0; i < so.exits.Count; i++)
{
var exit = so.exits[i];
if (exit == null || exit.targetScene == null) continue;
if (!nodeMap.TryGetValue(exit.targetScene, out var targetNode)) continue;
var outputPort = sourceNode.GetOutputPort(i);
var inputPort = targetNode.InputPort;
if (outputPort == null || inputPort == null) continue;
var edge = new Edge
{
output = outputPort,
input = inputPort
};
edge.output.Connect(edge);
edge.input.Connect(edge);
AddElement(edge);
}
}
}
public void ClearGraph()
{
// 清除所有元素
var elementsToRemove = graphElements.ToList();
foreach (var element in elementsToRemove)
{
RemoveElement(element);
}
}
public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
{
base.BuildContextualMenu(evt);
if (evt.target is GraphView)
{
var mousePosition = contentViewContainer.WorldToLocal(evt.mousePosition);
evt.menu.AppendAction("添加章节节点", (_) => CreateChapterNodeAt(mousePosition));
}
}
public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
{
var compatiblePorts = new List<Port>();
var startNode = startPort.node as ChapterNode;
if (startNode == null) return compatiblePorts;
ports.ForEach(port =>
{
var targetNode = port.node as ChapterNode;
if (targetNode == null) return;
if (targetNode == startNode) return;
if (startPort.direction == port.direction) return;
if (startPort.node == port.node) return;
compatiblePorts.Add(port);
});
return compatiblePorts;
}
private GraphViewChange OnGraphViewChanged(GraphViewChange change)
{
// 先处理节点删除确认:用户取消则从移除列表中剔除
if (change.elementsToRemove != null)
{
var nodesToRemove = change.elementsToRemove.OfType<ChapterNode>().ToList();
foreach (var node in nodesToRemove)
{
if (!ConfirmRemoveNode(node))
{
change.elementsToRemove.Remove(node);
// 同时保留与该节点相连的边,避免边被单独删掉
var edgesToKeep = change.elementsToRemove.OfType<Edge>()
.Where(e => e.output?.node == node || e.input?.node == node)
.ToList();
foreach (var edge in edgesToKeep)
{
change.elementsToRemove.Remove(edge);
}
}
}
}
// 处理 Edge 删除
if (change.elementsToRemove != null)
{
foreach (var edge in change.elementsToRemove.OfType<Edge>())
{
HandleEdgeRemoved(edge);
}
foreach (var node in change.elementsToRemove.OfType<ChapterNode>())
{
HandleNodeRemoved(node);
}
}
// 处理 Edge 创建
if (change.edgesToCreate != null)
{
foreach (var edge in change.edgesToCreate)
{
HandleEdgeCreated(edge);
}
}
// 处理节点移动
if (change.movedElements != null)
{
foreach (var element in change.movedElements)
{
if (element is ChapterNode node)
{
node.SyncPositionToSo();
}
}
}
return change;
}
private void HandleEdgeCreated(Edge edge)
{
if (!(edge.output?.node is ChapterNode sourceNode)) return;
if (!(edge.input?.node is ChapterNode targetNode)) return;
var sourceSo = sourceNode.SceneSo;
var targetSo = targetNode.SceneSo;
if (sourceSo == null || targetSo == null) return;
int exitIndex = edge.output.userData is int idx ? idx : -1;
if (exitIndex < 0 || exitIndex >= sourceSo.exits.Count) return;
Undo.RecordObject(sourceSo, "Connect Chapter");
sourceSo.exits[exitIndex].targetScene = targetSo;
EditorUtility.SetDirty(sourceSo);
}
private void HandleEdgeRemoved(Edge edge)
{
if (!(edge.output?.node is ChapterNode sourceNode)) return;
var sourceSo = sourceNode.SceneSo;
if (sourceSo == null) return;
int exitIndex = edge.output.userData is int idx ? idx : -1;
if (exitIndex < 0 || exitIndex >= sourceSo.exits.Count) return;
Undo.RecordObject(sourceSo, "Disconnect Chapter");
sourceSo.exits[exitIndex].targetScene = null;
EditorUtility.SetDirty(sourceSo);
}
private bool ConfirmRemoveNode(ChapterNode node)
{
var so = node.SceneSo;
if (so == null) return true;
var referrers = new List<string>();
foreach (var other in graphElements.OfType<ChapterNode>())
{
if (other == node) continue;
if (other.SceneSo == null || other.SceneSo.exits == null) continue;
foreach (var exit in other.SceneSo.exits)
{
if (exit.targetScene == so)
{
referrers.Add(string.IsNullOrEmpty(other.SceneSo.title) ? other.SceneSo.name : other.SceneSo.title);
break;
}
}
}
var name = string.IsNullOrEmpty(so.title) ? so.name : so.title;
var msg = $"确定要删除章节节点 \"{name}\" 吗?";
if (referrers.Count > 0)
{
msg += $"\n\n以下节点仍引用该节点:\n• {string.Join("\n ", referrers)}";
}
return EditorUtility.DisplayDialog("删除确认", msg, "删除", "取消");
}
private void HandleNodeRemoved(ChapterNode node)
{
var so = node.SceneSo;
if (so == null) return;
var path = AssetDatabase.GetAssetPath(so);
if (!string.IsNullOrEmpty(path))
{
AssetDatabase.DeleteAsset(path);
}
OnNodeDeleted?.Invoke(so);
}
public override void AddToSelection(ISelectable selectable)
{
base.AddToSelection(selectable);
NotifySelectionChanged();
}
public override void RemoveFromSelection(ISelectable selectable)
{
base.RemoveFromSelection(selectable);
NotifySelectionChanged();
}
public override void ClearSelection()
{
base.ClearSelection();
NotifySelectionChanged();
}
private void NotifySelectionChanged()
{
var selectedNode = selection.OfType<ChapterNode>().FirstOrDefault();
OnNodeSelected?.Invoke(selectedNode?.SceneSo);
}
public void CreateChapterNodeAt(Vector2 position, string defaultFolder = null)
{
var folder = defaultFolder ?? EditorPrefs.GetString(DefaultFolderPrefKey, DefaultFolder);
var path = EditorUtility.SaveFilePanelInProject("新建章节", "NewChapter", "asset", "选择新章节的保存位置", folder);
if (string.IsNullOrEmpty(path)) return;
var so = ScriptableObject.CreateInstance<TalkSceneSO>();
so.name = System.IO.Path.GetFileNameWithoutExtension(path);
so.title = "新章节";
so.nodeColor = new Color(0.85f, 0.45f, 0.45f);
so.graphPosition = position;
so.exits = new System.Collections.Generic.List<SceneExit>();
AssetDatabase.CreateAsset(so, path);
EditorUtility.SetDirty(so);
AssetDatabase.SaveAssets();
// 记住文件夹
var savedFolder = System.IO.Path.GetDirectoryName(path)?.Replace('\\', '/');
if (!string.IsNullOrEmpty(savedFolder))
{
EditorPrefs.SetString(DefaultFolderPrefKey, savedFolder);
}
var node = new ChapterNode(so);
AddElement(node);
}
public void AutoLayout()
{
var nodes = graphElements.OfType<ChapterNode>().ToList();
if (nodes.Count == 0) return;
// 计算入度
var inDegree = nodes.ToDictionary(n => n, _ => 0);
var edges = graphElements.OfType<Edge>().ToList();
foreach (var edge in edges)
{
if (edge.input?.node is ChapterNode target && inDegree.ContainsKey(target))
{
inDegree[target]++;
}
}
// 分层 BFS
var layer = new Dictionary<ChapterNode, int>();
var queue = new Queue<ChapterNode>();
// 入度为 0 的节点作为起点
foreach (var node in nodes)
{
if (inDegree[node] == 0)
{
layer[node] = 0;
queue.Enqueue(node);
}
}
// 如果没有入度为 0 的节点(纯循环),任选一个作为起点
if (queue.Count == 0)
{
layer[nodes[0]] = 0;
queue.Enqueue(nodes[0]);
}
while (queue.Count > 0)
{
var current = queue.Dequeue();
var currentLayer = layer[current];
foreach (var edge in edges)
{
if (edge.output?.node == current && edge.input?.node is ChapterNode next)
{
if (!layer.ContainsKey(next))
{
layer[next] = currentLayer + 1;
queue.Enqueue(next);
}
else
{
layer[next] = Math.Max(layer[next], currentLayer + 1);
}
}
}
}
// 为未分配层级的节点(孤立或不连通部分)分配层级
foreach (var node in nodes)
{
if (!layer.ContainsKey(node))
{
layer[node] = 0;
}
}
// 按层级分组并计算位置
var groups = layer.GroupBy(kvp => kvp.Value).OrderBy(g => g.Key).ToList();
Undo.RecordObjects(nodes.Select(n => n.SceneSo).ToArray(), "Auto Layout");
for (int i = 0; i < groups.Count; i++)
{
var group = groups[i];
var groupNodes = group.ToList();
float x = group.Key * HorizontalSpacing;
float startY = -(groupNodes.Count - 1) * VerticalSpacing / 2f;
for (int j = 0; j < groupNodes.Count; j++)
{
var node = groupNodes[j].Key;
var y = startY + j * VerticalSpacing;
var newPos = new Vector2(x, y);
node.SetPosition(new Rect(newPos, Vector2.zero));
node.SceneSo.graphPosition = newPos;
EditorUtility.SetDirty(node.SceneSo);
}
}
}
public void ValidateGraph()
{
var nodes = graphElements.OfType<ChapterNode>().ToList();
var issues = new List<string>();
var soSet = new HashSet<TalkSceneSO>(nodes.Select(n => n.SceneSo));
// 孤立节点
foreach (var node in nodes)
{
var so = node.SceneSo;
bool hasIncoming = graphElements.OfType<Edge>().Any(e => e.input?.node == node);
bool hasOutgoing = so.exits != null && so.exits.Any(e => e.targetScene != null);
if (!hasIncoming && !hasOutgoing)
{
issues.Add($"孤立节点: {so.name} (没有入边也没有出边)");
}
}
// 循环引用 (DFS)
var visited = new HashSet<TalkSceneSO>();
var visiting = new HashSet<TalkSceneSO>();
foreach (var so in soSet)
{
if (!visited.Contains(so))
{
CheckCycle(so, visited, visiting, soSet, issues);
}
}
// 空 exitName
foreach (var node in nodes)
{
var so = node.SceneSo;
if (so.exits == null) continue;
for (int i = 0; i < so.exits.Count; i++)
{
if (string.IsNullOrEmpty(so.exits[i].exitName))
{
issues.Add($"空 exitName: {so.name} 的第 {i + 1} 个出口");
}
}
}
if (issues.Count == 0)
{
EditorUtility.DisplayDialog("验证结果", "图表验证通过,未发现问题。", "确定");
}
else
{
var msg = string.Join("\n", issues.Take(20));
if (issues.Count > 20)
{
msg += $"\n... 还有 {issues.Count - 20} 个问题";
}
EditorUtility.DisplayDialog($"验证发现 {issues.Count} 个问题", msg, "确定");
}
}
private void CheckCycle(TalkSceneSO current, HashSet<TalkSceneSO> visited, HashSet<TalkSceneSO> visiting, HashSet<TalkSceneSO> soSet, List<string> issues)
{
visited.Add(current);
visiting.Add(current);
if (current.exits != null)
{
foreach (var exit in current.exits)
{
var next = exit.targetScene;
if (next == null || !soSet.Contains(next)) continue;
if (visiting.Contains(next))
{
issues.Add($"循环引用: {current.name} -> {next.name}");
continue;
}
if (!visited.Contains(next))
{
CheckCycle(next, visited, visiting, soSet, issues);
}
}
}
visiting.Remove(current);
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 913b090485a3d844fab3e436e7bf4f57
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
-282
View File
@@ -1,282 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AibisDream;
using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;
namespace AibisDream.SystemEditor
{
public class ChapterNode : Node
{
public TalkSceneSO SceneSo { get; }
public Port InputPort { get; private set; }
private const float PortLabelMaxWidth = 120f;
public ChapterNode(TalkSceneSO so)
{
SceneSo = so ?? throw new ArgumentNullException(nameof(so));
// 节点整体放大
style.minWidth = 240;
style.paddingTop = 8;
style.paddingBottom = 8;
style.paddingLeft = 10;
style.paddingRight = 10;
UpdateTitle();
UpdateColor();
BuildInputPort();
BuildOutputPorts();
BuildSubtitle();
BuildAddExitButton();
SetPosition(new Rect(so.graphPosition, Vector2.zero));
// 放大标题
var titleLabel = this.Q<Label>("title-label");
if (titleLabel != null)
{
titleLabel.style.fontSize = 16;
titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
}
}
public void UpdateTitle()
{
title = string.IsNullOrEmpty(SceneSo.title) ? SceneSo.name : SceneSo.title;
}
public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
{
base.BuildContextualMenu(evt);
if (SceneSo.exits != null && SceneSo.exits.Count > 0)
{
for (int i = 0; i < SceneSo.exits.Count; i++)
{
var exit = SceneSo.exits[i];
var name = string.IsNullOrEmpty(exit.exitName) ? $"出口{i}" : exit.exitName;
var idx = i;
evt.menu.AppendAction($"删除出口/{name}", _ => RemoveExitAt(idx));
}
}
}
public void UpdateColor()
{
style.backgroundColor = new StyleColor(SceneSo.nodeColor);
}
private void BuildInputPort()
{
InputPort = Port.Create<Edge>(
Orientation.Horizontal,
Direction.Input,
Port.Capacity.Single,
typeof(TalkSceneSO));
InputPort.portName = "▶";
InputPort.style.fontSize = 14;
InputPort.AddManipulator(new EdgeConnector<Edge>(new EmptyEdgeListener()));
inputContainer.Add(InputPort);
}
private void BuildOutputPorts()
{
outputContainer.Clear();
if (SceneSo.exits == null) return;
for (int i = 0; i < SceneSo.exits.Count; i++)
{
var exit = SceneSo.exits[i];
var port = CreateOutputPort(exit, i);
outputContainer.Add(port);
}
}
private Port CreateOutputPort(SceneExit exit, int index)
{
var port = Port.Create<Edge>(
Orientation.Horizontal,
Direction.Output,
Port.Capacity.Single,
typeof(TalkSceneSO));
port.portName = string.IsNullOrEmpty(exit.exitName) ? $"出口{index}" : exit.exitName;
port.userData = index;
// 限制端口标签宽度并放大字体
var label = port.Q<Label>("type");
if (label != null)
{
label.style.maxWidth = PortLabelMaxWidth;
label.style.fontSize = 12;
}
port.AddManipulator(new EdgeConnector<Edge>(new EmptyEdgeListener()));
port.RegisterCallback<ContextualMenuPopulateEvent>((evt) =>
{
var currentIndex = port.userData is int idx ? idx : -1;
evt.menu.AppendAction("删除出口", (_) => RemoveExitAt(currentIndex));
}, TrickleDown.TrickleDown);
return port;
}
private void BuildSubtitle()
{
var subtitleLabel = new Label($"{SceneSo.name}")
{
style =
{
fontSize = 12,
unityTextAlign = TextAnchor.MiddleCenter,
color = new Color(0.8f, 0.8f, 0.8f),
paddingTop = 4,
paddingBottom = 2
}
};
extensionContainer.Add(subtitleLabel);
RefreshExpandedState();
}
private void BuildAddExitButton()
{
var removeButton = new Button(() => RemoveLastExit())
{
text = "-",
style =
{
width = 26,
height = 22,
fontSize = 16,
unityFontStyleAndWeight = FontStyle.Bold,
marginRight = 4
}
};
titleButtonContainer.Add(removeButton);
var addButton = new Button(() => AddExit())
{
text = "+",
style = { width = 26, height = 22, fontSize = 16, unityFontStyleAndWeight = FontStyle.Bold }
};
titleButtonContainer.Add(addButton);
}
private void RemoveLastExit()
{
if (SceneSo.exits == null || SceneSo.exits.Count == 0) return;
RemoveExitAt(SceneSo.exits.Count - 1);
}
public void AddExit()
{
Undo.RecordObject(SceneSo, "Add Exit");
if (SceneSo.exits == null)
SceneSo.exits = new System.Collections.Generic.List<SceneExit>();
var newExit = new SceneExit
{
exitName = $"exit_{SceneSo.exits.Count}"
};
SceneSo.exits.Add(newExit);
EditorUtility.SetDirty(SceneSo);
var newPort = CreateOutputPort(newExit, SceneSo.exits.Count - 1);
outputContainer.Add(newPort);
RefreshPorts();
}
public void RemoveExitAt(int index)
{
if (SceneSo.exits == null || index < 0 || index >= SceneSo.exits.Count) return;
// 断开并移除该端口的所有边
var portToRemove = GetOutputPort(index);
if (portToRemove != null)
{
var edges = portToRemove.connections.ToList();
foreach (var edge in edges)
{
edge.input?.Disconnect(edge);
edge.output?.Disconnect(edge);
edge.RemoveFromHierarchy();
}
portToRemove.RemoveFromHierarchy();
}
Undo.RecordObject(SceneSo, "Remove Exit");
SceneSo.exits.RemoveAt(index);
EditorUtility.SetDirty(SceneSo);
// 更新后续端口的 userData 和 portName
for (int i = index; i < SceneSo.exits.Count; i++)
{
var port = GetOutputPort(i);
if (port != null)
{
port.userData = i;
var exit = SceneSo.exits[i];
port.portName = string.IsNullOrEmpty(exit.exitName) ? $"出口{i}" : exit.exitName;
}
}
RefreshPorts();
}
public void RefreshOutputPorts()
{
// 先断开所有输出端口的边
var edgesToRemove = outputContainer.Children()
.OfType<Port>()
.SelectMany(p => p.connections.ToList())
.ToList();
foreach (var edge in edgesToRemove)
{
edge.input?.Disconnect(edge);
edge.output?.Disconnect(edge);
edge.RemoveFromHierarchy();
}
BuildOutputPorts();
RefreshPorts();
}
public Port GetOutputPort(int index)
{
return outputContainer.Children().OfType<Port>().ElementAtOrDefault(index);
}
public override void SetPosition(Rect newPos)
{
base.SetPosition(newPos);
if (SceneSo != null)
{
SceneSo.graphPosition = newPos.position;
EditorUtility.SetDirty(SceneSo);
}
}
public void SyncPositionToSo()
{
if (SceneSo == null) return;
var pos = GetPosition();
if (SceneSo.graphPosition != pos.position)
{
SceneSo.graphPosition = pos.position;
EditorUtility.SetDirty(SceneSo);
}
}
private class EmptyEdgeListener : IEdgeConnectorListener
{
public void OnDropOutsidePort(Edge edge, Vector2 position) { }
public void OnDrop(GraphView graphView, Edge edge) { }
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: f3a4f1beaaa3a5740b1904f8b9b926b7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,32 +0,0 @@
using UnityEditor;
using UnityEngine;
namespace AibisDream.SystemEditor
{
[CustomEditor(typeof(TalkSceneSO))]
public class TalkSceneSOEditor : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(serializedObject.FindProperty("exits"), true);
EditorGUILayout.PropertyField(serializedObject.FindProperty("yarnProject"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("firstSceneName"));
EditorGUILayout.Space();
EditorGUILayout.LabelField("显示信息", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(serializedObject.FindProperty("title"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("titleKey"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("description"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("coverPic"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("chapter"));
EditorGUILayout.Space();
EditorGUILayout.LabelField("编辑器设置", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(serializedObject.FindProperty("nodeColor"), new GUIContent("节点颜色"));
serializedObject.ApplyModifiedProperties();
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 4988e2ee30c565e40989210408a234e3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
-8
View File
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 665561f3805a96446b45ea1d50bfe520
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,117 +0,0 @@
#if UNITY_EDITOR
using System.Linq;
using AibisDream.Kit;
using AibisDream.UI;
using AibisDream.Utility;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
namespace AibisDream.DeveloperMode.Editor
{
public sealed class DeveloperModeCoreTests
{
[Test]
public void RuntimeLogBufferEvictsOldestAndPreservesOrder()
{
var buffer = new RuntimeLogBuffer(3);
buffer.Add(LogEntry.System(LogLevel.Info, LogCategory.General, "one"));
buffer.Add(LogEntry.System(LogLevel.Warning, LogCategory.Save, "two"));
buffer.Add(LogEntry.System(LogLevel.Error, LogCategory.Yarn, "three"));
buffer.Add(LogEntry.System(LogLevel.Fatal, LogCategory.Scene, "four"));
var snapshot = buffer.Snapshot();
Assert.That(snapshot.Select(item => item.Message), Is.EqualTo(new[] { "two", "three", "four" }));
Assert.That(snapshot.Select(item => item.Sequence), Is.Ordered.Ascending);
Assert.That(snapshot.Length, Is.EqualTo(buffer.Capacity));
}
[Test]
public void RuntimeLogBufferAcceptsConcurrentWritersAndClears()
{
var buffer = new RuntimeLogBuffer(128);
System.Threading.Tasks.Parallel.For(0, 1000, index =>
buffer.Add(LogEntry.System(LogLevel.Info, LogCategory.General, index.ToString())));
var snapshot = buffer.Snapshot();
Assert.That(snapshot.Length, Is.EqualTo(128));
Assert.That(snapshot.Select(item => item.Sequence), Is.Ordered.Ascending);
buffer.Clear();
Assert.That(buffer.Snapshot(), Is.Empty);
}
[Test]
public void YarnStorageTypedSettersAndDebugSnapshotAreReadOnlyAndConsistent()
{
var go = new GameObject("YarnVariableStorage Test");
try
{
var storage = go.AddComponent<YarnVariableStorage>();
storage.SetValue("$score", 12.5f);
storage.SetValue("$global_flag", true);
storage.SetValue("$name", "AIBIS");
var snapshot = DeveloperVariableSnapshot.Capture(storage, null);
Assert.That(snapshot.Select(item => item.Name),
Is.EquivalentTo(new[] { "$score", "$global_flag", "$name" }));
Assert.That(snapshot.Single(item => item.Name == "$score").Value, Is.EqualTo("12.5"));
Assert.That(snapshot.Single(item => item.Name == "$global_flag").IsGlobal, Is.True);
Assert.That(snapshot.All(item => item.HasRuntimeOverride), Is.True);
storage.ClearLocal();
var remaining = DeveloperVariableSnapshot.Capture(storage, null);
Assert.That(remaining.Select(item => item.Name), Is.EqualTo(new[] { "$global_flag" }));
storage.Clear();
Assert.That(DeveloperVariableSnapshot.Capture(storage, null), Is.Empty);
}
finally
{
Object.DestroyImmediate(go);
}
}
[Test]
public void EditorBuildEnablesDeveloperMode()
{
Assert.That(DeveloperModeGate.IsEnabled, Is.True);
}
[Test]
public void PrefabIsConfiguredAndPersistenceReferencesIt()
{
const string prefabPath = "Assets/GameContent/Feature_MainUI/Prefabs/DeveloperModePanel.prefab";
const string scenePath = "Assets/Scenes/Persistence.unity";
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
Assert.That(prefab, Is.Not.Null);
Assert.That(prefab.activeSelf, Is.False);
var panel = prefab.GetComponent<DeveloperModePanel>();
Assert.That(panel, Is.Not.Null);
Assert.That(panel.IsConfigured, Is.True);
Assert.That(prefab.GetComponent<RectTransform>().sizeDelta, Is.EqualTo(new Vector2(840f, 760f)));
Assert.That(AssetDatabase.GetDependencies(scenePath), Does.Contain(prefabPath));
}
[Test]
public void PanelToggleIsNonModalAndEscapeClosesIt()
{
const string prefabPath = "Assets/GameContent/Feature_MainUI/Prefabs/DeveloperModePanel.prefab";
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
var instance = Object.Instantiate(prefab);
try
{
var panel = instance.GetComponent<DeveloperModePanel>();
var originalScale = Time.timeScale;
panel.TogglePanel();
Assert.That(panel.IsOpen, Is.True);
Assert.That(Time.timeScale, Is.EqualTo(originalScale));
Assert.That(panel.HandleEscape(), Is.True);
Assert.That(panel.IsOpen, Is.False);
}
finally
{
Object.DestroyImmediate(instance);
}
}
}
}
#endif
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: b95ba9d1e80e3dc4e83bc408569c4955
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,476 +0,0 @@
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using AibisDream.Framework;
using AibisDream.SaveSystem;
using Newtonsoft.Json;
using NUnit.Framework;
using UnityEngine;
namespace AibisDream.DeveloperMode.Editor.Tests
{
public sealed class TestSaveSystemTests
{
private string _root;
private TestSaveRepository _repository;
[SetUp]
public void SetUp()
{
_root = Path.Combine(
Path.GetTempPath(),
"AibisDream-TestSaveTests",
Guid.NewGuid().ToString("N"));
_repository = new TestSaveRepository(_root);
}
[TearDown]
public void TearDown()
{
if (Directory.Exists(_root))
{
Directory.Delete(_root, true);
}
}
[Test]
public void Record_SameAnchor_ReplacesSnapshotAndKeepsFirstSeenOrder()
{
var first = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneOne"));
var second = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneTwo"));
var scan = _repository.Scan("1.0");
Assert.That(scan.Entries.Count, Is.EqualTo(1));
Assert.That(second.Meta.firstSeenOrder, Is.EqualTo(first.Meta.firstSeenOrder));
Assert.That(scan.Entries[0].Meta.sceneName, Is.EqualTo("SceneTwo"));
Assert.That(_repository.TryLoad(scan.Entries[0], out var snapshot, out var error), Is.True, error);
Assert.That(snapshot.scene.sceneName, Is.EqualTo("SceneTwo"));
}
[Test]
public void Record_ReplacingEarlierEntry_DoesNotMoveItsOrder()
{
_repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA"));
_repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeB", "SceneB"));
_repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA2"));
var entries = _repository.Scan().Entries.ToArray();
Assert.That(entries.Select(item => item.NodeName), Is.EqualTo(new[] { "NodeA", "NodeB" }));
Assert.That(entries[0].Meta.firstSeenOrder, Is.LessThan(entries[1].Meta.firstSeenOrder));
}
[Test]
public void Scan_RetainsMissingSnapshotAsInvalid()
{
var entry = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA"));
File.Delete(entry.SnapshotPath);
var scan = _repository.Scan();
Assert.That(scan.ValidCount, Is.Zero);
Assert.That(scan.InvalidCount, Is.EqualTo(1));
Assert.That(scan.Entries[0].Status, Is.EqualTo(TestSaveEntryStatus.MissingSnapshot));
}
[Test]
public void Scan_RetainsOrphanSnapshotWithBrokenMeta()
{
var directory = Path.Combine(_root, "chapter", "broken");
Directory.CreateDirectory(directory);
File.WriteAllText(
Path.Combine(directory, TestSaveRepository.SnapshotFileName),
"{}",
Encoding.UTF8);
var scan = _repository.Scan();
Assert.That(scan.InvalidCount, Is.EqualTo(1));
Assert.That(scan.Entries[0].Status, Is.EqualTo(TestSaveEntryStatus.CorruptMeta));
}
[Test]
public void Validate_CorruptSnapshot_IsRetainedAndDisabledOnNextScan()
{
var entry = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA"));
File.WriteAllText(entry.SnapshotPath, "{broken", Encoding.UTF8);
Assert.That(_repository.Validate(entry, out var error), Is.False);
var rescanned = _repository.Scan();
Assert.That(error, Does.Contain("无法解析"));
Assert.That(rescanned.InvalidCount, Is.EqualTo(1));
Assert.That(rescanned.Entries[0].Status, Is.EqualTo(TestSaveEntryStatus.CorruptSnapshot));
}
[Test]
public void Scan_RecoversCompleteStagingDirectory()
{
var final = Path.Combine(_root, "chapter", "node");
var staging = final + ".__staging";
Directory.CreateDirectory(staging);
var request = CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA");
var meta = CreateMeta(request, 1);
File.WriteAllText(
Path.Combine(staging, TestSaveRepository.MetaFileName),
JsonConvert.SerializeObject(meta),
Encoding.UTF8);
File.WriteAllText(
Path.Combine(staging, TestSaveRepository.SnapshotFileName),
SnapshotPersistence.Serialize(request.Snapshot),
Encoding.UTF8);
var scan = _repository.Scan();
Assert.That(Directory.Exists(final), Is.True);
Assert.That(Directory.Exists(staging), Is.False);
Assert.That(scan.ValidCount, Is.EqualTo(1));
}
[Test]
public void Record_ConcurrentRequests_ProducesCompleteUniqueEntries()
{
var tasks = Enumerable.Range(0, 12)
.Select(index => Task.Run(() =>
_repository.Record(CreateRequest(
"ChapterA",
"ProjectA",
$"Node{index:00}",
$"Scene{index:00}"))))
.ToArray();
Task.WaitAll(tasks);
var scan = _repository.Scan();
Assert.That(scan.ValidCount, Is.EqualTo(12));
Assert.That(scan.InvalidCount, Is.Zero);
Assert.That(scan.Entries.Select(item => item.Meta.firstSeenOrder).Distinct().Count(), Is.EqualTo(12));
}
[Test]
public void DeleteInvalid_RemovesOnlyInvalidEntries()
{
_repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeA", "SceneA"));
var invalid = _repository.Record(CreateRequest("ChapterA", "ProjectA", "NodeB", "SceneB"));
File.Delete(invalid.SnapshotPath);
var removed = _repository.DeleteInvalid(out var error);
var scan = _repository.Scan();
Assert.That(error, Is.Null);
Assert.That(removed, Is.EqualTo(1));
Assert.That(scan.ValidCount, Is.EqualTo(1));
Assert.That(scan.InvalidCount, Is.Zero);
}
[TestCase(new[] { "hub" }, true)]
[TestCase(new[] { "linear" }, true)]
[TestCase(new[] { "content" }, true)]
[TestCase(new[] { "event" }, false)]
[TestCase(new[] { "hub", "no_save" }, false)]
[TestCase(new string[0], true)]
public void SilentCoverageEvaluator_MatchesSavePointTagSemantics(string[] tags, bool expected)
{
var actual = SavePointEvaluator.EvaluateNodeTagsForAutoSaveSilently(
"Node",
tags,
out _);
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase(new[] { "content" }, NodeSaveTiming.NodeEnter, SaveResumeMode.RestartNode)]
[TestCase(new[] { "interaction" }, NodeSaveTiming.DialogueExit, SaveResumeMode.StateOnly)]
[TestCase(new[] { "content", "save_on_exit" }, NodeSaveTiming.DialogueExit, SaveResumeMode.StateOnly)]
[TestCase(new[] { "event", "save_on_exit" }, NodeSaveTiming.DialogueExit, SaveResumeMode.StateOnly)]
[TestCase(new[] { "interaction", "save_on_exit" }, NodeSaveTiming.DialogueExit, SaveResumeMode.StateOnly)]
[TestCase(new[] { "interaction", "no_save" }, NodeSaveTiming.None, SaveResumeMode.RestartNode)]
[TestCase(new[] { "content", "event" }, NodeSaveTiming.None, SaveResumeMode.RestartNode)]
public void ResolveNodePolicy_MapsNodeTypeAndModifiers(
string[] tags,
NodeSaveTiming expectedTiming,
SaveResumeMode expectedResumeMode)
{
var policy = SavePointEvaluator.ResolveNodePolicy(
"Node",
tags,
out _,
logWarnings: false);
Assert.That(policy.Timing, Is.EqualTo(expectedTiming));
Assert.That(policy.ResumeMode, Is.EqualTo(expectedResumeMode));
}
[Test]
public void Repository_StateOnlySnapshot_PreservesAnchorNode()
{
var request = CreateRequest("ChapterA", "ProjectA", "InteractionNode", "SceneA");
request.Snapshot.anchor.startDialogueOnRestore = false;
request.SaveTrigger = "DialogueExit";
request.ResumeMode = nameof(SaveResumeMode.StateOnly);
request.StartDialogueOnRestore = false;
request.DedupeKey = TestSaveRecorder.BuildDedupeKey(
request.SceneSoName,
request.YarnProjectId,
request.NodeName,
request.ResumeMode);
request.EntryId = TestSaveRepository.StableHash(request.DedupeKey);
var entry = _repository.Record(request);
var loaded = _repository.TryLoad(entry, out var snapshot, out var error);
Assert.That(loaded, Is.True, error);
Assert.That(entry.NodeName, Is.EqualTo("InteractionNode"));
Assert.That(entry.ResumeMode, Is.EqualTo(nameof(SaveResumeMode.StateOnly)));
Assert.That(snapshot.anchor.nodeName, Is.EqualTo("InteractionNode"));
Assert.That(snapshot.anchor.startDialogueOnRestore, Is.False);
}
[Test]
public void SnapshotPersistence_MissingRestoreFlag_IsRejected()
{
const string json =
"{\"schemaVersion\":2,\"anchor\":{\"sceneSoName\":\"ChapterA\"," +
"\"yarnProjectId\":\"ProjectA\",\"nodeName\":\"NodeA\"}}";
Assert.Throws<Newtonsoft.Json.JsonSerializationException>(
() => SnapshotPersistence.Deserialize(json));
}
[Test]
public void SerialTaskQueue_DoesNotDropOrReorderRequests()
{
var queue = new SerialTaskQueue();
var releaseFirst = new TaskCompletionSource<bool>();
var firstStarted = new TaskCompletionSource<bool>();
var order = new List<int>();
var first = queue.Enqueue(async () =>
{
order.Add(1);
firstStarted.SetResult(true);
await releaseFirst.Task;
order.Add(2);
});
var second = queue.Enqueue(() =>
{
order.Add(3);
return Task.CompletedTask;
});
Assert.That(firstStarted.Task.Wait(TimeSpan.FromSeconds(2)), Is.True);
Assert.That(order, Is.EqualTo(new[] { 1 }));
Assert.That(queue.PendingCount, Is.EqualTo(2));
releaseFirst.SetResult(true);
Assert.That(
Task.WaitAll(
new[] { first.Completion, second.Completion },
TimeSpan.FromSeconds(2)),
Is.True);
Assert.That(first.Sequence, Is.LessThan(second.Sequence));
Assert.That(order, Is.EqualTo(new[] { 1, 2, 3 }));
}
[Test]
public void SaveRequest_MapsResumeModeToExplicitAnchorFlag()
{
var enter = SaveRequest.NodeEnter("NodeA", "ProjectA", "ChapterA", 3, 7);
var exit = SaveRequest.DialogueExit("NodeB", "ProjectA", "ChapterA", 3, 9);
Assert.That(enter.AnchorSpec.NodeName, Is.EqualTo("NodeA"));
Assert.That(enter.AnchorSpec.StartDialogueOnRestore, Is.True);
Assert.That(exit.AnchorSpec.NodeName, Is.EqualTo("NodeB"));
Assert.That(exit.AnchorSpec.StartDialogueOnRestore, Is.False);
}
[TestCase(false, "DetourNode", true)]
[TestCase(false, "NoSaveNode", false)]
[TestCase(true, "AnotherNode", true)]
public void ValidateDialogueTiming_NonExitRequestIgnoresLaterDialogueState(
bool explicitCommand,
string currentNodeName,
bool isDialogueRunning)
{
var request = explicitCommand
? SaveRequest.Explicit("SourceNode", "ProjectA", "ChapterA", 3, 7)
: SaveRequest.NodeEnter("SourceNode", "ProjectA", "ChapterA", 3, 7);
var valid = SaveRestoreOrchestrator.ValidateDialogueTiming(
request,
currentDialogueRunId: 99,
currentFlowRevision: 101,
currentNodeName: currentNodeName,
isDialogueRunning: isDialogueRunning,
out var reason);
Assert.That(valid, Is.True, reason);
Assert.That(request.AnchorSpec.NodeName, Is.EqualTo("SourceNode"));
}
[Test]
public void ValidateDialogueTiming_ConsecutiveNodeEnterRequestsRemainEligible()
{
var first = SaveRequest.NodeEnter("FirstNode", "ProjectA", "ChapterA", 3, 7);
var second = SaveRequest.NodeEnter("SecondNode", "ProjectA", "ChapterA", 3, 8);
var firstValid = SaveRestoreOrchestrator.ValidateDialogueTiming(
first,
currentDialogueRunId: 3,
currentFlowRevision: 8,
currentNodeName: "SecondNode",
isDialogueRunning: true,
out var firstReason);
var secondValid = SaveRestoreOrchestrator.ValidateDialogueTiming(
second,
currentDialogueRunId: 3,
currentFlowRevision: 8,
currentNodeName: "SecondNode",
isDialogueRunning: true,
out var secondReason);
Assert.That(firstValid, Is.True, firstReason);
Assert.That(secondValid, Is.True, secondReason);
Assert.That(first.AnchorSpec.NodeName, Is.EqualTo("FirstNode"));
Assert.That(second.AnchorSpec.NodeName, Is.EqualTo("SecondNode"));
}
[Test]
public void ValidateDialogueTiming_DialogueExitAcceptsStableCompletedFlow()
{
var request = SaveRequest.DialogueExit(
"InteractionNode",
"ProjectA",
"ChapterA",
3,
9);
var valid = SaveRestoreOrchestrator.ValidateDialogueTiming(
request,
currentDialogueRunId: 3,
currentFlowRevision: 9,
currentNodeName: null,
isDialogueRunning: false,
out var reason);
Assert.That(valid, Is.True, reason);
}
[TestCase(4, 9, null, false, "dialogue run changed")]
[TestCase(3, 10, null, false, "flow revision changed")]
[TestCase(3, 9, "DetourNode", false, "current Yarn node")]
[TestCase(3, 9, "NoSaveNode", false, "current Yarn node")]
[TestCase(3, 9, "JumpTarget", true, "still running")]
public void ValidateDialogueTiming_DialogueExitRejectsChangedOrRunningFlow(
long currentDialogueRunId,
long currentFlowRevision,
string currentNodeName,
bool isDialogueRunning,
string expectedReason)
{
var request = SaveRequest.DialogueExit(
"InteractionNode",
"ProjectA",
"ChapterA",
3,
9);
var valid = SaveRestoreOrchestrator.ValidateDialogueTiming(
request,
currentDialogueRunId,
currentFlowRevision,
currentNodeName,
isDialogueRunning,
out var reason);
Assert.That(valid, Is.False);
Assert.That(reason, Does.Contain(expectedReason));
}
[Test]
public void InteractionLock_ComposesWithDialogueLock()
{
var gameObject = new GameObject("EventSystemEx-Test");
try
{
var eventSystem = gameObject.AddComponent<EventSystemEx>();
EnumEventSystem.Global.Send(InteractionEventEnum.DialogStart);
var checkpointLock = eventSystem.AcquireInteractionLock("test");
EnumEventSystem.Global.Send(InteractionEventEnum.DialogEnd);
Assert.That(eventSystem.isLocked, Is.True);
checkpointLock.Dispose();
Assert.That(eventSystem.isLocked, Is.False);
}
finally
{
UnityEngine.Object.DestroyImmediate(gameObject);
}
}
private static TestSaveRecordRequest CreateRequest(
string sceneSoName,
string yarnProject,
string nodeName,
string sceneName)
{
var key = TestSaveRecorder.BuildDedupeKey(sceneSoName, yarnProject, nodeName);
return new TestSaveRecordRequest
{
Snapshot = new SaveSnapshot
{
gameVersion = "1.0",
savedAt = "2026-01-01 00:00:00",
scene = new SceneSnapshotDto { sceneName = sceneName },
anchor = new AnchorSnapshot
{
sceneSoName = sceneSoName,
yarnProjectId = yarnProject,
nodeName = nodeName,
startDialogueOnRestore = true
}
},
DedupeKey = key,
EntryId = TestSaveRepository.StableHash(key),
ChapterId = sceneSoName,
ChapterTitle = sceneSoName,
SceneSoName = sceneSoName,
YarnProjectId = yarnProject,
NodeName = nodeName,
SaveTrigger = "NodeEnter",
ResumeMode = nameof(SaveResumeMode.RestartNode),
StartDialogueOnRestore = true,
SceneName = sceneName,
GameVersion = "1.0"
};
}
private static TestSaveMeta CreateMeta(TestSaveRecordRequest request, long order)
{
return new TestSaveMeta
{
entryId = request.EntryId,
dedupeKey = request.DedupeKey,
chapterId = request.ChapterId,
chapterTitle = request.ChapterTitle,
sceneSoName = request.SceneSoName,
yarnProjectId = request.YarnProjectId,
nodeName = request.NodeName,
saveTrigger = request.SaveTrigger,
resumeMode = request.ResumeMode,
startDialogueOnRestore = request.StartDialogueOnRestore,
sceneName = request.SceneName,
firstSeenOrder = order,
firstRecordedAt = "2026-01-01 00:00:00",
lastRecordedAt = "2026-01-01 00:00:00",
snapshotSchemaVersion = SaveSnapshotSchema.CurrentVersion,
gameVersion = request.GameVersion
};
}
}
}
#endif
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: cbbba0c3621cf204483f453bad2d75a4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
-8
View File
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 9e244d3d62dec3f4f881f7e681da8c03
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,20 +0,0 @@
{
"name": "AibisDream.FrameAnimation.Editor",
"rootNamespace": "AibisDream.FrameAnimation.Editor",
"references": [
"AibisDream.FrameAnimation.Runtime",
"AibisDream.Menus",
"Unity.2D.Sprite.Editor"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: d1ffe139ca2417941a09835467d1e442
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,198 +0,0 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using UnityEngine;
namespace AibisDream.FrameAnimation.Editor
{
internal static class AsepriteJsonParser
{
public static bool TryParse(
string json,
string sourceId,
out AsepriteSourceDocument document,
out FrameAnimationImportIssue issue)
{
document = null;
issue = null;
try
{
if (string.IsNullOrWhiteSpace(json))
{
issue = Error(FrameAnimationImportIssueCode.JsonInvalid, sourceId, "Aseprite JSON 为空。");
return false;
}
var root = JObject.Parse(json);
var framesToken = root["frames"];
if (framesToken == null ||
(framesToken.Type != JTokenType.Object && framesToken.Type != JTokenType.Array))
{
issue = Error(FrameAnimationImportIssueCode.FramesMissing, sourceId, "JSON 缺少 Object 或 Array 格式的 frames。");
return false;
}
var frames = ParseFrames(framesToken);
var meta = root["meta"] as JObject;
var size = ParseSize(meta?["size"]);
var imageName = meta?.Value<string>("image") ?? string.Empty;
var tags = ParseTags(meta?["frameTags"]);
document = new AsepriteSourceDocument(frames, tags, size, imageName);
return true;
}
catch (Exception exception)
{
issue = Error(
FrameAnimationImportIssueCode.JsonInvalid,
sourceId,
$"Aseprite JSON 解析失败:{exception.Message}");
return false;
}
}
public static bool TryExpandTag(
AsepriteSourceTag tag,
int frameCount,
string sourceId,
out IReadOnlyList<int> indices,
out FrameAnimationImportIssue issue)
{
indices = Array.Empty<int>();
issue = null;
if (tag == null || tag.From < 0 || tag.To < tag.From || tag.To >= frameCount)
{
issue = Error(
FrameAnimationImportIssueCode.TagRangeInvalid,
sourceId,
$"Tag '{tag?.Name}' 的范围 {tag?.From}..{tag?.To} 无效。");
return false;
}
var result = new List<int>();
switch ((tag.Direction ?? string.Empty).ToLowerInvariant())
{
case "forward":
AddAscending(result, tag.From, tag.To);
break;
case "reverse":
AddDescending(result, tag.To, tag.From);
break;
case "pingpong":
AddAscending(result, tag.From, tag.To);
AddDescending(result, tag.To - 1, tag.From + 1);
break;
case "pingpong_reverse":
AddDescending(result, tag.To, tag.From);
AddAscending(result, tag.From + 1, tag.To - 1);
break;
default:
issue = Error(
FrameAnimationImportIssueCode.TagDirectionInvalid,
sourceId,
$"Tag '{tag.Name}' 使用了不支持的 direction '{tag.Direction}'。");
return false;
}
indices = result;
return true;
}
private static List<AsepriteSourceFrame> ParseFrames(JToken token)
{
var frames = new List<AsepriteSourceFrame>();
if (token is JObject objectFrames)
{
foreach (var property in objectFrames.Properties())
{
frames.Add(ParseFrame(property.Name, property.Value));
}
}
else
{
foreach (var item in (JArray)token)
{
frames.Add(ParseFrame(item.Value<string>("filename") ?? string.Empty, item));
}
}
return frames;
}
private static AsepriteSourceFrame ParseFrame(string frameName, JToken token)
{
return new AsepriteSourceFrame(
frameName,
ParseRect(token["frame"]),
token.Value<int?>("duration") ?? 0,
token.Value<bool?>("rotated") ?? false,
token.Value<bool?>("trimmed") ?? false,
ParseRect(token["spriteSourceSize"]),
ParseSize(token["sourceSize"]));
}
private static List<AsepriteSourceTag> ParseTags(JToken token)
{
var tags = new List<AsepriteSourceTag>();
if (!(token is JArray array))
{
return tags;
}
foreach (var item in array)
{
tags.Add(new AsepriteSourceTag(
(item.Value<string>("name") ?? string.Empty).Trim(),
item.Value<int?>("from") ?? -1,
item.Value<int?>("to") ?? -1,
item.Value<string>("direction") ?? string.Empty));
}
return tags;
}
private static RectInt ParseRect(JToken token)
{
return token == null
? default
: new RectInt(
token.Value<int?>("x") ?? 0,
token.Value<int?>("y") ?? 0,
token.Value<int?>("w") ?? 0,
token.Value<int?>("h") ?? 0);
}
private static Vector2Int ParseSize(JToken token)
{
return token == null
? default
: new Vector2Int(token.Value<int?>("w") ?? 0, token.Value<int?>("h") ?? 0);
}
private static void AddAscending(List<int> target, int from, int to)
{
for (var index = from; index <= to; index++)
{
target.Add(index);
}
}
private static void AddDescending(List<int> target, int from, int to)
{
for (var index = from; index >= to; index--)
{
target.Add(index);
}
}
private static FrameAnimationImportIssue Error(
FrameAnimationImportIssueCode code,
string sourceId,
string message)
{
return new FrameAnimationImportIssue(
FrameAnimationValidationSeverity.Error,
code,
sourceId,
sourceId,
message);
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: a21095473b1495440acab244f9e470c6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,3 +0,0 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("AibisDream.FrameAnimation.Tests.EditMode")]
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 8edeae42e1831084cbf05db9d9e5d1e2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,307 +0,0 @@
using System;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace AibisDream.FrameAnimation.Editor
{
internal sealed class FrameAnimationTextInputWindow : EditorWindow
{
private string label;
private string value;
private string warning;
private Func<string, string> validate;
private Action<string> confirm;
private Vector2 scroll;
public static void Show(
string title,
string label,
string initialValue,
string warning,
Func<string, string> validate,
Action<string> confirm)
{
var window = CreateInstance<FrameAnimationTextInputWindow>();
window.titleContent = new GUIContent(title);
window.label = label;
window.value = initialValue ?? string.Empty;
window.warning = warning ?? string.Empty;
window.validate = validate;
window.confirm = confirm;
window.minSize = new Vector2(460f, 220f);
window.maxSize = new Vector2(700f, 420f);
window.ShowUtility();
}
private void OnGUI()
{
scroll = EditorGUILayout.BeginScrollView(scroll);
EditorGUILayout.LabelField(label, EditorStyles.boldLabel);
GUI.SetNextControlName("FrameAnimationTextInput");
value = EditorGUILayout.TextField(value);
if (!string.IsNullOrEmpty(warning))
{
EditorGUILayout.HelpBox(warning, MessageType.Warning);
}
var error = validate?.Invoke(value) ?? string.Empty;
if (!string.IsNullOrEmpty(error))
{
EditorGUILayout.HelpBox(error, MessageType.Error);
}
GUILayout.FlexibleSpace();
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("取消", GUILayout.Width(90f)))
{
Close();
}
using (new EditorGUI.DisabledScope(!string.IsNullOrEmpty(error)))
{
if (GUILayout.Button("确认", GUILayout.Width(90f)))
{
var callback = confirm;
var result = value;
Close();
callback?.Invoke(result);
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndScrollView();
}
private void OnEnable()
{
EditorApplication.delayCall += FocusInput;
}
private void FocusInput()
{
if (this != null)
{
Focus();
EditorGUI.FocusTextInControl("FrameAnimationTextInput");
}
}
}
internal sealed class FrameAnimationManualClipWindow : EditorWindow
{
private FrameAnimationGraph graph;
private FrameClip source;
private string id = "NewClip";
private string displayName = "New Clip";
private bool external;
private Action<FrameClip> completed;
public static void Show(
FrameAnimationGraph graph,
FrameClip source,
Action<FrameClip> completed)
{
var window = CreateInstance<FrameAnimationManualClipWindow>();
window.titleContent = new GUIContent(source == null ? "Create Manual Clip" : "Copy As Manual Clip");
window.graph = graph;
window.source = source;
window.completed = completed;
var baseId = source != null ? source.Id + "_Manual" : "NewClip";
window.id = MakeUniqueId(graph, baseId);
window.displayName = source != null ? source.DisplayName + " Manual" : window.id;
window.minSize = new Vector2(460f, 260f);
window.maxSize = new Vector2(700f, 420f);
window.ShowUtility();
}
private void OnGUI()
{
EditorGUILayout.LabelField(source == null ? "创建 Manual Clip" : "复制为 Manual Clip", EditorStyles.boldLabel);
id = EditorGUILayout.TextField("ID", id);
displayName = EditorGUILayout.TextField("Display Name", displayName);
external = EditorGUILayout.ToggleLeft("保存为外部独立 .asset(默认保存为 Graph sub-asset", external);
var error = FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, id)
? string.Empty
: "ID 为空或与当前 Graph 的 Clip / Flow 冲突。";
if (!string.IsNullOrEmpty(error))
{
EditorGUILayout.HelpBox(error, MessageType.Error);
}
if (source != null)
{
EditorGUILayout.HelpBox(
"帧表将深拷贝,但 Sprite 仍复用原资源;ImportSource 关联会被清除。",
MessageType.Info);
}
GUILayout.FlexibleSpace();
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("取消", GUILayout.Width(90f)))
{
Close();
}
using (new EditorGUI.DisabledScope(!string.IsNullOrEmpty(error)))
{
if (GUILayout.Button("创建", GUILayout.Width(90f)))
{
Create();
}
}
EditorGUILayout.EndHorizontal();
}
private void Create()
{
var path = string.Empty;
if (external)
{
path = EditorUtility.SaveFilePanelInProject(
"保存外部 Manual Clip",
id,
"asset",
"请选择 Assets 下的保存位置。");
if (string.IsNullOrEmpty(path))
{
return;
}
}
var succeeded = source == null
? FrameAnimationAssetOperations.CreateManualClip(graph, id, displayName, path, out var result, out var error)
: FrameAnimationAssetOperations.CopyToManual(graph, source, id, displayName, path, out result, out error);
if (!succeeded)
{
EditorUtility.DisplayDialog("操作失败", error, "确定");
return;
}
var callback = completed;
Close();
callback?.Invoke(result);
}
private static string MakeUniqueId(FrameAnimationGraph graph, string baseId)
{
var candidate = baseId;
var suffix = 2;
while (!FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, candidate))
{
candidate = baseId + suffix++;
}
return candidate;
}
}
internal sealed class FrameAnimationExistingClipWindow : EditorWindow
{
private FrameAnimationGraph graph;
private FrameClip clip;
private Action<FrameClip> completed;
public static void Show(FrameAnimationGraph graph, Action<FrameClip> completed)
{
var window = CreateInstance<FrameAnimationExistingClipWindow>();
window.titleContent = new GUIContent("Add Existing Manual Clip");
window.graph = graph;
window.completed = completed;
window.minSize = new Vector2(460f, 180f);
window.maxSize = new Vector2(640f, 260f);
window.ShowUtility();
}
private void OnGUI()
{
EditorGUILayout.LabelField("添加外部 Manual Clip", EditorStyles.boldLabel);
clip = (FrameClip)EditorGUILayout.ObjectField("FrameClip", clip, typeof(FrameClip), false);
GUILayout.FlexibleSpace();
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("取消", GUILayout.Width(90f)))
{
Close();
}
using (new EditorGUI.DisabledScope(clip == null))
{
if (GUILayout.Button("添加", GUILayout.Width(90f)))
{
if (!FrameAnimationAssetOperations.AddExistingManualClip(graph, clip, out var error))
{
EditorUtility.DisplayDialog("添加失败", error, "确定");
return;
}
var callback = completed;
var result = clip;
Close();
callback?.Invoke(result);
}
}
EditorGUILayout.EndHorizontal();
}
}
internal sealed class FrameAnimationFlowWindow : EditorWindow
{
private FrameAnimationGraph graph;
private AnimationNode entry;
private string id;
private string displayName;
private Action<AnimationFlow> completed;
internal static void Show(
FrameAnimationGraph graph,
AnimationNode entry,
Action<AnimationFlow> completed)
{
var window = CreateInstance<FrameAnimationFlowWindow>();
window.titleContent = new GUIContent("Create Animation Flow");
window.graph = graph;
window.entry = entry;
window.id = FrameAnimationGraphMutationService.MakeUniqueFlowId(graph, entry);
window.displayName = window.id;
window.completed = completed;
window.minSize = new Vector2(460f, 230f);
window.maxSize = new Vector2(700f, 360f);
window.ShowUtility();
}
private void OnGUI()
{
EditorGUILayout.LabelField("从选中节点创建 Flow", EditorStyles.boldLabel);
using (new EditorGUI.DisabledScope(true))
{
EditorGUILayout.TextField("Entry Node", entry?.DisplayName ?? string.Empty);
}
id = EditorGUILayout.TextField("ID", id);
displayName = EditorGUILayout.TextField("Display Name", displayName);
var idError = FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, id)
? string.Empty
: "ID 为空或与当前 Graph 中的 Clip / Flow 冲突。";
var owner = graph?.Flows.FirstOrDefault(flow => flow != null && flow.EntryNodeId == entry?.InternalId);
if (owner != null)
{
idError = $"该节点已是 Flow '{owner.Id}' 的入口。";
}
if (!string.IsNullOrEmpty(idError))
{
EditorGUILayout.HelpBox(idError, MessageType.Error);
}
GUILayout.FlexibleSpace();
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("取消", GUILayout.Width(90f))) Close();
using (new EditorGUI.DisabledScope(!string.IsNullOrEmpty(idError)))
{
if (GUILayout.Button("创建", GUILayout.Width(90f)))
{
if (!FrameAnimationGraphMutationService.CreateFlow(
graph, entry, id, displayName, out var flow, out var error))
{
EditorUtility.DisplayDialog("创建 Flow 失败", error, "确定");
return;
}
var callback = completed;
Close();
callback?.Invoke(flow);
}
}
EditorGUILayout.EndHorizontal();
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: c8f58e7b80263c24087a98874796e91d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,254 +0,0 @@
using System;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace AibisDream.FrameAnimation.Editor
{
internal enum FrameAnimationBrowserEntryKind
{
Header,
Clip,
Flow,
Source
}
internal enum FrameAnimationBrowserIssueState
{
None,
Warning,
Error
}
internal sealed class FrameAnimationBrowserEntry
{
public FrameAnimationBrowserEntryKind Kind;
public string Title = string.Empty;
public string Id = string.Empty;
public string TypeLabel = string.Empty;
public string Meta = string.Empty;
public string StatusLabel = string.Empty;
public Color? AccentColor;
public FrameAnimationBrowserIssueState IssueState;
public FrameAnimationEditorSelection Selection;
public bool IsHeader => Kind == FrameAnimationBrowserEntryKind.Header;
}
internal readonly struct FrameAnimationFlowColorPalette
{
public readonly Color Raw;
public readonly Color Stroke;
public readonly Color Header;
public readonly Color Muted;
public readonly Color Text;
public FrameAnimationFlowColorPalette(Color raw, Color stroke, Color header, Color muted, Color text)
{
Raw = raw;
Stroke = stroke;
Header = header;
Muted = muted;
Text = text;
}
}
internal static class FrameAnimationFlowColorUtility
{
private static readonly Color Panel = new Color32(43, 47, 52, 255);
private static readonly Color LightText = new Color32(244, 246, 248, 255);
private static readonly Color DarkText = new Color32(24, 27, 30, 255);
private static readonly Color Neutral = new Color32(100, 168, 216, 255);
internal static Color ResolveRaw(FrameAnimationGraph graph, AnimationFlow flow)
{
return Opaque(ResolveStored(graph, flow));
}
internal static Color ResolveStored(FrameAnimationGraph graph, AnimationFlow flow)
{
if (graph == null || flow == null)
{
return Neutral;
}
var data = graph.EditorData?.FlowEditorData == null
? null
: System.Linq.Enumerable.FirstOrDefault(graph.EditorData.FlowEditorData,
item => item != null && item.FlowId == flow.Id);
return data?.Color ?? Neutral;
}
internal static FrameAnimationFlowColorPalette CreatePalette(Color value)
{
var raw = Opaque(value);
var luminance = Luminance(raw);
var stroke = luminance < 0.30f
? Color.Lerp(raw, Color.white, Mathf.InverseLerp(0.30f, 0.02f, luminance) * 0.42f)
: luminance > 0.82f
? Color.Lerp(raw, Color.black, Mathf.InverseLerp(0.82f, 1f, luminance) * 0.22f)
: raw;
stroke.a = 1f;
var header = Color.Lerp(Panel, stroke, 0.68f);
header.a = 1f;
var muted = Color.Lerp(Panel, stroke, 0.42f);
muted.a = 1f;
var text = Luminance(header) > 0.22f ? DarkText : LightText;
return new FrameAnimationFlowColorPalette(raw, stroke, header, muted, text);
}
internal static Color Opaque(Color value) => new Color(
Mathf.Clamp01(value.r), Mathf.Clamp01(value.g), Mathf.Clamp01(value.b), 1f);
internal static float Luminance(Color value)
{
var linear = value.linear;
return linear.r * 0.2126f + linear.g * 0.7152f + linear.b * 0.0722f;
}
}
internal sealed class FrameAnimationResourceRowElement : VisualElement
{
private readonly VisualElement flowDot;
private readonly Label typeBadge;
private readonly Label titleLabel;
private readonly Label idLabel;
private readonly Label metaLabel;
private readonly Label statusBadge;
internal FrameAnimationResourceRowElement()
{
AddToClassList("fa-resource-row");
var main = new VisualElement();
main.AddToClassList("fa-resource-row__main");
flowDot = new VisualElement { pickingMode = PickingMode.Ignore };
flowDot.AddToClassList("fa-flow-dot");
typeBadge = new Label { pickingMode = PickingMode.Ignore };
typeBadge.AddToClassList("fa-badge");
titleLabel = new Label { pickingMode = PickingMode.Ignore };
titleLabel.AddToClassList("fa-resource-row__name");
idLabel = new Label { pickingMode = PickingMode.Ignore };
idLabel.AddToClassList("fa-resource-row__id");
statusBadge = new Label { pickingMode = PickingMode.Ignore };
statusBadge.AddToClassList("fa-badge");
main.Add(flowDot);
main.Add(typeBadge);
main.Add(titleLabel);
main.Add(idLabel);
main.Add(statusBadge);
metaLabel = new Label { pickingMode = PickingMode.Ignore };
metaLabel.AddToClassList("fa-resource-row__meta");
Add(main);
Add(metaLabel);
}
internal void Bind(FrameAnimationBrowserEntry entry, bool selected)
{
userData = entry;
var isHeader = entry?.IsHeader == true;
EnableInClassList("fa-resource-group", isHeader);
EnableInClassList("fa-resource-row--selected", selected);
EnableInClassList("fa-resource-row--error", entry?.IssueState == FrameAnimationBrowserIssueState.Error);
EnableInClassList("fa-resource-row--warning", entry?.IssueState == FrameAnimationBrowserIssueState.Warning);
if (entry == null)
{
style.display = DisplayStyle.None;
return;
}
style.display = DisplayStyle.Flex;
titleLabel.text = entry.Title;
titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
idLabel.text = entry.Id;
metaLabel.text = entry.Meta;
typeBadge.text = entry.TypeLabel;
typeBadge.style.display = isHeader ? DisplayStyle.None : DisplayStyle.Flex;
idLabel.style.display = isHeader ? DisplayStyle.None : DisplayStyle.Flex;
metaLabel.style.display = isHeader ? DisplayStyle.None : DisplayStyle.Flex;
statusBadge.text = entry.StatusLabel;
statusBadge.style.display = isHeader || string.IsNullOrEmpty(entry.StatusLabel) ? DisplayStyle.None : DisplayStyle.Flex;
flowDot.style.display = !isHeader && entry.AccentColor.HasValue ? DisplayStyle.Flex : DisplayStyle.None;
if (entry.AccentColor.HasValue)
{
flowDot.style.backgroundColor = FrameAnimationFlowColorUtility.CreatePalette(entry.AccentColor.Value).Stroke;
}
typeBadge.EnableInClassList("fa-badge--blue", entry.Kind == FrameAnimationBrowserEntryKind.Clip);
typeBadge.EnableInClassList("fa-badge--purple", entry.Kind == FrameAnimationBrowserEntryKind.Flow);
typeBadge.EnableInClassList("fa-badge--green", entry.Kind == FrameAnimationBrowserEntryKind.Source);
statusBadge.EnableInClassList("fa-badge--red", entry.IssueState == FrameAnimationBrowserIssueState.Error);
statusBadge.EnableInClassList("fa-badge--yellow", entry.IssueState == FrameAnimationBrowserIssueState.Warning);
}
}
internal sealed class FrameAnimationFrameRowElement : VisualElement
{
private readonly ObjectField spriteField;
private readonly IntegerField durationField;
private readonly Label frameNameLabel;
private readonly Label sourceIndexLabel;
private FrameClip clip;
private int index;
private bool readOnly;
private Action changed;
internal FrameAnimationFrameRowElement()
{
AddToClassList("fa-frame-row");
spriteField = new ObjectField("Sprite") { objectType = typeof(Sprite), allowSceneObjects = false };
durationField = new IntegerField("Duration Ms");
var readOnlyRow = new VisualElement();
readOnlyRow.AddToClassList("fa-frame-row__readonly");
frameNameLabel = new Label();
sourceIndexLabel = new Label();
readOnlyRow.Add(frameNameLabel);
readOnlyRow.Add(sourceIndexLabel);
Add(spriteField);
Add(durationField);
Add(readOnlyRow);
spriteField.RegisterValueChangedCallback(OnSpriteChanged);
durationField.RegisterValueChangedCallback(OnDurationChanged);
}
internal void Bind(FrameClip value, int itemIndex, bool isReadOnly, Action onChanged)
{
clip = value;
index = itemIndex;
readOnly = isReadOnly;
changed = onChanged;
userData = itemIndex;
var frame = clip != null && index >= 0 && index < clip.Frames.Count ? clip.Frames[index] : null;
spriteField.SetValueWithoutNotify(frame?.Sprite);
durationField.SetValueWithoutNotify(frame?.DurationMs ?? 100);
frameNameLabel.text = "Frame " + (frame?.FrameName ?? string.Empty);
sourceIndexLabel.text = "Source " + (frame?.SourceIndex.ToString() ?? "-1");
spriteField.SetEnabled(!readOnly);
durationField.SetEnabled(!readOnly);
}
private void OnSpriteChanged(ChangeEvent<UnityEngine.Object> evt)
{
if (readOnly || clip == null || index < 0 || index >= clip.Frames.Count) return;
var serialized = new SerializedObject(clip);
serialized.Update();
var element = serialized.FindProperty("frames").GetArrayElementAtIndex(index);
element.FindPropertyRelative("sprite").objectReferenceValue = evt.newValue;
serialized.ApplyModifiedProperties();
EditorUtility.SetDirty(clip);
changed?.Invoke();
}
private void OnDurationChanged(ChangeEvent<int> evt)
{
if (readOnly || clip == null || index < 0 || index >= clip.Frames.Count) return;
var serialized = new SerializedObject(clip);
serialized.Update();
var element = serialized.FindProperty("frames").GetArrayElementAtIndex(index);
element.FindPropertyRelative("durationMs").intValue = Mathf.Max(1, evt.newValue);
serialized.ApplyModifiedProperties();
durationField.SetValueWithoutNotify(Mathf.Max(1, evt.newValue));
EditorUtility.SetDirty(clip);
changed?.Invoke();
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 876191b33b10eef44b133cc291ebc912
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,744 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace AibisDream.FrameAnimation.Editor
{
internal enum FrameAnimationEditorSelectionKind
{
None,
Graph,
Clip,
Flow,
Source,
Node,
Edge
}
internal readonly struct FrameAnimationEditorSelection
{
public FrameAnimationEditorSelectionKind Kind { get; }
public object Value { get; }
public FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind kind, object value)
{
Kind = kind;
Value = value;
}
}
internal enum FrameAnimationClipFilter
{
All,
Manual,
Imported,
Normal,
Missing,
Referenced,
Unused
}
internal enum FrameAnimationClipSort
{
Name,
Source,
FrameCount,
Duration,
Missing,
ReferenceCount
}
internal static class FrameAnimationAssetReferenceIndex
{
public static IReadOnlyList<FrameAnimationGraph> FindGraphsReferencing(FrameClip clip)
{
if (clip == null)
{
return Array.Empty<FrameAnimationGraph>();
}
return AssetDatabase.FindAssets("t:FrameAnimationGraph")
.Select(guid => AssetDatabase.LoadAssetAtPath<FrameAnimationGraph>(AssetDatabase.GUIDToAssetPath(guid)))
.Where(graph => graph != null && graph.Clips.Any(item => item == clip))
.Distinct()
.OrderBy(graph => AssetDatabase.GetAssetPath(graph), StringComparer.Ordinal)
.ToArray();
}
}
internal static class FrameAnimationResourceQuery
{
public static IReadOnlyList<FrameClip> QueryClips(
FrameAnimationGraph graph,
string search,
FrameAnimationClipFilter filter,
FrameAnimationClipSort sort)
{
if (graph == null)
{
return Array.Empty<FrameClip>();
}
var sourceNames = graph.ImportSources.Where(source => source != null)
.GroupBy(source => source.InternalId ?? string.Empty)
.ToDictionary(group => group.Key, group => group.First().DisplayName ?? string.Empty);
var query = graph.Clips.Where(clip => clip != null && MatchesSearch(clip, sourceNames, search));
query = filter switch
{
FrameAnimationClipFilter.Manual => query.Where(clip => !clip.IsImported),
FrameAnimationClipFilter.Imported => query.Where(clip => clip.IsImported),
FrameAnimationClipFilter.Normal => query.Where(clip => !clip.IsImported || !clip.ImportInfo.IsMissingFromSource),
FrameAnimationClipFilter.Missing => query.Where(clip => clip.IsImported && clip.ImportInfo.IsMissingFromSource),
FrameAnimationClipFilter.Referenced => query.Where(clip => ReferenceCount(graph, clip) > 0),
FrameAnimationClipFilter.Unused => query.Where(clip => ReferenceCount(graph, clip) == 0),
_ => query
};
IOrderedEnumerable<FrameClip> ordered = sort switch
{
FrameAnimationClipSort.Source => query.OrderBy(clip => SourceName(clip, sourceNames), StringComparer.Ordinal),
FrameAnimationClipSort.FrameCount => query.OrderBy(clip => clip.FrameCount),
FrameAnimationClipSort.Duration => query.OrderBy(clip => clip.TotalDurationMs),
FrameAnimationClipSort.Missing => query.OrderBy(clip => clip.IsImported && clip.ImportInfo.IsMissingFromSource ? 1 : 0),
FrameAnimationClipSort.ReferenceCount => query.OrderBy(clip => ReferenceCount(graph, clip)),
_ => query.OrderBy(clip => clip.DisplayName ?? clip.Id, StringComparer.Ordinal)
};
return ordered.ThenBy(clip => clip.Id, StringComparer.Ordinal)
.ThenBy(clip => AssetDatabase.GetAssetPath(clip), StringComparer.Ordinal)
.ToArray();
}
public static int ReferenceCount(FrameAnimationGraph graph, FrameClip clip)
{
return graph?.Nodes.Count(node => node != null && node.ClipId == clip?.Id) ?? 0;
}
public static string SourceName(
FrameClip clip,
IReadOnlyDictionary<string, string> sourceNames = null)
{
if (clip == null || !clip.IsImported)
{
return clip != null && clip.HasStandaloneImportSource ? "Standalone Source" : "Manual";
}
var sourceId = clip.ImportInfo?.ImportSourceId;
if (sourceNames != null && !string.IsNullOrEmpty(sourceId) &&
sourceNames.TryGetValue(sourceId, out var name))
{
return name;
}
return sourceId ?? string.Empty;
}
private static bool MatchesSearch(
FrameClip clip,
IReadOnlyDictionary<string, string> sourceNames,
string search)
{
if (string.IsNullOrWhiteSpace(search))
{
return true;
}
return Contains(clip.Id, search) || Contains(clip.DisplayName, search) ||
Contains(clip.ImportInfo?.SourceTagName, search) ||
Contains(SourceName(clip, sourceNames), search);
}
private static bool Contains(string value, string search)
{
return !string.IsNullOrEmpty(value) && value.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0;
}
}
internal static class FrameAnimationAssetOperations
{
public static bool IsPlayableIdAvailable(
FrameAnimationGraph graph,
string id,
FrameClip exceptClip = null,
AnimationFlow exceptFlow = null)
{
return graph != null && !string.IsNullOrWhiteSpace(id) &&
graph.Clips.All(clip => clip == null || clip == exceptClip || clip.Id != id) &&
graph.Flows.All(flow => flow == null || flow == exceptFlow || flow.Id != id);
}
public static bool CreateManualClip(
FrameAnimationGraph graph,
string id,
string displayName,
string externalPath,
out FrameClip clip,
out string error)
{
clip = null;
error = string.Empty;
if (!IsPlayableIdAvailable(graph, id))
{
error = "Clip id 为空或与现有 Clip / Flow 冲突。";
return false;
}
Undo.IncrementCurrentGroup();
var group = Undo.GetCurrentGroup();
Undo.SetCurrentGroupName("Create Manual Frame Clip");
try
{
Undo.RecordObject(graph, "Add Manual Frame Clip");
clip = ScriptableObject.CreateInstance<FrameClip>();
clip.name = id;
clip.Configure(id, string.IsNullOrWhiteSpace(displayName) ? id : displayName,
Array.Empty<FrameAnimationFrame>(), 1f,
graph.Settings.NewManualClipDefaultEndBehavior);
if (string.IsNullOrWhiteSpace(externalPath))
{
AssetDatabase.AddObjectToAsset(clip, graph);
}
else
{
AssetDatabase.CreateAsset(clip, externalPath);
}
Undo.RegisterCreatedObjectUndo(clip, "Create Manual Frame Clip");
graph.AddClip(clip);
EditorUtility.SetDirty(graph);
EditorUtility.SetDirty(clip);
AssetDatabase.SaveAssets();
Undo.CollapseUndoOperations(group);
return true;
}
catch (Exception exception)
{
Undo.RevertAllDownToGroup(group);
clip = null;
error = exception.Message;
return false;
}
}
public static bool AddExistingManualClip(
FrameAnimationGraph graph,
FrameClip clip,
out string error)
{
error = string.Empty;
if (graph == null || clip == null)
{
error = "Graph 或 Clip 为空。";
return false;
}
if (clip.IsImported || AssetDatabase.IsSubAsset(clip) ||
string.IsNullOrEmpty(AssetDatabase.GetAssetPath(clip)))
{
error = "只能添加独立 .asset 形式、且不由 Graph ImportSource 管理的 Clip。";
return false;
}
if (graph.Clips.Contains(clip))
{
error = "当前 Graph 已引用该 Clip。";
return false;
}
if (!IsPlayableIdAvailable(graph, clip.Id))
{
error = $"playable id '{clip.Id}' 与当前 Graph 中的 Clip / Flow 冲突。";
return false;
}
Undo.RecordObject(graph, "Add Existing Manual Frame Clip");
graph.AddClip(clip);
EditorUtility.SetDirty(graph);
return true;
}
public static bool CopyToManual(
FrameAnimationGraph graph,
FrameClip source,
string id,
string displayName,
string externalPath,
out FrameClip copy,
out string error)
{
copy = null;
error = string.Empty;
if (source == null || !IsPlayableIdAvailable(graph, id))
{
error = "来源为空,或新 id 与现有 Clip / Flow 冲突。";
return false;
}
Undo.IncrementCurrentGroup();
var group = Undo.GetCurrentGroup();
Undo.SetCurrentGroupName("Copy Frame Clip As Manual");
try
{
Undo.RecordObject(graph, "Add Manual Frame Clip Copy");
copy = ScriptableObject.CreateInstance<FrameClip>();
copy.name = id;
copy.Configure(id,
string.IsNullOrWhiteSpace(displayName) ? source.DisplayName : displayName,
source.Frames.Select(frame => frame == null
? null
: new FrameAnimationFrame(frame.Sprite, frame.DurationMs, frame.FrameName, frame.SourceIndex)),
source.Speed,
source.DefaultEndBehavior);
if (string.IsNullOrWhiteSpace(externalPath))
{
AssetDatabase.AddObjectToAsset(copy, graph);
}
else
{
AssetDatabase.CreateAsset(copy, externalPath);
}
Undo.RegisterCreatedObjectUndo(copy, "Create Manual Frame Clip Copy");
graph.AddClip(copy);
EditorUtility.SetDirty(graph);
EditorUtility.SetDirty(copy);
AssetDatabase.SaveAssets();
Undo.CollapseUndoOperations(group);
return true;
}
catch (Exception exception)
{
Undo.RevertAllDownToGroup(group);
copy = null;
error = exception.Message;
return false;
}
}
public static bool RenameClip(
FrameAnimationGraph graph,
FrameClip clip,
string newId,
out string error)
{
error = string.Empty;
if (graph == null || clip == null || !graph.Clips.Contains(clip))
{
error = "Clip 不属于当前 Graph。";
return false;
}
var owners = FrameAnimationAssetReferenceIndex.FindGraphsReferencing(clip);
if (!AssetDatabase.IsSubAsset(clip) && owners.Count > 1)
{
error = "外部 Manual Clip 被多个 Graph 共享,禁止重命名:" +
string.Join(", ", owners.Select(owner => owner.name));
return false;
}
if (!IsPlayableIdAvailable(graph, newId, clip))
{
error = "新 id 为空或与现有 Clip / Flow 冲突。";
return false;
}
var oldId = clip.Id;
Undo.IncrementCurrentGroup();
var group = Undo.GetCurrentGroup();
Undo.SetCurrentGroupName("Rename Frame Clip");
Undo.RecordObject(graph, "Update Frame Clip References");
Undo.RecordObject(clip, "Rename Frame Clip");
clip.SetId(newId);
foreach (var node in graph.Nodes.Where(node => node != null && node.ClipId == oldId))
{
node.SetClipId(newId);
}
if (graph.Settings.DefaultPlayableId == oldId)
{
graph.Settings.SetDefaultPlayableId(newId);
}
EditorUtility.SetDirty(graph);
EditorUtility.SetDirty(clip);
Undo.CollapseUndoOperations(group);
return true;
}
public static bool RenameFlow(
FrameAnimationGraph graph,
AnimationFlow flow,
string newId,
out string error)
{
error = string.Empty;
if (graph == null || flow == null || !graph.Flows.Contains(flow) ||
!IsPlayableIdAvailable(graph, newId, exceptFlow: flow))
{
error = "Flow 不属于当前 Graph,或新 id 与现有 Clip / Flow 冲突。";
return false;
}
var oldId = flow.Id;
Undo.RecordObject(graph, "Rename Animation Flow");
flow.SetId(newId);
if (graph.Settings.DefaultPlayableId == oldId)
{
graph.Settings.SetDefaultPlayableId(newId);
}
foreach (var data in graph.EditorData.FlowEditorData.Where(data => data != null && data.FlowId == oldId))
{
data.SetFlowId(newId);
}
EditorUtility.SetDirty(graph);
return true;
}
public static bool RenameGraph(FrameAnimationGraph graph, string newId, out string error)
{
error = string.Empty;
if (graph == null || string.IsNullOrWhiteSpace(newId))
{
error = "Graph id 不能为空。";
return false;
}
Undo.RecordObject(graph, "Rename Frame Animation Graph");
graph.SetId(newId);
EditorUtility.SetDirty(graph);
return true;
}
public static bool RemoveClip(FrameAnimationGraph graph, FrameClip clip, out string error)
{
if (!CanRemoveClip(graph, clip, out error))
{
return false;
}
Undo.IncrementCurrentGroup();
var group = Undo.GetCurrentGroup();
Undo.SetCurrentGroupName("Remove Frame Clip");
Undo.RecordObject(graph, "Remove Frame Clip Reference");
graph.RemoveClip(clip);
if (AssetDatabase.IsSubAsset(clip) && AssetDatabase.GetAssetPath(clip) == AssetDatabase.GetAssetPath(graph))
{
Undo.DestroyObjectImmediate(clip);
}
EditorUtility.SetDirty(graph);
AssetDatabase.SaveAssets();
Undo.CollapseUndoOperations(group);
return true;
}
public static bool CanRemoveClip(FrameAnimationGraph graph, FrameClip clip, out string error)
{
error = string.Empty;
if (graph == null || clip == null || !graph.Clips.Contains(clip))
{
error = "Clip 不属于当前 Graph。";
return false;
}
var nodeCount = graph.Nodes.Count(node => node != null && node.ClipId == clip.Id);
if (nodeCount > 0)
{
error = $"Clip 仍被 {nodeCount} 个 AnimationNode 引用。";
return false;
}
if (graph.Settings.DefaultPlayableId == clip.Id)
{
error = "Clip 仍被 defaultPlayableId 引用。";
return false;
}
return true;
}
public static bool RemoveFlow(FrameAnimationGraph graph, AnimationFlow flow, out string error)
{
if (!CanRemoveFlow(graph, flow, out error))
{
return false;
}
Undo.RecordObject(graph, "Remove Animation Flow");
graph.RemoveFlow(flow);
graph.EditorData.RemoveFlowData(flow.Id);
EditorUtility.SetDirty(graph);
return true;
}
public static bool CanRemoveFlow(FrameAnimationGraph graph, AnimationFlow flow, out string error)
{
error = string.Empty;
if (graph == null || flow == null || !graph.Flows.Contains(flow))
{
error = "Flow 不属于当前 Graph。";
return false;
}
if (graph.Settings.DefaultPlayableId == flow.Id)
{
error = "Flow 仍被 defaultPlayableId 引用。";
return false;
}
return true;
}
public static bool RemoveSource(
FrameAnimationGraph graph,
FrameAnimationImportSource source,
out string error)
{
error = string.Empty;
if (graph == null || source == null || !graph.ImportSources.Contains(source))
{
error = "ImportSource 不属于当前 Graph。";
return false;
}
var clips = graph.Clips.Where(clip => clip != null &&
clip.ImportInfo?.ImportSourceId == source.InternalId).ToArray();
if (clips.Length > 0)
{
error = "ImportSource 仍关联 Imported Clip" + string.Join(", ", clips.Select(clip => clip.Id));
return false;
}
Undo.RecordObject(graph, "Remove Frame Animation ImportSource");
graph.RemoveImportSource(source);
EditorUtility.SetDirty(graph);
return true;
}
}
internal sealed class FrameAnimationEditorIssue
{
public FrameAnimationValidationSeverity Severity { get; }
public string Code { get; }
public FrameAnimationEditorSelection Selection { get; }
public string Message { get; }
public string Suggestion { get; }
public FrameAnimationEditorIssue(
FrameAnimationValidationSeverity severity,
string code,
FrameAnimationEditorSelection selection,
string message,
string suggestion)
{
Severity = severity;
Code = code ?? string.Empty;
Selection = selection;
Message = message ?? string.Empty;
Suggestion = suggestion ?? string.Empty;
}
}
internal static class FrameAnimationEditorValidationService
{
public static IReadOnlyList<FrameAnimationEditorIssue> Validate(
FrameAnimationGraph graph,
bool includeImports,
out FrameAnimationImportPreview importPreview)
{
importPreview = null;
if (graph == null)
{
return Array.Empty<FrameAnimationEditorIssue>();
}
var issues = FrameAnimationGraphValidator.Validate(graph).Issues
.Select(issue => new FrameAnimationEditorIssue(
issue.Severity,
issue.Code.ToString(),
Locate(graph, issue.TargetType, issue.TargetId),
issue.Message,
Suggestion(issue.Code)))
.ToList();
foreach (var duplicate in graph.Clips.Where(clip => clip != null)
.GroupBy(clip => clip).Where(group => group.Count() > 1))
{
issues.Add(new FrameAnimationEditorIssue(
FrameAnimationValidationSeverity.Error,
"DuplicateClipReference",
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Clip, duplicate.Key),
$"Graph 重复引用 Clip '{duplicate.Key.Id}'。",
"移除重复的 Clip 引用。"));
}
var graphPath = AssetDatabase.GetAssetPath(graph);
foreach (var clip in graph.Clips.Where(clip => clip != null))
{
if (AssetDatabase.IsSubAsset(clip) && AssetDatabase.GetAssetPath(clip) != graphPath)
{
issues.Add(new FrameAnimationEditorIssue(
FrameAnimationValidationSeverity.Error,
"ForeignSubAssetClip",
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Clip, clip),
$"Clip '{clip.Id}' 是其他资产的 sub-asset。",
"移除该引用,并复制为当前 Graph 的 Manual Clip。"));
}
if (clip.IsImported && (!AssetDatabase.IsSubAsset(clip) || AssetDatabase.GetAssetPath(clip) != graphPath))
{
issues.Add(new FrameAnimationEditorIssue(
FrameAnimationValidationSeverity.Error,
"ImportedClipOwnership",
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Clip, clip),
$"Imported Clip '{clip.Id}' 不是当前 Graph 的 sub-asset。",
"删除非法引用并从对应 ImportSource 重新刷新。"));
}
}
foreach (var node in graph.Nodes.Where(node => node != null))
{
var clip = graph.Clips.SingleOrDefault(item => item != null && item.Id == node.ClipId);
if (clip?.ImportInfo?.IsMissingFromSource == true)
{
issues.Add(new FrameAnimationEditorIssue(
FrameAnimationValidationSeverity.Error,
"NodeReferencesMissingClip",
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Node, node),
$"Node '{node.InternalId}' 引用了 Missing Clip '{clip.Id}'。",
"恢复源 Tag、修改节点引用或删除该节点。"));
}
}
var nodeIds = new HashSet<string>(graph.Nodes.Where(node => node != null)
.Select(node => node.InternalId));
foreach (var node in graph.Nodes.Where(node => node != null &&
graph.EditorData.NodeEditorData.All(data => data == null || data.NodeId != node.InternalId)))
{
issues.Add(new FrameAnimationEditorIssue(
FrameAnimationValidationSeverity.Warning,
"EditorDataMissing",
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Node, node),
$"Node '{node.InternalId}' 缺少画布位置数据。",
"打开 Graph Editor 初始化位置,或执行自动布局。"));
}
foreach (var data in graph.EditorData.NodeEditorData.Where(data => data != null &&
!nodeIds.Contains(data.NodeId)))
{
issues.Add(new FrameAnimationEditorIssue(
FrameAnimationValidationSeverity.Warning,
"EditorDataOrphan",
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph),
$"存在孤立的 NodeEditorData '{data.NodeId}'。",
"该数据不会影响运行时;确认无引用后可由后续维护工具清理。"));
}
foreach (var duplicate in graph.EditorData.NodeEditorData.Where(data => data != null)
.GroupBy(data => data.NodeId).Where(group => group.Count() > 1))
{
issues.Add(new FrameAnimationEditorIssue(
FrameAnimationValidationSeverity.Error,
"EditorDataDuplicate",
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph),
$"NodeEditorData '{duplicate.Key}' 重复。",
"保留一条对应位置数据并移除重复项。"));
}
var flowIds = new HashSet<string>(graph.Flows.Where(flow => flow != null).Select(flow => flow.Id));
foreach (var flow in graph.Flows.Where(flow => flow != null &&
graph.EditorData.FlowEditorData.All(data => data == null || data.FlowId != flow.Id)))
{
issues.Add(new FrameAnimationEditorIssue(
FrameAnimationValidationSeverity.Warning,
"FlowEditorDataMissing",
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Flow, flow),
$"Flow '{flow.Id}' 缺少画布颜色数据。",
"打开 Graph Editor 初始化 Flow 颜色。"));
}
foreach (var data in graph.EditorData.FlowEditorData.Where(data => data != null &&
!flowIds.Contains(data.FlowId)))
{
issues.Add(new FrameAnimationEditorIssue(
FrameAnimationValidationSeverity.Warning,
"FlowEditorDataOrphan",
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph),
$"存在孤立的 FlowEditorData '{data.FlowId}'。",
"确认 Flow 已删除后可由后续维护工具清理。"));
}
foreach (var duplicate in graph.EditorData.FlowEditorData.Where(data => data != null)
.GroupBy(data => data.FlowId).Where(group => group.Count() > 1))
{
issues.Add(new FrameAnimationEditorIssue(
FrameAnimationValidationSeverity.Error,
"FlowEditorDataDuplicate",
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph),
$"FlowEditorData '{duplicate.Key}' 重复。",
"保留一条对应颜色数据并移除重复项。"));
}
if (!includeImports)
{
return issues;
}
importPreview = FrameAnimationImportService.PreviewAll(graph);
foreach (var sourcePreview in importPreview.Sources)
{
foreach (var importIssue in sourcePreview.Issues)
{
issues.Add(new FrameAnimationEditorIssue(
importIssue.Severity,
importIssue.Code.ToString(),
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Source, sourcePreview.Source),
importIssue.Message,
"打开对应 ImportSource,修复来源设置后重新 Preview。"));
}
}
return issues;
}
private static FrameAnimationEditorSelection Locate(
FrameAnimationGraph graph,
FrameAnimationValidationTargetType type,
string id)
{
return type switch
{
FrameAnimationValidationTargetType.Clip => new FrameAnimationEditorSelection(
FrameAnimationEditorSelectionKind.Clip, graph.Clips.FirstOrDefault(clip => clip != null && clip.Id == id)),
FrameAnimationValidationTargetType.Flow => new FrameAnimationEditorSelection(
FrameAnimationEditorSelectionKind.Flow, graph.Flows.FirstOrDefault(flow => flow != null && flow.Id == id)),
FrameAnimationValidationTargetType.Node => new FrameAnimationEditorSelection(
FrameAnimationEditorSelectionKind.Node, graph.Nodes.FirstOrDefault(node => node != null && node.InternalId == id)),
FrameAnimationValidationTargetType.Edge => new FrameAnimationEditorSelection(
FrameAnimationEditorSelectionKind.Edge, graph.Edges.FirstOrDefault(edge => edge != null && edge.InternalId == id)),
FrameAnimationValidationTargetType.ImportSource => new FrameAnimationEditorSelection(
FrameAnimationEditorSelectionKind.Source, graph.ImportSources.FirstOrDefault(source => source != null && source.InternalId == id)),
_ => new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph)
};
}
private static string Suggestion(FrameAnimationValidationCode code)
{
return code switch
{
FrameAnimationValidationCode.PlayableIdEmpty => "使用正式 Rename 命令设置唯一 playable id。",
FrameAnimationValidationCode.PlayableIdDuplicate => "重命名冲突的 Clip 或 Flow。",
FrameAnimationValidationCode.ClipFramesEmpty => "为 Manual Clip 添加帧,或刷新 Imported Clip 来源。",
FrameAnimationValidationCode.FrameDurationInvalid => "将 durationMs 修改为大于 0 的整数。",
FrameAnimationValidationCode.DefaultPlayableInvalid => "在 Graph 属性中选择有效的默认 Clip 或 Flow。",
FrameAnimationValidationCode.ImportedClipMissingFromSource => "恢复源 Tag,或确认没有引用后删除 Missing Clip。",
_ => "打开问题对象并修复对应字段或引用。"
};
}
}
internal static class FrameAnimationWorkspaceState
{
private const string Prefix = "AibisDream.FrameAnimation.Workspace.";
public static string Key(FrameAnimationGraph graph, string name)
{
var graphPath = graph != null ? AssetDatabase.GetAssetPath(graph) : string.Empty;
var graphGuid = string.IsNullOrEmpty(graphPath) ? "none" : AssetDatabase.AssetPathToGUID(graphPath);
var project = Hash128.Compute(Application.dataPath).ToString();
return Prefix + project + "." + graphGuid + "." + name;
}
public static float GetFloat(FrameAnimationGraph graph, string name, float fallback) =>
EditorPrefs.GetFloat(Key(graph, name), fallback);
public static void SetFloat(FrameAnimationGraph graph, string name, float value) =>
EditorPrefs.SetFloat(Key(graph, name), value);
public static string GetString(FrameAnimationGraph graph, string name, string fallback) =>
EditorPrefs.GetString(Key(graph, name), fallback);
public static void SetString(FrameAnimationGraph graph, string name, string value) =>
EditorPrefs.SetString(Key(graph, name), value ?? string.Empty);
public static bool GetBool(FrameAnimationGraph graph, string name, bool fallback) =>
EditorPrefs.GetBool(Key(graph, name), fallback);
public static void SetBool(FrameAnimationGraph graph, string name, bool value) =>
EditorPrefs.SetBool(Key(graph, name), value);
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 2643ad174ef237e43b98400d60c4a201
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,603 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace AibisDream.FrameAnimation.Editor
{
internal static class FrameAnimationFlowFocusPolicy
{
public static AnimationFlow FindFocusedFlow(FrameAnimationGraph graph, string focusedFlowId)
{
return graph?.Flows.FirstOrDefault(flow => flow != null && flow.Id == focusedFlowId);
}
public static bool Contains(
FrameAnimationGraph graph,
string focusedFlowId,
FrameAnimationEditorSelection selection)
{
var flow = FindFocusedFlow(graph, focusedFlowId);
if (flow == null)
{
return false;
}
var reachable = new FrameAnimationGraphTopology(graph).GetReachable(flow.EntryNodeId);
return selection.Value switch
{
AnimationNode node => reachable.Nodes.Contains(node),
AnimationEdge edge => reachable.Edges.Contains(edge),
AnimationFlow selectedFlow => selectedFlow == flow,
_ => false
};
}
public static bool ShouldExitForSelection(
FrameAnimationGraph graph,
string focusedFlowId,
FrameAnimationEditorSelection selection)
{
if (string.IsNullOrEmpty(focusedFlowId))
{
return false;
}
return selection.Kind switch
{
FrameAnimationEditorSelectionKind.None => false,
FrameAnimationEditorSelectionKind.Flow => false,
FrameAnimationEditorSelectionKind.Node => !Contains(graph, focusedFlowId, selection),
FrameAnimationEditorSelectionKind.Edge => !Contains(graph, focusedFlowId, selection),
_ => true
};
}
public static bool ShouldExitForSelectionSet(
FrameAnimationGraph graph,
string focusedFlowId,
IEnumerable<FrameAnimationEditorSelection> selections)
{
if (string.IsNullOrEmpty(focusedFlowId))
{
return false;
}
var items = (selections ?? Array.Empty<FrameAnimationEditorSelection>()).ToArray();
return items.Length > 0 && items.Any(item =>
(item.Kind == FrameAnimationEditorSelectionKind.Node ||
item.Kind == FrameAnimationEditorSelectionKind.Edge)
? !Contains(graph, focusedFlowId, item)
: ShouldExitForSelection(graph, focusedFlowId, item));
}
}
internal sealed class FrameAnimationGraphImpact
{
public IReadOnlyList<AnimationFlow> Flows { get; }
public int LostNodeCount { get; }
internal FrameAnimationGraphImpact(IReadOnlyList<AnimationFlow> flows, int lostNodeCount)
{
Flows = flows ?? Array.Empty<AnimationFlow>();
LostNodeCount = lostNodeCount;
}
}
internal static class FrameAnimationGraphImpactAnalyzer
{
public static FrameAnimationGraphImpact AnalyzeEdgeRemoval(
FrameAnimationGraph graph,
IEnumerable<AnimationEdge> edges)
{
var removed = new HashSet<AnimationEdge>((edges ?? Array.Empty<AnimationEdge>()).Where(edge => edge != null));
return Analyze(graph, Array.Empty<AnimationNode>(), removed);
}
public static FrameAnimationGraphImpact AnalyzeNodeRemoval(
FrameAnimationGraph graph,
IEnumerable<AnimationNode> nodes)
{
var removedNodes = new HashSet<AnimationNode>((nodes ?? Array.Empty<AnimationNode>()).Where(node => node != null));
var removedIds = new HashSet<string>(removedNodes.Select(node => node.InternalId));
var removedEdges = new HashSet<AnimationEdge>(graph.Edges.Where(edge => edge != null &&
(removedIds.Contains(edge.FromNodeId) || removedIds.Contains(edge.ToNodeId))));
return Analyze(graph, removedNodes, removedEdges);
}
public static FrameAnimationGraphImpact AnalyzeRemoval(
FrameAnimationGraph graph,
IEnumerable<AnimationNode> nodes,
IEnumerable<AnimationEdge> edges)
{
if (graph == null)
{
return new FrameAnimationGraphImpact(Array.Empty<AnimationFlow>(), 0);
}
var removedNodes = new HashSet<AnimationNode>((nodes ?? Array.Empty<AnimationNode>())
.Where(node => node != null));
var removedIds = new HashSet<string>(removedNodes.Select(node => node.InternalId));
var removedEdges = new HashSet<AnimationEdge>((edges ?? Array.Empty<AnimationEdge>())
.Where(edge => edge != null));
removedEdges.UnionWith(graph.Edges.Where(edge => edge != null &&
(removedIds.Contains(edge.FromNodeId) || removedIds.Contains(edge.ToNodeId))));
return Analyze(graph, removedNodes, removedEdges);
}
private static FrameAnimationGraphImpact Analyze(
FrameAnimationGraph graph,
IReadOnlyCollection<AnimationNode> removedNodes,
IReadOnlyCollection<AnimationEdge> removedEdges)
{
if (graph == null)
{
return new FrameAnimationGraphImpact(Array.Empty<AnimationFlow>(), 0);
}
var topology = new FrameAnimationGraphTopology(graph);
var removedNodeIds = new HashSet<string>(removedNodes.Select(node => node.InternalId));
var affected = new List<AnimationFlow>();
var lost = new HashSet<string>();
foreach (var flow in graph.Flows.Where(flow => flow != null))
{
var before = topology.GetReachable(flow.EntryNodeId).Nodes
.Where(node => node != null).Select(node => node.InternalId).ToHashSet();
var after = ReachableWithout(graph, flow.EntryNodeId, removedNodeIds, removedEdges);
var flowLost = before.Where(nodeId => !after.Contains(nodeId)).ToArray();
if (flowLost.Length > 0)
{
affected.Add(flow);
lost.UnionWith(flowLost);
}
}
return new FrameAnimationGraphImpact(
affected.OrderBy(flow => flow.Id, StringComparer.Ordinal).ToArray(),
lost.Count);
}
private static HashSet<string> ReachableWithout(
FrameAnimationGraph graph,
string entry,
HashSet<string> removedNodeIds,
IReadOnlyCollection<AnimationEdge> removedEdges)
{
var result = new HashSet<string>();
if (removedNodeIds.Contains(entry ?? string.Empty))
{
return result;
}
var outgoing = graph.Edges.Where(edge => edge != null && !removedEdges.Contains(edge))
.GroupBy(edge => edge.FromNodeId ?? string.Empty)
.ToDictionary(group => group.Key, group => group.ToArray());
var queue = new Queue<string>();
queue.Enqueue(entry ?? string.Empty);
while (queue.Count > 0)
{
var id = queue.Dequeue();
if (removedNodeIds.Contains(id) || !result.Add(id))
{
continue;
}
if (outgoing.TryGetValue(id, out var edges))
{
foreach (var edge in edges)
{
queue.Enqueue(edge.ToNodeId ?? string.Empty);
}
}
}
return result;
}
}
internal static class FrameAnimationGraphMutationService
{
private static readonly Color[] FlowColors =
{
new Color(0.24f, 0.65f, 1f), new Color(0.42f, 0.8f, 0.42f),
new Color(1f, 0.62f, 0.24f), new Color(0.82f, 0.42f, 0.9f),
new Color(1f, 0.38f, 0.52f), new Color(0.25f, 0.82f, 0.78f)
};
public static bool CreateNode(
FrameAnimationGraph graph,
FrameClip clip,
Vector2 position,
out AnimationNode node,
out string error)
{
node = null;
error = string.Empty;
if (graph == null || clip == null || !graph.Clips.Contains(clip))
{
error = "只能为当前 Graph 中的 FrameClip 创建节点。";
return false;
}
Undo.RecordObject(graph, "Create Frame Animation Node");
node = new AnimationNode(clip.Id,
string.IsNullOrWhiteSpace(clip.DisplayName) ? clip.Id : clip.DisplayName);
graph.AddNode(node);
graph.EditorData.GetOrCreateNodeData(node.InternalId, position).SetPosition(position);
Dirty(graph);
return true;
}
public static bool TryConnect(
FrameAnimationGraph graph,
AnimationNode from,
AnimationNode to,
out AnimationEdge edge,
out string error)
{
edge = null;
if (!CanConnect(graph, from, to, out error))
{
return false;
}
Undo.RecordObject(graph, "Connect Frame Animation Nodes");
edge = new AnimationEdge(from.InternalId, to.InternalId);
graph.AddEdge(edge);
Dirty(graph);
return true;
}
public static bool CanConnect(
FrameAnimationGraph graph,
AnimationNode from,
AnimationNode to,
out string error)
{
error = string.Empty;
if (graph == null || from == null || to == null ||
graph.Nodes.Count(node => node == from) != 1 || graph.Nodes.Count(node => node == to) != 1)
{
error = "连接两端必须唯一属于当前 Graph。";
return false;
}
if (from == to || from.InternalId == to.InternalId)
{
error = "节点不允许连接到自身。";
return false;
}
var topology = new FrameAnimationGraphTopology(graph);
if (topology.GetOutgoing(from.InternalId).Count > 0)
{
error = "第一版每个节点最多只能有一个后继。";
return false;
}
if (from.EndBehaviorOverride.HasValue)
{
error = "该节点已设置终点结束行为,请先清除覆盖。";
return false;
}
if (graph.Edges.Any(existing => existing != null &&
existing.FromNodeId == from.InternalId && existing.ToNodeId == to.InternalId))
{
error = "该连接已经存在。";
return false;
}
if (topology.WouldCreateCycle(from.InternalId, to.InternalId))
{
error = "该连接会形成自连接或多节点环路。";
return false;
}
return true;
}
public static void SetNodeDisplayName(FrameAnimationGraph graph, AnimationNode node, string value)
{
Undo.RecordObject(graph, "Rename Frame Animation Node");
node.SetDisplayName(value);
Dirty(graph);
}
public static void SetNodeClip(FrameAnimationGraph graph, AnimationNode node, FrameClip clip)
{
Undo.RecordObject(graph, "Set Frame Animation Node Clip");
node.SetClipId(clip != null ? clip.Id : string.Empty);
Dirty(graph);
}
public static void SetNodeSpeed(FrameAnimationGraph graph, AnimationNode node, float? value)
{
Undo.RecordObject(graph, "Set Frame Animation Node Speed");
node.SetSpeedOverride(value);
Dirty(graph);
}
public static bool SetNodeEndBehavior(
FrameAnimationGraph graph,
AnimationNode node,
FrameClipEndBehavior? value,
bool removeOutgoing,
out string error)
{
error = string.Empty;
var outgoing = graph.Edges.Where(edge => edge != null && edge.FromNodeId == node.InternalId).ToArray();
if (value.HasValue && outgoing.Length > 0 && !removeOutgoing)
{
error = "节点仍有后继 Edge。";
return false;
}
Undo.RecordObject(graph, "Set Frame Animation Node End Behavior");
if (value.HasValue)
{
foreach (var edge in outgoing)
{
graph.RemoveEdge(edge);
}
}
node.SetEndBehaviorOverride(value);
Dirty(graph);
return true;
}
public static void RemoveEdges(FrameAnimationGraph graph, IEnumerable<AnimationEdge> edges)
{
var items = (edges ?? Array.Empty<AnimationEdge>()).Where(edge => edge != null).Distinct().ToArray();
if (graph == null || items.Length == 0)
{
return;
}
Undo.RecordObject(graph, "Disconnect Frame Animation Nodes");
foreach (var edge in items)
{
graph.RemoveEdge(edge);
}
Dirty(graph);
}
public static bool RemoveNodes(
FrameAnimationGraph graph,
IEnumerable<AnimationNode> nodes,
bool deleteEntryFlows,
out string error)
{
return RemoveNodesAndEdges(
graph, nodes, Array.Empty<AnimationEdge>(), deleteEntryFlows, out error);
}
public static bool RemoveNodesAndEdges(
FrameAnimationGraph graph,
IEnumerable<AnimationNode> nodes,
IEnumerable<AnimationEdge> additionalEdges,
bool deleteEntryFlows,
out string error)
{
error = string.Empty;
if (graph == null)
{
error = "Graph 已失效。";
return false;
}
var items = (nodes ?? Array.Empty<AnimationNode>()).Where(node => node != null).Distinct().ToArray();
var extraEdges = (additionalEdges ?? Array.Empty<AnimationEdge>())
.Where(edge => edge != null).Distinct().ToArray();
var ids = new HashSet<string>(items.Select(node => node.InternalId));
var entryFlows = graph.Flows.Where(flow => flow != null && ids.Contains(flow.EntryNodeId)).ToArray();
if (entryFlows.Length > 0 && !deleteEntryFlows)
{
error = "删除项包含 Flow 入口节点。";
return false;
}
Undo.RecordObject(graph, "Delete Frame Animation Graph Elements");
foreach (var edge in graph.Edges.Where(edge => edge != null &&
(ids.Contains(edge.FromNodeId) || ids.Contains(edge.ToNodeId) || extraEdges.Contains(edge))).ToArray())
{
graph.RemoveEdge(edge);
}
foreach (var flow in entryFlows)
{
graph.RemoveFlow(flow);
graph.EditorData.RemoveFlowData(flow.Id);
if (graph.Settings.DefaultPlayableId == flow.Id)
{
graph.Settings.SetDefaultPlayableId(string.Empty);
}
}
foreach (var node in items)
{
graph.RemoveNode(node);
graph.EditorData.RemoveNodeData(node.InternalId);
}
Dirty(graph);
return true;
}
public static string MakeUniqueFlowId(FrameAnimationGraph graph, AnimationNode node)
{
var root = (string.IsNullOrWhiteSpace(node?.DisplayName) ? "Animation" : node.DisplayName) + "_Flow";
var candidate = root;
var suffix = 2;
while (!FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, candidate))
{
candidate = root + suffix++;
}
return candidate;
}
public static bool CreateFlow(
FrameAnimationGraph graph,
AnimationNode entry,
string id,
string displayName,
out AnimationFlow flow,
out string error)
{
flow = null;
error = string.Empty;
if (graph == null || entry == null || !graph.Nodes.Contains(entry))
{
error = "Flow 入口必须属于当前 Graph。";
return false;
}
var owner = graph.Flows.FirstOrDefault(existing => existing != null && existing.EntryNodeId == entry.InternalId);
if (owner != null)
{
error = $"该节点已是 Flow '{owner.Id}' 的入口。";
return false;
}
if (!FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, id))
{
error = "Flow id 为空或与 Clip / Flow 冲突。";
return false;
}
Undo.RecordObject(graph, "Create Animation Flow");
flow = new AnimationFlow(id, string.IsNullOrWhiteSpace(displayName) ? id : displayName, entry.InternalId);
graph.AddFlow(flow);
graph.EditorData.GetOrCreateFlowData(flow.Id, FlowColors[(graph.Flows.Count - 1) % FlowColors.Length]);
Dirty(graph);
return true;
}
public static bool SetFlowEntry(
FrameAnimationGraph graph,
AnimationFlow flow,
AnimationNode entry,
out string error)
{
error = string.Empty;
if (graph == null || flow == null || entry == null ||
!graph.Flows.Contains(flow) || !graph.Nodes.Contains(entry))
{
error = "Flow 或入口节点不属于当前 Graph。";
return false;
}
var owner = graph.Flows.FirstOrDefault(existing => existing != null && existing != flow &&
existing.EntryNodeId == entry.InternalId);
if (owner != null)
{
error = $"该节点已是 Flow '{owner.Id}' 的入口。";
return false;
}
Undo.RecordObject(graph, "Set Animation Flow Entry");
flow.SetEntryNodeId(entry.InternalId);
Dirty(graph);
return true;
}
public static void SetFlowColor(FrameAnimationGraph graph, AnimationFlow flow, Color value)
{
if (graph == null || flow == null)
{
return;
}
Undo.RecordObject(graph, "Set Animation Flow Color");
graph.EditorData.GetOrCreateFlowData(flow.Id, value).SetColor(value);
Dirty(graph);
}
public static bool EnsureEditorData(FrameAnimationGraph graph)
{
if (graph == null)
{
return false;
}
var missingNodes = graph.Nodes.Where(node => node != null &&
graph.EditorData.NodeEditorData.All(data => data == null || data.NodeId != node.InternalId)).ToArray();
var missingFlows = graph.Flows.Where(flow => flow != null &&
graph.EditorData.FlowEditorData.All(data => data == null || data.FlowId != flow.Id)).ToArray();
if (missingNodes.Length == 0 && missingFlows.Length == 0)
{
return false;
}
Undo.RecordObject(graph, "Initialize Frame Animation Graph Layout");
for (var index = 0; index < missingNodes.Length; index++)
{
var position = new Vector2((index % 4) * 300f, (index / 4) * 180f);
graph.EditorData.GetOrCreateNodeData(missingNodes[index].InternalId, position);
}
for (var index = 0; index < missingFlows.Length; index++)
{
graph.EditorData.GetOrCreateFlowData(missingFlows[index].Id,
FlowColors[(graph.Flows.ToList().IndexOf(missingFlows[index]) + FlowColors.Length) % FlowColors.Length]);
}
Dirty(graph);
return true;
}
public static Vector2 GetNodePosition(FrameAnimationGraph graph, AnimationNode node)
{
return graph?.EditorData.NodeEditorData.FirstOrDefault(data =>
data != null && data.NodeId == node?.InternalId)?.Position ?? Vector2.zero;
}
public static void SetNodePositions(
FrameAnimationGraph graph,
IReadOnlyDictionary<AnimationNode, Vector2> positions,
string undoName)
{
if (graph == null || positions == null || positions.Count == 0)
{
return;
}
Undo.RecordObject(graph, undoName);
foreach (var pair in positions)
{
graph.EditorData.GetOrCreateNodeData(pair.Key.InternalId, pair.Value).SetPosition(pair.Value);
}
Dirty(graph);
}
private static void Dirty(FrameAnimationGraph graph)
{
EditorUtility.SetDirty(graph);
}
}
internal static class FrameAnimationGraphLayoutService
{
public static IReadOnlyDictionary<AnimationNode, Vector2> Calculate(
FrameAnimationGraph graph,
IEnumerable<AnimationNode> scope)
{
var source = scope ?? (graph != null
? graph.Nodes
: (IEnumerable<AnimationNode>)Array.Empty<AnimationNode>());
var nodes = source.Where(node => node != null).Distinct()
.GroupBy(node => node.InternalId ?? string.Empty)
.Select(group => group.First()).ToArray();
if (graph == null || nodes.Length == 0)
{
return new Dictionary<AnimationNode, Vector2>();
}
var ids = new HashSet<string>(nodes.Select(node => node.InternalId));
var edges = graph.Edges.Where(edge => edge != null && ids.Contains(edge.FromNodeId) && ids.Contains(edge.ToNodeId)).ToArray();
var incoming = nodes.ToDictionary(node => node.InternalId, _ => 0);
foreach (var edge in edges)
{
incoming[edge.ToNodeId]++;
}
var ranks = nodes.ToDictionary(node => node.InternalId, _ => 0);
var queue = new Queue<AnimationNode>(nodes.Where(node => incoming[node.InternalId] == 0)
.OrderBy(node => node.InternalId, StringComparer.Ordinal));
var processed = new HashSet<string>();
while (queue.Count > 0)
{
var node = queue.Dequeue();
if (!processed.Add(node.InternalId)) continue;
foreach (var edge in edges.Where(edge => edge.FromNodeId == node.InternalId))
{
ranks[edge.ToNodeId] = Math.Max(ranks[edge.ToNodeId], ranks[node.InternalId] + 1);
incoming[edge.ToNodeId]--;
if (incoming[edge.ToNodeId] == 0)
{
queue.Enqueue(nodes.First(item => item.InternalId == edge.ToNodeId));
}
}
}
var oldPositions = nodes.ToDictionary(node => node,
node => FrameAnimationGraphMutationService.GetNodePosition(graph, node));
var originX = oldPositions.Values.Min(position => position.x);
var originY = oldPositions.Values.Min(position => position.y);
var result = new Dictionary<AnimationNode, Vector2>();
foreach (var group in nodes.GroupBy(node => ranks[node.InternalId]).OrderBy(group => group.Key))
{
var ordered = group.OrderBy(node => oldPositions[node].y)
.ThenBy(node => node.InternalId, StringComparer.Ordinal).ToArray();
for (var index = 0; index < ordered.Length; index++)
{
result[ordered[index]] = new Vector2(originX + group.Key * 300f, originY + index * 180f);
}
}
return result;
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: b397ed96c5b44df89626f866c32e03dd
@@ -1,33 +0,0 @@
using UnityEditor;
using UnityEngine;
namespace AibisDream.FrameAnimation.Editor
{
[CustomEditor(typeof(FrameAnimationGraph))]
public sealed class FrameAnimationGraphEditor : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
var graph = (FrameAnimationGraph)target;
EditorGUILayout.LabelField("FrameAnimationGraph", EditorStyles.boldLabel);
using (new EditorGUI.DisabledScope(true))
{
EditorGUILayout.TextField("ID", graph.Id);
EditorGUILayout.TextField("Display Name", graph.DisplayName);
EditorGUILayout.IntField("Clips", graph.Clips.Count);
EditorGUILayout.IntField("Nodes", graph.Nodes.Count);
EditorGUILayout.IntField("Edges", graph.Edges.Count);
EditorGUILayout.IntField("Flows", graph.Flows.Count);
EditorGUILayout.IntField("Import Sources", graph.ImportSources.Count);
EditorGUILayout.TextField("Default Playable", graph.Settings?.DefaultPlayableId ?? string.Empty);
}
EditorGUILayout.HelpBox(
"Graph 的资源、来源、重命名和删除操作只能在主工作台中执行。",
MessageType.Info);
if (GUILayout.Button("Open Frame Animation Graph Editor"))
{
FrameAnimationGraphEditorWindow.Open(graph);
}
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 74f3b50c2650607499ea1c82a282f00f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,783 +0,0 @@
:root {
--fa-bg: rgb(27, 29, 32);
--fa-canvas: rgb(30, 32, 35);
--fa-panel: rgb(36, 39, 43);
--fa-panel-raised: rgb(43, 46, 51);
--fa-panel-hover: rgb(48, 52, 58);
--fa-field: rgb(28, 31, 34);
--fa-line: rgb(17, 19, 21);
--fa-line-soft: rgb(62, 67, 74);
--fa-text: rgb(215, 217, 220);
--fa-muted: rgb(143, 150, 158);
--fa-accent: rgb(61, 145, 212);
--fa-accent-soft: rgb(36, 79, 112);
--fa-success: rgb(86, 185, 123);
--fa-warning: rgb(224, 184, 88);
--fa-error: rgb(223, 106, 106);
}
.fa-workbench {
flex-grow: 1;
flex-direction: column;
background-color: var(--fa-bg);
color: var(--fa-text);
font-size: 12px;
}
.fa-global-toolbar-host,
.fa-main-host,
.fa-bottom-host {
flex-shrink: 0;
}
.fa-main-host {
flex-grow: 1;
min-height: 0;
}
.fa-workbench .unity-toolbar {
min-height: 32px;
padding-left: 6px;
padding-right: 6px;
background-color: var(--fa-panel-raised);
border-bottom-width: 1px;
border-bottom-color: var(--fa-line);
}
.fa-workbench .unity-toolbar-button,
.fa-workbench .unity-button {
min-height: 24px;
margin-left: 2px;
margin-right: 2px;
padding-left: 8px;
padding-right: 8px;
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-left-color: var(--fa-line);
border-right-color: var(--fa-line);
border-top-color: var(--fa-line);
border-bottom-color: var(--fa-line);
border-top-left-radius: 3px;
border-top-right-radius: 3px;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
background-color: rgb(54, 58, 63);
color: var(--fa-text);
}
.fa-workbench .unity-toolbar-button:hover,
.fa-workbench .unity-button:hover {
background-color: rgb(70, 75, 81);
}
.fa-button--primary {
background-color: rgb(42, 105, 150);
border-left-color: rgb(25, 74, 104);
border-right-color: rgb(25, 74, 104);
border-top-color: rgb(25, 74, 104);
border-bottom-color: rgb(25, 74, 104);
}
.fa-button--danger {
background-color: rgb(87, 54, 56);
color: rgb(244, 205, 207);
}
.fa-toolbar-group {
flex-direction: row;
align-items: center;
flex-shrink: 0;
}
.fa-toolbar-separator {
width: 1px;
height: 21px;
margin-left: 5px;
margin-right: 5px;
background-color: var(--fa-line);
}
.fa-panel {
flex-direction: column;
flex-shrink: 0;
min-height: 0;
background-color: var(--fa-panel);
}
.fa-panel-header {
height: 28px;
min-height: 28px;
padding-left: 9px;
padding-right: 9px;
-unity-text-align: middle-left;
-unity-font-style: bold;
background-color: rgb(44, 47, 51);
border-bottom-width: 1px;
border-bottom-color: var(--fa-line);
}
.fa-resizer--vertical {
width: 4px;
background-color: rgb(24, 26, 29);
}
.fa-resizer--horizontal {
height: 4px;
background-color: rgb(24, 26, 29);
}
.fa-resizer--vertical:hover,
.fa-resizer--horizontal:hover {
background-color: var(--fa-accent);
}
.fa-resource-tabs {
min-height: 34px;
padding-left: 4px;
padding-right: 4px;
padding-top: 3px;
background-color: rgb(31, 34, 38);
}
.fa-resource-tabs .unity-toolbar-button,
.fa-bottom-tabs .unity-toolbar-button {
flex-grow: 1;
flex-basis: 0;
min-height: 28px;
margin-left: 1px;
margin-right: 1px;
border-left-width: 0;
border-right-width: 0;
border-top-width: 0;
border-bottom-width: 2px;
border-bottom-color: rgba(0, 0, 0, 0);
border-top-left-radius: 0;
border-top-right-radius: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background-color: rgba(0, 0, 0, 0);
color: rgb(169, 175, 182);
}
.fa-resource-tabs .unity-toolbar-button:hover,
.fa-bottom-tabs .unity-toolbar-button:hover {
color: var(--fa-text);
background-color: rgb(48, 52, 57);
}
.fa-resource-tabs .unity-toolbar-button.fa-tab--active,
.fa-bottom-tabs .unity-toolbar-button.fa-tab--active {
color: white;
background-color: var(--fa-panel-raised);
border-bottom-color: var(--fa-accent);
-unity-font-style: bold;
}
.fa-resource-tools {
padding-left: 7px;
padding-right: 7px;
padding-top: 7px;
padding-bottom: 7px;
border-bottom-width: 1px;
border-bottom-color: var(--fa-line);
}
.fa-resource-list {
flex-grow: 1;
min-height: 0;
background-color: var(--fa-panel);
}
.fa-resource-row {
height: 48px;
padding-left: 9px;
padding-right: 7px;
padding-top: 5px;
padding-bottom: 4px;
border-left-width: 3px;
border-left-color: rgba(0, 0, 0, 0);
border-bottom-width: 1px;
border-bottom-color: rgb(32, 35, 39);
}
.fa-resource-row:hover {
background-color: var(--fa-panel-hover);
}
.fa-resource-row--selected {
background-color: rgb(37, 75, 103);
border-left-color: rgb(98, 178, 234);
}
.fa-resource-row--error {
border-left-color: var(--fa-error);
}
.fa-resource-row--warning {
border-left-color: var(--fa-warning);
}
.fa-resource-row__main,
.fa-resource-row__meta,
.fa-badge-row {
flex-direction: row;
align-items: center;
}
.fa-resource-row__main {
height: 20px;
}
.fa-resource-row__name {
flex-grow: 1;
min-width: 0;
-unity-font-style: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.fa-resource-row__id {
max-width: 115px;
margin-left: 5px;
color: rgb(156, 163, 171);
font-size: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.fa-resource-row__meta {
height: 17px;
color: var(--fa-muted);
font-size: 10px;
}
.fa-resource-group {
height: 27px;
padding-left: 8px;
padding-right: 8px;
-unity-text-align: middle-left;
-unity-font-style: bold;
color: rgb(184, 189, 195);
background-color: rgb(41, 44, 49);
border-top-width: 1px;
border-top-color: rgb(52, 56, 61);
border-bottom-width: 1px;
border-bottom-color: rgb(26, 28, 31);
}
.fa-badge {
min-height: 16px;
margin-right: 4px;
padding-left: 5px;
padding-right: 5px;
-unity-text-align: middle-center;
font-size: 9px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
background-color: rgb(52, 56, 61);
color: rgb(174, 180, 187);
}
.fa-badge--blue { background-color: rgb(37, 63, 85); color: rgb(143, 202, 239); }
.fa-badge--green { background-color: rgb(35, 63, 49); color: rgb(131, 209, 160); }
.fa-badge--yellow { background-color: rgb(74, 62, 37); color: rgb(240, 200, 111); }
.fa-badge--red { background-color: rgb(75, 41, 44); color: rgb(239, 141, 147); }
.fa-badge--purple { background-color: rgb(59, 48, 75); color: rgb(195, 168, 232); }
.fa-flow-dot {
width: 8px;
height: 8px;
margin-right: 6px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
.fa-canvas-panel {
flex-grow: 1;
min-width: 280px;
min-height: 0;
flex-direction: column;
background-color: var(--fa-canvas);
}
.fa-canvas-toolbar {
min-height: 32px;
}
.fa-toolbar-label {
margin-left: 2px;
margin-right: 4px;
color: var(--fa-muted);
-unity-text-align: middle-left;
}
.fa-canvas-toolbar #flow-focus-field {
flex-shrink: 1;
}
.fa-flow-focus {
min-width: 110px;
max-width: 240px;
padding-left: 7px;
padding-right: 7px;
-unity-font-style: bold;
-unity-text-align: middle-left;
border-left-width: 3px;
border-left-color: rgba(0, 0, 0, 0);
}
.fa-empty-state {
color: var(--fa-muted);
}
.fa-node {
min-width: 224px;
width: 224px;
max-width: 224px;
min-height: 164px;
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-left-color: var(--fa-line);
border-right-color: var(--fa-line);
border-top-color: var(--fa-line);
border-bottom-color: var(--fa-line);
border-top-left-radius: 5px;
border-top-right-radius: 5px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
background-color: rgb(43, 47, 52);
}
.fa-node .title {
min-height: 31px;
padding-left: 7px;
padding-right: 7px;
background-color: rgb(46, 111, 155);
border-bottom-width: 1px;
border-bottom-color: var(--fa-line);
}
.fa-node .title-label {
-unity-font-style: bold;
}
.fa-node .input,
.fa-node .output {
min-width: 12px;
}
.fa-node__meta {
flex-direction: row;
justify-content: space-between;
margin-left: 8px;
margin-right: 8px;
margin-top: 4px;
margin-bottom: 4px;
color: rgb(175, 181, 188);
font-size: 10px;
}
.fa-node__clip-id {
flex-grow: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.fa-node__behavior {
margin-left: 5px;
color: rgb(201, 205, 210);
}
.fa-node__badges {
min-height: 21px;
margin-left: 8px;
margin-right: 8px;
margin-top: 5px;
flex-direction: row;
align-items: center;
overflow: hidden;
}
.fa-node__entry {
min-width: 16px;
height: 16px;
margin-right: 5px;
padding-left: 3px;
padding-right: 3px;
-unity-text-align: middle-center;
-unity-font-style: bold;
font-size: 9px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
.fa-node__preview {
height: 78px;
margin-left: 8px;
margin-right: 8px;
margin-top: 1px;
}
.fa-node--focused {
border-left-width: 2px;
}
.fa-node.selected {
border-left-width: 2px;
border-right-width: 2px;
border-top-width: 2px;
border-bottom-width: 2px;
border-left-color: rgb(105, 185, 237);
border-right-color: rgb(105, 185, 237);
border-top-color: rgb(105, 185, 237);
border-bottom-color: rgb(105, 185, 237);
}
.fa-node--preview-current {
border-left-color: rgb(55, 174, 240);
border-right-color: rgb(55, 174, 240);
border-top-color: rgb(55, 174, 240);
border-bottom-color: rgb(55, 174, 240);
}
.fa-node--preview-passed { border-left-color: rgb(83, 154, 113); }
.fa-node--preview-upcoming { opacity: 0.72; }
.fa-inspector-scroll,
.fa-bottom-scroll {
flex-grow: 1;
min-height: 0;
background-color: var(--fa-panel);
}
.fa-inspector-title {
padding-left: 11px;
padding-right: 11px;
padding-top: 10px;
padding-bottom: 8px;
border-bottom-width: 1px;
border-bottom-color: var(--fa-line);
}
.fa-inspector-title__name {
font-size: 15px;
-unity-font-style: bold;
}
.fa-inspector-title__id {
margin-top: 3px;
color: var(--fa-muted);
font-size: 10px;
}
.fa-section {
padding-bottom: 8px;
border-bottom-width: 1px;
border-bottom-color: var(--fa-line);
}
.fa-section__title {
height: 27px;
padding-left: 10px;
-unity-text-align: middle-left;
-unity-font-style: bold;
background-color: rgb(41, 44, 48);
}
.fa-section__body {
padding-left: 10px;
padding-right: 10px;
padding-top: 7px;
}
.fa-section__body > .unity-base-field,
.fa-section__body > .unity-button,
.fa-section__body > .fa-readonly-row,
.fa-section__body > .fa-callout {
margin-top: 2px;
margin-bottom: 2px;
}
.fa-readonly-row {
min-height: 22px;
flex-direction: row;
align-items: center;
}
.fa-readonly-row__label {
width: 118px;
color: rgb(173, 179, 186);
}
.fa-readonly-row__value {
flex-grow: 1;
min-width: 0;
color: rgb(197, 202, 208);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.fa-callout {
padding-left: 9px;
padding-right: 9px;
padding-top: 7px;
padding-bottom: 7px;
white-space: normal;
border-left-width: 3px;
border-left-color: var(--fa-accent);
background-color: rgb(34, 48, 58);
color: rgb(188, 209, 223);
}
.fa-callout--warning { border-left-color: var(--fa-warning); background-color: rgb(57, 51, 33); color: rgb(228, 208, 161); }
.fa-callout--error { border-left-color: var(--fa-error); background-color: rgb(63, 39, 42); color: rgb(239, 184, 188); }
.fa-frame-row {
min-height: 72px;
padding-left: 5px;
padding-right: 5px;
padding-top: 4px;
padding-bottom: 4px;
border-bottom-width: 1px;
border-bottom-color: rgb(48, 51, 55);
}
.fa-frame-row__readonly {
flex-direction: row;
}
.fa-frame-row__readonly > .unity-label {
flex-grow: 1;
color: var(--fa-muted);
font-size: 10px;
}
.fa-bottom-panel {
flex-direction: column;
min-height: 28px;
background-color: rgb(32, 35, 39);
border-top-width: 1px;
border-top-color: var(--fa-line);
}
.fa-bottom-toolbar {
min-height: 31px;
padding-left: 4px;
padding-right: 4px;
}
.fa-bottom-tabs {
min-width: 230px;
height: 30px;
flex-direction: row;
align-items: stretch;
}
.fa-bottom-tabs .unity-toolbar-button {
min-width: 108px;
padding-left: 14px;
padding-right: 14px;
}
.fa-bottom-toolbar .fa-bottom-collapse {
width: 28px;
min-width: 28px;
padding-left: 0;
padding-right: 0;
font-size: 14px;
}
.fa-result-controls {
min-height: 31px;
flex-direction: row;
align-items: center;
padding-left: 8px;
padding-right: 8px;
border-bottom-width: 1px;
border-bottom-color: var(--fa-line);
}
.fa-result-header,
.fa-result-row {
min-height: 29px;
flex-direction: row;
align-items: center;
padding-left: 10px;
padding-right: 10px;
border-bottom-width: 1px;
border-bottom-color: rgb(41, 44, 48);
}
.fa-result-header {
min-height: 26px;
color: var(--fa-muted);
background-color: rgb(40, 43, 48);
}
.fa-result-row:hover { background-color: var(--fa-panel-hover); }
.fa-result-severity { width: 82px; -unity-font-style: bold; }
.fa-result-kind { width: 135px; }
.fa-result-object { width: 180px; }
.fa-result-message { flex-grow: 1; min-width: 260px; white-space: normal; }
.fa-severity--error { color: var(--fa-error); }
.fa-severity--warning { color: var(--fa-warning); }
.fa-severity--info { color: rgb(114, 189, 232); }
.fa-preview {
position: relative;
overflow: hidden;
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-left-color: rgb(25, 25, 25);
border-right-color: rgb(25, 25, 25);
border-top-color: rgb(25, 25, 25);
border-bottom-color: rgb(25, 25, 25);
}
.fa-preview__image {
position: absolute;
}
.fa-preview__empty {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
-unity-text-align: middle-center;
color: rgb(190, 190, 190);
}
.fa-clip-preview-panel {
flex-shrink: 0;
padding-left: 6px;
padding-right: 6px;
padding-top: 4px;
padding-bottom: 5px;
border-bottom-width: 1px;
border-bottom-color: rgb(28, 28, 28);
}
.fa-selection-preview-header {
min-height: 34px;
flex-direction: column;
justify-content: center;
}
.fa-selection-preview-title {
color: var(--fa-text);
-unity-font-style: bold;
}
.fa-selection-preview-target {
margin-top: 1px;
color: var(--fa-muted);
font-size: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.fa-clip-preview-panel .fa-preview {
height: 190px;
margin-bottom: 4px;
}
.fa-preview-controls {
flex-direction: row;
align-items: center;
flex-shrink: 0;
min-height: 27px;
padding-left: 0;
padding-right: 0;
}
.fa-selection-preview-panel .fa-preview-controls .fa-preview-button {
width: 24px;
min-width: 24px;
max-width: 24px;
height: 24px;
min-height: 24px;
max-height: 24px;
padding-left: 0;
padding-right: 0;
padding-top: 0;
padding-bottom: 0;
margin-left: 1px;
margin-right: 1px;
flex-shrink: 0;
align-items: center;
justify-content: center;
}
.fa-preview-button__icon {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.fa-preview-timeline {
min-width: 28px;
flex-grow: 1;
flex-shrink: 1;
margin-left: 5px;
margin-right: 5px;
}
.fa-preview-time {
width: 82px;
min-width: 82px;
flex-shrink: 0;
white-space: nowrap;
-unity-text-align: middle-right;
}
.fa-preview-settings {
min-height: 27px;
flex-direction: row;
align-items: center;
}
.fa-preview-setting {
flex-grow: 1;
flex-basis: 0;
min-width: 0;
margin-right: 5px;
flex-direction: row;
align-items: center;
}
.fa-preview-setting > .unity-label {
width: 64px;
min-width: 64px;
color: var(--fa-muted);
font-size: 10px;
}
.fa-preview-setting__field {
flex-grow: 1;
min-width: 0;
}
.fa-preview-status {
font-size: 10px;
color: rgb(185, 185, 185);
margin-top: 2px;
white-space: normal;
}
@@ -1,9 +0,0 @@
fileFormatVersion: 2
guid: a38dd39c699e42348667cc1264ea9076
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ui:UXML xmlns:ui="UnityEngine.UIElements" editor-extension-mode="True">
<ui:VisualElement name="fa-workbench" class="fa-workbench">
<ui:VisualElement name="global-toolbar-host" class="fa-global-toolbar-host" />
<ui:VisualElement name="main-host" class="fa-main-host" />
<ui:VisualElement name="bottom-host" class="fa-bottom-host" />
</ui:VisualElement>
</ui:UXML>
@@ -1,10 +0,0 @@
fileFormatVersion: 2
guid: 3286d11c482fe8640a72c689bed6780c
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

Some files were not shown because too many files have changed in this diff Show More