- Current phase: Execution (team spawning)
- Owner: team-lead (Claude)
- Last updated: 2026-06-04
Mail attachments must carry rich per-item metadata for equipment/loot mails. Replacing the
previously shipped flat Rarity(string) + Level(int) attachment fields (see
Devlog_Mail_Attachment_RarityLevel.md, now superseded), each Item attachment now embeds
a JSON array of item instances inside PayoutAssetId. Currency attachments keep a plain
string PayoutAssetId (e.g. "mine_1").
Per item instance (PascalCase, matches PO example):
| Field | Type | Default |
|---|---|---|
BlueprintId |
string | "" |
CurrentLevel |
int | 1 |
Rarity |
enum int | 0 (None) |
InitialLevel |
int | 1 |
FromSource |
string | "" |
Rarity enum: None=0, Common=1, Rare=2, Epic=3, Legendary=4, Mythic=5. Stored/serialized as int.
Must flow end-to-end through every send path (Admin global, Admin user, User gift if applicable), survive store + read-back, and be authorable in both UnityClient AdminMailWindow and AdminWeb send form. QA writes fully passing tests.
PayoutAssetIdstaysstringat wire/storage layer (no schema type change).- For
AssetType == "Item":PayoutAssetIdvalue is a JSON array string ofItemSpecificAsset. - For
AssetType == "Currency"(or other):PayoutAssetIdstays a plain id string. - Remove flat
Rarity(string)/Level(int) fromMailAttachment,Payout,MailAttachmentDtoand all 5 mappers. Mappers already copyPayoutAssetId/ItemIdthrough, so the embedded JSON rides along for free — net mapper change is field removal. - Add shared
Rarityenum +ItemSpecificAssetmodel + serialize/parse helpers in each layer. - UI (AdminMailWindow + AdminWeb form): per-attachment, when type=Item, edit a list of item rows;
serialize rows → JSON array →
PayoutAssetIdon send. When type=Currency, plain id input.
- Replace old flat fields (not additive).
PayoutAssetIdfor Item is always a JSON array[{...}]even for a single item.- tmux panes if available (they are), else background-agent fallback.
Rarityenum +ItemSpecificAssetmodel in CloudCode, UnityClient, AdminWeb.- Remove flat Rarity/Level everywhere; update mappers + tests.
- Item PayoutAssetId = JSON array string; Currency = plain string.
- Author + send from AdminMailWindow (Unity Editor) and AdminWeb form.
- Round-trip + parse/serialize + enum + defaults + legacy-data tests, all green.
- No gameplay consumption of the new fields (grant logic unchanged beyond carrying data).
- No DB migration; additive-safe. Old mails: flat Rarity/Level simply disappear from schema; old item PayoutAssetId strings (plain) still deserialize (parser tolerates non-JSON → empty list).
- No new send endpoints.
"Attachments": [
{ "AssetType": "Currency", "Chance": 1, "PayoutAmount": 25, "PayoutAssetId": "mine_1" },
{ "AssetType": "Item", "Chance": 1, "PayoutAmount": 1,
"PayoutAssetId": "[{\"BlueprintId\":\"\",\"CurrentLevel\":1,\"Rarity\":1,\"InitialLevel\":1,\"FromSource\":\"\"}]" }
](PayoutAssetId for Item is a string whose content is the escaped JSON array.)
- Add
public enum Rarity { None=0, Common=1, Rare=2, Epic=3, Legendary=4, Mythic=5 }. - Add
public class ItemSpecificAssetwith the 5 fields +[JsonPropertyName]PascalCase, defaults per table.Rarityserialized as int (enum default = numeric). - Add
MailSchemaHelper.SerializeItemAssets(List<ItemSpecificAsset>) -> stringandParseItemAssets(string) -> List<ItemSpecificAsset>(tolerant: empty/non-JSON →[]). - Remove
Rarity/LevelfromMailAttachment(L82,85),Payout(L381,382),MailAttachmentDto(L476-480) and from mappersToAttachmentsx2,MapAttachments,MapPayouts,MapAttachmentDtos(drop the Rarity/Level copy lines). - Send modules (
SendGlobalMailModule,SendUserMailModule,GiftMailModule) unchanged — delegate to mappers; verify none copy removed fields manually. - Optional validation: when
AssetType=="Item",ParseItemAssets(PayoutAssetId)must succeed.
- Mirror
Rarityenum +ItemSpecificAsset(serializable). Remove flat rarity/level fromMailAttachment(~L111),MailAttachmentInfo(~L183),MailItem.attachmentsprojection(~L230). AdminMailWindow.cs: per-attachment draft — type dropdown; if Item, list of item rows (BlueprintId, CurrentLevel, Rarity dropdown, InitialLevel, FromSource) with add/remove;BuildAttachmentsserializes rows → JSON array →PayoutAssetId. Currency → plain id field.
- Remove flat
rarity/level(+ PascalCase variants) fromMailAttachment,AttachmentDraft,MailAttachmentInfo/AttachmentReadonlyEntry. - Add
ItemSpecificAssetTS interface +Rarityenum/const map. main.ts: attachment form — when type=Item show repeatable item-row editor; draft→request serializes rows to JSON array string inpayoutAssetId. Readonly view parses + displays rows.
| # | Task | Owner | Dep |
|---|---|---|---|
| 1 | Ratify field names/casing, enum int, array-always, per-layer struct+mapper list | architect | — |
| 2 | CloudCode: Rarity enum + ItemSpecificAsset + helpers; remove flat fields from structs+5 mappers; verify send modules | backend-dev | 1 |
| 3 | UnityClient: enum+model, remove flat fields, AdminMailWindow item-row editor + BuildAttachments | unity-client | 1 |
| 4 | AdminWeb: types.ts + main.ts item-row editor + serialize/parse, remove flat fields | unity-client | 1 |
| 5 | QA: CloudCode serialize/parse + round-trip + enum + defaults + legacy tests; UnityClient EditMode; AdminWeb build/type | QA | 2,3,4 |
- CloudCode (
BackpackAdventuresModuleTests~):ItemSpecificAssetserialize→parse round trip; enum int values (Common=1…Mythic=5,None=0); Item PayoutAssetId = JSON array survives MapPayouts→MapAttachmentDtos + ToAttachments; Currency PayoutAssetId stays plain string; defaults; legacy plain-string + missing-field tolerance.dotnet testgreen. - UnityClient (
Tests/EditMode): extendMailboxApiPositiveTests+FakeCloudCodeBackendto set + assert item rows round-trip through PayoutAssetId. - AdminWeb:
tsc --noEmit+vite buildexit 0; draft→request serialization unit-checked.
- All 3 layers compile/build.
- Item PayoutAssetId carries JSON array with all 5 fields; Rarity as int; Currency stays plain.
- Flat Rarity/Level fully removed; no dangling references.
- Defaults + legacy data deserialize safely.
- All new + existing tests pass.
| Phase | Model/Agent | Reason |
|---|---|---|
| Design ratify, final review | team-lead (Opus) + architect (sonnet) | Authoritative design + gate |
| CloudCode impl | backend-dev (sonnet) | Focused C# server edits |
| UnityClient + AdminWeb impl | unity-client (sonnet) | C# editor + TS UI |
| Tests | QA (sonnet) | Round-trip + build verification |
MCP: ai-game-developer for Unity-side inspection/test runs when connectable.
- Team
cloudcode-item-asset: architect(gate), backend-dev(CloudCode), unity-client(UnityClient+AdminWeb), QA(tests). All sonnet, tmux panes. - Architect ratified 2026-06-04 — design matches Devlog, no deviations. Key findings folded in:
- CloudCode: System.Text.Json →
[JsonPropertyName]; enum serializes as int by DEFAULT (no converter). - UnityClient: Newtonsoft.Json →
[JsonProperty](NOT JsonPropertyName); enum int by default. - AssetType casing: storage lowercase
"item", DTO PascalCase"Item"→ compare case-insensitive. - 5 mappers need ZERO logic change beyond removing flat Rarity/Level lines (PayoutAssetId pass-through).
- AdminWeb has BOTH list + inline attachment editors — both updated.
- CRG not indexed for UnityCloudCode (separate git repo) → architect used direct file read (stated).
- CloudCode: System.Text.Json →
- Breaking tests (QA owns): CloudCode
AttachmentTypeTests.cs(6 tests),AttachmentRarityLevelSendTests.cs(whole file), UnityClientMailboxApiPositiveTests.csP02B/P03B/P04B + P03A (Item plain-string assert) +FakeCloudCodeBackend.cs.
- CloudCode
dotnet test: 104 passed / 0 failed — independently re-run by team-lead via Windows dotnet (module net7.0, tests net9.0). Confirms System.Text.Json default emits Rarity as int.- New
Contract/ItemSpecificAssetTests.cs(9): serialize↔parse round-trip, Rarity int Theory (None=0…Mythic=5), JSON array survives MapPayouts→MapAttachmentDtos→ToAttachments, Currency stays plain string, defaults,[{}]→defaults, legacy non-JSON→empty, null/empty→empty,[]. - Replaced
AttachmentRarityLevelSendTests.cs→ItemSpecificAssetSendTests(2): JSON array in Item PayoutAssetId, Currency plain, no flat fields in stored Payout. - Removed 6 stale flat-field tests from
AttachmentTypeTests.cs; keptMapPayouts_PreservesCustomAssetTypeAndChance.
- New
- AdminWeb:
tsc --noEmitexit 0,vite buildexit 0 (QA-run). - UnityClient EditMode: 61 passed / 0 failed / 2 skipped — executed in-session via ai-game-developer MCP
tests-run(testMode:EditMode, testNamespace:"BackpackAdventures.CloudCode.Client.Tests"). MCP summary:Status: Passed, TotalTests: 507, PassedTests: 61, FailedTests: 0, SkippedTests: 2, Duration: 00:00:19.66. Replaced P02B/P03B/P04B + fixed P03A (Item now JSON array not plain "ticket"); cleanedFakeCloudCodeBackend.CreateMailItem; added Newtonsoft.Json to test asmdef. The 2 skips are pre-existing (N11_DeleteMail_GlobalMail_Rejected,R04_EvictionPolicy_NeverDropUnclaimedReward_EXPLICIT). Hermetic againstFakeCloudCodeBackend, no live UGS.
- UnityCloudCode is its OWN git repo (
dyCuong03/UnityCloudCode, branch develop), separate from the parent Unity project repo. - Working tree had heavy pre-existing line-ending (CRLF/LF) churn on unrelated files; HEAD contained NO flat Rarity/Level (entire prior flat-field session was uncommitted). Commit staged ONLY task files with real content; excluded
.metaand pure-EOL-churn files (AttachmentTypeTests.cs,FakeCloudCodeBackend.cs,style.css,MailboxTestRunner.cs,TestConstants.cs) which are content-identical to HEAD and compile clean.
- JSON escaping: PayoutAssetId Item value is escaped JSON-in-JSON; each layer must serialize/parse, not double-escape.
- Casing: nested item fields PascalCase; existing attachment camelCase input vs PascalCase output unchanged.
- Don't touch
.metafiles. - Removing flat fields breaks prior tests asserting them → QA must update/remove those.
COMPLETE (pending PO final review + UnityClient EditMode execution-method confirmation).
ItemSpecificAsset added end-to-end; flat Rarity/Level fully removed:
- CloudCode:
Rarityenum +ItemSpecificAsset+ Serialize/ParseItemAssets; Item PayoutAssetId = JSON array string (always), Currency = plain id; 5 mappers pass-through. 104 tests green. - UnityClient: Newtonsoft model + AdminMailWindow item-row editor (Currency plain / Item rows→JSON array).
- AdminWeb: types.ts + main.ts list & inline item-row editors; tsc + vite green.
Target JSON achieved:
"Attachments":[{"AssetType":"Item","Chance":1,"PayoutAmount":1,"PayoutAssetId":"[{\"BlueprintId\":\"\",\"CurrentLevel\":1,\"Rarity\":1,\"InitialLevel\":1,\"FromSource\":\"\"}]"}]Files (no .meta touched):
CloudCodeModule/BackpackAdventuresModule~/Mailbox/MailboxModels.csUnityClient/Runtime/CloudCodeModels.cs,UnityClient/Editor/CloudCodeFeature/AdminMailWindow.csAdminWeb/src/main.ts,AdminWeb/src/types.ts- Tests:
Contract/ItemSpecificAssetTests.cs(new),Contract/AttachmentRarityLevelSendTests.cs(replaced→send tests),UnityClient/Tests/EditMode/MailboxApiPositiveTests.cs,BackpackAdventures.CloudCode.Client.Tests.asmdef