@driftengine/entities 4.7.2 → 4.7.4

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/dist/index.d.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  * and a future inspector all read **one** description of a component, so there is no second one to
17
17
  * keep in sync.
18
18
  */
19
- export type { ComponentStoreOptions, ComponentType, StoreSnapshot } from './store.ts';
19
+ export type { ComponentDefault, ComponentStoreOptions, ComponentType, StoreSnapshot, } from './store.ts';
20
20
  export { ComponentStore, createStoreSnapshot, defineComponent } from './store.ts';
21
21
  export type { ComponentView, ViewColumn } from './view.ts';
22
22
  export type { CursorHost } from './query.ts';
package/dist/scene.js CHANGED
@@ -108,9 +108,11 @@ export function deserializeWorld(world, scene, types) {
108
108
  const instance = {};
109
109
  for (const field of from.fields)
110
110
  instance[field.name] = saved[field.id];
111
+ /* A field the file does not have takes the value the component declares for it, and zero or
112
+ nothing where it declares none. */
111
113
  const defaults = {};
112
114
  for (const field of type.schema.fields)
113
- defaults[field.name] = field.type === 'String' ? null : 0;
115
+ defaults[field.name] = type.defaults[field.name] ?? (field.type === 'String' ? null : 0);
114
116
  const result = migrate(instance, from, type.schema, defaults);
115
117
  if (!result.migrated)
116
118
  return { loaded: false, reason: result.reason };
package/dist/schedule.js CHANGED
@@ -77,7 +77,7 @@ function group(order) {
77
77
  const groups = [];
78
78
  let current = [];
79
79
  for (const system of order) {
80
- if (current.length > 0 && current.some((other) => conflicts(other, system))) {
80
+ if (current.some((other) => conflicts(other, system))) {
81
81
  groups.push(current);
82
82
  current = [];
83
83
  }
package/dist/store.d.ts CHANGED
@@ -65,8 +65,20 @@ export interface ComponentType {
65
65
  readonly id: number;
66
66
  /** Fields typed `Entity`, which a scene load has to rewrite through its id map. */
67
67
  readonly entityFields: readonly string[];
68
+ /**
69
+ * The value each field was declared with, by field name; a field with none is absent and starts
70
+ * at zero, `false` or nothing.
71
+ *
72
+ * **Read wherever a field is not given**: an entity added with some of its fields, a prefab that
73
+ * names a few, a saved scene from before the field existed. A DriftScript component's come from
74
+ * the module's metadata (`seed: u32 = 7`); before they were read, every one of those paths
75
+ * filled zero and nothing said so.
76
+ */
77
+ readonly defaults: Readonly<Record<string, ComponentDefault>>;
68
78
  }
69
- export declare function defineComponent(schema: Schema): ComponentType;
79
+ /** A field's declared value: a number for every numeric and enum column, a flag, or a string. */
80
+ export type ComponentDefault = number | string | boolean;
81
+ export declare function defineComponent(schema: Schema, defaults?: Readonly<Record<string, ComponentDefault>>): ComponentType;
70
82
  export declare function defineComponent(name: string, fields: Readonly<Record<string, string>>): ComponentType;
71
83
  /**
72
84
  * One store's contents, for a rewind. Reused between saves; see `AllocatorSnapshot`.
package/dist/store.js CHANGED
@@ -105,7 +105,13 @@ let nextComponentId = 0;
105
105
  * caller wanting a different id scheme, at which point the id belongs entirely to whoever declares
106
106
  * the field and this function stops building one at all.
107
107
  */
108
- export function defineComponent(first, fields) {
108
+ export function defineComponent(first, second) {
109
+ const fields = typeof first === 'string'
110
+ ? second
111
+ : undefined;
112
+ const defaults = typeof first === 'string'
113
+ ? {}
114
+ : (second ?? {});
109
115
  const schema = typeof first === 'string'
110
116
  ? {
111
117
  name: first,
@@ -125,10 +131,26 @@ export function defineComponent(first, fields) {
125
131
  'about layout, so it is added here rather than assumed.');
126
132
  }
127
133
  }
134
+ /* A default names a field and fits its column, or the type is refused here, where a wrong one
135
+ is a fault in whoever declared it and not a value some later entity quietly starts with. */
136
+ for (const [name, value] of Object.entries(defaults)) {
137
+ const field = schema.fields.find((candidate) => candidate.name === name);
138
+ if (field === undefined) {
139
+ throw new Error(`\`${schema.name}\` has a default for \`${name}\`, which is not one of its fields: ` +
140
+ `${schema.fields.map((f) => `\`${f.name}\``).join(', ')}.`);
141
+ }
142
+ const inner = optionInner(field.type) ?? field.type;
143
+ const wanted = inner === 'String' ? 'string' : inner === 'bool' ? 'boolean' : 'number';
144
+ if (typeof value !== wanted) {
145
+ throw new Error(`\`${schema.name}.${name}\` is \`${field.type}\` and its default is the ${typeof value} ` +
146
+ `${JSON.stringify(value)}.`);
147
+ }
148
+ }
128
149
  return {
129
150
  name: schema.name,
130
151
  id: nextComponentId++,
131
152
  schema,
153
+ defaults: { ...defaults },
132
154
  /* An optional handle is still a handle, so a scene load has to rewrite it through the id map
133
155
  exactly as it rewrites a required one. Leaving it out would serialise a handle into a world
134
156
  whose indices mean something else. */
@@ -242,7 +264,9 @@ export class ComponentStore {
242
264
  const column = this.columns.get(field.name);
243
265
  if (column === undefined)
244
266
  continue;
245
- const given = values[field.name];
267
+ /* Omitted takes the declared value; a `null` given for an optional field is its absence. */
268
+ const own = values[field.name];
269
+ const given = own === undefined ? this.type.defaults[field.name] : own;
246
270
  if (Array.isArray(column))
247
271
  column[at] = given ?? null;
248
272
  else
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/entities",
3
- "version": "4.7.2",
3
+ "version": "4.7.4",
4
4
  "description": "Entities with generational identity, component storage, systems and scenes",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  ],
28
28
  "sideEffects": false,
29
29
  "dependencies": {
30
- "driftscript": "1.13.1"
30
+ "driftscript": "1.14.0"
31
31
  },
32
32
  "author": "Drift Technologies",
33
33
  "repository": {
package/src/index.ts CHANGED
@@ -16,7 +16,12 @@
16
16
  * and a future inspector all read **one** description of a component, so there is no second one to
17
17
  * keep in sync.
18
18
  */
19
- export type { ComponentStoreOptions, ComponentType, StoreSnapshot } from './store.ts';
19
+ export type {
20
+ ComponentDefault,
21
+ ComponentStoreOptions,
22
+ ComponentType,
23
+ StoreSnapshot,
24
+ } from './store.ts';
20
25
  export { ComponentStore, createStoreSnapshot, defineComponent } from './store.ts';
21
26
 
22
27
  export type { ComponentView, ViewColumn } from './view.ts';
package/src/scene.ts CHANGED
@@ -136,9 +136,11 @@ export function deserializeWorld(
136
136
  const instance: Record<string, unknown> = {};
137
137
  for (const field of from.fields) instance[field.name] = saved[field.id];
138
138
 
139
+ /* A field the file does not have takes the value the component declares for it, and zero or
140
+ nothing where it declares none. */
139
141
  const defaults: Record<string, unknown> = {};
140
142
  for (const field of type.schema.fields)
141
- defaults[field.name] = field.type === 'String' ? null : 0;
143
+ defaults[field.name] = type.defaults[field.name] ?? (field.type === 'String' ? null : 0);
142
144
 
143
145
  const result = migrate(instance, from, type.schema, defaults);
144
146
  if (!result.migrated) return { loaded: false, reason: result.reason };
package/src/schedule.ts CHANGED
@@ -116,7 +116,7 @@ function group(order: readonly SystemDefinition[]): (readonly SystemDefinition[]
116
116
  const groups: SystemDefinition[][] = [];
117
117
  let current: SystemDefinition[] = [];
118
118
  for (const system of order) {
119
- if (current.length > 0 && current.some((other) => conflicts(other, system))) {
119
+ if (current.some((other) => conflicts(other, system))) {
120
120
  groups.push(current);
121
121
  current = [];
122
122
  }
package/src/store.ts CHANGED
@@ -139,11 +139,27 @@ export interface ComponentType {
139
139
  readonly id: number;
140
140
  /** Fields typed `Entity`, which a scene load has to rewrite through its id map. */
141
141
  readonly entityFields: readonly string[];
142
+ /**
143
+ * The value each field was declared with, by field name; a field with none is absent and starts
144
+ * at zero, `false` or nothing.
145
+ *
146
+ * **Read wherever a field is not given**: an entity added with some of its fields, a prefab that
147
+ * names a few, a saved scene from before the field existed. A DriftScript component's come from
148
+ * the module's metadata (`seed: u32 = 7`); before they were read, every one of those paths
149
+ * filled zero and nothing said so.
150
+ */
151
+ readonly defaults: Readonly<Record<string, ComponentDefault>>;
142
152
  }
143
153
 
154
+ /** A field's declared value: a number for every numeric and enum column, a flag, or a string. */
155
+ export type ComponentDefault = number | string | boolean;
156
+
144
157
  let nextComponentId = 0;
145
158
 
146
- export function defineComponent(schema: Schema): ComponentType;
159
+ export function defineComponent(
160
+ schema: Schema,
161
+ defaults?: Readonly<Record<string, ComponentDefault>>,
162
+ ): ComponentType;
147
163
  export function defineComponent(
148
164
  name: string,
149
165
  fields: Readonly<Record<string, string>>,
@@ -168,8 +184,16 @@ export function defineComponent(
168
184
  */
169
185
  export function defineComponent(
170
186
  first: Schema | string,
171
- fields?: Readonly<Record<string, string>>,
187
+ second?: Readonly<Record<string, string>> | Readonly<Record<string, ComponentDefault>>,
172
188
  ): ComponentType {
189
+ const fields =
190
+ typeof first === 'string'
191
+ ? (second as Readonly<Record<string, string>> | undefined)
192
+ : undefined;
193
+ const defaults =
194
+ typeof first === 'string'
195
+ ? {}
196
+ : ((second as Readonly<Record<string, ComponentDefault>> | undefined) ?? {});
173
197
  const schema: Schema =
174
198
  typeof first === 'string'
175
199
  ? {
@@ -194,10 +218,31 @@ export function defineComponent(
194
218
  }
195
219
  }
196
220
 
221
+ /* A default names a field and fits its column, or the type is refused here, where a wrong one
222
+ is a fault in whoever declared it and not a value some later entity quietly starts with. */
223
+ for (const [name, value] of Object.entries(defaults)) {
224
+ const field = schema.fields.find((candidate) => candidate.name === name);
225
+ if (field === undefined) {
226
+ throw new Error(
227
+ `\`${schema.name}\` has a default for \`${name}\`, which is not one of its fields: ` +
228
+ `${schema.fields.map((f) => `\`${f.name}\``).join(', ')}.`,
229
+ );
230
+ }
231
+ const inner = optionInner(field.type) ?? field.type;
232
+ const wanted = inner === 'String' ? 'string' : inner === 'bool' ? 'boolean' : 'number';
233
+ if (typeof value !== wanted) {
234
+ throw new Error(
235
+ `\`${schema.name}.${name}\` is \`${field.type}\` and its default is the ${typeof value} ` +
236
+ `${JSON.stringify(value)}.`,
237
+ );
238
+ }
239
+ }
240
+
197
241
  return {
198
242
  name: schema.name,
199
243
  id: nextComponentId++,
200
244
  schema,
245
+ defaults: { ...defaults },
201
246
  /* An optional handle is still a handle, so a scene load has to rewrite it through the id map
202
247
  exactly as it rewrites a required one. Leaving it out would serialise a handle into a world
203
248
  whose indices mean something else. */
@@ -354,7 +399,9 @@ export class ComponentStore {
354
399
  for (const field of this.type.schema.fields) {
355
400
  const column = this.columns.get(field.name);
356
401
  if (column === undefined) continue;
357
- const given = values[field.name];
402
+ /* Omitted takes the declared value; a `null` given for an optional field is its absence. */
403
+ const own = values[field.name];
404
+ const given = own === undefined ? this.type.defaults[field.name] : own;
358
405
  if (Array.isArray(column)) column[at] = given ?? null;
359
406
  else (column as NumericColumn)[at] = typeof given === 'number' ? given : Number(given ?? 0);
360
407
  /*