--!strict

local BufferStreamStatic = {}

local BufferStreamClass = {}
BufferStreamClass.__index = BufferStreamClass
BufferStreamClass.ClassName = "BufferStream"

export type BufferStream = typeof(setmetatable(
	{} :: {
		b: buffer,
		cursor: number,
		length: number,
	},
	BufferStreamClass
))

function BufferStreamStatic.new(b: buffer)
	local self = setmetatable({}, BufferStreamClass) :: BufferStream

	self.b = b
	self.cursor = 0
	self.length = buffer.len(b)

	return self
end

-- Private

local function readUnsignedBytes(b: buffer, offset: number, bytes: number)
	-- selene: allow(incorrect_standard_library_use)
	return buffer.readbits(b, offset * 8, bytes * 8)
end

local function writeUnsignedBytes(b: buffer, offset: number, bytes: number, value: number)
	-- selene: allow(incorrect_standard_library_use)
	return buffer.writebits(b, offset * 8, bytes * 8, value)
end

local function cursorWrite<T...>(self: BufferStream, bytes: number, callback: (buffer, number, T...) -> (), ...: T...)
	local newCursor = self.cursor + bytes
	if newCursor > self.length then
		self:expand(newCursor - self.length)
	end
	callback(self.b, self.cursor, ...)
	self.cursor = newCursor
end

-- Methods

function BufferStreamClass.expand(self: BufferStream, bytes: number)
	self.length = self.length + bytes
	local expanded = buffer.create(self.length)
	buffer.copy(expanded, 0, self.b)
	self.b = expanded
end

function BufferStreamClass.readUnsignedBytes(self: BufferStream, bytes: number)
	local result = readUnsignedBytes(self.b, self.cursor, bytes)
	self.cursor = self.cursor + bytes
	return result
end

function BufferStreamClass.writeUnsignedBytes(self: BufferStream, bytes: number, value: number)
	cursorWrite(self, bytes, writeUnsignedBytes, bytes, value)
end

function BufferStreamClass.readu8(self: BufferStream)
	local result = buffer.readu8(self.b, self.cursor)
	self.cursor = self.cursor + 1
	return result
end

function BufferStreamClass.writeu8(self: BufferStream, u8: number)
	cursorWrite(self, 1, buffer.writeu8, u8)
end

function BufferStreamClass.readu16(self: BufferStream)
	local result = buffer.readu16(self.b, self.cursor)
	self.cursor = self.cursor + 2
	return result
end

function BufferStreamClass.writeu16(self: BufferStream, u16: number)
	cursorWrite(self, 2, buffer.writeu16, u16)
end

function BufferStreamClass.readu32(self: BufferStream)
	local result = buffer.readu32(self.b, self.cursor)
	self.cursor = self.cursor + 4
	return result
end

function BufferStreamClass.writeu32(self: BufferStream, u32: number)
	cursorWrite(self, 4, buffer.writeu32, u32)
end

function BufferStreamClass.readi8(self: BufferStream)
	local result = buffer.readi8(self.b, self.cursor)
	self.cursor = self.cursor + 1
	return result
end

function BufferStreamClass.writei8(self: BufferStream, i8: number)
	cursorWrite(self, 1, buffer.writei8, i8)
end

function BufferStreamClass.readi16(self: BufferStream)
	local result = buffer.readi16(self.b, self.cursor)
	self.cursor = self.cursor + 2
	return result
end

function BufferStreamClass.writei16(self: BufferStream, i16: number)
	cursorWrite(self, 2, buffer.writei16, i16)
end

function BufferStreamClass.readi32(self: BufferStream)
	local result = buffer.readi32(self.b, self.cursor)
	self.cursor = self.cursor + 4
	return result
end

function BufferStreamClass.writei32(self: BufferStream, i32: number)
	cursorWrite(self, 4, buffer.writei32, i32)
end

function BufferStreamClass.readf32(self: BufferStream)
	local result = buffer.readf32(self.b, self.cursor)
	self.cursor = self.cursor + 4
	return result
end

function BufferStreamClass.writef32(self: BufferStream, f32: number)
	cursorWrite(self, 4, buffer.writef32, f32)
end

function BufferStreamClass.readf64(self: BufferStream)
	local result = buffer.readf64(self.b, self.cursor)
	self.cursor = self.cursor + 8
	return result
end

function BufferStreamClass.writef64(self: BufferStream, f64: number)
	cursorWrite(self, 8, buffer.writef64, f64)
end

function BufferStreamClass.readString(self: BufferStream, bytes: number)
	local result = buffer.readstring(self.b, self.cursor, bytes)
	self.cursor = self.cursor + bytes
	return result
end

function BufferStreamClass.writeString(self: BufferStream, value: string, count: number?)
	local length = (count or #value)
	cursorWrite(self, length, buffer.writestring, value, length)
end

function BufferStreamClass.readBuffer(self: BufferStream, bytes: number)
	local b = buffer.create(bytes)
	buffer.copy(b, 0, self.b, self.cursor, bytes)
	self.cursor = self.cursor + bytes
	return b
end

function BufferStreamClass.writeBuffer(self: BufferStream, b: buffer, offset: number?, count: number?)
	local length = count or buffer.len(b)
	cursorWrite(self, length, buffer.copy, b, offset or 0, length)
end

function BufferStreamClass.readTypeId(self: BufferStream)
	return self:readu8()
end

function BufferStreamClass.writeTypeId(self: BufferStream, typeId: number)
	self:writeu8(typeId)
end

--

return BufferStreamStatic


local dataTypesRoot = script.Parent
local TypeIds = require(dataTypesRoot.TypeIds)
local BufferStream = require(dataTypesRoot.Parent.BufferStream)

local DataTypeDefinitionStatic = {}

local DataTypeDefinitionClass = {}
DataTypeDefinitionClass.__index = DataTypeDefinitionClass
DataTypeDefinitionClass.ClassName = "DataTypeDefinition"

export type BufferStream = BufferStream.BufferStream

type Definition<T> = {
	write: (BufferStream, T) -> (),
	read: (BufferStream) -> T,
}

export type DataTypeDefinition<T> = typeof(setmetatable(
	{} :: {
		typeId: number,
		typeOf: string,
		typing: string,
		definition: Definition<T>,
		overridable: boolean,
		support: "implemented" | "unimplemented" | "never",
	},
	DataTypeDefinitionClass
))

function DataTypeDefinitionStatic.new<T>(typeOf: string, definition: Definition<T>)
	local self = setmetatable({}, DataTypeDefinitionClass) :: DataTypeDefinition<T>

	self.typeId = TypeIds.assert(typeOf)
	self.typeOf = typeOf
	self.typing = typeOf
	self.definition = table.clone(definition)
	self.overridable = true
	self.support = "implemented"

	return self
end

function DataTypeDefinitionStatic.unimplemented(typeOf: string)
	local definition = DataTypeDefinitionStatic.new(typeOf, {
		read = function(_stream)
			error(`Reading the '{typeOf}' type is not supported.`)
			return nil :: any
		end,
		write = function(_stream, _value)
			error(`Writing the '{typeOf}' type is not supported.`)
		end,
	})

	definition.overridable = false
	definition.support = "unimplemented"

	return definition
end

function DataTypeDefinitionStatic.never(typeOf: string)
	local definition = DataTypeDefinitionStatic.unimplemented(typeOf)
	definition.support = "never"
	return definition
end

function DataTypeDefinitionStatic.createReader<T...>(readsById: { [number]: (BufferStream) -> T... })
	return function(stream: BufferStream)
		local readId = stream:readu8()
		return readsById[readId](stream)
	end
end

function DataTypeDefinitionClass.isOverridable<T>(self: DataTypeDefinition<T>)
	return self.overridable
end

function DataTypeDefinitionClass.readStream<T>(self: DataTypeDefinition<T>, stream: BufferStream)
	local typeId = stream:readTypeId()
	assert(typeId == self.typeId, "TypeId does not match.")
	return self.definition.read(stream)
end

function DataTypeDefinitionClass.writeStream<T>(self: DataTypeDefinition<T>, stream: BufferStream, value: T)
	stream:writeTypeId(self.typeId)
	self.definition.write(stream, value)
end

return DataTypeDefinitionStatic


local dataTypesRoot = script.Parent
local BufferStream = require(dataTypesRoot.Parent.BufferStream)

local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)
local UserdataDefinition = require(dataTypesRoot.Standalone.Userdata.UserdataDefinition)

local DataTypePackerStatic = {}

local DataTypePackerClass = {}
DataTypePackerClass.__index = DataTypePackerClass
DataTypePackerClass.ClassName = "DataTypePacker"

export type DataTypeDefinition<T> = DataTypeDefinition.DataTypeDefinition<T>

export type DataTypePacker = typeof(setmetatable(
	{} :: {
		definitionsByTypeOf: { [string]: DataTypeDefinition<any> },
		definitionsByTypeId: { [number]: DataTypeDefinition<any> },
	},
	DataTypePackerClass
))

function DataTypePackerStatic.new(inputs: { DataTypeDefinition<any> | DataTypePacker })
	local self = setmetatable({}, DataTypePackerClass) :: DataTypePacker

	self.definitionsByTypeOf = {}
	self.definitionsByTypeId = {}

	local definitions: { DataTypeDefinition<any> } = {}
	for _, input in inputs do
		local className: string? = if typeof(input) == "table" then input.ClassName else nil
		if className == "DataTypeDefinition" then
			table.insert(definitions, input :: DataTypeDefinition<any>)
		elseif className == "DataTypePacker" then
			local packer = input :: DataTypePacker
			for _, definition in packer.definitionsByTypeOf do
				table.insert(definitions, definition :: DataTypeDefinition.DataTypeDefinition<any>)
			end
		end
	end

	for _, definition in definitions do
		local typeOf = definition.typeOf
		assert(not self.definitionsByTypeOf[typeOf], `Overlapping TypeName for '{typeOf}'`)
		self.definitionsByTypeOf[typeOf] = definition :: any

		local typeId = definition.typeId
		assert(not self.definitionsByTypeId[typeId], `Overlapping TypeId for '{typeId}'.`)
		self.definitionsByTypeId[typeId] = definition :: any
	end

	return self
end

function DataTypePackerStatic.packModules(instances: { Instance })
	local inputs = {}
	for _, instance in instances do
		if instance:IsA("ModuleScript") and instance.Name:sub(-5) ~= ".spec" then
			table.insert(inputs, require(instance) :: any)
		end
	end
	return DataTypePackerStatic.new(inputs)
end

-- Methods

function DataTypePackerClass.readStream(self: DataTypePacker, stream: BufferStream.BufferStream)
	local prevCursor = stream.cursor
	local typeId = stream:readTypeId()
	stream.cursor = prevCursor
	local definition = assert(self.definitionsByTypeId[typeId], `No type definition exists for typeId '{typeId}'.`)
	return definition:readStream(stream)
end

function DataTypePackerClass.writeStream(self: DataTypePacker, stream: BufferStream.BufferStream, value: any)
	local typeofValue = typeof(value)
	if typeofValue == "userdata" then
		local subTypeOf = UserdataDefinition.subTypeOf(value)
		if subTypeOf then
			typeofValue = "userdata:" .. subTypeOf
		end
	end

	local definition =
		assert(self.definitionsByTypeOf[typeofValue], `No type definition exists for type '{typeofValue}'.`)
	definition:writeStream(stream, value)
end

return DataTypePackerStatic


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypePacker = require(dataTypesRoot.DataTypePacker)

return DataTypePacker.packModules(script:GetChildren())


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local BufferStream = require(dataTypesRoot.Parent.BufferStream)
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)
local StandaloneDataTypes = require(dataTypesRoot.Standalone)
local TableHelper = require(dataTypesRoot.Helpers.TableHelper)

type BufferStream = BufferStream.BufferStream

local tbl_u8TypeId = 1
local tbl_u16TypeId = 2
local tbl_u32TypeId = 3

local tblRef_u8TypeId = 4
local tblRef_u16TypeId = 5
local tblRef_u32TypeId = 6

local arr_u8TypeId = 7
local arr_u16TypeId = 8
local arr_u32TypeId = 9
local dict_u8TypeId = 10
local dict_u16TypeId = 11
local dict_u32TypeId = 12

local internalTypeId = 13
local standaloneTypeId = 14

local function createUnsignedReadWrite(u8TypeId: number, u16TypeId: number, u32TypeId: number)
	local reader = DataTypeDefinition.createReader({
		[u8TypeId] = function(stream)
			return stream:readu8()
		end,
		[u16TypeId] = function(stream)
			return stream:readu16()
		end,
		[u32TypeId] = function(stream)
			return stream:readu32()
		end,
	})

	local function writer(stream: BufferStream, unsigned: number)
		if unsigned <= 0xFF then
			stream:writeu8(u8TypeId)
			stream:writeu8(unsigned)
		elseif unsigned <= 0xFFFF then
			stream:writeu8(u16TypeId)
			stream:writeu16(unsigned)
		else
			stream:writeu8(u32TypeId)
			stream:writeu32(unsigned)
		end
	end

	local function isKind(stream: BufferStream)
		local prevCursor = stream.cursor
		local id = stream:readu8()
		stream.cursor = prevCursor
		return id == u8TypeId or id == u16TypeId or id == u32TypeId
	end

	return {
		isKind = isKind,
		read = reader,
		write = writer,
	}
end

local tblReadWrite = createUnsignedReadWrite(tbl_u8TypeId, tbl_u16TypeId, tbl_u32TypeId)
local tblRefReadWrite = createUnsignedReadWrite(tblRef_u8TypeId, tblRef_u16TypeId, tblRef_u32TypeId)
local arrReadWrite = createUnsignedReadWrite(arr_u8TypeId, arr_u16TypeId, arr_u32TypeId)
local dictReadWrite = createUnsignedReadWrite(dict_u8TypeId, dict_u16TypeId, dict_u32TypeId)

local function readDeepTblStream(stream: BufferStream)
	local nUniqueTbls = tblReadWrite.read(stream)

	local uniqueTblsArr: { { [any]: any } } = {}
	for i = 1, nUniqueTbls do
		uniqueTblsArr[i] = {}
	end

	local function readStep()
		local id = stream:readu8()
		if id == internalTypeId and tblRefReadWrite.isKind(stream) then
			local tblRefIndex = tblRefReadWrite.read(stream)
			return uniqueTblsArr[tblRefIndex]
		else
			return StandaloneDataTypes:readStream(stream)
		end
	end

	for i = 1, nUniqueTbls do
		local tbl = uniqueTblsArr[i]
		local isArray = arrReadWrite.isKind(stream)
		local keyCount = if isArray then arrReadWrite.read(stream) else dictReadWrite.read(stream)

		if isArray then
			for j = 1, keyCount do
				tbl[j] = readStep()
			end
		else
			local values = {}
			for j = 1, keyCount do
				values[j] = readStep()
			end

			local keys = {}
			for j = 1, keyCount do
				keys[j] = readStep()
			end

			for j = 1, keyCount do
				tbl[keys[j]] = values[j]
			end
		end
	end

	return uniqueTblsArr[1]
end

local function writeDeepTblStream(stream: BufferStream, rootTbl: { [any]: any })
	local uniqueTblsArr = TableHelper.getUniqueTblsArray(rootTbl)

	local tblRefIndexByTbl = {}
	for i, tbl in uniqueTblsArr do
		tblRefIndexByTbl[tbl] = i
	end

	tblReadWrite.write(stream, #uniqueTblsArr)

	for _, tbl in uniqueTblsArr do
		local keyStream = BufferStream.new(buffer.create(0))
		local valueStream = BufferStream.new(buffer.create(0))

		local keyCount = 0
		local isArray = true
		for key, value in tbl do
			keyCount = keyCount + 1

			local keyTypeOf = typeof(key)
			if keyTypeOf == "table" then
				local tblRefIndex = tblRefIndexByTbl[key]
				keyStream:writeu8(internalTypeId)
				tblRefReadWrite.write(keyStream, tblRefIndex)
			else
				-- stylua: ignore
				if isArray and not (keyTypeOf == "number" and key % 1 == 0 and key >= 1 and tbl[math.max(1, key - 1)] ~= nil) then
					isArray = false
				end
				keyStream:writeu8(standaloneTypeId)
				StandaloneDataTypes:writeStream(keyStream, key)
			end

			if typeof(value) == "table" then
				local tblRefIndex = tblRefIndexByTbl[value]
				valueStream:writeu8(internalTypeId)
				tblRefReadWrite.write(valueStream, tblRefIndex)
			else
				valueStream:writeu8(standaloneTypeId)
				StandaloneDataTypes:writeStream(valueStream, value)
			end
		end

		local tblStream = BufferStream.new(buffer.create(0))
		if isArray then
			arrReadWrite.write(tblStream, keyCount)
			tblStream:writeBuffer(valueStream.b)
		else
			dictReadWrite.write(tblStream, keyCount)
			tblStream:writeBuffer(valueStream.b)
			tblStream:writeBuffer(keyStream.b)
		end

		stream:writeBuffer(tblStream.b)
	end
end

local tblDefinition = DataTypeDefinition.new("table", {
	read = readDeepTblStream,
	write = writeDeepTblStream,
})

tblDefinition.overridable = false
tblDefinition.typing = "{ any }\n\t| { [any]: any }"

return tblDefinition


-- there are a few data types that need to read / write CFrames
-- for these cases it's helpful to have a module that that can be
-- required by other definitions

local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local BufferStream = require(dataTypesRoot.Parent.BufferStream)

local F32_EPSILON = math.pow(2, -23)

local function sanitizeCF(cframe: CFrame)
	local matrix = { select(4, cframe:GetComponents()) }
	for i = 1, 3 do
		local x = matrix[i + 0]
		local y = matrix[i + 3]
		local z = matrix[i + 6]
		-- check if the magnitude of the vector is 1
		if 1 - (x * x + y * y + z * z) <= F32_EPSILON then
			local absX = math.abs(x)
			local absY = math.abs(y)
			local absZ = math.abs(z)
			-- check that one of the individual components is 1
			if 1 - math.max(absX, absY, absZ) <= F32_EPSILON then
				matrix[i + 0] = math.round(x)
				matrix[i + 3] = math.round(y)
				matrix[i + 6] = math.round(z)
				continue
			end
		end
		return false, cframe
	end
	return true, CFrame.new(cframe.X, cframe.Y, cframe.Z, table.unpack(matrix, 1, 9))
end

local function u32Rotation(sanitizedCF: CFrame)
	local m11, m12, m13, m21, m22, m23, m31, m32, m33 = select(4, sanitizedCF:GetComponents())
	local r1 = math.max(m11 + m12 + m13, 0)
	local r2 = math.max(m21 + m22 + m23, 0)
	local r3 = math.max(m31 + m32 + m33, 0)
	-- stylua: ignore
	return tonumber(table.concat({
		r1, math.abs(m11), math.abs(m12), math.abs(m13),
		r2, math.abs(m21), math.abs(m22), math.abs(m23),
		r3, math.abs(m31), math.abs(m32), math.abs(m33),
	}, ""), 2) :: number
end

local function generateRotationLookups(rotations: { CFrame })
	local byIndex = {}
	local byU32 = {}
	for i, rotation in rotations do
		local success, sanitizedCF = sanitizeCF(rotation.Rotation)
		assert(success, "Tried to sanitize the rotation matrix of a complex CFrame.")
		local rotation32 = u32Rotation(sanitizedCF)
		byIndex[i] = sanitizedCF
		byU32[rotation32] = i
	end

	return {
		byIndex = byIndex,
		byU32 = byU32,
	}
end

local function rotationYXZ(degX: number, degY: number, degZ: number)
	return CFrame.fromEulerAngles(math.rad(degX), math.rad(degY), math.rad(degZ), Enum.RotationOrder.YXZ)
end

local ROTATION_LOOKUP = generateRotationLookups({
	rotationYXZ(0, 0, 0),
	rotationYXZ(90, 0, 0),
	rotationYXZ(0, 180, 180),
	rotationYXZ(-90, 0, 0),
	rotationYXZ(0, 180, 90),
	rotationYXZ(0, 90, 90),
	rotationYXZ(0, 0, 90),
	rotationYXZ(0, -90, 90),
	rotationYXZ(-90, -90, 0),
	rotationYXZ(0, -90, 0),
	rotationYXZ(90, -90, 0),
	rotationYXZ(0, 90, 180),
	rotationYXZ(0, 180, 0),
	rotationYXZ(-90, -180, 0),
	rotationYXZ(0, 0, 180),
	rotationYXZ(90, 180, 0),
	rotationYXZ(0, 0, -90),
	rotationYXZ(0, -90, -90),
	rotationYXZ(0, -180, -90),
	rotationYXZ(0, 90, -90),
	rotationYXZ(90, 90, 0),
	rotationYXZ(0, 90, 0),
	rotationYXZ(-90, 90, 0),
	rotationYXZ(0, -90, 180),
})

local function read(stream: BufferStream.BufferStream)
	local index = stream:readu8()
	if index > 0 then
		local rotation = ROTATION_LOOKUP.byIndex[index]
		return CFrame.new(stream:readf32(), stream:readf32(), stream:readf32()) * rotation
	else
		local components = {}
		for i = 1, 12 do
			components[i] = stream:readf32()
		end
		return CFrame.new(table.unpack(components, 1, 12))
	end
end

local function write(stream: BufferStream.BufferStream, cframe: CFrame)
	local success, sanitizedCF = sanitizeCF(cframe)

	if success then
		local rotation32 = u32Rotation(sanitizedCF)
		local rotationIndex = ROTATION_LOOKUP.byU32[rotation32]

		stream:writeu8(rotationIndex)
		stream:writef32(cframe.X)
		stream:writef32(cframe.Y)
		stream:writef32(cframe.Z)
	else
		stream:writeu8(0)
		for _, component in { cframe:GetComponents() } do
			stream:writef32(component)
		end
	end
end

local function equals(a: CFrame, b: CFrame)
	local componentsA = { a:GetComponents() }
	local componentsB = { b:GetComponents() }

	for i = 1, 3 do
		if componentsA[i] ~= componentsB[i] then
			return false
		end
	end

	for i = 4, 12 do
		if math.abs(componentsA[i] - componentsB[i]) > F32_EPSILON then
			return false
		end
	end

	return true
end

return {
	read = read,
	write = write,
	equals = equals,
}


local TableHelper = {}

-- Private

local function standardProcessKV(kv: any)
	return kv
end

local function standardCompare(a: any, b: any)
	return a == b
end

-- Public

function TableHelper.getUniqueTblsArray(rootTbl: { [any]: any })
	local i = 1
	local uniqueArr = { rootTbl }

	local visited = {}
	local inArray = { [rootTbl] = true }

	while uniqueArr[i] do
		visited[uniqueArr[i]] = true

		for key, value in uniqueArr[i] do
			if typeof(key) == "table" and not inArray[key] then
				inArray[key] = true
				table.insert(uniqueArr, key)
			end
			if typeof(value) == "table" and not inArray[value] then
				inArray[value] = true
				table.insert(uniqueArr, value)
			end
		end

		while visited[uniqueArr[i + 1]] do
			i = i + 1
		end
		i = i + 1
	end

	return uniqueArr
end

function TableHelper.replaceKV(rootTbl: { [any]: any }, replaceKV: (any) -> any)
	local uniqueTblsArr = TableHelper.getUniqueTblsArray(rootTbl)

	local indexByTbl = {}
	for i, tbl in uniqueTblsArr do
		indexByTbl[tbl] = i
	end

	for i, tbl in uniqueTblsArr do
		for key, value in tbl do
			local newKey = key
			if not indexByTbl[key] then
				newKey = replaceKV(key)
			end

			local newValue = value
			if not indexByTbl[value] then
				newValue = replaceKV(value)
			end

			tbl[key] = nil
			tbl[newKey] = newValue
		end
	end
end

function TableHelper.copyDeep(rootTbl: { [any]: any }, customProcessKV: ((any) -> any)?)
	local processKV = customProcessKV or standardProcessKV

	local uniqueTblsArrCopy = {}
	local uniqueTblsArr = TableHelper.getUniqueTblsArray(rootTbl)

	local indexByTbl = {}
	for i, tbl in uniqueTblsArr do
		indexByTbl[tbl] = i
		uniqueTblsArrCopy[i] = {}
	end

	for i, tbl in uniqueTblsArr do
		local tblCopy = uniqueTblsArrCopy[i]
		for key, value in tbl do
			local newKey = uniqueTblsArrCopy[indexByTbl[key]]
			if not newKey then
				newKey = processKV(key)
			end

			local newValue = uniqueTblsArrCopy[indexByTbl[value]]
			if not newValue then
				newValue = processKV(value)
			end

			tblCopy[newKey] = newValue
		end
	end

	return uniqueTblsArrCopy[1]
end

function TableHelper.equalsDeep(
	rootTblA: { [any]: any },
	rootTblB: { [any]: any },
	customCompare: ((any, any) -> boolean)?
)
	local compare = customCompare or standardCompare

	local uniqueTblsArrA = TableHelper.getUniqueTblsArray(rootTblA)
	local uniqueTblsArrB = TableHelper.getUniqueTblsArray(rootTblB)

	local uniqueCount = #uniqueTblsArrA
	if uniqueCount ~= #uniqueTblsArrB then
		return false
	end

	local refSet = {}
	local refByTbl = {}
	for i = 1, uniqueCount do
		local ref = { index = i }
		refSet[ref] = true
		refByTbl[uniqueTblsArrA[i]] = ref
		refByTbl[uniqueTblsArrB[i]] = ref
	end

	for i, tbl in uniqueTblsArrA do
		local newTbl = {}
		for key, value in tbl do
			newTbl[refByTbl[key] or key] = refByTbl[value] or value
		end
		uniqueTblsArrA[i] = newTbl
	end

	for i, tbl in uniqueTblsArrB do
		local newTbl = {}
		for key, value in tbl do
			newTbl[refByTbl[key] or key] = refByTbl[value] or value
		end
		uniqueTblsArrB[i] = newTbl
	end

	for i = 1, uniqueCount do
		local tblA = uniqueTblsArrA[i]
		local tblB = uniqueTblsArrB[i]

		local visitedKeysSet = {}
		for keyA, valueA in tblA do
			if refSet[valueA] then
				if valueA ~= tblB[keyA] then
					return false
				end
			elseif not compare(valueA, tblB[keyA]) then
				return false
			end
			visitedKeysSet[keyA] = true
		end

		for key, value in tblB do
			if not visitedKeysSet[key] then
				return false
			end
		end
	end

	return true
end

--

return TableHelper


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("boolean", {
	read = function(stream)
		return stream:readu8() == 1
	end,
	write = function(stream, value)
		stream:writeu8(if value then 1 else 0)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("buffer", {
	read = function(stream)
		return stream:readBuffer(stream:readu32())
	end,
	write = function(stream, b)
		local length = buffer.len(b)
		stream:writeu32(length)
		stream:writeBuffer(b, 0, length)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

-- named 'luaFunction' b/c auto-import is being annoying if the file is just called 'function'

return DataTypeDefinition.never("function")


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("nil", {
	read = function(_stream)
		return nil
	end,
	write = function(_stream, _null) end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

local number_nan = 1
local number_huge = 2
local number_int = 3
local number_f64 = 4

return DataTypeDefinition.new("number", {
	read = DataTypeDefinition.createReader({
		[number_nan] = function(_stream)
			return 0 / 0
		end,
		[number_huge] = function(stream)
			return math.huge * stream:readi8()
		end,
		[number_int] = function(stream)
			local signedBytes = stream:readi8()
			local bytes = math.abs(signedBytes)
			local sign = if signedBytes < 0 then -1 else 1
			return stream:readUnsignedBytes(bytes) * sign
		end,
		[number_f64] = function(stream)
			return stream:readf64()
		end,
	}),
	write = function(stream, num)
		if num ~= num then
			stream:writeu8(number_nan)
		else
			local abs = math.abs(num)
			if abs == math.huge then
				stream:writeu8(number_huge)
				stream:writei8(if num < 0 then -1 else 1)
			elseif num % 1 == 0 and abs <= 0xFFFFFFFF then
				stream:writeu8(number_int)

				local bytes = 4
				if abs <= 0xFF then
					bytes = 1
				elseif abs <= 0xFFFF then
					bytes = 2
				end

				stream:writei8(if num < 0 then -bytes else bytes)
				stream:writeUnsignedBytes(bytes, abs)
			else
				stream:writeu8(number_f64)
				stream:writef64(num)
			end
		end
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("string", {
	read = function(stream)
		local bytes = stream:readu8()
		local length = stream:readUnsignedBytes(bytes)
		return stream:readString(length)
	end,
	write = function(stream, str)
		local length = #str

		local bytes = 4
		if length <= 0xFF then
			bytes = 1
		elseif length <= 0xFFFF then
			bytes = 2
		end

		stream:writeu8(bytes)
		stream:writeUnsignedBytes(bytes, length)
		stream:writeString(str, length)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.never("userdata")


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

local AXIS_NAME_ORDER = { "X", "Y", "Z" }

return DataTypeDefinition.new("Axes", {
	read = function(stream)
		local index = stream:readu8()
		local enums: { Enum.Axis } = {}
		for i, axisName in AXIS_NAME_ORDER do
			if bit32.band(index, bit32.lshift(1, (i - 1))) > 0 then
				table.insert(enums, (Enum.Axis :: any)[axisName])
			end
		end
		return Axes.new(table.unpack(enums))
	end,
	write = function(stream, axes)
		local index = 0
		for i, axisName in AXIS_NAME_ORDER do
			if (axes :: any)[axisName] then
				index = bit32.bor(index, bit32.lshift(1, (i - 1)))
			end
		end
		stream:writeu8(index)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("BrickColor", {
	read = function(stream)
		return BrickColor.new(stream:readu32())
	end,
	write = function(stream, brickColor)
		stream:writeu32(brickColor.Number)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local CFrameStream = require(dataTypesRoot.Helpers.CFrameStream)
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("CFrame", {
	read = CFrameStream.read,
	write = CFrameStream.write,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

-- TODO: This can be supported, it just has a lot of properties...
return DataTypeDefinition.unimplemented("CatalogSearchParams")


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("Color3", {
	read = function(stream)
		return Color3.fromRGB(stream:readu8(), stream:readu8(), stream:readu8())
	end,
	write = function(stream, color)
		local rHex, gHex, bHex = color:ToHex():match("(..)(..)(..)")
		stream:writeu8(tonumber(rHex, 16) :: number)
		stream:writeu8(tonumber(gHex, 16) :: number)
		stream:writeu8(tonumber(bHex, 16) :: number)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("ColorSequence", {
	read = function(stream)
		local keypoints = {}
		local nKeypoints = stream:readu32()
		for i = 1, nKeypoints do
			local t = stream:readf32()
			local color = Color3.fromRGB(stream:readu8(), stream:readu8(), stream:readu8())
			keypoints[i] = ColorSequenceKeypoint.new(t, color)
		end
		return ColorSequence.new(keypoints)
	end,
	write = function(stream, sequence)
		local keypoints = sequence.Keypoints
		stream:writeu32(#keypoints)
		for _, kp in keypoints do
			local rHex, gHex, bHex = kp.Value:ToHex():match("(..)(..)(..)")
			stream:writef32(kp.Time)
			stream:writeu8(tonumber(rHex, 16) :: number)
			stream:writeu8(tonumber(gHex, 16) :: number)
			stream:writeu8(tonumber(bHex, 16) :: number)
		end
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("ColorSequenceKeypoint", {
	read = function(stream)
		local t = stream:readf32()
		local color = Color3.fromRGB(stream:readu8(), stream:readu8(), stream:readu8())
		return ColorSequenceKeypoint.new(t, color)
	end,
	write = function(stream, kp)
		local rHex, gHex, bHex = kp.Value:ToHex():match("(..)(..)(..)")
		stream:writef32(kp.Time)
		stream:writeu8(tonumber(rHex, 16) :: number)
		stream:writeu8(tonumber(gHex, 16) :: number)
		stream:writeu8(tonumber(bHex, 16) :: number)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.never("Content")


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.never("DockWidgetPluginGuiInfo")


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("Enum", {
	read = function(stream)
		local enumTypeName = stream:readString(stream:readu8())
		return (Enum :: any)[enumTypeName] :: Enum
	end,
	write = function(stream, enumType)
		local enumTypeName = tostring(enumType)
		local length = #enumTypeName
		stream:writeu8(length)
		stream:writeString(enumTypeName, length)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("EnumItem", {
	read = function(stream)
		local enumName = stream:readString(stream:readu8())
		local enumType = (Enum :: any)[enumName] :: Enum
		return enumType:FromValue(stream:readu16()) :: EnumItem
	end,
	write = function(stream, enumItem)
		local enumTypeName = tostring(enumItem.EnumType)
		local length = #enumTypeName
		stream:writeu8(length)
		stream:writeString(enumTypeName, length)
		stream:writeu16(enumItem.Value)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("Enums", {
	read = function(_stream)
		return Enum
	end,
	write = function(_stream) end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

local FACE_NAME_ORDER = { "Front", "Bottom", "Left", "Back", "Top", "Right" }

return DataTypeDefinition.new("Faces", {
	read = function(stream)
		local index = stream:readu8()
		local enums: { Enum.NormalId } = {}
		for i, faceName in FACE_NAME_ORDER do
			if bit32.band(index, bit32.lshift(1, (i - 1))) > 0 then
				table.insert(enums, (Enum.NormalId :: any)[faceName])
			end
		end
		return Faces.new(table.unpack(enums))
	end,
	write = function(stream, faces)
		local index = 0
		for i, faceName in FACE_NAME_ORDER do
			if (faces :: any)[faceName] then
				index = bit32.bor(index, bit32.lshift(1, (i - 1)))
			end
		end
		stream:writeu8(index)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

local function readTangent(stream: DataTypeDefinition.BufferStream, tangentId: number): number?
	local prevCursor = stream.cursor
	if stream:readu8() == tangentId then
		return stream:readf32()
	end
	stream.cursor = prevCursor
	return nil
end

return DataTypeDefinition.new("FloatCurveKey", {
	read = function(stream)
		local t = stream:readf32()
		local v = stream:readf32()
		local interpolation = Enum.KeyInterpolationMode:FromValue(stream:readu16()) :: Enum.KeyInterpolationMode
		local ck = FloatCurveKey.new(t, v, interpolation)

		-- tangent props are typed as number, but they should be number?
		ck.LeftTangent = readTangent(stream, 1) :: number
		ck.RightTangent = readTangent(stream, 2) :: number

		return ck
	end,
	write = function(stream, ck)
		stream:writef32(ck.Time)
		stream:writef32(ck.Value)
		stream:writeu16(ck.Interpolation.Value)

		if ck.LeftTangent then
			stream:writeu8(1)
			stream:writef32(ck.LeftTangent)
		else
			stream:writeu8(0)
		end

		if ck.RightTangent then
			stream:writeu8(2)
			stream:writef32(ck.RightTangent)
		else
			stream:writeu8(0)
		end
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("Font", {
	read = function(stream)
		local weight = Enum.FontWeight:FromValue(stream:readu16()) :: Enum.FontWeight
		local style = Enum.FontStyle:FromValue(stream:readu16()) :: Enum.FontStyle

		local length = stream:readu8()
		if length == 0 then
			-- font id case
			local fontId = stream:readu32()
			return Font.fromId(fontId, weight, style)
		else
			-- family name case
			local familyName = stream:readString(length - 1)
			if familyName == "" then
				-- Enum.Font.Arial is the first font and maps to Arimo
				familyName = "Arimo"
			end
			return Font.fromName(familyName, weight, style)
		end
	end,
	write = function(stream, font)
		local family = font.Family
		local fontId = family:match("%d+$")

		stream:writeu16(font.Weight.Value)
		stream:writeu16(font.Style.Value)

		if fontId then
			stream:writeu8(0)
			stream:writeu32(tonumber(fontId) :: number)
		else
			local familyName = family:match("/(%w+).json$") or ""
			local length = #familyName

			-- we store the length as length + 1 since we're using this byte both for
			-- the length of the string, but also to identify if it's storing a font id or not
			assert(length <= 254, `Family name cannot exceed 254 characters: '{familyName}'`)
			stream:writeu8(length + 1)
			stream:writeString(familyName, length)
		end
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.unimplemented("Instance")


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("NumberRange", {
	read = function(stream)
		return NumberRange.new(stream:readf32(), stream:readf32())
	end,
	write = function(stream, range)
		stream:writef32(range.Min)
		stream:writef32(range.Max)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("NumberSequence", {
	read = function(stream)
		local keypoints = {}
		local nKeypoints = stream:readu32()
		for i = 1, nKeypoints do
			keypoints[i] = NumberSequenceKeypoint.new(stream:readf32(), stream:readf32(), stream:readf32())
		end
		return NumberSequence.new(keypoints)
	end,
	write = function(stream, sequence)
		local keypoints = sequence.Keypoints
		stream:writeu32(#keypoints)
		for _, kp in keypoints do
			stream:writef32(kp.Time)
			stream:writef32(kp.Value)
			stream:writef32(kp.Envelope)
		end
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("NumberSequenceKeypoint", {
	read = function(stream)
		return NumberSequenceKeypoint.new(stream:readf32(), stream:readf32(), stream:readf32())
	end,
	write = function(stream, kp)
		stream:writef32(kp.Time)
		stream:writef32(kp.Value)
		stream:writef32(kp.Envelope)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.never("OverlapParams")


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("Path2DControlPoint", {
	read = function(stream)
		local pos = UDim2.new(stream:readf32(), stream:readi32(), stream:readf32(), stream:readi32())
		local left = UDim2.new(stream:readf32(), stream:readi32(), stream:readf32(), stream:readi32())
		local right = UDim2.new(stream:readf32(), stream:readi32(), stream:readf32(), stream:readi32())
		return Path2DControlPoint.new(pos, left, right)
	end,
	write = function(stream, ctrlP)
		local pos = ctrlP.Position
		stream:writef32(pos.X.Scale)
		stream:writei32(pos.X.Offset)
		stream:writef32(pos.Y.Scale)
		stream:writei32(pos.Y.Offset)

		local left = ctrlP.LeftTangent
		stream:writef32(left.X.Scale)
		stream:writei32(left.X.Offset)
		stream:writef32(left.Y.Scale)
		stream:writei32(left.Y.Offset)

		local right = ctrlP.RightTangent
		stream:writef32(right.X.Scale)
		stream:writei32(right.X.Offset)
		stream:writef32(right.Y.Scale)
		stream:writei32(right.Y.Offset)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

local label_u8 = 1
local label_u16 = 2
local label_u32 = 3

local labelReader = DataTypeDefinition.createReader({
	[label_u8] = function(stream)
		return stream:readString(stream:readu8())
	end,
	[label_u16] = function(stream)
		return stream:readString(stream:readu8())
	end,
	[label_u32] = function(stream)
		return stream:readString(stream:readu8())
	end,
})

return DataTypeDefinition.new("PathWaypoint", {
	read = function(stream)
		local position = Vector3.new(stream:readf32(), stream:readf32(), stream:readf32())
		local action = Enum.PathWaypointAction:FromValue(stream:readu16()) :: Enum.PathWaypointAction
		local label = labelReader(stream)
		return PathWaypoint.new(position, action, label)
	end,
	write = function(stream, wp)
		stream:writef32(wp.Position.X)
		stream:writef32(wp.Position.Y)
		stream:writef32(wp.Position.Z)
		stream:writeu16(wp.Action.Value)

		local label = wp.Label
		local length = #label
		if length <= 0xFF then
			stream:writeu8(label_u8)
			stream:writeu8(length)
		elseif length <= 0xFFFF then
			stream:writeu8(label_u16)
			stream:writeu16(length)
		else
			stream:writeu8(label_u32)
			stream:writeu32(length)
		end

		stream:writeString(label, length)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

-- lsp doesn't seem to like acoustic absorption, but it does exist!

local newPhysicalProps = PhysicalProperties.new :: (...number) -> PhysicalProperties

return DataTypeDefinition.new("PhysicalProperties", {
	read = function(stream)
		return newPhysicalProps(
			stream:readf32(),
			stream:readf32(),
			stream:readf32(),
			stream:readf32(),
			stream:readf32(),
			stream:readf32()
		)
	end,
	write = function(stream, phys)
		stream:writef32(phys.Density)
		stream:writef32(phys.Friction)
		stream:writef32(phys.Elasticity)
		stream:writef32(phys.FrictionWeight)
		stream:writef32(phys.ElasticityWeight)
		stream:writef32((phys :: any).AcousticAbsorption :: number)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.never("RBXScriptConnection")


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.never("RBXScriptSignal")


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.never("Random")


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("Ray", {
	read = function(stream)
		return Ray.new(
			Vector3.new(stream:readf32(), stream:readf32(), stream:readf32()),
			Vector3.new(stream:readf32(), stream:readf32(), stream:readf32())
		)
	end,
	write = function(stream, ray)
		stream:writef32(ray.Origin.X)
		stream:writef32(ray.Origin.Y)
		stream:writef32(ray.Origin.Z)
		stream:writef32(ray.Direction.X)
		stream:writef32(ray.Direction.Y)
		stream:writef32(ray.Direction.Z)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.never("RaycastParams")


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.never("RaycastResult")


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("Rect", {
	read = function(stream)
		return Rect.new(stream:readf32(), stream:readf32(), stream:readf32(), stream:readf32())
	end,
	write = function(stream, rect)
		stream:writef32(rect.Min.X)
		stream:writef32(rect.Min.Y)
		stream:writef32(rect.Max.X)
		stream:writef32(rect.Max.Y)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("Region3", {
	read = function(stream)
		local vMin = Vector3.new(stream:readf32(), stream:readf32(), stream:readf32())
		local vMax = Vector3.new(stream:readf32(), stream:readf32(), stream:readf32())
		return Region3.new(vMin, vMax)
	end,
	write = function(stream, r3)
		local sx, sy, sz = r3.Size.X / 2, r3.Size.Y / 2, r3.Size.Z / 2
		local vMin = r3.CFrame * Vector3.new(-sx, -sy, -sz)
		local vMax = r3.CFrame * Vector3.new(sx, sy, sz)

		stream:writef32(math.min(vMin.X, vMax.X))
		stream:writef32(math.min(vMin.Y, vMax.Y))
		stream:writef32(math.min(vMin.Z, vMax.Z))
		stream:writef32(math.max(vMin.X, vMax.X))
		stream:writef32(math.max(vMin.Y, vMax.Y))
		stream:writef32(math.max(vMin.Z, vMax.Z))
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("Region3int16", {
	read = function(stream)
		local vMin = Vector3int16.new(stream:readi16(), stream:readi16(), stream:readi16())
		local vMax = Vector3int16.new(stream:readi16(), stream:readi16(), stream:readi16())
		return Region3int16.new(vMin, vMax)
	end,
	write = function(stream, r3int16)
		local r3Min = r3int16.Min
		local r3Max = r3int16.Max

		stream:writei16(r3Min.X)
		stream:writei16(r3Min.Y)
		stream:writei16(r3Min.Z)
		stream:writei16(r3Max.X)
		stream:writei16(r3Max.Y)
		stream:writei16(r3Max.Z)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local CFrameStream = require(dataTypesRoot.Helpers.CFrameStream)
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("RotationCurveKey", {
	read = function(stream)
		local t = stream:readf32()
		local value = CFrameStream.read(stream)
		local interpolation = Enum.KeyInterpolationMode:FromValue(stream:readu16()) :: Enum.KeyInterpolationMode
		return RotationCurveKey.new(t, value, interpolation)
	end,
	write = function(stream, rck)
		stream:writef32(rck.Time)
		CFrameStream.write(stream, rck.Value)
		stream:writeu16(rck.Interpolation.Value)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.never("Secret")


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

-- technically we could use a modified version of the table type, but that would require this to also be dependant
return DataTypeDefinition.unimplemented("SharedTable")


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("TweenInfo", {
	read = function(stream)
		local t = stream:readf32()
		local easingStyle = Enum.EasingStyle:FromValue(stream:readu16()) :: Enum.EasingStyle
		local easingDirection = Enum.EasingDirection:FromValue(stream:readu16()) :: Enum.EasingDirection
		local repeatCount = stream:readu32() - 1
		local reverses = stream:readu8() == 1
		local delayTime = stream:readf32()

		return TweenInfo.new(t, easingStyle, easingDirection, repeatCount, reverses, delayTime)
	end,
	write = function(stream, info)
		stream:writef32(info.Time)
		stream:writeu16(info.EasingStyle.Value)
		stream:writeu16(info.EasingDirection.Value)
		stream:writeu32(math.max(-1, info.RepeatCount) + 1) --  not sure abt this?
		stream:writeu8(if info.Reverses then 1 else 0)
		stream:writef32(info.DelayTime)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("UDim", {
	read = function(stream)
		return UDim.new(stream:readf32(), stream:readi32())
	end,
	write = function(stream, udim)
		stream:writef32(udim.Scale)
		stream:writei32(udim.Offset)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("UDim2", {
	read = function(stream)
		return UDim2.new(stream:readf32(), stream:readi32(), stream:readf32(), stream:readi32())
	end,
	write = function(stream, udim2)
		stream:writef32(udim2.X.Scale)
		stream:writei32(udim2.X.Offset)
		stream:writef32(udim2.Y.Scale)
		stream:writei32(udim2.Y.Offset)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.never("ValueCurveKey")


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("Vector2", {
	read = function(stream)
		local x = stream:readf32()
		local y = stream:readf32()
		return Vector2.new(x, y)
	end,
	write = function(stream, vec)
		stream:writef32(vec.X)
		stream:writef32(vec.Y)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("Vector2int16", {
	read = function(stream)
		local x = stream:readi16()
		local y = stream:readi16()
		return Vector2int16.new(x, y)
	end,
	write = function(stream, vec)
		stream:writei16(vec.X)
		stream:writei16(vec.Y)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("Vector3", {
	read = function(stream)
		local x = stream:readf32()
		local y = stream:readf32()
		local z = stream:readf32()
		return Vector3.new(x, y, z)
	end,
	write = function(stream, vec)
		stream:writef32(vec.X)
		stream:writef32(vec.Y)
		stream:writef32(vec.Z)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("Vector3int16", {
	read = function(stream)
		local x = stream:readi16()
		local y = stream:readi16()
		local z = stream:readi16()
		return Vector3int16.new(x, y, z)
	end,
	write = function(stream, vec)
		stream:writei16(vec.X)
		stream:writei16(vec.Y)
		stream:writei16(vec.Z)
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

return DataTypeDefinition.new("vector", {
	read = function(stream)
		local x = stream:readf32()
		local y = stream:readf32()
		local z = stream:readf32()
		return vector.create(x, y, z)
	end,
	write = function(stream, vec)
		stream:writef32(vec.x)
		stream:writef32(vec.y)
		stream:writef32(vec.z)
	end,
})


local userdataRoot = script:FindFirstAncestor("Userdata")
local UserdataDefinition = require(userdataRoot.UserdataDefinition)

return UserdataDefinition.new("deduplication-pointer", function(index: number)
	return { index = index }
end)


local userdataRoot = script:FindFirstAncestor("Userdata")
local UserdataDefinition = require(userdataRoot.UserdataDefinition)

return UserdataDefinition.new("instance-pointer", function(index: number)
	return { index = index }
end)


local userdataRoot = script:FindFirstAncestor("Userdata")
local UserdataDefinition = require(userdataRoot.UserdataDefinition)

return UserdataDefinition.new("userdefined", function(kind: string, b: buffer)
	return {
		kind = kind,
		b = b,
	}
end)


return {
	Userdefined = require(script.Userdefined),
	InstancePointer = require(script.InstancePointer),
	DeduplicationPointer = require(script.DeduplicationPointer),
}


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

local userdataRoot = script:FindFirstAncestor("Userdata")
local UserdataDefinitions = require(userdataRoot.Definitions)
local userdataDefinition = UserdataDefinitions.DeduplicationPointer

local pointer_u8 = 1
local pointer_u16 = 2
local pointer_u32 = 3
local pointer_f64 = 4

return DataTypeDefinition.new(userdataDefinition.typeOf, {
	read = DataTypeDefinition.createReader({
		[pointer_u8] = function(stream)
			return userdataDefinition:create(stream:readu8())
		end,
		[pointer_u16] = function(stream)
			return userdataDefinition:create(stream:readu16())
		end,
		[pointer_u32] = function(stream)
			return userdataDefinition:create(stream:readu32())
		end,
		[pointer_f64] = function(stream)
			return userdataDefinition:create(stream:readf64())
		end,
	}),
	write = function(stream, userdata)
		local pointer = userdataDefinition:read(userdata)
		local pointerIndex = pointer.index

		if pointerIndex <= 0xFF then
			stream:writeu8(pointer_u8)
			stream:writeu8(pointerIndex)
		elseif pointerIndex <= 0xFFFF then
			stream:writeu8(pointer_u16)
			stream:writeu16(pointerIndex)
		elseif pointerIndex <= 0xFFFFFFFF then
			stream:writeu8(pointer_u32)
			stream:writeu32(pointerIndex)
		else
			stream:writeu8(pointer_f64)
			stream:writef64(pointerIndex)
		end
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

local userdataRoot = script:FindFirstAncestor("Userdata")
local UserdataDefinitions = require(userdataRoot.Definitions)
local userdataDefinition = UserdataDefinitions.InstancePointer

local pointer_u8 = 1
local pointer_u16 = 2
local pointer_u32 = 3
local pointer_f64 = 4

return DataTypeDefinition.new(userdataDefinition.typeOf, {
	read = DataTypeDefinition.createReader({
		[pointer_u8] = function(stream)
			return userdataDefinition:create(stream:readu8())
		end,
		[pointer_u16] = function(stream)
			return userdataDefinition:create(stream:readu16())
		end,
		[pointer_u32] = function(stream)
			return userdataDefinition:create(stream:readu32())
		end,
		[pointer_f64] = function(stream)
			return userdataDefinition:create(stream:readf64())
		end,
	}),
	write = function(stream, userdata)
		local pointer = userdataDefinition:read(userdata)
		local pointerIndex = pointer.index

		if pointerIndex <= 0xFF then
			stream:writeu8(pointer_u8)
			stream:writeu8(pointerIndex)
		elseif pointerIndex <= 0xFFFF then
			stream:writeu8(pointer_u16)
			stream:writeu16(pointerIndex)
		elseif pointerIndex <= 0xFFFFFFFF then
			stream:writeu8(pointer_u32)
			stream:writeu32(pointerIndex)
		else
			stream:writeu8(pointer_f64)
			stream:writef64(pointerIndex)
		end
	end,
})


local dataTypesRoot = script:FindFirstAncestor("DataTypes")
local DataTypeDefinition = require(dataTypesRoot.DataTypeDefinition)

local userdataRoot = script:FindFirstAncestor("Userdata")
local UserdataDefinitions = require(userdataRoot.Definitions)
local userdataDefinition = UserdataDefinitions.Userdefined

local kind_u8 = 1
local kind_u16 = 2
local kind_u32 = 3

local readKind = DataTypeDefinition.createReader({
	[kind_u8] = function(stream)
		return stream:readString(stream:readu8())
	end,
	[kind_u16] = function(stream)
		return stream:readString(stream:readu16())
	end,
	[kind_u32] = function(stream)
		return stream:readString(stream:readu32())
	end,
})

local buffer_u8 = 4
local buffer_u16 = 5
local buffer_u32 = 6

local readBuffer = DataTypeDefinition.createReader({
	[buffer_u8] = function(stream)
		return stream:readBuffer(stream:readu8())
	end,
	[buffer_u16] = function(stream)
		return stream:readBuffer(stream:readu16())
	end,
	[buffer_u32] = function(stream)
		return stream:readBuffer(stream:readu32())
	end,
})

return DataTypeDefinition.new(userdataDefinition.typeOf, {
	read = function(stream)
		local kind = readKind(stream)
		local b = readBuffer(stream)
		return userdataDefinition:create(kind, b)
	end,
	write = function(stream, userdata)
		local userdefined = userdataDefinition:read(userdata)
		local kindLength = #userdefined.kind
		local bufferLength = buffer.len(userdefined.b)

		if kindLength <= 0xFF then
			stream:writeu8(kind_u8)
			stream:writeu8(kindLength)
		elseif kindLength <= 0xFFFF then
			stream:writeu8(kind_u16)
			stream:writeu16(kindLength)
		else
			stream:writeu8(kind_u32)
			stream:writeu32(kindLength)
		end
		stream:writeString(userdefined.kind)

		if bufferLength <= 0xFF then
			stream:writeu8(buffer_u8)
			stream:writeu8(bufferLength)
		elseif bufferLength <= 0xFFFF then
			stream:writeu8(buffer_u16)
			stream:writeu16(bufferLength)
		else
			stream:writeu8(buffer_u32)
			stream:writeu32(bufferLength)
		end
		stream:writeBuffer(userdefined.b, 0, bufferLength)
	end,
})


local SUB_TYPE_OF_KEY = "_$_$_bufferize_typeof"

local UserdataDefinitionStatic = {}

local UserdataDefinitionClass = {}
UserdataDefinitionClass.__index = UserdataDefinitionClass
UserdataDefinitionClass.ClassName = "UserdataDefinition"

type userdata<T> = {
	["_$_$_bufferize_typeof"]: string,
	value: T,
}

type Options<T> = {
	write: () -> T,
	read: (userdata<T>) -> T,
}

export type UserdataDefinition<T, U...> = typeof(setmetatable(
	{} :: {
		subTypeOf: string,
		typeOf: string,
		write: (U...) -> T,
	},
	UserdataDefinitionClass
))

function UserdataDefinitionStatic.new<T, U...>(subTypeOf: string, write: (U...) -> T)
	local self = setmetatable({}, UserdataDefinitionClass) :: UserdataDefinition<T, U...>

	self.subTypeOf = subTypeOf
	self.typeOf = "userdata:" .. subTypeOf
	self.write = write

	return self
end

function UserdataDefinitionStatic.subTypeOf(userdata: any): string?
	local mt = typeof(userdata) == "userdata" and getmetatable(userdata)
	local mtIndex = typeof(mt) == "table" and rawget(mt, "__index")
	return typeof(mtIndex) == "table" and rawget(mtIndex :: any, SUB_TYPE_OF_KEY) or nil
end

function UserdataDefinitionClass.isTypeOf<T, U...>(self: UserdataDefinition<T, U...>, userdata: any)
	return UserdataDefinitionStatic.subTypeOf(userdata) == self.subTypeOf
end

function UserdataDefinitionClass.read<T, U...>(self: UserdataDefinition<T, U...>, userdata: userdata<T>)
	assert(typeof(userdata) == "userdata", "Cannot read non-userdata.")
	local subTypeOf = UserdataDefinitionStatic.subTypeOf(userdata)
	assert(
		subTypeOf == self.subTypeOf,
		`Attempt to read userdata typeof '{subTypeOf}' with typeof '{self.subTypeOf}' reader.`
	)
	return userdata.value :: T
end

function UserdataDefinitionClass.create<T, U...>(self: UserdataDefinition<T, U...>, ...: U...)
	local userdata = newproxy(true) :: userdata<T>
	getmetatable(userdata :: any).__index = {
		[SUB_TYPE_OF_KEY] = self.subTypeOf,
		value = self.write(...),
	}
	return userdata
end

return UserdataDefinitionStatic


local bufferizeRoot = script.Parent.Parent
local CargoSemver = require(bufferizeRoot.Parent.CargoSemver)

local TypeIds = {}

local registeredArr = {}
local function increment(semver: string)
	local registered = {
		index = #registeredArr + 1,
		semver = semver,
	}
	table.insert(registeredArr, registered)
	return registered
end

local REGISTERED_BY_TYPEOF: { [string]: typeof(increment("")) } = {
	["nil"] = increment("2.0.0"),
	["boolean"] = increment("2.0.0"),
	["function"] = increment("2.0.0"),
	["number"] = increment("2.0.0"),
	["string"] = increment("2.0.0"),
	["buffer"] = increment("2.0.0"),
	["table"] = increment("2.0.0"),

	["userdata"] = increment("2.0.0"),
	["userdata:userdefined"] = increment("2.0.0"),
	["userdata:instance-pointer"] = increment("2.0.0"),
	["userdata:deduplication-pointer"] = increment("2.0.0"),

	["Axes"] = increment("2.0.0"),
	["BrickColor"] = increment("2.0.0"),
	["CatalogSearchParams"] = increment("2.0.0"),
	["CFrame"] = increment("2.0.0"),
	["Color3"] = increment("2.0.0"),
	["ColorSequence"] = increment("2.0.0"),
	["ColorSequenceKeypoint"] = increment("2.0.0"),
	["Content"] = increment("2.0.0"),
	["DockWidgetPluginGuiInfo"] = increment("2.0.0"),
	["Enum"] = increment("2.0.0"),
	["EnumItem"] = increment("2.0.0"),
	["Enums"] = increment("2.0.0"),
	["Faces"] = increment("2.0.0"),
	["FloatCurveKey"] = increment("2.0.0"),
	["Font"] = increment("2.0.0"),
	["Instance"] = increment("2.0.0"),
	["NumberRange"] = increment("2.0.0"),
	["NumberSequence"] = increment("2.0.0"),
	["NumberSequenceKeypoint"] = increment("2.0.0"),
	["OverlapParams"] = increment("2.0.0"),
	["Path2DControlPoint"] = increment("2.0.0"),
	["PathWaypoint"] = increment("2.0.0"),
	["PhysicalProperties"] = increment("2.0.0"),
	["Random"] = increment("2.0.0"),
	["Ray"] = increment("2.0.0"),
	["RaycastParams"] = increment("2.0.0"),
	["RaycastResult"] = increment("2.0.0"),
	["RBXScriptConnection"] = increment("2.0.0"),
	["RBXScriptSignal"] = increment("2.0.0"),
	["Rect"] = increment("2.0.0"),
	["Region3"] = increment("2.0.0"),
	["Region3int16"] = increment("2.0.0"),
	["RotationCurveKey"] = increment("2.0.0"),
	["Secret"] = increment("2.0.0"),
	["SharedTable"] = increment("2.0.0"),
	["TweenInfo"] = increment("2.0.0"),
	["UDim"] = increment("2.0.0"),
	["UDim2"] = increment("2.0.0"),
	["ValueCurveKey"] = increment("2.0.0"),
	["vector"] = increment("2.0.0"),
	["Vector2"] = increment("2.0.0"),
	["Vector2int16"] = increment("2.0.0"),
	["Vector3"] = increment("2.0.0"),
	["Vector3int16"] = increment("2.0.0"),
}

table.sort(registeredArr, function(a, b)
	local aVer = CargoSemver.Version.parse(a.semver)
	local bVer = CargoSemver.Version.parse(b.semver)

	if aVer ~= bVer then
		return aVer < bVer
	end

	return a.index < b.index
end)

for i, registered in registeredArr do
	registered.index = i
end

local typeIdMax = -1
local typeIdByTypeof = {}
for typeOf, registered in REGISTERED_BY_TYPEOF do
	local typeId = registered.index - 1
	typeIdByTypeof[typeOf] = typeId
	typeIdMax = math.max(typeIdMax, typeId)
end

function TypeIds.assert(name: string)
	return assert(typeIdByTypeof[name], `No TypeId registered for name '{name}'`)
end

assert(typeIdMax <= 255, "Registered TypeIds exceed u8 limits.")
return TypeIds


export type Supported = nil
	| boolean
	| number
	| string
	| buffer
	| { any }
	| { [any]: any }
	| Axes
	| BrickColor
	| CFrame
	| Color3
	| ColorSequence
	| ColorSequenceKeypoint
	| Enum
	| EnumItem
	| Enums
	| Faces
	| FloatCurveKey
	| Font
	| NumberRange
	| NumberSequence
	| NumberSequenceKeypoint
	| Path2DControlPoint
	| PathWaypoint
	| PhysicalProperties
	| Ray
	| Rect
	| Region3
	| Region3int16
	| RotationCurveKey
	| TweenInfo
	| UDim
	| UDim2
	| vector
	| Vector2
	| Vector2int16
	| Vector3
	| Vector3int16

export type Overridable = nil
	| boolean
	| number
	| string
	| buffer
	| Axes
	| BrickColor
	| CFrame
	| Color3
	| ColorSequence
	| ColorSequenceKeypoint
	| Enum
	| EnumItem
	| Enums
	| Faces
	| FloatCurveKey
	| Font
	| NumberRange
	| NumberSequence
	| NumberSequenceKeypoint
	| Path2DControlPoint
	| PathWaypoint
	| PhysicalProperties
	| Ray
	| Rect
	| Region3
	| Region3int16
	| RotationCurveKey
	| TweenInfo
	| UDim
	| UDim2
	| vector
	| Vector2
	| Vector2int16
	| Vector3
	| Vector3int16

return {}


local TypeUnions = require(script.TypeUnions)
local DataTypePacker = require(script.DataTypePacker)
local UserdataDefinitions = require(script.Standalone.Userdata.Definitions)

export type Supported = TypeUnions.Supported
export type Overridable = TypeUnions.Overridable

return {
	Userdata = UserdataDefinitions,
	Encoder = DataTypePacker.new({
		require(script.Standalone),
		require(script.Dependant),
	}),
}


local packagesRoot = script.Parent.Parent
local CargoSemver = require(packagesRoot.CargoSemver)

local Wally = require(script.Parent.Wally)
local DataTypes = require(script.Parent.DataTypes)
local BufferStream = require(script.Parent.BufferStream)

export type Supported = DataTypes.Supported
export type Overridable = DataTypes.Overridable

type CopyStackEntry<T> = {
	original: T,
	copy: T,
}

type CustomEncoder<T> = {
	read: (buffer) -> T,
	write: (T) -> buffer,
}

local function getWallySemVer()
	local parsed = CargoSemver.Version.parse(Wally.package.version)

	local ver = CargoSemver.Version.new(
		parsed.major,
		parsed.minor,
		parsed.patch,
		CargoSemver.Prerelease.EMPTY,
		CargoSemver.BuildMetadata.EMPTY
	)

	local majorVer =
		CargoSemver.Version.new(parsed.major, 0, 0, CargoSemver.Prerelease.EMPTY, CargoSemver.BuildMetadata.EMPTY)

	local nextMajorVer = CargoSemver.Version.new(
		parsed.major + 1,
		parsed.minor,
		parsed.patch,
		CargoSemver.Prerelease.EMPTY,
		CargoSemver.BuildMetadata.EMPTY
	)

	local length = #tostring(ver)
	assert(length <= 255, "Wally version cannot exceed 255 characters.")
	local stream = BufferStream.new(buffer.create(0))
	stream:writeu8(length)
	stream:writeString(tostring(ver), length)

	return {
		ver = ver,
		majorVer = majorVer,
		nextMajorVer = nextMajorVer,
		header = stream.b,
	}
end

local WALLY_SEMVER = getWallySemVer()

local function encodeInternal(rootTbl: { [any]: any }, encoders: { [string]: CustomEncoder<any> })
	if next(encoders) then
		local stack = {} :: { CopyStackEntry<any> }

		local pointerCount = 0
		local pointersByOriginal = {}
		local copiesByPointerIndex = {}

		local function stackCopyTbl(tbl: { [any]: any })
			local copy: { [any]: any } = {}
			pointerCount = pointerCount + 1
			pointersByOriginal[tbl] = pointerCount
			copiesByPointerIndex[pointerCount] = copy
			table.insert(stack, {
				original = tbl,
				copy = copy,
			})
			return copy
		end

		local rootTblCopy = stackCopyTbl(rootTbl)
		while #stack > 0 do
			local popped = table.remove(stack) :: CopyStackEntry<any>
			for key, value in popped.original do
				local newKey = key
				local typeofKey = typeof(key)
				if typeofKey == "table" then
					local pointerIndex = pointersByOriginal[key]
					newKey = copiesByPointerIndex[pointerIndex] or stackCopyTbl(key)
				else
					local encoder = encoders[typeofKey]
					if encoder then
						newKey = DataTypes.Userdata.Userdefined:create(typeofKey, encoder.write(key))
					end
				end

				local newValue = value
				local typeofValue = typeof(value)
				if typeofValue == "table" then
					local pointerIndex = pointersByOriginal[value]
					newValue = copiesByPointerIndex[pointerIndex] or stackCopyTbl(value)
				else
					local encoder = encoders[typeofValue]
					if encoder then
						newValue = DataTypes.Userdata.Userdefined:create(typeofValue, encoder.write(value))
					end
				end

				popped.copy[newKey] = newValue
			end
		end

		rootTbl = rootTblCopy
	end

	local stream = BufferStream.new(buffer.create(0))
	stream:writeBuffer(WALLY_SEMVER.header)
	DataTypes.Encoder:writeStream(stream, rootTbl)
	return stream.b
end

local function decodeInternal(b: buffer, encoders: { [string]: CustomEncoder<any> })
	local stream = BufferStream.new(b)
	local length = stream:readu8()
	local wallyVersion = stream:readString(length)

	local headerVer = assert(CargoSemver.Version.parse(wallyVersion), "Failed to read header version.")
	assert(
		headerVer >= WALLY_SEMVER.majorVer and headerVer < WALLY_SEMVER.nextMajorVer,
		`Attempted to decode a buffer encoded with a different major version '{headerVer}'.`
	)

	assert(
		headerVer <= WALLY_SEMVER.ver,
		`Attempted to decode a buffer encoded with a newer version of Bufferize '{headerVer}'.`
	)

	-- also don't want the version to be greater than WALLY_SEMVER.ver

	local rootTbl = DataTypes.Encoder:readStream(stream)

	if next(encoders) then
		local stack = { rootTbl }
		while #stack > 0 do
			local popped = table.remove(stack) :: { [any]: any }
			for key, value in popped do
				local newKey = key
				if typeof(key) == "table" then
					table.insert(stack, key)
				elseif DataTypes.Userdata.Userdefined:isTypeOf(key) then
					local userdefined = DataTypes.Userdata.Userdefined:read(key)
					local encoder = encoders[userdefined.kind]
					if encoder then
						newKey = encoder.read(userdefined.b)
					end
				end

				local newValue = value
				if typeof(value) == "table" then
					table.insert(stack, value)
				elseif DataTypes.Userdata.Userdefined:isTypeOf(value) then
					local userdefined = DataTypes.Userdata.Userdefined:read(value)
					local encoder = encoders[userdefined.kind]
					if encoder then
						newValue = encoder.read(userdefined.b)
					end
				end

				popped[key] = nil
				popped[newKey] = newValue
			end
		end
	end

	return rootTbl
end

-- Class

local EncoderStatic = {}
EncoderStatic.VERSION = tostring(WALLY_SEMVER.ver)

local EncoderClass = {}
EncoderClass.__index = EncoderClass
EncoderClass.ClassName = "Encoder"

export type Encoder = typeof(setmetatable(
	{} :: {
		encoders: { [string]: CustomEncoder<any> },
	},
	EncoderClass
))

function EncoderStatic.new()
	local self = setmetatable({}, EncoderClass) :: Encoder

	self.encoders = {}

	return self
end

function EncoderStatic.readVersion(b: buffer)
	local stream = BufferStream.new(b)
	local length = stream:readu8()
	local wallyVersion = stream:readString(length)
	local ver = CargoSemver.Version.parse(wallyVersion)
	return if ver then tostring(ver) else nil
end

function EncoderClass.override<T>(self: Encoder, typeOf: string, encoder: CustomEncoder<T>)
	local definition = DataTypes.Encoder.definitionsByTypeOf[typeOf]
	if definition then
		assert(definition:isOverridable(), `Cannot override encoding for the '{typeOf}' type.`)
	end
	self.encoders[typeOf] = encoder
end

function EncoderClass.encode(self: Encoder, ...: Supported)
	-- we always wrap our inputs in a table b/c it will ensure
	-- pointers are being used for duplicated tables
	local packed = table.pack(...)

	local hasGap = false
	for i = 1, packed.n do
		if packed[i] == nil then
			hasGap = true
			break
		end
	end

	if not hasGap then
		local packedAny = packed :: any
		packedAny.n = nil
	end

	return encodeInternal(packed, self.encoders)
end

function EncoderClass.decode(self: Encoder, b: buffer)
	local packed = decodeInternal(b, self.encoders)
	local packedN = packed.n or #packed
	return table.unpack(packed, 1, packedN)
end

--

return EncoderStatic


local bufferizeRoot = script.Parent.Parent
local DataTypes = require(bufferizeRoot.DataTypes)
local TableHelper = require(bufferizeRoot.DataTypes.Helpers.TableHelper)
local pointerUserdata = DataTypes.Userdata.DeduplicationPointer

local Deduplicator = {}

function Deduplicator.serialize(supportedSet: { [any]: boolean }, rootTbl: { [any]: any }): { { [any]: any } }
	if not next(supportedSet) then
		return { {}, rootTbl }
	end
	local pointerByKV = {}
	local deduplicatedArr = {}
	local deduplicatedCount = 0
	for kv, _ in supportedSet do
		deduplicatedCount = deduplicatedCount + 1
		deduplicatedArr[deduplicatedCount] = kv
		pointerByKV[kv] = pointerUserdata:create(deduplicatedCount)
	end

	local rootTblCopy = TableHelper.copyDeep(rootTbl, function(kv)
		return pointerByKV[kv] or kv
	end)

	return { deduplicatedArr, rootTblCopy }
end

function Deduplicator.serializeTypeof(typeOfsSet: { [string]: boolean }, rootTbl: { [any]: any }): { { [any]: any } }
	if not next(typeOfsSet) then
		return Deduplicator.serialize({}, rootTbl)
	end

	local countByKV = {}
	TableHelper.replaceKV(rootTbl, function(kv)
		countByKV[kv] = (countByKV[kv] or 0) + 1
		return kv
	end)

	local supportedSet = {}
	for kv, count in countByKV do
		if typeOfsSet[typeof(kv)] and count > 1 then
			supportedSet[kv] = true
		end
	end

	return Deduplicator.serialize(supportedSet, rootTbl)
end

function Deduplicator.deserialize(compressed: { { [any]: any } })
	local deduplicatedArr = compressed[1]
	return TableHelper.copyDeep(compressed[2], function(kv)
		if pointerUserdata:isTypeOf(kv) then
			return deduplicatedArr[pointerUserdata:read(kv).index]
		end
		return kv
	end)
end

return Deduplicator


local ReflectionService = game:GetService("ReflectionService") :: ReflectionService

local DataTypes = require(script.Parent.Parent.DataTypes)
local pointerUserdata = DataTypes.Userdata.InstancePointer

local InstanceSerializer = {}

local function shouldSerializeProperty(property: { [string]: any })
	return property.Permits
		and property.Permits.Read
		and property.Permits.Write
		and property.Display
		and not property.Display.DeprecationMessage
end

function InstanceSerializer.serialize(rootInstance: Instance)
	local serializedArr = {}

	local pointerCount = 0
	local pointerByInstance = {}
	local nilPointer = pointerUserdata:create(0)
	local function assignPointer(instance: Instance)
		pointerCount = pointerCount + 1
		pointerByInstance[instance] = pointerUserdata:create(pointerCount)
	end

	assignPointer(rootInstance)
	for _, descendant in rootInstance:GetDescendants() do
		assignPointer(descendant)
	end

	for instance, pointer in pointerByInstance do
		local className = instance.ClassName

		local instanceAny = instance :: any
		local defaultInstance = Instance.new(className)
		local defaultInstanceAny = defaultInstance :: any

		-- technically not a true array b/c mismatched types, but this will encode more efficiently
		local serialized: { any } = { className }
		local properties = ReflectionService:GetPropertiesOfClass(className, {
			Security = SecurityCapabilities.fromCurrent(),
			ExcludeInherited = false,
			ExcludeDisplay = false,
		})

		local j = 2
		for _, property in properties do
			local shouldSerialize = shouldSerializeProperty(property)
			if shouldSerialize and not (instance == rootInstance and property.Name == "Parent") then
				local success, err = pcall(function()
					local value = instanceAny[property.Name]
					local defaultValue = defaultInstanceAny[property.Name]
					if value ~= defaultValue then
						if typeof(value) == "Instance" then
							value = pointerByInstance[value] or nilPointer
						elseif value == nil then
							value = nilPointer
						end

						serialized[j + 1] = property.Name
						serialized[j + 2] = value
						j = j + 2
					end
				end)

				if not success then
					warn(err)
				end
			end
		end

		for attributeName, attributeValue in instance:GetAttributes() do
			serialized[j + 1] = "$" .. attributeName -- use dollar sign to identify attribute keys
			serialized[j + 2] = attributeValue
			j = j + 2
		end

		local k = 0
		for _, tagName in instance:GetTags() do
			k = k + 1
			serialized[j + 1 + k] = tagName
		end

		defaultInstance:Destroy()

		serialized[j + 1] = if k > 0 then k else nil
		serialized[2] = (j - 2) // 2
		serializedArr[pointer.value.index] = serialized
	end

	return serializedArr
end

function InstanceSerializer.deserialize(serializedArr: { any })
	local instances = {}

	for i, serialized in serializedArr do
		local className = serialized[1]
		local keyValueCount = serialized[2] * 2
		local tagCount = serialized[3 + keyValueCount] or 0

		local instance = Instance.new(className)
		local instanceAny = instance :: any

		local isAttribute = false
		for j = 1, keyValueCount, 2 do
			local key = serialized[j + 2]
			local value = serialized[j + 3]

			if pointerUserdata:isTypeOf(value) then
				local pointer = pointerUserdata:read(value)
				value = instances[pointer.index]
			end

			if isAttribute or (typeof(key) == "string" and key:sub(1, 1) == "$") then
				isAttribute = true
				instance:SetAttribute(key:sub(2), value)
			else
				instanceAny[key] = value
			end
		end

		for j = 1, tagCount do
			local tagName = serialized[3 + keyValueCount + j]
			instance:AddTag(tagName)
		end

		instances[i] = instance
	end

	return instances[1]
end

return InstanceSerializer


local Encoder = require(script.Encoder)
local BufferStream = require(script.BufferStream)

local Deduplicator = require(script.Serializers.Deduplicator)
local InstanceSerializer = require(script.Serializers.InstanceSerializer)

export type Encoder = Encoder.Encoder
export type BufferStream = BufferStream.BufferStream

export type Supported = Encoder.Supported
export type Overridable = Encoder.Overridable

local Bufferize = {}
local defaultEncoder = Encoder.new()

Bufferize.VERSION = Encoder.VERSION
Bufferize.readVersion = Encoder.readVersion

Bufferize.custom = Encoder.new
Bufferize.stream = BufferStream.new

Bufferize.deduplicateTypeof = Deduplicator.serializeTypeof
Bufferize.deduplicate = Deduplicator.serialize
Bufferize.reduplicate = Deduplicator.deserialize

Bufferize.serializeInstance = InstanceSerializer.serialize
Bufferize.deserializeInstance = InstanceSerializer.deserialize

function Bufferize.encode(...: Supported)
	return defaultEncoder:encode(...)
end

function Bufferize.decode(b: buffer)
	return defaultEncoder:decode(b)
end

return Bufferize


local pkgRoot = game.ReplicatedStorage.Packages.Bufferize
local runCLI = require(game.ReplicatedStorage.DevPackages.Jest).runCLI

local processServiceExists, ProcessService = pcall(function()
	return game:GetService("ProcessService")
end)

local status, result = runCLI(pkgRoot, {
	ci = false,
	passWithNoTests = true,
}, { pkgRoot }):awaitStatus()

if status == "Rejected" then
	print(result)
end

if status == "Resolved" and result.results.numFailedTestSuites == 0 and result.results.numFailedTests == 0 then
	if processServiceExists then
		ProcessService:ExitAsync(0)
	end
end

if processServiceExists then
	ProcessService:ExitAsync(1)
end

return nil


local devPackagesRoot = game.ReplicatedStorage.DevPackages
local JestGlobals = require(devPackagesRoot.JestGlobals)

local it = JestGlobals.it
local expect = JestGlobals.expect
local describe = JestGlobals.describe

local bufferizeRoot = game.ReplicatedStorage.Packages.Bufferize
local DataTypeDefinition = require(bufferizeRoot.DataTypes.DataTypeDefinition)
local BufferStream = require(bufferizeRoot.BufferStream)

local TRUNCATED_MAX = 100

type DataTypeTest<T> = {
	values: { [number]: T, n: number },
	compare: (T, T) -> boolean,
}

local TestHelper = {}

function TestHelper.values<T>(values: { [number]: T, n: number }, callback: (T) -> ())
	if values.n == 0 then
		it("should have no tests to run.", function()
			expect(true).toEqual(true)
		end)
	end

	for i = 1, values.n do
		local value = values[i]
		local valueTypeof = typeof(value)
		local truncated = tostring(value):gsub(`^{valueTypeof}: `, "")
		if #truncated > TRUNCATED_MAX then
			truncated = truncated:sub(1, TRUNCATED_MAX) .. "... [truncated]"
		end

		describe(`{i}. {valueTypeof}: {truncated}`, function()
			callback(value)
		end)
	end
end

function TestHelper.dataType<T>(definition: DataTypeDefinition.DataTypeDefinition<T>, test: DataTypeTest<T>)
	return TestHelper.values(test.values, function(value)
		local stream = BufferStream.new(buffer.create(0))

		if definition.support ~= "implemented" then
			it("should fail to encode b/c the definition is unimplemented.", function()
				local success = pcall(function()
					definition:writeStream(stream, value)
				end)
				expect(success).toEqual(false)
			end)
		else
			it("should encode to a buffer.", function()
				definition:writeStream(stream, value)
				expect(typeof(stream.b)).toEqual("buffer")
			end)

			it("should decode losslessly.", function()
				stream.cursor = 0
				local decodedValue = definition:readStream(stream)
				expect(test.compare(decodedValue, value)).toEqual(true)
			end)
		end
	end)
end

function TestHelper.SKIP_WRAP(callback: () -> ())
	return describe.skip("SKIPPED", callback)
end

return TestHelper


local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local RBXConnectionManager = {}
RBXConnectionManager.__index = RBXConnectionManager

-- Constructor --
function RBXConnectionManager.new(monitoring_checker_interval: number?, osdate_format: string?)
	local self = setmetatable({}, RBXConnectionManager)
	self._connections = {} :: { [string]: RBXScriptConnection }
	self._internal_connections = {} :: { RBXScriptConnection }
	self._monitoring = {} :: { [string]: any }
	self._osdate_format = osdate_format or "%Y-%m-%d %H:%M:%S"
	self._threads = { ["auto_disconnect"] = {} }

	-- Cleanup player-specific connections (server-side feature)
	if RunService:IsServer() then
		local globalPlayersRemovingConnection = Players.PlayerRemoving:Connect(function(playerObj: Player)
			self:DisconnectAllInGroup(tostring(playerObj.UserId))
		end)
		table.insert(self._internal_connections, globalPlayersRemovingConnection)
	end

	self:_StartMonitoring(monitoring_checker_interval)

	return self
end

-- Methods to change settings --
function RBXConnectionManager:ChangeMonitoringCheckInterval(new_interval: number)
	self:_StartMonitoring(new_interval)
end

function RBXConnectionManager:ChangeOsDateFormat(new_osdate_format: string)
	self._osdate_format = new_osdate_format
end

function RBXConnectionManager:AddAutoDisconnect(group_name: string, event: RBXScriptSignal)
	self._threads["auto_disconnect"][group_name] = {}
	table.insert(
		self._threads["auto_disconnect"][group_name],
		event:Connect(function()
			self:DisconnectAllInGroup(group_name)
		end)
	)
end

-- Action Methods --

-- Add a connection
function RBXConnectionManager:Connect(
	name: string,
	event: RBXScriptSignal,
	callback,
	monitoring: boolean?
): RBXScriptConnection
	if self._connections[name] then
		-- Disconnect and replace the existing connection
		self:Disconnect(name)
		self._connections[name] = nil
	end

	-- Enable monitoring if specified
	if monitoring == true then
		self._monitoring[name] = {}
		local original_callback = callback
		callback = function(...)
			-- Log the data with date time format
			table.insert(self._monitoring[name], {
				date_time = os.date(self._osdate_format),
				arguments = { ... },
			})

			-- Execute the original callback
			original_callback(...)
		end
	end

	-- Create the connection
	local connection = event:Connect(callback)
	self._connections[name] = connection

	return connection
end

-- Disconnect connection by name
function RBXConnectionManager:Disconnect(name: string)
	local connection = self._connections[name]
	if connection and connection.Connected then
		connection:Disconnect()
		self._connections[name] = nil
	end
end

-- Disconnect all connections within a specific group
function RBXConnectionManager:DisconnectAllInGroup(group: string)
	for name, connection in pairs(self._connections) do
		if string.find(name, group) then
			if connection.Connected then
				connection:Disconnect()
			end
			self._connections[name] = nil
		end
	end
end

-- Disconnect all connections
function RBXConnectionManager:DisconnectAll()
	for _, connection in pairs(self._connections) do
		if connection.Connected then
			connection:Disconnect()
		end
	end
	self._connections = {}
end

-- Self-destruct
function RBXConnectionManager:Destroy()
	-- Disconnect the module's internal connections
	for _, connection in pairs(self._internal_connections) do
		if connection.Connected then
			connection:Disconnect()
		end
	end
	self._internal_connections = {}

	self:DisconnectAll()
	setmetatable(self, nil)
end

-- Get monitoring results gathered until now
function RBXConnectionManager:GetAllMonitoringData()
	for connection_name, monitoring_data in self._monitoring do
		if not monitoring_data or type(monitoring_data) ~= "table" then
			warn("Monitoring disabled for " .. connection_name)
		else
			local total_logs = #monitoring_data
			for i, log in ipairs(monitoring_data) do
				warn(
					"Monitoring log for " .. connection_name .. ": #" .. tostring(i) .. "/" .. tostring(total_logs),
					log
				)
			end
		end
	end
end

-- Will show up new monitoring results when collected
function RBXConnectionManager:_StartMonitoring(checker_interval: number?)
	if self._threads["monitoring"] ~= nil then
		task.cancel(self._threads["monitoring"])
	end
	self._threads["monitoring"] = task.spawn(function()
		-- Store latest known logs for each connection
		local last_log_counts = {}

		while true do
			for connection_name, monitoring_data in pairs(self._monitoring) do
				-- Check if monitoring is enabled for this connection
				if type(monitoring_data) == "table" then
					local previous_count = last_log_counts[connection_name] or 0
					local current_count = #monitoring_data

					-- If there are new logs, show them
					if current_count > previous_count then
						for i = previous_count + 1, current_count do
							local log = monitoring_data[i]
							warn(
								"Monitoring log for "
									.. connection_name
									.. ": #"
									.. tostring(i)
									.. "/"
									.. tostring(current_count),
								log
							)
						end

						-- Update latest known log count
						last_log_counts[connection_name] = current_count
					end
				end
			end
			task.wait(checker_interval or 1)
		end
	end)
end

return RBXConnectionManager


--[[
Cryptography library: Advanced Encryption Standard (Galois/Counter Mode)

	Security Limitations:
		Personally don't really like AES-GCM as it has a 96-bit nonce
		which creates birthday bound issues after 2^32 encryptions with the same key,
		collision probability reaches 2^-32 (NIST's "soft" birthday bound).
		Which is why I prefer ChaCha20-Poly1305 but you could rotate keys often.

		AES-GCM is also not key committing meaning the same ciphertext can decrypt to
		different plaintexts under different keys with valid authentication tags.
		This is the "Invisible Salamander" attack, where an attacker has a message
		that appears normal to moderators but evil to recievers, or vice versa.
		Affects group messaging, and any scenario with multiple valid keys.

		The S-box lookup tables are vulnerable to cache timing attacks in
		theory, but constant time implementations would kill performance.
		And it's not really possible in Roblox in the first place.
		https://bearssl.org/constanttime.html

	IV Handling:
		- Store IV alongside ciphertext (IVs are not secret)
		- You can send IV with encrypted data
		- Never reuse IV with same key (this breaks the security)
		- Consider key rotation after 2^32 operations to avoid birthday attacks
--]]
--!strict

local ReplicatedStorage = game:GetService("ReplicatedStorage")

-- Get the libraries
local Cryptography = require(ReplicatedStorage:WaitForChild("Cryptography"))
local AES = Cryptography.Encryption.AES
local CSPRNG = Cryptography.Utilities.CSPRNG

local Key = CSPRNG.RandomBytes(32)
local IV = CSPRNG.RandomBytes(12)

local Message = "This is a secret message"
local Plaintext = buffer.fromstring(Message)

-- Optional: Additional Authenticated Data (authenticated but not encrypted)
local AAD = buffer.fromstring("user:john,timestamp:456745683")

local Ciphertext, Tag = AES.Encrypt(Plaintext, Key, IV, AAD)

print("Encryption successful!")
print("Ciphertext length:", buffer.len(Ciphertext))
print("Tag length:", buffer.len(Tag))

-- Decrypt and verify (returns false on failure, true and buffer on success)
local Success, DecryptedData = AES.Decrypt(Ciphertext, Key, IV, Tag, AAD)
if Success and DecryptedData then
	local DecryptedMessage = buffer.tostring(DecryptedData)
	print("Decryption successful!")
	print("Original message:", Message)
	print("Decrypted message:", DecryptedMessage)
	print("Messages match:", Message == DecryptedMessage)
else
	warn("Decryption failed, data was messed with!")
end


--[=[
	Cryptography library: EdDSA Batch Verification
	Efficient Verification of Multiple Signatures

	This example shows how u can batch verification of multiple Ed25519 signatures.
	Batch verification is more efficient than verifying signatures individually
	when you need to validate many signatures at once.
--]=]
--!strict

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Cryptography = require(ReplicatedStorage:WaitForChild("Cryptography"))

local EdDSA = Cryptography.Verification.EdDSA
local CSPRNG = Cryptography.Utilities.CSPRNG

-- Generate multiple keypairs (different signers)
local Signer1Private = CSPRNG.Ed25519Random()
local Signer1Public = EdDSA.PublicKey(Signer1Private)

local Signer2Private = CSPRNG.Ed25519Random()
local Signer2Public = EdDSA.PublicKey(Signer2Private)

local Signer3Private = CSPRNG.Ed25519Random()
local Signer3Public = EdDSA.PublicKey(Signer3Private)

-- Create messages and sign them
local Message1 = buffer.fromstring("Transaction: Alice sends 100 to Bob")
local Message2 = buffer.fromstring("Transaction: Bob sends 50 to Charlie")
local Message3 = buffer.fromstring("Transaction: Charlie sends 25 to Dave")

local Signature1 = EdDSA.Sign(Message1, Signer1Private, Signer1Public)
local Signature2 = EdDSA.Sign(Message2, Signer2Private, Signer2Public)
local Signature3 = EdDSA.Sign(Message3, Signer3Private, Signer3Public)

-- Batch verification: verify all signatures at once
local Entries = {
	{ Message = Message1, PublicKey = Signer1Public, Signature = Signature1 },
	{ Message = Message2, PublicKey = Signer2Public, Signature = Signature2 },
	{  Message = Message3, PublicKey = Signer3Public,Signature = Signature3 },
}

-- One of the drawbacks is that you do not know which sgnature failed and have to throw out the whole batch
local AllValid = EdDSA.VerifyBatch(Entries)
if AllValid then
	print("All signatures verified successfully")
else
	print("Batch verification failed at least one signature is invalid")
end

-- Show that batch fails if any signature is tampered
local TamperedSignature = buffer.create(64)
buffer.copy(TamperedSignature, 0, Signature3, 0, 64)
buffer.writeu8(TamperedSignature, 0, bit32.bxor(buffer.readu8(TamperedSignature, 0), 0x01))

local TamperedEntries = {
	{ Message = Message1, PublicKey = Signer1Public, Signature = Signature1 },
	{ Message = Message2, PublicKey = Signer2Public, Signature = Signature2 },
	{ Message = Message3, PublicKey = Signer3Public, Signature = TamperedSignature },
}

local TamperedValid = EdDSA.VerifyBatch(TamperedEntries)
assert(TamperedValid == false, "Tampered batch should fail verification")

print("Batch verification completed")


--[[ Cryptography library: CSPRNG Example ]]--
--!strict

local ReplicatedStorage = game:GetService("ReplicatedStorage")

-- Get the library
local Cryptography = require(ReplicatedStorage:WaitForChild("Cryptography"))
local CSPRNG = Cryptography.Utilities.CSPRNG
-- Also accessible through EdDSA, MLKEM andMLDSA: Verification.EdDSA/MLKEM/MLDSA.CSPRNG

--[[
	CSPRNG stands for:
		Cryptographically Secure
		Pseudo Random Number Generator

	CSPRNG works by creating entropy out of various factors
	then using the entropy as the internal state of the RNG

	To generate random numbers, CSPRNG creates a 64 byte block
	of random data and takes however many bytes is needed for
	the output data i.e 8 bytes for a float/integer, 20 bytes for a 20
	character long string. Once all 64 bytes are used up, an
	another block is generated which may slow down the call.
	The generated block can be extended to an arbitrary length
	with Blake3 by enabling CSPRNG.BlockExpansion

	Developers can renew the state and add their own entropy by passing it as a buffer in CSRNG.Reseed/CSPRNG.AddEntropyProvider

	CSPRNG will also automatically renew the state after a certain amount of calls controlled
	by CSRNG.RekeyAfter (by default 1024, so 64 * 1024 bytes without block expansion)
--]]

-- Generate a random number between 0 and 1
print(CSPRNG.Random())

-- Generate a random number between 10 and 100
print(CSPRNG.RandomNumber(10, 100))

-- Generate a random number between 0 and 100
print(CSPRNG.RandomNumber(100))

-- Generate a random key for EdDSA
local PrivateKey = CSPRNG.Ed25519Random()

--[[
	Reseed the CSPRNG instance with custom entropy for extra security
	You can pass any buffer as long as it's 1024 bytes or less, althought not all of it
	may be used as the normal entropy is added first, custom has last priority
	You can also use CSPRNG.AddEntropyProvider to add a function that returns a buffer
	AddEntropyProvider will only be called once its used up all the entropy from init
	So you need to do `CSPRNG.Reseed(CustomEntropy())` if you want it from the gecko
]]
local LastPlayerWeaponUse = 12317127.123
local AmountOfRemoteCalls = 1282
local CustomEntropy = buffer.create(8 + 4)
buffer.writef64(CustomEntropy, 0, LastPlayerWeaponUse) -- 8 bytes
buffer.writeu32(CustomEntropy, 8, AmountOfRemoteCalls) -- 4 bytes

local Provider = function()
	return CustomEntropy
end

CSPRNG.AddEntropyProvider(Provider)
CSPRNG.RemoveEntropyProvider(Provider)
CSPRNG.Reseed(CustomEntropy)


--[=[
	Cryptography library: Ephemeral X25519 Key Exchange
	Forward Secrecy - Client to Server Key Exchange with Ephemeral Keys

	This example shows how to do proper ephemeral key exchange that provides forward secrecy.
	Fresh private keys are generated each session and destroyed after use.
	Even if long-term keys are compromised, past communications remain secure.
--]=]
--!strict

local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Get the library
local Cryptography = require(ReplicatedStorage:WaitForChild("Cryptography"))

-- Create variables for easy access
local Blake3 = Cryptography.Hashing.Blake3
local FromHex = Cryptography.Utilities.Conversions.FromHex
local EdDSA = Cryptography.Verification.EdDSA
local AEAD = Cryptography.Encryption.AEAD
local CSPRNG = Cryptography.Utilities.CSPRNG

-- Long-term identity keys (persistent across sessions)
local ClientIdentityPrivate = CSPRNG.Ed25519Random()
local ClientIdentityPublic = EdDSA.PublicKey(ClientIdentityPrivate)

local ServerIdentityPrivate = CSPRNG.Ed25519Random()
local _ServerIdentityPublic = EdDSA.PublicKey(ServerIdentityPrivate)

-- *Client and server exchange identity public keys*

local function GetSessionKey(EphemeralSharedSecret: buffer, ClientPublic: buffer, ServerPublic: buffer): buffer
	local CombinedInput = buffer.create(96)

	buffer.copy(CombinedInput, 0, EphemeralSharedSecret, 0, 32)
	buffer.copy(CombinedInput, 32, ClientPublic, 0, 32)
	buffer.copy(CombinedInput, 64, ServerPublic, 0, 32)

	local KeyMaterial = Blake3.Digest(CombinedInput, 32)
	return FromHex(KeyMaterial)
end

-- SESSION 1: Generate fresh ephemeral keys
local ClientEphemeralPrivate1 = CSPRNG.Ed25519Random()
local ClientEphemeralPublic1 = EdDSA.PublicKey(ClientEphemeralPrivate1)

local ServerEphemeralPrivate1 = CSPRNG.Ed25519Random()
local ServerEphemeralPublic1 = EdDSA.PublicKey(ServerEphemeralPrivate1)

-- *Ephemeral public keys are exchanged*

-- Both sides derive the same session key from ephemeral exchange
-- Note: In real implementation, use proper X25519.Exchange(ClientEphemeralPrivate1, ServerEphemeralPublic1)
local SharedSecret1 = CSPRNG.RandomBytes(32) -- Simulated shared secret
local SessionKey1 = GetSessionKey(SharedSecret1, ClientEphemeralPublic1, ServerEphemeralPublic1)

-- Encrypt with ephemeral session key
local Message1 = buffer.fromstring("Session 1 data")
local Nonce1 = buffer.fromstring("nonce1234567")
local Encrypted1, Tag1 = AEAD.Encrypt(Message1, SessionKey1, Nonce1)

-- Overwrite ephemeral private keys (the actual forward secrecy)
ClientEphemeralPrivate1 = buffer.create(0)
ServerEphemeralPrivate1 = buffer.create(0)

-- SESSION 2: Generate completely new ephemeral keys
local ClientEphemeralPrivate2 = CSPRNG.Ed25519Random()
local ClientEphemeralPublic2 = EdDSA.PublicKey(ClientEphemeralPrivate2)

local ServerEphemeralPrivate2 = CSPRNG.Ed25519Random()
local ServerEphemeralPublic2 = EdDSA.PublicKey(ServerEphemeralPrivate2)

-- Derive different session key for session 2
local SharedSecret2 = CSPRNG.RandomBytes(32) -- Different shared secret
local SessionKey2 = GetSessionKey(SharedSecret2, ClientEphemeralPublic2, ServerEphemeralPublic2)

-- Encrypt new message
local Message2 = buffer.fromstring("Session 2 data")
local Nonce2 = buffer.fromstring("nonce7654321")
local Encrypted2, Tag2 = AEAD.Encrypt(Message2, SessionKey2, Nonce2)

-- Destroy second session ephemeral keys
ClientEphemeralPrivate2 = buffer.create(0)
ServerEphemeralPrivate2 = buffer.create(0)

-- Authentication: Sign ephemeral public keys with long-term identity keys
local EphemeralSignature = EdDSA.Sign(ClientEphemeralPublic2, ClientIdentityPrivate, ClientIdentityPublic)
local SignatureValid = EdDSA.Verify(ClientEphemeralPublic2, ClientIdentityPublic, EphemeralSignature)

-- Verify decryption works
local Decrypted1 = AEAD.Decrypt(Encrypted1, SessionKey1, Nonce1, Tag1)
local Decrypted2 = AEAD.Decrypt(Encrypted2, SessionKey2, Nonce2, Tag2)

assert(Decrypted1 and buffer.tostring(Decrypted1) == "Session 1 data")
assert(Decrypted2 and buffer.tostring(Decrypted2) == "Session 2 data")
assert(SignatureValid)

print("Forward secrecy key exchange completed")


--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")

-- Get the library
local Cryptography = require(ReplicatedStorage:WaitForChild("Cryptography"))
local MLDSA = Cryptography.Verification.MlDSA
local CSPRNG = Cryptography.Utilities.CSPRNG

--[[
	ML-DSA stands for:
		Module-Lattice-based
		Digital Signature Algorithm

	ML-DSA is a post-quantum digital signature scheme standardized
	in NIST FIPS 204. It provides three security levels and supports
	both deterministic and hedged (randomized) signing.

	Hedged signing uses 32 bytes of randomness for enhanced security.
	Context can be used for domain separation (max 255 bytes).
--]]

-- Generate key pair
local PubKey, SecKey = MLDSA.ML_DSA_44.GenerateKeys()

-- Sign message with hedged signing (recommended)
local Message = buffer.fromstring("Hello, post-quantum world!")
local Context = buffer.create(0) -- Empty context
local Randomness = CSPRNG.RandomBytes(32)
local Signature = buffer.create(MLDSA.ML_DSA_44.SigByteLen)

local SignSuccess = MLDSA.ML_DSA_44.Sign(Message, Randomness, SecKey, Context, Signature)
if not SignSuccess then
	error("Signing failed")
end

-- Verify signature
local Valid = MLDSA.ML_DSA_44.Verify(Message, PubKey, Context, Signature)
print("Signature valid:", Valid)

-- Deterministic signing (zero randomness)
local DetRandom = buffer.create(32)
local DetSig = buffer.create(MLDSA.ML_DSA_44.SigByteLen)
MLDSA.ML_DSA_44.Sign(Message, DetRandom, SecKey, Context, DetSig)

-- Context example
local ContextData = buffer.fromstring("game-session")
local CtxSig = buffer.create(MLDSA.ML_DSA_44.SigByteLen)
MLDSA.ML_DSA_44.Sign(Message, CSPRNG.RandomBytes(32), SecKey, ContextData, CtxSig)
local CtxValid = MLDSA.ML_DSA_44.Verify(Message, PubKey, ContextData, CtxSig)
print("Context signature valid:", CtxValid)

-- Different security levels
local Pub65, Sec65 = MLDSA.ML_DSA_65.GenerateKeys()
local Pub87, Sec87 = MLDSA.ML_DSA_87.GenerateKeys()

-- Key and signature sizes
print("ML-DSA-44 sizes:", buffer.len(PubKey), buffer.len(SecKey), buffer.len(Signature))
print("ML-DSA-65 sizes:", buffer.len(Pub65), buffer.len(Sec65), MLDSA.ML_DSA_65.SigByteLen)
print("ML-DSA-87 sizes:", buffer.len(Pub87), buffer.len(Sec87), MLDSA.ML_DSA_87.SigByteLen)
-- Quite big


--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Get the library
local Cryptography = require(ReplicatedStorage:WaitForChild("Cryptography"))
local MlKem = Cryptography.Verification.MlKEM
local CSPRNG = Cryptography.Utilities.CSPRNG

--[[
	ML-KEM stands for:
		Module-Lattice-based
		Key Encapsulation Mechanism

	ML-KEM is a post-quantum key encapsulation method from NIST FIPS 203.
	It allows two parties to establish a shared 32 byte secret
	that can be used for symmetric encryption.

	ML-KEM needs two 32 byte seeds for key generation and does
	encapsulation with a 32 byte message to create a ciphertext and shared secret.
	The recipient uses their secret key to decapsulate and recover the same shared secret.
--]]

-- Generate keypair using required seeds
local SeedD = CSPRNG.RandomBytes(32)
local SeedZ = CSPRNG.RandomBytes(32)
local PublicKey, SecretKey = MlKem.MLKEM_768.KeyGen(SeedD, SeedZ)

--[[
	ValidateDecapsulationKey checks the integrity of a decapsulation key.

	The decapsulation key contains an embedded copy of the public key and
	a hash of that public key. This function recomputes the hash and verifies
	it matches the stored hash, detecting any corruption or tampering.

	Per FIPS 203 Section 7.3, this check doesn't need to run on every
	decapsulation. You can call it once when:
		- Loading a key from storage
		- Receiving a key from another source
		- After key generation (optional, as freshly generated keys are valid)

	Returns true if the key is valid, false if corrupted/tampered.
--]]
local IsKeyValid = MlKem.MLKEM_768.ValidateDecapsulationKey(SecretKey)
if not IsKeyValid then
	error("Decapsulation key failed integrity check")
end

-- Encapsulate with 32-byte message
local Message = CSPRNG.RandomBytes(32)
local Ciphertext, SharedSecret = MlKem.MLKEM_768.Encapsulate(Message, PublicKey)

if not Ciphertext or not SharedSecret then
	error("Encapsulation failed")
end

-- Decapsulate to recover shared secret
local RecoveredSecret = MlKem.MLKEM_768.Decapsulate(Ciphertext, SecretKey)

-- Verify secrets match
print("Success:", MlKem.SecretsEqual(SharedSecret, RecoveredSecret))

-- Different security levels
local Pub512, Sec512 = MlKem.MLKEM_512.KeyGen(CSPRNG.RandomBytes(32), CSPRNG.RandomBytes(32))
local Pub1024, Sec1024 = MlKem.MLKEM_1024.KeyGen(CSPRNG.RandomBytes(32), CSPRNG.RandomBytes(32))

-- Key sizes
print("ML-KEM-512 sizes:", buffer.len(Pub512), buffer.len(Sec512))
print("ML-KEM-768 sizes:", buffer.len(PublicKey), buffer.len(SecretKey))
print("ML-KEM-1024 sizes:", buffer.len(Pub1024), buffer.len(Sec1024))
-- Quite big