Exact source used by this build. SHA-256: 1bc90d7aa7edffca141f6013283fe96017f64f929f56b5777d836b15a4391c4f.

1 import { z } from "zod";
2 import type { IntentResult, Player, SimEvent, SimRules, TilePosition, Warp, Weather, WorldConfig, WorldState } from "../types";
3 
4 export interface IntentPolicy {
5   /** Transient input may be checkpointed at the server’s bounded persistence interval. */
6   persistence?: "immediate" | "batched";
7   /** Independent, server-maintained elapsed-time budget; only accepted inputs consume it. */
8   timing?: string;
9   /** Initial credit absorbs ordinary network jitter without granting credit per packet. */
10   timingBufferMs?: number;
11   allowPaused?: boolean;
12   allowSleeping?: boolean;
13 }
14 export interface SystemContext {
15   state: WorldState;
16   rules: SimRules;
17   playerId?: string;
18   elapsedMs?: number;
19   preserveRemainder?: boolean;
20   connected?: boolean;
21   weatherOverride?: Weather;
22   positionError?(player: Player, x: number, y: number): string | undefined;
23   isWarpUnlocked?(warp: Warp): boolean;
24   allowsOutOfSeasonCrop?(areaId: string, x: number, y: number): boolean;
25   overnight(state: WorldState, events?: SimEvent[], preserveRemainder?: boolean): IntentResult;
26 }
27 export interface IntentContext extends SystemContext {
28   playerId: string; player: Player;
29   useItemTool?: (context: IntentContext, use: ItemToolUse) => IntentResult | undefined;
30   movementSpeed?: number;
31 }
32 export interface ItemToolUse { itemId: string; tile: TilePosition; targetId?: string }
33 export interface RegisteredIntent<S extends z.ZodType<{ type: string }> = z.ZodType<{ type: string }>> {
34   schema: S;
35   type: string;
36   policy: IntentPolicy;
37   run(context: IntentContext, value: unknown): IntentResult;
38 }
39 /** Handler inputs are inferred from their own schema, independent of the global union. */
40 export function defineIntent<S extends z.ZodType<{ type: string }>>(type: z.infer<S>["type"], schema: S, handle: (context: IntentContext, intent: z.infer<S>) => IntentResult, policy: IntentPolicy = {}): RegisteredIntent<S> {
41   return { type, schema, policy, run: (context, value) => handle(context, schema.parse(value)) };
42 }
43 export type HookName = "onOvernight" | "onClockTick" | "onElapsedTime" | "onPlayerAdded" | "onPresenceChange" | "onOfflineElapsed";
44 export interface SimSystem {
45   id: string;
46   worldDefaults: () => Record<string, unknown>;
47   playerDefaults: () => Record<string, unknown>;
48   intents: readonly RegisteredIntent[];
49   events: readonly string[];
50   /** Compatibility fields for the original P0 API. New systems use their own systems[id] slice. */
51   initializeWorld?: (config: WorldConfig) => Partial<WorldState>;
52   initializePlayer?: (state: WorldState, player: { id: string; name: string; areaId: string }) => Partial<Player>;
53   onOvernight?: (context: SystemContext) => IntentResult;
54   onClockTick?: (context: SystemContext) => IntentResult;
55   /** Real elapsed time while the world runs, split at calendar tick boundaries. */
56   onElapsedTime?: (context: SystemContext) => IntentResult;
57   /** Request a faster authoritative update cadence only while this system is active. */
58   updateIntervalMs?: (state: WorldState, rules: SimRules) => number | undefined;
59   onPlayerAdded?: (context: SystemContext) => IntentResult;
60   onPresenceChange?: (context: SystemContext) => IntentResult;
61   /** Server-supplied elapsed time while nobody is connected; the calendar stays frozen. */
62   onOfflineElapsed?: (context: SystemContext) => IntentResult;
63   /** Feature-owned item tools and contextual uses of existing tools. */
64   onItemTool?: (context: IntentContext, use: ItemToolUse) => IntentResult | undefined;
65   movementSpeedMultiplier?: (state: WorldState, player: Player, rules: SimRules) => number;
66   weatherOverride?: (state: WorldState, rules: SimRules) => Weather | undefined;
67   pausesClock?: (state: WorldState, rules: SimRules) => boolean;
68   /** Time to the next event that can end a calendar freeze, for exact elapsed splitting. */
69   clockPauseRemainingMs?: (state: WorldState, rules: SimRules) => number | undefined;
70   guardIntent?: (context: IntentContext, intent: { type: string; [key: string]: unknown }) => string | undefined;
71   blocksPosition?: (context: { state: WorldState; rules: SimRules; player: Player }, x: number, y: number) => string | undefined;
72   warpUnlocked?: (state: WorldState, rules: SimRules, warp: Warp) => boolean;
73   allowsOutOfSeasonCrop?: (state: WorldState, rules: SimRules, areaId: string, x: number, y: number) => boolean;
74 }
75 export function defineSystem<const S extends SimSystem>(system: S): S { return system; }
76 export type RegistryIntent<T extends readonly SimSystem[]> = z.infer<T[number]["intents"][number]["schema"]>;
77 
78 /** Recursively backfill newly added JSON fields while retaining saved values and unknown keys. */
79 function mergeDefaults(defaults: Record<string, unknown>, saved: Record<string, unknown> | undefined): Record<string, unknown> {
80   const isObject = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
81   return Object.fromEntries(Object.entries({ ...defaults, ...saved }).map(([key, value]) => [key,
82     isObject(defaults[key]) && isObject(value) ? mergeDefaults(defaults[key], value) : value]));
83 }
84 export function createSystemRegistry<const T extends readonly SimSystem[]>(systems: T) {
85   function getMovementSpeed(state: WorldState, player: Player, rules: SimRules): number {
86     return systems.reduce((speed, system) => {
87       const multiplier = system.movementSpeedMultiplier?.(state, player, rules) ?? 1;
88       return Number.isFinite(multiplier) && multiplier > 0 ? speed * multiplier : speed;
89     }, 4);
90   }
91   function useItemTool(context: IntentContext, use: ItemToolUse): IntentResult | undefined {
92     for (const system of systems) {
93       const result = system.onItemTool?.(context, use);
94       if (result) return result;
95     }
96     return undefined;
97   }
98   const handlers = new Map<string, RegisteredIntent>();
99   const ids = new Set<string>();
100   for (const system of systems) {
101     if (ids.has(system.id)) throw new Error(`Duplicate system: ${system.id}`);
102     ids.add(system.id);
103     for (const intent of system.intents) {
104       if (handlers.has(intent.type)) throw new Error(`Duplicate intent: ${intent.type}`);
105       handlers.set(intent.type, intent);
106     }
107   }
108   const schemas = [...handlers.values()].map((entry) => entry.schema);
109   // z.union needs a tuple; runtime validation and the inferred registry union share the same schemas.
110   const intentSchema = (schemas.length ? z.union(schemas as [z.ZodType<{ type: string }>, ...z.ZodType<{ type: string }>[]]) : z.never()) as z.ZodType<RegistryIntent<T>>;
111   function capabilities(state: WorldState, rules: SimRules) {
112     return {
113       weatherOverride: systems.map((system) => system.weatherOverride?.(state, rules)).find((weather) => weather !== undefined),
114       positionError: (player: Player, x: number, y: number) => {
115         for (const system of systems) { const error = system.blocksPosition?.({ state, rules, player }, x, y); if (error) return error; }
116         return undefined;
117       },
118       isWarpUnlocked: (warp: Warp) => systems.some((system) => system.warpUnlocked?.(state, rules, warp) === true),
119       allowsOutOfSeasonCrop: (areaId: string, x: number, y: number) => systems.some((system) => system.allowsOutOfSeasonCrop?.(state, rules, areaId, x, y) === true),
120     };
121   }
122   function runHook(name: HookName, state: WorldState, rules: SimRules = { crops: {} }, options: Pick<SystemContext, "playerId" | "connected" | "preserveRemainder" | "elapsedMs"> = {}): IntentResult {
123     let next = state;
124     const events: SimEvent[] = [];
125     for (const system of systems) {
126       const hook = system[name];
127       if (!hook) continue;
128       const result = hook({ ...options, ...capabilities(next, rules), state: next, rules, overnight: (value, supplied = [], preserveRemainder = false) => {
129         const night = runHook("onOvernight", value, rules, { preserveRemainder });
130         return { ...night, events: [...supplied, ...night.events] };
131       } });
132       next = result.state; events.push(...result.events);
133       if (result.error) return { state: next, events, error: result.error };
134     }
135     return { state: next, events };
136   }
137   function dispatch(state: WorldState, playerId: string, input: RegistryIntent<T>, options: { elapsedMs?: number; rules?: SimRules } = {}): IntentResult {
138     const player = state.players[playerId];
139     if (!player) return { state, events: [], error: "Unknown player" };
140     if (!player.connected) return { state, events: [], error: "Player is not connected" };
141     const parsed = intentSchema.safeParse(input);
142     if (!parsed.success) return { state, events: [], error: "Invalid intent" };
143     const entry = handlers.get(parsed.data.type);
144     if (!entry) return { state, events: [], error: "Unknown intent" };
145     if (state.paused && !entry.policy.allowPaused) return { state, events: [], error: "World is paused" };
146     if (player.sleeping && !entry.policy.allowSleeping) return { state, events: [], error: "Player is sleeping" };
147     const rules = options.rules ?? { crops: {} };
148     const context: IntentContext = { ...capabilities(state, rules), state, rules, playerId, player, elapsedMs: options.elapsedMs, useItemTool, movementSpeed: getMovementSpeed(state, player, rules), overnight: (value, supplied = [], preserveRemainder = false) => {
149       const night = runHook("onOvernight", value, rules, { preserveRemainder });
150       return { ...night, events: [...supplied, ...night.events] };
151     } };
152     for (const system of systems) {
153       const error = system.guardIntent?.(context, parsed.data);
154       if (error) return { state, events: [], error };
155     }
156     return entry.run(context, parsed.data);
157   }
158   return {
159     systems, intentSchema, dispatch, runHook, getMovementSpeed,
160     getUpdateIntervalMs: (state: WorldState, rules: SimRules, fallbackMs: number): number => {
161       if (state.paused || !Object.values(state.players).some((player) => player.connected)) return fallbackMs;
162       return systems.reduce((interval, system) => {
163         const requested = system.updateIntervalMs?.(state, rules);
164         return requested !== undefined && Number.isFinite(requested) && requested > 0 ? Math.min(interval, Math.max(100, requested)) : interval;
165       }, fallbackMs);
166     },
167     getClockPauseRemainingMs: (state: WorldState, rules: SimRules): number => systems.reduce((remaining, system) => {
168       const requested = system.clockPauseRemainingMs?.(state, rules);
169       return requested !== undefined && Number.isFinite(requested) && requested > 0 ? Math.min(remaining, requested) : remaining;
170     }, Infinity),
171     pausesClock: (state: WorldState, rules: SimRules) => systems.some((system) => system.pausesClock?.(state, rules) === true),
172     getIntentPolicy: (type: string): IntentPolicy | undefined => handlers.get(type)?.policy,
173     worldDefaults: () => Object.fromEntries(systems.map((system) => [system.id, system.worldDefaults()])),
174     playerDefaults: () => Object.fromEntries(systems.map((system) => [system.id, system.playerDefaults()])),
175     initializeState: (state: WorldState): WorldState => ({
176       ...state,
177       systems: mergeDefaults(Object.fromEntries(systems.map((system) => [system.id, system.worldDefaults()])), state.systems),
178       players: Object.fromEntries(Object.entries(state.players).map(([id, player]) => [id, { ...player,
179         systems: mergeDefaults(Object.fromEntries(systems.map((system) => [system.id, system.playerDefaults()])), player.systems),
180       }])),
181     }),
182     initializeWorld: (config: WorldConfig): Partial<WorldState> => Object.assign({}, ...systems.map((system) => system.initializeWorld?.(config) ?? {})),
183     initializePlayer: (state: WorldState, player: { id: string; name: string; areaId: string }): Partial<Player> => Object.assign({}, ...systems.map((system) => system.initializePlayer?.(state, player) ?? {})),
184   };
185 }
186 export function shouldCheckpoint(events: readonly SimEvent[]): boolean { return events.some((event) => event.checkpoint === true); }
187