Gallery Done

This commit is contained in:
Savya Bikram Shah
2026-05-27 10:55:39 +05:45
parent 3791708a17
commit f3b53be39d
19 changed files with 415 additions and 201 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:

492
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 ├── Data/
│ └── ColorRegionDTO.cs ├── Inputs/ (Input System .inputactions)
── Coloring/ ── Settings/
├── IColorPalette.cs ├── Persistance/Resources/ (ProtectedPlayerPrefs settings)
── PaintCommandDTO.cs ── Rendering/ (URP renderer + asset)
├── Paper/ (shared art rig — RT-as-paper) └── Scenes/URP2DSceneTemplate.unity
│ ├── IPaperRig.cs
│ │ └── IArtInputBridge.cs └── Code/
├── History/ ├── App/
── ICommand.cs ── LifetimeScopes/
│ └── IUndoStack.cs └── RootLifetimeScope.cs ← scope loads serialized IServiceModule list
├── Gallery/ │ Darkmatter.App.asmdef
│ ├── IGalleryService.cs
│ │ └── SavedArtworkDTO.cs ├── Core/ (asmdef name: `Core`, namespace root `Darkmatter.Core.*`)
│ ├── Capture/ │ ├── Compatibility/
│ │ └── ICaptureService.cs │ │ └── IsExternalInit.cs (C#9 init shim for older runtimes)
│ ├── Progression/ │ ├── Contracts/
│ │ ── IProgressionService.cs │ │ ── Paper/ ← misplaced empty folder — should be Contracts/Features/Paper/ (delete or move)
│ └── Signals/ │ └── Services/
│ ├── DrawingSelectedSignal.cs │ ├── Assets/IAssetProviderService.cs
│ ├── ShapeAssembledSignal.cs │ ├── Audio/IAudioService.cs, ISfxPlayer.cs
│ ├── ColorAppliedSignal.cs │ ├── Camera/ICameraService.cs
│ ├── ArtworkCapturedSignal.cs │ ├── Capture/ ← (planned — ICaptureService)
└── ArtworkSavedSignal.cs ├── 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/ ├── Libs/
│ ├── CommandStack/ (generic bounded undo/redo) │ ├── FSM/ (IState, State, StateMachine + Docs)
├── EventBus/ (shared with bus game if monorepo) Libs.FSM.asmdef
── FSM/ (optional, for ColorBookFlow) ── 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/ ── Services/
├── Audio/ ├── Analytics/
│ ├── Inputs/ │ ├── Installers/AnalyticsServiceModule.cs
│ ├── Assets/ (Addressables wrapper — IAssetProviderService) │ └── Systems/FirebaseAnalyticsSystem.cs
│ ├── Scenes/ │ Services.Analytics.asmdef
├── Persistence/ (JSON / PlayerPrefs for non-image state) ├── Assets/
│ ├── Gallery/ (file IO — PNG + sidecar JSON) ├── AddressableAssetProviderService.cs
│ └── Capture/ (RenderTexture → PNG, paper bg compositing) └── AddressableLoadHandleTracker.cs
│ Services.Assets.asmdef
└── Features/ ├── Audio/
├── MainMenu/ │ ├── AudioService.cs
├── DrawingCatalog/ │ └── SfxPlayer.cs
├── Paper/ (RT paper rig — ArtCamera + RenderTexture + input bridge) Services.Audio.asmdef
├── ShapeBuilder/ ├── Camera/
├── Coloring/ │ ├── Installers/CameraServiceModule.cs
├── History/ │ └── Service/CameraService.cs
├── Capture/ │ Services.Camera.asmdef
├── Progression/ ├── Inputs/
├── ColorBookFlow/ (orchestrates panel swap, next, capture chain) │ ├── Generated/GameInputs.cs (Input System codegen)
└── ArtBook/ │ ├── 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)
├── UI/
│ ├── *Presenter.cs Pure C#, listens to model, drives view │ ├── *Presenter.cs Pure C#, listens to model, drives view
│ └── *View.cs MonoBehaviour, setters only │ └── *View.cs MonoBehaviour, setters only
├── Views/ World-space MonoBehaviours (sprites, colliders) ├── Views/ (only if the feature has world-space MonoBehaviours)
└── Docs/ Feature-specific markdown └── Features.<Name>.asmdef
``` ```
### 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.
--- ---
@@ -1092,13 +1192,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 +1206,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.