@jgengine/convex 0.8.0 → 0.10.0
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/CHANGELOG.md +36 -0
- package/dist/createConvexGameTransport.d.ts +3 -2
- package/dist/createConvexGameTransport.js +1 -1
- package/dist/hostedServer.d.ts +96 -0
- package/dist/hostedServer.js +350 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/occ.d.ts +24 -0
- package/dist/occ.js +16 -0
- package/dist/server.d.ts +55 -9
- package/dist/server.js +141 -18
- package/llms.txt +202 -654
- package/package.json +6 -2
package/dist/server.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { DataModelFromSchemaDefinition, GenericMutationCtx } from "convex/server";
|
|
1
|
+
import type { Auth, DataModelFromSchemaDefinition, GenericMutationCtx } from "convex/server";
|
|
2
2
|
import type { GenericId } from "convex/values";
|
|
3
3
|
import type { ChatMessage } from "@jgengine/core/game/chat";
|
|
4
4
|
import type { ChatSendOutcome } from "@jgengine/core/multiplayer/chatContract";
|
|
5
|
+
import { type PoseSyncRules } from "@jgengine/core/multiplayer/presenceModel";
|
|
5
6
|
import type { GameRuntime } from "@jgengine/core/runtime/gameRuntime";
|
|
6
7
|
import type { LeaderboardIncrement } from "@jgengine/core/runtime/hostPersistence";
|
|
7
8
|
export declare function jgengineTables(): {
|
|
@@ -9,6 +10,7 @@ export declare function jgengineTables(): {
|
|
|
9
10
|
createdAt: number;
|
|
10
11
|
dirtyAt?: number | undefined;
|
|
11
12
|
gameId: string;
|
|
13
|
+
joinCode?: string | undefined;
|
|
12
14
|
lastSavedAt?: number | undefined;
|
|
13
15
|
memberUserIds: string[];
|
|
14
16
|
mode?: string | undefined;
|
|
@@ -24,11 +26,14 @@ export declare function jgengineTables(): {
|
|
|
24
26
|
status: "closed" | "open" | "running";
|
|
25
27
|
tickAnchorMs: number;
|
|
26
28
|
updatedAt: number;
|
|
29
|
+
visibility?: "private" | "public" | undefined;
|
|
27
30
|
}, {
|
|
28
31
|
gameId: import("convex/values").VString<string, "required">;
|
|
29
32
|
status: import("convex/values").VUnion<"closed" | "open" | "running", [import("convex/values").VLiteral<"open", "required">, import("convex/values").VLiteral<"running", "required">, import("convex/values").VLiteral<"closed", "required">], "required", never>;
|
|
30
33
|
mode: import("convex/values").VString<string | undefined, "optional">;
|
|
31
34
|
modeConfig: import("convex/values").VAny<any, "optional", string>;
|
|
35
|
+
visibility: import("convex/values").VUnion<"private" | "public" | undefined, [import("convex/values").VLiteral<"public", "required">, import("convex/values").VLiteral<"private", "required">], "optional", never>;
|
|
36
|
+
joinCode: import("convex/values").VString<string | undefined, "optional">;
|
|
32
37
|
memberUserIds: import("convex/values").VArray<string[], import("convex/values").VString<string, "required">, "required">;
|
|
33
38
|
slotsPerServer: import("convex/values").VFloat64<number, "required">;
|
|
34
39
|
save: import("convex/values").VUnion<"none" | {
|
|
@@ -49,7 +54,7 @@ export declare function jgengineTables(): {
|
|
|
49
54
|
dirtyAt: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
50
55
|
createdAt: import("convex/values").VFloat64<number, "required">;
|
|
51
56
|
updatedAt: import("convex/values").VFloat64<number, "required">;
|
|
52
|
-
}, "required", "createdAt" | "dirtyAt" | "gameId" | "lastSavedAt" | "memberUserIds" | "mode" | "modeConfig" | "revision" | "save" | "save.auto" | "save.scope" | "serverState" | "sessionPlayers" | "slotsPerServer" | "status" | "tickAnchorMs" | "updatedAt" | `modeConfig.${string}` | `serverState.${string}` | `sessionPlayers.${string}`>, {
|
|
57
|
+
}, "required", "createdAt" | "dirtyAt" | "gameId" | "joinCode" | "lastSavedAt" | "memberUserIds" | "mode" | "modeConfig" | "revision" | "save" | "save.auto" | "save.scope" | "serverState" | "sessionPlayers" | "slotsPerServer" | "status" | "tickAnchorMs" | "updatedAt" | "visibility" | `modeConfig.${string}` | `serverState.${string}` | `sessionPlayers.${string}`>, {
|
|
53
58
|
by_game_and_status: ["gameId", "status", "_creationTime"];
|
|
54
59
|
}, {}, {}>;
|
|
55
60
|
jgPlayerProfiles: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
@@ -158,11 +163,42 @@ export declare function jgengineTables(): {
|
|
|
158
163
|
by_server_channel: ["serverId", "channelId", "_creationTime"];
|
|
159
164
|
}, {}, {}>;
|
|
160
165
|
};
|
|
166
|
+
/**
|
|
167
|
+
* Convex table for the hosted GameContext-world path: one row per (gameId, serverId) holding the full
|
|
168
|
+
* `WorldSnapshot` blob, its revision, the member roster, and each member's held input. A sibling of
|
|
169
|
+
* {@link jgengineTables} — spread either (or both) into `defineSchema`.
|
|
170
|
+
*/
|
|
171
|
+
export declare function jgengineHostedTables(): {
|
|
172
|
+
jgHostedWorlds: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
173
|
+
createdAt: number;
|
|
174
|
+
gameId: string;
|
|
175
|
+
inputs: any;
|
|
176
|
+
memberUserIds: string[];
|
|
177
|
+
revision: number;
|
|
178
|
+
serverId: string;
|
|
179
|
+
snapshot: any;
|
|
180
|
+
tickAnchorMs: number;
|
|
181
|
+
updatedAt: number;
|
|
182
|
+
}, {
|
|
183
|
+
gameId: import("convex/values").VString<string, "required">;
|
|
184
|
+
serverId: import("convex/values").VString<string, "required">;
|
|
185
|
+
snapshot: import("convex/values").VAny<any, "required", string>;
|
|
186
|
+
revision: import("convex/values").VFloat64<number, "required">;
|
|
187
|
+
memberUserIds: import("convex/values").VArray<string[], import("convex/values").VString<string, "required">, "required">;
|
|
188
|
+
inputs: import("convex/values").VAny<any, "required", string>;
|
|
189
|
+
tickAnchorMs: import("convex/values").VFloat64<number, "required">;
|
|
190
|
+
createdAt: import("convex/values").VFloat64<number, "required">;
|
|
191
|
+
updatedAt: import("convex/values").VFloat64<number, "required">;
|
|
192
|
+
}, "required", "createdAt" | "gameId" | "inputs" | "memberUserIds" | "revision" | "serverId" | "snapshot" | "tickAnchorMs" | "updatedAt" | `inputs.${string}` | `snapshot.${string}`>, {
|
|
193
|
+
by_game_and_server: ["gameId", "serverId", "_creationTime"];
|
|
194
|
+
}, {}, {}>;
|
|
195
|
+
};
|
|
161
196
|
declare const schemaForTypes: import("convex/server").SchemaDefinition<{
|
|
162
197
|
jgGameServers: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
163
198
|
createdAt: number;
|
|
164
199
|
dirtyAt?: number | undefined;
|
|
165
200
|
gameId: string;
|
|
201
|
+
joinCode?: string | undefined;
|
|
166
202
|
lastSavedAt?: number | undefined;
|
|
167
203
|
memberUserIds: string[];
|
|
168
204
|
mode?: string | undefined;
|
|
@@ -178,11 +214,14 @@ declare const schemaForTypes: import("convex/server").SchemaDefinition<{
|
|
|
178
214
|
status: "closed" | "open" | "running";
|
|
179
215
|
tickAnchorMs: number;
|
|
180
216
|
updatedAt: number;
|
|
217
|
+
visibility?: "private" | "public" | undefined;
|
|
181
218
|
}, {
|
|
182
219
|
gameId: import("convex/values").VString<string, "required">;
|
|
183
220
|
status: import("convex/values").VUnion<"closed" | "open" | "running", [import("convex/values").VLiteral<"open", "required">, import("convex/values").VLiteral<"running", "required">, import("convex/values").VLiteral<"closed", "required">], "required", never>;
|
|
184
221
|
mode: import("convex/values").VString<string | undefined, "optional">;
|
|
185
222
|
modeConfig: import("convex/values").VAny<any, "optional", string>;
|
|
223
|
+
visibility: import("convex/values").VUnion<"private" | "public" | undefined, [import("convex/values").VLiteral<"public", "required">, import("convex/values").VLiteral<"private", "required">], "optional", never>;
|
|
224
|
+
joinCode: import("convex/values").VString<string | undefined, "optional">;
|
|
186
225
|
memberUserIds: import("convex/values").VArray<string[], import("convex/values").VString<string, "required">, "required">;
|
|
187
226
|
slotsPerServer: import("convex/values").VFloat64<number, "required">;
|
|
188
227
|
save: import("convex/values").VUnion<"none" | {
|
|
@@ -203,7 +242,7 @@ declare const schemaForTypes: import("convex/server").SchemaDefinition<{
|
|
|
203
242
|
dirtyAt: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
204
243
|
createdAt: import("convex/values").VFloat64<number, "required">;
|
|
205
244
|
updatedAt: import("convex/values").VFloat64<number, "required">;
|
|
206
|
-
}, "required", "createdAt" | "dirtyAt" | "gameId" | "lastSavedAt" | "memberUserIds" | "mode" | "modeConfig" | "revision" | "save" | "save.auto" | "save.scope" | "serverState" | "sessionPlayers" | "slotsPerServer" | "status" | "tickAnchorMs" | "updatedAt" | `modeConfig.${string}` | `serverState.${string}` | `sessionPlayers.${string}`>, {
|
|
245
|
+
}, "required", "createdAt" | "dirtyAt" | "gameId" | "joinCode" | "lastSavedAt" | "memberUserIds" | "mode" | "modeConfig" | "revision" | "save" | "save.auto" | "save.scope" | "serverState" | "sessionPlayers" | "slotsPerServer" | "status" | "tickAnchorMs" | "updatedAt" | "visibility" | `modeConfig.${string}` | `serverState.${string}` | `sessionPlayers.${string}`>, {
|
|
207
246
|
by_game_and_status: ["gameId", "status", "_creationTime"];
|
|
208
247
|
}, {}, {}>;
|
|
209
248
|
jgPlayerProfiles: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
@@ -315,23 +354,27 @@ declare const schemaForTypes: import("convex/server").SchemaDefinition<{
|
|
|
315
354
|
type JGDataModel = DataModelFromSchemaDefinition<typeof schemaForTypes>;
|
|
316
355
|
type JGMutationCtx = GenericMutationCtx<JGDataModel>;
|
|
317
356
|
export type JgAuthMode = "anonymous" | "required";
|
|
357
|
+
export declare const DEFAULT_CONVEX_POSE_RULES: PoseSyncRules;
|
|
358
|
+
/** Resolve the acting user id under a {@link JgAuthMode}: the Convex identity when signed in (rejecting a mismatched claim), the claimed external id under `"anonymous"`, else `null`. */
|
|
359
|
+
export declare function resolveActor(ctx: {
|
|
360
|
+
auth: Auth;
|
|
361
|
+
}, claimedExternalId: string | undefined, mode: JgAuthMode): Promise<string | null>;
|
|
318
362
|
export declare function applyLeaderboardIncrements(ctx: JGMutationCtx, gameId: string, entries: LeaderboardIncrement[]): Promise<void>;
|
|
319
363
|
export declare const JG_RUNTIME_TICK_MS = 1000;
|
|
320
364
|
export declare function createGameServerFunctions(options?: {
|
|
321
365
|
runtimes?: GameRuntime[];
|
|
322
366
|
auth?: JgAuthMode;
|
|
367
|
+
slotsPerServer?: number;
|
|
368
|
+
allowedFeedActions?: readonly string[];
|
|
323
369
|
}): {
|
|
324
370
|
joinServer: import("convex/server").RegisteredMutation<"public", {
|
|
325
371
|
externalId?: string | undefined;
|
|
326
372
|
gameId: string;
|
|
373
|
+
joinCode?: string | undefined;
|
|
327
374
|
mode?: string | undefined;
|
|
328
375
|
modeConfig?: any;
|
|
329
|
-
save?: "none" | {
|
|
330
|
-
auto: string;
|
|
331
|
-
scope: "chunks" | "player" | "player+chunks";
|
|
332
|
-
} | undefined;
|
|
333
376
|
serverId?: GenericId<"jgGameServers"> | undefined;
|
|
334
|
-
|
|
377
|
+
visibility?: "private" | "public" | undefined;
|
|
335
378
|
}, Promise<{
|
|
336
379
|
serverId: GenericId<"jgGameServers">;
|
|
337
380
|
isNew: boolean;
|
|
@@ -371,7 +414,9 @@ export declare function createGameServerFunctions(options?: {
|
|
|
371
414
|
};
|
|
372
415
|
revision: number;
|
|
373
416
|
serverState: any;
|
|
374
|
-
sessionPlayers:
|
|
417
|
+
sessionPlayers: {
|
|
418
|
+
[x: string]: {} | null;
|
|
419
|
+
};
|
|
375
420
|
updatedAt: number;
|
|
376
421
|
} | null>>;
|
|
377
422
|
getPlayerProfile: import("convex/server").RegisteredQuery<"public", {
|
|
@@ -448,6 +493,7 @@ export declare function createLeaderboardFunctions(options?: {
|
|
|
448
493
|
export declare function createPresenceFunctions(options?: {
|
|
449
494
|
auth?: JgAuthMode;
|
|
450
495
|
freshWindowMs?: number;
|
|
496
|
+
poseRules?: PoseSyncRules;
|
|
451
497
|
}): {
|
|
452
498
|
list: import("convex/server").RegisteredQuery<"public", {
|
|
453
499
|
externalId?: string | undefined;
|
package/dist/server.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { defineSchema, defineTable, internalMutationGeneric, mutationGeneric, queryGeneric, } from "convex/server";
|
|
2
2
|
import { ConvexError, v } from "convex/values";
|
|
3
|
+
import { createFeedWriteGate, validateFeedWrite, } from "@jgengine/core/multiplayer/feedWriteGate";
|
|
4
|
+
import { decidePoseSync, spawnPresenceState, } from "@jgengine/core/multiplayer/presenceModel";
|
|
3
5
|
import { createGameRuntime } from "@jgengine/core/runtime/gameRuntime";
|
|
4
6
|
import { buildHydratePlayers, planServerPersist, shouldAutoSave, trimFeedEntries } from "@jgengine/core/runtime/hostPersistence";
|
|
5
7
|
import { clearDirtyFlags, createEmptyServerRow } from "@jgengine/core/runtime/snapshot";
|
|
8
|
+
import { applyCommandWithOcc, commitIfRevisionMatch } from "./occ.js";
|
|
6
9
|
const saveConfigValidator = v.union(v.literal("none"), v.object({
|
|
7
10
|
auto: v.string(),
|
|
8
11
|
scope: v.union(v.literal("player"), v.literal("chunks"), v.literal("player+chunks")),
|
|
@@ -14,6 +17,8 @@ export function jgengineTables() {
|
|
|
14
17
|
status: v.union(v.literal("open"), v.literal("running"), v.literal("closed")),
|
|
15
18
|
mode: v.optional(v.string()),
|
|
16
19
|
modeConfig: v.optional(v.any()),
|
|
20
|
+
visibility: v.optional(v.union(v.literal("public"), v.literal("private"))),
|
|
21
|
+
joinCode: v.optional(v.string()),
|
|
17
22
|
memberUserIds: v.array(v.string()),
|
|
18
23
|
slotsPerServer: v.number(),
|
|
19
24
|
save: saveConfigValidator,
|
|
@@ -82,11 +87,39 @@ export function jgengineTables() {
|
|
|
82
87
|
}).index("by_server_channel", ["serverId", "channelId"]),
|
|
83
88
|
};
|
|
84
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Convex table for the hosted GameContext-world path: one row per (gameId, serverId) holding the full
|
|
92
|
+
* `WorldSnapshot` blob, its revision, the member roster, and each member's held input. A sibling of
|
|
93
|
+
* {@link jgengineTables} — spread either (or both) into `defineSchema`.
|
|
94
|
+
*/
|
|
95
|
+
export function jgengineHostedTables() {
|
|
96
|
+
return {
|
|
97
|
+
jgHostedWorlds: defineTable({
|
|
98
|
+
gameId: v.string(),
|
|
99
|
+
serverId: v.string(),
|
|
100
|
+
snapshot: v.any(),
|
|
101
|
+
revision: v.number(),
|
|
102
|
+
memberUserIds: v.array(v.string()),
|
|
103
|
+
inputs: v.any(),
|
|
104
|
+
tickAnchorMs: v.number(),
|
|
105
|
+
createdAt: v.number(),
|
|
106
|
+
updatedAt: v.number(),
|
|
107
|
+
}).index("by_game_and_server", ["gameId", "serverId"]),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
85
110
|
const schemaForTypes = defineSchema(jgengineTables());
|
|
86
111
|
const query = queryGeneric;
|
|
87
112
|
const mutation = mutationGeneric;
|
|
88
113
|
const internalMutation = internalMutationGeneric;
|
|
89
|
-
|
|
114
|
+
export const DEFAULT_CONVEX_POSE_RULES = {
|
|
115
|
+
maxSpeed: 12,
|
|
116
|
+
maxVerticalOffset: 3,
|
|
117
|
+
minElapsedSec: 0.05,
|
|
118
|
+
maxElapsedSec: 0.5,
|
|
119
|
+
keepAliveRefreshMs: 10_000,
|
|
120
|
+
};
|
|
121
|
+
/** Resolve the acting user id under a {@link JgAuthMode}: the Convex identity when signed in (rejecting a mismatched claim), the claimed external id under `"anonymous"`, else `null`. */
|
|
122
|
+
export async function resolveActor(ctx, claimedExternalId, mode) {
|
|
90
123
|
const identity = await ctx.auth.getUserIdentity();
|
|
91
124
|
if (identity !== null) {
|
|
92
125
|
if (claimedExternalId !== undefined &&
|
|
@@ -101,6 +134,14 @@ async function resolveActor(ctx, claimedExternalId, mode) {
|
|
|
101
134
|
}
|
|
102
135
|
return null;
|
|
103
136
|
}
|
|
137
|
+
async function requireServerMember(ctx, serverId, actorUserId) {
|
|
138
|
+
const server = await ctx.db.get("jgGameServers", serverId);
|
|
139
|
+
if (!server)
|
|
140
|
+
return null;
|
|
141
|
+
if (!server.memberUserIds.includes(actorUserId))
|
|
142
|
+
return null;
|
|
143
|
+
return server;
|
|
144
|
+
}
|
|
104
145
|
const builtinRuntimeCommands = {
|
|
105
146
|
"engine.ping": {
|
|
106
147
|
validate: (_snapshot) => null,
|
|
@@ -132,6 +173,8 @@ function serverDocToRecord(server) {
|
|
|
132
173
|
status: server.status,
|
|
133
174
|
mode: server.mode,
|
|
134
175
|
modeConfig: server.modeConfig,
|
|
176
|
+
visibility: server.visibility,
|
|
177
|
+
joinCode: server.joinCode,
|
|
135
178
|
memberUserIds: server.memberUserIds,
|
|
136
179
|
slotsPerServer: server.slotsPerServer,
|
|
137
180
|
save: server.save,
|
|
@@ -177,6 +220,7 @@ async function loadServerSnapshot(ctx, server, runtime) {
|
|
|
177
220
|
serverRow: server.serverState,
|
|
178
221
|
playersByUserId,
|
|
179
222
|
chunksByKey,
|
|
223
|
+
revision: server.revision,
|
|
180
224
|
});
|
|
181
225
|
}
|
|
182
226
|
export async function applyLeaderboardIncrements(ctx, gameId, entries) {
|
|
@@ -254,6 +298,14 @@ async function persistServerSnapshot(ctx, server, snapshot, save) {
|
|
|
254
298
|
});
|
|
255
299
|
}
|
|
256
300
|
}
|
|
301
|
+
for (const chunkKey of plan.deletedChunks) {
|
|
302
|
+
const existing = await ctx.db
|
|
303
|
+
.query("jgWorldChunks")
|
|
304
|
+
.withIndex("by_server_and_chunk", (q) => q.eq("serverId", server._id).eq("chunkKey", chunkKey))
|
|
305
|
+
.unique();
|
|
306
|
+
if (existing)
|
|
307
|
+
await ctx.db.delete(existing._id);
|
|
308
|
+
}
|
|
257
309
|
await ctx.db.patch(server._id, {
|
|
258
310
|
serverState: plan.server.serverState,
|
|
259
311
|
sessionPlayers: plan.server.sessionPlayers,
|
|
@@ -273,16 +325,18 @@ async function flushServerIfDue(ctx, server, now, runtime) {
|
|
|
273
325
|
}
|
|
274
326
|
export const JG_RUNTIME_TICK_MS = 1_000;
|
|
275
327
|
export function createGameServerFunctions(options) {
|
|
276
|
-
const mode = options?.auth ?? "
|
|
328
|
+
const mode = options?.auth ?? "required";
|
|
329
|
+
const hostSlotsPerServer = options?.slotsPerServer ?? 16;
|
|
330
|
+
const feedWriteGate = createFeedWriteGate(options?.allowedFeedActions ?? []);
|
|
277
331
|
const registry = buildRuntimeRegistry(options?.runtimes);
|
|
278
332
|
const joinServer = mutation({
|
|
279
333
|
args: {
|
|
280
334
|
gameId: v.string(),
|
|
281
335
|
serverId: v.optional(v.id("jgGameServers")),
|
|
282
|
-
slotsPerServer: v.optional(v.number()),
|
|
283
|
-
save: v.optional(saveConfigValidator),
|
|
284
336
|
mode: v.optional(v.string()),
|
|
285
337
|
modeConfig: v.optional(v.any()),
|
|
338
|
+
visibility: v.optional(v.union(v.literal("public"), v.literal("private"))),
|
|
339
|
+
joinCode: v.optional(v.string()),
|
|
286
340
|
externalId: v.optional(v.string()),
|
|
287
341
|
},
|
|
288
342
|
returns: v.object({
|
|
@@ -296,9 +350,12 @@ export function createGameServerFunctions(options) {
|
|
|
296
350
|
}
|
|
297
351
|
const now = Date.now();
|
|
298
352
|
const runtime = resolveRuntime(registry, args.gameId);
|
|
299
|
-
const save =
|
|
300
|
-
const slotsPerServer =
|
|
353
|
+
const save = runtime.save;
|
|
354
|
+
const slotsPerServer = hostSlotsPerServer;
|
|
301
355
|
let server = args.serverId !== undefined ? await ctx.db.get("jgGameServers", args.serverId) : null;
|
|
356
|
+
if (args.serverId !== undefined && !server) {
|
|
357
|
+
throw new ConvexError("Server not found");
|
|
358
|
+
}
|
|
302
359
|
if (server && server.gameId !== args.gameId) {
|
|
303
360
|
throw new ConvexError("Server belongs to a different game");
|
|
304
361
|
}
|
|
@@ -312,8 +369,13 @@ export function createGameServerFunctions(options) {
|
|
|
312
369
|
joinable.push(...rows);
|
|
313
370
|
}
|
|
314
371
|
server =
|
|
315
|
-
joinable.find((row) =>
|
|
316
|
-
row.memberUserIds.
|
|
372
|
+
joinable.find((row) => {
|
|
373
|
+
if (row.memberUserIds.includes(actorUserId))
|
|
374
|
+
return true;
|
|
375
|
+
if (row.memberUserIds.length >= row.slotsPerServer)
|
|
376
|
+
return false;
|
|
377
|
+
return (row.visibility ?? "public") !== "private";
|
|
378
|
+
}) ?? null;
|
|
317
379
|
}
|
|
318
380
|
if (!server) {
|
|
319
381
|
const serverId = await ctx.db.insert("jgGameServers", {
|
|
@@ -321,6 +383,8 @@ export function createGameServerFunctions(options) {
|
|
|
321
383
|
status: "running",
|
|
322
384
|
mode: args.mode,
|
|
323
385
|
modeConfig: args.modeConfig,
|
|
386
|
+
visibility: args.visibility ?? "public",
|
|
387
|
+
joinCode: args.joinCode,
|
|
324
388
|
memberUserIds: [],
|
|
325
389
|
slotsPerServer,
|
|
326
390
|
save,
|
|
@@ -339,6 +403,11 @@ export function createGameServerFunctions(options) {
|
|
|
339
403
|
!server.memberUserIds.includes(actorUserId)) {
|
|
340
404
|
throw new ConvexError("Server is full");
|
|
341
405
|
}
|
|
406
|
+
if ((server.visibility ?? "public") === "private" &&
|
|
407
|
+
!server.memberUserIds.includes(actorUserId) &&
|
|
408
|
+
args.serverId === undefined) {
|
|
409
|
+
throw new ConvexError("Server is private");
|
|
410
|
+
}
|
|
342
411
|
const profile = await ctx.db
|
|
343
412
|
.query("jgPlayerProfiles")
|
|
344
413
|
.withIndex("by_user_and_game", (q) => q.eq("userId", actorUserId).eq("gameId", args.gameId))
|
|
@@ -421,13 +490,30 @@ export function createGameServerFunctions(options) {
|
|
|
421
490
|
if (!server.memberUserIds.includes(actorUserId)) {
|
|
422
491
|
return { ok: false, reason: "Not a member of this server" };
|
|
423
492
|
}
|
|
493
|
+
const loadedRevision = server.revision;
|
|
424
494
|
const runtime = resolveRuntime(registry, server.gameId);
|
|
425
495
|
const snapshot = await loadServerSnapshot(ctx, server, runtime);
|
|
426
|
-
const result =
|
|
496
|
+
const result = applyCommandWithOcc({
|
|
497
|
+
loadedRevision,
|
|
498
|
+
currentRevision: server.revision,
|
|
499
|
+
snapshot,
|
|
500
|
+
runtime,
|
|
501
|
+
actorUserId,
|
|
502
|
+
command: args.command,
|
|
503
|
+
input: args.input,
|
|
504
|
+
});
|
|
427
505
|
if (!result.ok) {
|
|
428
506
|
return result;
|
|
429
507
|
}
|
|
430
|
-
await
|
|
508
|
+
const latest = await ctx.db.get("jgGameServers", args.serverId);
|
|
509
|
+
if (!latest) {
|
|
510
|
+
return { ok: false, reason: "Server not found" };
|
|
511
|
+
}
|
|
512
|
+
const commit = commitIfRevisionMatch(loadedRevision, latest.revision);
|
|
513
|
+
if (!commit.ok) {
|
|
514
|
+
return commit;
|
|
515
|
+
}
|
|
516
|
+
await persistServerSnapshot(ctx, latest, result.snapshot, latest.save);
|
|
431
517
|
return { ok: true };
|
|
432
518
|
},
|
|
433
519
|
});
|
|
@@ -480,6 +566,8 @@ export function createGameServerFunctions(options) {
|
|
|
480
566
|
return null;
|
|
481
567
|
if (!server.memberUserIds.includes(actorUserId))
|
|
482
568
|
return null;
|
|
569
|
+
const sessionPlayers = server.sessionPlayers;
|
|
570
|
+
const ownSession = sessionPlayers[actorUserId];
|
|
483
571
|
return {
|
|
484
572
|
_id: server._id,
|
|
485
573
|
gameId: server.gameId,
|
|
@@ -490,7 +578,7 @@ export function createGameServerFunctions(options) {
|
|
|
490
578
|
save: server.save,
|
|
491
579
|
revision: server.revision,
|
|
492
580
|
serverState: server.serverState,
|
|
493
|
-
sessionPlayers:
|
|
581
|
+
sessionPlayers: ownSession === undefined ? {} : { [actorUserId]: ownSession },
|
|
494
582
|
updatedAt: server.updatedAt,
|
|
495
583
|
};
|
|
496
584
|
},
|
|
@@ -560,6 +648,10 @@ export function createGameServerFunctions(options) {
|
|
|
560
648
|
if (!actorUserId) {
|
|
561
649
|
throw new ConvexError("Not authenticated");
|
|
562
650
|
}
|
|
651
|
+
const allowed = validateFeedWrite(feedWriteGate, args.action);
|
|
652
|
+
if (!allowed.ok) {
|
|
653
|
+
throw new ConvexError(allowed.reason);
|
|
654
|
+
}
|
|
563
655
|
const server = await ctx.db.get("jgGameServers", args.serverId);
|
|
564
656
|
if (!server || !server.memberUserIds.includes(actorUserId)) {
|
|
565
657
|
throw new ConvexError("Not a member of this server");
|
|
@@ -740,8 +832,9 @@ export function createLeaderboardFunctions(options) {
|
|
|
740
832
|
return { getTop, getProfile, incrementMany };
|
|
741
833
|
}
|
|
742
834
|
export function createPresenceFunctions(options) {
|
|
743
|
-
const mode = options?.auth ?? "
|
|
835
|
+
const mode = options?.auth ?? "required";
|
|
744
836
|
const freshWindowMs = options?.freshWindowMs ?? 10_000;
|
|
837
|
+
const poseRules = options?.poseRules ?? DEFAULT_CONVEX_POSE_RULES;
|
|
745
838
|
const list = query({
|
|
746
839
|
args: {
|
|
747
840
|
serverId: v.string(),
|
|
@@ -755,6 +848,11 @@ export function createPresenceFunctions(options) {
|
|
|
755
848
|
lastSeenAt: v.number(),
|
|
756
849
|
})),
|
|
757
850
|
handler: async (ctx, args) => {
|
|
851
|
+
const actorUserId = await resolveActor(ctx, args.externalId, mode);
|
|
852
|
+
if (!actorUserId)
|
|
853
|
+
return [];
|
|
854
|
+
if ((await requireServerMember(ctx, args.serverId, actorUserId)) === null)
|
|
855
|
+
return [];
|
|
758
856
|
const threshold = Date.now() - freshWindowMs;
|
|
759
857
|
const rows = await ctx.db
|
|
760
858
|
.query("jgPoses")
|
|
@@ -788,21 +886,38 @@ export function createPresenceFunctions(options) {
|
|
|
788
886
|
const actorUserId = await resolveActor(ctx, args.externalId, mode);
|
|
789
887
|
if (!actorUserId)
|
|
790
888
|
return null;
|
|
889
|
+
if ((await requireServerMember(ctx, args.serverId, actorUserId)) === null)
|
|
890
|
+
return null;
|
|
791
891
|
const now = Date.now();
|
|
792
892
|
const existing = await ctx.db
|
|
793
893
|
.query("jgPoses")
|
|
794
894
|
.withIndex("by_server_and_user", (q) => q.eq("serverId", args.serverId).eq("userId", actorUserId))
|
|
795
895
|
.unique();
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
896
|
+
const current = existing === null
|
|
897
|
+
? spawnPresenceState(undefined, now, poseRules)
|
|
898
|
+
: {
|
|
899
|
+
position: { x: existing.x, y: existing.y, z: existing.z },
|
|
900
|
+
rotationY: existing.rotationY,
|
|
901
|
+
rotationPitch: existing.rotationPitch,
|
|
902
|
+
lastSeenAtMs: existing.updatedAt,
|
|
903
|
+
};
|
|
904
|
+
const decision = decidePoseSync(current, {
|
|
905
|
+
position: { x: args.pose.x, y: args.pose.y, z: args.pose.z },
|
|
800
906
|
rotationY: args.pose.rotationY,
|
|
801
907
|
rotationPitch: args.pose.rotationPitch,
|
|
908
|
+
}, poseRules, now);
|
|
909
|
+
const patch = {
|
|
910
|
+
x: decision.position.x,
|
|
911
|
+
y: decision.position.y,
|
|
912
|
+
z: decision.position.z,
|
|
913
|
+
rotationY: decision.rotationY,
|
|
914
|
+
rotationPitch: decision.rotationPitch,
|
|
802
915
|
updatedAt: now,
|
|
803
916
|
};
|
|
804
917
|
if (existing) {
|
|
805
|
-
|
|
918
|
+
if (decision.changed || decision.refreshKeepAlive) {
|
|
919
|
+
await ctx.db.patch(existing._id, patch);
|
|
920
|
+
}
|
|
806
921
|
}
|
|
807
922
|
else {
|
|
808
923
|
await ctx.db.insert("jgPoses", { serverId: args.serverId, userId: actorUserId, ...patch });
|
|
@@ -832,7 +947,7 @@ export function createPresenceFunctions(options) {
|
|
|
832
947
|
return { list, sync, leave };
|
|
833
948
|
}
|
|
834
949
|
export function createChatFunctions(options) {
|
|
835
|
-
const mode = options?.auth ?? "
|
|
950
|
+
const mode = options?.auth ?? "required";
|
|
836
951
|
const historyLimit = options?.historyLimit ?? 100;
|
|
837
952
|
const maxBodyLength = options?.maxBodyLength ?? 500;
|
|
838
953
|
const minIntervalMs = options?.minIntervalMs ?? 300;
|
|
@@ -850,6 +965,11 @@ export function createChatFunctions(options) {
|
|
|
850
965
|
at: v.number(),
|
|
851
966
|
})),
|
|
852
967
|
handler: async (ctx, args) => {
|
|
968
|
+
const actorUserId = await resolveActor(ctx, args.externalId, mode);
|
|
969
|
+
if (!actorUserId)
|
|
970
|
+
return [];
|
|
971
|
+
if ((await requireServerMember(ctx, args.serverId, actorUserId)) === null)
|
|
972
|
+
return [];
|
|
853
973
|
const rows = await ctx.db
|
|
854
974
|
.query("jgChatMessages")
|
|
855
975
|
.withIndex("by_server_channel", (q) => q.eq("serverId", args.serverId).eq("channelId", args.channelId))
|
|
@@ -879,6 +999,9 @@ export function createChatFunctions(options) {
|
|
|
879
999
|
const actorUserId = await resolveActor(ctx, args.externalId, mode);
|
|
880
1000
|
if (!actorUserId)
|
|
881
1001
|
return { ok: false, reason: "not signed in" };
|
|
1002
|
+
if ((await requireServerMember(ctx, args.serverId, actorUserId)) === null) {
|
|
1003
|
+
return { ok: false, reason: "not a member of this server" };
|
|
1004
|
+
}
|
|
882
1005
|
const body = args.body.trim();
|
|
883
1006
|
if (body.length === 0)
|
|
884
1007
|
return { ok: false, reason: "empty message" };
|