solanapolis 1.0.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.
Files changed (202) hide show
  1. package/README.md +518 -0
  2. package/bin/solanapolis.js +197 -0
  3. package/convex/_generated/api.d.ts +175 -0
  4. package/convex/_generated/api.js +23 -0
  5. package/convex/_generated/dataModel.d.ts +60 -0
  6. package/convex/_generated/server.d.ts +143 -0
  7. package/convex/_generated/server.js +93 -0
  8. package/convex/agent/conversation.ts +352 -0
  9. package/convex/agent/embeddingsCache.ts +110 -0
  10. package/convex/agent/memory.ts +450 -0
  11. package/convex/agent/schema.ts +53 -0
  12. package/convex/aiChat.ts +54 -0
  13. package/convex/aiTown/agent.ts +382 -0
  14. package/convex/aiTown/agentDescription.ts +27 -0
  15. package/convex/aiTown/agentInputs.ts +155 -0
  16. package/convex/aiTown/agentOperations.ts +178 -0
  17. package/convex/aiTown/conversation.ts +395 -0
  18. package/convex/aiTown/conversationMembership.ts +38 -0
  19. package/convex/aiTown/game.ts +371 -0
  20. package/convex/aiTown/ids.ts +32 -0
  21. package/convex/aiTown/inputHandler.ts +9 -0
  22. package/convex/aiTown/inputs.ts +25 -0
  23. package/convex/aiTown/insertInput.ts +20 -0
  24. package/convex/aiTown/location.ts +32 -0
  25. package/convex/aiTown/main.ts +154 -0
  26. package/convex/aiTown/movement.ts +189 -0
  27. package/convex/aiTown/player.ts +310 -0
  28. package/convex/aiTown/playerDescription.ts +35 -0
  29. package/convex/aiTown/schema.ts +79 -0
  30. package/convex/aiTown/world.ts +65 -0
  31. package/convex/aiTown/worldMap.ts +74 -0
  32. package/convex/chat.ts +79 -0
  33. package/convex/constants.ts +78 -0
  34. package/convex/convex.config.ts +6 -0
  35. package/convex/crons.ts +89 -0
  36. package/convex/engine/abstractGame.ts +199 -0
  37. package/convex/engine/historicalObject.ts +355 -0
  38. package/convex/engine/schema.ts +56 -0
  39. package/convex/http.ts +36 -0
  40. package/convex/init.ts +110 -0
  41. package/convex/messages.ts +53 -0
  42. package/convex/npcCarAgents.ts +415 -0
  43. package/convex/schema.ts +61 -0
  44. package/convex/streaming.ts +23 -0
  45. package/convex/testing.ts +202 -0
  46. package/convex/tsconfig.json +18 -0
  47. package/convex/util/FastIntegerCompression.ts +221 -0
  48. package/convex/util/assertNever.ts +4 -0
  49. package/convex/util/asyncMap.ts +20 -0
  50. package/convex/util/compression.ts +71 -0
  51. package/convex/util/geometry.ts +132 -0
  52. package/convex/util/isSimpleObject.ts +11 -0
  53. package/convex/util/llm.ts +724 -0
  54. package/convex/util/minheap.ts +38 -0
  55. package/convex/util/object.ts +22 -0
  56. package/convex/util/sleep.ts +3 -0
  57. package/convex/util/types.ts +33 -0
  58. package/convex/util/xxhash.ts +228 -0
  59. package/convex/world.ts +257 -0
  60. package/data/animations/campfire.json +45 -0
  61. package/data/animations/gentlesparkle.json +37 -0
  62. package/data/animations/gentlesplash.json +61 -0
  63. package/data/animations/gentlewaterfall.json +61 -0
  64. package/data/animations/windmill.json +78 -0
  65. package/data/characters.ts +121 -0
  66. package/data/convertMap.js +74 -0
  67. package/data/gentle.js +330 -0
  68. package/data/spritesheets/f1.ts +75 -0
  69. package/data/spritesheets/f2.ts +75 -0
  70. package/data/spritesheets/f3.ts +75 -0
  71. package/data/spritesheets/f4.ts +75 -0
  72. package/data/spritesheets/f5.ts +75 -0
  73. package/data/spritesheets/f6.ts +75 -0
  74. package/data/spritesheets/f7.ts +75 -0
  75. package/data/spritesheets/f8.ts +75 -0
  76. package/data/spritesheets/p1.ts +59 -0
  77. package/data/spritesheets/p2.ts +59 -0
  78. package/data/spritesheets/p3.ts +59 -0
  79. package/data/spritesheets/player.ts +59 -0
  80. package/data/spritesheets/types.ts +26 -0
  81. package/eslint.config.mjs +37 -0
  82. package/next.config.ts +7 -0
  83. package/package.json +85 -0
  84. package/postcss.config.mjs +7 -0
  85. package/public/file.svg +1 -0
  86. package/public/globe.svg +1 -0
  87. package/public/helius-icon.svg +84 -0
  88. package/public/helius-logo.svg +85 -0
  89. package/public/next.svg +1 -0
  90. package/public/plane.glb +0 -0
  91. package/public/vercel.svg +1 -0
  92. package/public/window.svg +1 -0
  93. package/scripts/clear-city.ts +74 -0
  94. package/scripts/seed-wallets.ts +185 -0
  95. package/scripts/setup-webhook.ts +73 -0
  96. package/src/app/api/auth/callback/route.ts +6 -0
  97. package/src/app/api/auth/link-wallet/route.ts +6 -0
  98. package/src/app/api/auth/phantom/route.ts +6 -0
  99. package/src/app/api/broadcast-position/route.ts +59 -0
  100. package/src/app/api/leaderboard/route.ts +85 -0
  101. package/src/app/api/network-stats/route.ts +86 -0
  102. package/src/app/api/parcel-reward/route.ts +181 -0
  103. package/src/app/api/queue-status/route.ts +30 -0
  104. package/src/app/api/snapshots/route.ts +37 -0
  105. package/src/app/api/transactions/enhanced/route.ts +57 -0
  106. package/src/app/api/treasury/route.ts +83 -0
  107. package/src/app/api/wallet/[address]/balances/route.ts +124 -0
  108. package/src/app/api/wallet/[address]/identity/route.ts +32 -0
  109. package/src/app/api/wallet/[address]/route.ts +216 -0
  110. package/src/app/api/wallet/[address]/traded-tokens/route.ts +41 -0
  111. package/src/app/api/wallets/route.ts +68 -0
  112. package/src/app/api/webhooks/helius/route.ts +76 -0
  113. package/src/app/auth/callback/page.tsx +29 -0
  114. package/src/app/favicon.ico +0 -0
  115. package/src/app/globals.css +39 -0
  116. package/src/app/layout.tsx +43 -0
  117. package/src/app/page.tsx +16 -0
  118. package/src/components/AITownNPCs.tsx +206 -0
  119. package/src/components/ActivityFeed.tsx +189 -0
  120. package/src/components/AuthPanel.tsx +163 -0
  121. package/src/components/BeachScene.tsx +280 -0
  122. package/src/components/Building.tsx +138 -0
  123. package/src/components/CesiumFlight.tsx +1768 -0
  124. package/src/components/CesiumGlobe.tsx +616 -0
  125. package/src/components/CitizenCard.tsx +442 -0
  126. package/src/components/CitizenCardModal.tsx +153 -0
  127. package/src/components/CityGrid.tsx +313 -0
  128. package/src/components/CityLandmarks.tsx +427 -0
  129. package/src/components/CityScene.tsx +1289 -0
  130. package/src/components/CitySlotsBadge.tsx +68 -0
  131. package/src/components/CockpitHUD.tsx +460 -0
  132. package/src/components/ConvexWrapper.tsx +19 -0
  133. package/src/components/DubaiDistrict.tsx +630 -0
  134. package/src/components/FlightMiniMap.tsx +133 -0
  135. package/src/components/GameChat.tsx +383 -0
  136. package/src/components/GameHUD.tsx +393 -0
  137. package/src/components/Ground.tsx +14 -0
  138. package/src/components/HowItWorksModal.tsx +251 -0
  139. package/src/components/IngestionBanner.tsx +123 -0
  140. package/src/components/InstancedBuildings.tsx +316 -0
  141. package/src/components/InstancedCars.tsx +504 -0
  142. package/src/components/InstancedCityPlanes.tsx +259 -0
  143. package/src/components/InstancedHouses.tsx +246 -0
  144. package/src/components/InstancedLampPosts.tsx +201 -0
  145. package/src/components/InstancedResidentCars.tsx +357 -0
  146. package/src/components/InstancedRoadDashes.tsx +42 -0
  147. package/src/components/InstancedSkyscrapers.tsx +434 -0
  148. package/src/components/InstancedTrees.tsx +67 -0
  149. package/src/components/LeaderboardPanel.tsx +136 -0
  150. package/src/components/MultiplayerPlanes.tsx +128 -0
  151. package/src/components/NetworkStats.tsx +83 -0
  152. package/src/components/NewBuildingSpotlight.tsx +93 -0
  153. package/src/components/ParcelChallengeBanner.tsx +242 -0
  154. package/src/components/ParcelReward.tsx +191 -0
  155. package/src/components/Park.tsx +42 -0
  156. package/src/components/PhantomWrapper.tsx +22 -0
  157. package/src/components/PixelStreamViewer.tsx +335 -0
  158. package/src/components/PlaneMode.tsx +190 -0
  159. package/src/components/PlayerCar.tsx +211 -0
  160. package/src/components/PlayerPlane.tsx +255 -0
  161. package/src/components/ProjectileRenderer.tsx +249 -0
  162. package/src/components/QueueStatusBanner.tsx +86 -0
  163. package/src/components/RealPlayerTags.tsx +82 -0
  164. package/src/components/SceneLighting.tsx +382 -0
  165. package/src/components/SelectionBeam.tsx +59 -0
  166. package/src/components/SwapPanel.tsx +104 -0
  167. package/src/components/SwapParticles.tsx +237 -0
  168. package/src/components/TreasureGate.tsx +505 -0
  169. package/src/components/WalletPanel.tsx +421 -0
  170. package/src/components/WalletSearch.tsx +244 -0
  171. package/src/components/WelcomeOverlay.tsx +135 -0
  172. package/src/components/WindowTooltip.tsx +498 -0
  173. package/src/context/AuthContext.tsx +230 -0
  174. package/src/lib/bot-detection.ts +125 -0
  175. package/src/lib/building-math.ts +136 -0
  176. package/src/lib/building-shader.ts +253 -0
  177. package/src/lib/car-paths.ts +244 -0
  178. package/src/lib/car-system.ts +182 -0
  179. package/src/lib/city-constants.ts +29 -0
  180. package/src/lib/city-slots.ts +35 -0
  181. package/src/lib/city-zoning.ts +64 -0
  182. package/src/lib/collision-map.ts +147 -0
  183. package/src/lib/day-night.ts +252 -0
  184. package/src/lib/export-card.ts +28 -0
  185. package/src/lib/helius-webhook.ts +90 -0
  186. package/src/lib/helius.ts +74 -0
  187. package/src/lib/house-shader.ts +119 -0
  188. package/src/lib/mock-data.ts +56 -0
  189. package/src/lib/multiplayer-manager.ts +329 -0
  190. package/src/lib/plane-physics.ts +66 -0
  191. package/src/lib/player-car.ts +147 -0
  192. package/src/lib/player-plane.ts +200 -0
  193. package/src/lib/projectile-system.ts +272 -0
  194. package/src/lib/skyscraper-types.ts +52 -0
  195. package/src/lib/sound-engine.ts +464 -0
  196. package/src/lib/supabase-admin.ts +9 -0
  197. package/src/lib/supabase.ts +8 -0
  198. package/src/lib/swap-events.ts +70 -0
  199. package/src/middleware.ts +37 -0
  200. package/src/types/phantom.d.ts +16 -0
  201. package/src/types/wallet.ts +20 -0
  202. package/tsconfig.json +34 -0
@@ -0,0 +1,371 @@
1
+ import { Infer, v } from 'convex/values';
2
+ import { Doc, Id } from '../_generated/dataModel';
3
+ import {
4
+ ActionCtx,
5
+ DatabaseReader,
6
+ MutationCtx,
7
+ internalMutation,
8
+ internalQuery,
9
+ } from '../_generated/server';
10
+ import { World, serializedWorld } from './world';
11
+ import { WorldMap, serializedWorldMap } from './worldMap';
12
+ import { PlayerDescription, serializedPlayerDescription } from './playerDescription';
13
+ import { Location, locationFields, playerLocation } from './location';
14
+ import { runAgentOperation } from './agent';
15
+ import { GameId, IdTypes, allocGameId } from './ids';
16
+ import { InputArgs, InputNames, inputs } from './inputs';
17
+ import {
18
+ AbstractGame,
19
+ EngineUpdate,
20
+ applyEngineUpdate,
21
+ engineUpdate,
22
+ loadEngine,
23
+ } from '../engine/abstractGame';
24
+ import { internal } from '../_generated/api';
25
+ import { HistoricalObject } from '../engine/historicalObject';
26
+ import { AgentDescription, serializedAgentDescription } from './agentDescription';
27
+ import { parseMap, serializeMap } from '../util/object';
28
+
29
+ const gameState = v.object({
30
+ world: v.object(serializedWorld),
31
+ playerDescriptions: v.array(v.object(serializedPlayerDescription)),
32
+ agentDescriptions: v.array(v.object(serializedAgentDescription)),
33
+ worldMap: v.object(serializedWorldMap),
34
+ });
35
+ type GameState = Infer<typeof gameState>;
36
+
37
+ const gameStateDiff = v.object({
38
+ world: v.object(serializedWorld),
39
+ playerDescriptions: v.optional(v.array(v.object(serializedPlayerDescription))),
40
+ agentDescriptions: v.optional(v.array(v.object(serializedAgentDescription))),
41
+ worldMap: v.optional(v.object(serializedWorldMap)),
42
+ agentOperations: v.array(v.object({ name: v.string(), args: v.any() })),
43
+ });
44
+ type GameStateDiff = Infer<typeof gameStateDiff>;
45
+
46
+ export class Game extends AbstractGame {
47
+ tickDuration = 16;
48
+ stepDuration = 1000;
49
+ maxTicksPerStep = 600;
50
+ maxInputsPerStep = 32;
51
+
52
+ world: World;
53
+
54
+ historicalLocations: Map<GameId<'players'>, HistoricalObject<Location>>;
55
+
56
+ descriptionsModified: boolean;
57
+ worldMap: WorldMap;
58
+ playerDescriptions: Map<GameId<'players'>, PlayerDescription>;
59
+ agentDescriptions: Map<GameId<'agents'>, AgentDescription>;
60
+
61
+ pendingOperations: Array<{ name: string; args: any }> = [];
62
+
63
+ numPathfinds: number;
64
+
65
+ constructor(
66
+ engine: Doc<'engines'>,
67
+ public worldId: Id<'worlds'>,
68
+ state: GameState,
69
+ ) {
70
+ super(engine);
71
+
72
+ this.world = new World(state.world);
73
+ delete this.world.historicalLocations;
74
+
75
+ this.descriptionsModified = false;
76
+ this.worldMap = new WorldMap(state.worldMap);
77
+ this.agentDescriptions = parseMap(state.agentDescriptions, AgentDescription, (a) => a.agentId);
78
+ this.playerDescriptions = parseMap(
79
+ state.playerDescriptions,
80
+ PlayerDescription,
81
+ (p) => p.playerId,
82
+ );
83
+
84
+ this.historicalLocations = new Map();
85
+
86
+ this.numPathfinds = 0;
87
+ }
88
+
89
+ static async load(
90
+ db: DatabaseReader,
91
+ worldId: Id<'worlds'>,
92
+ generationNumber: number,
93
+ ): Promise<{ engine: Doc<'engines'>; gameState: GameState }> {
94
+ const worldDoc = await db.get(worldId);
95
+ if (!worldDoc) {
96
+ throw new Error(`No world found with id ${worldId}`);
97
+ }
98
+ const worldStatus = await db
99
+ .query('worldStatus')
100
+ .withIndex('worldId', (q) => q.eq('worldId', worldId))
101
+ .unique();
102
+ if (!worldStatus) {
103
+ throw new Error(`No engine found for world ${worldId}`);
104
+ }
105
+ const engine = await loadEngine(db, worldStatus.engineId, generationNumber);
106
+ const playerDescriptionsDocs = await db
107
+ .query('playerDescriptions')
108
+ .withIndex('worldId', (q) => q.eq('worldId', worldId))
109
+ .collect();
110
+ const agentDescriptionsDocs = await db
111
+ .query('agentDescriptions')
112
+ .withIndex('worldId', (q) => q.eq('worldId', worldId))
113
+ .collect();
114
+ const worldMapDoc = await db
115
+ .query('maps')
116
+ .withIndex('worldId', (q) => q.eq('worldId', worldId))
117
+ .unique();
118
+ if (!worldMapDoc) {
119
+ throw new Error(`No map found for world ${worldId}`);
120
+ }
121
+ // Discard the system fields and historicalLocations from the world state.
122
+ const { _id, _creationTime, historicalLocations: _, ...world } = worldDoc;
123
+ const playerDescriptions = playerDescriptionsDocs
124
+ // Discard player descriptions for players that no longer exist.
125
+ .filter((d) => !!world.players.find((p) => p.id === d.playerId))
126
+ .map(({ _id, _creationTime, worldId: _, ...doc }) => doc);
127
+ const agentDescriptions = agentDescriptionsDocs
128
+ .filter((a) => !!world.agents.find((p) => p.id === a.agentId))
129
+ .map(({ _id, _creationTime, worldId: _, ...doc }) => doc);
130
+ const {
131
+ _id: _mapId,
132
+ _creationTime: _mapCreationTime,
133
+ worldId: _mapWorldId,
134
+ ...worldMap
135
+ } = worldMapDoc;
136
+ return {
137
+ engine,
138
+ gameState: {
139
+ world,
140
+ playerDescriptions,
141
+ agentDescriptions,
142
+ worldMap,
143
+ },
144
+ };
145
+ }
146
+
147
+ allocId<T extends IdTypes>(idType: T): GameId<T> {
148
+ const id = allocGameId(idType, this.world.nextId);
149
+ this.world.nextId += 1;
150
+ return id;
151
+ }
152
+
153
+ scheduleOperation(name: string, args: unknown) {
154
+ this.pendingOperations.push({ name, args });
155
+ }
156
+
157
+ handleInput<Name extends InputNames>(now: number, name: Name, args: InputArgs<Name>) {
158
+ const handler = inputs[name]?.handler;
159
+ if (!handler) {
160
+ throw new Error(`Invalid input: ${name}`);
161
+ }
162
+ return handler(this, now, args as any);
163
+ }
164
+
165
+ beginStep(_now: number) {
166
+ // Store the current location of all players in the history tracking buffer.
167
+ this.historicalLocations.clear();
168
+ for (const player of this.world.players.values()) {
169
+ this.historicalLocations.set(
170
+ player.id,
171
+ new HistoricalObject(locationFields, playerLocation(player)),
172
+ );
173
+ }
174
+ this.numPathfinds = 0;
175
+ }
176
+
177
+ tick(now: number) {
178
+ for (const player of this.world.players.values()) {
179
+ player.tick(this, now);
180
+ }
181
+ for (const player of this.world.players.values()) {
182
+ player.tickPathfinding(this, now);
183
+ }
184
+ for (const player of this.world.players.values()) {
185
+ player.tickPosition(this, now);
186
+ }
187
+ for (const conversation of this.world.conversations.values()) {
188
+ conversation.tick(this, now);
189
+ }
190
+ for (const agent of this.world.agents.values()) {
191
+ agent.tick(this, now);
192
+ }
193
+
194
+ // Save each player's location into the history buffer at the end of
195
+ // each tick.
196
+ for (const player of this.world.players.values()) {
197
+ let historicalObject = this.historicalLocations.get(player.id);
198
+ if (!historicalObject) {
199
+ historicalObject = new HistoricalObject(locationFields, playerLocation(player));
200
+ this.historicalLocations.set(player.id, historicalObject);
201
+ }
202
+ historicalObject.update(now, playerLocation(player));
203
+ }
204
+ }
205
+
206
+ async saveStep(ctx: ActionCtx, engineUpdate: EngineUpdate): Promise<void> {
207
+ const diff = this.takeDiff();
208
+ await ctx.runMutation(internal.aiTown.game.saveWorld, {
209
+ engineId: this.engine._id,
210
+ engineUpdate,
211
+ worldId: this.worldId,
212
+ worldDiff: diff,
213
+ });
214
+ }
215
+
216
+ takeDiff(): GameStateDiff {
217
+ const historicalLocations = [];
218
+ let bufferSize = 0;
219
+ for (const [id, historicalObject] of this.historicalLocations.entries()) {
220
+ const buffer = historicalObject.pack();
221
+ if (!buffer) {
222
+ continue;
223
+ }
224
+ historicalLocations.push({ playerId: id, location: buffer });
225
+ bufferSize += buffer.byteLength;
226
+ }
227
+ if (bufferSize > 0) {
228
+ console.debug(
229
+ `Packed ${Object.entries(historicalLocations).length} history buffers in ${(
230
+ bufferSize / 1024
231
+ ).toFixed(2)}KiB.`,
232
+ );
233
+ }
234
+ this.historicalLocations.clear();
235
+
236
+ const result: GameStateDiff = {
237
+ world: { ...this.world.serialize(), historicalLocations },
238
+ agentOperations: this.pendingOperations,
239
+ };
240
+ this.pendingOperations = [];
241
+ if (this.descriptionsModified) {
242
+ result.playerDescriptions = serializeMap(this.playerDescriptions);
243
+ result.agentDescriptions = serializeMap(this.agentDescriptions);
244
+ result.worldMap = this.worldMap.serialize();
245
+ this.descriptionsModified = false;
246
+ }
247
+ return result;
248
+ }
249
+
250
+ static async saveDiff(ctx: MutationCtx, worldId: Id<'worlds'>, diff: GameStateDiff) {
251
+ const existingWorld = await ctx.db.get(worldId);
252
+ if (!existingWorld) {
253
+ throw new Error(`No world found with id ${worldId}`);
254
+ }
255
+ const newWorld = diff.world;
256
+ // Archive newly deleted players, conversations, and agents.
257
+ for (const player of existingWorld.players) {
258
+ if (!newWorld.players.some((p) => p.id === player.id)) {
259
+ await ctx.db.insert('archivedPlayers', { worldId, ...player });
260
+ }
261
+ }
262
+ for (const conversation of existingWorld.conversations) {
263
+ if (!newWorld.conversations.some((c) => c.id === conversation.id)) {
264
+ const participants = conversation.participants.map((p) => p.playerId);
265
+ const archivedConversation = {
266
+ worldId,
267
+ id: conversation.id,
268
+ created: conversation.created,
269
+ creator: conversation.creator,
270
+ ended: Date.now(),
271
+ lastMessage: conversation.lastMessage,
272
+ numMessages: conversation.numMessages,
273
+ participants,
274
+ };
275
+ await ctx.db.insert('archivedConversations', archivedConversation);
276
+ for (let i = 0; i < participants.length; i++) {
277
+ for (let j = 0; j < participants.length; j++) {
278
+ if (i == j) {
279
+ continue;
280
+ }
281
+ const player1 = participants[i];
282
+ const player2 = participants[j];
283
+ await ctx.db.insert('participatedTogether', {
284
+ worldId,
285
+ conversationId: conversation.id,
286
+ player1,
287
+ player2,
288
+ ended: Date.now(),
289
+ });
290
+ }
291
+ }
292
+ }
293
+ }
294
+ for (const conversation of existingWorld.agents) {
295
+ if (!newWorld.agents.some((a) => a.id === conversation.id)) {
296
+ await ctx.db.insert('archivedAgents', { worldId, ...conversation });
297
+ }
298
+ }
299
+ // Update the world state.
300
+ await ctx.db.replace(worldId, newWorld);
301
+
302
+ // Update the larger description tables if they changed.
303
+ const { playerDescriptions, agentDescriptions, worldMap } = diff;
304
+ if (playerDescriptions) {
305
+ for (const description of playerDescriptions) {
306
+ const existing = await ctx.db
307
+ .query('playerDescriptions')
308
+ .withIndex('worldId', (q) =>
309
+ q.eq('worldId', worldId).eq('playerId', description.playerId),
310
+ )
311
+ .unique();
312
+ if (existing) {
313
+ await ctx.db.replace(existing._id, { worldId, ...description });
314
+ } else {
315
+ await ctx.db.insert('playerDescriptions', { worldId, ...description });
316
+ }
317
+ }
318
+ }
319
+ if (agentDescriptions) {
320
+ for (const description of agentDescriptions) {
321
+ const existing = await ctx.db
322
+ .query('agentDescriptions')
323
+ .withIndex('worldId', (q) => q.eq('worldId', worldId).eq('agentId', description.agentId))
324
+ .unique();
325
+ if (existing) {
326
+ await ctx.db.replace(existing._id, { worldId, ...description });
327
+ } else {
328
+ await ctx.db.insert('agentDescriptions', { worldId, ...description });
329
+ }
330
+ }
331
+ }
332
+ if (worldMap) {
333
+ const existing = await ctx.db
334
+ .query('maps')
335
+ .withIndex('worldId', (q) => q.eq('worldId', worldId))
336
+ .unique();
337
+ if (existing) {
338
+ await ctx.db.replace(existing._id, { worldId, ...worldMap });
339
+ } else {
340
+ await ctx.db.insert('maps', { worldId, ...worldMap });
341
+ }
342
+ }
343
+ // Start the desired agent operations.
344
+ for (const operation of diff.agentOperations) {
345
+ await runAgentOperation(ctx, operation.name, operation.args);
346
+ }
347
+ }
348
+ }
349
+
350
+ export const loadWorld = internalQuery({
351
+ args: {
352
+ worldId: v.id('worlds'),
353
+ generationNumber: v.number(),
354
+ },
355
+ handler: async (ctx, args) => {
356
+ return await Game.load(ctx.db, args.worldId, args.generationNumber);
357
+ },
358
+ });
359
+
360
+ export const saveWorld = internalMutation({
361
+ args: {
362
+ engineId: v.id('engines'),
363
+ engineUpdate,
364
+ worldId: v.id('worlds'),
365
+ worldDiff: gameStateDiff,
366
+ },
367
+ handler: async (ctx, args) => {
368
+ await applyEngineUpdate(ctx, args.engineId, args.engineUpdate);
369
+ await Game.saveDiff(ctx, args.worldId, args.worldDiff);
370
+ },
371
+ });
@@ -0,0 +1,32 @@
1
+ import { v } from 'convex/values';
2
+
3
+ const IdShortCodes = { agents: 'a', conversations: 'c', players: 'p', operations: 'o' };
4
+ export type IdTypes = keyof typeof IdShortCodes;
5
+
6
+ export type GameId<T extends IdTypes> = string & { __type: T };
7
+
8
+ export function parseGameId<T extends IdTypes>(idType: T, gameId: string): GameId<T> {
9
+ const type = gameId[0];
10
+ const match = Object.entries(IdShortCodes).find(([_, value]) => value === type);
11
+ if (!match || match[0] !== idType) {
12
+ throw new Error(`Invalid game ID type: ${type}`);
13
+ }
14
+ const number = parseInt(gameId.slice(2), 10);
15
+ if (isNaN(number) || !Number.isInteger(number) || number < 0) {
16
+ throw new Error(`Invalid game ID number: ${gameId}`);
17
+ }
18
+ return gameId as GameId<T>;
19
+ }
20
+
21
+ export function allocGameId<T extends IdTypes>(idType: T, idNumber: number): GameId<T> {
22
+ const type = IdShortCodes[idType];
23
+ if (!type) {
24
+ throw new Error(`Invalid game ID type: ${idType}`);
25
+ }
26
+ return `${type}:${idNumber}` as GameId<T>;
27
+ }
28
+
29
+ export const conversationId = v.string();
30
+ export const playerId = v.string();
31
+ export const agentId = v.string();
32
+ export const operationId = v.string();
@@ -0,0 +1,9 @@
1
+ import { ObjectType, PropertyValidators, Value } from 'convex/values';
2
+ import type { Game } from './game';
3
+
4
+ export function inputHandler<ArgsValidator extends PropertyValidators, Return extends Value>(def: {
5
+ args: ArgsValidator;
6
+ handler: (game: Game, now: number, args: ObjectType<ArgsValidator>) => Return;
7
+ }) {
8
+ return def;
9
+ }
@@ -0,0 +1,25 @@
1
+ import { ObjectType } from 'convex/values';
2
+ import { playerInputs } from './player';
3
+ import { conversationInputs } from './conversation';
4
+ import { agentInputs } from './agentInputs';
5
+
6
+ // It's easy to hit circular dependencies with these imports,
7
+ // so assert at module scope so we hit errors when analyzing.
8
+ if (playerInputs === undefined || conversationInputs === undefined || agentInputs === undefined) {
9
+ throw new Error("Input map is undefined, check if there's a circular import.");
10
+ }
11
+ export const inputs = {
12
+ ...playerInputs,
13
+ // Inputs for the messaging layer.
14
+ ...conversationInputs,
15
+ // Inputs for the agent layer.
16
+ ...agentInputs,
17
+ };
18
+ export type Inputs = typeof inputs;
19
+ export type InputNames = keyof Inputs;
20
+ export type InputArgs<Name extends InputNames> = ObjectType<Inputs[Name]['args']>;
21
+ export type InputReturnValue<Name extends InputNames> = ReturnType<
22
+ Inputs[Name]['handler']
23
+ > extends Promise<infer T>
24
+ ? T
25
+ : never;
@@ -0,0 +1,20 @@
1
+ import { MutationCtx } from '../_generated/server';
2
+ import { Id } from '../_generated/dataModel';
3
+ import { engineInsertInput } from '../engine/abstractGame';
4
+ import { InputNames, InputArgs } from './inputs';
5
+
6
+ export async function insertInput<Name extends InputNames>(
7
+ ctx: MutationCtx,
8
+ worldId: Id<'worlds'>,
9
+ name: Name,
10
+ args: InputArgs<Name>,
11
+ ): Promise<Id<'inputs'>> {
12
+ const worldStatus = await ctx.db
13
+ .query('worldStatus')
14
+ .withIndex('worldId', (q) => q.eq('worldId', worldId))
15
+ .unique();
16
+ if (!worldStatus) {
17
+ throw new Error(`World for engine ${worldId} not found`);
18
+ }
19
+ return await engineInsertInput(ctx, worldStatus.engineId, name, args);
20
+ }
@@ -0,0 +1,32 @@
1
+ import { FieldConfig } from '../engine/historicalObject';
2
+ import { Player } from './player';
3
+
4
+ export type Location = {
5
+ // Unpacked player position.
6
+ x: number;
7
+ y: number;
8
+
9
+ // Normalized facing vector.
10
+ dx: number;
11
+ dy: number;
12
+
13
+ speed: number;
14
+ };
15
+
16
+ export const locationFields: FieldConfig = [
17
+ { name: 'x', precision: 8 },
18
+ { name: 'y', precision: 8 },
19
+ { name: 'dx', precision: 8 },
20
+ { name: 'dy', precision: 8 },
21
+ { name: 'speed', precision: 16 },
22
+ ];
23
+
24
+ export function playerLocation(player: Player): Location {
25
+ return {
26
+ x: player.position.x,
27
+ y: player.position.y,
28
+ dx: player.facing.dx,
29
+ dy: player.facing.dy,
30
+ speed: player.speed,
31
+ };
32
+ }
@@ -0,0 +1,154 @@
1
+ import { ConvexError, v } from 'convex/values';
2
+ import { DatabaseReader, MutationCtx, internalAction, mutation, query } from '../_generated/server';
3
+ import { insertInput } from './insertInput';
4
+ import { Game } from './game';
5
+ import { internal } from '../_generated/api';
6
+ import { sleep } from '../util/sleep';
7
+ import { Id } from '../_generated/dataModel';
8
+ import { ENGINE_ACTION_DURATION } from '../constants';
9
+
10
+ export async function createEngine(ctx: MutationCtx) {
11
+ const now = Date.now();
12
+ const engineId = await ctx.db.insert('engines', {
13
+ currentTime: now,
14
+ generationNumber: 0,
15
+ running: true,
16
+ });
17
+ return engineId;
18
+ }
19
+
20
+ async function loadWorldStatus(db: DatabaseReader, worldId: Id<'worlds'>) {
21
+ const worldStatus = await db
22
+ .query('worldStatus')
23
+ .withIndex('worldId', (q) => q.eq('worldId', worldId))
24
+ .unique();
25
+ if (!worldStatus) {
26
+ throw new Error(`No engine found for world ${worldId}`);
27
+ }
28
+ return worldStatus;
29
+ }
30
+
31
+ export async function startEngine(ctx: MutationCtx, worldId: Id<'worlds'>) {
32
+ const { engineId } = await loadWorldStatus(ctx.db, worldId);
33
+ const engine = await ctx.db.get(engineId);
34
+ if (!engine) {
35
+ throw new Error(`Invalid engine ID: ${engineId}`);
36
+ }
37
+ if (engine.running) {
38
+ throw new Error(`Engine ${engineId} isn't currently stopped`);
39
+ }
40
+ const now = Date.now();
41
+ const generationNumber = engine.generationNumber + 1;
42
+ await ctx.db.patch(engineId, {
43
+ // Forcibly advance time to the present. This does mean we'll skip
44
+ // simulating the time the engine was stopped, but we don't want
45
+ // to have to simulate a potentially large stopped window and send
46
+ // it down to clients.
47
+ lastStepTs: engine.currentTime,
48
+ currentTime: now,
49
+ running: true,
50
+ generationNumber,
51
+ });
52
+ await ctx.scheduler.runAfter(0, internal.aiTown.main.runStep, {
53
+ worldId: worldId,
54
+ generationNumber,
55
+ maxDuration: ENGINE_ACTION_DURATION,
56
+ });
57
+ }
58
+
59
+ export async function kickEngine(ctx: MutationCtx, worldId: Id<'worlds'>) {
60
+ const { engineId } = await loadWorldStatus(ctx.db, worldId);
61
+ const engine = await ctx.db.get(engineId);
62
+ if (!engine) {
63
+ throw new Error(`Invalid engine ID: ${engineId}`);
64
+ }
65
+ if (!engine.running) {
66
+ throw new Error(`Engine ${engineId} isn't currently running`);
67
+ }
68
+ const generationNumber = engine.generationNumber + 1;
69
+ await ctx.db.patch(engineId, { generationNumber });
70
+ await ctx.scheduler.runAfter(0, internal.aiTown.main.runStep, {
71
+ worldId: worldId,
72
+ generationNumber,
73
+ maxDuration: ENGINE_ACTION_DURATION,
74
+ });
75
+ }
76
+
77
+ export async function stopEngine(ctx: MutationCtx, worldId: Id<'worlds'>) {
78
+ const { engineId } = await loadWorldStatus(ctx.db, worldId);
79
+ const engine = await ctx.db.get(engineId);
80
+ if (!engine) {
81
+ throw new Error(`Invalid engine ID: ${engineId}`);
82
+ }
83
+ if (!engine.running) {
84
+ throw new Error(`Engine ${engineId} isn't currently running`);
85
+ }
86
+ await ctx.db.patch(engineId, { running: false });
87
+ }
88
+
89
+ export const runStep = internalAction({
90
+ args: {
91
+ worldId: v.id('worlds'),
92
+ generationNumber: v.number(),
93
+ maxDuration: v.number(),
94
+ },
95
+ handler: async (ctx, args) => {
96
+ try {
97
+ const { engine, gameState } = await ctx.runQuery(internal.aiTown.game.loadWorld, {
98
+ worldId: args.worldId,
99
+ generationNumber: args.generationNumber,
100
+ });
101
+ const game = new Game(engine, args.worldId, gameState);
102
+
103
+ let now = Date.now();
104
+ const deadline = now + args.maxDuration;
105
+ while (now < deadline) {
106
+ await game.runStep(ctx, now);
107
+ const sleepUntil = Math.min(now + game.stepDuration, deadline);
108
+ await sleep(sleepUntil - now);
109
+ now = Date.now();
110
+ }
111
+ await ctx.scheduler.runAfter(0, internal.aiTown.main.runStep, {
112
+ worldId: args.worldId,
113
+ generationNumber: game.engine.generationNumber,
114
+ maxDuration: args.maxDuration,
115
+ });
116
+ } catch (e: unknown) {
117
+ if (e instanceof ConvexError) {
118
+ if (e.data.kind === 'engineNotRunning') {
119
+ console.debug(`Engine is not running: ${e.message}`);
120
+ return;
121
+ }
122
+ if (e.data.kind === 'generationNumber') {
123
+ console.debug(`Generation number mismatch: ${e.message}`);
124
+ return;
125
+ }
126
+ }
127
+ throw e;
128
+ }
129
+ },
130
+ });
131
+
132
+ export const sendInput = mutation({
133
+ args: {
134
+ worldId: v.id('worlds'),
135
+ name: v.string(),
136
+ args: v.any(),
137
+ },
138
+ handler: async (ctx, args) => {
139
+ return await insertInput(ctx, args.worldId, args.name as any, args.args);
140
+ },
141
+ });
142
+
143
+ export const inputStatus = query({
144
+ args: {
145
+ inputId: v.id('inputs'),
146
+ },
147
+ handler: async (ctx, args) => {
148
+ const input = await ctx.db.get(args.inputId);
149
+ if (!input) {
150
+ throw new Error(`Invalid input ID: ${args.inputId}`);
151
+ }
152
+ return input.returnValue ?? null;
153
+ },
154
+ });