Add a presenter
A feature reaches the Client only as a presenter. Build it as one vertical slice, in this order.
1. Server contract
In projectz-nakama (v3): add the operation, DTOs, error codes, auth /
mutation / idempotency metadata, the projection policy and literal
request/success/error/replay fixtures. Regenerate the contract artifacts with
tools/contract-sync.sh; CI publishes the contract bundle. Tag the contract
revision (contract/<major>.<minor>) when it changes.
2. Import the contract
In ProjectV-SDK, import the validated bundle with
ci/import-contract-bundle.sh (through the typed generator) and run
ci/run-generator-gates.sh: the backend-dtos . --check drift check and the
normalized contract gate. tools/ContractGuard fails on a missing, extra,
renamed or metadata-mismatched operation.
3. Typed client
In com.silvertiger.projectv.client: backend-neutral DTOs, a strict decoder,
the typed capability client and its state slot / projection policy. Async
surface uses Task; no Unity, no raw RPC, no client-side arithmetic for
server-owned values.
4. Nakama binding
In com.silvertiger.projectv.client.nakama: bind the typed operation to the
Nakama transport and register it in the internal capability clients. Never
add a generic RPC method as a shortcut.
5. Presenter
In com.silvertiger.projectv.unity/Runtime/Presentation/<Feature>/ follow
the existing layout (Shop is a complete example):
| File | Visibility | Content |
|---|---|---|
ProjectV<Feature>Contracts.cs | public | IProjectV<Feature>Presenter, snapshot, status enum, error, command result |
ProjectV<Feature>Snapshots.cs | public | Immutable view types used by the snapshot |
IProjectV<Feature>Backend.cs | internal | Port the presenter calls |
ProjectV<Feature>SdkAdapter.cs | internal | Implements the port over the typed client and state store |
ProjectV<Feature>Presenter.cs | internal | Single-flight commands, snapshot publication, recovery state |
Snapshot rules: immutable; Status + data + Error (+ Recovery on data
screens); safe player text in SafeMessage or a LocalizationKey; typed IDs
instead of raw strings.
Register the presenter in ProjectVPresentationLifetimeScope
(com.silvertiger.projectv.integrations.vcontainer) as the interface only.
6. Screen contract
A page-facing feature also gets a screen contract in
ProjectV<Feature>Screen.cs (Shop and Campaign are small examples):
| Type | Visibility | Content |
|---|---|---|
ProjectV<Feature>View | public | Immutable view: IsLoading, LoadError and the confirmed data |
IProjectV<Feature>Screen | public | Current, Observe(render) and the commands, each returning ProjectVResult |
ProjectV<Feature>Screen | internal | Built on ProjectVScreenCore<TView>: joins repeated commands, dispatches renders, keeps the last confirmed view |
ProjectVScreens.<Feature>(presenter) | public | Factory for hosts without VContainer and for tests |
Rules: render runs only when the view data changes, so compare the data by
reference and keep the shown view when a read answers from the cache.
ProjectVResult.Message is player-facing text only. Register the screen as a
singleton next to its presenter.
7. Tests
Add tests for stale, duplicate, logout, replay, projection and failure paths,
and run the suites with ci/run-projectv-tests.sh (the .local/ roots from
Local development). The normal mutation path must be
one RPC with zero follow-up reads.
8. Release and adopt
Build a candidate and release it through BuildOps (Release pipeline), then pin it in the Client. Add the Client screen only against a released package, and add a page to the Presenter reference in the same change as the presenter (see Documentation standard).
Done criteria
- Contract guard and generator gates pass against the intended server revision.
- Unknown, duplicate, malformed and incomplete projections fail closed.
- Replays return the same projections.
- The public API adds only the presenter and screen contracts and their snapshot and view types; no raw RPC, reflection activation or service locator.
- The request budget in Architecture holds.
- The presenter page exists and
npm run buildinwebsite/passes.