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.
- package/README.md +518 -0
- package/bin/solanapolis.js +197 -0
- package/convex/_generated/api.d.ts +175 -0
- package/convex/_generated/api.js +23 -0
- package/convex/_generated/dataModel.d.ts +60 -0
- package/convex/_generated/server.d.ts +143 -0
- package/convex/_generated/server.js +93 -0
- package/convex/agent/conversation.ts +352 -0
- package/convex/agent/embeddingsCache.ts +110 -0
- package/convex/agent/memory.ts +450 -0
- package/convex/agent/schema.ts +53 -0
- package/convex/aiChat.ts +54 -0
- package/convex/aiTown/agent.ts +382 -0
- package/convex/aiTown/agentDescription.ts +27 -0
- package/convex/aiTown/agentInputs.ts +155 -0
- package/convex/aiTown/agentOperations.ts +178 -0
- package/convex/aiTown/conversation.ts +395 -0
- package/convex/aiTown/conversationMembership.ts +38 -0
- package/convex/aiTown/game.ts +371 -0
- package/convex/aiTown/ids.ts +32 -0
- package/convex/aiTown/inputHandler.ts +9 -0
- package/convex/aiTown/inputs.ts +25 -0
- package/convex/aiTown/insertInput.ts +20 -0
- package/convex/aiTown/location.ts +32 -0
- package/convex/aiTown/main.ts +154 -0
- package/convex/aiTown/movement.ts +189 -0
- package/convex/aiTown/player.ts +310 -0
- package/convex/aiTown/playerDescription.ts +35 -0
- package/convex/aiTown/schema.ts +79 -0
- package/convex/aiTown/world.ts +65 -0
- package/convex/aiTown/worldMap.ts +74 -0
- package/convex/chat.ts +79 -0
- package/convex/constants.ts +78 -0
- package/convex/convex.config.ts +6 -0
- package/convex/crons.ts +89 -0
- package/convex/engine/abstractGame.ts +199 -0
- package/convex/engine/historicalObject.ts +355 -0
- package/convex/engine/schema.ts +56 -0
- package/convex/http.ts +36 -0
- package/convex/init.ts +110 -0
- package/convex/messages.ts +53 -0
- package/convex/npcCarAgents.ts +415 -0
- package/convex/schema.ts +61 -0
- package/convex/streaming.ts +23 -0
- package/convex/testing.ts +202 -0
- package/convex/tsconfig.json +18 -0
- package/convex/util/FastIntegerCompression.ts +221 -0
- package/convex/util/assertNever.ts +4 -0
- package/convex/util/asyncMap.ts +20 -0
- package/convex/util/compression.ts +71 -0
- package/convex/util/geometry.ts +132 -0
- package/convex/util/isSimpleObject.ts +11 -0
- package/convex/util/llm.ts +724 -0
- package/convex/util/minheap.ts +38 -0
- package/convex/util/object.ts +22 -0
- package/convex/util/sleep.ts +3 -0
- package/convex/util/types.ts +33 -0
- package/convex/util/xxhash.ts +228 -0
- package/convex/world.ts +257 -0
- package/data/animations/campfire.json +45 -0
- package/data/animations/gentlesparkle.json +37 -0
- package/data/animations/gentlesplash.json +61 -0
- package/data/animations/gentlewaterfall.json +61 -0
- package/data/animations/windmill.json +78 -0
- package/data/characters.ts +121 -0
- package/data/convertMap.js +74 -0
- package/data/gentle.js +330 -0
- package/data/spritesheets/f1.ts +75 -0
- package/data/spritesheets/f2.ts +75 -0
- package/data/spritesheets/f3.ts +75 -0
- package/data/spritesheets/f4.ts +75 -0
- package/data/spritesheets/f5.ts +75 -0
- package/data/spritesheets/f6.ts +75 -0
- package/data/spritesheets/f7.ts +75 -0
- package/data/spritesheets/f8.ts +75 -0
- package/data/spritesheets/p1.ts +59 -0
- package/data/spritesheets/p2.ts +59 -0
- package/data/spritesheets/p3.ts +59 -0
- package/data/spritesheets/player.ts +59 -0
- package/data/spritesheets/types.ts +26 -0
- package/eslint.config.mjs +37 -0
- package/next.config.ts +7 -0
- package/package.json +85 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/helius-icon.svg +84 -0
- package/public/helius-logo.svg +85 -0
- package/public/next.svg +1 -0
- package/public/plane.glb +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/scripts/clear-city.ts +74 -0
- package/scripts/seed-wallets.ts +185 -0
- package/scripts/setup-webhook.ts +73 -0
- package/src/app/api/auth/callback/route.ts +6 -0
- package/src/app/api/auth/link-wallet/route.ts +6 -0
- package/src/app/api/auth/phantom/route.ts +6 -0
- package/src/app/api/broadcast-position/route.ts +59 -0
- package/src/app/api/leaderboard/route.ts +85 -0
- package/src/app/api/network-stats/route.ts +86 -0
- package/src/app/api/parcel-reward/route.ts +181 -0
- package/src/app/api/queue-status/route.ts +30 -0
- package/src/app/api/snapshots/route.ts +37 -0
- package/src/app/api/transactions/enhanced/route.ts +57 -0
- package/src/app/api/treasury/route.ts +83 -0
- package/src/app/api/wallet/[address]/balances/route.ts +124 -0
- package/src/app/api/wallet/[address]/identity/route.ts +32 -0
- package/src/app/api/wallet/[address]/route.ts +216 -0
- package/src/app/api/wallet/[address]/traded-tokens/route.ts +41 -0
- package/src/app/api/wallets/route.ts +68 -0
- package/src/app/api/webhooks/helius/route.ts +76 -0
- package/src/app/auth/callback/page.tsx +29 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +39 -0
- package/src/app/layout.tsx +43 -0
- package/src/app/page.tsx +16 -0
- package/src/components/AITownNPCs.tsx +206 -0
- package/src/components/ActivityFeed.tsx +189 -0
- package/src/components/AuthPanel.tsx +163 -0
- package/src/components/BeachScene.tsx +280 -0
- package/src/components/Building.tsx +138 -0
- package/src/components/CesiumFlight.tsx +1768 -0
- package/src/components/CesiumGlobe.tsx +616 -0
- package/src/components/CitizenCard.tsx +442 -0
- package/src/components/CitizenCardModal.tsx +153 -0
- package/src/components/CityGrid.tsx +313 -0
- package/src/components/CityLandmarks.tsx +427 -0
- package/src/components/CityScene.tsx +1289 -0
- package/src/components/CitySlotsBadge.tsx +68 -0
- package/src/components/CockpitHUD.tsx +460 -0
- package/src/components/ConvexWrapper.tsx +19 -0
- package/src/components/DubaiDistrict.tsx +630 -0
- package/src/components/FlightMiniMap.tsx +133 -0
- package/src/components/GameChat.tsx +383 -0
- package/src/components/GameHUD.tsx +393 -0
- package/src/components/Ground.tsx +14 -0
- package/src/components/HowItWorksModal.tsx +251 -0
- package/src/components/IngestionBanner.tsx +123 -0
- package/src/components/InstancedBuildings.tsx +316 -0
- package/src/components/InstancedCars.tsx +504 -0
- package/src/components/InstancedCityPlanes.tsx +259 -0
- package/src/components/InstancedHouses.tsx +246 -0
- package/src/components/InstancedLampPosts.tsx +201 -0
- package/src/components/InstancedResidentCars.tsx +357 -0
- package/src/components/InstancedRoadDashes.tsx +42 -0
- package/src/components/InstancedSkyscrapers.tsx +434 -0
- package/src/components/InstancedTrees.tsx +67 -0
- package/src/components/LeaderboardPanel.tsx +136 -0
- package/src/components/MultiplayerPlanes.tsx +128 -0
- package/src/components/NetworkStats.tsx +83 -0
- package/src/components/NewBuildingSpotlight.tsx +93 -0
- package/src/components/ParcelChallengeBanner.tsx +242 -0
- package/src/components/ParcelReward.tsx +191 -0
- package/src/components/Park.tsx +42 -0
- package/src/components/PhantomWrapper.tsx +22 -0
- package/src/components/PixelStreamViewer.tsx +335 -0
- package/src/components/PlaneMode.tsx +190 -0
- package/src/components/PlayerCar.tsx +211 -0
- package/src/components/PlayerPlane.tsx +255 -0
- package/src/components/ProjectileRenderer.tsx +249 -0
- package/src/components/QueueStatusBanner.tsx +86 -0
- package/src/components/RealPlayerTags.tsx +82 -0
- package/src/components/SceneLighting.tsx +382 -0
- package/src/components/SelectionBeam.tsx +59 -0
- package/src/components/SwapPanel.tsx +104 -0
- package/src/components/SwapParticles.tsx +237 -0
- package/src/components/TreasureGate.tsx +505 -0
- package/src/components/WalletPanel.tsx +421 -0
- package/src/components/WalletSearch.tsx +244 -0
- package/src/components/WelcomeOverlay.tsx +135 -0
- package/src/components/WindowTooltip.tsx +498 -0
- package/src/context/AuthContext.tsx +230 -0
- package/src/lib/bot-detection.ts +125 -0
- package/src/lib/building-math.ts +136 -0
- package/src/lib/building-shader.ts +253 -0
- package/src/lib/car-paths.ts +244 -0
- package/src/lib/car-system.ts +182 -0
- package/src/lib/city-constants.ts +29 -0
- package/src/lib/city-slots.ts +35 -0
- package/src/lib/city-zoning.ts +64 -0
- package/src/lib/collision-map.ts +147 -0
- package/src/lib/day-night.ts +252 -0
- package/src/lib/export-card.ts +28 -0
- package/src/lib/helius-webhook.ts +90 -0
- package/src/lib/helius.ts +74 -0
- package/src/lib/house-shader.ts +119 -0
- package/src/lib/mock-data.ts +56 -0
- package/src/lib/multiplayer-manager.ts +329 -0
- package/src/lib/plane-physics.ts +66 -0
- package/src/lib/player-car.ts +147 -0
- package/src/lib/player-plane.ts +200 -0
- package/src/lib/projectile-system.ts +272 -0
- package/src/lib/skyscraper-types.ts +52 -0
- package/src/lib/sound-engine.ts +464 -0
- package/src/lib/supabase-admin.ts +9 -0
- package/src/lib/supabase.ts +8 -0
- package/src/lib/swap-events.ts +70 -0
- package/src/middleware.ts +37 -0
- package/src/types/phantom.d.ts +16 -0
- package/src/types/wallet.ts +20 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Infer, ObjectType, v } from 'convex/values';
|
|
2
|
+
|
|
3
|
+
// `layer[position.x][position.y]` is the tileIndex or -1 if empty.
|
|
4
|
+
const tileLayer = v.array(v.array(v.number()));
|
|
5
|
+
export type TileLayer = Infer<typeof tileLayer>;
|
|
6
|
+
|
|
7
|
+
const animatedSprite = {
|
|
8
|
+
x: v.number(),
|
|
9
|
+
y: v.number(),
|
|
10
|
+
w: v.number(),
|
|
11
|
+
h: v.number(),
|
|
12
|
+
layer: v.number(),
|
|
13
|
+
sheet: v.string(),
|
|
14
|
+
animation: v.string(),
|
|
15
|
+
};
|
|
16
|
+
export type AnimatedSprite = ObjectType<typeof animatedSprite>;
|
|
17
|
+
|
|
18
|
+
export const serializedWorldMap = {
|
|
19
|
+
width: v.number(),
|
|
20
|
+
height: v.number(),
|
|
21
|
+
|
|
22
|
+
tileSetUrl: v.string(),
|
|
23
|
+
// Width & height of tileset image, px.
|
|
24
|
+
tileSetDimX: v.number(),
|
|
25
|
+
tileSetDimY: v.number(),
|
|
26
|
+
|
|
27
|
+
// Tile size in pixels (assume square)
|
|
28
|
+
tileDim: v.number(),
|
|
29
|
+
bgTiles: v.array(v.array(v.array(v.number()))),
|
|
30
|
+
objectTiles: v.array(tileLayer),
|
|
31
|
+
animatedSprites: v.array(v.object(animatedSprite)),
|
|
32
|
+
};
|
|
33
|
+
export type SerializedWorldMap = ObjectType<typeof serializedWorldMap>;
|
|
34
|
+
|
|
35
|
+
export class WorldMap {
|
|
36
|
+
width: number;
|
|
37
|
+
height: number;
|
|
38
|
+
|
|
39
|
+
tileSetUrl: string;
|
|
40
|
+
tileSetDimX: number;
|
|
41
|
+
tileSetDimY: number;
|
|
42
|
+
|
|
43
|
+
tileDim: number;
|
|
44
|
+
|
|
45
|
+
bgTiles: TileLayer[];
|
|
46
|
+
objectTiles: TileLayer[];
|
|
47
|
+
animatedSprites: AnimatedSprite[];
|
|
48
|
+
|
|
49
|
+
constructor(serialized: SerializedWorldMap) {
|
|
50
|
+
this.width = serialized.width;
|
|
51
|
+
this.height = serialized.height;
|
|
52
|
+
this.tileSetUrl = serialized.tileSetUrl;
|
|
53
|
+
this.tileSetDimX = serialized.tileSetDimX;
|
|
54
|
+
this.tileSetDimY = serialized.tileSetDimY;
|
|
55
|
+
this.tileDim = serialized.tileDim;
|
|
56
|
+
this.bgTiles = serialized.bgTiles;
|
|
57
|
+
this.objectTiles = serialized.objectTiles;
|
|
58
|
+
this.animatedSprites = serialized.animatedSprites;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
serialize(): SerializedWorldMap {
|
|
62
|
+
return {
|
|
63
|
+
width: this.width,
|
|
64
|
+
height: this.height,
|
|
65
|
+
tileSetUrl: this.tileSetUrl,
|
|
66
|
+
tileSetDimX: this.tileSetDimX,
|
|
67
|
+
tileSetDimY: this.tileSetDimY,
|
|
68
|
+
tileDim: this.tileDim,
|
|
69
|
+
bgTiles: this.bgTiles,
|
|
70
|
+
objectTiles: this.objectTiles,
|
|
71
|
+
animatedSprites: this.animatedSprites,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
package/convex/chat.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { query, mutation, internalQuery } from "./_generated/server";
|
|
2
|
+
import { StreamId } from "@convex-dev/persistent-text-streaming";
|
|
3
|
+
import { v } from "convex/values";
|
|
4
|
+
import { streamingComponent } from "./streaming";
|
|
5
|
+
|
|
6
|
+
/** List the most recent 100 chat messages, oldest first */
|
|
7
|
+
export const listMessages = query({
|
|
8
|
+
args: {},
|
|
9
|
+
handler: async (ctx) => {
|
|
10
|
+
const messages = await ctx.db
|
|
11
|
+
.query("chat_messages")
|
|
12
|
+
.order("desc")
|
|
13
|
+
.take(100);
|
|
14
|
+
return messages.reverse();
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/** Send a player chat message + create an AI response placeholder */
|
|
19
|
+
export const sendMessage = mutation({
|
|
20
|
+
args: {
|
|
21
|
+
wallet_address: v.string(),
|
|
22
|
+
display_name: v.optional(v.string()),
|
|
23
|
+
message: v.string(),
|
|
24
|
+
},
|
|
25
|
+
handler: async (ctx, args) => {
|
|
26
|
+
// Insert the player's message
|
|
27
|
+
await ctx.db.insert("chat_messages", {
|
|
28
|
+
wallet_address: args.wallet_address,
|
|
29
|
+
display_name: args.display_name,
|
|
30
|
+
message: args.message.slice(0, 280),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Create a stream for the AI response
|
|
34
|
+
const streamId = await streamingComponent.createStream(ctx);
|
|
35
|
+
|
|
36
|
+
// Insert the AI response placeholder
|
|
37
|
+
const aiMsgId = await ctx.db.insert("chat_messages", {
|
|
38
|
+
wallet_address: "helios-ai",
|
|
39
|
+
display_name: "Helios AI",
|
|
40
|
+
message: "",
|
|
41
|
+
is_ai: true,
|
|
42
|
+
responseStreamId: streamId,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return { aiMsgId, streamId };
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** Get chat history for AI context */
|
|
50
|
+
export const getHistory = internalQuery({
|
|
51
|
+
args: {},
|
|
52
|
+
handler: async (ctx) => {
|
|
53
|
+
const messages = await ctx.db
|
|
54
|
+
.query("chat_messages")
|
|
55
|
+
.order("desc")
|
|
56
|
+
.take(20);
|
|
57
|
+
|
|
58
|
+
const history: { role: "user" | "assistant"; content: string }[] = [];
|
|
59
|
+
|
|
60
|
+
for (const msg of messages.reverse()) {
|
|
61
|
+
if (msg.is_ai && msg.responseStreamId) {
|
|
62
|
+
const body = await streamingComponent.getStreamBody(
|
|
63
|
+
ctx,
|
|
64
|
+
msg.responseStreamId as StreamId,
|
|
65
|
+
);
|
|
66
|
+
if (body.text) {
|
|
67
|
+
history.push({ role: "assistant", content: body.text });
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
history.push({
|
|
71
|
+
role: "user",
|
|
72
|
+
content: `[${msg.display_name || msg.wallet_address}]: ${msg.message}`,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return history;
|
|
78
|
+
},
|
|
79
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export const ACTION_TIMEOUT = 120_000; // more time for local dev
|
|
2
|
+
// export const ACTION_TIMEOUT = 60_000;// normally fine
|
|
3
|
+
|
|
4
|
+
export const IDLE_WORLD_TIMEOUT = 5 * 60 * 1000;
|
|
5
|
+
export const WORLD_HEARTBEAT_INTERVAL = 60 * 1000;
|
|
6
|
+
|
|
7
|
+
export const MAX_STEP = 10 * 60 * 1000;
|
|
8
|
+
export const TICK = 16;
|
|
9
|
+
export const STEP_INTERVAL = 1000;
|
|
10
|
+
|
|
11
|
+
export const PATHFINDING_TIMEOUT = 60 * 1000;
|
|
12
|
+
export const PATHFINDING_BACKOFF = 1000;
|
|
13
|
+
export const CONVERSATION_DISTANCE = 1.3;
|
|
14
|
+
export const MIDPOINT_THRESHOLD = 4;
|
|
15
|
+
export const TYPING_TIMEOUT = 15 * 1000;
|
|
16
|
+
export const COLLISION_THRESHOLD = 0.75;
|
|
17
|
+
|
|
18
|
+
// How many human players can be in a world at once.
|
|
19
|
+
export const MAX_HUMAN_PLAYERS = 8;
|
|
20
|
+
|
|
21
|
+
// Don't talk to anyone for 15s after having a conversation.
|
|
22
|
+
export const CONVERSATION_COOLDOWN = 15000;
|
|
23
|
+
|
|
24
|
+
// Don't do another activity for 10s after doing one.
|
|
25
|
+
export const ACTIVITY_COOLDOWN = 10_000;
|
|
26
|
+
|
|
27
|
+
// Don't talk to a player within 60s of talking to them.
|
|
28
|
+
export const PLAYER_CONVERSATION_COOLDOWN = 60000;
|
|
29
|
+
|
|
30
|
+
// Invite 80% of invites that come from other agents.
|
|
31
|
+
export const INVITE_ACCEPT_PROBABILITY = 0.8;
|
|
32
|
+
|
|
33
|
+
// Wait for 1m for invites to be accepted.
|
|
34
|
+
export const INVITE_TIMEOUT = 60000;
|
|
35
|
+
|
|
36
|
+
// Wait for another player to say something before jumping in.
|
|
37
|
+
export const AWKWARD_CONVERSATION_TIMEOUT = 60_000; // more time locally
|
|
38
|
+
// export const AWKWARD_CONVERSATION_TIMEOUT = 20_000;
|
|
39
|
+
|
|
40
|
+
// Leave a conversation after participating too long.
|
|
41
|
+
export const MAX_CONVERSATION_DURATION = 10 * 60_000; // more time locally
|
|
42
|
+
// export const MAX_CONVERSATION_DURATION = 2 * 60_000;
|
|
43
|
+
|
|
44
|
+
// Leave a conversation if it has more than 8 messages;
|
|
45
|
+
export const MAX_CONVERSATION_MESSAGES = 8;
|
|
46
|
+
|
|
47
|
+
// Wait for 1s after sending an input to the engine. We can remove this
|
|
48
|
+
// once we can await on an input being processed.
|
|
49
|
+
export const INPUT_DELAY = 1000;
|
|
50
|
+
|
|
51
|
+
// How many memories to get from the agent's memory.
|
|
52
|
+
// This is over-fetched by 10x so we can prioritize memories by more than relevance.
|
|
53
|
+
export const NUM_MEMORIES_TO_SEARCH = 3;
|
|
54
|
+
|
|
55
|
+
// Wait for at least two seconds before sending another message.
|
|
56
|
+
export const MESSAGE_COOLDOWN = 2000;
|
|
57
|
+
|
|
58
|
+
// Don't run a turn of the agent more than once a second.
|
|
59
|
+
export const AGENT_WAKEUP_THRESHOLD = 1000;
|
|
60
|
+
|
|
61
|
+
// How old we let memories be before we vacuum them
|
|
62
|
+
export const VACUUM_MAX_AGE = 2 * 7 * 24 * 60 * 60 * 1000;
|
|
63
|
+
export const DELETE_BATCH_SIZE = 64;
|
|
64
|
+
|
|
65
|
+
export const HUMAN_IDLE_TOO_LONG = 5 * 60 * 1000;
|
|
66
|
+
|
|
67
|
+
export const ACTIVITIES = [
|
|
68
|
+
{ description: 'reading a book', emoji: '📖', duration: 60_000 },
|
|
69
|
+
{ description: 'daydreaming', emoji: '🤔', duration: 60_000 },
|
|
70
|
+
{ description: 'gardening', emoji: '🥕', duration: 60_000 },
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
export const ENGINE_ACTION_DURATION = 30000;
|
|
74
|
+
|
|
75
|
+
// Bound the number of pathfinding searches we do per game step.
|
|
76
|
+
export const MAX_PATHFINDS_PER_STEP = 16;
|
|
77
|
+
|
|
78
|
+
export const DEFAULT_NAME = 'Me';
|
package/convex/crons.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { cronJobs } from 'convex/server';
|
|
2
|
+
import { DELETE_BATCH_SIZE, IDLE_WORLD_TIMEOUT, VACUUM_MAX_AGE } from './constants';
|
|
3
|
+
import { internal } from './_generated/api';
|
|
4
|
+
import { internalMutation } from './_generated/server';
|
|
5
|
+
import { TableNames } from './_generated/dataModel';
|
|
6
|
+
import { v } from 'convex/values';
|
|
7
|
+
|
|
8
|
+
const crons = cronJobs();
|
|
9
|
+
|
|
10
|
+
crons.interval(
|
|
11
|
+
'stop inactive worlds',
|
|
12
|
+
{ seconds: IDLE_WORLD_TIMEOUT / 1000 },
|
|
13
|
+
internal.world.stopInactiveWorlds,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
crons.interval('restart dead worlds', { seconds: 60 }, internal.world.restartDeadWorlds);
|
|
17
|
+
|
|
18
|
+
crons.daily('vacuum old entries', { hourUTC: 4, minuteUTC: 20 }, internal.crons.vacuumOldEntries);
|
|
19
|
+
|
|
20
|
+
export default crons;
|
|
21
|
+
|
|
22
|
+
const TablesToVacuum: TableNames[] = [
|
|
23
|
+
// Un-comment this to also clean out old conversations.
|
|
24
|
+
// 'conversationMembers', 'conversations', 'messages',
|
|
25
|
+
|
|
26
|
+
// Inputs aren't useful unless you're trying to replay history.
|
|
27
|
+
// If you want to support that, you should add a snapshot table, so you can
|
|
28
|
+
// replay from a certain time period. Or stop vacuuming inputs and replay from
|
|
29
|
+
// the beginning of time
|
|
30
|
+
'inputs',
|
|
31
|
+
|
|
32
|
+
// We can keep memories without their embeddings for inspection, but we won't
|
|
33
|
+
// retrieve them when searching memories via vector search.
|
|
34
|
+
'memories',
|
|
35
|
+
// We can vacuum fewer tables without serious consequences, but the only
|
|
36
|
+
// one that will cause issues over time is having >>100k vectors.
|
|
37
|
+
'memoryEmbeddings',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export const vacuumOldEntries = internalMutation({
|
|
41
|
+
args: {},
|
|
42
|
+
handler: async (ctx, args) => {
|
|
43
|
+
const before = Date.now() - VACUUM_MAX_AGE;
|
|
44
|
+
for (const tableName of TablesToVacuum) {
|
|
45
|
+
console.log(`Checking ${tableName}...`);
|
|
46
|
+
const exists = await ctx.db
|
|
47
|
+
.query(tableName)
|
|
48
|
+
.withIndex('by_creation_time', (q) => q.lt('_creationTime', before))
|
|
49
|
+
.first();
|
|
50
|
+
if (exists) {
|
|
51
|
+
console.log(`Vacuuming ${tableName}...`);
|
|
52
|
+
await ctx.scheduler.runAfter(0, internal.crons.vacuumTable, {
|
|
53
|
+
tableName,
|
|
54
|
+
before,
|
|
55
|
+
cursor: null,
|
|
56
|
+
soFar: 0,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const vacuumTable = internalMutation({
|
|
64
|
+
args: {
|
|
65
|
+
tableName: v.string(),
|
|
66
|
+
before: v.number(),
|
|
67
|
+
cursor: v.union(v.string(), v.null()),
|
|
68
|
+
soFar: v.number(),
|
|
69
|
+
},
|
|
70
|
+
handler: async (ctx, { tableName, before, cursor, soFar }) => {
|
|
71
|
+
const results = await ctx.db
|
|
72
|
+
.query(tableName as TableNames)
|
|
73
|
+
.withIndex('by_creation_time', (q) => q.lt('_creationTime', before))
|
|
74
|
+
.paginate({ cursor, numItems: DELETE_BATCH_SIZE });
|
|
75
|
+
for (const row of results.page) {
|
|
76
|
+
await ctx.db.delete(row._id);
|
|
77
|
+
}
|
|
78
|
+
if (!results.isDone) {
|
|
79
|
+
await ctx.scheduler.runAfter(0, internal.crons.vacuumTable, {
|
|
80
|
+
tableName,
|
|
81
|
+
before,
|
|
82
|
+
soFar: results.page.length + soFar,
|
|
83
|
+
cursor: results.continueCursor,
|
|
84
|
+
});
|
|
85
|
+
} else {
|
|
86
|
+
console.log(`Vacuumed ${soFar + results.page.length} entries from ${tableName}`);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { ConvexError, Infer, Value, v } from 'convex/values';
|
|
2
|
+
import { Doc, Id } from '../_generated/dataModel';
|
|
3
|
+
import { ActionCtx, DatabaseReader, MutationCtx, internalQuery } from '../_generated/server';
|
|
4
|
+
import { engine } from '../engine/schema';
|
|
5
|
+
import { internal } from '../_generated/api';
|
|
6
|
+
|
|
7
|
+
export abstract class AbstractGame {
|
|
8
|
+
abstract tickDuration: number;
|
|
9
|
+
abstract stepDuration: number;
|
|
10
|
+
abstract maxTicksPerStep: number;
|
|
11
|
+
abstract maxInputsPerStep: number;
|
|
12
|
+
|
|
13
|
+
constructor(public engine: Doc<'engines'>) {}
|
|
14
|
+
|
|
15
|
+
abstract handleInput(now: number, name: string, args: object): Value;
|
|
16
|
+
abstract tick(now: number): void;
|
|
17
|
+
|
|
18
|
+
// Optional callback at the beginning of each step.
|
|
19
|
+
beginStep(now: number) {}
|
|
20
|
+
abstract saveStep(ctx: ActionCtx, engineUpdate: EngineUpdate): Promise<void>;
|
|
21
|
+
|
|
22
|
+
async runStep(ctx: ActionCtx, now: number) {
|
|
23
|
+
const inputs = await ctx.runQuery(internal.engine.abstractGame.loadInputs, {
|
|
24
|
+
engineId: this.engine._id,
|
|
25
|
+
processedInputNumber: this.engine.processedInputNumber,
|
|
26
|
+
max: this.maxInputsPerStep,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const lastStepTs = this.engine.currentTime;
|
|
30
|
+
const startTs = lastStepTs ? lastStepTs + this.tickDuration : now;
|
|
31
|
+
let currentTs = startTs;
|
|
32
|
+
let inputIndex = 0;
|
|
33
|
+
let numTicks = 0;
|
|
34
|
+
let processedInputNumber = this.engine.processedInputNumber;
|
|
35
|
+
const completedInputs = [];
|
|
36
|
+
|
|
37
|
+
this.beginStep(currentTs);
|
|
38
|
+
|
|
39
|
+
while (numTicks < this.maxTicksPerStep) {
|
|
40
|
+
numTicks += 1;
|
|
41
|
+
|
|
42
|
+
// Collect all of the inputs for this tick.
|
|
43
|
+
const tickInputs = [];
|
|
44
|
+
while (inputIndex < inputs.length) {
|
|
45
|
+
const input = inputs[inputIndex];
|
|
46
|
+
if (input.received > currentTs) {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
inputIndex += 1;
|
|
50
|
+
processedInputNumber = input.number;
|
|
51
|
+
tickInputs.push(input);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Feed the inputs to the game.
|
|
55
|
+
for (const input of tickInputs) {
|
|
56
|
+
let returnValue;
|
|
57
|
+
try {
|
|
58
|
+
const value = this.handleInput(currentTs, input.name, input.args);
|
|
59
|
+
returnValue = { kind: 'ok' as const, value };
|
|
60
|
+
} catch (e: any) {
|
|
61
|
+
console.error(`Input ${input._id} failed: ${e.message}`);
|
|
62
|
+
returnValue = { kind: 'error' as const, message: e.message };
|
|
63
|
+
}
|
|
64
|
+
completedInputs.push({ inputId: input._id, returnValue });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Simulate the game forward one tick.
|
|
68
|
+
this.tick(currentTs);
|
|
69
|
+
|
|
70
|
+
const candidateTs = currentTs + this.tickDuration;
|
|
71
|
+
if (now < candidateTs) {
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
currentTs = candidateTs;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Commit the step by moving time forward, consuming our inputs, and saving the game's state.
|
|
78
|
+
const expectedGenerationNumber = this.engine.generationNumber;
|
|
79
|
+
this.engine.currentTime = currentTs;
|
|
80
|
+
this.engine.lastStepTs = lastStepTs;
|
|
81
|
+
this.engine.generationNumber += 1;
|
|
82
|
+
this.engine.processedInputNumber = processedInputNumber;
|
|
83
|
+
const { _id, _creationTime, ...engine } = this.engine;
|
|
84
|
+
const engineUpdate = { engine, completedInputs, expectedGenerationNumber };
|
|
85
|
+
await this.saveStep(ctx, engineUpdate);
|
|
86
|
+
|
|
87
|
+
console.debug(`Simulated from ${startTs} to ${currentTs} (${currentTs - startTs}ms)`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const completedInput = v.object({
|
|
92
|
+
inputId: v.id('inputs'),
|
|
93
|
+
returnValue: v.union(
|
|
94
|
+
v.object({
|
|
95
|
+
kind: v.literal('ok'),
|
|
96
|
+
value: v.any(),
|
|
97
|
+
}),
|
|
98
|
+
v.object({
|
|
99
|
+
kind: v.literal('error'),
|
|
100
|
+
message: v.string(),
|
|
101
|
+
}),
|
|
102
|
+
),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
export const engineUpdate = v.object({
|
|
106
|
+
engine,
|
|
107
|
+
expectedGenerationNumber: v.number(),
|
|
108
|
+
completedInputs: v.array(completedInput),
|
|
109
|
+
});
|
|
110
|
+
export type EngineUpdate = Infer<typeof engineUpdate>;
|
|
111
|
+
|
|
112
|
+
export async function loadEngine(
|
|
113
|
+
db: DatabaseReader,
|
|
114
|
+
engineId: Id<'engines'>,
|
|
115
|
+
generationNumber: number,
|
|
116
|
+
) {
|
|
117
|
+
const engine = await db.get(engineId);
|
|
118
|
+
if (!engine) {
|
|
119
|
+
throw new Error(`No engine found with id ${engineId}`);
|
|
120
|
+
}
|
|
121
|
+
if (!engine.running) {
|
|
122
|
+
throw new ConvexError({
|
|
123
|
+
kind: 'engineNotRunning',
|
|
124
|
+
message: `Engine ${engineId} is not running`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (engine.generationNumber !== generationNumber) {
|
|
128
|
+
throw new ConvexError({ kind: 'generationNumber', message: 'Generation number mismatch' });
|
|
129
|
+
}
|
|
130
|
+
return engine;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function engineInsertInput(
|
|
134
|
+
ctx: MutationCtx,
|
|
135
|
+
engineId: Id<'engines'>,
|
|
136
|
+
name: string,
|
|
137
|
+
args: any,
|
|
138
|
+
): Promise<Id<'inputs'>> {
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
const prevInput = await ctx.db
|
|
141
|
+
.query('inputs')
|
|
142
|
+
.withIndex('byInputNumber', (q) => q.eq('engineId', engineId))
|
|
143
|
+
.order('desc')
|
|
144
|
+
.first();
|
|
145
|
+
const number = prevInput ? prevInput.number + 1 : 0;
|
|
146
|
+
const inputId = await ctx.db.insert('inputs', {
|
|
147
|
+
engineId,
|
|
148
|
+
number,
|
|
149
|
+
name,
|
|
150
|
+
args,
|
|
151
|
+
received: now,
|
|
152
|
+
});
|
|
153
|
+
return inputId;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export const loadInputs = internalQuery({
|
|
157
|
+
args: {
|
|
158
|
+
engineId: v.id('engines'),
|
|
159
|
+
processedInputNumber: v.optional(v.number()),
|
|
160
|
+
max: v.number(),
|
|
161
|
+
},
|
|
162
|
+
handler: async (ctx, args) => {
|
|
163
|
+
return await ctx.db
|
|
164
|
+
.query('inputs')
|
|
165
|
+
.withIndex('byInputNumber', (q) =>
|
|
166
|
+
q.eq('engineId', args.engineId).gt('number', args.processedInputNumber ?? -1),
|
|
167
|
+
)
|
|
168
|
+
.order('asc')
|
|
169
|
+
.take(args.max);
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
export async function applyEngineUpdate(
|
|
174
|
+
ctx: MutationCtx,
|
|
175
|
+
engineId: Id<'engines'>,
|
|
176
|
+
update: EngineUpdate,
|
|
177
|
+
) {
|
|
178
|
+
const engine = await loadEngine(ctx.db, engineId, update.expectedGenerationNumber);
|
|
179
|
+
if (
|
|
180
|
+
engine.currentTime &&
|
|
181
|
+
update.engine.currentTime &&
|
|
182
|
+
update.engine.currentTime < engine.currentTime
|
|
183
|
+
) {
|
|
184
|
+
throw new Error('Time moving backwards');
|
|
185
|
+
}
|
|
186
|
+
await ctx.db.replace(engine._id, update.engine);
|
|
187
|
+
|
|
188
|
+
for (const completedInput of update.completedInputs) {
|
|
189
|
+
const input = await ctx.db.get(completedInput.inputId);
|
|
190
|
+
if (!input) {
|
|
191
|
+
throw new Error(`Input ${completedInput.inputId} not found`);
|
|
192
|
+
}
|
|
193
|
+
if (input.returnValue) {
|
|
194
|
+
throw new Error(`Input ${completedInput.inputId} already completed`);
|
|
195
|
+
}
|
|
196
|
+
input.returnValue = completedInput.returnValue;
|
|
197
|
+
await ctx.db.replace(input._id, input);
|
|
198
|
+
}
|
|
199
|
+
}
|