Builtin Library
README.md
field
Single-field component schema builder. Every public.X on a component
declares its type, default value, and Sync/NoSync replication mode in
one place via the Field.<kind>(default, mode) constructor that
produces a FieldDesc<T> descriptor. The engine walks public at
component registration, extracts each descriptor into the
per-component asset-fields + sync registries, runs each asset default
through the category-aware resolver, and replaces the descriptor with
its unwrapped default so user code reads public.X as the value (not
the descriptor).
This module is exposed as the Field, Sync, and NoSync Luau
globals via --!global directives. Component authors call
Field.<kind> and Sync / NoSync directly without a require.
--!global-types exports SyncMode, FieldDesc<T>, AssetRef<C>,
EntityRef, and ComponentRef<T> to every typed source.
Exports
Field.number(default, mode, marker?) -> FieldDesc<number>Field.range(min, max, default, mode, marker?) -> FieldDesc<number>Field.integer(min, max, default, mode, marker?) -> FieldDesc<number>Field.string(default, mode, marker?) -> FieldDesc<string>Field.enum(values, default, mode, marker?) -> FieldDesc<string>Field.bool(default, mode, marker?) -> FieldDesc<boolean>Field.vec2(default, mode, marker?) -> FieldDesc<vec2>Field.vec3(default, mode, marker?) -> FieldDesc<vec3>Field.quat(default, mode, marker?) -> FieldDesc<quat>Field.color(default, mode, marker?) -> FieldDesc<color>Field.entityRef(default, mode, marker?) -> FieldDesc<EntityRef>Field.assetRef<C>(category, default, mode, marker?) -> FieldDesc<AssetRef<C>>Field.dataRef<C>(contract, default, mode, marker?) -> FieldDesc<AssetRef<"data">>Field.componentRef<T>(componentType, default, mode, marker?) -> FieldDesc<ComponentRef<T>>Field.table<T>(default, mode, marker?) -> FieldDesc<T>Field.alias(target, description?) -> FieldDesc<any>Sync/NoSync— string singletons used as the replication-mode argument.
Every constructor's trailing marker argument accepts the bare
Serialized marker (unchanged meaning), a plain string read as the
field's description, or a FieldOptions table carrying serialized
and/or description.
Types (global via --!global-types):
SyncMode = "Sync" | "NoSync"FieldOptions = { serialized: SerializedMode?, description: string? }FieldDesc<T> = { __zero_field: boolean, kind: string, default: T?, sync: boolean, serialized: boolean?, description: string?, enforced: boolean, category: string?, componentType: string? }AssetRef<C> = { __ref: string, type: C, name: string, guid: string, identity: string, path: string, primary: string? }— the envelopeasset.resolve/asset.ref/asset.listhand back. The category is intype;primaryis the single content file the resolver found, whichgetSourcereads, falling back topath.EntityRef— the live entity proxy (entity(id)/entity.spawn()/bundle:instantiate()return it; carriesid,name,position,component, …). AnentityReffield accepts a proxy OR a raw id string on write and reads back a live proxy (.idfor the string,nilif unset); the stored/serialised form is the plain id.ComponentRef<T> = { __ref: string, entity: string, componentType: T }
Usage
-- Inside a `public` block on a component
public = {
positionX = Field.number(0, Sync),
speed = Field.number(5, Sync, "Metres per second at full throttle."),
enabled = Field.bool(true, Sync),
material = Field.assetRef("material", "@my-library::materials.gold", Sync),
source = Field.assetRef("bundle", nil, Sync),
target = Field.entityRef(nil, Sync),
aimCam = Field.componentRef("Camera", nil, NoSync),
idMap = Field.table({} :: { [string]: string }, NoSync),
}
Notes
Sync/NoSyncis REQUIRED on every constructor. Passingnil,true,false, or anything else raises a load-time error so a multiplayer-by-default engine can't silently ship single-player components.Field.assetRef'scategoryargument is inferred as a singleton string type (C). Cross-category assignment is therefore a type error:AssetRef<"mesh">does not unify withAssetRef<"material">.Field.componentRef'scomponentTypeargument works the same way — the engine validates the referent's component type at every write.Field.table<T>: pin the shape by ascribing the default,Field.table({} :: MyShape, NoSync).- A live component proxy held in a
Field.tablevalue, at any depth, is saved in the scene record as its reference: the{ __ref, entity, componentType }envelopeField.componentRefstores, so a component holding itself saves one envelope naming itself. Field.rangeandField.enumcarry aconstraintthe engine checks on the field's default and on every write, throughfield_constraints.module. A value outside the declared interval or member set is refused with the interval or the whole set named, so what the field reads back is a value its consumer can use.Field.integercarries the samerangeconstraint withwhole = true: a fraction is refused as well as a value outside the interval, for a count, a level or a group number, where a fraction would be read as some other whole number.- The engine resolves any string default for
assetRefthroughasset.resolve(identity, category)at component registration, so the first read ofpublic.<field>returns a resolved envelope, not a raw string. Field.dataRefrides the sameassetRefmachinery (category"data") plus a contract gate: every write (and the registration-time default) must resolve to a.datainstance whose dataType contract chain includescontract, checked through the generic field-constraint hook. A rejected write raises and leaves the field's previous value unchanged.- Constrained fields (
Field.dataRef) are top-level only — declaring one insideField.struct/Field.listaborts component registration with an error. FieldDesc.enforcedis true exactly for the kinds that carry aconstraint(enum,range,bitmask,dataRef,taggedRef,instantiableRef) — the field thefield_constraintshook checks on every write and default. Every other kind readsenforced = false.