Compare commits

...

2 Commits

Author SHA1 Message Date
Savya Bikram Shah
50ca3a0a0f readme updates 2026-05-27 11:07:05 +05:45
Savya Bikram Shah
f3b53be39d Gallery Done 2026-05-27 10:55:39 +05:45
19 changed files with 470 additions and 245 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5f9926c57855c418c8298f8b3c44e034
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,6 +1,6 @@
using UnityEngine;
namespace Darkmatter.Core.Contracts.Paper
namespace Darkmatter.Core.Contracts.Features.Paper
{
public interface IArtInputBridge
{

View File

@@ -1,6 +1,6 @@
using UnityEngine;
namespace Darkmatter.Core.Contracts.Paper
namespace Darkmatter.Core.Contracts.Features.Paper
{
public interface IPaperRig
{

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3681f5b008ffa422cb0d53ea3a04d839
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,10 @@
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace Darkmatter.Core.Contracts.Services.Gallery
{
public interface IGalleryService
{
void SaveImageAsync(Texture2D sprite, string fileName);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 02a64c2248ad94919af786bbe61fb43e

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b570aac99834740da891dc197d1183a1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6f6d23b09611c45aca2d6f5a3cd6196c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,13 @@
using Darkmatter.Core.Contracts.Services.Gallery;
using UnityEngine;
namespace Darkmatter.Services.Gallery
{
public class GalleryService : IGalleryService
{
public void SaveImageAsync(Texture2D image, string fileName)
{
NativeGallery.SaveImageToGallery(image, "ColorBook", fileName);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ef8e2d865f2c14be98f8f21f8081ac15

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 49bd8a78f60094fc390a5b99b2c78b66
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,19 @@
using UnityEngine;
namespace Darkmatter.Services.Gallery
{
public class GalleryServiceModule : MonoBehaviour
{
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f03c84255756e497f96c3baa7f6abe16

View File

@@ -0,0 +1,19 @@
{
"name": "Services.Gallery",
"rootNamespace": "Darkmatter.Services.Gallery",
"references": [
"GUID:6a0a834eb41764f12ba55c3fb04a40cb",
"GUID:c1c03c0e5b2f4412b9f2be1c20d6a9b1",
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc",
"GUID:6e5063adab271564ba0098a06a8cebda"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b817d098d27854edb9b82cfee89717a8
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

597
Readme.md
View File

@@ -68,139 +68,209 @@ Features ──► Core ◄── Services
## 4. Folder Structure
This section reflects the **actual project on disk today**. Empty folders that have been reserved for upcoming work are marked `(planned)`; everything else has at least one file in it. Aspirational additions for the rest of the game are listed in §4c at the bottom.
### 4a. Actual layout on disk
```
Assets/Darkmatter/Code/
├── App/
── Boot/
│ │ └── AppBoot.cs
│ └── LifetimeScopes/
│ ├── RootLifetimeScope.cs
│ ├── MainMenuLifetimeScope.cs
│ ├── ColorBookLifetimeScope.cs
│ └── ArtBookLifetimeScope.cs
Assets/Darkmatter/
├── Scenes/
── Boot.unity ← only scene wired so far
├── Core/
── Drawing/
── IDrawingTemplate.cs
│ │ ├── IDrawingTemplateCatalog.cs
│ │ ├── ShapePieceDTO.cs
│ │ └── ColorRegionDTO.cs
│ ├── Coloring/
│ │ ├── IColorPalette.cs
│ │ └── PaintCommandDTO.cs
│ ├── Paper/ (shared art rig — RT-as-paper)
│ │ ├── IPaperRig.cs
│ │ └── IArtInputBridge.cs
│ ├── History/
│ │ ├── ICommand.cs
│ │ └── IUndoStack.cs
│ ├── Gallery/
│ │ ├── IGalleryService.cs
│ │ └── SavedArtworkDTO.cs
│ ├── Capture/
│ │ └── ICaptureService.cs
│ ├── Progression/
│ │ └── IProgressionService.cs
│ └── Signals/
│ ├── DrawingSelectedSignal.cs
│ ├── ShapeAssembledSignal.cs
│ ├── ColorAppliedSignal.cs
│ ├── ArtworkCapturedSignal.cs
│ └── ArtworkSavedSignal.cs
├── Content/ ← singular ("Content", not "Contents")
── Gameplay/
── PaperRig/ ← (planned — paper rig prefabs)
├── Libs/
│ ├── CommandStack/ (generic bounded undo/redo)
── EventBus/ (shared with bus game if monorepo)
└── FSM/ (optional, for ColorBookFlow)
├── Data/
│ ├── Inputs/ (Input System .inputactions)
── Settings/
├── Persistance/Resources/ (ProtectedPlayerPrefs settings)
│ ├── Rendering/ (URP renderer + asset)
│ └── Scenes/URP2DSceneTemplate.unity
── Services/
├── Audio/
── Inputs/
│ ├── Assets/ (Addressables wrapper — IAssetProviderService)
├── Scenes/
├── Persistence/ (JSON / PlayerPrefs for non-image state)
├── Gallery/ (file IO — PNG + sidecar JSON)
│ └── Capture/ (RenderTexture → PNG, paper bg compositing)
└── Features/
├── MainMenu/
├── DrawingCatalog/
├── Paper/ (RT paper rig — ArtCamera + RenderTexture + input bridge)
├── ShapeBuilder/
├── Coloring/
├── History/
├── Capture/
├── Progression/
├── ColorBookFlow/ (orchestrates panel swap, next, capture chain)
└── ArtBook/
── Code/
├── App/
── LifetimeScopes/
│ └── RootLifetimeScope.cs ← scope loads serialized IServiceModule list
│ Darkmatter.App.asmdef
├── Core/ (asmdef name: `Core`, namespace root `Darkmatter.Core.*`)
│ ├── Compatibility/
│ │ └── IsExternalInit.cs (C#9 init shim for older runtimes)
│ ├── Contracts/
│ │ ├── Paper/ ← misplaced empty folder — should be Contracts/Features/Paper/ (delete or move)
│ │ └── Services/
│ │ ├── Assets/IAssetProviderService.cs
│ │ ├── Audio/IAudioService.cs, ISfxPlayer.cs
│ │ ├── Camera/ICameraService.cs
│ │ ├── Capture/ ← (planned — ICaptureService)
│ │ ├── Inputs/IInputReader.cs
│ │ └── Scenes/ISceneService.cs
│ ├── Data/
│ │ ├── Dynamic/Services/Audio/ (AudioHandle, AudioRequest)
│ │ └── Static/Services/Audio/ (SfxCatalogSO)
│ └── Enums/
│ └── Services/
│ ├── Audio/ (AudioChannel, AudioPlayMode, SfxId)
│ ├── Camera/CameraType.cs (MainCamera, UICamera — ArtCamera not added yet)
│ └── Scenes/GameScene.cs
├── Features/ ← (planned — empty folder today)
├── Libs/
│ ├── FSM/ (IState, State, StateMachine + Docs)
│ │ Libs.FSM.asmdef
│ ├── Installers/ (IServiceModule — Register(IContainerBuilder))
│ │ Libs.Installers.asmdef
│ ├── Observer/ (IEventBus, EventBus — note: not named "EventBus")
│ │ Libs.Observer.asmdef
│ ├── PlayerPrefs/ (ProtectedPlayerPrefs — used in place of a Persistence service)
│ │ ├── Editor/ Libs.PlayerPrefs.Editor.asmdef
│ │ └── Runtime/ Libs.PlayerPrefs.asmdef
│ └── UI/ (ToggleButton, ToggleButtonGroup)
│ Libs.UI.asmdef
└── Services/
├── Analytics/
│ ├── Installers/AnalyticsServiceModule.cs
│ └── Systems/FirebaseAnalyticsSystem.cs
│ Services.Analytics.asmdef
├── Assets/
│ ├── AddressableAssetProviderService.cs
│ └── AddressableLoadHandleTracker.cs
│ Services.Assets.asmdef
├── Audio/
│ ├── AudioService.cs
│ └── SfxPlayer.cs
│ Services.Audio.asmdef
├── Camera/
│ ├── Installers/CameraServiceModule.cs
│ └── Service/CameraService.cs
│ Services.Camera.asmdef
├── Inputs/
│ ├── Generated/GameInputs.cs (Input System codegen)
│ ├── Installers/InputServiceModule.cs
│ └── Readers/InputReaderSO.cs
│ Services.Inputs.asmdef
└── Scenes/
└── SceneService.cs
Services.Scenes.asmdef
```
### Per-feature folder layout
### 4b. Conventions visible in current code
Every feature follows the same internal shape:
- **Asmdef per Service / Lib / App / Core.** No Feature asmdefs yet (folder is empty).
- **Core sub-tree shape:** `Core/{Compatibility, Contracts, Data, Enums}` — deeply nested by Service rather than by topic. Game-specific Core types (Drawing, Coloring, Paper, Gallery, History, Progression, Signals) will be added under either `Core/Contracts/<Area>/` or new top-level `Core/<Area>/` — pick one convention before adding the first one.
- **Service sub-shape varies** — some are flat (`Services/Audio/*.cs`), some split (`Services/Camera/Installers/`, `Services/Camera/Service/`). The "official" per-feature shape (§4d) hasn't been validated against real code yet.
- **Installer pattern:** `MonoBehaviour, IServiceModule` with `Register(IContainerBuilder builder)`. **Not** `IInstaller.Install(...)` as older sections of this doc imply. New installers must follow `IServiceModule`.
- **No `AppBoot`** entry point exists yet. `RootLifetimeScope` only iterates a serialized `MonoBehaviour[] serviceModules` and calls `Register` on each `IServiceModule`. Boot sequence in §29 is aspirational.
- **No Persistence service.** `Libs/PlayerPrefs` (the `ProtectedPlayerPrefs` library) is in place for small-key state. JSON persistence for gallery sidecars will need a separate service when Gallery lands.
- **Camera service is a registry**, not a fitter. `ICameraService.Register/Get` only — no aspect-fit logic.
### 4c. Planned additions (not on disk yet)
All new game code follows the same nesting pattern as existing Services — `Contracts/Features/<Name>/`, `Data/{Dynamic,Static}/Features/<Name>/`, `Features/<Name>/` — and asmdefs drop the `Darkmatter.` prefix to match `Core`, `Services.Audio`, `Libs.Observer`.
Rough landing order for ColorBook scene to be playable:
| Path | Role |
|---|---|
| `Core/Contracts/Features/Paper/IPaperRig.cs`, `IArtInputBridge.cs` | Paper rig contracts |
| `Core/Contracts/Services/Capture/ICaptureService.cs` | Capture service contract |
| `Core/Contracts/Services/Gallery/IGalleryService.cs` | Gallery service contract |
| `Core/Contracts/Features/Drawing/IDrawingTemplate.cs`, `IDrawingTemplateCatalog.cs` | Drawing template contracts |
| `Core/Contracts/Features/Coloring/IColorPalette.cs` | Palette contract |
| `Core/Contracts/Features/History/ICommand.cs`, `IUndoStack.cs` | Undo/redo contracts |
| `Core/Contracts/Features/Progression/IProgressionService.cs` | Progression contract (despite the name, it's a feature contract since it's game-specific) |
| `Core/Data/Static/Features/Drawing/` (DrawingTemplateSO) | Authored drawing data |
| `Core/Data/Static/Features/Coloring/` (ColorPaletteSO) | Authored palette data |
| `Core/Data/Dynamic/Features/Drawing/ShapePieceDTO.cs`, `ColorRegionDTO.cs` | Runtime drawing structs |
| `Core/Data/Dynamic/Features/Coloring/PaintCommandDTO.cs` | Runtime coloring struct |
| `Core/Data/Dynamic/Features/Gallery/SavedArtworkDTO.cs` | Runtime gallery struct |
| `Core/Data/Dynamic/Features/Signals/` (DrawingSelectedSignal, ShapeAssembledSignal, ColorAppliedSignal, ArtworkCapturedSignal, ArtworkSavedSignal) | Cross-feature signal structs |
| `Core/Enums/Services/Camera/CameraType.cs` | Add `ArtCamera` enum value to existing file |
| `Libs/CommandStack/` (+ `Libs.CommandStack.asmdef`) | Bounded undo/redo |
| `Services/Capture/` (+ `Services.Capture.asmdef`) | `RenderTextureCaptureService` reads `IPaperRig.Surface` |
| `Services/Gallery/` (+ `Services.Gallery.asmdef`) | `FileGalleryService` — PNG + sidecar JSON IO |
| `Features/Paper/` (+ `Features.Paper.asmdef`) | Scene-bound RT rig |
| `Features/{MainMenu,DrawingCatalog,ShapeBuilder,Coloring,History,Capture,Progression,ColorBookFlow,ArtBook}/` (+ asmdefs each) | Game features |
| `App/LifetimeScopes/{MainMenu,ColorBook,ArtBook}LifetimeScope.cs` | Per-scene scopes |
| `App/Boot/AppBoot.cs` | Bootstrap entry point |
| `Assets/Darkmatter/Scenes/{MainMenu,ColorBook,ArtBook}.unity` | Scenes |
| `Content/Gameplay/Drawings/<theme>/<id>/{Template.asset, Pieces/, Regions/, PaperBackground.png}` | Authored drawings (under existing `Content/Gameplay/` root) |
| `Content/Gameplay/Palettes/*.asset` | Color palettes |
| `Content/Audio/{UI,Coloring}/` | SFX banks |
### 4d. Per-feature folder layout (matches existing Services pattern)
Look at `Services/Camera/` (`Installers/` + `Service/`) and `Services/Analytics/` (`Installers/` + `Systems/`) — that's the convention. Features adopt the same shape, adding `UI/` or `Views/` only when there's something to put in them.
```
Features/[Name]/
├── Installers/ IInstaller — VContainer registrations
├── Systems/ Controllers, services (pure C#)
├── Repository/ In-memory state holders
├── Commands/ ICommand implementations (if feature mutates undoable state)
├── UI/
│ ├── *Presenter.cs Pure C#, listens to model, drives view
│ └── *View.cs MonoBehaviour, setters only
├── Views/ World-space MonoBehaviours (sprites, colliders)
└── Docs/ Feature-specific markdown
├── Installers/ IServiceModule — VContainer registration
├── Systems/ (or Service/) Controllers, repositories, factories (pure C#)
├── UI/ (only if the feature has Canvas UI)
│ ├── *Presenter.cs Pure C#, listens to model, drives view
│ └── *View.cs MonoBehaviour, setters only
├── Views/ (only if the feature has world-space MonoBehaviours)
└── Features.<Name>.asmdef
```
### Asset folder parallel
```
Assets/Darkmatter/Contents/
├── Drawings/
│ ├── Animals/<id>/
│ │ ├── Template.asset (DrawingTemplateSO)
│ │ ├── Pieces/*.png
│ │ ├── Regions/*.png
│ │ └── PaperBackground.png
│ └── Vehicles/...
├── Palettes/*.asset (ColorPaletteSO)
├── Audio/
│ ├── UI/ (tap, swipe, button)
│ └── Coloring/ (fill, complete, sparkle)
└── UI/ (HUD prefabs, fonts, icons)
```
Rules of thumb pulled from current Services:
- **Use `Service/` (singular)** when the feature has exactly one main implementation class (like `Services/Camera/Service/CameraService.cs`).
- **Use `Systems/` (plural)** when there are multiple pure-C# coordinators (like `Services/Analytics/Systems/`).
- **Skip nesting entirely** when the feature has only 12 files at root (like `Services/Audio/AudioService.cs` + `SfxPlayer.cs` flat).
- **`Docs/` is per-folder** in current code — drop a `Docs/` inside any sub-folder that needs notes, don't make a global feature-level Docs.
---
## 5. Namespaces
## 5. Namespaces & Asmdef naming
`Darkmatter.[Layer].[Module]`
C# namespace pattern is `Darkmatter.<Layer>.<Module>[.<SubArea>]` — the `Darkmatter.` prefix stays on namespaces. Examples already in code:
- `Darkmatter.Features.Coloring`
- `Darkmatter.Features.ShapeBuilder`
- `Darkmatter.Services.Gallery`
- `Darkmatter.Services.Capture`
- `Darkmatter.Core.Drawing`
- `Darkmatter.Lib.CommandStack`
- `Darkmatter.Core.Contracts.Services.Camera` ([ICameraService.cs](Assets/Darkmatter/Code/Core/Contracts/Services/Camera/ICameraService.cs))
- `Darkmatter.Services.Camera` ([CameraService.cs](Assets/Darkmatter/Code/Services/Camera/Service/CameraService.cs))
- `Darkmatter.Services.Camera.Installers` ([CameraServiceModule.cs](Assets/Darkmatter/Code/Services/Camera/Installers/CameraServiceModule.cs))
- `Darkmatter.Libs.Installers` ([IServiceModule.cs](Assets/Darkmatter/Code/Libs/Installers/IServiceModule.cs))
Each maps 1:1 to a `.asmdef`.
**Asmdef names drop the `Darkmatter.` prefix.** Existing pattern:
| Namespace | Asmdef |
|---|---|
| `Darkmatter.Core.*` | `Core` |
| `Darkmatter.App` | `Darkmatter.App` (one exception — keep as-is, don't churn) |
| `Darkmatter.Libs.Observer` | `Libs.Observer` |
| `Darkmatter.Libs.FSM` | `Libs.FSM` |
| `Darkmatter.Libs.Installers` | `Libs.Installers` |
| `Darkmatter.Libs.PlayerPrefs` | `Libs.PlayerPrefs` (+ `Libs.PlayerPrefs.Editor`) |
| `Darkmatter.Libs.UI` | `Libs.UI` |
| `Darkmatter.Services.Audio` | `Services.Audio` |
| `Darkmatter.Services.Assets` | `Services.Assets` |
| `Darkmatter.Services.Camera` | `Services.Camera` |
| `Darkmatter.Services.Inputs` | `Services.Inputs` |
| `Darkmatter.Services.Scenes` | `Services.Scenes` |
| `Darkmatter.Services.Analytics` | `Services.Analytics` |
New asmdefs follow the same convention: `Services.Capture`, `Services.Gallery`, `Libs.CommandStack`, `Features.Paper`, `Features.Coloring`, etc.
---
## 6. Scenes & Lifetime Scopes
| Scene | Scope | Contents |
|---|---|---|
| `Boot.unity` | `RootLifetimeScope` | All Services + `IEventBus`. Persists forever. |
| `MainMenu.unity` | `MainMenuLifetimeScope` | Menu presenter, art book entry. |
| `ColorBook.unity` | `ColorBookLifetimeScope` | DrawingCatalog, ShapeBuilder, Coloring, History, Capture, ColorBookFlow. |
| `ArtBook.unity` | `ArtBookLifetimeScope` | Gallery presenter, viewer, share. |
| Scene | Scope | Status | Contents |
|---|---|---|---|
| `Boot.unity` | `RootLifetimeScope` | ✅ exists | All Services + Libs. Persists forever. |
| `MainMenu.unity` | `MainMenuLifetimeScope` | ⚠️ planned | Menu presenter, art book entry. |
| `ColorBook.unity` | `ColorBookLifetimeScope` | ⚠️ planned | `PaperRig`, DrawingCatalog, ShapeBuilder, Coloring, History, Capture, ColorBookFlow. |
| `ArtBook.unity` | `ArtBookLifetimeScope` | ⚠️ planned | Gallery presenter, viewer, share. |
Only `Boot.unity` exists today; the three scene scope classes haven't been written yet either (only `RootLifetimeScope` exists in [App/LifetimeScopes/](Assets/Darkmatter/Code/App/LifetimeScopes/)).
Scopes nest: `Root → (MainMenu | ColorBook | ArtBook)`. Services resolved from the root parent. Scene scopes only register their own features.
### Boot chain
### Boot chain (planned)
`AppBoot` runs once, in order:
No `AppBoot` class exists yet — today `RootLifetimeScope` only registers services and stops there. When `AppBoot` is added (as an `IAsyncStartable` registered via `builder.RegisterEntryPoint<AppBoot>()`), it should run once, in order:
1. Initialize `IAssetProviderService` (Addressables init).
2. Preload essential bundles (palettes, UI sounds).
@@ -275,8 +345,10 @@ All Core types are pure data or interfaces.
### Drawing
> Contracts live in `Darkmatter.Core.Contracts.Features.Drawing`; DTOs in `Darkmatter.Core.Data.Dynamic.Features.Drawing`.
```csharp
namespace Darkmatter.Core.Drawing;
namespace Darkmatter.Core.Contracts.Features.Drawing;
public interface IDrawingTemplate {
string Id { get; }
@@ -305,8 +377,10 @@ public readonly struct ColorRegionDTO {
### Coloring
> Contracts in `Darkmatter.Core.Contracts.Features.Coloring`; DTOs in `Darkmatter.Core.Data.Dynamic.Features.Coloring`.
```csharp
namespace Darkmatter.Core.Coloring;
namespace Darkmatter.Core.Contracts.Features.Coloring;
public interface IColorPalette {
string Id { get; }
@@ -322,8 +396,10 @@ public readonly struct PaintCommandDTO {
### Paper (RT rig + input bridge)
> Contracts live in `Darkmatter.Core.Contracts.Features.Paper`. Files at `Core/Contracts/Features/Paper/`.
```csharp
namespace Darkmatter.Core.Paper;
namespace Darkmatter.Core.Contracts.Features.Paper;
public interface IPaperRig {
Camera ArtCamera { get; } // offscreen, targetTexture = Surface
@@ -346,8 +422,10 @@ public interface IArtInputBridge {
### History
> Contracts in `Darkmatter.Core.Contracts.Features.History`.
```csharp
namespace Darkmatter.Core.History;
namespace Darkmatter.Core.Contracts.Features.History;
public interface ICommand {
void Execute();
@@ -366,8 +444,10 @@ public interface IUndoStack {
### Gallery & Capture
> `IGalleryService` is a Service contract → `Darkmatter.Core.Contracts.Services.Gallery`. `SavedArtworkDTO` is a runtime data struct → `Darkmatter.Core.Data.Dynamic.Features.Gallery`. `ICaptureService` → `Darkmatter.Core.Contracts.Services.Capture`.
```csharp
namespace Darkmatter.Core.Gallery;
namespace Darkmatter.Core.Data.Dynamic.Features.Gallery;
public readonly struct SavedArtworkDTO {
public string Id { get; }
@@ -377,6 +457,8 @@ public readonly struct SavedArtworkDTO {
public string ThumbnailPath { get; }
}
namespace Darkmatter.Core.Contracts.Services.Gallery;
public interface IGalleryService {
UniTask<SavedArtworkDTO> SaveAsync(byte[] png, string templateId);
UniTask<IReadOnlyList<SavedArtworkDTO>> ListAsync();
@@ -384,7 +466,7 @@ public interface IGalleryService {
UniTask DeleteAsync(string artworkId);
}
namespace Darkmatter.Core.Capture;
namespace Darkmatter.Core.Contracts.Services.Capture;
public interface ICaptureService {
// No camera or paperBg args — capture reads directly from IPaperRig.Surface.
@@ -397,8 +479,10 @@ public interface ICaptureService {
### Signals
> Signal structs live in `Darkmatter.Core.Data.Dynamic.Features.Signals` (runtime data, cross-feature).
```csharp
namespace Darkmatter.Core.Signals;
namespace Darkmatter.Core.Data.Dynamic.Features.Signals;
public readonly struct DrawingSelectedSignal {
public string TemplateId { get; }
@@ -713,109 +797,123 @@ Maintained alongside the [Darkmatter Architecture Guide](../Assets/Darkmatter_Ar
## 20. Assembly Definition Map
Every folder under `Code/` is its own `.asmdef`. References follow the layer rules exactly.
Every Lib / Service / Feature is its own `.asmdef`. The `Darkmatter.` prefix is **only** on the App asmdef; everything else uses bare `<Layer>.<Module>` names. References follow the layer rules.
### On disk today
| Asmdef | Path | References |
|---|---|---|
| `Darkmatter.App` | `App/` | All Features, all Services, Core, Libs |
| `Darkmatter.Core` | `Core/` | (none — `UniTask` allowed in async signatures) |
| `Darkmatter.Lib.CommandStack` | `Libs/CommandStack/` | `Darkmatter.Core` |
| `Darkmatter.Lib.EventBus` | `Libs/EventBus/` | `Darkmatter.Core` |
| `Darkmatter.Lib.FSM` | `Libs/FSM/` | `Darkmatter.Core` |
| `Darkmatter.Services.Audio` | `Services/Audio/` | `Darkmatter.Core` |
| `Darkmatter.Services.Inputs` | `Services/Inputs/` | `Darkmatter.Core` |
| `Darkmatter.Services.Assets` | `Services/Assets/` | `Darkmatter.Core` |
| `Darkmatter.Services.Scenes` | `Services/Scenes/` | `Darkmatter.Core` |
| `Darkmatter.Services.Persistence` | `Services/Persistence/` | `Darkmatter.Core` |
| `Darkmatter.Services.Gallery` | `Services/Gallery/` | `Darkmatter.Core` |
| `Darkmatter.Services.Capture` | `Services/Capture/` | `Darkmatter.Core` |
| `Darkmatter.Features.MainMenu` | `Features/MainMenu/` | `Darkmatter.Core`, Libs |
| `Darkmatter.Features.DrawingCatalog` | `Features/DrawingCatalog/` | `Darkmatter.Core`, Libs |
| `Darkmatter.Features.Paper` | `Features/Paper/` | `Darkmatter.Core`, `Lib.Installers` |
| `Darkmatter.Features.ShapeBuilder` | `Features/ShapeBuilder/` | `Darkmatter.Core`, Libs |
| `Darkmatter.Features.Coloring` | `Features/Coloring/` | `Darkmatter.Core`, `Lib.CommandStack` |
| `Darkmatter.Features.History` | `Features/History/` | `Darkmatter.Core`, `Lib.CommandStack` |
| `Darkmatter.Features.Capture` | `Features/Capture/` | `Darkmatter.Core` |
| `Darkmatter.Features.Progression` | `Features/Progression/` | `Darkmatter.Core` |
| `Darkmatter.Features.ColorBookFlow` | `Features/ColorBookFlow/` | `Darkmatter.Core`, `Lib.FSM` |
| `Darkmatter.Features.ArtBook` | `Features/ArtBook/` | `Darkmatter.Core` |
| `Darkmatter.App` | `App/` | All Services, Libs, Core (Features when they land) |
| `Core` | `Core/` | (none — `UniTask` allowed in async signatures) |
| `Libs.FSM` | `Libs/FSM/` | `Core` |
| `Libs.Installers` | `Libs/Installers/` | (VContainer only) |
| `Libs.Observer` | `Libs/Observer/` | `Core` |
| `Libs.PlayerPrefs` | `Libs/PlayerPrefs/Runtime/` | (standalone) |
| `Libs.PlayerPrefs.Editor` | `Libs/PlayerPrefs/Editor/` | `Libs.PlayerPrefs` |
| `Libs.UI` | `Libs/UI/` | `Core` |
| `Services.Analytics` | `Services/Analytics/` | `Core`, `Libs.Installers` |
| `Services.Assets` | `Services/Assets/` | `Core`, `Libs.Installers` |
| `Services.Audio` | `Services/Audio/` | `Core`, `Libs.Installers` |
| `Services.Camera` | `Services/Camera/` | `Core`, `Libs.Installers` |
| `Services.Inputs` | `Services/Inputs/` | `Core`, `Libs.Installers` |
| `Services.Scenes` | `Services/Scenes/` | `Core`, `Libs.Installers` |
**Hard rule:** No Service asmdef references any Feature asmdef. No Feature asmdef references another Feature asmdef. Compiler enforces the architecture.
### Planned (not on disk yet)
| Asmdef | Path | References |
|---|---|---|
| `Libs.CommandStack` | `Libs/CommandStack/` | `Core` |
| `Services.Capture` | `Services/Capture/` | `Core`, `Libs.Installers` |
| `Services.Gallery` | `Services/Gallery/` | `Core`, `Libs.Installers` |
| `Features.Paper` | `Features/Paper/` | `Core`, `Libs.Installers` |
| `Features.MainMenu` | `Features/MainMenu/` | `Core`, `Libs.Installers` |
| `Features.DrawingCatalog` | `Features/DrawingCatalog/` | `Core`, `Libs.Installers` |
| `Features.ShapeBuilder` | `Features/ShapeBuilder/` | `Core`, `Libs.Installers` |
| `Features.Coloring` | `Features/Coloring/` | `Core`, `Libs.Installers`, `Libs.CommandStack` |
| `Features.History` | `Features/History/` | `Core`, `Libs.Installers`, `Libs.CommandStack` |
| `Features.Capture` | `Features/Capture/` | `Core`, `Libs.Installers` |
| `Features.Progression` | `Features/Progression/` | `Core`, `Libs.Installers`, `Libs.PlayerPrefs` |
| `Features.ColorBookFlow` | `Features/ColorBookFlow/` | `Core`, `Libs.Installers`, `Libs.FSM` |
| `Features.ArtBook` | `Features/ArtBook/` | `Core`, `Libs.Installers` |
**Hard rules:**
- No Service asmdef references any Feature asmdef.
- No Feature asmdef references another Feature asmdef.
- All Services and Features depend on `Libs.Installers` so they can implement `IServiceModule`.
- The compiler enforces this — if a `using` won't resolve, the dependency is wrong.
---
## 21. LifetimeScope Concrete Sample
### `RootLifetimeScope` (Boot scene, persists forever)
All scopes use the same pattern: a serialized `MonoBehaviour[]` list of `IServiceModule` installers. Each installer is a MonoBehaviour on a child GameObject of the scope. Scope iterates and calls `Register`. **No hardcoded registrations in the scope itself.** This is exactly what [RootLifetimeScope.cs](Assets/Darkmatter/Code/App/LifetimeScopes/RootLifetimeScope.cs) already does today.
### `RootLifetimeScope` (Boot scene, persists forever) — actual code
```csharp
namespace Darkmatter.App.LifetimeScopes;
using Darkmatter.Libs.Installers;
using UnityEngine;
using VContainer;
using VContainer.Unity;
public sealed class RootLifetimeScope : LifetimeScope {
[SerializeField] private AudioServiceConfig _audioConfig;
[SerializeField] private InputReaderSO _inputReader;
public class RootLifetimeScope : LifetimeScope {
[SerializeField] private MonoBehaviour[] serviceModules;
protected override void Configure(IContainerBuilder builder) {
// EventBus
builder.Register<IEventBus, EventBus>(Lifetime.Singleton);
// Services
builder.RegisterInstance(_inputReader).As<IInputReader>();
builder.Register<IAudioService, AudioService>(Lifetime.Singleton)
.WithParameter(_audioConfig);
builder.Register<IAssetProviderService, AddressableAssetProviderService>(Lifetime.Singleton);
builder.Register<ISceneService, SceneService>(Lifetime.Singleton);
builder.Register<IPersistenceService, JsonPersistenceService>(Lifetime.Singleton);
builder.Register<IGalleryService, FileGalleryService>(Lifetime.Singleton);
builder.Register<ICaptureService, RenderTextureCaptureService>(Lifetime.Singleton);
// App entry
builder.RegisterEntryPoint<AppBoot>();
foreach (var module in serviceModules) {
if (module is IServiceModule serviceModule)
serviceModule.Register(builder);
}
}
}
```
### `ColorBookLifetimeScope` (per-scene, child of Root)
The inspector lists the installer MonoBehaviours in `serviceModules[]`. Drag the children of the Boot scope GameObject (e.g. `AudioServiceModule`, `CameraServiceModule`, `InputServiceModule`, `AssetProviderServiceModule`, `AnalyticsServiceModule`, `SceneServiceModule`) into that slot. Each is a `MonoBehaviour, IServiceModule`.
### `ColorBookLifetimeScope` (per-scene, child of Root) — same pattern
```csharp
namespace Darkmatter.App.LifetimeScopes;
public sealed class ColorBookLifetimeScope : LifetimeScope {
[SerializeField] private ColorBookSceneRefs _sceneRefs; // ArtCamera, panel roots, prefabs
[SerializeField] private IInstaller[] _installers; // assigned in inspector
[SerializeField] private MonoBehaviour[] sceneModules;
protected override void Configure(IContainerBuilder builder) {
builder.RegisterInstance(_sceneRefs);
// Each feature ships an IInstaller
foreach (var installer in _installers) installer.Install(builder);
// Scene-scoped orchestrator
builder.RegisterEntryPoint<ColorBookFlowController>();
foreach (var module in sceneModules) {
if (module is IServiceModule serviceModule)
serviceModule.Register(builder);
}
}
}
```
Drag these installers in the inspector:
- `DrawingCatalogServiceModule`
- `ShapeBuilderServiceModule`
- `ColoringServiceModule`
- `HistoryServiceModule`
- `CaptureFeatureModule`
- `ProgressionServiceModule`
Drag the scene's installer MonoBehaviours into `sceneModules[]`:
- `PaperRigModule`
- `DrawingCatalogModule`
- `ShapeBuilderModule`
- `ColoringModule`
- `HistoryModule`
- `CaptureModule`
- `ProgressionModule`
- `ColorBookFlowModule`
Each registers its own classes via `IServiceModule.Register(IContainerBuilder)`.
> If a scope needs a non-installer reference (e.g. a `ColorBookSceneRefs` MB holding camera + roots), expose it as a separate `[SerializeField]` and `builder.RegisterInstance(...)` it inside the scope's `Configure`. Don't put scene refs inside an installer — keep installers stateless across scenes.
---
## 22. Installer Pattern — Concrete Coloring Sample
Mirrors the existing [CameraServiceModule.cs](Assets/Darkmatter/Code/Services/Camera/Installers/CameraServiceModule.cs) and [InputServiceModule.cs](Assets/Darkmatter/Code/Services/Inputs/Installers/InputServiceModule.cs) — a `MonoBehaviour` implementing `IServiceModule.Register`.
```csharp
namespace Darkmatter.Features.Coloring.Installers;
[CreateAssetMenu(menuName = "Darkmatter/Installers/Coloring")]
public sealed class ColoringServiceModule : ScriptableObject, IInstaller {
public sealed class ColoringModule : MonoBehaviour, IServiceModule {
[SerializeField] private ColoringConfig _config;
public void Install(IContainerBuilder builder) {
public void Register(IContainerBuilder builder) {
builder.RegisterInstance(_config);
builder.Register<ColoringStateRepository>(Lifetime.Scoped).AsSelf();
builder.Register<ColoringController>(Lifetime.Scoped)
@@ -828,9 +926,11 @@ public sealed class ColoringServiceModule : ScriptableObject, IInstaller {
```
Convention:
- One `IInstaller` per feature.
- `ScriptableObject` so it can be referenced by scene scope inspector.
- One `IServiceModule` per feature, named `<Feature>Module` (matches `CameraServiceModule`, `InputServiceModule`, `AnalyticsServiceModule` already in the project).
- `MonoBehaviour` lives on a GameObject under the scope's hierarchy; dragged into the scope's `serviceModules[]` / `sceneModules[]` inspector list.
- Method name is `Register`, not `Install`. There is **no `IInstaller`** in this project — uses `IServiceModule` from [Libs.Installers](Assets/Darkmatter/Code/Libs/Installers/IServiceModule.cs).
- Registers only its own types. Never touches another feature's types.
- If the installer needs to wire scene-bound MonoBehaviours into DI, expose them as `[SerializeField]` fields on the installer itself and `builder.RegisterInstance<IFoo>(_foo)` them. See the planned `PaperRigModule` in §32.5b for an example.
---
@@ -1070,8 +1170,10 @@ Paths are **relative** to `persistentDataPath`. Never store absolute paths — t
## 29. Boot & Error Handling
> **Status: not implemented.** No `AppBoot` class exists. Today, [RootLifetimeScope.cs](Assets/Darkmatter/Code/App/LifetimeScopes/RootLifetimeScope.cs) only iterates installer MonoBehaviours and registers them — nothing runs after that. The block below is the *target* sequence when `AppBoot` is added as an `IAsyncStartable` entry point under `App/Boot/`.
```
AppBoot.StartAsync()
AppBoot.StartAsync() (planned — Features/Boot/AppBoot.cs, registered via builder.RegisterEntryPoint<AppBoot>())
├─ try Addressables.InitializeAsync()
│ fail → show "Tap to retry" splash
├─ try preload palette + UI sounds (Addressables labels)
@@ -1092,13 +1194,13 @@ Toddler-mode error UI:
## 30. Setup Checklist (new dev, day one)
1. Open `Bus Game.sln` (color book lives in same repo / Unity project per plan).
2. Verify Addressables groups exist: `Drawings_*`, `Palettes`, `Audio_*`.
3. Open `Boot.unity` → confirm `RootLifetimeScope` references the right configs.
4. Open `ColorBook.unity` → confirm `ColorBookLifetimeScope._installers[]` is fully populated.
5. Hit Play from `Boot.unity` (entry scene). Never start mid-flow — DI parent scope must exist.
6. To author a new drawing: duplicate `Animals/elephant/`, edit `Template.asset` (pieces + regions), add to the appropriate Addressables group.
7. Run `Tests > EditMode` and `Tests > PlayMode` before pushing.
1. Open `Colorbook.sln` at the repo root.
2. Open `Assets/Darkmatter/Scenes/Boot.unity` (currently the only scene wired).
3. Inspect the `RootLifetimeScope` GameObject — confirm its `serviceModules[]` list references the child installer MonoBehaviours (`AudioServiceModule`, `CameraServiceModule`, `InputServiceModule`, etc.).
4. Hit Play from `Boot.unity`. Other scenes (`MainMenu`, `ColorBook`, `ArtBook`) don't exist yet — they're listed in §6 / §4c as planned work.
5. When new scene scopes land, the same rule applies: never start a scene mid-flow, always enter from `Boot.unity` so the root scope exists.
6. When drawings are authored: duplicate the template folder under `Content/Gameplay/Drawings/<theme>/<id>/`, edit `Template.asset` (pieces + regions), add to the appropriate Addressables group.
7. Run `Tests > EditMode` and `Tests > PlayMode` before pushing (test infra not set up yet — see §16).
---
@@ -1106,19 +1208,19 @@ Toddler-mode error UI:
| Class | Layer | Asmdef |
|---|---|---|
| `IDrawingTemplate`, `ShapePieceDTO`, `ColorRegionDTO` | Core | `Darkmatter.Core` |
| `IPaperRig`, `IArtInputBridge` | Core | `Darkmatter.Core` |
| `ICommand`, `IUndoStack` | Core | `Darkmatter.Core` |
| `BoundedUndoStack` | Libs | `Darkmatter.Lib.CommandStack` |
| `AddressableAssetProviderService` | Services | `Darkmatter.Services.Assets` |
| `FileGalleryService` | Services | `Darkmatter.Services.Gallery` |
| `RenderTextureCaptureService` | Services | `Darkmatter.Services.Capture` |
| `PaperRig`, `ArtInputBridge`, `PaperRigModule` | Features | `Darkmatter.Features.Paper` |
| `ColoringController`, `PaintRegionCommand` | Features | `Darkmatter.Features.Coloring` |
| `ShapeBuilderController`, `ShapePieceView` | Features | `Darkmatter.Features.ShapeBuilder` |
| `HistoryController` | Features | `Darkmatter.Features.History` |
| `ColorBookFlowController` | Features | `Darkmatter.Features.ColorBookFlow` |
| `GalleryPresenter`, `GalleryGridView` | Features | `Darkmatter.Features.ArtBook` |
| `IDrawingTemplate`, `ShapePieceDTO`, `ColorRegionDTO` | Core | `Core` |
| `IPaperRig`, `IArtInputBridge` | Core | `Core` |
| `ICommand`, `IUndoStack` | Core | `Core` |
| `BoundedUndoStack` | Libs | `Libs.CommandStack` |
| `AddressableAssetProviderService` | Services | `Services.Assets` |
| `FileGalleryService` | Services | `Services.Gallery` |
| `RenderTextureCaptureService` | Services | `Services.Capture` |
| `PaperRig`, `ArtInputBridge`, `PaperRigModule` | Features | `Features.Paper` |
| `ColoringController`, `PaintRegionCommand` | Features | `Features.Coloring` |
| `ShapeBuilderController`, `ShapePieceView` | Features | `Features.ShapeBuilder` |
| `HistoryController` | Features | `Features.History` |
| `ColorBookFlowController` | Features | `Features.ColorBookFlow` |
| `GalleryPresenter`, `GalleryGridView` | Features | `Features.ArtBook` |
| `ColorBookLifetimeScope`, `AppBoot` | App | `Darkmatter.App` |
If a class's natural home doesn't match its asmdef, the architecture is bent — fix the placement, don't add a reference.
@@ -1127,6 +1229,8 @@ If a class's natural home doesn't match its asmdef, the architecture is bent —
## 32. Class Reference (Detailed)
> **Status: target spec, mostly unimplemented.** Of everything below, only the following Service classes exist on disk today: `AddressableAssetProviderService`, `AudioService` / `SfxPlayer`, `CameraService`, `SceneService`, `InputReaderSO`, `FirebaseAnalyticsSystem`. Everything else (Paper, Drawing, Coloring, History, Capture, Gallery, Progression, ColorBookFlow, ArtBook, AppBoot) is the target shape for when those classes are written. Treat this section as a contract for new code, not documentation of current state.
Canonical breakdown of every concrete class and interface. For each: **purpose**, **public surface** (signatures), **injected dependencies**, and **collaborators** (signals or interfaces it talks to).
> Convention used below
@@ -1134,6 +1238,7 @@ Canonical breakdown of every concrete class and interface. For each: **purpose**
> - `// pub:` = events / signals fired
> - `// sub:` = events / signals consumed
> - All async returns are `UniTask` unless noted.
> - Folder labels follow the actual nesting pattern: `Core/Contracts/Features/<Name>/`, `Core/Contracts/Services/<Name>/`, `Core/Data/Dynamic/Features/<Name>/`, `Features/<Name>/<Sub>/`, `Services/<Name>/<Sub>/`.
---
@@ -1141,7 +1246,7 @@ Canonical breakdown of every concrete class and interface. For each: **purpose**
Pure interfaces and DTOs. Zero logic.
#### `IDrawingTemplate` *(Core/Drawing)*
#### `IDrawingTemplate` *(Core/Contracts/Features/Drawing — planned)*
Immutable view of a single drawing's authored data.
```csharp
public interface IDrawingTemplate {
@@ -1155,7 +1260,7 @@ public interface IDrawingTemplate {
```
Implemented by `DrawingTemplateSO` (ScriptableObject) loaded via Addressables.
#### `IDrawingTemplateCatalog` *(Core/Drawing)*
#### `IDrawingTemplateCatalog` *(Core/Contracts/Features/Drawing — planned)*
Authority on which drawings exist, completion state, and "next" selection.
```csharp
public interface IDrawingTemplateCatalog {
@@ -1168,7 +1273,7 @@ public interface IDrawingTemplateCatalog {
}
```
#### `IColorPalette` *(Core/Coloring)*
#### `IColorPalette` *(Core/Contracts/Features/Coloring — planned)*
Set of colors offered to the child. Authored as `ColorPaletteSO`.
```csharp
public interface IColorPalette {
@@ -1177,10 +1282,10 @@ public interface IColorPalette {
}
```
#### `ICommand` & `IUndoStack` *(Core/History)*
#### `ICommand` & `IUndoStack` *(Core/Contracts/Features/History — planned)*
Already shown in section 8. Each undoable user action is one `ICommand`; the stack is bounded.
#### `IGalleryService` *(Core/Gallery)*
#### `IGalleryService` *(Core/Contracts/Services/Gallery — planned)*
Persistent store of saved artwork PNGs.
```csharp
public interface IGalleryService {
@@ -1192,7 +1297,7 @@ public interface IGalleryService {
}
```
#### `ICaptureService` *(Core/Capture)*
#### `ICaptureService` *(Core/Contracts/Services/Capture — planned)*
Snapshots the paper RT to a PNG blob. No arguments — dimensions and content come from `IPaperRig.Surface`.
```csharp
public interface ICaptureService {
@@ -1200,7 +1305,7 @@ public interface ICaptureService {
}
```
#### `IPaperRig` *(Core/Paper)*
#### `IPaperRig` *(Core/Contracts/Features/Paper — planned)*
Shared art rig. The single source of truth for everything that lives in the drawing world.
```csharp
public interface IPaperRig {
@@ -1212,7 +1317,7 @@ public interface IPaperRig {
}
```
#### `IArtInputBridge` *(Core/Paper)*
#### `IArtInputBridge` *(Core/Contracts/Features/Paper — planned)*
Converts screen-space pointer coords to art-world coords inside the RT.
```csharp
public interface IArtInputBridge {
@@ -1221,7 +1326,7 @@ public interface IArtInputBridge {
```
Returns `false` when the pointer is outside the displayed RawImage rect (toddler tapped the HUD or backdrop). Every art-world raycast goes through this.
#### `IProgressionService` *(Core/Progression)*
#### `IProgressionService` *(Core/Contracts/Features/Progression — planned)*
Tracks which templates the child has completed and what they last opened.
```csharp
public interface IProgressionService {
@@ -1234,7 +1339,7 @@ public interface IProgressionService {
}
```
#### `IAssetProviderService` *(Core/Assets)*
#### `IAssetProviderService` *(Core/Contracts/Services/Assets — ✅ exists)*
Addressables wrapper. Hides handle bookkeeping from features.
```csharp
public interface IAssetProviderService {
@@ -1246,7 +1351,7 @@ public interface IAssetProviderService {
}
```
#### `IEventBus` *(Libs/EventBus, also referenced from Core)*
#### `IEventBus` *(Libs/Observer — ✅ exists; note the folder is `Observer`, not `EventBus`)*
```csharp
public interface IEventBus {
void Publish<T>(T signal) where T : struct;
@@ -1259,15 +1364,15 @@ Signals are structs to avoid GC. Disposable subscription so presenters can unsub
### 32.2 Services Layer
Concrete infrastructure. One implementation each. All singletons in `RootLifetimeScope`.
Concrete infrastructure. One implementation each. All singletons in `RootLifetimeScope`, registered via per-service `MonoBehaviour, IServiceModule` installers.
#### `AddressableAssetProviderService` *(Services/Assets)*
#### `AddressableAssetProviderService` *(Services/Assets — ✅ exists)*
Implements `IAssetProviderService`.
- **Responsibility:** Wrap `Addressables.LoadAssetAsync<T>` and ref-count handles by address.
- **State:** `Dictionary<string, AsyncOperationHandle>` keyed by address.
- **Notes:** `Release(address)` decrements; `ReleaseAll()` for scene teardown. Initialization must complete before any other service may load.
#### `FileGalleryService` *(Services/Gallery)*
#### `FileGalleryService` *(Services/Gallery — planned)*
Implements `IGalleryService`.
```csharp
// fields:
@@ -1280,13 +1385,13 @@ Implements `IGalleryService`.
- **List flow:** enumerate `*.json` in `Gallery/`, deserialize, sort by `CreatedUtc desc`.
- **Delete flow:** delete png + thumb + json; missing files ignored (idempotent).
#### `RenderTextureCaptureService` *(Services/Capture)*
#### `RenderTextureCaptureService` *(Services/Capture — planned)*
Implements `ICaptureService`.
- **Steps:** allocate `RenderTexture(width, height, 0, ARGB32)` → bind to `artCamera.targetTexture``artCamera.Render()``ReadPixels` into `Texture2D` → composite `paperBackground` underneath (single shader blit) → `EncodeToPNG` → release RT + textures.
- **Threading:** PNG encode happens on a `UniTask.RunOnThreadPool` to avoid hitching the main thread on tablets.
- **Sizing:** default 2048², overridable. Capped at device max texture size.
#### `JsonPersistenceService` *(Services/Persistence)*
#### `JsonPersistenceService` *(Services/Persistence — planned; today `Libs/PlayerPrefs` covers small-key state)*
Implements `IPersistenceService` (small JSON blob; not the gallery).
```csharp
public interface IPersistenceService {
@@ -1298,7 +1403,7 @@ public interface IPersistenceService {
- **Format:** single JSON object keyed by `key` so multiple services can share one file.
- **Atomicity:** write to `save.json.tmp` → rename.
#### `SceneService` *(Services/Scenes)*
#### `SceneService` *(Services/Scenes — ✅ exists)*
Implements `ISceneService`. Wraps `SceneManager.LoadSceneAsync` with `UniTask` plus a fade-curtain.
```csharp
public interface ISceneService {
@@ -1307,7 +1412,7 @@ public interface ISceneService {
}
```
#### `AudioService` *(Services/Audio)*
#### `AudioService` *(Services/Audio — ✅ exists; see also `SfxPlayer`)*
Implements `IAudioService`. Plays SFX clips loaded by address, mixes via Unity AudioMixer groups.
```csharp
public interface IAudioService {
@@ -1318,7 +1423,7 @@ public interface IAudioService {
```
Holds an internal `Dictionary<string, AudioClip>` populated at preload.
#### `InputReaderSO` *(Services/Inputs)*
#### `InputReaderSO` *(Services/Inputs/Readers — ✅ exists)*
ScriptableObject wrapping the new Input System; exposes events.
```csharp
public interface IInputReader {
@@ -1335,19 +1440,19 @@ public interface IInputReader {
Generic, project-agnostic utilities.
#### `BoundedUndoStack` *(Libs/CommandStack)*
#### `BoundedUndoStack` *(Libs/CommandStack — planned)*
Implements `IUndoStack`. Source already in section 24.
- **Capacity:** default 20.
- **Invariant:** `_redo` cleared on any new `Push`.
- **Edge cases:** `Undo`/`Redo` on empty stack is a no-op (never throws).
#### `EventBus` *(Libs/EventBus)*
#### `EventBus` *(Libs/Observer — ✅ exists)*
Implements `IEventBus` with a `Dictionary<Type, Delegate>` of `Action<T>` per signal type.
- **Subscribe** returns an `IDisposable` that removes the handler on `Dispose`.
- **Publish** snapshots the invocation list before iterating (so handlers may safely unsubscribe during dispatch).
#### `Fsm<TState>` *(Libs/FSM)*
Generic state machine used by `ColorBookFlowController`.
#### `StateMachine` / `IState` / `State` *(Libs/FSM — ✅ exists)*
Generic state machine. Current shape on disk uses `IState` / `State` / `StateMachine` (see [Libs/FSM/](Assets/Darkmatter/Code/Libs/FSM/)). `ColorBookFlowController` (planned) will use this. The generic sketch below is the target shape if you decide to make it strongly-typed via an enum — verify against actual API before consuming.
```csharp
public sealed class Fsm<TState> where TState : struct, Enum {
public TState Current { get; }
@@ -1361,7 +1466,7 @@ public interface IFsmState { void Enter(); void Exit(); }
---
### 32.4 Feature — `DrawingCatalog`
### 32.4 Feature — `DrawingCatalog` *(planned)*
#### `DrawingCatalogController` *(Systems)*
Headless logic. Owns the list of template IDs visible in the grid.
@@ -1398,7 +1503,7 @@ public interface IDrawingCatalogView {
---
### 32.5 Feature — `ShapeBuilder`
### 32.5 Feature — `ShapeBuilder` *(planned)*
#### `ShapeBuilderController` *(Systems)*
Spawns shape pieces for the selected template, tracks snap progress, fires `ShapeAssembledSignal` when complete.
@@ -1437,7 +1542,7 @@ public sealed class ShapePieceFactory {
---
### 32.5b Feature — `Paper`
### 32.5b Feature — `Paper` *(planned)*
The shared art rig — RT, offscreen camera, screen↔world bridge. Every other feature in the ColorBook scene resolves `IPaperRig` and `IArtInputBridge` from DI and never touches `Screen.*` or `Camera.*` directly.
@@ -1508,7 +1613,7 @@ public void Register(IContainerBuilder builder) {
---
### 32.6 Feature — `Coloring`
### 32.6 Feature — `Coloring` *(planned)*
#### `ColoringStateRepository` *(Repository)*
In-memory model. Owns "currently selected color" and the palette in use.
@@ -1570,7 +1675,7 @@ Mirror of `ShapePieceFactory` for regions. Pool-friendly.
---
### 32.7 Feature — `History`
### 32.7 Feature — `History` *(planned)*
#### `HistoryController` *(Systems)* — `IStartable, IDisposable`
Owns the per-session `IUndoStack` (registered scoped, so a new ColorBook scene = new stack).
@@ -1602,7 +1707,7 @@ Wires controller `StateChanged` ↔ view enable/disable; view click events → c
---
### 32.8 Feature — `Capture`
### 32.8 Feature — `Capture` *(planned)*
#### `CaptureController` *(Systems)*
The orchestrator behind the "Capture" button. Stateless other than guarding against concurrent captures.
@@ -1623,7 +1728,7 @@ Wires button click → `CaptureController.CaptureCurrentAsync`. Disables button
---
### 32.9 Feature — `Progression`
### 32.9 Feature — `Progression` *(planned)*
#### `ProgressionService` *(Systems)* — implements `IProgressionService`
The only place that knows what "completed" means.
@@ -1636,7 +1741,7 @@ Pure in-memory holder used by the service. Separated so tests can inspect state
---
### 32.10 Feature — `ColorBookFlow`
### 32.10 Feature — `ColorBookFlow` *(planned)*
#### `ColorBookFlowController` *(Systems)* — `IStartable, IDisposable`
**The only orchestrator inside the ColorBook scene.** Drives the panel FSM: `Catalog → Building → Coloring → Done`.
@@ -1664,7 +1769,7 @@ Pure in-memory holder used by the service. Separated so tests can inspect state
---
### 32.11 Feature — `ArtBook`
### 32.11 Feature — `ArtBook` *(planned)*
#### `GalleryPresenter` *(UI)* — `IAsyncStartable, IDisposable`
Lists artworks, opens fullscreen view, deletes, shares.
@@ -1699,7 +1804,7 @@ public interface IExternalShareService {
### 32.12 App Layer
#### `AppBoot` *(App/Boot)* — `IAsyncStartable`
#### `AppBoot` *(App/Boot — planned; folder doesn't exist yet)* — `IAsyncStartable`
Single entry point. Steps in section 29.
```csharp
// fields: IAssetProviderService _assets, IPersistenceService _persist, IProgressionService _progress,
@@ -1710,25 +1815,29 @@ public sealed class AppBoot : IAsyncStartable {
```
#### LifetimeScopes
- `RootLifetimeScope`section 21. Registers all services + `IEventBus`. Persists for app lifetime.
- `MainMenuLifetimeScope`registers `MainMenuPresenter` and view.
- `ColorBookLifetimeScope`section 21. Registers feature installers + `ColorBookFlowController` as entry point.
- `ArtBookLifetimeScope`registers `GalleryPresenter` + view + `IExternalShareService`.
- `RootLifetimeScope`✅ exists ([source](Assets/Darkmatter/Code/App/LifetimeScopes/RootLifetimeScope.cs)). Iterates a serialized `MonoBehaviour[] serviceModules` and calls `Register` on each `IServiceModule`. Persists for app lifetime.
- `MainMenuLifetimeScope`planned. Same pattern as Root (serialized installer list, no hardcoded registrations).
- `ColorBookLifetimeScope`planned. Same pattern; installer list includes `PaperRigModule`, feature installers, and the flow controller installer.
- `ArtBookLifetimeScope`planned.
All scope classes are thin: serialized fields for scene refs, `Configure(IContainerBuilder)` only.
All scope classes are thin: a serialized installer-MonoBehaviour list (+ optional scene refs as separate fields) and a `Configure(IContainerBuilder)` that iterates and calls `Register`.
---
### 32.13 Cross-cutting types
#### `ColorBookSceneRefs : MonoBehaviour` *(App)*
Aggregates all scene-bound Unity references that features need: `Camera artCamera`, `Transform catalogRoot`, `Transform builderRoot`, `Transform coloringRoot`, `RectTransform hudRoot`, `ColorPaletteView paletteView`, `HistoryButtonsView historyView`. Registered as a singleton in `ColorBookLifetimeScope` so features don't `Find` things.
#### `ColorBookSceneRefs : MonoBehaviour` *(App — planned)*
Aggregates scene-bound Unity references that features need: `Camera artCamera`, `Transform catalogRoot`, `Transform builderRoot`, `Transform coloringRoot`, `RectTransform hudRoot`, `ColorPaletteView paletteView`, `HistoryButtonsView historyView`. Registered in `ColorBookLifetimeScope` via `builder.RegisterInstance(_sceneRefs)` so features don't `Find` things.
#### `IInstaller` *(App)*
> Most of these refs are subsumed by `IPaperRig` now (which owns `ArtCamera` and `PaperRoot`). `ColorBookSceneRefs` reduces to the HUD-side refs (palette view, history buttons, panel roots).
#### `IServiceModule` *(Libs/Installers — ✅ exists)*
```csharp
public interface IInstaller { void Install(IContainerBuilder builder); }
public interface IServiceModule {
void Register(IContainerBuilder builder);
}
```
Implemented as `ScriptableObject` per feature so scopes can drag them in the inspector (section 22).
Implemented as `MonoBehaviour` per feature/service so scopes can drag them in the inspector ([CameraServiceModule.cs](Assets/Darkmatter/Code/Services/Camera/Installers/CameraServiceModule.cs) shows the pattern). The method is `Register`, not `Install` — there is no `IInstaller` in this project.
---
@@ -1766,3 +1875,5 @@ Implemented as `ScriptableObject` per feature so scopes can drag them in the ins
| `ProgressionService` | Service | Completion tracking | persistence |
If you add a class not in this table, add it here in the same PR. This table is the cheap mental-model index — keep it honest.
> Today only these rows are real on disk: `RootLifetimeScope` (App), `AddressableAssetProviderService` (Service), `AudioService` (Service), `CameraService` (Service), `SceneService` (Service), `InputReaderSO` (Service), plus the Firebase analytics class, plus the `Libs.*` entries (`EventBus`, `StateMachine`, `IServiceModule`, PlayerPrefs lib, UI toggles). Everything else is the target.