@arken/node 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/modules/character/character.service.js.map +1 -1
- package/build/modules/chat/chat.service.js.map +1 -1
- package/build/modules/core/core.models.js.map +1 -1
- package/build/modules/core/core.service.js.map +1 -1
- package/build/modules/profile/profile.service.js.map +1 -1
- package/build/package.json +2 -2
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/types.d.ts +1 -0
- package/build/types.js +1 -0
- package/build/types.js.map +1 -1
- package/build/util/mongo.js.map +1 -1
- package/db.ts +76 -1
- package/index.ts +351 -18
- package/{util/mongo.ts → mongo.ts} +2 -0
- package/package.json +3 -3
- package/tsconfig.json +33 -2
- package/types.ts +2 -0
- package/util.ts +1 -0
- package/modules/area/area.models.ts +0 -15
- package/modules/area/area.router.ts +0 -74
- package/modules/area/area.schema.ts +0 -22
- package/modules/area/area.service.ts +0 -124
- package/modules/area/area.types.ts +0 -26
- package/modules/area/index.ts +0 -5
- package/modules/asset/asset.models.ts +0 -59
- package/modules/asset/asset.router.ts +0 -55
- package/modules/asset/asset.schema.ts +0 -27
- package/modules/asset/asset.service.ts +0 -85
- package/modules/asset/asset.types.ts +0 -22
- package/modules/asset/index.ts +0 -5
- package/modules/chain/chain.models.ts +0 -50
- package/modules/chain/chain.router.ts +0 -104
- package/modules/chain/chain.schema.ts +0 -52
- package/modules/chain/chain.service.ts +0 -167
- package/modules/chain/chain.types.ts +0 -24
- package/modules/chain/index.ts +0 -5
- package/modules/character/character.models.ts +0 -174
- package/modules/character/character.router.ts +0 -314
- package/modules/character/character.schema.ts +0 -147
- package/modules/character/character.service.ts +0 -875
- package/modules/character/character.types.ts +0 -64
- package/modules/character/index.ts +0 -5
- package/modules/chat/chat.models.ts +0 -43
- package/modules/chat/chat.router.ts +0 -67
- package/modules/chat/chat.schema.ts +0 -36
- package/modules/chat/chat.service.ts +0 -120
- package/modules/chat/chat.types.ts +0 -20
- package/modules/chat/index.ts +0 -5
- package/modules/collection/collection.models.ts +0 -76
- package/modules/collection/collection.router.ts +0 -91
- package/modules/collection/collection.schema.ts +0 -90
- package/modules/collection/collection.service.ts +0 -192
- package/modules/collection/collection.types.ts +0 -36
- package/modules/collection/index.ts +0 -5
- package/modules/core/core.models.ts +0 -1379
- package/modules/core/core.router.ts +0 -1781
- package/modules/core/core.schema.ts +0 -847
- package/modules/core/core.service.ts +0 -2822
- package/modules/core/core.types.ts +0 -340
- package/modules/core/index.ts +0 -5
- package/modules/core/mail/applyPatchesOrMail.ts +0 -568
- package/modules/core/mail/mailClaimablePatchesBatch.ts +0 -381
- package/modules/game/game.models.ts +0 -53
- package/modules/game/game.router.ts +0 -110
- package/modules/game/game.schema.ts +0 -23
- package/modules/game/game.service.ts +0 -143
- package/modules/game/game.types.ts +0 -28
- package/modules/game/index.ts +0 -5
- package/modules/interface/index.ts +0 -5
- package/modules/interface/interface.canonicalize.ts +0 -279
- package/modules/interface/interface.models.ts +0 -40
- package/modules/interface/interface.router.ts +0 -175
- package/modules/interface/interface.schema.ts +0 -59
- package/modules/interface/interface.service.ts +0 -356
- package/modules/interface/interface.types.ts +0 -25
- package/modules/item/index.ts +0 -5
- package/modules/item/item.models.ts +0 -124
- package/modules/item/item.router.ts +0 -103
- package/modules/item/item.schema.ts +0 -120
- package/modules/item/item.service.ts +0 -167
- package/modules/item/item.types.ts +0 -74
- package/modules/job/index.ts +0 -5
- package/modules/job/job.models.ts +0 -14
- package/modules/job/job.router.ts +0 -44
- package/modules/job/job.schema.ts +0 -9
- package/modules/job/job.service.ts +0 -243
- package/modules/job/job.types.ts +0 -23
- package/modules/market/index.ts +0 -5
- package/modules/market/market.models.ts +0 -113
- package/modules/market/market.router.ts +0 -73
- package/modules/market/market.schema.ts +0 -140
- package/modules/market/market.service.ts +0 -122
- package/modules/market/market.types.ts +0 -56
- package/modules/product/index.ts +0 -5
- package/modules/product/product.models.ts +0 -166
- package/modules/product/product.router.ts +0 -93
- package/modules/product/product.schema.ts +0 -149
- package/modules/product/product.service.ts +0 -160
- package/modules/product/product.types.ts +0 -33
- package/modules/profile/index.ts +0 -5
- package/modules/profile/profile.models.ts +0 -214
- package/modules/profile/profile.router.ts +0 -72
- package/modules/profile/profile.schema.ts +0 -156
- package/modules/profile/profile.service.ts +0 -147
- package/modules/profile/profile.types.ts +0 -22
- package/modules/raffle/index.ts +0 -5
- package/modules/raffle/raffle.models.ts +0 -44
- package/modules/raffle/raffle.router.ts +0 -90
- package/modules/raffle/raffle.schema.ts +0 -32
- package/modules/raffle/raffle.service.ts +0 -167
- package/modules/raffle/raffle.types.ts +0 -30
- package/modules/skill/index.ts +0 -5
- package/modules/skill/skill.models.ts +0 -16
- package/modules/skill/skill.router.ts +0 -201
- package/modules/skill/skill.schema.ts +0 -40
- package/modules/skill/skill.service.ts +0 -390
- package/modules/skill/skill.types.ts +0 -33
- package/modules/video/index.ts +0 -5
- package/modules/video/video.models.ts +0 -25
- package/modules/video/video.router.ts +0 -143
- package/modules/video/video.schema.ts +0 -46
- package/modules/video/video.service.ts +0 -274
- package/modules/video/video.types.ts +0 -33
- package/util/db/index.ts +0 -7
- package/util/db/isPostgresError.ts +0 -9
- package/util/db/isUniqueConstraintViolation.ts +0 -3
- package/util/db.ts +0 -62
- package/util/index.ts +0 -351
- /package/{util/api.ts → api.ts} +0 -0
- /package/{util/array.ts → array.ts} +0 -0
- /package/{util/browser.ts → browser.ts} +0 -0
- /package/{util/codebase.ts → codebase.ts} +0 -0
- /package/{util/config.ts → config.ts} +0 -0
- /package/{util/decoder.test.ts → decoder.test.ts} +0 -0
- /package/{util/decoder.ts → decoder.ts} +0 -0
- /package/{util/format.ts → format.ts} +0 -0
- /package/{util/guid.ts → guid.ts} +0 -0
- /package/{util/json.ts → json.ts} +0 -0
- /package/{util/log.ts → log.ts} +0 -0
- /package/{util/math.ts → math.ts} +0 -0
- /package/{util/merkle.ts → merkle.ts} +0 -0
- /package/{util/number.ts → number.ts} +0 -0
- /package/{util/object.ts → object.ts} +0 -0
- /package/{util/otp.ts → otp.ts} +0 -0
- /package/{util/physics.ts → physics.ts} +0 -0
- /package/{util/process.ts → process.ts} +0 -0
- /package/{util/rpc.ts → rpc.ts} +0 -0
- /package/{util/seer.ts → seer.ts} +0 -0
- /package/{util/string.ts → string.ts} +0 -0
- /package/{util/text.ts → text.ts} +0 -0
- /package/{util/time → time}/date.ts +0 -0
- /package/{util/time → time}/fancyTimeFormat.ts +0 -0
- /package/{util/time → time}/index.ts +0 -0
- /package/{util/time → time}/now.ts +0 -0
- /package/{util/types → types}/mongo.d.ts +0 -0
- /package/{util/web3 → web3}/httpProvider.ts +0 -0
- /package/{util/web3.ts → web3.ts} +0 -0
- /package/{util/websocket.ts → websocket.ts} +0 -0
- /package/{util/zk.ts → zk.ts} +0 -0
- /package/{util/zod.ts → zod.ts} +0 -0
|
@@ -1,568 +0,0 @@
|
|
|
1
|
-
// node/modules/core/mail/applyPatchesOrMail.ts
|
|
2
|
-
import get from 'lodash/get';
|
|
3
|
-
import set from 'lodash/set';
|
|
4
|
-
import type { RouterContext } from '../core.types';
|
|
5
|
-
import type { PatchOp, EntityPatch } from '../../../types';
|
|
6
|
-
|
|
7
|
-
// Reuse your existing function or move it here
|
|
8
|
-
export function applyPatchToObject(obj: any, patch: PatchOp[]) {
|
|
9
|
-
for (const p of patch) {
|
|
10
|
-
if (p.op === 'set') {
|
|
11
|
-
set(obj, p.key, p.value);
|
|
12
|
-
} else if (p.op === 'unset') {
|
|
13
|
-
const parts = p.key.split('.');
|
|
14
|
-
const last = parts.pop();
|
|
15
|
-
const parent = parts.reduce((acc: any, k) => (acc ? acc[k] : undefined), obj);
|
|
16
|
-
if (parent && last) delete parent[last];
|
|
17
|
-
} else if (p.op === 'inc') {
|
|
18
|
-
const cur = Number(get(obj, p.key)) || 0;
|
|
19
|
-
set(obj, p.key, cur + Number(p.value || 0));
|
|
20
|
-
} else if (p.op === 'push') {
|
|
21
|
-
const cur = get(obj, p.key);
|
|
22
|
-
const arr = Array.isArray(cur) ? cur : [];
|
|
23
|
-
arr.push(p.value);
|
|
24
|
-
set(obj, p.key, arr);
|
|
25
|
-
} else if (p.op === 'merge') {
|
|
26
|
-
const cur = get(obj, p.key);
|
|
27
|
-
const base = cur && typeof cur === 'object' ? cur : {};
|
|
28
|
-
set(obj, p.key, { ...base, ...(p.value || {}) });
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ─────────────────────────────────────────────────────────────
|
|
34
|
-
// Inventory normalization hooks
|
|
35
|
-
// ─────────────────────────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
async function ensureItemByKey(ctx: RouterContext, itemKey: string, name?: string) {
|
|
38
|
-
const Item = (ctx.app as any)?.model?.Item;
|
|
39
|
-
if (!Item) return null;
|
|
40
|
-
|
|
41
|
-
const found = await Item.findOne?.({ key: itemKey })?.exec?.();
|
|
42
|
-
if (found) return found;
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
const res = await Item.findOneAndUpdate?.(
|
|
46
|
-
{ key: itemKey },
|
|
47
|
-
{
|
|
48
|
-
$setOnInsert: {
|
|
49
|
-
key: itemKey,
|
|
50
|
-
name: name || itemKey,
|
|
51
|
-
status: 'Active',
|
|
52
|
-
meta: { name: name || itemKey },
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
{ new: true, upsert: true }
|
|
56
|
-
)?.exec?.();
|
|
57
|
-
|
|
58
|
-
return res || (await Item.findOne?.({ key: itemKey })?.exec?.());
|
|
59
|
-
} catch {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Normalizes inventory patch ops:
|
|
66
|
-
* push inventory.0.items { itemKey, quantity? } -> repeated push { itemId, ... }
|
|
67
|
-
*/
|
|
68
|
-
export async function normalizeInventoryPatch(ctx: RouterContext, patch: PatchOp[]) {
|
|
69
|
-
const out: PatchOp[] = [];
|
|
70
|
-
|
|
71
|
-
for (const p of patch) {
|
|
72
|
-
if (p.op === 'push' && (p.key === 'inventory.0.items' || p.key.startsWith('inventory.0.items'))) {
|
|
73
|
-
const v = (p as any).value || {};
|
|
74
|
-
|
|
75
|
-
// already normalized
|
|
76
|
-
if (v.itemId) {
|
|
77
|
-
const q = Math.max(1, Number(v.quantity ?? 1));
|
|
78
|
-
for (let i = 0; i < q; i++) {
|
|
79
|
-
out.push({
|
|
80
|
-
op: 'push',
|
|
81
|
-
key: p.key,
|
|
82
|
-
value: { itemId: v.itemId, x: v.x ?? 1, y: v.y ?? 1, meta: v.meta ?? undefined },
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (v.itemKey) {
|
|
89
|
-
const itemKey = String(v.itemKey);
|
|
90
|
-
const item = await ensureItemByKey(ctx, itemKey, itemKey);
|
|
91
|
-
const q = Math.max(1, Number(v.quantity ?? 1));
|
|
92
|
-
|
|
93
|
-
if (item?._id) {
|
|
94
|
-
for (let i = 0; i < q; i++) {
|
|
95
|
-
out.push({
|
|
96
|
-
op: 'push',
|
|
97
|
-
key: p.key,
|
|
98
|
-
value: { itemId: item._id, x: v.x ?? 1, y: v.y ?? 1, meta: v.meta ?? undefined },
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
} else {
|
|
102
|
-
// fallback keep itemKey
|
|
103
|
-
for (let i = 0; i < q; i++) {
|
|
104
|
-
out.push({
|
|
105
|
-
op: 'push',
|
|
106
|
-
key: p.key,
|
|
107
|
-
value: { itemKey, x: v.x ?? 1, y: v.y ?? 1, meta: v.meta ?? undefined },
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
out.push(p);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return out;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ─────────────────────────────────────────────────────────────
|
|
123
|
-
// Generic sync emission (NO wrappers)
|
|
124
|
-
// ─────────────────────────────────────────────────────────────
|
|
125
|
-
|
|
126
|
-
type InventorySyncOp =
|
|
127
|
-
| { op: 'add'; itemKey: string; quantity?: number }
|
|
128
|
-
| { op: 'remove'; itemKey: string; quantity?: number };
|
|
129
|
-
|
|
130
|
-
function inventoryOpsFromPatchOps(patchOps: any[]): InventorySyncOp[] {
|
|
131
|
-
const ops: InventorySyncOp[] = [];
|
|
132
|
-
const list = Array.isArray(patchOps) ? patchOps : [];
|
|
133
|
-
|
|
134
|
-
for (const op of list) {
|
|
135
|
-
const key = String(op?.key || '');
|
|
136
|
-
const isInvItems =
|
|
137
|
-
key === 'inventory.0.items' ||
|
|
138
|
-
key.startsWith('inventory.0.items') ||
|
|
139
|
-
// allow future bags without rewriting
|
|
140
|
-
key.includes('.items');
|
|
141
|
-
|
|
142
|
-
if (!isInvItems) continue;
|
|
143
|
-
|
|
144
|
-
if (op?.op === 'push') {
|
|
145
|
-
const v = op?.value || {};
|
|
146
|
-
const itemKey = v?.itemKey ?? v?.itemId;
|
|
147
|
-
if (!itemKey) continue;
|
|
148
|
-
const qty = Number(v?.quantity ?? 1);
|
|
149
|
-
ops.push({ op: 'add', itemKey: String(itemKey), quantity: Number.isFinite(qty) ? qty : 1 });
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (op?.op === 'pull') {
|
|
154
|
-
const v = op?.value || {};
|
|
155
|
-
const itemKey = v?.itemKey ?? v?.itemId;
|
|
156
|
-
if (!itemKey) continue;
|
|
157
|
-
const qty = Number(v?.quantity ?? 1);
|
|
158
|
-
ops.push({ op: 'remove', itemKey: String(itemKey), quantity: Number.isFinite(qty) ? qty : 1 });
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return ops;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function inventoryOpsFromEntityPatches(
|
|
167
|
-
patches: EntityPatch[] | undefined
|
|
168
|
-
): Array<{ characterId: string; ops: InventorySyncOp[] }> {
|
|
169
|
-
const out: Array<{ characterId: string; ops: InventorySyncOp[] }> = [];
|
|
170
|
-
for (const ep of patches || []) {
|
|
171
|
-
if (ep?.entityType !== 'character.inventory') continue;
|
|
172
|
-
const characterId = String(ep?.entityId || '');
|
|
173
|
-
if (!characterId) continue;
|
|
174
|
-
|
|
175
|
-
const ops = inventoryOpsFromPatchOps((ep as any)?.ops || []);
|
|
176
|
-
if (ops.length) out.push({ characterId, ops });
|
|
177
|
-
}
|
|
178
|
-
return out;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async function emitSyncPatch(ctx: RouterContext, input: { target: string; patch: any; reason?: string }) {
|
|
182
|
-
try {
|
|
183
|
-
await (ctx.client as any)?.emit?.sync?.mutate?.({
|
|
184
|
-
kind: 'patch',
|
|
185
|
-
target: input.target,
|
|
186
|
-
patch: input.patch,
|
|
187
|
-
reason: input.reason,
|
|
188
|
-
});
|
|
189
|
-
} catch (e) {
|
|
190
|
-
// never crash gameplay because push failed
|
|
191
|
-
console.warn('[applyPatchesOrMail] emit.sync failed', e);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ─────────────────────────────────────────────────────────────
|
|
196
|
-
// Mail + Claim abstraction
|
|
197
|
-
// ─────────────────────────────────────────────────────────────
|
|
198
|
-
|
|
199
|
-
type MailKind = 'mail' | 'system' | 'support' | 'dm' | 'group';
|
|
200
|
-
|
|
201
|
-
export type MailReward = { type: 'item' | 'token' | 'reward'; id: string; quantity?: number; meta?: any };
|
|
202
|
-
export type MailEffect = {
|
|
203
|
-
type: 'stat' | 'flag' | 'buff' | 'debuff' | 'effect';
|
|
204
|
-
key?: string;
|
|
205
|
-
delta?: number;
|
|
206
|
-
value?: any;
|
|
207
|
-
label?: string;
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
export type MailPatchMessagePayload = {
|
|
211
|
-
kind: 'patch-grant';
|
|
212
|
-
source: string;
|
|
213
|
-
title?: string;
|
|
214
|
-
body?: string;
|
|
215
|
-
|
|
216
|
-
patches: EntityPatch[];
|
|
217
|
-
|
|
218
|
-
ui?: {
|
|
219
|
-
rewards?: Array<MailReward>;
|
|
220
|
-
effects?: Array<MailEffect>;
|
|
221
|
-
};
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
export type ApplyPatchesResult = {
|
|
225
|
-
appliedNow: EntityPatch[];
|
|
226
|
-
mailed: {
|
|
227
|
-
conversationId: string;
|
|
228
|
-
messageId: string;
|
|
229
|
-
} | null;
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
export async function ensureMailConversation(params: {
|
|
233
|
-
ctx: RouterContext;
|
|
234
|
-
profileId: string;
|
|
235
|
-
kind?: MailKind;
|
|
236
|
-
conversationKey: string;
|
|
237
|
-
title?: string;
|
|
238
|
-
category?: string;
|
|
239
|
-
importance?: number;
|
|
240
|
-
}) {
|
|
241
|
-
const { ctx, profileId, kind = 'mail', conversationKey, title, category, importance } = params;
|
|
242
|
-
|
|
243
|
-
const mongoose = (ctx.app as any).db?.mongoose ?? (ctx.app as any).mongoose;
|
|
244
|
-
const Conversation = (ctx.app as any).model.Conversation;
|
|
245
|
-
|
|
246
|
-
const profileObjId = (() => {
|
|
247
|
-
try {
|
|
248
|
-
return new mongoose.Types.ObjectId(profileId);
|
|
249
|
-
} catch {
|
|
250
|
-
return profileId;
|
|
251
|
-
}
|
|
252
|
-
})();
|
|
253
|
-
|
|
254
|
-
let convo =
|
|
255
|
-
(await Conversation.findOne?.({
|
|
256
|
-
kind,
|
|
257
|
-
key: conversationKey,
|
|
258
|
-
$or: [{ profileId: profileObjId }, { 'participants.profileId': profileObjId }],
|
|
259
|
-
status: { $ne: 'Archived' },
|
|
260
|
-
})?.exec?.()) ?? null;
|
|
261
|
-
|
|
262
|
-
if (convo) return convo;
|
|
263
|
-
|
|
264
|
-
const updated =
|
|
265
|
-
(await Conversation.findOneAndUpdate?.(
|
|
266
|
-
{
|
|
267
|
-
kind,
|
|
268
|
-
key: conversationKey,
|
|
269
|
-
profileId: profileObjId,
|
|
270
|
-
status: { $ne: 'Archived' },
|
|
271
|
-
},
|
|
272
|
-
{
|
|
273
|
-
$setOnInsert: {
|
|
274
|
-
profileId: profileObjId,
|
|
275
|
-
kind,
|
|
276
|
-
key: conversationKey,
|
|
277
|
-
isLocked: true,
|
|
278
|
-
allowUserSend: false,
|
|
279
|
-
participants: [{ profileId: profileObjId, role: 'user', unreadCount: 0, lastReadDate: new Date(0) }],
|
|
280
|
-
name: title ?? 'System',
|
|
281
|
-
category: category ?? 'system',
|
|
282
|
-
importance: Number(importance ?? 0),
|
|
283
|
-
lastMessageDate: null,
|
|
284
|
-
lastMessagePreview: '',
|
|
285
|
-
messageCount: 0,
|
|
286
|
-
messages: [],
|
|
287
|
-
status: 'Active',
|
|
288
|
-
},
|
|
289
|
-
},
|
|
290
|
-
{ new: true, upsert: true }
|
|
291
|
-
)?.exec?.()) ?? null;
|
|
292
|
-
|
|
293
|
-
if (updated) return updated;
|
|
294
|
-
|
|
295
|
-
return await Conversation.findOne?.({ kind, conversationKey, profileId: profileObjId })?.exec?.();
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function splitClaimable(patches: EntityPatch[]) {
|
|
299
|
-
const claimable: EntityPatch[] = [];
|
|
300
|
-
const immediate: EntityPatch[] = [];
|
|
301
|
-
|
|
302
|
-
for (const p of patches || []) {
|
|
303
|
-
if (!p?.entityType || !Array.isArray(p.ops)) continue;
|
|
304
|
-
if ((p as any).claimable) claimable.push(p);
|
|
305
|
-
else immediate.push(p);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return { claimable, immediate };
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* ✅ Applies NON-claimable patches immediately.
|
|
313
|
-
* ✅ Mails ALL claimable patches.
|
|
314
|
-
* ✅ Emits generic sync.patch for any immediate inventory change.
|
|
315
|
-
*/
|
|
316
|
-
export async function applyPatchesWithInventoryViaMail(params: {
|
|
317
|
-
ctx: RouterContext;
|
|
318
|
-
profile: any;
|
|
319
|
-
character?: any;
|
|
320
|
-
patches: EntityPatch[];
|
|
321
|
-
mail: {
|
|
322
|
-
profileId: string;
|
|
323
|
-
kind?: MailKind;
|
|
324
|
-
conversationKey: string;
|
|
325
|
-
source: string;
|
|
326
|
-
title?: string;
|
|
327
|
-
body?: string;
|
|
328
|
-
category?: string;
|
|
329
|
-
importance?: number;
|
|
330
|
-
ui?: MailPatchMessagePayload['ui'];
|
|
331
|
-
dedupeKey?: string;
|
|
332
|
-
};
|
|
333
|
-
}): Promise<ApplyPatchesResult> {
|
|
334
|
-
const { ctx, profile, character, patches, mail } = params;
|
|
335
|
-
|
|
336
|
-
const { claimable, immediate } = splitClaimable(patches);
|
|
337
|
-
|
|
338
|
-
// Track inventory changes applied immediately
|
|
339
|
-
const immediateInventorySync = inventoryOpsFromEntityPatches(immediate);
|
|
340
|
-
|
|
341
|
-
// 1) Apply immediate patches now
|
|
342
|
-
for (const patch of immediate) {
|
|
343
|
-
if (patch.entityType === 'profile.meta') {
|
|
344
|
-
if (!profile.meta) profile.meta = {};
|
|
345
|
-
applyPatchToObject(profile.meta, patch.ops);
|
|
346
|
-
profile.markModified?.('meta');
|
|
347
|
-
} else if (patch.entityType === 'character.data') {
|
|
348
|
-
if (!character) continue;
|
|
349
|
-
if (!character.data) character.data = {};
|
|
350
|
-
applyPatchToObject(character.data, patch.ops);
|
|
351
|
-
character.markModified?.('data');
|
|
352
|
-
} else if (patch.entityType === 'character.inventory') {
|
|
353
|
-
if (!character) continue;
|
|
354
|
-
if (!Array.isArray(character.inventory)) character.inventory = [];
|
|
355
|
-
if (!character.inventory[0]) character.inventory[0] = { items: [] };
|
|
356
|
-
|
|
357
|
-
const normalized = await normalizeInventoryPatch(ctx, patch.ops);
|
|
358
|
-
applyPatchToObject(character, normalized);
|
|
359
|
-
character.markModified?.('inventory');
|
|
360
|
-
} else {
|
|
361
|
-
throw new Error(`applyPatchesWithInventoryViaMail: unsupported entityType=${patch.entityType}`);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// persist immediate effects
|
|
366
|
-
await profile.save?.();
|
|
367
|
-
if (character && immediate.some((p) => p.entityType?.startsWith('character.'))) {
|
|
368
|
-
await character.save?.();
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// ✅ emit sync for immediate inventory changes
|
|
372
|
-
for (const hit of immediateInventorySync) {
|
|
373
|
-
await emitSyncPatch(ctx, {
|
|
374
|
-
target: 'character.inventory',
|
|
375
|
-
patch: {
|
|
376
|
-
characterId: hit.characterId,
|
|
377
|
-
ops: hit.ops, // already reduced, generic
|
|
378
|
-
mode: 'patch',
|
|
379
|
-
source: mail.source,
|
|
380
|
-
reason: 'immediate',
|
|
381
|
-
},
|
|
382
|
-
reason: 'immediate',
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// 2) If no claimable patches, done
|
|
387
|
-
if (!claimable.length) return { appliedNow: immediate, mailed: null };
|
|
388
|
-
|
|
389
|
-
// 3) Find/create inbox + create message with claim payload (deduped)
|
|
390
|
-
const convo = await ensureMailConversation({
|
|
391
|
-
ctx,
|
|
392
|
-
profileId: mail.profileId,
|
|
393
|
-
kind: mail.kind ?? 'mail',
|
|
394
|
-
conversationKey: mail.conversationKey,
|
|
395
|
-
title: mail.title ?? 'System',
|
|
396
|
-
category: mail.category ?? 'system',
|
|
397
|
-
importance: mail.importance ?? 0,
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
const ConversationMessage = (ctx.app as any).model.ConversationMessage;
|
|
401
|
-
const Conversation = (ctx.app as any).model.Conversation;
|
|
402
|
-
|
|
403
|
-
const dedupeKey = mail.dedupeKey ?? null;
|
|
404
|
-
|
|
405
|
-
if (dedupeKey) {
|
|
406
|
-
const existing = await ConversationMessage.findOne?.({
|
|
407
|
-
conversationId: convo._id,
|
|
408
|
-
'claim.dedupeKey': dedupeKey,
|
|
409
|
-
})?.exec?.();
|
|
410
|
-
|
|
411
|
-
if (existing) {
|
|
412
|
-
return {
|
|
413
|
-
appliedNow: immediate,
|
|
414
|
-
mailed: { conversationId: String(convo._id), messageId: String(existing._id) },
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
const payload: MailPatchMessagePayload = {
|
|
420
|
-
kind: 'patch-grant',
|
|
421
|
-
source: mail.source,
|
|
422
|
-
title: mail.title,
|
|
423
|
-
body: mail.body,
|
|
424
|
-
patches: claimable,
|
|
425
|
-
ui: mail.ui,
|
|
426
|
-
};
|
|
427
|
-
|
|
428
|
-
const msg = await ConversationMessage.create?.({
|
|
429
|
-
conversationId: convo._id,
|
|
430
|
-
role: 'system',
|
|
431
|
-
type: 'reward',
|
|
432
|
-
content: mail.body ?? '',
|
|
433
|
-
payload,
|
|
434
|
-
claim: {
|
|
435
|
-
isClaimable: true,
|
|
436
|
-
claimedDate: null,
|
|
437
|
-
claimedByProfileId: null,
|
|
438
|
-
dedupeKey,
|
|
439
|
-
attachments: [],
|
|
440
|
-
},
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
try {
|
|
444
|
-
const preview = (mail.title ? `${mail.title} — ` : '') + (mail.body ?? '');
|
|
445
|
-
await Conversation.updateOne?.(
|
|
446
|
-
{ _id: convo._id },
|
|
447
|
-
{
|
|
448
|
-
$set: {
|
|
449
|
-
lastMessageDate: new Date(),
|
|
450
|
-
lastMessagePreview: String(preview).slice(0, 140),
|
|
451
|
-
},
|
|
452
|
-
$inc: { messageCount: 1 },
|
|
453
|
-
$push: { messages: msg._id },
|
|
454
|
-
}
|
|
455
|
-
)?.exec?.();
|
|
456
|
-
} catch {
|
|
457
|
-
// ignore
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
return { appliedNow: immediate, mailed: { conversationId: String(convo._id), messageId: String(msg._id) } };
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* ✅ Central claim handler
|
|
465
|
-
* ✅ Emits generic sync.patch for any claimed inventory changes.
|
|
466
|
-
*/
|
|
467
|
-
export async function claimMailMessage(params: {
|
|
468
|
-
ctx: RouterContext;
|
|
469
|
-
profile: any;
|
|
470
|
-
character?: any;
|
|
471
|
-
messageId: string;
|
|
472
|
-
}): Promise<{ ok: true }> {
|
|
473
|
-
const { ctx, profile, character, messageId } = params;
|
|
474
|
-
|
|
475
|
-
const ConversationMessage = (ctx.app as any).model.ConversationMessage;
|
|
476
|
-
|
|
477
|
-
const msg = await ConversationMessage.findOneAndUpdate?.(
|
|
478
|
-
{
|
|
479
|
-
_id: messageId,
|
|
480
|
-
'claim.isClaimable': true,
|
|
481
|
-
'claim.claimedDate': null,
|
|
482
|
-
'claim.claimedByProfileId': null,
|
|
483
|
-
},
|
|
484
|
-
{
|
|
485
|
-
$set: {
|
|
486
|
-
'claim.claimedDate': new Date(),
|
|
487
|
-
'claim.claimedByProfileId': String(profile._id),
|
|
488
|
-
},
|
|
489
|
-
},
|
|
490
|
-
{ new: true }
|
|
491
|
-
)?.exec?.();
|
|
492
|
-
|
|
493
|
-
if (!msg) {
|
|
494
|
-
const err: any = new Error('Already claimed or not claimable');
|
|
495
|
-
err.code = 'ALREADY_CLAIMED';
|
|
496
|
-
throw err;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
const payload = msg.payload as MailPatchMessagePayload | undefined;
|
|
500
|
-
if (!payload || payload.kind !== 'patch-grant' || !Array.isArray(payload.patches)) return { ok: true };
|
|
501
|
-
|
|
502
|
-
// collect inventory sync intents BEFORE normalization mutates anything
|
|
503
|
-
const claimedInventorySync = inventoryOpsFromEntityPatches(payload.patches);
|
|
504
|
-
|
|
505
|
-
let touchedProfileMeta = false;
|
|
506
|
-
let touchedCharacterData = false;
|
|
507
|
-
let touchedCharacterInventory = false;
|
|
508
|
-
|
|
509
|
-
for (const patch of payload.patches) {
|
|
510
|
-
if (!patch?.entityType || !Array.isArray(patch.ops)) continue;
|
|
511
|
-
|
|
512
|
-
if (patch.entityType === 'profile.meta') {
|
|
513
|
-
if (!profile.meta) profile.meta = {};
|
|
514
|
-
applyPatchToObject(profile.meta, patch.ops);
|
|
515
|
-
touchedProfileMeta = true;
|
|
516
|
-
continue;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
if (patch.entityType === 'character.data') {
|
|
520
|
-
if (!character) throw new Error('No character loaded for character.data claim');
|
|
521
|
-
if (!character.data) character.data = {};
|
|
522
|
-
applyPatchToObject(character.data, patch.ops);
|
|
523
|
-
touchedCharacterData = true;
|
|
524
|
-
continue;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
if (patch.entityType === 'character.inventory') {
|
|
528
|
-
if (!character) throw new Error('No character loaded for inventory claim');
|
|
529
|
-
|
|
530
|
-
if (!Array.isArray(character.inventory)) character.inventory = [];
|
|
531
|
-
if (!character.inventory[0]) character.inventory[0] = { items: [] };
|
|
532
|
-
|
|
533
|
-
const normalized = await normalizeInventoryPatch(ctx, patch.ops);
|
|
534
|
-
applyPatchToObject(character, normalized);
|
|
535
|
-
touchedCharacterInventory = true;
|
|
536
|
-
continue;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
throw new Error(`claimMailMessage: unsupported entityType=${patch.entityType}`);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
if (touchedProfileMeta) profile.markModified?.('meta');
|
|
543
|
-
if (touchedCharacterData) character?.markModified?.('data');
|
|
544
|
-
if (touchedCharacterInventory) character?.markModified?.('inventory');
|
|
545
|
-
|
|
546
|
-
await profile.save?.();
|
|
547
|
-
if (character && (touchedCharacterData || touchedCharacterInventory)) {
|
|
548
|
-
await character.save?.();
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// ✅ emit sync for claimed inventory changes
|
|
552
|
-
for (const hit of claimedInventorySync) {
|
|
553
|
-
await ctx.client.emit.sync.mutate({
|
|
554
|
-
kind: 'patch',
|
|
555
|
-
target: 'character.inventory',
|
|
556
|
-
patch: {
|
|
557
|
-
characterId: hit.characterId,
|
|
558
|
-
ops: hit.ops,
|
|
559
|
-
mode: 'patch',
|
|
560
|
-
source: payload.source || 'mail.claim',
|
|
561
|
-
reason: 'claim',
|
|
562
|
-
},
|
|
563
|
-
reason: 'claim',
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
return { ok: true };
|
|
568
|
-
}
|