Exact source used by this build. SHA-256: 6d9cde07cff07016e6c057bbb1bf45eaa750039a32f813f203205fd4af05da77.
1 import { z } from "zod";
2 import type { Area, Clock, Intent, Player, SimEvent, Tile, Wallet, Weather, WorldState } from "./types";
3
4 import { FacingSchema, TilePositionSchema } from "./systems/schema";
5 import { IntentSchema } from "./systems";
6 export { FacingSchema, TilePositionSchema, IntentSchema };
7 export const SystemStateSchema = z.record(z.string(), z.unknown());
8 export const WeatherSchema = z.enum(["sunny", "rain", "storm", "wind", "snow"]);
9 export const WarpSchema = z.object({ id: z.string(), x: z.number(), y: z.number(), width: z.number().positive(), height: z.number().positive(), targetAreaId: z.string(), targetX: z.number(), targetY: z.number(), locked: z.boolean(), label: z.string(), kind: z.string().optional() });
10 export const SeasonAppearanceSchema = z.object({ tint: z.number(), groundCover: z.string(), groundColor: z.number(), foliageColor: z.number() });
11 export const SeasonSchema = z.enum(["spring", "summer", "autumn", "winter"]);
12 export const CropInstanceSchema = z.object({ cropId: z.string().min(1), stage: z.number().int().nonnegative(), daysInStage: z.number().int().nonnegative(), ready: z.boolean(), regrowing: z.boolean() });
13 export const TileSchema = z.object({ ground: z.string(), tillable: z.boolean(), tilled: z.boolean(), watered: z.boolean(), objectId: z.string().nullable(), walkableObject: z.boolean().optional(), crop: CropInstanceSchema.nullable() });
14 export const LandmarkSchema = z.object({ id: z.string(), kind: z.literal("home"), x: z.number().int().nonnegative(), y: z.number().int().nonnegative(), width: z.number().int().positive(), height: z.number().int().positive() });
15 export const PointOfInterestSchema = z.object({ id: z.string(), kind: z.string(), x: z.number(), y: z.number(), width: z.number().nonnegative(), height: z.number().nonnegative() });
16 export const AreaSchema = z.object({ id: z.string(), width: z.number().int().positive(), height: z.number().int().positive(), tiles: z.array(TileSchema), clockRateMsPerGameMinute: z.number().positive(), spawn: TilePositionSchema, landmarks: z.array(LandmarkSchema), pois: z.array(PointOfInterestSchema).optional(), name: z.string().optional(), outdoor: z.boolean(), locked: z.boolean(), collision: z.array(z.boolean()), groundTiles: z.array(z.number()), decorTiles: z.array(z.number()), warps: z.array(WarpSchema), seasons: z.object({ spring: SeasonAppearanceSchema, summer: SeasonAppearanceSchema, autumn: SeasonAppearanceSchema, winter: SeasonAppearanceSchema }).optional(), systems: SystemStateSchema });
17 export const PlayerSchema = z.object({
18 id: z.string(), name: z.string(), areaId: z.string(), x: z.number(), y: z.number(), facing: FacingSchema,
19 energy: z.number().nonnegative(), maxEnergy: z.number().positive(), inventory: z.record(z.string(), z.number().int().nonnegative()),
20 connected: z.boolean(), sleeping: z.boolean(), sleepConfirmedAt: z.number().int().nullable(), passedOut: z.boolean(), systems: SystemStateSchema,
21 });
22 export const ClockSchema = z.object({ year: z.number().int().positive(), season: SeasonSchema, day: z.number().int().min(1).max(28), weekday: z.number().int().min(0).max(6), minuteOfDay: z.number().int(), realMsRemainder: z.number().nonnegative(), machineMinutes: z.number().int().nonnegative() });
23 export const WalletLedgerEntrySchema = z.object({ fromPlayerId: z.string(), toPlayerId: z.string(), amount: z.number().int().positive() });
24 export const WalletSchema = z.object({ mode: z.enum(["shared", "separate"]), shared: z.number().int().nonnegative(), balances: z.record(z.string(), z.number().int().nonnegative()), ledger: z.array(WalletLedgerEntrySchema) });
25 export const StarterPlayerSchema = z.object({ areaId: z.string(), x: z.number(), y: z.number(), facing: FacingSchema, inventory: z.record(z.string(), z.number().int().nonnegative()) });
26 export const WorldStateSchema = z.object({ revision: z.number().int().nonnegative(), clock: ClockSchema, areas: z.record(z.string(), AreaSchema), players: z.record(z.string(), PlayerSchema), wallet: WalletSchema, rngState: z.number().int().nonnegative(), paused: z.boolean(), starterPlayer: StarterPlayerSchema, weather: WeatherSchema, systems: SystemStateSchema });
27 export const SimEventSchema = z.object({ transient: z.boolean().optional(), type: z.string().min(1), playerId: z.string().optional(), areaId: z.string().optional(), message: z.string().optional(), checkpoint: z.boolean().optional(), snapshot: z.boolean().optional(), data: z.record(z.string(), z.unknown()).optional() });
28 export const ClientMessageSchema = z.discriminatedUnion("type", [
29 z.object({ type: z.literal("join"), inviteCode: z.string().min(1), token: z.string().min(1).optional(), enrollmentSecret: z.string().regex(/^[a-f0-9]{64}$/).optional(), name: z.string().min(1).max(32) }),
30 z.object({ type: z.literal("intent"), intent: IntentSchema }),
31 ]);
32 export type ClientMessage = z.infer<typeof ClientMessageSchema>;
33
34 export interface TilePatch { areaId: string; index: number; tile: Tile }
35 export interface WorldPatch {
36 type: "patch";
37 revision: number;
38 players?: Record<string, Player>;
39 tiles?: TilePatch[];
40 clock?: Clock;
41 wallet?: Wallet;
42 paused?: boolean;
43 weather?: Weather;
44 rngState?: number;
45 systems?: Record<string, unknown>;
46 areas?: Record<string, Area>;
47 /** Mutable per-area system slices never repeat static tile maps. */
48 areaSystems?: Record<string, Record<string, unknown>>;
49 removedAreas?: string[];
50 }
51 export type ServerMessage =
52 | { type: "joined"; playerId: string; token: string }
53 | { type: "snapshot"; state: WorldState }
54 | WorldPatch
55 | { type: "event"; event: SimEvent }
56 | { type: "clock"; clock: Clock }
57 | { type: "error"; message: string };
58
59 export const ServerMessageSchema = z.discriminatedUnion("type", [
60 z.object({ type: z.literal("joined"), playerId: z.string(), token: z.string() }),
61 z.object({ type: z.literal("snapshot"), state: WorldStateSchema }),
62 z.object({ type: z.literal("patch"), revision: z.number().int(), players: z.record(z.string(), PlayerSchema).optional(), tiles: z.array(z.object({ areaId: z.string(), index: z.number().int().nonnegative(), tile: TileSchema })).optional(), clock: ClockSchema.optional(), wallet: WalletSchema.optional(), paused: z.boolean().optional(), weather: WeatherSchema.optional(), rngState: z.number().int().nonnegative().optional(), systems: SystemStateSchema.optional(), areas: z.record(z.string(), AreaSchema).optional(), areaSystems: z.record(z.string(), SystemStateSchema).optional(), removedAreas: z.array(z.string()).optional() }),
63 z.object({ type: z.literal("event"), event: SimEventSchema }),
64 z.object({ type: z.literal("clock"), clock: ClockSchema }),
65 z.object({ type: z.literal("error"), message: z.string() }),
66 ]);
67
68 export function buildPatch(before: WorldState, after: WorldState): WorldPatch {
69 const tiles: TilePatch[] = [];
70 const areas: Record<string, Area> = {};
71 const areaSystems: Record<string, Record<string, unknown>> = {};
72 for (const [areaId, area] of Object.entries(after.areas)) {
73 const old = before.areas[areaId];
74 if (!old || (Object.keys(area) as (keyof Area)[]).some((key) => key !== "tiles" && key !== "systems" && old[key] !== area[key])) { areas[areaId] = area; continue; }
75 if (old.systems !== area.systems) areaSystems[areaId] = area.systems;
76 if (area === old || area.tiles === old.tiles) continue;
77 area.tiles.forEach((tile, index) => { if (old.tiles[index] !== tile) tiles.push({ areaId, index, tile }); });
78 }
79 const patch: WorldPatch = { type: "patch", revision: after.revision };
80 // Player records are tiny (two slots) and a full replacement avoids stale presences.
81 if (before.players !== after.players) patch.players = after.players;
82 if (tiles.length) patch.tiles = tiles;
83 if (Object.keys(areas).length) patch.areas = areas;
84 if (Object.keys(areaSystems).length) patch.areaSystems = areaSystems;
85 const removedAreas = Object.keys(before.areas).filter((id) => !after.areas[id]);
86 if (removedAreas.length) patch.removedAreas = removedAreas;
87 if (before.systems !== after.systems) patch.systems = after.systems;
88 if (before.weather !== after.weather) patch.weather = after.weather;
89 if (before.rngState !== after.rngState) patch.rngState = after.rngState;
90 if (before.clock !== after.clock) patch.clock = after.clock;
91 if (before.wallet !== after.wallet) patch.wallet = after.wallet;
92 if (before.paused !== after.paused) patch.paused = after.paused;
93 return patch;
94 }
95
96 export function applyPatch(state: WorldState, patch: WorldPatch): WorldState {
97 const areas = { ...state.areas, ...patch.areas };
98 for (const id of patch.removedAreas ?? []) delete areas[id];
99 for (const [id, systems] of Object.entries(patch.areaSystems ?? {})) if (areas[id]) areas[id] = { ...areas[id], systems };
100 for (const update of patch.tiles ?? []) {
101 const area = areas[update.areaId];
102 if (!area || update.index >= area.tiles.length) continue;
103 const tiles = area.tiles.slice(); tiles[update.index] = update.tile; areas[update.areaId] = { ...area, tiles };
104 }
105 return { ...state, revision: patch.revision, areas, players: patch.players ?? state.players, clock: patch.clock ?? state.clock, wallet: patch.wallet ?? state.wallet, paused: patch.paused ?? state.paused, weather: patch.weather ?? state.weather, rngState: patch.rngState ?? state.rngState, systems: patch.systems ?? state.systems };
106 }
107
108 // Compile-time check that validated intent messages line up with the simulation union.
109 export const asIntent = (intent: z.infer<typeof IntentSchema>): Intent => intent;
110 /** Ensures Zod's validated server payload remains assignable to the public protocol type. */
111 export const asServerMessage = (message: z.infer<typeof ServerMessageSchema>): ServerMessage => message;
112 /** Ensures every public server payload remains accepted by the Zod schema type. */
113 export const asValidatedServerMessage = (message: ServerMessage): z.infer<typeof ServerMessageSchema> => message;
114