From fb7fd1844e27c49ba8cbb19f26dd17d0d8ef5665 Mon Sep 17 00:00:00 2001 From: manee_too Date: Sun, 27 Sep 2026 19:42:11 +0700 Subject: [PATCH 1/5] added new 6 types into `types/index.d.ts`: Ray, Region3/-int16, Faces, Axes, Content --- types/index.d.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index f387bd7..2e9fbd4 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -21,6 +21,12 @@ declare namespace Scribe { readonly Timed: (this: void, value: T) => Schema.Timed; readonly Dynamic: (this: void, factory: (this: void) => T) => T; /** Annotate compute arguments: the enclosing template is not in scope here. */ + readonly Ray: (this: void, value: Ray) => Ray; + readonly Region3: (this: void, value: Region3) => Region3; + readonly Region3int16: (this: void, value: Region3int16) => Region3int16; + readonly Faces: (this: void, value: Faces) => Faces; + readonly Axes: (this: void, value: Axes) => Axes; + readonly Content: (this: void, value: Content) => Content; readonly Derived: (this: void, output: T, inputs: readonly string[], compute: (this: void, ...args: A) => Schema.ValueOf) => Schema.Derived; readonly Optional: (this: void, value: T) => Schema.Optional; readonly ArrayOf: (this: void, shape: T, options?: ArrayOpts) => Schema.ArrayOf; @@ -66,7 +72,6 @@ declare namespace Scribe { readonly Unpack: (this: void, name: K, bytes: buffer) => DatatypeValues[K]; readonly NONFINITE: "Scribe:nonfinite:"; }; - } interface NumberMeta { Min?: number; Max?: number; } @@ -75,6 +80,7 @@ declare namespace Scribe { CFrame: CFrame; Color3: Color3; BrickColor: BrickColor; UDim: UDim; UDim2: UDim2; Rect: Rect; NumberRange: NumberRange; NumberSequence: NumberSequence; ColorSequence: ColorSequence; DateTime: DateTime; EnumItem: EnumItem; Font: Font; PhysicalProperties: PhysicalProperties; + Ray: Ray; Region3: Region3; Region3int16: Region3int16; Faces: Faces; Axes: Axes; Content: Content; } interface FloatMeta extends NumberMeta { Precision?: number | "f32"; } interface BigMeta { Min?: number | string; Max?: number | string; } From 12869cb402602bf7e695276ddcb3865c1cc218ac Mon Sep 17 00:00:00 2001 From: manee_too Date: Sun, 27 Sep 2026 19:52:33 +0700 Subject: [PATCH 2/5] Added new 6 types into `init.luau` (Main Scribe Module): Ray, Region3/-int16, Faces, Axes, Content --- src/init.luau | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/src/init.luau b/src/init.luau index d946e71..94eff26 100644 --- a/src/init.luau +++ b/src/init.luau @@ -2896,10 +2896,11 @@ end compact packed buffer; `Get`/`Set` convert at the boundary, so your code only ever sees the real `Vector3`. - All 17 datatype declarators share this shape: + All 23 datatype declarators share this shape: `Vector3`, `Vector2`, `Vector3int16`, `Vector2int16`, `CFrame`, `Color3`, `BrickColor`, `UDim`, `UDim2`, `Rect`, `NumberRange`, `NumberSequence`, - `ColorSequence`, `DateTime`, `EnumItem`, `Font`, `PhysicalProperties`. + `ColorSequence`, `DateTime`, `EnumItem`, `Font`, `PhysicalProperties`, + `Ray`, `Region3`, `Region3int16`, `Faces`, `Axes`, `Content`. ]=] -- Declares Vector3 data, packed for storage and replication. function Scribe.Vector3(default: Vector3): Vector3 @@ -2954,6 +2955,23 @@ function Scribe.CFrame(default: CFrame, meta: CFrameMeta?): CFrame return datatypeMarker("CFrame", default, if meta then meta.Precision else nil) end +--[=[ + @within Scribe + @tag Field Types + @function Content + @param default Content + @return Content + Declares a `Content` field (`Content.fromUri` or `Content.none`), for a + player-chosen texture, image or mesh reference. + + A default (or any later `Set`) built with `Content.fromObject` is refused: + it wraps a live `Instance`, which cannot survive a DataStore round trip or + a server restart. Store an asset id/URI instead. +]=] +function Scribe.Content(default: Content): Content + return datatypeMarker("Content", default) +end + function Scribe.Color3(default: Color3): Color3 return datatypeMarker("Color3", default) end @@ -3002,6 +3020,27 @@ function Scribe.PhysicalProperties(default: PhysicalProperties): PhysicalPropert return datatypeMarker("PhysicalProperties", default) end +function Scribe.Ray(default: Ray): Ray + return datatypeMarker("Ray", default) +end + +function Scribe.Region3(default: Region3): Region3 + return datatypeMarker("Region3", default) +end + +function Scribe.Region3int16(default: Region3int16): Region3int16 + return datatypeMarker("Region3int16", default) +end + +function Scribe.Faces(default: Faces): Faces + return datatypeMarker("Faces", default) +end + +function Scribe.Axes(default: Axes): Axes + return datatypeMarker("Axes", default) +end + + --[=[ @within Scribe @tag Field Types From 9d2299363a85cd9c3eb579d954eb55379c3aa81c Mon Sep 17 00:00:00 2001 From: manee_too Date: Sun, 27 Sep 2026 20:16:23 +0700 Subject: [PATCH 3/5] Added new 6 types into `src/Internal/datatypes.luau` - Ray, Region3/-int16, Faces, Axes, Content --- src/Internal/Datatypes.luau | 190 ++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) diff --git a/src/Internal/Datatypes.luau b/src/Internal/Datatypes.luau index e13215f..7bfb759 100644 --- a/src/Internal/Datatypes.luau +++ b/src/Internal/Datatypes.luau @@ -268,6 +268,154 @@ codecs.CFrame = { end, } +-- Spatial ------------------------------------------------------------------------ +-- Ray: 24 bytes, Origin (3xf32) then Direction (3xf32). Direction is packed at +-- full magnitude, not renormalized - it carries both heading and cast length, +-- and Ray.new(origin, direction) expects that back unchanged. +codecs.Ray = { + Pack = function(value: Ray): buffer + local origin, direction = value.Origin, value.Direction + local b = buffer.create(24) + wf32(b, 0, origin.X) + wf32(b, 4, origin.Y) + wf32(b, 8, origin.Z) + wf32(b, 12, direction.X) + wf32(b, 16, direction.Y) + wf32(b, 20, direction.Z) + return b + end, + Unpack = function(b: buffer): Ray + checkLen(b, 24, "Ray") + return Ray.new( + Vector3.new(buffer.readf32(b, 0), buffer.readf32(b, 4), buffer.readf32(b, 8)), + Vector3.new(buffer.readf32(b, 12), buffer.readf32(b, 16), buffer.readf32(b, 20)) + ) + end, +} + +-- Region3: 24 bytes, Center (3xf32) then Size (3xf32). Region3 only exposes +-- .CFrame (center, axis-aligned) and .Size, not the original min/max corners, +-- so center+size is the only lossless round trip; Region3.new(min, max) is +-- rebuilt from it on Unpack. +codecs.Region3 = { + Pack = function(value: Region3): buffer + local center, size = value.CFrame.Position, value.Size + local b = buffer.create(24) + wf32(b, 0, center.X) + wf32(b, 4, center.Y) + wf32(b, 8, center.Z) + wf32(b, 12, size.X) + wf32(b, 16, size.Y) + wf32(b, 20, size.Z) + return b + end, + Unpack = function(b: buffer): Region3 + checkLen(b, 24, "Region3") + local center = Vector3.new(buffer.readf32(b, 0), buffer.readf32(b, 4), buffer.readf32(b, 8)) + local size = Vector3.new(buffer.readf32(b, 12), buffer.readf32(b, 16), buffer.readf32(b, 20)) + local half = size * 0.5 + return Region3.new(center - half, center + half) + end, +} + +-- Region3int16: 12 bytes, Min then Max, 3xi16 each. Unlike Region3, this type +-- exposes Min/Max directly, so it's an exact round trip, not a reconstruction. +codecs.Region3int16 = { + Pack = function(value: Region3int16): buffer + local min, max = value.Min, value.Max + local b = buffer.create(12) + buffer.writei16(b, 0, min.X) + buffer.writei16(b, 2, min.Y) + buffer.writei16(b, 4, min.Z) + buffer.writei16(b, 6, max.X) + buffer.writei16(b, 8, max.Y) + buffer.writei16(b, 10, max.Z) + return b + end, + Unpack = function(b: buffer): Region3int16 + checkLen(b, 12, "Region3int16") + return Region3int16.new( + Vector3int16.new(buffer.readi16(b, 0), buffer.readi16(b, 2), buffer.readi16(b, 4)), + Vector3int16.new(buffer.readi16(b, 6), buffer.readi16(b, 8), buffer.readi16(b, 10)) + ) + end, +} + +-- Flags --------------------------------------------------------------------------- + +local FACE_BITS = { + { name = "Right", bit = 1 }, + { name = "Top", bit = 2 }, + { name = "Back", bit = 4 }, + { name = "Left", bit = 8 }, + { name = "Bottom", bit = 16 }, + { name = "Front", bit = 32 }, +} + +codecs.Faces = { + Pack = function(value: Faces): buffer + local mask = 0 + for _, face in FACE_BITS do + if (value :: any)[face.name] then + mask = bit32.bor(mask, face.bit) + end + end + local b = buffer.create(1) + buffer.writeu8(b, 0, mask) + return b + end, + Unpack = function(b: buffer): Faces + checkLen(b, 1, "Faces") + local mask = buffer.readu8(b, 0) + if mask >= 64 then + error("Scribe: malformed packed Faces") + end + local members = {} + for _, face in FACE_BITS do + if bit32.btest(mask, face.bit) then + table.insert(members, (Enum.NormalId :: any)[face.name]) + end + end + return Faces.new(table.unpack(members)) + end, +} + +codecs.Axes = { + Pack = function(value: Axes): buffer + local mask = 0 + if value.X then + mask = bit32.bor(mask, 1) + end + if value.Y then + mask = bit32.bor(mask, 2) + end + if value.Z then + mask = bit32.bor(mask, 4) + end + local b = buffer.create(1) + buffer.writeu8(b, 0, mask) + return b + end, + Unpack = function(b: buffer): Axes + checkLen(b, 1, "Axes") + local mask = buffer.readu8(b, 0) + if mask >= 8 then + error("Scribe: malformed packed Axes") + end + local members = {} + if bit32.btest(mask, 1) then + table.insert(members, Enum.Axis.X) + end + if bit32.btest(mask, 2) then + table.insert(members, Enum.Axis.Y) + end + if bit32.btest(mask, 4) then + table.insert(members, Enum.Axis.Z) + end + return Axes.new(table.unpack(members)) + end, +} + -- Colors ------------------------------------------------------------------------ -- Layout: [u8 mode] then 3xu8 (mode 0, exact fromRGB colors) or 3xf32 (mode 1). @@ -598,6 +746,48 @@ codecs.PhysicalProperties = { end, } +-- Content ----------------------------------------------------------------------- + +local CONTENT_NONE = 0 +local CONTENT_URI = 1 + +codecs.Content = { + Pack = function(value: Content): buffer + if value.SourceType == Enum.ContentSourceType.None then + local b = buffer.create(1) + buffer.writeu8(b, 0, CONTENT_NONE) + return b + elseif value.SourceType == Enum.ContentSourceType.Uri then + local uri = value.Uri :: string + if #uri > 255 then + error("Scribe: Content URI too long") + end + local b = buffer.create(2 + #uri) + buffer.writeu8(b, 0, CONTENT_URI) + writeShortString(b, 1, uri) + return b + else + error( + "Scribe: cannot persist a Content built from an Instance (SourceType == Object) - " + .. "it cannot survive a DataStore round trip. Store an asset id/URI instead." + ) + end + end, + Unpack = function(b: buffer): Content + checkLen(b, 1, "Content") + local tag = buffer.readu8(b, 0) + if tag == CONTENT_NONE then + return Content.none + elseif tag == CONTENT_URI then + checkLen(b, 2, "Content") + local uri = readShortString(b, 1) + return Content.fromUri(uri) + else + error(`Scribe: malformed packed Content (unknown tag {tag})`) + end + end, +} + -------------------------------------------------------------------------------- function Datatypes.IsSupported(name: string): boolean From 4d637ebf50adb77985d8980af725e4cc4bb01e0c Mon Sep 17 00:00:00 2001 From: manee_too Date: Sun, 27 Sep 2026 20:18:12 +0700 Subject: [PATCH 4/5] hotfix: changed `17 datatypes` into `23 datatypes` --- src/init.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/init.luau b/src/init.luau index 94eff26..ab8d0ea 100644 --- a/src/init.luau +++ b/src/init.luau @@ -3045,7 +3045,7 @@ end @within Scribe @tag Field Types @prop Datatypes table - Low-level helpers for the 17 Roblox datatypes, exposed so a migration can convert + Low-level helpers for the 23 Roblox datatypes, exposed so a migration can convert a legacy representation into the packed form. | Call | Returns | From 9813a70cd00d5f67e390d6bf476ed18f09c730e5 Mon Sep 17 00:00:00 2001 From: manee_too Date: Sun, 27 Sep 2026 20:40:12 +0700 Subject: [PATCH 5/5] hotfix: format at line `3043` fixed --- src/init.luau | 1 - 1 file changed, 1 deletion(-) diff --git a/src/init.luau b/src/init.luau index ab8d0ea..7dd023e 100644 --- a/src/init.luau +++ b/src/init.luau @@ -3040,7 +3040,6 @@ function Scribe.Axes(default: Axes): Axes return datatypeMarker("Axes", default) end - --[=[ @within Scribe @tag Field Types