1041 lines
30 KiB
Markdown
1041 lines
30 KiB
Markdown
# FlowScope Game Core P1 Production Hardening Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 在 P0 全绿和人工验收完成后,交付第一批 P1 生产化能力:资源后端适配层、配置/存档扩展点、轻量 UI 导航与预加载、MainMenu 样例验收升级,以及框架使用规范。
|
||
|
||
**Architecture:** P1 不重写 P0 核心,不引入全局 EventBus、大型 DI、完整包生态或编辑器工具链。所有新增能力优先通过小接口和适配层扩展,保持 `FlowScope.Runtime` 主接口稳定;涉及 Addressables 的强依赖从核心 Runtime 中拆出为独立适配程序集。每个任务独立提交,最终由集成任务跑全量 EditMode、PlayMode 和人工样例验收。
|
||
|
||
**Tech Stack:** Unity 2022.3.62f2c1, C#, Unity Test Framework, R3, Addressables, Task/CancellationToken, asmdef, JSON 本地配置/存档。
|
||
|
||
---
|
||
|
||
## P1 范围确认
|
||
|
||
### 本轮 P1 包含
|
||
|
||
1. 资源生产化第一步:引入 `IResourceBackend`,将 Addressables 反射适配从 `FlowScope.Runtime` 拆到 `FlowScope.Addressables`。
|
||
2. 配置扩展点:引入 `IConfigSource` 与 `IConfigParser`,让 JSON provider 从“硬编码 JSON 文本映射”变成“source + parser”的组合。
|
||
3. 存档迁移扩展点:引入 `ISaveMigration` 与带版本包裹的 JSON serializer,支持单 key 数据从旧版本迁移到当前版本。
|
||
4. UI 生产化第一步:引入轻量 `IUIScreenNavigator` 与 panel 预加载服务,仍保持 `UIManager` 的层内 LIFO 规则。
|
||
5. MainMenuP0 升级为 P1 验收样例:验证 UI 打开、Gold 点击递增、保存恢复、资源/订阅释放。
|
||
6. 使用规范文档:Bootstrap、Feature 生命周期、资源释放、UI 路径、配置 DTO、存档迁移、资源后端接入。
|
||
|
||
### 本轮 P1 不包含
|
||
|
||
- Luban 完整接入。
|
||
- YooAsset / AssetBundle 后端实现。
|
||
- 云存档、加密、压缩、冲突解决。
|
||
- 完整 UI 路由系统、URL 式导航、跨 Feature 页面恢复。
|
||
- AudioMixer、3D 音频、动态音乐。
|
||
- Unity Package 分发结构、Samples~、编辑器导入向导。
|
||
- Source Generator 深度优化。
|
||
|
||
---
|
||
|
||
## 推荐工作流
|
||
|
||
1. 从当前 `main` 创建 `p1-contracts` worktree/branch。
|
||
2. Task 0 完成并合并后,再并行执行 Task A、Task B、Task C。
|
||
3. Task D 依赖 A/B/C 合并后执行。
|
||
4. Task E 做最终集成、文档同步和验收。
|
||
5. 所有 plan、汇报、commit message 使用中文。
|
||
|
||
推荐 worktrees:
|
||
|
||
| Worktree | Purpose |
|
||
| --- | --- |
|
||
| `p1-contracts` | P1 小接口、asmdef 边界、文档边界 |
|
||
| `p1-resource-backend` | `IResourceBackend` 与 Addressables 独立适配 |
|
||
| `p1-config-save-extensions` | `IConfigSource`、`IConfigParser`、`ISaveMigration` |
|
||
| `p1-ui-navigation-preload` | `IUIScreenNavigator` 与 Panel 预加载 |
|
||
| `p1-sample-docs` | MainMenuP0 验收升级和使用规范 |
|
||
| `p1-integration` | 合并、修缝、全量验证 |
|
||
|
||
---
|
||
|
||
## Task 0: P1 Contracts Worktree
|
||
|
||
**Worktree:** `p1-contracts`
|
||
|
||
**Goal:** 先稳定 P1 的接口边界,让后续并行任务不互相改核心契约。
|
||
|
||
**Owned files:**
|
||
- Create: `My project/Assets/FlowScope/Runtime/Resources/IResourceBackend.cs`
|
||
- Create: `My project/Assets/FlowScope/Runtime/Config/IConfigSource.cs`
|
||
- Create: `My project/Assets/FlowScope/Runtime/Config/IConfigParser.cs`
|
||
- Create: `My project/Assets/FlowScope/Runtime/Save/ISaveMigration.cs`
|
||
- Create: `My project/Assets/FlowScope/Runtime/UI/IUIScreenNavigator.cs`
|
||
- Create: `My project/Assets/FlowScope/Runtime/UI/IUIPreloadService.cs`
|
||
- Modify: `docs/requirements/p0-requirements-set.md`
|
||
- Create: `docs/requirements/p1-production-hardening.md`
|
||
|
||
**Do not modify:**
|
||
- `My project/Assets/FlowScope/Runtime/Resources/AddressablesResourceService.cs`
|
||
- `My project/Assets/FlowScope/Runtime/Config/JsonConfigProvider.cs`
|
||
- `My project/Assets/FlowScope/Runtime/Save/SaveService.cs`
|
||
- `My project/Assets/FlowScope/Runtime/UI/UIManager.cs`
|
||
- Any sample scene or prefab files.
|
||
|
||
- [ ] **Step 1: Create `IResourceBackend` contract**
|
||
|
||
Create `My project/Assets/FlowScope/Runtime/Resources/IResourceBackend.cs`:
|
||
|
||
```csharp
|
||
using System;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
|
||
namespace FlowScope.Resources
|
||
{
|
||
public interface IResourceBackend
|
||
{
|
||
string Name { get; }
|
||
|
||
Task<object> LoadAsync(
|
||
string key,
|
||
Type assetType,
|
||
CancellationToken cancellationToken);
|
||
|
||
void Release(string key, Type assetType, object asset);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Create config extension contracts**
|
||
|
||
Create `My project/Assets/FlowScope/Runtime/Config/IConfigSource.cs`:
|
||
|
||
```csharp
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
|
||
namespace FlowScope.Config
|
||
{
|
||
public interface IConfigSource
|
||
{
|
||
string Name { get; }
|
||
|
||
Task<string> LoadTextAsync(
|
||
string fileName,
|
||
CancellationToken cancellationToken);
|
||
}
|
||
}
|
||
```
|
||
|
||
Create `My project/Assets/FlowScope/Runtime/Config/IConfigParser.cs`:
|
||
|
||
```csharp
|
||
using System;
|
||
using System.Collections.Generic;
|
||
|
||
namespace FlowScope.Config
|
||
{
|
||
public interface IConfigParser
|
||
{
|
||
string Format { get; }
|
||
|
||
IReadOnlyList<T> ParseRows<T>(
|
||
string fileName,
|
||
string text)
|
||
where T : class, IConfigRow;
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Create save migration contract**
|
||
|
||
Create `My project/Assets/FlowScope/Runtime/Save/ISaveMigration.cs`:
|
||
|
||
```csharp
|
||
namespace FlowScope.Save
|
||
{
|
||
public interface ISaveMigration
|
||
{
|
||
string Key { get; }
|
||
int FromVersion { get; }
|
||
int ToVersion { get; }
|
||
|
||
string Migrate(string json);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: Create UI navigation and preload contracts**
|
||
|
||
Create `My project/Assets/FlowScope/Runtime/UI/IUIScreenNavigator.cs`:
|
||
|
||
```csharp
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
|
||
namespace FlowScope.UI
|
||
{
|
||
public interface IUIScreenNavigator
|
||
{
|
||
Task<TPanel> PushAsync<TPanel, TViewModel>(
|
||
TViewModel viewModel,
|
||
CancellationToken cancellationToken)
|
||
where TPanel : UIPanelBase<TViewModel>;
|
||
|
||
bool CanGoBack { get; }
|
||
|
||
void GoBack();
|
||
|
||
void Clear();
|
||
}
|
||
}
|
||
```
|
||
|
||
Create `My project/Assets/FlowScope/Runtime/UI/IUIPreloadService.cs`:
|
||
|
||
```csharp
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
|
||
namespace FlowScope.UI
|
||
{
|
||
public interface IUIPreloadService
|
||
{
|
||
Task PreloadAsync<TPanel>(CancellationToken cancellationToken);
|
||
bool IsPreloaded<TPanel>();
|
||
void Release<TPanel>();
|
||
void ReleaseAll();
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5: Write P1 requirement summary**
|
||
|
||
Create `docs/requirements/p1-production-hardening.md` with these sections:
|
||
|
||
```markdown
|
||
# FlowScope Game Core P1 生产化需求
|
||
|
||
## 目标
|
||
|
||
P1 在 P0 核心接口稳定后推进,目标是补齐第一批生产化扩展点,而不是扩张成完整框架生态。
|
||
|
||
## 本轮范围
|
||
|
||
- `IResourceBackend` 与 Addressables 独立适配。
|
||
- `IConfigSource` / `IConfigParser` 配置扩展点。
|
||
- `ISaveMigration` 存档迁移扩展点。
|
||
- `IUIScreenNavigator` 与 Panel 预加载。
|
||
- MainMenuP0 样例验收升级。
|
||
- 使用规范文档。
|
||
|
||
## 不进入本轮
|
||
|
||
- Luban 完整接入。
|
||
- YooAsset / AssetBundle 后端实现。
|
||
- 云存档。
|
||
- AudioMixer / 3D 音频。
|
||
- Package 分发。
|
||
```
|
||
|
||
- [ ] **Step 6: Compile contracts**
|
||
|
||
Run:
|
||
|
||
```powershell
|
||
dotnet build "My project\FlowScope.Runtime.csproj" --no-restore
|
||
```
|
||
|
||
Expected: `0 个错误`. Existing Unity package reference warnings are acceptable.
|
||
|
||
- [ ] **Step 7: Commit contracts**
|
||
|
||
```powershell
|
||
git add "My project/Assets/FlowScope/Runtime/Resources/IResourceBackend.cs" `
|
||
"My project/Assets/FlowScope/Runtime/Config/IConfigSource.cs" `
|
||
"My project/Assets/FlowScope/Runtime/Config/IConfigParser.cs" `
|
||
"My project/Assets/FlowScope/Runtime/Save/ISaveMigration.cs" `
|
||
"My project/Assets/FlowScope/Runtime/UI/IUIScreenNavigator.cs" `
|
||
"My project/Assets/FlowScope/Runtime/UI/IUIPreloadService.cs" `
|
||
"docs/requirements/p1-production-hardening.md" `
|
||
"docs/requirements/p0-requirements-set.md"
|
||
git commit -m "定义 Game Core P1 生产化契约"
|
||
```
|
||
|
||
---
|
||
|
||
## Task A: Resource Backend Split
|
||
|
||
**Worktree:** `p1-resource-backend`
|
||
|
||
**Depends on:** Task 0
|
||
|
||
**Goal:** 让核心资源服务只依赖 `IResourceBackend`,将 Addressables 具体依赖放进独立 asmdef,降低 Runtime 核心升级风险。
|
||
|
||
**Owned files:**
|
||
- Modify: `My project/Assets/FlowScope/Runtime/Resources/AddressablesResourceService.cs`
|
||
- Create: `My project/Assets/FlowScope/Runtime/Resources/ResourceService.cs`
|
||
- Create: `My project/Assets/FlowScope/Addressables/FlowScope.Addressables.asmdef`
|
||
- Create: `My project/Assets/FlowScope/Addressables/AddressablesResourceBackend.cs`
|
||
- Create: `My project/Assets/FlowScope/Tests/EditMode/Resources/ResourceServiceBackendTests.cs`
|
||
- Create: `My project/Assets/FlowScope/Tests/PlayMode/Resources/AddressablesResourceBackendTests.cs`
|
||
- Modify: `My project/Assets/FlowScope/Tests/PlayMode/FlowScope.Tests.PlayMode.asmdef`
|
||
|
||
**Do not modify:**
|
||
- Config, Save, UI, Audio, Flow runtime files.
|
||
- MainMenu sample files.
|
||
|
||
- [ ] **Step 1: Write backend service edit mode tests**
|
||
|
||
Create `My project/Assets/FlowScope/Tests/EditMode/Resources/ResourceServiceBackendTests.cs` with tests for:
|
||
|
||
```csharp
|
||
[Test]
|
||
public void LoadAsync_WhenSameKeyLoadsConcurrently_UsesOneBackendLoad()
|
||
|
||
[Test]
|
||
public void LoadAsync_WhenCanceled_DoesNotCreateHandle()
|
||
|
||
[Test]
|
||
public void DisposeLastHandle_ReleasesBackendAsset()
|
||
|
||
[Test]
|
||
public void CreateGroup_DisposesAllHandles()
|
||
```
|
||
|
||
Use a fake backend:
|
||
|
||
```csharp
|
||
private sealed class FakeResourceBackend : IResourceBackend
|
||
{
|
||
public string Name => "Fake";
|
||
public int LoadCalls { get; private set; }
|
||
public int ReleaseCalls { get; private set; }
|
||
public TaskCompletionSource<object> Gate { get; } = new();
|
||
|
||
public async Task<object> LoadAsync(string key, Type assetType, CancellationToken cancellationToken)
|
||
{
|
||
LoadCalls++;
|
||
return await Gate.Task;
|
||
}
|
||
|
||
public void Release(string key, Type assetType, object asset)
|
||
{
|
||
ReleaseCalls++;
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Implement backend-based `ResourceService`**
|
||
|
||
Create `ResourceService` by moving current reference counting, shared load, group creation, and completed-load behavior from `AddressablesResourceService` into:
|
||
|
||
```csharp
|
||
public sealed class ResourceService : IResourceService, ICompletedResourceService
|
||
{
|
||
public ResourceService(IResourceBackend backend)
|
||
{
|
||
_backend = backend ?? throw new ArgumentNullException(nameof(backend));
|
||
}
|
||
}
|
||
```
|
||
|
||
Keep P0 semantics:
|
||
|
||
- Same key + type concurrent load shares backend load.
|
||
- Each caller receives an independent handle.
|
||
- Last handle disposal calls backend release.
|
||
- Canceled waiter does not prevent other waiters from completing.
|
||
- If all waiters cancel before backend completes, release completed asset when backend eventually resolves.
|
||
|
||
- [ ] **Step 3: Convert `AddressablesResourceService` into compatibility wrapper**
|
||
|
||
Modify `AddressablesResourceService` to preserve existing API:
|
||
|
||
```csharp
|
||
public sealed class AddressablesResourceService : IResourceService, ICompletedResourceService
|
||
{
|
||
private readonly ResourceService _inner;
|
||
|
||
public AddressablesResourceService()
|
||
: this(new AddressablesResourceBackend())
|
||
{
|
||
}
|
||
|
||
public AddressablesResourceService(IResourceBackend backend)
|
||
{
|
||
_inner = new ResourceService(backend);
|
||
}
|
||
}
|
||
```
|
||
|
||
If `AddressablesResourceBackend` lives in a separate asmdef and cannot be referenced by Runtime, keep this wrapper in `FlowScope.Addressables` instead and update usages in sample/integration. Do not keep reflection backend in `FlowScope.Runtime`.
|
||
|
||
- [ ] **Step 4: Create Addressables asmdef**
|
||
|
||
Create `My project/Assets/FlowScope/Addressables/FlowScope.Addressables.asmdef`:
|
||
|
||
```json
|
||
{
|
||
"name": "FlowScope.Addressables",
|
||
"rootNamespace": "FlowScope.Addressables",
|
||
"references": [
|
||
"FlowScope.Runtime",
|
||
"Unity.Addressables"
|
||
],
|
||
"includePlatforms": [],
|
||
"excludePlatforms": [],
|
||
"allowUnsafeCode": false,
|
||
"overrideReferences": false,
|
||
"precompiledReferences": [],
|
||
"autoReferenced": true,
|
||
"defineConstraints": [],
|
||
"versionDefines": [],
|
||
"noEngineReferences": false
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5: Implement typed Addressables backend**
|
||
|
||
Create `AddressablesResourceBackend` using direct API references:
|
||
|
||
```csharp
|
||
using System;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using FlowScope.Resources;
|
||
using UnityEngine.AddressableAssets;
|
||
|
||
namespace FlowScope.Addressables
|
||
{
|
||
public sealed class AddressablesResourceBackend : IResourceBackend
|
||
{
|
||
public string Name => "Addressables";
|
||
|
||
public async Task<object> LoadAsync(string key, Type assetType, CancellationToken cancellationToken)
|
||
{
|
||
var method = typeof(AddressablesResourceBackend)
|
||
.GetMethod(nameof(LoadTypedAsync), System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)
|
||
.MakeGenericMethod(assetType);
|
||
return await (Task<object>)method.Invoke(null, new object[] { key, cancellationToken });
|
||
}
|
||
|
||
public void Release(string key, Type assetType, object asset)
|
||
{
|
||
Addressables.Release(asset);
|
||
}
|
||
|
||
private static async Task<object> LoadTypedAsync<T>(string key, CancellationToken cancellationToken)
|
||
{
|
||
var handle = Addressables.LoadAssetAsync<T>(key);
|
||
await WaitWithCancellation(handle.Task, cancellationToken);
|
||
return handle.Result;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Add `WaitWithCancellation` helper matching P0 cancellation semantics.
|
||
|
||
- [ ] **Step 6: Run resource tests**
|
||
|
||
Run:
|
||
|
||
```powershell
|
||
dotnet build "My project\FlowScope.Tests.EditMode.csproj" --no-restore
|
||
dotnet build "My project\FlowScope.Tests.PlayMode.csproj" --no-restore
|
||
```
|
||
|
||
Expected: both builds pass with `0 个错误`.
|
||
|
||
- [ ] **Step 7: Commit**
|
||
|
||
```powershell
|
||
git add "My project/Assets/FlowScope/Runtime/Resources" `
|
||
"My project/Assets/FlowScope/Addressables" `
|
||
"My project/Assets/FlowScope/Tests/EditMode/Resources" `
|
||
"My project/Assets/FlowScope/Tests/PlayMode/Resources" `
|
||
"My project/Assets/FlowScope/Tests/PlayMode/FlowScope.Tests.PlayMode.asmdef"
|
||
git commit -m "拆分 P1 资源后端适配层"
|
||
```
|
||
|
||
---
|
||
|
||
## Task B: Config Source, Parser, and Save Migration
|
||
|
||
**Worktree:** `p1-config-save-extensions`
|
||
|
||
**Depends on:** Task 0
|
||
|
||
**Goal:** 给配置和存档补扩展点,但不接入 Luban、云存档或复杂对象图。
|
||
|
||
**Owned files:**
|
||
- Modify: `My project/Assets/FlowScope/Runtime/Config/JsonConfigProvider.cs`
|
||
- Create: `My project/Assets/FlowScope/Runtime/Config/JsonConfigParser.cs`
|
||
- Create: `My project/Assets/FlowScope/Runtime/Config/InMemoryConfigSource.cs`
|
||
- Create: `My project/Assets/FlowScope/Runtime/Config/FileConfigSource.cs`
|
||
- Modify: `My project/Assets/FlowScope/Runtime/Save/JsonSaveSerializer.cs`
|
||
- Create: `My project/Assets/FlowScope/Runtime/Save/SaveEnvelope.cs`
|
||
- Create: `My project/Assets/FlowScope/Runtime/Save/SaveMigrationRegistry.cs`
|
||
- Test: `My project/Assets/FlowScope/Tests/EditMode/Config/ConfigExtensionTests.cs`
|
||
- Test: `My project/Assets/FlowScope/Tests/EditMode/Save/SaveMigrationTests.cs`
|
||
|
||
**Do not modify:**
|
||
- Container, Resource, UI, Audio, Flow implementation files.
|
||
- Existing public `ISaveService` signature.
|
||
|
||
- [ ] **Step 1: Write config extension tests**
|
||
|
||
Create tests:
|
||
|
||
```csharp
|
||
[Test]
|
||
public void LoadAllAsync_UsesConfiguredSourceAndParser()
|
||
|
||
[Test]
|
||
public void LoadAllAsync_WhenSourceFails_IncludesSourceAndFileName()
|
||
|
||
[Test]
|
||
public void LoadAllAsync_WhenParserFindsDuplicateId_IncludesFileName()
|
||
```
|
||
|
||
Expected behavior:
|
||
|
||
- `JsonConfigProvider.Mapping<T>("player.json", source, parser)` loads from source by file name.
|
||
- Parser returns rows and provider indexes by `Id`.
|
||
- Duplicate ids still throw.
|
||
|
||
- [ ] **Step 2: Implement `JsonConfigParser`**
|
||
|
||
Create parser that wraps existing `ReactivePropertyJsonConverter.DeserializeArray<T>`:
|
||
|
||
```csharp
|
||
public sealed class JsonConfigParser : IConfigParser
|
||
{
|
||
public string Format => "json";
|
||
|
||
public IReadOnlyList<T> ParseRows<T>(string fileName, string text)
|
||
where T : class, IConfigRow
|
||
{
|
||
return ReactivePropertyJsonConverter.DeserializeArray<T>(text);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Implement config sources**
|
||
|
||
Create `InMemoryConfigSource` for tests and simple samples:
|
||
|
||
```csharp
|
||
public sealed class InMemoryConfigSource : IConfigSource
|
||
{
|
||
private readonly Dictionary<string, string> _files;
|
||
public string Name { get; }
|
||
|
||
public Task<string> LoadTextAsync(string fileName, CancellationToken cancellationToken)
|
||
{
|
||
cancellationToken.ThrowIfCancellationRequested();
|
||
if (!_files.TryGetValue(fileName, out var text))
|
||
{
|
||
throw new InvalidOperationException($"Config file is missing: {fileName}");
|
||
}
|
||
return Task.FromResult(text);
|
||
}
|
||
}
|
||
```
|
||
|
||
Create `FileConfigSource` using `File.ReadAllTextAsync` when available; otherwise use `Task.Run(() => File.ReadAllText(path), cancellationToken)`.
|
||
|
||
- [ ] **Step 4: Extend `JsonConfigProvider.ConfigMapping` without breaking P0 API**
|
||
|
||
Keep current API:
|
||
|
||
```csharp
|
||
JsonConfigProvider.Mapping<T>(string fileName, Func<CancellationToken, Task<string>> loadTextAsync)
|
||
```
|
||
|
||
Add overload:
|
||
|
||
```csharp
|
||
JsonConfigProvider.Mapping<T>(
|
||
string fileName,
|
||
IConfigSource source,
|
||
IConfigParser parser)
|
||
where T : class, IConfigRow
|
||
```
|
||
|
||
The old overload should internally wrap `loadTextAsync` as an `IConfigSource` and use `JsonConfigParser`.
|
||
|
||
- [ ] **Step 5: Write save migration tests**
|
||
|
||
Create tests:
|
||
|
||
```csharp
|
||
[Test]
|
||
public void Deserialize_WhenEnvelopeVersionIsOld_AppliesMigrationChain()
|
||
|
||
[Test]
|
||
public void Deserialize_WhenMigrationIsMissing_ThrowsWithKeyAndVersion()
|
||
|
||
[Test]
|
||
public void Serialize_WritesEnvelopeWithCurrentVersion()
|
||
```
|
||
|
||
Use a fake data class:
|
||
|
||
```csharp
|
||
private sealed class PlayerSaveV2
|
||
{
|
||
public R3.ReactiveProperty<int> Gold { get; set; } = new();
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 6: Implement save envelope and migration registry**
|
||
|
||
Create `SaveEnvelope`:
|
||
|
||
```csharp
|
||
internal sealed class SaveEnvelope
|
||
{
|
||
public int Version { get; set; }
|
||
public string Payload { get; set; }
|
||
}
|
||
```
|
||
|
||
Create `SaveMigrationRegistry`:
|
||
|
||
```csharp
|
||
public sealed class SaveMigrationRegistry
|
||
{
|
||
public void Register(ISaveMigration migration);
|
||
|
||
public string Migrate(
|
||
string key,
|
||
int fromVersion,
|
||
int currentVersion,
|
||
string json);
|
||
}
|
||
```
|
||
|
||
Migration chain must apply `fromVersion -> fromVersion + 1` until `currentVersion`. Missing step throws `InvalidOperationException` containing key and version.
|
||
|
||
- [ ] **Step 7: Extend `JsonSaveSerializer`**
|
||
|
||
Add constructor:
|
||
|
||
```csharp
|
||
public JsonSaveSerializer(
|
||
int currentVersion = 1,
|
||
SaveMigrationRegistry migrations = null)
|
||
```
|
||
|
||
Behavior:
|
||
|
||
- Existing raw JSON saves remain readable as version `1`.
|
||
- New saves are wrapped in envelope when `currentVersion > 1`.
|
||
- Migration applies before `PopulateObject`.
|
||
|
||
- [ ] **Step 8: Run config/save tests**
|
||
|
||
Run:
|
||
|
||
```powershell
|
||
dotnet build "My project\FlowScope.Tests.EditMode.csproj" --no-restore
|
||
```
|
||
|
||
Expected: `0 个错误`.
|
||
|
||
- [ ] **Step 9: Commit**
|
||
|
||
```powershell
|
||
git add "My project/Assets/FlowScope/Runtime/Config" `
|
||
"My project/Assets/FlowScope/Runtime/Save" `
|
||
"My project/Assets/FlowScope/Tests/EditMode/Config" `
|
||
"My project/Assets/FlowScope/Tests/EditMode/Save"
|
||
git commit -m "扩展 P1 配置来源和存档迁移"
|
||
```
|
||
|
||
---
|
||
|
||
## Task C: UI Navigation and Panel Preload
|
||
|
||
**Worktree:** `p1-ui-navigation-preload`
|
||
|
||
**Depends on:** Task 0
|
||
|
||
**Goal:** 在不破坏 P0 `UIManager` 层内 LIFO 的前提下,提供轻量页面返回策略和 Panel 预加载。
|
||
|
||
**Owned files:**
|
||
- Modify: `My project/Assets/FlowScope/Runtime/UI/UIManager.cs`
|
||
- Create: `My project/Assets/FlowScope/Runtime/UI/UIPreloadService.cs`
|
||
- Create: `My project/Assets/FlowScope/Runtime/UI/UIScreenNavigator.cs`
|
||
- Test: `My project/Assets/FlowScope/Tests/PlayMode/UI/UIPreloadServiceTests.cs`
|
||
- Test: `My project/Assets/FlowScope/Tests/PlayMode/UI/UIScreenNavigatorTests.cs`
|
||
- Modify: `My project/Assets/FlowScope/Tests/PlayMode/UI/UIManagerTests.cs`
|
||
|
||
**Do not modify:**
|
||
- Resource service internals.
|
||
- MainMenu sample except in Task D.
|
||
- UI panel base contract unless Task 0 contract is insufficient and integration owner approves.
|
||
|
||
- [ ] **Step 1: Write preload tests**
|
||
|
||
Create tests:
|
||
|
||
```csharp
|
||
[UnityTest]
|
||
public IEnumerator PreloadAsync_LoadsPanelResourceAndKeepsHandle()
|
||
|
||
[UnityTest]
|
||
public IEnumerator OpenAsync_WhenPanelIsPreloaded_ReusesPreloadedHandle()
|
||
|
||
[UnityTest]
|
||
public IEnumerator Release_DisposesPreloadedHandle()
|
||
```
|
||
|
||
Fake resource service must count load and dispose calls.
|
||
|
||
- [ ] **Step 2: Implement `UIPreloadService`**
|
||
|
||
Behavior:
|
||
|
||
- Resolve panel path using same rules as `UIManager`.
|
||
- Load `GameObject` handle and store by panel type.
|
||
- Repeated preload for same panel is no-op.
|
||
- `Release<TPanel>` disposes that handle.
|
||
- `Dispose` releases all preloaded handles.
|
||
|
||
- [ ] **Step 3: Add optional preload lookup to `UIManager`**
|
||
|
||
Add constructor overload:
|
||
|
||
```csharp
|
||
public UIManager(IResourceService resources, IUIPreloadService preloadService = null)
|
||
```
|
||
|
||
Open behavior:
|
||
|
||
- If preload service has a handle for the requested panel, create an additional handle only if the resource service contract requires independent handle ownership.
|
||
- If preloaded handle can be safely reused, keep preload handle owned by preload service and let panel record use a lightweight retained handle.
|
||
- Tests must prove closing a preloaded Destroy panel does not dispose the preload cache.
|
||
|
||
- [ ] **Step 4: Write navigator tests**
|
||
|
||
Create tests:
|
||
|
||
```csharp
|
||
[UnityTest]
|
||
public IEnumerator PushAsync_OpensPanelAndRecordsHistory()
|
||
|
||
[UnityTest]
|
||
public IEnumerator GoBack_ClosesTopPanel()
|
||
|
||
[UnityTest]
|
||
public IEnumerator Clear_ClosesAllNavigatorPanels()
|
||
|
||
[UnityTest]
|
||
public IEnumerator GoBack_WhenOnlyOnePanelOpen_DoesNothing()
|
||
```
|
||
|
||
- [ ] **Step 5: Implement `UIScreenNavigator`**
|
||
|
||
Create `UIScreenNavigator`:
|
||
|
||
```csharp
|
||
public sealed class UIScreenNavigator : IUIScreenNavigator
|
||
{
|
||
private readonly UIManager _uiManager;
|
||
private readonly Stack<Action> _closeStack = new();
|
||
}
|
||
```
|
||
|
||
Rules:
|
||
|
||
- `PushAsync<TPanel, TViewModel>` calls `UIManager.OpenAsync`.
|
||
- It pushes a close action that calls `UIManager.Close<TPanel>()`.
|
||
- `CanGoBack` is true when close stack count is greater than 1.
|
||
- `GoBack` closes only top screen if `CanGoBack` is true.
|
||
- `Clear` closes all panels opened by navigator.
|
||
- It does not close panels opened outside navigator.
|
||
|
||
- [ ] **Step 6: Run UI play mode build**
|
||
|
||
Run:
|
||
|
||
```powershell
|
||
dotnet build "My project\FlowScope.Tests.PlayMode.csproj" --no-restore
|
||
```
|
||
|
||
Expected: `0 个错误`.
|
||
|
||
- [ ] **Step 7: Commit**
|
||
|
||
```powershell
|
||
git add "My project/Assets/FlowScope/Runtime/UI" `
|
||
"My project/Assets/FlowScope/Tests/PlayMode/UI"
|
||
git commit -m "扩展 P1 UI 导航和预加载"
|
||
```
|
||
|
||
---
|
||
|
||
## Task D: MainMenuP0 Sample and Usage Docs
|
||
|
||
**Worktree:** `p1-sample-docs`
|
||
|
||
**Depends on:** Task A, Task B, Task C
|
||
|
||
**Goal:** 让样例成为 P1 规则的可见验收样板,并补清楚框架使用约定。
|
||
|
||
**Owned files:**
|
||
- Modify: `My project/Assets/FlowScope/Samples/MainMenuP0/Scripts/GameBootstrap.cs`
|
||
- Modify: `My project/Assets/FlowScope/Samples/MainMenuP0/Scripts/MainMenuFeature.cs`
|
||
- Modify: `My project/Assets/FlowScope/Samples/MainMenuP0/Scripts/MainMenuPanel.cs`
|
||
- Modify: `My project/Assets/FlowScope/Samples/MainMenuP0/Scenes/MainMenuP0.unity`
|
||
- Modify: `My project/Assets/FlowScope/Samples/MainMenuP0/Prefabs/MainMenuPanel.prefab`
|
||
- Modify: `My project/Assets/FlowScope/Tests/PlayMode/Samples/MainMenuP0Tests.cs`
|
||
- Create: `docs/guides/flowscope-runtime-usage.md`
|
||
- Create: `docs/guides/main-menu-p1-sample.md`
|
||
|
||
**Do not modify:**
|
||
- Core Resource, Config, Save, UI implementations except through public APIs.
|
||
- Test assembly asmdefs unless a new adapter assembly reference is required.
|
||
|
||
- [ ] **Step 1: Write upgraded sample play mode tests**
|
||
|
||
Update `MainMenuP0Tests.cs` to include:
|
||
|
||
```csharp
|
||
[UnityTest]
|
||
public IEnumerator Bootstrap_StartsAndOpensVisibleMainMenuPanel()
|
||
|
||
[UnityTest]
|
||
public IEnumerator ClickIncrement_UpdatesGoldTextImmediately()
|
||
|
||
[UnityTest]
|
||
public IEnumerator ShutdownAndRestart_RestoresSavedGold()
|
||
|
||
[UnityTest]
|
||
public IEnumerator Shutdown_ReleasesFeatureResourcesAndSubscriptions()
|
||
```
|
||
|
||
Tests may instantiate `GameBootstrap` in an inactive root and call `StartupAsync` manually to avoid `Awake async void` races.
|
||
|
||
- [ ] **Step 2: Update `GameBootstrap` registration to use P1 extension points**
|
||
|
||
Use:
|
||
|
||
- `IConfigSource` / `JsonConfigParser` for config.
|
||
- `JsonSaveSerializer(currentVersion: 1, migrations: registry)` for save.
|
||
- `ResourceService` with sample backend or Addressables backend depending on Task A outcome.
|
||
- `UIPreloadService` and `UIScreenNavigator` if sample flow uses navigation.
|
||
|
||
Keep no-console-error behavior.
|
||
|
||
- [ ] **Step 3: Make sample UI visibly meaningful**
|
||
|
||
Ensure scene/prefab contains:
|
||
|
||
- A Canvas.
|
||
- A configured `MainMenuPanel`.
|
||
- A visible Gold text.
|
||
- A visible increment button.
|
||
- Serialized references wired in prefab or scene where appropriate.
|
||
|
||
Runtime fallback creation may remain, but manual sample validation should use real scene/prefab references.
|
||
|
||
- [ ] **Step 4: Write runtime usage guide**
|
||
|
||
Create `docs/guides/flowscope-runtime-usage.md` with sections:
|
||
|
||
```markdown
|
||
# FlowScope Runtime 使用规范
|
||
|
||
## Bootstrap
|
||
## Container 注册规则
|
||
## Feature 生命周期
|
||
## 资源所有权
|
||
## UI 路径和层级
|
||
## 配置 DTO 约束
|
||
## 存档版本迁移
|
||
## Addressables 后端接入
|
||
## 测试建议
|
||
```
|
||
|
||
- [ ] **Step 5: Write sample guide**
|
||
|
||
Create `docs/guides/main-menu-p1-sample.md` with:
|
||
|
||
- How to open the scene.
|
||
- What the user should see.
|
||
- How Gold increment works.
|
||
- How save/restore is validated.
|
||
- What Console output is acceptable.
|
||
|
||
- [ ] **Step 6: Run sample tests**
|
||
|
||
Run:
|
||
|
||
```powershell
|
||
dotnet build "My project\FlowScope.Tests.PlayMode.csproj" --no-restore
|
||
```
|
||
|
||
Then in Unity Test Runner run:
|
||
|
||
- PlayMode: `FlowScope.Tests.PlayMode.Samples.MainMenuP0Tests`
|
||
- PlayMode full suite
|
||
|
||
Expected: all green, no unexpected Console Error.
|
||
|
||
- [ ] **Step 7: Manual validation**
|
||
|
||
Open:
|
||
|
||
```text
|
||
My project/Assets/FlowScope/Samples/MainMenuP0/Scenes/MainMenuP0.unity
|
||
```
|
||
|
||
Verify:
|
||
|
||
- UI opens.
|
||
- Gold increments on click.
|
||
- UI updates immediately.
|
||
- Exit saves data.
|
||
- Restart restores data.
|
||
- Closing Feature releases resources and subscriptions.
|
||
|
||
- [ ] **Step 8: Commit**
|
||
|
||
```powershell
|
||
git add "My project/Assets/FlowScope/Samples/MainMenuP0" `
|
||
"My project/Assets/FlowScope/Tests/PlayMode/Samples" `
|
||
"docs/guides/flowscope-runtime-usage.md" `
|
||
"docs/guides/main-menu-p1-sample.md"
|
||
git commit -m "升级 P1 主菜单样例和使用规范"
|
||
```
|
||
|
||
---
|
||
|
||
## Task E: Final P1 Integration
|
||
|
||
**Worktree:** `p1-integration`
|
||
|
||
**Depends on:** Task A, Task B, Task C, Task D
|
||
|
||
**Goal:** 合并全部 P1 任务,修正模块边界缝隙,完成全量验证与最终文档。
|
||
|
||
**Owned files:**
|
||
- Modify only files needed to resolve integration seams.
|
||
- Modify: `docs/requirements/p1-production-hardening.md`
|
||
- Modify: `docs/reviews/flowscope-framework-review.md`
|
||
- Create: `docs/reviews/p1-completion-report.md`
|
||
|
||
**Do not modify:**
|
||
- Add new P1 feature scope beyond this plan.
|
||
- Introduce P2 package/editor tooling.
|
||
- Revert P0 behavior unless tests prove a bug and doc is updated.
|
||
|
||
- [ ] **Step 1: Merge P1 branches in order**
|
||
|
||
Recommended order:
|
||
|
||
```text
|
||
p1-contracts
|
||
p1-resource-backend
|
||
p1-config-save-extensions
|
||
p1-ui-navigation-preload
|
||
p1-sample-docs
|
||
```
|
||
|
||
- [ ] **Step 2: Run compile validation**
|
||
|
||
Run:
|
||
|
||
```powershell
|
||
dotnet build "My project\FlowScope.Runtime.csproj" --no-restore
|
||
dotnet build "My project\FlowScope.Tests.EditMode.csproj" --no-restore
|
||
dotnet build "My project\FlowScope.Tests.PlayMode.csproj" --no-restore
|
||
```
|
||
|
||
Expected: all pass with `0 个错误`.
|
||
|
||
- [ ] **Step 3: Run Unity Test Runner**
|
||
|
||
Run in Unity:
|
||
|
||
- EditMode full suite.
|
||
- PlayMode full suite.
|
||
|
||
Expected:
|
||
|
||
- EditMode all green.
|
||
- PlayMode all green.
|
||
- No unexpected Error logs.
|
||
|
||
- [ ] **Step 4: Manual sample validation**
|
||
|
||
Repeat Task D manual validation after all branches are merged.
|
||
|
||
- [ ] **Step 5: Update review and completion docs**
|
||
|
||
Create `docs/reviews/p1-completion-report.md`:
|
||
|
||
```markdown
|
||
# FlowScope Game Core P1 完成报告
|
||
|
||
## 范围
|
||
## 已完成
|
||
## 测试结果
|
||
## 人工验收
|
||
## 未进入本轮
|
||
## P2 建议
|
||
```
|
||
|
||
Update `docs/reviews/flowscope-framework-review.md` risk table:
|
||
|
||
- Mark Addressables reflection risk as resolved or reduced.
|
||
- Mark Config/Save extension risk as reduced.
|
||
- Keep package/editor tooling as P2.
|
||
|
||
- [ ] **Step 6: Final status check**
|
||
|
||
Run:
|
||
|
||
```powershell
|
||
git -c core.quotepath=false status --short
|
||
git diff --check
|
||
```
|
||
|
||
Expected:
|
||
|
||
- Only intended files changed before final commit.
|
||
- `git diff --check` has no whitespace errors.
|
||
|
||
- [ ] **Step 7: Commit integration**
|
||
|
||
```powershell
|
||
git add "My project/Assets/FlowScope" docs
|
||
git commit -m "完成 Game Core P1 生产化集成"
|
||
```
|
||
|
||
---
|
||
|
||
## 验收标准总表
|
||
|
||
| Area | Acceptance |
|
||
| --- | --- |
|
||
| Resource | `FlowScope.Runtime` 不再包含 Addressables 反射 backend;核心资源服务通过 `IResourceBackend` 工作;Addressables 适配在独立 asmdef 中。 |
|
||
| Config | `JsonConfigProvider` 支持 source + parser;旧 mapping API 继续可用;重复 id、缺文件、格式错误仍有明确异常。 |
|
||
| Save | `JsonSaveSerializer` 支持版本 envelope 和 migration chain;旧 raw JSON 存档可读。 |
|
||
| UI | Navigator 可 push/back/clear;预加载可加载、复用、释放 panel 资源;P0 UIManager 行为不回退。 |
|
||
| Sample | MainMenuP0 场景可见、可点、可保存恢复;测试覆盖 startup/click/shutdown/restart。 |
|
||
| Docs | 有 P1 使用规范、样例说明、完成报告;旧评审风险状态同步更新。 |
|
||
| Tests | EditMode 全绿;PlayMode 全绿;人工样例验收通过;无未预期 Console Error。 |
|
||
|
||
---
|
||
|
||
## 自检清单
|
||
|
||
- [ ] 每个任务都有明确 Owned files。
|
||
- [ ] 每个任务都有 Do not modify 边界。
|
||
- [ ] 每个任务都有测试和提交步骤。
|
||
- [ ] P1 范围没有包含 P2/P3 能力。
|
||
- [ ] P0 现有公开接口不被无故破坏。
|
||
- [ ] 所有 commit message 使用中文。
|
||
- [ ] 执行时使用新对话或新 worktree,避免 P0 调试上下文污染 P1 实现。
|