local ServerScriptService = game:GetService("ServerScriptService")

local Test = require(ServerScriptService.TestRunner.Test)

return function(ctx: Test.TestContext)
	local TableUtil = require(script.Parent)

	ctx:Describe("Copy (Deep)", function()
		ctx:Test("should create a deep table copy", function()
			local tbl = { a = { b = { c = { d = 32 } } } }
			local tblCopy = TableUtil.Copy(tbl, true)
			ctx:Expect(tbl):Not():ToBe(tblCopy)
			ctx:Expect(tbl.a):Not():ToBe(tblCopy.a)
			ctx:Expect(tblCopy.a.b.c.d):ToBe(tbl.a.b.c.d)
		end)
	end)

	ctx:Describe("Copy (Shallow)", function()
		ctx:Test("should create a shallow dictionary copy", function()
			local tbl = { a = { b = { c = { d = 32 } } } }
			local tblCopy = TableUtil.Copy(tbl)
			ctx:Expect(tblCopy):Not():ToBe(tbl)
			ctx:Expect(tblCopy.a):ToBe(tbl.a)
			ctx:Expect(tblCopy.a.b.c.d):ToBe(tbl.a.b.c.d)
		end)

		ctx:Test("should create a shallow array copy", function()
			local tbl = { 10, 20, 30, 40 }
			local tblCopy = TableUtil.Copy(tbl)
			ctx:Expect(tblCopy):Not():ToBe(tbl)
			for i, v in ipairs(tbl) do
				ctx:Expect(tblCopy[i]):ToBe(v)
			end
		end)
	end)

	ctx:Describe("Sync", function()
		ctx:Test("should sync tables", function()
			local template = { a = 32, b = 64, c = 128, e = { h = 1 } }
			local tblSrc = { a = 32, b = 10, d = 1, e = { h = 2, n = 2 }, f = { x = 10 } }
			local tbl = TableUtil.Sync(tblSrc, template)
			ctx:Expect(tbl.a):ToBe(template.a)
			ctx:Expect(tbl.b):ToBe(10)
			ctx:Expect(tbl.c):ToBe(template.c)
			ctx:Expect(tbl.d):ToBeNil()
			ctx:Expect(tbl.e.h):ToBe(2)
			ctx:Expect(tbl.e.n):ToBeNil()
			ctx:Expect(tbl.f):ToBeNil()
		end)
	end)

	ctx:Describe("Reconcile", function()
		ctx:Test("should reconcile table", function()
			local template = { kills = 0, deaths = 0, xp = 10, stuff = {}, stuff2 = "abc", stuff3 = { "data" } }
			local data =
				{ kills = 10, deaths = 4, stuff = { "abc", "xyz" }, extra = 5, stuff2 = { abc = 10 }, stuff3 = true }
			local reconciled = TableUtil.Reconcile(data, template)
			ctx:Expect(reconciled):Not():ToBe(data)
			ctx:Expect(reconciled):Not():ToBe(template)
			ctx:Expect(reconciled.kills):ToBe(10)
			ctx:Expect(reconciled.deaths):ToBe(4)
			ctx:Expect(reconciled.xp):ToBe(10)
			ctx:Expect(reconciled.stuff[1]):ToBe("abc")
			ctx:Expect(reconciled.stuff[2]):ToBe("xyz")
			ctx:Expect(reconciled.extra):ToBe(5)
			ctx:Expect(type(reconciled.stuff2)):ToBe("table")
			ctx:Expect(reconciled.stuff2):Not():ToBe(data.stuff2)
			ctx:Expect(reconciled.stuff2.abc):ToBe(10)
			ctx:Expect(type(reconciled.stuff3)):ToBe("boolean")
			ctx:Expect(reconciled.stuff3):ToBe(true)
		end)
	end)

	ctx:Describe("SwapRemove", function()
		ctx:Test("should swap remove index", function()
			local tbl = { 1, 2, 3, 4, 5 }
			TableUtil.SwapRemove(tbl, 3)
			ctx:Expect(#tbl):ToBe(4)
			ctx:Expect(tbl[3]):ToBe(5)
		end)
	end)

	ctx:Describe("SwapRemoveFirstValue", function()
		ctx:Test("should swap remove first value given", function()
			local tbl = { "hello", "world", "goodbye", "planet" }
			TableUtil.SwapRemoveFirstValue(tbl, "world")
			ctx:Expect(#tbl):ToBe(3)
			ctx:Expect(tbl[2]):ToBe("planet")
		end)
	end)

	ctx:Describe("Map", function()
		ctx:Test("should map table", function()
			local tbl = {
				{ FirstName = "John", LastName = "Doe" },
				{ FirstName = "Jane", LastName = "Smith" },
			}
			local tblMapped = TableUtil.Map(tbl, function(person)
				return person.FirstName .. " " .. person.LastName
			end)
			ctx:Expect(tblMapped[1]):ToBe("John Doe")
			ctx:Expect(tblMapped[2]):ToBe("Jane Smith")
		end)
	end)

	ctx:Describe("Filter", function()
		ctx:Test("should filter table", function()
			local tbl = { 10, 20, 30, 40, 50, 60, 70, 80, 90 }
			local tblFiltered = TableUtil.Filter(tbl, function(n)
				return (n >= 30 and n <= 60)
			end)
			ctx:Expect(#tblFiltered):ToBe(4)
			ctx:Expect(tblFiltered[1]):ToBe(30)
			ctx:Expect(tblFiltered[#tblFiltered]):ToBe(60)
		end)
	end)

	ctx:Describe("Reduce", function()
		ctx:Test("should reduce table with numbers", function()
			local tbl = { 1, 2, 3, 4, 5 }
			local reduced = TableUtil.Reduce(tbl, function(accum, value)
				return accum + value
			end)
			ctx:Expect(reduced):ToBe(15)
		end)

		ctx:Test("should reduce table", function()
			local tbl = { { Score = 10 }, { Score = 20 }, { Score = 30 } }
			local reduced = TableUtil.Reduce(tbl, function(accum, value)
				return accum + value.Score
			end, 0)
			ctx:Expect(reduced):ToBe(60)
		end)

		ctx:Test("should reduce table with initial value", function()
			local tbl = { { Score = 10 }, { Score = 20 }, { Score = 30 } }
			local reduced = TableUtil.Reduce(tbl, function(accum, value)
				return accum + value.Score
			end, 40)
			ctx:Expect(reduced):ToBe(100)
		end)

		ctx:Test("should reduce functions", function()
			local function Square(x)
				return x * x
			end
			local function Double(x)
				return x * 2
			end
			local Func = TableUtil.Reduce({ Square, Double }, function(a, b)
				return function(x)
					return a(b(x))
				end
			end)
			local result = Func(10)
			ctx:Expect(result):ToBe(400)
		end)
	end)

	ctx:Describe("Assign", function()
		ctx:Test("should assign tables", function()
			local target = { a = 32, x = 100 }
			local t1 = { b = 64, c = 128 }
			local t2 = { a = 10, c = 100, d = 200 }
			local tbl = TableUtil.Assign(target, t1, t2)
			ctx:Expect(tbl.a):ToBe(10)
			ctx:Expect(tbl.b):ToBe(64)
			ctx:Expect(tbl.c):ToBe(100)
			ctx:Expect(tbl.d):ToBe(200)
			ctx:Expect(tbl.x):ToBe(100)
		end)
	end)

	ctx:Describe("Extend", function()
		ctx:Test("should extend tables", function()
			local tbl = { "a", "b", "c" }
			local extension = { "d", "e", "f" }
			local extended = TableUtil.Extend(tbl, extension)
			ctx:Expect(table.concat(extended)):ToBe("abcdef")
		end)
	end)

	ctx:Describe("Reverse", function()
		ctx:Test("should create a table in reverse", function()
			local tbl = { 1, 2, 3 }
			local tblRev = TableUtil.Reverse(tbl)
			ctx:Expect(table.concat(tblRev)):ToBe("321")
		end)
	end)

	ctx:Describe("Shuffle", function()
		ctx:Test("should shuffle a table", function()
			local tbl = { 1, 2, 3, 4, 5 }
			ctx:Expect(function()
				TableUtil.Shuffle(tbl)
			end)
				:Not()
				:ToThrow()
		end)
	end)

	ctx:Describe("Sample", function()
		ctx:Test("should sample a table", function()
			local tbl = { 1, 2, 3, 4, 5 }
			local sample = TableUtil.Sample(tbl, 3)
			ctx:Expect(#sample):ToBe(3)
		end)
	end)

	ctx:Describe("Flat", function()
		ctx:Test("should flatten table", function()
			local tbl = { 1, 2, 3, { 4, 5, { 6, 7 } } }
			local tblFlat = TableUtil.Flat(tbl, 3)
			ctx:Expect(table.concat(tblFlat)):ToBe("1234567")
		end)
	end)

	ctx:Describe("FlatMap", function()
		ctx:Test("should map and flatten table", function()
			local tbl = { 1, 2, 3, 4, 5, 6, 7 }
			local tblFlat = TableUtil.FlatMap(tbl, function(n)
				return { n, n * 2 }
			end)
			ctx:Expect(table.concat(tblFlat)):ToBe("12243648510612714")
		end)
	end)

	ctx:Describe("Keys", function()
		ctx:Test("should give all keys of table", function()
			local tbl = { a = 1, b = 2, c = 3 }
			local keys = TableUtil.Keys(tbl)
			ctx:Expect(#keys):ToBe(3)
			ctx:Expect(table.find(keys, "a")):ToBeOk()
			ctx:Expect(table.find(keys, "b")):ToBeOk()
			ctx:Expect(table.find(keys, "c")):ToBeOk()
		end)
	end)

	ctx:Describe("Values", function()
		ctx:Test("should give all values of table", function()
			local tbl = { a = 1, b = 2, c = 3 }
			local values = TableUtil.Values(tbl)
			ctx:Expect(#values):ToBe(3)
			ctx:Expect(table.find(values, 1)):ToBeOk()
			ctx:Expect(table.find(values, 2)):ToBeOk()
			ctx:Expect(table.find(values, 3)):ToBeOk()
		end)
	end)

	ctx:Describe("Find", function()
		ctx:Test("should find item in array", function()
			local tbl = { 10, 20, 30 }
			local item, index = TableUtil.Find(tbl, function(value)
				return (value == 20)
			end)
			ctx:Expect(item):ToBeOk()
			ctx:Expect(index):ToBe(2)
			ctx:Expect(item):ToBe(20)
		end)

		ctx:Test("should find item in dictionary", function()
			local tbl = { { Score = 10 }, { Score = 20 }, { Score = 30 } }
			local item, index = TableUtil.Find(tbl, function(value)
				return (value.Score == 20)
			end)
			ctx:Expect(item):ToBeOk()
			ctx:Expect(index):ToBe(2)
			ctx:Expect(item.Score):ToBe(20)
		end)
	end)

	ctx:Describe("Every", function()
		ctx:Test("should see every value is above 20", function()
			local tbl = { 21, 40, 200 }
			local every = TableUtil.Every(tbl, function(n)
				return (n > 20)
			end)
			ctx:Expect(every):ToBe(true)
		end)

		ctx:Test("should see every value is not above 20", function()
			local tbl = { 20, 40, 200 }
			local every = TableUtil.Every(tbl, function(n)
				return (n > 20)
			end)
			ctx:Expect(every):Not():ToBe(true)
		end)
	end)

	ctx:Describe("Some", function()
		ctx:Test("should see some value is above 20", function()
			local tbl = { 5, 40, 1 }
			local every = TableUtil.Some(tbl, function(n)
				return (n > 20)
			end)
			ctx:Expect(every):ToBe(true)
		end)

		ctx:Test("should see some value is not above 20", function()
			local tbl = { 5, 15, 1 }
			local every = TableUtil.Some(tbl, function(n)
				return (n > 20)
			end)
			ctx:Expect(every):Not():ToBe(true)
		end)
	end)

	ctx:Describe("Truncate", function()
		ctx:Test("should truncate an array", function()
			local t1 = { 1, 2, 3, 4, 5 }
			local t2 = TableUtil.Truncate(t1, 3)
			ctx:Expect(#t2):ToBe(3)
			ctx:Expect(t2[1]):ToBe(t1[1])
			ctx:Expect(t2[2]):ToBe(t1[2])
			ctx:Expect(t2[3]):ToBe(t1[3])
		end)

		ctx:Test("should truncate an array with out of bounds sizes", function()
			local t1 = { 1, 2, 3, 4, 5 }
			ctx:Expect(function()
				TableUtil.Truncate(t1, -1)
			end)
				:Not()
				:ToThrow()
			ctx:Expect(function()
				TableUtil.Truncate(t1, #t1 + 1)
			end)
				:Not()
				:ToThrow()
			local t2 = TableUtil.Truncate(t1, #t1 + 10)
			ctx:Expect(#t2):ToBe(#t1)
			ctx:Expect(t2):Not():ToBe(t1)
		end)
	end)

	ctx:Describe("Lock", function()
		ctx:Test("should lock a table", function()
			local t = { abc = { xyz = { num = 32 } } }
			ctx:Expect(function()
				t.abc.xyz.num = 64
			end)
				:Not()
				:ToThrow()
			local t2 = TableUtil.Lock(t)
			ctx:Expect(t.abc.xyz.num):ToBe(64)
			ctx:Expect(t):ToBe(t2)
			ctx:Expect(function()
				t.abc.xyz.num = 10
			end):ToThrow()
		end)
	end)

	ctx:Describe("Zip", function()
		ctx:Test("should zip arrays together", function()
			local t1 = { 1, 2, 3, 4, 5 }
			local t2 = { 9, 8, 7, 6, 5 }
			local t3 = { 1, 1, 1, 1, 1 }
			local lastIndex = 0
			for i, v in TableUtil.Zip(t1, t2, t3) do
				lastIndex = i
				ctx:Expect(v[1]):ToBe(t1[i])
				ctx:Expect(v[2]):ToBe(t2[i])
				ctx:Expect(v[3]):ToBe(t3[i])
			end
			ctx:Expect(lastIndex):ToBe(math.min(#t1, #t2, #t3))
		end)

		ctx:Test("should zip arrays of different lengths together", function()
			local t1 = { 1, 2, 3, 4, 5 }
			local t2 = { 9, 8, 7, 6 }
			local t3 = { 1, 1, 1 }
			local lastIndex = 0
			for i, v in TableUtil.Zip(t1, t2, t3) do
				lastIndex = i
				ctx:Expect(v[1]):ToBe(t1[i])
				ctx:Expect(v[2]):ToBe(t2[i])
				ctx:Expect(v[3]):ToBe(t3[i])
			end
			ctx:Expect(lastIndex):ToBe(math.min(#t1, #t2, #t3))
		end)

		ctx:Test("should zip maps together", function()
			local t1 = { a = 10, b = 20, c = 30 }
			local t2 = { a = 100, b = 200, c = 300 }
			local t3 = { a = 3000, b = 2000, c = 3000 }
			for k, v in TableUtil.Zip(t1, t2, t3) do
				ctx:Expect(v[1]):ToBe(t1[k])
				ctx:Expect(v[2]):ToBe(t2[k])
				ctx:Expect(v[3]):ToBe(t3[k])
			end
		end)

		ctx:Test("should zip maps of different keys together", function()
			local t1 = { a = 10, b = 20, c = 30, d = 40 }
			local t2 = { a = 100, b = 200, c = 300, z = 10 }
			local t3 = { a = 3000, b = 2000, c = 3000, x = 0 }
			for k, v in TableUtil.Zip(t1, t2, t3) do
				ctx:Expect(v[1]):ToBe(t1[k])
				ctx:Expect(v[2]):ToBe(t2[k])
				ctx:Expect(v[3]):ToBe(t3[k])
			end
		end)
	end)

	ctx:Describe("IsEmpty", function()
		ctx:Test("should detect that table is empty", function()
			local tbl = {}
			local isEmpty = TableUtil.IsEmpty(tbl)
			ctx:Expect(isEmpty):ToBe(true)
		end)

		ctx:Test("should detect that array is not empty", function()
			local tbl = { 10, 20, 30 }
			local isEmpty = TableUtil.IsEmpty(tbl)
			ctx:Expect(isEmpty):ToBe(false)
		end)

		ctx:Test("should detect that dictionary is not empty", function()
			local tbl = { a = 10, b = 20, c = 30 }
			local isEmpty = TableUtil.IsEmpty(tbl)
			ctx:Expect(isEmpty):ToBe(false)
		end)
	end)

	ctx:Describe("JSON", function()
		ctx:Test("should encode json", function()
			local tbl = { hello = "world" }
			local json = TableUtil.EncodeJSON(tbl)
			ctx:Expect(json):ToBe('{"hello":"world"}')
		end)

		ctx:Test("should decode json", function()
			local json = '{"hello":"world"}'
			local tbl = TableUtil.DecodeJSON(json)
			ctx:Expect(tbl):ToBeA("table")
			ctx:Expect(tbl.hello):ToBe("world")
		end)
	end)
end


-- TaskQueue
-- Stephen Leitnick
-- November 20, 2021

--[=[
	@class TaskQueue
	A queue that flushes all objects at the end of the current
	execution step. This works by scheduling all tasks with
	`task.defer`.

	A possible use-case is to batch all requests being sent through
	a RemoteEvent to help prevent calling it too many times on
	the same frame.

	```lua
	local bulletQueue = TaskQueue.new(function(bullets)
		bulletRemoteEvent:FireAllClients(bullets)
	end)

	-- Add 3 bullets. Because they're all added on the same
	-- execution step, they will all be grouped together on
	-- the next queue flush, which the above function will
	-- handle.
	bulletQueue:Add(someBullet)
	bulletQueue:Add(someBullet)
	bulletQueue:Add(someBullet)
	```
]=]
local TaskQueue = {}
TaskQueue.__index = TaskQueue

--[=[
	@param onFlush ({T}) -> ()
	@return TaskQueue<T>
	Constructs a new TaskQueue.
]=]
function TaskQueue.new<T>(onFlush: ({ T }) -> ())
	local self = setmetatable({}, TaskQueue)

	self._queue = {}
	self._flushing = false
	self._scheduled = nil
	self._onFlush = onFlush

	return self
end

--[=[
	@param object T
	Add an object to the queue.
]=]
function TaskQueue:Add<T>(object: T)
	table.insert(self._queue, object)

	if self._scheduled == nil then
		self._scheduled = task.defer(function()
			self._flushing = true
			self._onFlush(self._queue)
			table.clear(self._queue)
			self._flushing = false
			self._scheduled = nil
		end)
	end
end

--[=[
	Clears the TaskQueue. This will clear any tasks
	that were scheduled to be flushed on the current
	execution frame.

	```lua
	queue:Add(something1)
	queue:Add(something2)
	queue:Clear()
	```
]=]
function TaskQueue:Clear()
	if self._flushing then
		return
	end

	if self._scheduled ~= nil then
		task.cancel(self._scheduled)
		self._scheduled = nil
	end

	table.clear(self._queue)
end

--[=[
	Destroys the TaskQueue. Just an alias for `Clear()`.
]=]
function TaskQueue:Destroy()
	self:Clear()
end

return TaskQueue


--!strict

local RunService = game:GetService("RunService")

local Signal = require(script.Parent.Signal)

--[=[
	@within Timer
	@type CallbackFn () -> ()
	Callback function.
]=]
type CallbackFn = () -> nil

--[=[
	@within Timer
	@type TimeFn () -> number
	Time function.
]=]
type TimeFn = () -> number

--[=[
	@class Timer

	The Timer class allows for code to run periodically at specified intervals.

	```lua
	local timer = Timer.new(2)
	timer.Tick:Connect(function()
		print("Tock")
	end)
	timer:Start()
	```
]=]
local Timer = {}
Timer.__index = Timer

export type Timer = typeof(setmetatable(
	{} :: {
		Interval: number,
		UpdateSignal: RBXScriptSignal,
		TimeFunction: () -> number,
		AllowDrift: boolean,
		Tick: Signal.Signal<>,

		_runHandle: RBXScriptConnection?,
	},
	Timer
))

--[=[
	@within Timer
	@prop Interval number
	Interval at which the `Tick` event fires.
]=]
--[=[
	@within Timer
	@prop UpdateSignal RBXScriptSignal | Signal
	The signal which updates the timer internally.
]=]
--[=[
	@within Timer
	@prop TimeFunction TimeFn
	The function which gets the current time.
]=]
--[=[
	@within Timer
	@prop AllowDrift boolean
	Flag which indicates if the timer is allowed to drift. This
	is set to `true` by default. This flag must be set before
	calling `Start` or `StartNow`. This flag should only be set
	to `false` if it is necessary for drift to be eliminated.
]=]
--[=[
	@within Timer
	@prop Tick RBXScriptSignal | Signal
	The event which is fired every time the timer hits its interval.
]=]

--[=[
	@return Timer

	Creates a new timer.
]=]
function Timer.new(interval: number): Timer
	assert(type(interval) == "number", "argument #1 to Timer.new must be a number; got " .. type(interval))
	assert(interval >= 0, "argument #1 to Timer.new must be greater or equal to 0; got " .. tostring(interval))

	local self = setmetatable({}, Timer)

	self._runHandle = nil
	self.Interval = interval
	self.UpdateSignal = RunService.Heartbeat
	self.TimeFunction = time
	self.AllowDrift = true
	self.Tick = Signal.new()

	return self
end

--[=[
	@return RBXScriptConnection

	Creates a simplified timer which just fires off a callback function at the given interval.

	```lua
	-- Basic:
	Timer.simple(1, function()
		print("Tick")
	end)

	-- Using other arguments:
	Timer.simple(1, function()
		print("Tick")
	end, true, RunService.Heartbeat, os.clock)
	```
]=]
function Timer.simple(
	interval: number,
	callback: CallbackFn,
	startNow: boolean?,
	updateSignal: RBXScriptSignal?,
	timeFn: TimeFn?
)
	local update = updateSignal or RunService.Heartbeat
	local t = timeFn or time
	local nextTick = t() + interval

	if startNow then
		task.defer(callback)
	end

	return update:Connect(function()
		local now = t()
		if now >= nextTick then
			nextTick = now + interval
			task.defer(callback)
		end
	end)
end

--[=[
	Returns `true` if the given object is a Timer.
]=]
function Timer.is(obj: any): boolean
	return type(obj) == "table" and getmetatable(obj) == Timer
end

function Timer._startTimer(self: Timer)
	local t = self.TimeFunction
	local nextTick = t() + self.Interval

	self._runHandle = self.UpdateSignal:Connect(function()
		local now = t()
		if now >= nextTick then
			nextTick = now + self.Interval
			self.Tick:Fire()
		end
	end)
end

function Timer._startTimerNoDrift(self: Timer)
	assert(self.Interval > 0, "interval must be greater than 0 when AllowDrift is set to false")

	local t = self.TimeFunction
	local n = 1
	local start = t()
	local nextTick = start + self.Interval
	self._runHandle = self.UpdateSignal:Connect(function()
		local now = t()
		while now >= nextTick do
			n += 1
			nextTick = start + (self.Interval * n)
			self.Tick:Fire()
		end
	end)
end

--[=[
	Starts the timer. Will do nothing if the timer is already running.

	```lua
	timer:Start()
	```
]=]
function Timer.Start(self: Timer)
	if self._runHandle then
		return
	end
	if self.AllowDrift then
		self:_startTimer()
	else
		self:_startTimerNoDrift()
	end
end

--[=[
	Starts the timer and fires off the Tick event immediately. Will do
	nothing if the timer is already running.

	```lua
	timer:StartNow()
	```
]=]
function Timer.StartNow(self: Timer)
	if self._runHandle then
		return
	end
	self.Tick:Fire()
	self:Start()
end

--[=[
	Stops the timer. Will do nothing if the timer is already stopped.

	```lua
	timer:Stop()
	```
]=]
function Timer.Stop(self: Timer)
	if not self._runHandle then
		return
	end

	self._runHandle:Disconnect()
	self._runHandle = nil
end

--[=[
	Returns `true` if the timer is currently running.

	```lua
	if timer:IsRunning() then
		-- Do something
	end
	```
]=]
function Timer.IsRunning(self: Timer): boolean
	return self._runHandle ~= nil
end

--[=[
	Destroys the timer. This will also stop the timer.
]=]
function Timer.Destroy(self: Timer)
	self.Tick:Destroy()
	self:Stop()
end

return Timer


local ServerScriptService = game:GetService("ServerScriptService")

local Test = require(ServerScriptService.TestRunner.Test)

return function(ctx: Test.TestContext)
	local Timer = require(script.Parent)

	ctx:Describe("Timer", function()
		local timer

		ctx:BeforeEach(function()
			timer = Timer.new(0.1)
			timer.TimeFunction = os.clock
		end)

		ctx:AfterEach(function()
			if timer then
				timer:Destroy()
				timer = nil
			end
		end)

		ctx:Test("should create a new timer", function()
			ctx:Expect(Timer.is(timer)):ToBe(true)
		end)

		ctx:Test("should tick appropriately", function()
			local start = os.clock()
			timer:Start()
			timer.Tick:Wait()
			local duration = (os.clock() - start)
			ctx:Expect(duration):ToBeNear(duration, 0.02)
		end)

		ctx:Test("should start immediately", function()
			local start = os.clock()
			local stop = nil
			timer.Tick:Connect(function()
				if not stop then
					stop = os.clock()
				end
			end)
			timer:StartNow()
			timer.Tick:Wait()
			ctx:Expect(stop):ToBeA("number")
			local duration = (stop - start)
			ctx:Expect(duration):ToBeNear(0, 0.02)
		end)

		ctx:Test("should stop", function()
			local ticks = 0
			timer.Tick:Connect(function()
				ticks += 1
			end)
			timer:StartNow()
			timer:Stop()
			task.wait(1)
			ctx:Expect(ticks):ToBe(1)
		end)

		ctx:Test("should detect if running", function()
			ctx:Expect(timer:IsRunning()):ToBe(false)
			timer:Start()
			ctx:Expect(timer:IsRunning()):ToBe(true)
			timer:Stop()
			ctx:Expect(timer:IsRunning()):ToBe(false)
			timer:StartNow()
			ctx:Expect(timer:IsRunning()):ToBe(true)
			timer:Stop()
			ctx:Expect(timer:IsRunning()):ToBe(false)
		end)
	end)
end


local DELIM = "/"

local function FullNameToPath(instance: Instance): string
	return instance:GetFullName():gsub("%.", DELIM)
end

--[=[
	@class Tree
]=]
local Tree = {}

--[=[
	Similar to FindFirstChild, with a few key differences:
	- An error is thrown if the instance is not found
	- A path to the instance can be provided, delimited by forward slashes (e.g. `Path/To/Child`)
	- Optionally, the instance's type can be asserted using `IsA`

	```lua
	-- Find "Child" directly under parent:
	local instance = Tree.Find(parent, "Child")

	-- Find "Child" descendant:
	local instance = Tree.Find(parent, "Path/To/Child")

	-- Find "Child" descendant and assert that it's a BasePart:
	local instance = Tree.Find(parent, "Path/To/Child", "BasePart") :: BasePart
	```
]=]
function Tree.Find(parent: Instance, path: string, assertIsA: string?): Instance
	local instance = parent
	local paths = path:split(DELIM)

	for _, p in paths do
		-- Error for empty path parts:
		if p == "" then
			error(`Invalid path: {path}`, 2)
		end

		instance = instance:FindFirstChild(p)

		-- Error if instance is not found:
		if instance == nil then
			error(`Failed to find {path} in {FullNameToPath(parent)}`, 2)
		end
	end

	-- Assert class type if argument is supplied:
	if assertIsA and not instance:IsA(assertIsA) then
		error(`Got class {instance.ClassName}; expected to be of type {assertIsA}`, 2)
	end

	return instance
end

--[=[
	Returns `true` if the instance is found. Similar to `Tree.Find`, except this returns `true|false`. No error is thrown unless the path is invalid.

	```lua
	-- Check if "Child" exists directly in `parent`:
	if Tree.Exists(parent, "Child") then ... end

	-- Check if "Child" descendant exists at `parent.Path.To.Child`:
	if Tree.Exists(parent, "Path/To/Child") then ... end

	-- Check if "Child" descendant exists at `parent.Path.To.Child` and is a BasePart:
	if Tree.Exists(parent, "Path/To/Child", "BasePart") then ... end
	```
]=]
function Tree.Exists(parent: Instance, path: string, assertIsA: string?): boolean
	local instance = parent
	local paths = path:split(DELIM)

	for _, p in paths do
		-- Error for empty path parts:
		if p == "" then
			error(`Invalid path: {path}`, 2)
		end

		instance = instance:FindFirstChild(p)

		if instance == nil then
			return false
		end
	end

	if assertIsA and not instance:IsA(assertIsA) then
		return false
	end

	return true
end

--[=[
	@yields
	Waits for the path to exist within the parent instance. Similar to `Tree.Find`, except `WaitForChild`
	is used internally. An optional `timeout` can be supplied, which is passed along to each call to
	`WaitForChild`.

	An error is thrown if the path fails to resolve. This will only happen if the path is invalid _or_ if
	the supplied timeout is reached.

	:::caution Indefinite Yield Possible
	If the `timeout` parameter is not supplied, then the internal call to `WaitForChild` will yield
	indefinitely until the child is found. It is good practice to supply a timeout parameter.
	:::

	```lua
	local child = Tree.Await(parent, "Path/To/Child", 30)
	```
]=]
function Tree.Await(parent: Instance, path: string, timeout: number?, assertIsA: string?): Instance
	local instance = parent
	local paths = path:split(DELIM)

	for _, p in paths do
		-- Error for empty path parts:
		if p == "" then
			error(`Invalid path: {path}`, 2)
		end

		instance = instance:WaitForChild(p, timeout)

		-- Error if instance is not found:
		if instance == nil then
			error(`Failed to await {path} in {FullNameToPath(parent)} (timeout reached)`, 2)
		end
	end

	-- Assert class type if argument is supplied:
	if assertIsA and not instance:IsA(assertIsA) then
		error(`Got class {instance.ClassName}; expected to be of type {assertIsA}`, 2)
	end

	return instance
end

return Tree


--!strict

type Signal<T...> = {
	Connect: (self: Signal<T...>, fn: (T...) -> ()) -> RBXScriptConnection,
	ConnectParallel: (self: Signal<T...>, fn: (T...) -> ()) -> RBXScriptConnection,
	Once: (self: Signal<T...>, fn: (T...) -> ()) -> RBXScriptConnection,
	Wait: (self: Signal<T...>) -> T...,
}

type PlayerSignal<T...> = {
	Connect: (self: PlayerSignal<T...>, fn: (player: Player, T...) -> ()) -> RBXScriptConnection,
	ConnectParallel: (self: PlayerSignal<T...>, fn: (player: Player, T...) -> ()) -> RBXScriptConnection,
	Once: (self: PlayerSignal<T...>, fn: (player: Player, T...) -> ()) -> RBXScriptConnection,
	Wait: (self: PlayerSignal<T...>) -> (Player, T...),
}

--[=[
	@within TypedRemote
	@interface Event<T...>
	.OnClientEvent Signal<T...>,
	.OnServerEvent PlayerSignal<T...>,
	.FireClient (self: Event<T...>, player: Player, T...) -> (),
	.FireAllClients (self: Event<T...>, T...) -> (),
	.FireServer (self: Event<T...>, T...) -> (),
]=]
export type Event<T...> = Instance & {
	OnClientEvent: Signal<T...>,
	OnServerEvent: PlayerSignal<T...>,
	FireClient: (self: Event<T...>, player: Player, T...) -> (),
	FireAllClients: (self: Event<T...>, T...) -> (),
	FireServer: (self: Event<T...>, T...) -> (),
}

--[=[
	@within TypedRemote
	@interface UnreliableEvent<T...>
	.OnClientEvent Signal<T...>,
	.OnServerEvent PlayerSignal<T...>,
	.FireClient (self: Event<T...>, player: Player, T...) -> (),
	.FireAllClients (self: Event<T...>, T...) -> (),
	.FireServer (self: Event<T...>, T...) -> (),
]=]
export type UnreliableEvent<T...> = Event<T...>

--[=[
	@within TypedRemote
	@interface Function<T..., R...>
	.InvokeServer (self: Function<T..., R...>, T...) -> R...,
	.OnServerInvoke (player: Player, T...) -> R...,
]=]
export type Function<T..., R...> = Instance & {
	InvokeServer: (self: Function<T..., R...>, T...) -> R...,
	OnServerInvoke: (player: Player, T...) -> R...,
}

type CreatorFn<T> = (name: string) -> T

local IS_SERVER = game:GetService("RunService"):IsServer()

--[=[
	@class TypedRemote

	Simple networking package that helps create typed RemoteEvents and RemoteFunctions.

	```lua
	-- ReplicatedStorage.Network (ModuleScript)

	local TypedRemote = require(ReplicatedStorage.Packages.TypedRemote)

	-- Get the RF, RE and URE instance creators, which create RemoteFunctions/RemoteEvents/UnreliableRemoteEvents
	-- within the given parent (the script by default):
	local RF, RE, URE = TypedRemote.parent()

	-- Redeclare the TypedRemote types for simplicity:
	type RF<T..., R...> = TypedRemote.Function<T..., R...>
	type RE<T...> = TypedRemote.Event<T...>
	type URE<T...> = TypedRemote.UnreliableEvent<T...>

	-- Define network table:
	return {
		-- RemoteFunction that takes two arguments (boolean, string) and returns a number:
		MyFunc = RF("MyFunc") :: RF<(boolean, string), (number)>,

		-- RemoteEvent that takes two arguments - a string and a number:
		MyEvent = RE("MyEvent") :: RE<string, number>,

		-- UnreliableRemoteEvent that takes two arguments - a string and a number:
		MyUnreliableEvent = URE("MyUnreliableEvent") :: URE<string, number>,
	}
	```

	```lua
	-- Example usage of the above Network module:

	local Network = require(ReplicatedStorage.Network)

	-- If you type this out, intellisense will help with what the function signature should be:
	Network.MyEvent.OnClientEvent:Connect(function(player, str, num)
		-- Foo
	end)
	```

	In most cases, the `TypedRemote.parent()` function will be used to create the memoized
	RemoteFunction and RemoteEvent builder functions. From there, call the given functions
	with the desired name per remote.

	The `TypedRemote.func` and `TypedRemote.event` functions can also be used, but the
	parent must be supplied to each call, hence the helpful `parent()` memoizer.
]=]
local TypedRemote = {}

--[=[
	@return ((name: string) -> RemoteFunction, (name: string) -> RemoteEvent)

	Creates a memoized version of the `func` and `event` functions that include the `parent`
	in each call.

	```lua
	-- Create RF and RE functions that use the current script as the instance parent:
	local RF, RE = TypedRemote.parent(script)

	local remoteFunc = RF("RemoteFunc")
	```
]=]
function TypedRemote.parent(
	parent: Instance?
): (CreatorFn<RemoteFunction>, CreatorFn<RemoteEvent>, CreatorFn<UnreliableRemoteEvent>)
	return function(name: string)
		return TypedRemote.func(name, parent)
	end, function(name: string)
		return TypedRemote.event(name, parent)
	end, function(name: string)
		return TypedRemote.unreliableEvent(name, parent)
	end
end

--[=[
	Creates a RemoteFunction with `name` and parents it inside of `parent`.

	If the `parent` argument is not included or is `nil`, then it defaults to the parent of
	this TypedRemote ModuleScript.
]=]
function TypedRemote.func(name: string, parent: Instance?): RemoteFunction
	local rf: RemoteFunction
	if IS_SERVER then
		rf = Instance.new("RemoteFunction")
		rf.Name = name
		rf.Parent = if parent then parent else script
	else
		rf = (if parent then parent else script):WaitForChild(name)
		assert(rf:IsA("RemoteFunction"), "expected remote function")
	end
	return rf
end

--[=[
	Creates a RemoteEvent with `name` and parents it inside of `parent`.

	If the `parent` argument is not included or is `nil`, then it defaults to the parent of
	this TypedRemote ModuleScript.
]=]
function TypedRemote.event(name: string, parent: Instance?): RemoteEvent
	local re: RemoteEvent
	if IS_SERVER then
		re = Instance.new("RemoteEvent")
		re.Name = name
		re.Parent = if parent then parent else script
	else
		re = (if parent then parent else script):WaitForChild(name)
		assert(re:IsA("RemoteEvent"), "expected remote event")
	end
	return re
end

--[=[
	Creates an UnreliableRemoteEvent with `name` and parents it inside of `parent`.

	If the `parent` argument is not included or is `nil`, then it defaults to the parent of
	this TypedRemote ModuleScript.
]=]
function TypedRemote.unreliableEvent(name: string, parent: Instance?): UnreliableRemoteEvent
	local ure: UnreliableRemoteEvent
	if IS_SERVER then
		ure = Instance.new("UnreliableRemoteEvent")
		ure.Name = name
		ure.Parent = if parent then parent else script
	else
		ure = (if parent then parent else script):WaitForChild(name)
		assert(ure:IsA("UnreliableRemoteEvent"), "expected unreliable remote event")
	end
	return ure
end

table.freeze(TypedRemote)

return TypedRemote


-- WaitFor
-- Stephen Leitnick
-- January 17, 2022

local RunService = game:GetService("RunService")

local Promise = require(script.Parent.Promise)

local DEFAULT_TIMEOUT = 60

--[=[
	@class WaitFor
	Utility class for awaiting the existence of instances.

	By default, all promises timeout after 60 seconds, unless the `timeout`
	argument is specified.

	:::note
	Promises will be rejected if the parent (or any ancestor) is unparented
	from the game.
	:::

	:::caution Set name before parent
	When waiting for instances based on name (e.g. `WaitFor.Child`), the `WaitFor`
	system is listening to events to capture these instances being added. This
	means that the name must be set _before_ being parented into the object.
	:::
]=]
local WaitFor = {}

--[=[
	@within WaitFor
	@prop Error {Unparented: string, ParentChanged: string}
]=]
WaitFor.Error = {
	Unparented = "Unparented",
	ParentChanged = "ParentChanged",
}

local function PromiseWatchAncestry(instance: Instance, promise)
	return Promise.race({
		promise,
		Promise.fromEvent(instance.AncestryChanged, function(_, newParent)
			return newParent == nil
		end):andThen(function()
			return Promise.reject(WaitFor.Error.Unparented)
		end),
	})
end

--[=[
	@return Promise<Instance>
	Wait for a child to exist within a given parent based on the child name.

	```lua
	WaitFor.Child(parent, "SomeObject"):andThen(function(someObject)
		print(someObject, "now exists")
	end):catch(warn)
	```
]=]
function WaitFor.Child(parent: Instance, childName: string, timeout: number?)
	local child = parent:FindFirstChild(childName)
	if child then
		return Promise.resolve(child)
	end
	return PromiseWatchAncestry(
		parent,
		Promise.fromEvent(parent.ChildAdded, function(c)
			return c.Name == childName
		end):timeout(timeout or DEFAULT_TIMEOUT)
	)
end

--[=[
	@return Promise<{Instance}>
	Wait for all children to exist within the given parent.

	```lua
	WaitFor.Children(parent, {"SomeObject01", "SomeObject02"}):andThen(function(children)
		local someObject01, someObject02 = table.unpack(children)
	end)
	```

	:::note
	Once all children are found, a second check is made to ensure that all children
	are still directly parented to the given `parent` (since one child's parent
	might have changed before another child was found). A rejected promise with the
	`WaitFor.Error.ParentChanged` error will be thrown if any parents of the children
	no longer match the given `parent`.
	:::
]=]
function WaitFor.Children(parent: Instance, childrenNames: { string }, timeout: number?)
	local all = table.create(#childrenNames)
	for i, childName in ipairs(childrenNames) do
		all[i] = WaitFor.Child(parent, childName, timeout)
	end
	return Promise.all(all):andThen(function(children)
		-- Check that all are still parented
		for _, child in ipairs(children) do
			if child.Parent ~= parent then
				return Promise.reject(WaitFor.Error.ParentChanged)
			end
		end
		return children
	end)
end

--[=[
	@return Promise<Instance>
	Wait for a descendant to exist within a given parent. This is similar to
	`WaitFor.Child`, except it looks for all descendants instead of immediate
	children.

	```lua
	WaitFor.Descendant(parent, "SomeDescendant"):andThen(function(someDescendant)
		print("SomeDescendant now exists")
	end)
	```
]=]
function WaitFor.Descendant(parent: Instance, descendantName: string, timeout: number?)
	local descendant = parent:FindFirstChild(descendantName, true)
	if descendant then
		return Promise.resolve(descendant)
	end
	return PromiseWatchAncestry(
		parent,
		Promise.fromEvent(parent.DescendantAdded, function(d)
			return d.Name == descendantName
		end):timeout(timeout or DEFAULT_TIMEOUT)
	)
end

--[=[
	@return Promise<{Instance}>
	Wait for all descendants to exist within a given parent.

	```lua
	WaitFor.Descendants(parent, {"SomeDescendant01", "SomeDescendant02"}):andThen(function(descendants)
		local someDescendant01, someDescendant02 = table.unpack(descendants)
	end)
	```

	:::note
	Once all descendants are found, a second check is made to ensure that none of the
	instances have moved outside of the parent (since one instance might change before
	another instance is found). A rejected promise with the `WaitFor.Error.ParentChanged`
	error will be thrown if any of the instances are no longer descendants of the given
	`parent`.
	:::
]=]
function WaitFor.Descendants(parent: Instance, descendantNames: { string }, timeout: number?)
	local all = table.create(#descendantNames)
	for i, descendantName in ipairs(descendantNames) do
		all[i] = WaitFor.Descendant(parent, descendantName, timeout)
	end
	return Promise.all(all):andThen(function(descendants)
		-- Check that all are still parented
		for _, descendant in ipairs(descendants) do
			if not descendant:IsDescendantOf(parent) then
				return Promise.reject(WaitFor.Error.ParentChanged)
			end
		end
		return descendants
	end)
end

--[=[
	@return Promise<Instance>
	Wait for the PrimaryPart of a model to exist.

	```lua
	WaitFor.PrimaryPart(model):andThen(function(primaryPart)
		print(primaryPart == model.PrimaryPart)
	end)
	```
]=]
function WaitFor.PrimaryPart(model: Model, timeout: number?)
	local primary = model.PrimaryPart
	if primary then
		return Promise.resolve(primary)
	end
	return PromiseWatchAncestry(
		model,
		Promise.fromEvent(model:GetPropertyChangedSignal("PrimaryPart"), function()
			primary = model.PrimaryPart
			return primary ~= nil
		end)
			:andThen(function()
				return primary
			end)
			:timeout(timeout or DEFAULT_TIMEOUT)
	)
end

--[=[
	@return Promise<Instance>
	Wait for the Value of an ObjectValue to exist.

	```lua
	WaitFor.ObjectValue(someObjectValue):andThen(function(value)
		print("someObjectValue's value is", value)
	end)
	```
]=]
function WaitFor.ObjectValue(objectValue: ObjectValue, timeout: number?)
	local value = objectValue.Value
	if value then
		return Promise.resolve(value)
	end
	return PromiseWatchAncestry(
		objectValue,
		Promise.fromEvent(objectValue.Changed, function(v)
			value = v
			return value ~= nil
		end)
			:andThen(function()
				return value
			end)
			:timeout(timeout or DEFAULT_TIMEOUT)
	)
end

--[=[
	@return Promise<T>
	Wait for the given predicate function to return a non-nil value of
	of type `T`. The predicate is fired every RunService Heartbeat step.

	```lua
	-- Example, waiting for some property to be set:
	WaitFor.Custom(function() return vectorForce.Attachment0 end):andThen(function(a0)
		print(a0)
	end)
	```
]=]
function WaitFor.Custom<T>(predicate: () -> T?, timeout: number?)
	local value = predicate()
	if value ~= nil then
		return Promise.resolve(value)
	end
	return Promise.new(function(resolve, _reject, onCancel)
		local heartbeat
		local function OnDone()
			heartbeat:Disconnect()
		end
		local function Update()
			local v = predicate()
			if v ~= nil then
				OnDone()
				resolve(v)
			end
		end
		heartbeat = RunService.Heartbeat:Connect(Update)
		onCancel(OnDone)
	end):timeout(timeout or DEFAULT_TIMEOUT)
end

return WaitFor


local ServerScriptService = game:GetService("ServerScriptService")

local Test = require(ServerScriptService.TestRunner.Test)

return function(ctx: Test.TestContext)
	local Promise = require(script.Parent.Parent.Promise)
	local WaitFor = require(script.Parent)

	local instances = {}

	local function Create(name, parent)
		local instance = Instance.new("Folder")
		instance.Name = name
		instance.Parent = parent
		table.insert(instances, instance)
		return instance
	end

	ctx:Describe("WaitFor", function()
		ctx:AfterEach(function()
			for _, inst in ipairs(instances) do
				task.delay(0, function()
					inst:Destroy()
				end)
			end
			table.clear(instances)
		end)

		ctx:Test("should wait for child", function()
			local parent = workspace
			local childName = "TestChild"

			task.delay(0.1, Create, childName, parent)

			local success, instance = WaitFor.Child(parent, childName):await()
			ctx:Expect(success):ToBe(true)
			ctx:Expect(typeof(instance)):ToBe("Instance")
			ctx:Expect(instance.Name):ToBe(childName)
			ctx:Expect(instance.Parent):ToBe(parent)
		end)

		ctx:Test("should stop waiting for child if parent is unparented", function()
			local parent = Create("SomeParent", workspace)
			local childName = "TestChild"

			task.delay(0.1, function()
				parent:Destroy()
			end)

			local success, err = WaitFor.Child(parent, childName):await()
			ctx:Expect(success):ToBe(false)
			ctx:Expect(err):ToBe(WaitFor.Error.Unparented)
		end)

		ctx:Test("should stop waiting for child if timeout is reached", function()
			local success, err = WaitFor.Child(workspace, "InstanceThatDoesNotExist", 0.1):await()
			ctx:Expect(success):ToBe(false)
			ctx:Expect(Promise.Error.isKind(err, Promise.Error.Kind.TimedOut)):ToBe(true)
		end)

		ctx:Test("should wait for children", function()
			local parent = workspace
			local childrenNames = { "TestChild01", "TestChild02", "TestChild03" }

			task.delay(0.1, Create, childrenNames[1], parent)
			task.delay(0.2, Create, childrenNames[2], parent)
			task.delay(0.05, Create, childrenNames[3], parent)

			local success, children = WaitFor.Children(parent, childrenNames):await()
			ctx:Expect(success):ToBe(true)
			for i, child in ipairs(children) do
				ctx:Expect(typeof(child)):ToBe("Instance")
				ctx:Expect(child.Name):ToBe(childrenNames[i])
				ctx:Expect(child.Parent):ToBe(parent)
			end
		end)

		ctx:Test("should fail if any children are no longer parented in parent", function()
			local parent = workspace
			local childrenNames = { "TestChild04", "TestChild05", "TestChild06" }

			local child3

			task.delay(0.1, Create, childrenNames[1], parent)
			task.delay(0.2, Create, childrenNames[2], parent)
			task.delay(0.05, function()
				child3 = Create(childrenNames[3], parent)
			end)
			task.delay(0.1, function()
				child3:Destroy()
			end)

			local success, err = WaitFor.Children(parent, childrenNames):await()
			ctx:Expect(success):ToBe(false)
			ctx:Expect(err):ToBe(WaitFor.Error.ParentChanged)
		end)

		ctx:Test("should wait for descendant", function()
			local parent = workspace
			local descendantName = "TestDescendant"

			task.delay(0.1, Create, descendantName, Create("TestFolder", parent))

			local success, descendant = WaitFor.Descendant(parent, descendantName):await()
			ctx:Expect(success):ToBe(true)
			ctx:Expect(typeof(descendant)):ToBe("Instance")
			ctx:Expect(descendant.Name):ToBe(descendantName)
			ctx:Expect(descendant:IsDescendantOf(parent)):ToBe(true)
		end)

		ctx:Test("should wait for many descendants", function()
			local parent = workspace
			local descendantNames = { "TestDescendant01", "TestDescendant02", "TestDescendant03" }

			task.delay(0.1, Create, descendantNames[1], Create("TestFolder1", parent))
			task.delay(0.05, Create, descendantNames[2], Create("TestFolder2", parent))
			task.delay(0.2, Create, descendantNames[3], Create("TestFolder4", Create("TestFolder3", parent)))

			local success, descendants = WaitFor.Descendants(parent, descendantNames):await()
			ctx:Expect(success):ToBe(true)
			for i, descendant in ipairs(descendants) do
				ctx:Expect(typeof(descendant)):ToBe("Instance")
				ctx:Expect(descendant.Name == descendantNames[i]):ToBe(true)
				ctx:Expect(descendant:IsDescendantOf(parent)):ToBe(true)
			end
		end)

		ctx:Test("should wait for primarypart", function()
			local model = Instance.new("Model")
			local part = Instance.new("Part")
			part.Anchored = true

			part.Parent = model
			model.Parent = workspace

			task.delay(0.1, function()
				model.PrimaryPart = part
			end)

			local success, primary = WaitFor.PrimaryPart(model):await()
			ctx:Expect(success):ToBe(true)
			ctx:Expect(typeof(primary)):ToBe("Instance")
			ctx:Expect(primary):ToBe(part)
			ctx:Expect(model.PrimaryPart):ToBe(primary)

			model:Destroy()
		end)

		ctx:Test("should wait for objectvalue", function()
			local objValue = Instance.new("ObjectValue")
			objValue.Parent = workspace

			local instance = Create("SomeInstance", workspace)

			task.delay(0.1, function()
				objValue.Value = instance
			end)

			local success, value = WaitFor.ObjectValue(objValue):await()
			ctx:Expect(success):ToBe(true)
			ctx:Expect(typeof(value)):ToBe("Instance")
			ctx:Expect(value):ToBe(instance)
			ctx:Expect(objValue.Value == value)

			objValue:Destroy()
		end)

		ctx:Test("should wait for custom predicate", function()
			local instance
			task.delay(0.1, function()
				instance = Create("CustomInstance", workspace)
			end)

			local success, inst = WaitFor.Custom(function()
				return instance
			end):await()
			ctx:Expect(success):ToBe(true)
			ctx:Expect(typeof(inst)):ToBe("Instance")
			ctx:Expect(inst):ToBe(instance)
		end)
	end)
end


require(script.Parent.TestEZ).TestBootstrap:run({
	game:GetService("ServerScriptService").Test
})


--!optimize 2
--!strict
--@EternityDev
local Client = {}

local Thread = require("./Util/Thread")
local Buffer = require("./Util/Buffer")
local Replication = require("./Replication")
local Xor = require("./Util/Xor")

local RunService = game:GetService("RunService")
local Event: RemoteEvent = script.Parent:WaitForChild("Event")
local UnreliableEvent: UnreliableRemoteEvent = script.Parent:WaitForChild("UnreliableEvent")
local deltaT: number, cycle: number = 0, 1 / 61
local writer: Buffer.Writer = Buffer.createWriter()

type Connection = {
	Connected: boolean,
	Disconnect: (self: Connection) -> (),
}
type Event = {
	i: number,
	c: (Player, ...any?) -> ...any?,
}

local queueEvent: { { any } } = {}
local queueUnreliableEvent: { { any } } = {}
local eventListeners: { [number]: { Event } } = {}
local eventSchemas: { [number]: Buffer.SchemaType } = {}

local lastDelta: { [number]: { any } } = {}
local pendingInvokes: { [string]: thread } = {}
local invokeId = 0

--@optional
-- Yields the current thread until the client has successfully initialized and synchronized with the server's replication data (identifier). Its optionally, but highly recommended to call this before firing or connecting to any events to ensure the network is fully ready.
Client.awaitReady = Replication.wait_for_ready

--@remoteName string
--@schema Buffer.SchemaType
-- Define a schema for strict data packing on a specific event.
Client.useSchema = function(remoteName: string, schema: Buffer.SchemaType)
	local id = Replication.get_id[remoteName]
	if not id then
		warn(`[Warp]: ".useSchema"::"{remoteName}" does not exist, likely its not registered on the server yet.`)
		return
	end
	eventSchemas[id] = schema
end

--@remoteName string
--@fn function
-- Connect to an event to receive incoming data from the server.
Client.Connect = function(remoteName: string, fn: (Player, ...any?) -> ...any?): Connection
	local id = Replication.get_id[remoteName]
	if not id then
		warn(`[Warp]: ".Connect"::"{remoteName}" does not exist, likely its not registered on the server yet.`)
		return {
			Connected = false,
			Disconnect = function()
				return
			end,
		} :: Connection
	end
	local detail = {
		i = id,
		c = fn,
	}
	if not eventListeners[id] then
		eventListeners[id] = {}
	end
	table.insert(eventListeners[id], detail)
	return {
		Connected = true,
		Disconnect = function(self: Connection)
			if not self.Connected then
				return
			end
			self.Connected = false
			local bucket = eventListeners[detail.i]
			if bucket then
				local idx = table.find(bucket, detail)
				if idx then
					table.remove(bucket, idx)
				end
			end
		end,
	} :: Connection
end

--@remoteName string
--@fn function
-- Similar to :Connect but automatically disconnects after the first firing.
Client.Once = function(remoteName: string, fn: (...any?) -> ()): Connection
	local connection
	connection = Client.Connect(remoteName, function(...: any?)
		if connection then
			connection:Disconnect()
		end
		fn(...)
	end)
	return connection
end

--@remoteName string
-- Wait for an event to be triggered. Yields the current thread.
Client.Wait = function(remoteName: string): (number, ...any?)
	local thread, t = coroutine.running(), os.clock()
	Client.Once(remoteName, function(...: any?)
		task.spawn(thread, os.clock() - t, ...)
	end)
	return coroutine.yield()
end

--@remoteName string
-- Disconnect all connections for a specific event.
Client.DisconnectAll = function(remoteName: string)
	local id = Replication.get_id[remoteName]
	if not id then
		return
	end
	eventListeners[id] = nil
end

--@remoteName string
-- Disconnect all connections and remove the event.
Client.Destroy = Client.DisconnectAll

--@remoteName string
--@reliable boolean
-- Fire an event to the server.
Client.Fire = function(remoteName: string, reliable: boolean, ...: any?)
	local id = Replication.get_id[remoteName]
	if id then
		table.insert(reliable and queueEvent or queueUnreliableEvent, {
			Replication.get_id[remoteName],
			{ ... } :: any,
		})
	end
end

--@remoteName string
--@timeout number?
-- Invoke the server with timeout support. Yields the current thread. Returns nil if timeout occurs.
Client.Invoke = function(remoteName: string, timeout: number?, ...: any?): ...any?
	local id = Replication.get_id[remoteName]
	if not id then
		return nil
	end
	invokeId += 1
	local reqid, thread = `{invokeId}`, coroutine.running()

	pendingInvokes[reqid] = thread
	task.delay(timeout or 2, function()
		local pending = pendingInvokes[reqid]
		if not pending then
			return
		end
		task.spawn(pending, nil)
		pendingInvokes[reqid] = nil
	end)
	table.insert(queueEvent, {
		0,
		{ id, reqid :: any, { ... } :: any } :: any,
	})
	return coroutine.yield()
end

if RunService:IsClient() then
	local function processIncoming(b: buffer, ref: { Instance }?, handleInvokes: boolean)
		if type(b) ~= "buffer" then return end
		local decoded: buffer = handleInvokes and Xor.decodeClient(b) or b
		local contents = Buffer.readEvents(decoded, ref, eventSchemas)
		for _, content in contents do
			local remote = content[1]
			local args = content[2]
			if handleInvokes then
				if remote == 0 then
					local id = content[1]
					local results = content[2]
					local pending = pendingInvokes[id]
					if pending then
						task.spawn(pending :: any, table.unpack(results))
						pendingInvokes[id] = nil
					end
					continue
				end
				if remote == 1 then
					local remoteName, id, rargs = args[1], args[2], args[3]
					local connections = eventListeners[remoteName]
					if connections and #connections > 0 then
						Thread(function()
							local results = { connections[1].c(table.unpack(rargs)) }
							table.insert(queueEvent, { 1, { id, results } :: any })
						end)
					end
					continue
				end
			end
			local connections = eventListeners[remote]
			if connections then
				for _, connection in connections do
					Thread(connection.c, table.unpack(args))
				end
			end
		end
	end

	Event.OnClientEvent:Connect(function(b: buffer, ref: { Instance }?)
		processIncoming(b, ref, true)
	end)

	UnreliableEvent.OnClientEvent:Connect(function(b: buffer, ref: { Instance }?)
		processIncoming(b, ref, false)
	end)

	RunService.PostSimulation:Connect(function(d: number)
		deltaT += d
		if deltaT < cycle then return end
		deltaT = 0

		--reliable
		if #queueEvent > 0 then
			Buffer.writeEvents(writer, queueEvent, eventSchemas)
			do
				local buf, ref = Buffer.buildWithRefs(writer)
				local encoded = Xor.encodeClient(buf)
				Buffer.reset(writer)
				if not ref or #ref == 0 then
					Event:FireServer(encoded)
				else
					Event:FireServer(encoded, ref)
				end
			end
			table.clear(queueEvent)
		end
		-- unreliable
		if #queueUnreliableEvent > 0 then
			Buffer.writeEvents(writer, queueUnreliableEvent, eventSchemas)
			do
				local buf, ref = Buffer.buildWithRefs(writer)
				Buffer.reset(writer)
				if not ref or #ref == 0 then
					UnreliableEvent:FireServer(buf)
				else
					UnreliableEvent:FireServer(buf, ref)
				end
			end
			table.clear(queueUnreliableEvent)
		end
	end)
end

--[[
    @class Client
    @schema
    define a schema for your data and use a strict packing
]]
Client.Schema = Buffer.Schema

return Client :: typeof(Client)


--!optimize 2
--!strict
local Replication = {}

local RunService = game:GetService("RunService")
local _repl: RemoteEvent = script.Parent:WaitForChild("_repl")

local Buffer = require("./Util/Buffer")
local Identifier = require("./Util/Identifier")

local identifiers_schema = Buffer.Schema.string
local warp_identifier_registry = shared.__warp_identifier_registry

if RunService:IsClient() or RunService:IsRunMode() then
	local pending_id_yields, pending_name_yields, ready_yields = {}, {}, {}
	local is_ready = false

	if RunService:IsClient() then
		_repl.OnClientEvent:Connect(function(b: buffer)
			if type(b) ~= "buffer" then
				return
			end
			local contents = Buffer.readRepl(b, identifiers_schema)
			if #contents == 0 then return end
			for _, content in contents do
				local id: number, remote: string = content[1], content[2]
				warp_identifier_registry.cache[remote] = id
				warp_identifier_registry.name[id] = remote

				if pending_id_yields[remote] then
					for _, thread in pending_id_yields[remote] do
						task.spawn(thread, id)
					end
					pending_id_yields[remote] = nil
				end
				if pending_name_yields[id] then
					for _, thread in pending_name_yields[id] do
						task.spawn(thread, remote)
					end
					pending_name_yields[id] = nil
				end
			end
			if not is_ready then
				is_ready = true
				for _, thread in ready_yields do
					task.spawn(thread :: thread)
				end
				table.clear(ready_yields)
			end
		end)
		_repl:FireServer()
	end

	--@yield
	-- wait for the identifiers to be replicated from the server
	Replication.wait_for_ready = function()
		if is_ready then return end

		local thread = coroutine.running()
		table.insert(ready_yields, thread)
		coroutine.yield()
	end

	Replication.get_id = warp_identifier_registry.cache
	Replication.get_name = warp_identifier_registry.name
else
	local replication_ready: { Player }, pending_replications = {}, {}
	local writer: Buffer.Writer = Buffer.createWriter()
	local replication_id: number = Identifier.get_id("id_replication") or 1
	local is_scheduled = false

	if not Identifier.has_name("id_replication") or not replication_id then
		replication_id = Identifier.get_id("id_replication") or 1
	end

	local function replicateToAll(content: any, id: number?)
		if #replication_ready == 0 then return end

		if type(content) == "string" and id then
			pending_replications[content] = id
		else
			for k, v in content :: any do
				pending_replications[k] = v
			end
		end

		if not is_scheduled then
			is_scheduled = true
			task.defer(function()
				is_scheduled = false

				local count = 0
				for _ in pending_replications do count += 1 end
				if count == 0 then return end

				Buffer.writeRepl(writer, pending_replications, count, identifiers_schema)
				local buf = Buffer.build(writer)
				Buffer.reset(writer)

				for _, player: Player in replication_ready do
					_repl:FireClient(player, buf)
				end

				table.clear(pending_replications)
			end)
		end
	end

	local function replicateTo(player: Player)
		Buffer.writeRepl(writer, warp_identifier_registry.cache, warp_identifier_registry.counter, identifiers_schema)
		do
			local buf = Buffer.build(writer)
			Buffer.reset(writer)
			_repl:FireClient(player, buf)
		end
	end

	Identifier.on_added(replicateToAll)

	_repl.OnServerEvent:Connect(function(player: Player)
		if table.find(replication_ready, player) then return end
		table.insert(replication_ready, player)
		replicateTo(player)
	end)

	Replication.remove = function(player: Player)
		local idx = table.find(replication_ready, player)
		if not idx then return end
		table.remove(replication_ready, idx)
	end
end

return Replication :: typeof(Replication)


--!optimize 2
--!strict
--@EternityDev
local BITS: number = 8
local MAX_VALUE: number = bit32.lshift(1, BITS) - 1
local hook_fn: (string, number) -> ()

if not shared.__warp_identifier_registry then
	shared.__warp_identifier_registry = {
		cache = {} :: { [string]: number },
		name = {} :: { [number]: string },
		counter = 0,
	}
end

local registry = shared.__warp_identifier_registry
local function fastAssert(condition: boolean, ...: any)
	if not condition then
		error(...)
	end
end

local Identifier = {}

Identifier.on_added = function(fn: (string, number) -> ())
	assert(hook_fn == nil, ".on_added can only be called once.")
	hook_fn = fn
end

Identifier.has_name = function(name: string): boolean
	return registry.cache[name] ~= nil
end

Identifier.has_id = function(id: number): boolean
	return registry.name[id] ~= nil
end

Identifier.get_id = function(name: string): number
	local cached = registry.cache[name]
	if cached then
		return cached
	end

	local id = registry.counter + 1
	fastAssert(id <= MAX_VALUE, `Identifier overflow: exceeded {MAX_VALUE + 1} unique names.`)
	registry.counter = id
	registry.cache[name] = id
	registry.name[id] = name
	if hook_fn then
		task.defer(hook_fn, name, id)
	end
	return id
end

Identifier.get_name = function(id: number): string
	return registry.name[id]
end

return Identifier :: typeof(Identifier)


--!strict
--!optimize 2
--@EternityDev
local thread: thread?

local function passer<T...>(func: (T...) -> (), ...: T...): ()
	local HoldThread: thread = thread :: thread
	thread = nil
	func(...)
	thread = HoldThread
end

local function newThread(): ()
	thread = coroutine.running()
	while true do passer(coroutine.yield()) end
end

return function<T...>(func: (T...) -> (), ...: T...): ()
	if not thread then task.spawn(newThread) end
	task.spawn(thread :: thread, func, ...)
end


--!strict
--!native
--!optimize 2
local Xor = {}

type XorEntry = { buf: buffer, len: number }

local serverPrev: { [Player]: XorEntry } = {}
local clientPrev: { [Player]: XorEntry } = {}

local clientOutPrev: XorEntry? = nil
local clientInPrev: XorEntry? = nil

local function xorApply(current: buffer, curLen: number, prev: buffer, prevLen: number): buffer
	local out = buffer.create(curLen)
	local common = if curLen < prevLen then curLen else prevLen

	local wordEnd = (common // 4) * 4
	for off = 0, wordEnd - 4, 4 do
		buffer.writeu32(out, off, bit32.bxor(buffer.readu32(current, off), buffer.readu32(prev, off)))
	end
	for i = wordEnd, common - 1 do
		buffer.writeu8(out, i, bit32.bxor(buffer.readu8(current, i), buffer.readu8(prev, i)))
	end

	if curLen > common then
		buffer.copy(out, common, current, common, curLen - common)
	end
	return out
end

local function storeEntry(entry: XorEntry?, src: buffer, len: number): XorEntry
	local e: XorEntry = entry or { buf = buffer.create(len > 64 and len or 64), len = 0 }

	if buffer.len(e.buf) < len then
		e.buf = buffer.create(len * 2)
	end

	buffer.copy(e.buf, 0, src, 0, len)
	e.len = len
	return e
end

function Xor.remove(player: Player)
	serverPrev[player] = nil
	clientPrev[player] = nil
end

function Xor.encodeServer(player: Player, buf: buffer): buffer
	local len = buffer.len(buf)
	local entry = serverPrev[player]

	if not entry then
		serverPrev[player] = storeEntry(nil, buf, len)
		return buf
	end

	local encoded = xorApply(buf, len, entry.buf, entry.len)
	serverPrev[player] = storeEntry(entry, buf, len)
	return encoded
end

function Xor.decodeClient(buf: buffer): buffer
	local len = buffer.len(buf)
	local prev = clientInPrev

	if not prev then
		clientInPrev = storeEntry(nil, buf, len)
		return buf
	end

	local recovered = xorApply(buf, len, prev.buf, prev.len)
	clientInPrev = storeEntry(prev, recovered, len)
	return recovered
end

function Xor.encodeClient(buf: buffer): buffer
	local len = buffer.len(buf)
	local prev = clientOutPrev

	if not prev then
		clientOutPrev = storeEntry(nil, buf, len)
		return buf
	end

	local encoded = xorApply(buf, len, prev.buf, prev.len)
	clientOutPrev = storeEntry(prev, buf, len)
	return encoded
end

function Xor.decodeServer(player: Player, buf: buffer): buffer
	local len = buffer.len(buf)
	local entry = clientPrev[player]

	if not entry then
		clientPrev[player] = storeEntry(nil, buf, len)
		return buf
	end

	local recovered = xorApply(buf, len, entry.buf, entry.len)
	clientPrev[player] = storeEntry(entry, recovered, len)
	return recovered
end

Xor._apply = xorApply

return Xor


--!strict
--@EternityDev
local Remote = {}

if game.RunService:IsServer() then
	if not script:FindFirstChild("_repl") then
		Instance.new("RemoteEvent", script).Name = "_repl"
	end
	if not script:FindFirstChild("Event") then
		Instance.new("RemoteEvent", script).Name = "Event"
	end
	if not script:FindFirstChild("UnreliableEvent") then
		Instance.new("UnreliableRemoteEvent", script).Name = "UnreliableEvent"
	end
end

local Client = require("@self/Client")
local Server = require("@self/Server")
local Buffer = require("@self/Util/Buffer")

--[[
	@class Remote
	@client
]]
Remote.Client = function()
	return Client
end

--[[
	@class Remote
	@server
]]
Remote.Server = function()
	return Server
end

--[[
	@class Remote
	@schema
	define a schema for your data and use a strict packing
]]
Remote.Buffer = Buffer

return Remote :: typeof(Remote)


--!strict

local RunService = game:GetService("RunService") :: RunService
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Demo = ReplicatedStorage.Demo
local ViewportWindow = require(ReplicatedStorage.Packages.ViewportWindow)

local sky = game.Lighting:FindFirstChildWhichIsA("Sky")

local function setupPortal()
	local windowA = ViewportWindow.fromPart(workspace.World.PortalA, Enum.NormalId.Front)
	local windowB = ViewportWindow.fromPart(workspace.World.PortalB, Enum.NormalId.Front)

	windowA:GetSurfaceGui().ResetOnSpawn = false
	windowB:GetSurfaceGui().ResetOnSpawn = false
	windowA:SetSky(sky)
	windowB:SetSky(sky)

	local Portal = require(Demo.Portal)
	local portal = Portal.new(windowA, windowB)

	RunService.RenderStepped:Connect(function()
		portal:Render()
	end)
end

local function setupMirror()
	local window = ViewportWindow.fromPart(workspace.World.Mirror, Enum.NormalId.Front)

	window:GetSurfaceGui().ResetOnSpawn = false
	window:SetSky(sky)

	local Mirror = require(Demo.Mirror)
	local mirror = Mirror.new(window)

	RunService.RenderStepped:Connect(function()
		mirror:Render()
	end)
end

local function setupSeeThrough()
	local surface = workspace.World.SeeThrough
	local window = ViewportWindow.fromPart(surface, Enum.NormalId.Front)

	window:GetSurfaceGui().ResetOnSpawn = false
	--window:SetSky(sky)

	for _, child in surface:GetChildren() do
		child.Parent = window:GetViewportFrame()
	end

	RunService.RenderStepped:Connect(function()
		window:Render()
	end)
end

setupPortal()
setupMirror()
setupSeeThrough()


--!strict

local Copy = {}

function Copy.instance<T>(instance: T & Instance)
	local archivableMap = {}
	for _, child in instance:GetChildren() do
		archivableMap[child] = child.Archivable
		child.Archivable = false
	end

	local archivable = instance.Archivable
	instance.Archivable = true
	local copy = instance:Clone()
	instance.Archivable = archivable

	for child, prevArchivable in archivableMap do
		child.Archivable = prevArchivable
	end

	return copy :: T
end

function Copy.hierarchy(root: Instance)
	local copyMap = {}
	local stack = { root }
	while #stack > 0 do
		local popped = table.remove(stack) :: Instance
		copyMap[popped] = Copy.instance(popped)
		for _, child in popped:GetChildren() do
			table.insert(stack, child)
		end
	end
	return copyMap[root], copyMap
end

return Copy


--!strict

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

local Trove = require(game.ReplicatedStorage.Packages.Trove)

local MatchHandlers: { [string]: (Instance, Instance, Trove.Trove) -> () } = {}

function MatchHandlers.Default(real, copy, trove)
	-- this will cause issues if not using deferred signaling
	trove:Add(real.Changed:Connect(function(property)
		pcall(function()
			local copyAny = copy :: any
			local realAny = real :: any
			copyAny[property] = realAny[property]
		end)
	end))
end

function MatchHandlers.BasePart(real, copy, trove)
	if not (real:IsA("BasePart") and copy:IsA("BasePart")) then
		return
	end

	copy.Anchored = true

	trove:Add(RunService.Heartbeat:Connect(function()
		if not real.Anchored then
			copy.CFrame = real.CFrame
		end
	end))
end

return MatchHandlers


--!strict

-- This is kind of messy, but it gets the job done
-- Allows for cloning of instance hierarchies in the data model and matching their properties
-- Useful for having a matching world in viewport frames

local Trove = require(game.ReplicatedStorage.Packages.Trove)
local MatchHandlers = require(script.MatchHandlers)
local Copy = require(script.Copy)

local SUPPORTED = {
	"BasePart",
	"BodyColors",
	"Decal",
	"Folder",
	"Humanoid",
	"JointInstance",
	"Model",
	"Pants",
	"Shirt",
	"SpecialMesh",
	"SurfaceAppearance",
	"Texture",
	"WrapLayer",
	"WrapTarget",
}

local REF_PROPS: { [string]: { string } } = {
	JointInstance = { "Part0", "Part1" },
}

local UNSUPPORTED = { "Terrain", "Status" }

type RefWaitList = {
	[Instance]: {
		{
			Copy: Instance,
			Prop: string,
		}
	},
}

-- Class

local CloneMatchClass = {}
CloneMatchClass.__index = CloneMatchClass
CloneMatchClass.ClassName = "CloneMatch"

export type CloneMatch = typeof(setmetatable(
	{} :: {
		trove: Trove.Trove,
		root: Instance,
		handlers: typeof(MatchHandlers),
		copyMap: { [Instance]: Instance },
		cleanMap: { [Instance]: Trove.Trove },
		refWaitList: RefWaitList,
	},
	CloneMatchClass
))

-- Constructors

function CloneMatchClass.raw(root: Instance, rootCopy: Instance, handlers: typeof(MatchHandlers))
	local self = setmetatable({}, CloneMatchClass) :: CloneMatch

	self.trove = Trove.new()

	self.root = root
	self.copyMap = {}
	self.cleanMap = {}
	self.refWaitList = {}

	self.handlers = table.clone(MatchHandlers)
	for key, handler in handlers do
		self.handlers[key] = handler
	end

	self:_track(root, rootCopy)

	for _, descendant in root:GetDescendants() do
		if self:_isSupported(descendant) then
			self:_add(descendant)
		end
	end

	self.trove:Add(root.DescendantAdded:Connect(function(descendant)
		if self:_isSupported(descendant) then
			self:_add(descendant)
		end
	end))

	self.trove:Add(root.DescendantRemoving:Connect(function(descendant)
		self:_remove(descendant)
	end))

	self.trove:Add(function()
		for instance, _ in self.cleanMap do
			self:_remove(instance)
		end
	end)

	return self
end

function CloneMatchClass.workspace(handlers: typeof(MatchHandlers)?)
	local worldModel = Instance.new("WorldModel")
	worldModel.Name = "Workspace"
	return CloneMatchClass.raw(workspace, worldModel, handlers or {})
end

function CloneMatchClass.new(root: Instance, handlers: typeof(MatchHandlers)?)
	return CloneMatchClass.raw(root, Copy.instance(root), handlers or {})
end

-- Private

function CloneMatchClass._isSupported(_self: CloneMatch, instance: Instance)
	for _, className in UNSUPPORTED do
		if instance:IsA(className) then
			return false
		end
	end

	for _, className in SUPPORTED do
		if instance:IsA(className) then
			return true
		end
	end

	return false
end

function CloneMatchClass._findNearestAncestor(self: CloneMatch, instance: Instance)
	local parent = instance.Parent
	while parent and parent:IsDescendantOf(self.root) do
		if self.copyMap[parent] then
			return parent
		end
		parent = parent.Parent
	end
	return self.root
end

function CloneMatchClass._track(self: CloneMatch, instance: Instance, copy: Instance)
	local trove = Trove.new()

	for _, handler in self.handlers do
		handler(instance, copy, trove)
	end

	if self.refWaitList[instance] then
		for _, instancePropInfo in self.refWaitList[instance] do
			local copyAny = instancePropInfo.Copy :: any
			copyAny[instancePropInfo.Prop] = copy
		end
		self.refWaitList[instance] = nil
	end

	for className, props in REF_PROPS do
		if not instance:IsA(className) then
			continue
		end

		for _, prop in props do
			local instanceAny = instance :: any
			local originalRef = instanceAny[prop]
			local doppelganger = self.copyMap[originalRef]
			if doppelganger then
				local copyAny = copy :: any
				copyAny[prop] = doppelganger
			else
				local waitList = self.refWaitList[originalRef]

				if not waitList then
					waitList = {}
					self.refWaitList[originalRef] = waitList
				end

				table.insert(waitList, { Copy = copy, Prop = prop })
			end
		end
	end

	trove:Add(function()
		copy:Destroy()
		self.copyMap[instance] = nil
	end)

	self.copyMap[instance] = copy
	self.cleanMap[instance] = trove
end

function CloneMatchClass._add(self: CloneMatch, instance: Instance)
	local ancestor = self:_findNearestAncestor(instance)
	local copy = Copy.instance(instance)

	self:_track(instance, copy)
	copy.Parent = self.copyMap[ancestor]
end

function CloneMatchClass._remove(self: CloneMatch, instance: Instance)
	local trove = self.cleanMap[instance]
	self.cleanMap[instance] = nil
	if trove then
		trove:Destroy()
	end
end

-- Public

function CloneMatchClass.Get(self: CloneMatch)
	return self.copyMap[self.root]
end

function CloneMatchClass.Destroy(self: CloneMatch)
	self.trove:Destroy()
end

--

return CloneMatchClass


--!strict

local RunService = game:GetService("RunService")

local ViewportWindow = require(game.ReplicatedStorage.Packages.ViewportWindow)
local Trove = require(game.ReplicatedStorage.Packages.Trove)
local CloneMatch = require(script.Parent.Helpers.CloneMatch)

local MirrorClass = {}
MirrorClass.__index = MirrorClass
MirrorClass.ClassName = "Mirror"

type ViewportWindow = ViewportWindow.ViewportWindow

export type Mirror = typeof(setmetatable(
	{} :: {
		window: ViewportWindow,
	},
	MirrorClass
))

function MirrorClass.new(window: ViewportWindow)
	local self = setmetatable({}, MirrorClass) :: Mirror

	self.window = window

	local copy = CloneMatch.workspace({
		["BasePart"] = function(...)
			return self:_handleBasePart(...)
		end,
	})
	copy:Get().Parent = self.window:GetViewportFrame()

	return self
end

function MirrorClass._handleBasePart(self: Mirror, real: Instance, copy: Instance, trove: Trove.Trove)
	if not (real:IsA("BasePart") and copy:IsA("BasePart")) then
		return
	end

	local function update()
		local surfaceCF = self.window:GetSurfaceCFrameSize()
		local offset = surfaceCF:ToObjectSpace(real.CFrame)
		local x, y, z, m11, m12, m13, m21, m22, m23, m31, m32, m33 = offset:GetComponents()

		-- stylua: ignore
		local rotatedOffset = CFrame.new(
			x, y, -z,
			-m11, m12, m13,
			-m21, m22, m23,
			m31, -m32, -m33
		)

		copy.CFrame = surfaceCF * rotatedOffset
	end

	copy.Anchored = true

	update()
	trove:Add(RunService.Heartbeat:Connect(function()
		update()
	end))
end

function MirrorClass.Render(self: Mirror)
	self.window:Render()
end

return MirrorClass


--!strict

local ViewportWindow = require(game.ReplicatedStorage.Packages.ViewportWindow)
local CloneMatch = require(script.Parent.Helpers.CloneMatch)

local PortalClass = {}
PortalClass.__index = PortalClass
PortalClass.ClassName = "Portal"

type ViewportWindow = ViewportWindow.ViewportWindow

export type Portal = typeof(setmetatable(
	{} :: {
		windowA: ViewportWindow,
		windowB: ViewportWindow,
	},
	PortalClass
))

function PortalClass.new(windowA: ViewportWindow, windowB: ViewportWindow)
	local self = setmetatable({}, PortalClass) :: Portal

	self.windowA = windowA
	self.windowB = windowB

	CloneMatch.workspace():Get().Parent = self.windowA:GetViewportFrame()
	CloneMatch.workspace():Get().Parent = self.windowB:GetViewportFrame()

	return self
end

function PortalClass.Render(self: Portal)
	local camera = workspace.CurrentCamera
	local cameraCFrame = camera.CFrame

	local surfaceACF, surfaceASize = self.windowA:GetSurfaceCFrameSize()
	local surfaceBCF, surfaceBSize = self.windowB:GetSurfaceCFrameSize()

	local rotatedSurfaceBCF = surfaceBCF * CFrame.fromEulerAnglesYXZ(0, math.pi, 0)
	local rotatedSurfaceACF = surfaceACF * CFrame.fromEulerAnglesYXZ(0, math.pi, 0)

	local renderACF = rotatedSurfaceBCF * surfaceACF:ToObjectSpace(cameraCFrame)
	local renderBCF = rotatedSurfaceACF * surfaceBCF:ToObjectSpace(cameraCFrame)

	self.windowA:RenderManual(renderACF, rotatedSurfaceBCF, surfaceBSize)
	self.windowB:RenderManual(renderBCF, rotatedSurfaceACF, surfaceASize)

	return cameraCFrame
end

return PortalClass


--!strict

local SKYBOX_MAP: { [string]: CFrame } = {
	["Bk"] = CFrame.fromEulerAnglesYXZ(0, math.rad(180), 0),
	["Dn"] = CFrame.fromEulerAnglesYXZ(-math.rad(90), math.rad(90), 0),
	["Ft"] = CFrame.fromEulerAnglesYXZ(0, 0, 0),
	["Lf"] = CFrame.fromEulerAnglesYXZ(0, -math.rad(90), 0),
	["Rt"] = CFrame.fromEulerAnglesYXZ(0, math.rad(90), 0),
	["Up"] = CFrame.fromEulerAnglesYXZ(math.rad(90), math.rad(90), 0),
}

local SkyboxHelper = {}

function SkyboxHelper.skySphere(sky: Sky)
	local model = Instance.new("Model")
	model.Name = "SkyboxModel"

	for property, rotationCF in SKYBOX_MAP do
		local side = Instance.new("Part")
		side.Anchored = true
		side.Name = property
		side.CFrame = rotationCF
		side.Size = Vector3.one
		side.Parent = model

		local mesh = Instance.new("SpecialMesh")
		mesh.MeshId = "rbxassetid://3083991485"
		mesh.Offset = Vector3.new(0, 0, -0.683)
		mesh.Parent = side

		local decal = Instance.new("Decal")
		decal.Texture = (sky :: any)["Skybox" .. property]
		decal.Parent = side
	end

	return model
end

return SkyboxHelper


--!strict

local Players = game:GetService("Players") :: Players
local Lighting = game:GetService("Lighting") :: Lighting

local Trove = require(script.Parent.Trove)
local SkyboxHelper = require(script.SkyboxHelper)

local VEC_XZ = Vector3.new(1, 0, 1)
local VEC_YZ = Vector3.new(0, 1, 1)

local ViewportWindowClass = {}
ViewportWindowClass.__index = ViewportWindowClass
ViewportWindowClass.ClassName = "ViewportWindow"

export type ViewportWindow = typeof(setmetatable(
	{} :: {
		trove: Trove.Trove,
		surfaceGui: SurfaceGui,

		nearPlaneZ: number?,

		worldCamera: Camera,
		worldViewportFrame: ViewportFrame,

		skyTrove: Trove.Trove,
		skyboxCamera: Camera,
		skyboxViewportFrame: ViewportFrame,
	},
	ViewportWindowClass
))

function ViewportWindowClass.fromPart(part: BasePart, face: Enum.NormalId)
	local self = setmetatable({}, ViewportWindowClass) :: ViewportWindow

	local player = assert(Players.LocalPlayer, "No local player found.")
	local playerGui = assert(player.PlayerGui, "Player has no PlayerGui.")

	self.trove = Trove.new()

	self.surfaceGui = Instance.new("SurfaceGui")
	self.surfaceGui.Name = "ViewportWindow"
	self.surfaceGui.Face = face
	self.surfaceGui.Adornee = part
	self.surfaceGui.ClipsDescendants = true
	self.surfaceGui.SizingMode = Enum.SurfaceGuiSizingMode.FixedSize
	self.surfaceGui.CanvasSize = Vector2.new(1024, 1024)
	self.surfaceGui.ZOffset = 2 -- Fixes an issue viewing the surface gui closely on semi-transparent parts
	self.surfaceGui.Parent = playerGui
	self.trove:Add(self.surfaceGui)

	self.nearPlaneZ = 0

	self.worldCamera = Instance.new("Camera")
	self.worldCamera.Name = "WorldCamera"
	self.worldCamera.Parent = self.surfaceGui
	self.trove:Add(self.worldCamera)

	self.worldViewportFrame = Instance.new("ViewportFrame")
	self.worldViewportFrame.BackgroundTransparency = 1
	self.worldViewportFrame.Size = UDim2.fromScale(1, 1)
	self.worldViewportFrame.Name = "World"
	self.worldViewportFrame.ZIndex = 2
	self.worldViewportFrame.Ambient = Lighting.Ambient
	self.worldViewportFrame.LightColor = Color3.new(1, 1, 1)
	self.worldViewportFrame.LightDirection = -Lighting:GetSunDirection()
	self.worldViewportFrame.CurrentCamera = self.worldCamera
	self.worldViewportFrame.Parent = self.surfaceGui
	self.trove:Add(self.worldViewportFrame)

	self.skyTrove = Trove.new()

	self.skyboxCamera = Instance.new("Camera")
	self.skyboxCamera.Name = "SkyboxCamera"
	self.skyboxCamera.Parent = self.surfaceGui
	self.trove:Add(self.skyboxCamera)

	self.skyboxViewportFrame = Instance.new("ViewportFrame")
	self.skyboxViewportFrame.BackgroundTransparency = 1
	self.skyboxViewportFrame.Size = UDim2.fromScale(1, 1)
	self.skyboxViewportFrame.Name = "Skybox"
	self.skyboxViewportFrame.ZIndex = 1
	self.skyboxViewportFrame.Ambient = Color3.new(1, 1, 1)
	self.skyboxViewportFrame.LightColor = Color3.new(1, 1, 1)
	self.skyboxViewportFrame.LightDirection = -Lighting:GetSunDirection()
	self.skyboxViewportFrame.CurrentCamera = self.skyboxCamera
	self.skyboxViewportFrame.Parent = self.surfaceGui
	self.trove:Add(self.skyboxViewportFrame)

	return self
end

-- Public Methods

function ViewportWindowClass.SetSky(self: ViewportWindow, sky: Sky?)
	self.skyTrove:Clean()

	if sky and sky:IsA("Sky") then
		local skyCopyA = sky:Clone()
		skyCopyA.Name = "Sky"
		skyCopyA.Parent = self.worldViewportFrame
		self.skyTrove:Add(skyCopyA)

		local skyCopyB = sky:Clone()
		skyCopyB.Name = "Sky"
		skyCopyB.Parent = self.skyboxViewportFrame
		self.skyTrove:Add(skyCopyB)

		-- we can't do a small sphere b/c then the near clipping plane
		-- can cut-off some skybox when close to the surface
		local skyModel = SkyboxHelper.skySphere(sky)
		skyModel:ScaleTo(10)
		skyModel.Parent = self.skyboxViewportFrame
		self.skyTrove:Add(skyModel)
	end
end

function ViewportWindowClass.SetNearPlaneZ(self: ViewportWindow, nearPlaneZ: number?)
	self.nearPlaneZ = nearPlaneZ
end

function ViewportWindowClass.GetNearPlaneZ(self: ViewportWindow)
	return self.nearPlaneZ
end

function ViewportWindowClass.GetSurfaceGui(self: ViewportWindow)
	return self.surfaceGui
end

function ViewportWindowClass.GetViewportFrame(self: ViewportWindow)
	return self.worldViewportFrame
end

function ViewportWindowClass.GetSurfacePart(self: ViewportWindow)
	local surfacePart = self.surfaceGui.Adornee
	assert(surfacePart and surfacePart:IsA("BasePart"), "No surface part found.")
	return surfacePart
end

function ViewportWindowClass.GetSurfaceCFrameSize(self: ViewportWindow)
	local surfacePart = self:GetSurfacePart()

	local v = -Vector3.FromNormalId(self.surfaceGui.Face)
	local u = Vector3.new(v.Y, math.abs(v.X + v.Z), 0)
	local relativeCF = CFrame.fromMatrix(Vector3.zero, u:Cross(v), u, v)
	local worldCF = surfacePart.CFrame * CFrame.new(-v * surfacePart.Size / 2) * relativeCF

	local size = Vector3.new(
		math.abs(relativeCF.XVector:Dot(surfacePart.Size)),
		math.abs(relativeCF.YVector:Dot(surfacePart.Size)),
		math.abs(relativeCF.ZVector:Dot(surfacePart.Size))
	)

	return worldCF, size
end

function ViewportWindowClass.RenderManual(
	self: ViewportWindow,
	cameraCFrame: CFrame,
	surfaceCFrame: CFrame,
	surfaceSize: Vector3
)
	local camera = workspace.CurrentCamera
	local cameraPosition = cameraCFrame.Position
	local distance = surfaceCFrame:PointToObjectSpace(cameraPosition).Z

	if distance > 0 then
		-- the camera cannot see the surface so there's no point in continuing the calculation
		return
	end

	-- please see EmilyBendsSpace's post/code on the devforum as it provides the foundation for what's being done here
	-- https://devforum.roblox.com/t/re-creating-a-portal-effect/337159/78
	local xCross = surfaceCFrame.YVector:Cross(cameraCFrame.ZVector)
	local xVector = xCross:Dot(xCross) > 0 and xCross.Unit or cameraCFrame.XVector
	local levelCameraCFrame = CFrame.fromMatrix(cameraPosition, xVector, surfaceCFrame.YVector)

	local topCenter = surfaceCFrame * Vector3.new(0, surfaceSize.Y / 2, 0)
	local bottomCenter = surfaceCFrame * Vector3.new(0, -surfaceSize.Y / 2, 0)
	local cameraSpaceTopCenter = levelCameraCFrame:PointToObjectSpace(topCenter)
	local cameraSpaceBottomCenter = levelCameraCFrame:PointToObjectSpace(bottomCenter)

	local topDirection = (cameraSpaceTopCenter * VEC_YZ).Unit
	local bottomDirection = (cameraSpaceBottomCenter * VEC_YZ).Unit
	local alpha = math.sign(topDirection.Y) * math.acos(-topDirection.Z)
	local beta = math.sign(bottomDirection.Y) * math.acos(-bottomDirection.Z)

	local fovHeight = 2 * math.tan(math.rad(camera.FieldOfView / 2))
	local surfaceFovHeight = math.tan(alpha) - math.tan(beta)
	local fovHeightRatio = surfaceFovHeight / fovHeight

	local dv = surfaceCFrame:VectorToObjectSpace(surfaceCFrame.Position - cameraCFrame.Position)
	local dvXZ = (dv * VEC_XZ).Unit
	local dvYZ = dv * VEC_YZ

	local dvZ = -dvXZ.Z
	local camXZ = (surfaceCFrame:VectorToObjectSpace(cameraCFrame.LookVector) * VEC_XZ).Unit
	local scale = camXZ:Dot(dvXZ) / dvZ
	local tanArcCos = math.sqrt(1 - dvZ * dvZ) / dvZ

	local w, h = 1, 1
	if self.surfaceGui.SizingMode == Enum.SurfaceGuiSizingMode.FixedSize then
		local canvasSize = self.surfaceGui.CanvasSize
		h = (canvasSize.Y / canvasSize.X) * (surfaceSize.X / surfaceSize.Y)
	end

	local dx = math.sign(dv.X * dv.Z) * tanArcCos
	local dy = dvYZ.Y / dvYZ.Z * h
	local dz = math.abs(scale * fovHeightRatio * h)

	-- alternatively:
	-- w, h = w / dz, h / dz
	-- dx, dy = dx / dz, dy / dz
	-- dz = 1

	local m = math.max(w, h, math.abs(dx), math.abs(dy), dz)

	local nearZCFrame = CFrame.identity
	if self.nearPlaneZ then
		local cameraNearPlaneZ = camera.NearPlaneZ
		local minNearPlaneZ = math.min(cameraNearPlaneZ, distance + self.nearPlaneZ)
		local nz = (minNearPlaneZ / cameraNearPlaneZ) * (dz / m)
		nearZCFrame = CFrame.new(0, 0, 0, 1 / nz, 0, 0, 0, 1 / nz, 0, 0, 0, 1 / nz)
	end

	local renderRotation = surfaceCFrame.Rotation
		* CFrame.fromEulerAnglesYXZ(0, math.pi, 0)
		* CFrame.new(0, 0, 0, w / m, 0, 0, 0, h / m, 0, dx / m, dy / m, dz / m)

	local renderCFrame = renderRotation + cameraPosition

	self.worldCamera.FieldOfView = camera.FieldOfView
	self.worldCamera.CFrame = renderCFrame * nearZCFrame

	self.skyboxCamera.FieldOfView = camera.FieldOfView
	self.skyboxCamera.CFrame = renderCFrame.Rotation
end

function ViewportWindowClass.Render(self: ViewportWindow)
	local camera = workspace.CurrentCamera
	local surfaceCFrame, surfaceSize = self:GetSurfaceCFrameSize()
	return self:RenderManual(camera.CFrame, surfaceCFrame, surfaceSize)
end

function ViewportWindowClass.Destroy(self: ViewportWindow)
	self.skyTrove:Destroy()
	self.trove:Destroy()
end

--

return ViewportWindowClass


--!strict

--[=[
	@class Observers

	A collection of observer utility functions.
]=]

return {
	observeTag = require(script.observeTag),
	observeAttribute = require(script.observeAttribute),
	observeProperty = require(script.observeProperty),
	observePlayer = require(script.observePlayer),
	observeCharacter = require(script.observeCharacter),
	observeLocalCharacter = require(script.observeLocalCharacter),
}


--!strict

export type AttributeValue =
	| string
	| boolean
	| number
	| UDim
	| UDim2
	| BrickColor
	| Color3
	| Vector2
	| Vector3
	| CFrame
	| NumberSequence
	| ColorSequence
	| NumberRange
	| Rect
	| Font

type Callback = (value: AttributeValue) -> (() -> ())?

local function defaultGuard(_value: AttributeValue)
	return true
end

--[=[
	@within Observers

	Creates an observer around an attribute of a given instance. The callback will fire for any non-nil
	attribute value.

	```lua
	observeAttribute(workspace.Model, "MyAttribute", function(value)
		print("MyAttribute is now:", value)

		return function()
			-- Cleanup
			print("MyAttribute is no longer:", value)
		end
	end)
	```

	An optional `guard` predicate function can be supplied to further narrow which values trigger the observer.
	For instance, if only strings are wanted:

	```lua
	observeAttribute(
		workspace.Model,
		"MyAttribute",
		function(value) print("value is a string", value) end,
		function(value) return typeof(value) == "string" end
	)
	```

	The observer also returns a function that can be called to clean up the observer:
	```lua
	local stopObserving = observeAttribute(workspace.Model, "MyAttribute", function(value) ... end)

	task.wait(10)
	stopObserving()
	```
]=]
local function observeAttribute(
	instance: Instance,
	name: string,
	callback: Callback,
	guard: ((value: AttributeValue) -> boolean)?
): () -> ()
	local cleanFn: (() -> ())? = nil

	local onAttrChangedConn: RBXScriptConnection
	local changedId = 0

	local valueGuard: (value: AttributeValue) -> boolean = if guard ~= nil then guard else defaultGuard

	local function onAttributeChanged()
		if cleanFn ~= nil then
			task.spawn(cleanFn)
			cleanFn = nil
		end

		changedId += 1
		local id = changedId

		local value = instance:GetAttribute(name) :: AttributeValue?

		if value ~= nil and valueGuard(value) then
			task.spawn(function()
				local clean = (callback :: any)(value)
				if typeof(clean) == "function" then
					if id == changedId and onAttrChangedConn.Connected then
						cleanFn = clean
					else
						task.spawn(clean)
					end
				end
			end)
		end
	end

	-- Get changed values:
	onAttrChangedConn = instance:GetAttributeChangedSignal(name):Connect(onAttributeChanged)

	-- Get initial value:
	task.defer(function()
		if not onAttrChangedConn.Connected then
			return
		end

		onAttributeChanged()
	end)

	-- Cleanup:
	return function()
		onAttrChangedConn:Disconnect()
		if cleanFn ~= nil then
			task.spawn(cleanFn)
			cleanFn = nil
		end
	end
end

return observeAttribute


--!strict

local observePlayer = require(script.Parent.observePlayer)

type Callback = (player: Player, character: Model) -> (() -> ())?

--[=[
	@within Observers

	Creates an observer that captures each character in the game.

	```lua
	observeCharacter(function(player, character)
		print("Character spawned for " .. player.Name)

		return function()
			-- Cleanup
			print("Character removed for " .. player.Name)
		end
	end)
	```

	An optional `allowedPlayers` array can be provided. Only characters
	belonging to players in the array are observed.

	```lua
	observeCharacter(function(player, character)
		-- ...
	end, { somePlayer, anotherPlayer })
	```
]=]
local function observeCharacter(callback: Callback, allowedPlayers: { Player }?): () -> ()
	return observePlayer(function(player)
		if allowedPlayers ~= nil and not table.find(allowedPlayers, player) then
			return nil
		end

		local cleanupFn: (() -> ())? = nil

		local characterAddedConn: RBXScriptConnection

		local function onCharacterAdded(character: Model)
			local currentCharCleanup: (() -> ())? = nil

			-- Call the callback:
			task.defer(function()
				local cleanup = (callback :: any)(player, character)
				-- If a cleanup function is given, save it for later:
				if typeof(cleanup) == "function" then
					if characterAddedConn.Connected and character.Parent then
						currentCharCleanup = cleanup
						cleanupFn = cleanup
					else
						-- Character is already gone or observer has stopped; call cleanup immediately:
						task.spawn(cleanup)
					end
				end
			end)

			-- Watch for the character to be removed from the game hierarchy:
			local ancestryChangedConn: RBXScriptConnection
			ancestryChangedConn = character.AncestryChanged:Connect(function(_, newParent)
				if newParent == nil and ancestryChangedConn.Connected then
					ancestryChangedConn:Disconnect()
					if currentCharCleanup ~= nil then
						task.spawn(currentCharCleanup)
						if cleanupFn == currentCharCleanup then
							cleanupFn = nil
						end
						currentCharCleanup = nil
					end
				end
			end)
		end

		-- Handle character added:
		characterAddedConn = player.CharacterAdded:Connect(onCharacterAdded)

		-- Handle initial character:
		task.defer(function()
			if player.Character ~= nil and characterAddedConn.Connected then
				task.spawn(onCharacterAdded, player.Character :: Model)
			end
		end)

		-- Cleanup:
		return function()
			characterAddedConn:Disconnect()
			if cleanupFn ~= nil then
				task.spawn(cleanupFn)
				cleanupFn = nil
			end
		end
	end)
end

return observeCharacter


--!strict

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local IS_CLIENT = RunService:IsClient()

type Callback = (character: Model) -> (() -> ())?

--[=[
	@within Observers
	@client

	Creates an observer that captures the local character in the game. This
	can only be called from client-side code.

	```lua
	-- In a LocalScript or Client-context script
	observeLocalCharacter(function(character)
		print("Local character spawned")

		return function()
			-- Cleanup
			print("Local character removed")
		end
	end)
	```
]=]
local function observeLocalCharacter(callback: Callback): () -> ()
	assert(IS_CLIENT, "observeLocalCharacter can only be called from the client")

	local cleanup: (() -> ())? = nil
	local subscribed = true

	local function onCharacterAdded(character: Model)
		local cleanupFn: (() -> ())? = nil

		local ancestryChangedConn: RBXScriptConnection
		ancestryChangedConn = character.AncestryChanged:Connect(function(_, parent)
			if parent == nil and ancestryChangedConn.Connected then
				ancestryChangedConn:Disconnect()
				if typeof(cleanupFn) == "function" and subscribed then
					task.spawn(cleanupFn)
					if cleanup == cleanupFn then
						cleanup = nil
					end
				end
			end
		end)

		cleanupFn = callback(character)
		if typeof(cleanupFn) == "function" then
			if ancestryChangedConn.Connected then
				cleanup = cleanupFn
			else
				task.spawn(cleanupFn)
			end
		end
	end

	local characterAddedConn = Players.LocalPlayer.CharacterAdded:Connect(onCharacterAdded)
	if Players.LocalPlayer.Character ~= nil then
		task.spawn(onCharacterAdded, Players.LocalPlayer.Character :: Model)
	end

	return function()
		subscribed = false
		characterAddedConn:Disconnect()
		if typeof(cleanup) == "function" then
			task.spawn(cleanup)
			cleanup = nil
		end
	end
end

return observeLocalCharacter


--!strict

type Callback = (player: Player) -> (() -> ())?

local Players = game:GetService("Players")

--[=[
	@within Observers

	Creates an observer that captures each player in the game.

	```lua
	observePlayer(function(player)
		print("Player entered game", player.Name)

		return function(exitReason)
			-- Cleanup
			print("Player left game", player.Name, "reason", exitReason.Name)
		end
	end)
	```
]=]
local function observePlayer(callback: Callback): () -> ()
	local playerAddedConn: RBXScriptConnection
	local playerRemovingConn: RBXScriptConnection

	local cleanupsPerPlayer: { [Player]: (exitReason: Enum.PlayerExitReason) -> () } = {}
	local inflight: { [Player]: Enum.PlayerExitReason } = {}

	local function onPlayerAdded(player: Player)
		if not playerAddedConn.Connected then
			return
		end

		task.spawn(function()
			inflight[player] = Enum.PlayerExitReason.Unknown
			local cleanup = (callback :: any)(player)
			if typeof(cleanup) == "function" then
				if playerAddedConn.Connected and player.Parent then
					cleanupsPerPlayer[player] = cleanup
				else
					task.spawn(cleanup, inflight[player])
				end
			end
			inflight[player] = nil
		end)
	end

	local function onPlayerRemoving(player: Player, exitReason: Enum.PlayerExitReason)
		if inflight[player] then
			inflight[player] = exitReason
		end

		local cleanup = cleanupsPerPlayer[player]
		cleanupsPerPlayer[player] = nil
		if typeof(cleanup) == "function" then
			task.spawn(cleanup, exitReason)
		end
	end

	-- Listen for changes:
	playerAddedConn = Players.PlayerAdded:Connect(onPlayerAdded)
	playerRemovingConn = Players.PlayerRemoving:Connect(onPlayerRemoving)

	-- Initial:
	task.defer(function()
		if not playerAddedConn.Connected then
			return
		end

		for _, player in Players:GetPlayers() do
			task.spawn(onPlayerAdded, player)
		end
	end)

	-- Cleanup:
	return function()
		playerAddedConn:Disconnect()
		playerRemovingConn:Disconnect()

		local player = next(cleanupsPerPlayer)
		while player do
			onPlayerRemoving(player, inflight[player])
			player = next(cleanupsPerPlayer)
		end
	end
end

return observePlayer


--!strict

type Callback<T> = (value: T) -> (() -> ())?

--[=[
	@within Observers

	Creates an observer around a property of a given instance.

	```lua
	observeProperty(workspace.Model, "Name", function(newName: string)
		print("New name:", name)

		return function()
			-- Cleanup
			print("Model's name is no longer:", name)
		end
	end)
	```
]=]
local function observeProperty(instance: Instance, property: string, callback: Callback<unknown>): () -> ()
	local cleanFn: (() -> ())? = nil

	local propChangedConn: RBXScriptConnection
	local changedId = 0

	local function onPropertyChanged()
		if cleanFn ~= nil then
			task.spawn(cleanFn)
			cleanFn = nil
		end

		changedId += 1
		local id = changedId

		local value = (instance :: any)[property]

		task.spawn(function()
			local clean = (callback :: any)(value)
			if typeof(clean) == "function" then
				if id == changedId and propChangedConn.Connected then
					cleanFn = clean
				else
					task.spawn(clean)
				end
			end
		end)
	end

	-- Get changed values:
	propChangedConn = instance:GetPropertyChangedSignal(property):Connect(onPropertyChanged)

	-- Get initial value:
	task.defer(function()
		if not propChangedConn.Connected then
			return
		end
		onPropertyChanged()
	end)

	-- Cleanup:
	return function()
		propChangedConn:Disconnect()
		if cleanFn ~= nil then
			task.spawn(cleanFn)
			cleanFn = nil
		end
	end
end

return observeProperty


--!strict

local CollectionService = game:GetService("CollectionService")

type InstanceStatus = "__inflight__" | "__dead__"

type Callback<T> = (instance: T) -> (() -> ())?

--[=[
	@within Observers

	Creates an observer around a CollectionService tag. The given callback will fire for each instance
	that has the given tag.

	The callback should return a function, which will be called when the given instance's tag is either
	destroyed, loses the given tag, or (if the `ancestors` table is provided) goes outside of the allowed
	ancestors.

	The function itself returns a function that can be called to stop the observer. This will also call
	any cleanup functions of currently-observed instances.

	```lua
	local stopObserver = Observers.observeTag("MyTag", function(instance: Instance)
		print("Observing", instance)

		-- The "cleanup" function:
		return function()
			print("Stopped observing", instance)
		end
	end)

	-- Optionally, the `stopObserver` function can be called to completely stop the observer:
	task.wait(10)
	stopObserver()
	```

	#### Ancestor Inclusion List
	By default, the `observeTag` function will observe a tagged instance anywhere in the Roblox game
	hierarchy. The `ancestors` table can optionally be used, which will restrict the observer to only
	observe tagged instances that are descendants of instances within the `ancestors` table.

	For instance, if a tagged instance should only be observed when it is in the Workspace, the Workspace
	can be added to the `ancestors` list. This might be useful if a tagged model prefab exist somewhere
	such as ServerStorage, but shouldn't be observed until placed into the Workspace.

	```lua
	local allowedAncestors = { workspace }

	Observers.observeTag(
		"MyTag",
		function(instance: Instance)
			...
		end,
		allowedAncestors
	)
	```
]=]
function observeTag<T>(tag: string, callback: Callback<T>, ancestors: { Instance }?): () -> ()
	local instances: { [Instance]: InstanceStatus | () -> () } = {}
	local ancestryConn: { [Instance]: RBXScriptConnection } = {}

	local onInstAddedConn: RBXScriptConnection
	local onInstRemovedConn: RBXScriptConnection

	local function isGoodAncestor(instance: Instance)
		if ancestors == nil then
			return true
		end

		for _, ancestor in ancestors do
			if instance:IsDescendantOf(ancestor) then
				return true
			end
		end

		return false
	end

	local function attemptStartup(instance: Instance)
		-- Mark instance as starting up:
		instances[instance] = "__inflight__"

		-- Attempt to run the callback:
		task.defer(function()
			if instances[instance] ~= "__inflight__" then
				return
			end

			-- Run the callback in protected mode:
			local success, cleanup = xpcall(function(inst: Instance)
				local clean = callback(inst)
				if clean ~= nil then
					assert(typeof(clean) == "function", "callback must return a function or nil")
				end
				return clean
			end, debug.traceback :: (string) -> string, instance)

			-- If callback errored, print out the traceback:
			if not success then
				local err = ""
				local firstLine = string.split(cleanup :: any, "\n")[1]
				local lastColon = string.find(firstLine, ": ")
				if lastColon then
					err = firstLine:sub(lastColon + 1)
				end
				warn(`error while calling observeTag("{tag}") callback: {err}\n{cleanup}`)
				return
			end

			if instances[instance] ~= "__inflight__" then
				-- Instance lost its tag or was destroyed before callback completed; call cleanup immediately:
				if cleanup ~= nil then
					task.spawn(cleanup :: any)
				end
			else
				-- Good startup; mark the instance with the associated cleanup function:
				instances[instance] = cleanup :: any
			end
		end)
	end

	local function attemptCleanup(instance: Instance)
		local cleanup = instances[instance]
		instances[instance] = "__dead__"

		if typeof(cleanup) == "function" then
			task.spawn(cleanup)
		end
	end

	local function onAncestryChanged(instance: Instance)
		if isGoodAncestor(instance) then
			if instances[instance] == "__dead__" then
				attemptStartup(instance)
			end
		else
			attemptCleanup(instance)
		end
	end

	local function onInstanceAdded(instance: Instance)
		if not onInstAddedConn.Connected then
			return
		end
		if instances[instance] ~= nil then
			return
		end

		instances[instance] = "__dead__"

		ancestryConn[instance] = instance.AncestryChanged:Connect(function()
			onAncestryChanged(instance)
		end)
		onAncestryChanged(instance)
	end

	local function onInstanceRemoved(instance: Instance)
		attemptCleanup(instance)

		local ancestry = ancestryConn[instance]
		if ancestry then
			ancestry:Disconnect()
			ancestryConn[instance] = nil
		end

		instances[instance] = nil
	end

	-- Hook up added/removed listeners for the given tag:
	onInstAddedConn = CollectionService:GetInstanceAddedSignal(tag):Connect(onInstanceAdded)
	onInstRemovedConn = CollectionService:GetInstanceRemovedSignal(tag):Connect(onInstanceRemoved)

	-- Attempt to mark already-existing tagged instances right away:
	task.defer(function()
		if not onInstAddedConn.Connected then
			return
		end

		for _, instance in CollectionService:GetTagged(tag) do
			task.spawn(onInstanceAdded, instance)
		end
	end)

	-- Full observer cleanup function:
	return function()
		onInstAddedConn:Disconnect()
		onInstRemovedConn:Disconnect()

		-- Clear all instances:
		local instance = next(instances)
		while instance do
			onInstanceRemoved(instance)
			instance = next(instances)
		end
	end
end

return observeTag


--!strict

--[=[
	@class GravityCamera

	This module acts as a typed interface for client-sided control of the modifications made to the camera controller.
]=]

local Players = game:GetService("Players")

-- stylua: ignore
local playerModuleObject = require(Players.LocalPlayer:WaitForChild("PlayerScripts"):WaitForChild("PlayerModule")) :: any
local cameraModuleObject = playerModuleObject:GetCameras() :: any
local controlModuleObject = playerModuleObject:GetControls() :: any

local GravityCamera = {}

function GravityCamera.getUpVector(): Vector3
	return cameraModuleObject:GetUpVector()
end

function GravityCamera.setUpVector(target: Vector3)
	cameraModuleObject:SetTargetUpVector(target)
end

function GravityCamera.getSpinPart(): BasePart
	return cameraModuleObject:GetSpinPart()
end

function GravityCamera.setSpinPart(part: BasePart)
	cameraModuleObject:SetSpinPart(part)
end

function GravityCamera.getRotationType(): Enum.RotationType
	return cameraModuleObject:GetRotationType()
end

function GravityCamera.getMoveVector(cameraCF: CFrame, inputMove: Vector3?): Vector3
	local move = inputMove or controlModuleObject:GetMoveVector() :: Vector3
	local _, _, _, r00, r01, r02, _, r11, r12, _, _, r22 = cameraCF:GetComponents()

	local c, s
	local q = math.sign(r11)

	if r12 < 1 and r12 > -1 then
		c = r22
		s = r02
	else
		c = r00
		s = -r01 * math.sign(r12)
	end

	local norm = math.sqrt(c * c + s * s)
	local x = (c * move.X * q + s * move.Z) / norm
	local z = (c * move.Z - s * move.X * q) / norm

	return Vector3.new(x, 0, z)
end

return GravityCamera