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; using UnityEngine;
namespace Darkmatter.Core.Contracts.Paper namespace Darkmatter.Core.Contracts.Features.Paper
{ {
public interface IArtInputBridge public interface IArtInputBridge
{ {

View File

@@ -1,6 +1,6 @@
using UnityEngine; using UnityEngine;
namespace Darkmatter.Core.Contracts.Paper namespace Darkmatter.Core.Contracts.Features.Paper
{ {
public interface IPaperRig 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 ## 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/ Assets/Darkmatter/
├── App/ ├── Scenes/
── Boot/ ── Boot.unity ← only scene wired so far
│ │ └── AppBoot.cs
│ └── LifetimeScopes/
│ ├── RootLifetimeScope.cs
│ ├── MainMenuLifetimeScope.cs
│ ├── ColorBookLifetimeScope.cs
│ └── ArtBookLifetimeScope.cs
├── Core/ ├── Content/ ← singular ("Content", not "Contents")
── Drawing/ ── Gameplay/
── IDrawingTemplate.cs ── PaperRig/ ← (planned — paper rig prefabs)
│ │ ├── 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
├── Libs/ ├── Data/
│ ├── CommandStack/ (generic bounded undo/redo) │ ├── Inputs/ (Input System .inputactions)
── EventBus/ (shared with bus game if monorepo) ── Settings/
└── FSM/ (optional, for ColorBookFlow) ├── Persistance/Resources/ (ProtectedPlayerPrefs settings)
│ ├── Rendering/ (URP renderer + asset)
│ └── Scenes/URP2DSceneTemplate.unity
── Services/ ── Code/
├── Audio/ ├── App/
── Inputs/ ── LifetimeScopes/
│ ├── Assets/ (Addressables wrapper — IAssetProviderService) │ └── RootLifetimeScope.cs ← scope loads serialized IServiceModule list
├── Scenes/ │ Darkmatter.App.asmdef
├── Persistence/ (JSON / PlayerPrefs for non-image state)
├── Gallery/ (file IO — PNG + sidecar JSON) ├── Core/ (asmdef name: `Core`, namespace root `Darkmatter.Core.*`)
│ └── Capture/ (RenderTexture → PNG, paper bg compositing) │ ├── Compatibility/
│ │ └── IsExternalInit.cs (C#9 init shim for older runtimes)
└── Features/ │ ├── Contracts/
├── MainMenu/ │ │ ├── Paper/ ← misplaced empty folder — should be Contracts/Features/Paper/ (delete or move)
├── DrawingCatalog/ │ │ └── Services/
├── Paper/ (RT paper rig — ArtCamera + RenderTexture + input bridge) │ │ ├── Assets/IAssetProviderService.cs
├── ShapeBuilder/ │ │ ├── Audio/IAudioService.cs, ISfxPlayer.cs
├── Coloring/ │ │ ├── Camera/ICameraService.cs
├── History/ │ │ ├── Capture/ ← (planned — ICaptureService)
├── Capture/ │ │ ├── Inputs/IInputReader.cs
├── Progression/ │ │ └── Scenes/ISceneService.cs
├── ColorBookFlow/ (orchestrates panel swap, next, capture chain) │ ├── Data/
└── ArtBook/ │ │ ├── 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]/ Features/[Name]/
├── Installers/ IInstaller — VContainer registrations ├── Installers/ IServiceModule — VContainer registration
├── Systems/ Controllers, services (pure C#) ├── Systems/ (or Service/) Controllers, repositories, factories (pure C#)
├── Repository/ In-memory state holders ├── UI/ (only if the feature has Canvas UI)
├── Commands/ ICommand implementations (if feature mutates undoable state) │ ├── *Presenter.cs Pure C#, listens to model, drives view
├── UI/ │ └── *View.cs MonoBehaviour, setters only
│ ├── *Presenter.cs Pure C#, listens to model, drives view ├── Views/ (only if the feature has world-space MonoBehaviours)
│ └── *View.cs MonoBehaviour, setters only └── Features.<Name>.asmdef
├── Views/ World-space MonoBehaviours (sprites, colliders)
└── Docs/ Feature-specific markdown
``` ```
### Asset folder parallel 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/`).
Assets/Darkmatter/Contents/ - **Skip nesting entirely** when the feature has only 12 files at root (like `Services/Audio/AudioService.cs` + `SfxPlayer.cs` flat).
├── Drawings/ - **`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.
│ ├── 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)
```
--- ---
## 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.Core.Contracts.Services.Camera` ([ICameraService.cs](Assets/Darkmatter/Code/Core/Contracts/Services/Camera/ICameraService.cs))
- `Darkmatter.Features.ShapeBuilder` - `Darkmatter.Services.Camera` ([CameraService.cs](Assets/Darkmatter/Code/Services/Camera/Service/CameraService.cs))
- `Darkmatter.Services.Gallery` - `Darkmatter.Services.Camera.Installers` ([CameraServiceModule.cs](Assets/Darkmatter/Code/Services/Camera/Installers/CameraServiceModule.cs))
- `Darkmatter.Services.Capture` - `Darkmatter.Libs.Installers` ([IServiceModule.cs](Assets/Darkmatter/Code/Libs/Installers/IServiceModule.cs))
- `Darkmatter.Core.Drawing`
- `Darkmatter.Lib.CommandStack`
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 ## 6. Scenes & Lifetime Scopes
| Scene | Scope | Contents | | Scene | Scope | Status | Contents |
|---|---|---| |---|---|---|---|
| `Boot.unity` | `RootLifetimeScope` | All Services + `IEventBus`. Persists forever. | | `Boot.unity` | `RootLifetimeScope` | ✅ exists | All Services + Libs. Persists forever. |
| `MainMenu.unity` | `MainMenuLifetimeScope` | Menu presenter, art book entry. | | `MainMenu.unity` | `MainMenuLifetimeScope` | ⚠️ planned | Menu presenter, art book entry. |
| `ColorBook.unity` | `ColorBookLifetimeScope` | DrawingCatalog, ShapeBuilder, Coloring, History, Capture, ColorBookFlow. | | `ColorBook.unity` | `ColorBookLifetimeScope` | ⚠️ planned | `PaperRig`, DrawingCatalog, ShapeBuilder, Coloring, History, Capture, ColorBookFlow. |
| `ArtBook.unity` | `ArtBookLifetimeScope` | Gallery presenter, viewer, share. | | `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. 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). 1. Initialize `IAssetProviderService` (Addressables init).
2. Preload essential bundles (palettes, UI sounds). 2. Preload essential bundles (palettes, UI sounds).
@@ -275,8 +345,10 @@ All Core types are pure data or interfaces.
### Drawing ### Drawing
> Contracts live in `Darkmatter.Core.Contracts.Features.Drawing`; DTOs in `Darkmatter.Core.Data.Dynamic.Features.Drawing`.
```csharp ```csharp
namespace Darkmatter.Core.Drawing; namespace Darkmatter.Core.Contracts.Features.Drawing;
public interface IDrawingTemplate { public interface IDrawingTemplate {
string Id { get; } string Id { get; }
@@ -305,8 +377,10 @@ public readonly struct ColorRegionDTO {
### Coloring ### Coloring
> Contracts in `Darkmatter.Core.Contracts.Features.Coloring`; DTOs in `Darkmatter.Core.Data.Dynamic.Features.Coloring`.
```csharp ```csharp
namespace Darkmatter.Core.Coloring; namespace Darkmatter.Core.Contracts.Features.Coloring;
public interface IColorPalette { public interface IColorPalette {
string Id { get; } string Id { get; }
@@ -322,8 +396,10 @@ public readonly struct PaintCommandDTO {
### Paper (RT rig + input bridge) ### Paper (RT rig + input bridge)
> Contracts live in `Darkmatter.Core.Contracts.Features.Paper`. Files at `Core/Contracts/Features/Paper/`.
```csharp ```csharp
namespace Darkmatter.Core.Paper; namespace Darkmatter.Core.Contracts.Features.Paper;
public interface IPaperRig { public interface IPaperRig {
Camera ArtCamera { get; } // offscreen, targetTexture = Surface Camera ArtCamera { get; } // offscreen, targetTexture = Surface
@@ -346,8 +422,10 @@ public interface IArtInputBridge {
### History ### History
> Contracts in `Darkmatter.Core.Contracts.Features.History`.
```csharp ```csharp
namespace Darkmatter.Core.History; namespace Darkmatter.Core.Contracts.Features.History;
public interface ICommand { public interface ICommand {
void Execute(); void Execute();
@@ -366,8 +444,10 @@ public interface IUndoStack {
### Gallery & Capture ### 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 ```csharp
namespace Darkmatter.Core.Gallery; namespace Darkmatter.Core.Data.Dynamic.Features.Gallery;
public readonly struct SavedArtworkDTO { public readonly struct SavedArtworkDTO {
public string Id { get; } public string Id { get; }
@@ -377,6 +457,8 @@ public readonly struct SavedArtworkDTO {
public string ThumbnailPath { get; } public string ThumbnailPath { get; }
} }
namespace Darkmatter.Core.Contracts.Services.Gallery;
public interface IGalleryService { public interface IGalleryService {
UniTask<SavedArtworkDTO> SaveAsync(byte[] png, string templateId); UniTask<SavedArtworkDTO> SaveAsync(byte[] png, string templateId);
UniTask<IReadOnlyList<SavedArtworkDTO>> ListAsync(); UniTask<IReadOnlyList<SavedArtworkDTO>> ListAsync();
@@ -384,7 +466,7 @@ public interface IGalleryService {
UniTask DeleteAsync(string artworkId); UniTask DeleteAsync(string artworkId);
} }
namespace Darkmatter.Core.Capture; namespace Darkmatter.Core.Contracts.Services.Capture;
public interface ICaptureService { public interface ICaptureService {
// No camera or paperBg args — capture reads directly from IPaperRig.Surface. // No camera or paperBg args — capture reads directly from IPaperRig.Surface.
@@ -397,8 +479,10 @@ public interface ICaptureService {
### Signals ### Signals
> Signal structs live in `Darkmatter.Core.Data.Dynamic.Features.Signals` (runtime data, cross-feature).
```csharp ```csharp
namespace Darkmatter.Core.Signals; namespace Darkmatter.Core.Data.Dynamic.Features.Signals;
public readonly struct DrawingSelectedSignal { public readonly struct DrawingSelectedSignal {
public string TemplateId { get; } public string TemplateId { get; }
@@ -713,109 +797,123 @@ Maintained alongside the [Darkmatter Architecture Guide](../Assets/Darkmatter_Ar
## 20. Assembly Definition Map ## 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 | | Asmdef | Path | References |
|---|---|---| |---|---|---|
| `Darkmatter.App` | `App/` | All Features, all Services, Core, Libs | | `Darkmatter.App` | `App/` | All Services, Libs, Core (Features when they land) |
| `Darkmatter.Core` | `Core/` | (none — `UniTask` allowed in async signatures) | | `Core` | `Core/` | (none — `UniTask` allowed in async signatures) |
| `Darkmatter.Lib.CommandStack` | `Libs/CommandStack/` | `Darkmatter.Core` | | `Libs.FSM` | `Libs/FSM/` | `Core` |
| `Darkmatter.Lib.EventBus` | `Libs/EventBus/` | `Darkmatter.Core` | | `Libs.Installers` | `Libs/Installers/` | (VContainer only) |
| `Darkmatter.Lib.FSM` | `Libs/FSM/` | `Darkmatter.Core` | | `Libs.Observer` | `Libs/Observer/` | `Core` |
| `Darkmatter.Services.Audio` | `Services/Audio/` | `Darkmatter.Core` | | `Libs.PlayerPrefs` | `Libs/PlayerPrefs/Runtime/` | (standalone) |
| `Darkmatter.Services.Inputs` | `Services/Inputs/` | `Darkmatter.Core` | | `Libs.PlayerPrefs.Editor` | `Libs/PlayerPrefs/Editor/` | `Libs.PlayerPrefs` |
| `Darkmatter.Services.Assets` | `Services/Assets/` | `Darkmatter.Core` | | `Libs.UI` | `Libs/UI/` | `Core` |
| `Darkmatter.Services.Scenes` | `Services/Scenes/` | `Darkmatter.Core` | | `Services.Analytics` | `Services/Analytics/` | `Core`, `Libs.Installers` |
| `Darkmatter.Services.Persistence` | `Services/Persistence/` | `Darkmatter.Core` | | `Services.Assets` | `Services/Assets/` | `Core`, `Libs.Installers` |
| `Darkmatter.Services.Gallery` | `Services/Gallery/` | `Darkmatter.Core` | | `Services.Audio` | `Services/Audio/` | `Core`, `Libs.Installers` |
| `Darkmatter.Services.Capture` | `Services/Capture/` | `Darkmatter.Core` | | `Services.Camera` | `Services/Camera/` | `Core`, `Libs.Installers` |
| `Darkmatter.Features.MainMenu` | `Features/MainMenu/` | `Darkmatter.Core`, Libs | | `Services.Inputs` | `Services/Inputs/` | `Core`, `Libs.Installers` |
| `Darkmatter.Features.DrawingCatalog` | `Features/DrawingCatalog/` | `Darkmatter.Core`, Libs | | `Services.Scenes` | `Services/Scenes/` | `Core`, `Libs.Installers` |
| `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` |
**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 ## 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 ```csharp
namespace Darkmatter.App.LifetimeScopes; using Darkmatter.Libs.Installers;
using UnityEngine;
using VContainer;
using VContainer.Unity;
public sealed class RootLifetimeScope : LifetimeScope { public class RootLifetimeScope : LifetimeScope {
[SerializeField] private AudioServiceConfig _audioConfig; [SerializeField] private MonoBehaviour[] serviceModules;
[SerializeField] private InputReaderSO _inputReader;
protected override void Configure(IContainerBuilder builder) { protected override void Configure(IContainerBuilder builder) {
// EventBus foreach (var module in serviceModules) {
builder.Register<IEventBus, EventBus>(Lifetime.Singleton); if (module is IServiceModule serviceModule)
serviceModule.Register(builder);
// 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>();
} }
} }
``` ```
### `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 ```csharp
namespace Darkmatter.App.LifetimeScopes; namespace Darkmatter.App.LifetimeScopes;
public sealed class ColorBookLifetimeScope : LifetimeScope { public sealed class ColorBookLifetimeScope : LifetimeScope {
[SerializeField] private ColorBookSceneRefs _sceneRefs; // ArtCamera, panel roots, prefabs [SerializeField] private MonoBehaviour[] sceneModules;
[SerializeField] private IInstaller[] _installers; // assigned in inspector
protected override void Configure(IContainerBuilder builder) { protected override void Configure(IContainerBuilder builder) {
builder.RegisterInstance(_sceneRefs); foreach (var module in sceneModules) {
if (module is IServiceModule serviceModule)
// Each feature ships an IInstaller serviceModule.Register(builder);
foreach (var installer in _installers) installer.Install(builder); }
// Scene-scoped orchestrator
builder.RegisterEntryPoint<ColorBookFlowController>();
} }
} }
``` ```
Drag these installers in the inspector: Drag the scene's installer MonoBehaviours into `sceneModules[]`:
- `DrawingCatalogServiceModule` - `PaperRigModule`
- `ShapeBuilderServiceModule` - `DrawingCatalogModule`
- `ColoringServiceModule` - `ShapeBuilderModule`
- `HistoryServiceModule` - `ColoringModule`
- `CaptureFeatureModule` - `HistoryModule`
- `ProgressionServiceModule` - `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 ## 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 ```csharp
namespace Darkmatter.Features.Coloring.Installers; namespace Darkmatter.Features.Coloring.Installers;
[CreateAssetMenu(menuName = "Darkmatter/Installers/Coloring")] public sealed class ColoringModule : MonoBehaviour, IServiceModule {
public sealed class ColoringServiceModule : ScriptableObject, IInstaller {
[SerializeField] private ColoringConfig _config; [SerializeField] private ColoringConfig _config;
public void Install(IContainerBuilder builder) { public void Register(IContainerBuilder builder) {
builder.RegisterInstance(_config); builder.RegisterInstance(_config);
builder.Register<ColoringStateRepository>(Lifetime.Scoped).AsSelf(); builder.Register<ColoringStateRepository>(Lifetime.Scoped).AsSelf();
builder.Register<ColoringController>(Lifetime.Scoped) builder.Register<ColoringController>(Lifetime.Scoped)
@@ -828,9 +926,11 @@ public sealed class ColoringServiceModule : ScriptableObject, IInstaller {
``` ```
Convention: Convention:
- One `IInstaller` per feature. - One `IServiceModule` per feature, named `<Feature>Module` (matches `CameraServiceModule`, `InputServiceModule`, `AnalyticsServiceModule` already in the project).
- `ScriptableObject` so it can be referenced by scene scope inspector. - `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. - 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 ## 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() ├─ try Addressables.InitializeAsync()
│ fail → show "Tap to retry" splash │ fail → show "Tap to retry" splash
├─ try preload palette + UI sounds (Addressables labels) ├─ try preload palette + UI sounds (Addressables labels)
@@ -1092,13 +1194,13 @@ Toddler-mode error UI:
## 30. Setup Checklist (new dev, day one) ## 30. Setup Checklist (new dev, day one)
1. Open `Bus Game.sln` (color book lives in same repo / Unity project per plan). 1. Open `Colorbook.sln` at the repo root.
2. Verify Addressables groups exist: `Drawings_*`, `Palettes`, `Audio_*`. 2. Open `Assets/Darkmatter/Scenes/Boot.unity` (currently the only scene wired).
3. Open `Boot.unity` → confirm `RootLifetimeScope` references the right configs. 3. Inspect the `RootLifetimeScope` GameObject — confirm its `serviceModules[]` list references the child installer MonoBehaviours (`AudioServiceModule`, `CameraServiceModule`, `InputServiceModule`, etc.).
4. Open `ColorBook.unity` → confirm `ColorBookLifetimeScope._installers[]` is fully populated. 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. Hit Play from `Boot.unity` (entry scene). Never start mid-flow — DI parent scope must exist. 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. To author a new drawing: duplicate `Animals/elephant/`, edit `Template.asset` (pieces + regions), add to the appropriate Addressables group. 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. 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 | | Class | Layer | Asmdef |
|---|---|---| |---|---|---|
| `IDrawingTemplate`, `ShapePieceDTO`, `ColorRegionDTO` | Core | `Darkmatter.Core` | | `IDrawingTemplate`, `ShapePieceDTO`, `ColorRegionDTO` | Core | `Core` |
| `IPaperRig`, `IArtInputBridge` | Core | `Darkmatter.Core` | | `IPaperRig`, `IArtInputBridge` | Core | `Core` |
| `ICommand`, `IUndoStack` | Core | `Darkmatter.Core` | | `ICommand`, `IUndoStack` | Core | `Core` |
| `BoundedUndoStack` | Libs | `Darkmatter.Lib.CommandStack` | | `BoundedUndoStack` | Libs | `Libs.CommandStack` |
| `AddressableAssetProviderService` | Services | `Darkmatter.Services.Assets` | | `AddressableAssetProviderService` | Services | `Services.Assets` |
| `FileGalleryService` | Services | `Darkmatter.Services.Gallery` | | `FileGalleryService` | Services | `Services.Gallery` |
| `RenderTextureCaptureService` | Services | `Darkmatter.Services.Capture` | | `RenderTextureCaptureService` | Services | `Services.Capture` |
| `PaperRig`, `ArtInputBridge`, `PaperRigModule` | Features | `Darkmatter.Features.Paper` | | `PaperRig`, `ArtInputBridge`, `PaperRigModule` | Features | `Features.Paper` |
| `ColoringController`, `PaintRegionCommand` | Features | `Darkmatter.Features.Coloring` | | `ColoringController`, `PaintRegionCommand` | Features | `Features.Coloring` |
| `ShapeBuilderController`, `ShapePieceView` | Features | `Darkmatter.Features.ShapeBuilder` | | `ShapeBuilderController`, `ShapePieceView` | Features | `Features.ShapeBuilder` |
| `HistoryController` | Features | `Darkmatter.Features.History` | | `HistoryController` | Features | `Features.History` |
| `ColorBookFlowController` | Features | `Darkmatter.Features.ColorBookFlow` | | `ColorBookFlowController` | Features | `Features.ColorBookFlow` |
| `GalleryPresenter`, `GalleryGridView` | Features | `Darkmatter.Features.ArtBook` | | `GalleryPresenter`, `GalleryGridView` | Features | `Features.ArtBook` |
| `ColorBookLifetimeScope`, `AppBoot` | App | `Darkmatter.App` | | `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. 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) ## 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). 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 > Convention used below
@@ -1134,6 +1238,7 @@ Canonical breakdown of every concrete class and interface. For each: **purpose**
> - `// pub:` = events / signals fired > - `// pub:` = events / signals fired
> - `// sub:` = events / signals consumed > - `// sub:` = events / signals consumed
> - All async returns are `UniTask` unless noted. > - 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. Pure interfaces and DTOs. Zero logic.
#### `IDrawingTemplate` *(Core/Drawing)* #### `IDrawingTemplate` *(Core/Contracts/Features/Drawing — planned)*
Immutable view of a single drawing's authored data. Immutable view of a single drawing's authored data.
```csharp ```csharp
public interface IDrawingTemplate { public interface IDrawingTemplate {
@@ -1155,7 +1260,7 @@ public interface IDrawingTemplate {
``` ```
Implemented by `DrawingTemplateSO` (ScriptableObject) loaded via Addressables. 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. Authority on which drawings exist, completion state, and "next" selection.
```csharp ```csharp
public interface IDrawingTemplateCatalog { 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`. Set of colors offered to the child. Authored as `ColorPaletteSO`.
```csharp ```csharp
public interface IColorPalette { 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. 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. Persistent store of saved artwork PNGs.
```csharp ```csharp
public interface IGalleryService { 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`. Snapshots the paper RT to a PNG blob. No arguments — dimensions and content come from `IPaperRig.Surface`.
```csharp ```csharp
public interface ICaptureService { 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. Shared art rig. The single source of truth for everything that lives in the drawing world.
```csharp ```csharp
public interface IPaperRig { 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. Converts screen-space pointer coords to art-world coords inside the RT.
```csharp ```csharp
public interface IArtInputBridge { 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. 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. Tracks which templates the child has completed and what they last opened.
```csharp ```csharp
public interface IProgressionService { 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. Addressables wrapper. Hides handle bookkeeping from features.
```csharp ```csharp
public interface IAssetProviderService { 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 ```csharp
public interface IEventBus { public interface IEventBus {
void Publish<T>(T signal) where T : struct; 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 ### 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`. Implements `IAssetProviderService`.
- **Responsibility:** Wrap `Addressables.LoadAssetAsync<T>` and ref-count handles by address. - **Responsibility:** Wrap `Addressables.LoadAssetAsync<T>` and ref-count handles by address.
- **State:** `Dictionary<string, AsyncOperationHandle>` keyed 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. - **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`. Implements `IGalleryService`.
```csharp ```csharp
// fields: // fields:
@@ -1280,13 +1385,13 @@ Implements `IGalleryService`.
- **List flow:** enumerate `*.json` in `Gallery/`, deserialize, sort by `CreatedUtc desc`. - **List flow:** enumerate `*.json` in `Gallery/`, deserialize, sort by `CreatedUtc desc`.
- **Delete flow:** delete png + thumb + json; missing files ignored (idempotent). - **Delete flow:** delete png + thumb + json; missing files ignored (idempotent).
#### `RenderTextureCaptureService` *(Services/Capture)* #### `RenderTextureCaptureService` *(Services/Capture — planned)*
Implements `ICaptureService`. 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. - **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. - **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. - **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). Implements `IPersistenceService` (small JSON blob; not the gallery).
```csharp ```csharp
public interface IPersistenceService { public interface IPersistenceService {
@@ -1298,7 +1403,7 @@ public interface IPersistenceService {
- **Format:** single JSON object keyed by `key` so multiple services can share one file. - **Format:** single JSON object keyed by `key` so multiple services can share one file.
- **Atomicity:** write to `save.json.tmp` → rename. - **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. Implements `ISceneService`. Wraps `SceneManager.LoadSceneAsync` with `UniTask` plus a fade-curtain.
```csharp ```csharp
public interface ISceneService { 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. Implements `IAudioService`. Plays SFX clips loaded by address, mixes via Unity AudioMixer groups.
```csharp ```csharp
public interface IAudioService { public interface IAudioService {
@@ -1318,7 +1423,7 @@ public interface IAudioService {
``` ```
Holds an internal `Dictionary<string, AudioClip>` populated at preload. 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. ScriptableObject wrapping the new Input System; exposes events.
```csharp ```csharp
public interface IInputReader { public interface IInputReader {
@@ -1335,19 +1440,19 @@ public interface IInputReader {
Generic, project-agnostic utilities. Generic, project-agnostic utilities.
#### `BoundedUndoStack` *(Libs/CommandStack)* #### `BoundedUndoStack` *(Libs/CommandStack — planned)*
Implements `IUndoStack`. Source already in section 24. Implements `IUndoStack`. Source already in section 24.
- **Capacity:** default 20. - **Capacity:** default 20.
- **Invariant:** `_redo` cleared on any new `Push`. - **Invariant:** `_redo` cleared on any new `Push`.
- **Edge cases:** `Undo`/`Redo` on empty stack is a no-op (never throws). - **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. Implements `IEventBus` with a `Dictionary<Type, Delegate>` of `Action<T>` per signal type.
- **Subscribe** returns an `IDisposable` that removes the handler on `Dispose`. - **Subscribe** returns an `IDisposable` that removes the handler on `Dispose`.
- **Publish** snapshots the invocation list before iterating (so handlers may safely unsubscribe during dispatch). - **Publish** snapshots the invocation list before iterating (so handlers may safely unsubscribe during dispatch).
#### `Fsm<TState>` *(Libs/FSM)* #### `StateMachine` / `IState` / `State` *(Libs/FSM — ✅ exists)*
Generic state machine used by `ColorBookFlowController`. 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 ```csharp
public sealed class Fsm<TState> where TState : struct, Enum { public sealed class Fsm<TState> where TState : struct, Enum {
public TState Current { get; } 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)* #### `DrawingCatalogController` *(Systems)*
Headless logic. Owns the list of template IDs visible in the grid. 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)* #### `ShapeBuilderController` *(Systems)*
Spawns shape pieces for the selected template, tracks snap progress, fires `ShapeAssembledSignal` when complete. 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. 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)* #### `ColoringStateRepository` *(Repository)*
In-memory model. Owns "currently selected color" and the palette in use. 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` #### `HistoryController` *(Systems)* — `IStartable, IDisposable`
Owns the per-session `IUndoStack` (registered scoped, so a new ColorBook scene = new stack). 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)* #### `CaptureController` *(Systems)*
The orchestrator behind the "Capture" button. Stateless other than guarding against concurrent captures. 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` #### `ProgressionService` *(Systems)* — implements `IProgressionService`
The only place that knows what "completed" means. 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` #### `ColorBookFlowController` *(Systems)* — `IStartable, IDisposable`
**The only orchestrator inside the ColorBook scene.** Drives the panel FSM: `Catalog → Building → Coloring → Done`. **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` #### `GalleryPresenter` *(UI)* — `IAsyncStartable, IDisposable`
Lists artworks, opens fullscreen view, deletes, shares. Lists artworks, opens fullscreen view, deletes, shares.
@@ -1699,7 +1804,7 @@ public interface IExternalShareService {
### 32.12 App Layer ### 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. Single entry point. Steps in section 29.
```csharp ```csharp
// fields: IAssetProviderService _assets, IPersistenceService _persist, IProgressionService _progress, // fields: IAssetProviderService _assets, IPersistenceService _persist, IProgressionService _progress,
@@ -1710,25 +1815,29 @@ public sealed class AppBoot : IAsyncStartable {
``` ```
#### LifetimeScopes #### LifetimeScopes
- `RootLifetimeScope`section 21. Registers all services + `IEventBus`. Persists for app lifetime. - `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`registers `MainMenuPresenter` and view. - `MainMenuLifetimeScope`planned. Same pattern as Root (serialized installer list, no hardcoded registrations).
- `ColorBookLifetimeScope`section 21. Registers feature installers + `ColorBookFlowController` as entry point. - `ColorBookLifetimeScope`planned. Same pattern; installer list includes `PaperRigModule`, feature installers, and the flow controller installer.
- `ArtBookLifetimeScope`registers `GalleryPresenter` + view + `IExternalShareService`. - `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 ### 32.13 Cross-cutting types
#### `ColorBookSceneRefs : MonoBehaviour` *(App)* #### `ColorBookSceneRefs : MonoBehaviour` *(App — planned)*
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. 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 ```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 | | `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. 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.