local __start = os.clock()

local Log = require("@shared/log")
Log:SetPrefix("[SB]")
Log.print("Loading...")

do
	local wm = require("@shared/wm")
	wm.isWorkerManager = false
	table.freeze(wm)
end

-- Fetch assets and destroy script
local sbActor = script.Parent
local root = sbActor.Parent

do
	local thread = coroutine.running()
	task.defer(
		function() -- Defer has a small yield (under a frame) allowing us to delete the script (instances can't change their parent instantly after there were parented / created)
			sbActor:Destroy()
			script:Destroy()
			script = nil

			task.spawn(thread)
		end
	)

	coroutine.yield() -- Yield thread until script has been destroyed, so events dont get connected in the process (then disconnected by :Destroy())
end

local Assets = require("@shared/assets")
Assets:Init(root, "assets")

local clientScripts = Assets:get("clientScripts")
do
	local clientAssets = clientScripts:WaitForChild("assets")
	clientAssets:SetAttribute("_descendants", #clientAssets:GetDescendants())
end

-- Services and modules
local Players = game:GetService("Players")

Log.debug("Loading modules...")

local Network = require("@server/network")
local Commands = require("@server/commands")
local Output = require("@server/output")
local World = require("@server/world")
local WorkerManagers = require("@shared/workerManagers")
local Functions = require("@shared/functions")
local Protection = require("@shared/protection")

WorkerManagers:Init(root, "workerManager")
Network:Init()
Commands:Init({ require("@server/commands/default"), require("@server/commands/get") })

Log.debug("Loading systems...") -- Maybe we should move these into their own modules?

-- Player system
do
	local PlayerList = require("@shared/playerList")

	local function disconnectPlayer(player: Player)
		Output:appendToOthers(
			player,
			Output.MessageType.Success,
			`{Functions.formatPlayerName(player)} has left the server.`
		)
	end

	local function connectPlayer(player: Player)
		Log.debug(`Checking if {player.Name} can join...`)

		if _G.accountAgeLimit > 0 and player.AccountAge < _G.accountAgeLimit then
			Log.debug(`{player.Name}'s account age is too young (<{_G.accountAgeLimit} days).`)
			player:Kick(`Your account needs to be atleast {_G.accountAgeLimit} days old to play this game.`)

			Output:appendToOthers(
				player,
				Output.MessageType.Success,
				`{Functions.formatPlayerName(player)} was kicked due to their account not being older than one week.`
			)

			return
		end

		Output:appendToOthers(
			player,
			Output.MessageType.Success,
			`{Functions.formatPlayerName(player)} has joined the server.`
		)

		do
			Log.debug(`Loading SB client on {player.Name}...`)

			local playerGui = player:FindFirstChildOfClass("PlayerGui")
			if not playerGui then
				Log.debug(`{player.Name} had no player gui.`)
				player:Kick("PlayerGui was not found while loading.")

				return
			end

			local screenGui = clientScripts:Clone()
			screenGui.Name = Functions.randomInstanceName()
			screenGui.Archivable = false

			local sbClientActor = screenGui:WaitForChild("sbActor")
			sbClientActor.Name = Functions.randomInstanceName()
			sbClientActor.Archivable = false

			local sbClient = sbClientActor:WaitForChild("sb")
			sbClient.Name = Functions.randomInstanceName()
			sbClient.Archivable = false

			local clientWorkerManager = screenGui:WaitForChild("localWorkerManager")
			local clientAssets = screenGui:WaitForChild("assets")

			-- Tranfer the remote attribute to find the remote
			sbClient:SetAttribute(Network.attributeName, Network.attributeValue)

			screenGui.Parent = playerGui
			task.delay(10, function()
				screenGui:Destroy()
				sbClientActor:Destroy()
				sbClient:Destroy()
				clientWorkerManager:Destroy()
				clientAssets:Destroy()
			end)
		end
	end

	PlayerList.Added:Connect(connectPlayer)
	PlayerList.Removed:Connect(disconnectPlayer)

	for _, player: Player in ipairs(PlayerList:GetList()) do
		task.spawn(connectPlayer, player)
	end
end

task.spawn(function()
	Log.debug("Protecting TextChatService...")

	local TextChatService = game:GetService("TextChatService")

	local function protectWithExpectedStructure(instance: Instance, structure: { [string]: any })
		for name, subStructure in structure do
			local child = instance:WaitForChild(name)
			Protection.add(child, "write")

			protectWithExpectedStructure(child, subStructure)
		end
	end

	Protection.add(TextChatService, "write")
	protectWithExpectedStructure(TextChatService, {
		ChatWindowConfiguration = {},
		TextChannels = {
			RBXGeneral = {},
			RBXSystem = {},
		},
		TextChatCommands = {
			RBXHelpCommand = {},
			RBXUnmuteCommand = {},
			RBXTeamCommand = {},
			RBXClearCommand = {},
			RBXEmoteCommand = {},
			RBXWhisperCommand = {},
			RBXMuteCommand = {},
			RBXVersionCommand = {},
			RBXConsoleCommand = {},
		},
		ChatInputBarConfiguration = {},
		ChannelTabsConfiguration = {},
		BubbleChatConfiguration = {},
	})

	Log.debug("TextChatService should be fully protected")
end)

-- Finalize
Log.debug("Finalizing...")

World:Init()
Players.CharacterAutoLoads = true
for _, player: Player in ipairs(Players:GetPlayers()) do
	task.defer(player.LoadCharacter, player)
end

WorkerManagers:ready()
Log.print(`Loaded in {math.round((os.clock() - __start) * 1000)}ms.`)

return nil


--!native

--[[
	FastCanvas is a simple, but very fast and efficent
	drawing canvas with per pixel methods via EditableImage.

	This module was designed to be intergrated with CanvasDraw.
	A real-time roblox pixel graphics engine.

	This can be used on normal GUI Frames AND Decals, Textures, MeshParts, etc

	Written by @Ethanthegrand14

	Created: 9/11/2023
	Last Updated: 22/09/2025
]]

local AssetService = game:GetService("AssetService")

local FastCanvas = {}

type ParentType = GuiObject | MeshPart | Decal | Texture | SurfaceAppearance

local bit32bor = bit32.bor
local bit32lshift = bit32.lshift

function FastCanvas.new(Width: number, Height: number, CanvasParent: ParentType, Blur: boolean?)
	local IsUiParent = CanvasParent:IsA("GuiObject")

	local Canvas = {} -- The canvas object
	local Grid = buffer.create(Width * Height * 4) -- Local pixel grid containing RGBA values
	local CurrentClearRGBA = buffer.create(4)
	local ClearingGrid = buffer.create(Width * Height * 4) -- For Canvas:Clear()

	local Origin = Vector2.zero
	local Resolution = Vector2.new(Width, Height)

	-- Local functions
	local function GetGridIndex(X, Y)
		return (X + (Y - 1) * Width) * 4 - 4
	end

	-- Initialise buffers
	buffer.fill(Grid, 0, 255)
	buffer.fill(ClearingGrid, 0, 255)

	buffer.writeu32(CurrentClearRGBA, 0, 4294967295) -- White
	-- Create gui objects

	local EditableImage = AssetService:CreateEditableImage({Size = Resolution})

	if not EditableImage then
		warn("Failed to create Canvas due to EditableImage memory limit being hit!")
		return nil :: typeof(Canvas)
	end

	local CanvasFrame
	local AspectRatio

	if IsUiParent then
		CanvasFrame = Instance.new("ImageLabel")
		CanvasFrame.Name = "FastCanvas"
		CanvasFrame.BackgroundTransparency = 1
		CanvasFrame.ClipsDescendants = true
		CanvasFrame.Size = UDim2.fromScale(1, 1)
		CanvasFrame.Position = UDim2.fromScale(0.5, 0.5)
		CanvasFrame.AnchorPoint = Vector2.new(0.5, 0.5)

		if not Blur then
			CanvasFrame.ResampleMode = Enum.ResamplerMode.Pixelated
		end

		AspectRatio = Instance.new("UIAspectRatioConstraint")
		AspectRatio.AspectRatio = Width / Height
		AspectRatio.Parent = CanvasFrame

		CanvasFrame.ImageContent = Content.fromObject(EditableImage)
		CanvasFrame.Parent = CanvasParent
	elseif CanvasParent:IsA("Decal") or CanvasParent:IsA("Texture") or CanvasParent:IsA("MeshPart") then
		CanvasParent.TextureContent = Content.fromObject(EditableImage)
	else
		error(CanvasParent.ClassName .. " is currently not supported as a canvas parent!")
	end

	-- Properties
	Canvas.Buffer = Grid

	-- Properties [READ ONLY]
	Canvas.ImageLabel = CanvasFrame
	Canvas.Image = EditableImage
	Canvas.Width, Canvas.Height = Width, Height

	-- Pixel methods

	--[[
		Sets the pixels RGB value from a Color3 value.

		For more efficient and faster alternatives, try;
		- Canvas:SetU32()
		- Canvas:SetRGBA()
	]]
	function Canvas:SetColor3(X: number, Y: number, Colour: Color3)
		local Index = GetGridIndex(X, Y)
		buffer.writeu8(Grid, Index, Colour.R * 255)
		buffer.writeu8(Grid, Index + 1, Colour.G * 255)
		buffer.writeu8(Grid, Index + 2, Colour.B * 255)
	end

	-- Sets a pixel's RGB value on the canvas
	function Canvas:SetRGB(X: number, Y: number, R: number, G: number, B: number)
		local Index = GetGridIndex(X, Y)
		buffer.writeu8(Grid, Index, R * 255)
		buffer.writeu8(Grid, Index + 1, G * 255)
		buffer.writeu8(Grid, Index + 2, B * 255)
	end

	-- Sets a pixel's RGBA value on the canvas
	function Canvas:SetRGBA(X: number, Y: number, R: number, G: number, B: number, A: number)
		buffer.writeu32(Grid, GetGridIndex(X, Y), bit32bor(
			bit32lshift(A * 255, 24),
			bit32lshift(B * 255, 16),
			bit32lshift(G * 255, 8),
			R * 255)
		)
	end

	-- Sets the alpha value of a pixel on the canvas
	function Canvas:SetAlpha(X: number, Y: number, Alpha: number)
		buffer.writeu8(Grid, GetGridIndex(X, Y) + 3, Alpha * 255)
	end

	-- Sets the pixel value on the canvas by using a U32 colour value
	function Canvas:SetU32(X: number, Y: number, Value: number)
		buffer.writeu32(Grid, GetGridIndex(X, Y), Value)
	end

	-- Pixel fetch methods

	-- Returns the pixel value on the canvas as a tuple in order; R, G, B
	function Canvas:GetRGB(X: number, Y: number): (number, number, number)
		local Index = GetGridIndex(X, Y)

		return buffer.readu8(Grid, Index) / 255, buffer.readu8(Grid, Index + 1) / 255, buffer.readu8(Grid, Index + 2) / 255
	end

	-- Returns the pixel value on the canvas as a tuple in order; R, G, B, A
	function Canvas:GetRGBA(X: number, Y: number): (number, number, number, number)
		local Index = GetGridIndex(X, Y)

		return buffer.readu8(Grid, Index) / 255, buffer.readu8(Grid, Index + 1) / 255, buffer.readu8(Grid, Index + 2) / 255, buffer.readu8(Grid, Index + 3) / 255
	end

	-- Returns the pixel value on the canvas as a U32 number
	function Canvas:GetU32(X: number, Y: number): number
		return buffer.readu32(Grid, GetGridIndex(X, Y))
	end

	--[[
		Returns the pixel value as a Color3 object

		For faster alternatives, try;
		- Canvas:GetU32()
		- Canvas:GetRGBA()
	]]
	function Canvas:GetColor3(X: number, Y: number): Color3
		local Index = GetGridIndex(X, Y)

		return Color3.new(buffer.readu8(Grid, Index) / 255, buffer.readu8(Grid, Index + 1) / 255, buffer.readu8(Grid, Index + 2) / 255)
	end

	-- Returns the pixel alpha value on the canvas
	function Canvas:GetAlpha(X: number, Y: number): number
		local Index = GetGridIndex(X, Y)

		return buffer.readu8(Grid, Index + 3) / 255
	end

	-- Canvas methods

	--[[
		Takes a buffer of RGBA unsigned 8 bit int values (range from 0 to 255) to render all pixels on the canvas.

		You can also optionally use the X, Y and BufferWidth, BufferHeight parameters to also place a sized buffer region.

		The size of the buffer is assumed to be Width × Height × 4
	]]
	function Canvas:SetBuffer(Buffer: buffer, X: number?, Y: number?, BufferWidth: number?, BufferHeight: number?)
		-- Set a rectangular region from PointA to PointB
		if X and Y and BufferWidth and BufferHeight then
			if buffer.len(Buffer) ~= BufferWidth * BufferHeight * 4 then
				warn("'Canvas:SetBuffer()' failed as the given buffer's size is not equal to the given 'Size' parameter")
				return
			end

			local ScaledWidth = BufferWidth

			local OffsetY = 0
			local OffsetX = 0

			-- Clip start X
			if X < 1 then
				OffsetX = 1 + -X
				ScaledWidth -= 1 + -X
			end

			-- Clip width X
			if X + BufferWidth - 1 > Width then
				ScaledWidth -= (X + BufferWidth - 2) - Width + 1
			end

			-- Clip start Y
			if Y < 1 then
				OffsetY = 1 + -Y
			end

			local SampleStartX = 1 + OffsetX

			if SampleStartX > BufferWidth then return end

			if X > Width then return end

			for ImgY = 1 + OffsetY, BufferHeight do
				local PlacementY = Y + ImgY - 1

				if PlacementY > Height then break end

				local ImageBufferStartIndex = (SampleStartX + (ImgY - 1) * BufferWidth) * 4 - 4
				local DrawStartIndex = (X + OffsetX + (PlacementY - 1) * Width) * 4 - 4

				-- Copy a row of the image straight onto the main buffer
				buffer.copy(
					Grid,
					DrawStartIndex,
					Buffer,
					ImageBufferStartIndex,
					ScaledWidth * 4
				)
			end
		else
			-- Direct replacement
			buffer.copy(Grid, 0, Buffer)
		end
	end

	function Canvas:SetBufferLine(ColourU32, X1, X2, Y)
		if X1 > X2 then
			X1, X2 = X2, X1
		end

		-- Clipping
		X1 = math.floor(math.clamp(X1, 1, Width))
		X2 = math.floor(math.clamp(X2, 1, Width))
		Y = math.floor(math.clamp(Y, 1, Height))

		local StartIndex = (X1 + (Y - 1) * Width) * 4 - 4
		local EndIndex = (X2 + (Y - 1) * Width) * 4

		local Length = EndIndex - StartIndex
		local SizeStep = 4
		local Iterations = math.ceil(math.log(Length / 4, 2))

		Canvas:SetU32(X1, Y, ColourU32) -- Initial pixel to copy

		-- Copy until we fill the scanline
		for _ = 1, Iterations do
			local Count = math.min(SizeStep, Length - SizeStep)

			buffer.copy(Grid, StartIndex + SizeStep, Grid, StartIndex, Count)
			SizeStep += Count
		end
	end

	--[[
		Returns a buffer of RGBA values ranging from 0 to 255

		The size of this buffer must equal to Width × Height × 4
	]]
	function Canvas:GetBuffer(X: number?, Y: number?, BufferWidth: number?, BufferHeight: number?): buffer
		-- Gets a rectangular region from PointA to PointB
		if X and Y and BufferWidth and BufferHeight then
			if X < 1 or Y < 1 or X + BufferWidth - 1 > Width or Y + BufferHeight - 1 > Height then
				warn("'Canvas:GetBuffer()' failed as the given buffer's position and size are outside the Canvas bounds")
				return
			end

			local ReturnBuffer = buffer.create(BufferWidth * BufferHeight * 4)

			local ScaledWidth = BufferWidth

			local OffsetY = 0
			local OffsetX = 0

			-- Clip start X
			if X < 1 then
				OffsetX = 1 + -X
				ScaledWidth -= 1 + -X
			end

			-- Clip width X
			if X + BufferWidth - 1 > Width then
				ScaledWidth -= (X + BufferWidth - 2) - Width + 1
			end

			-- Clip start Y
			if Y < 1 then
				OffsetY = 1 + -Y
			end

			local SampleStartX = 1 + OffsetX

			if SampleStartX > BufferWidth then return end

			for ImgY = 1 + OffsetY, BufferHeight do
				local PlacementY = Y + ImgY - 1

				if PlacementY > Height then break end

				local ImageBufferStartIndex = (SampleStartX + (ImgY - 1) * BufferWidth) * 4 - 4
				local DrawStartIndex = (X + OffsetX + (PlacementY - 1) * Width) * 4 - 4

				-- Copy a row of the image straight onto the main buffer
				buffer.copy(
					ReturnBuffer,
					ImageBufferStartIndex,
					Grid,
					DrawStartIndex,
					ScaledWidth * 4
				)
			end

			return ReturnBuffer
		else
			local ReturnBuffer = buffer.create(Width * Height * 4)
			buffer.copy(ReturnBuffer, 0, Grid)

			return ReturnBuffer
		end

	end

	function Canvas:SetClearRGBA(R, G, B, A)
		local ClearColU32 = bit32bor(
			bit32lshift(A * 255, 24),
			bit32lshift(B * 255, 16),
			bit32lshift(G * 255, 8),
			R * 255
		)

		buffer.writeu32(CurrentClearRGBA, 0, ClearColU32)

		-- Create line to copy from

		local Length = Width * 4
		local SizeStep = 4
		local Iterations = math.ceil(math.log(Length / 4, 2))

		buffer.writeu32(ClearingGrid, 0, ClearColU32) -- Initial pixel to copy

		-- Copy until we fill the scanline
		for _ = 1, Iterations do
			local Count = math.min(SizeStep, Length - SizeStep)

			buffer.copy(ClearingGrid, SizeStep, ClearingGrid, 0, Count)
			SizeStep += Count
		end

		for Y = 1, Height - 1 do
			buffer.copy(
				ClearingGrid,
				Y * Width * 4,
				ClearingGrid,
				0,
				Length
			)
		end
	end

	--[[
		Clears the canvas and replaces all pixels with the canvas clearing colour.
		This method is much faster than Canvas:Fill()

		Use Canvas:SetClearRGBA() to change the colour used
	]]
	function Canvas:Clear()
		buffer.copy(Grid, 0, ClearingGrid, 0)
	end

	function Canvas:Render()
		EditableImage:WritePixelsBuffer(Origin, Resolution, Grid)
	end

	function Canvas:Resize(NewWidth, NewHeight)
		local NewEditableImage = AssetService:CreateEditableImage({Size = Vector2.new(NewWidth, NewHeight)})

		if not EditableImage then
			warn("Failed to resize Canvas due to EditableImage memory limit being hit!")
			return
		end

		Width, Height = NewWidth, NewHeight
		Resolution = Vector2.new(NewWidth, NewHeight)

		EditableImage:Destroy()
		EditableImage = NewEditableImage

		CanvasFrame.ImageContent = Content.fromObject(EditableImage)

		self.Width, self.Height = NewWidth, NewHeight

		if AspectRatio then
			AspectRatio.AspectRatio = NewWidth / NewHeight
		end

		-- Initialise buffers

		Grid = buffer.create(NewWidth * NewHeight * 4)
		ClearingGrid = buffer.create(NewWidth * NewHeight * 4)

		buffer.fill(Grid, 0, 255)

		self:SetClearRGBA(
			buffer.readu8(CurrentClearRGBA, 0) / 255,
			buffer.readu8(CurrentClearRGBA, 1) / 255,
			buffer.readu8(CurrentClearRGBA, 2) / 255,
			buffer.readu8(CurrentClearRGBA, 3) / 255
		)

		self:Clear()

		self.Buffer = Grid
	end

	function Canvas:Destroy()
		if CanvasFrame then
			CanvasFrame:Destroy()
		end
		if EditableImage then
			EditableImage:Destroy()
		end
		Grid = nil
		Canvas = nil
	end

	return Canvas
end

return FastCanvas


--!native

local Module = {}

local Lerp = math.lerp
local bit32bor = bit32.bor
local bit32lshift = bit32.lshift

function Module.new(ImageDataResX, ImageDataResY, Buffer)
	local ImageData = {
		ImageBuffer = Buffer,
		ImageResolution = Vector2.new(ImageDataResX, ImageDataResY),
		Width = ImageDataResX,
		Height = ImageDataResY,
	}

	local function GetIndex(X, Y)
		return (X + (Y - 1) * ImageDataResX) * 4 - 4
	end

	--== ImageData methods ==--

	--[[
		Fetches a pixel from a Vector2 point on the image.
		For more efficient and faster alternatives, try;
		- ImageData:GetU32()
		- ImageData:GetRGBA()
	]]
	function ImageData:GetPixel(Point: Vector2): (Color3, number)
		local PixelBuffer = self.ImageBuffer
		local X, Y = math.floor(Point.X), math.floor(Point.Y)
		local Index = GetIndex(X, Y)

		return Color3.new(buffer.readu8(PixelBuffer, Index) / 255, buffer.readu8(PixelBuffer, Index + 1) / 255, buffer.readu8(PixelBuffer, Index + 2) / 255), buffer.readu8(PixelBuffer, Index + 3) / 255
	end

	--[[
		Fetches a pixel from a point on the image.
		For more efficient and faster alternatives, try;
		- ImageData:GetU32()
		- ImageData:GetRGBA()
	]]
	function ImageData:GetPixelXY(X: number, Y: number): (Color3, number)
		local PixelBuffer = self.ImageBuffer
		local Index = GetIndex(X, Y)

		return Color3.new(buffer.readu8(PixelBuffer, Index) / 255, buffer.readu8(PixelBuffer, Index + 1) / 255, buffer.readu8(PixelBuffer, Index + 2) / 255), buffer.readu8(PixelBuffer, Index + 3) / 255
	end

	-- Returns a tuple in order of the pixel's RGB values
	function ImageData:GetRGB(X: number, Y: number): (number, number, number)
		local PixelBuffer = self.ImageBuffer
		local Index = GetIndex(X, Y)

		return buffer.readu8(PixelBuffer, Index) / 255, buffer.readu8(PixelBuffer, Index + 1) / 255, buffer.readu8(PixelBuffer, Index + 2) / 255
	end

	-- Returns a tuple in order of the pixel's RGBA values
	function ImageData:GetRGBA(X: number, Y: number): (number, number, number, number)
		local PixelBuffer = self.ImageBuffer
		local Index = GetIndex(X, Y)

		return buffer.readu8(PixelBuffer, Index) / 255, buffer.readu8(PixelBuffer, Index + 1) / 255, buffer.readu8(PixelBuffer, Index + 2) / 255, buffer.readu8(PixelBuffer, Index + 3) / 255
	end

	-- Returns a tuple in order of the pixel's RGB values
	function ImageData:GetU32(X: number, Y: number): number
		local PixelBuffer = self.ImageBuffer
		return buffer.readu32(PixelBuffer, GetIndex(X, Y))
	end

	-- Returns a tuple in order of the pixel's alpha value
	function ImageData:GetAlpha(X: number, Y: number): number
		return buffer.readu8(self.ImageBuffer, GetIndex(X, Y) + 3) / 255
	end

	--[[
		Returns a buffer of RGBA values ranging from 0 to 255

		The size of this buffer is equal to Width × Height × 4
	]]
	function ImageData:GetBuffer(X: number?, Y: number?, BufferWidth: number?, BufferHeight: number?): buffer
		local ImageBuffer = ImageData.ImageBuffer

		if X and Y and BufferWidth and BufferHeight then
			local Width, Height = ImageData.Width, ImageData.Height

			if X < 1 or Y < 1 or X + BufferWidth - 1 > Width or Y + BufferHeight - 1 > Height then
				warn("'ImageData:GetBuffer()' failed as the given buffer's position and size are outside the ImageData bounds")
				return
			end

			-- Gets a rectangular region from PointA to PointB
			local ReturnBuffer = buffer.create(BufferWidth * BufferHeight * 4)

			local ScaledWidth = BufferWidth

			local OffsetY = 0
			local OffsetX = 0

			-- Clip start X
			if X < 1 then
				OffsetX = 1 + -X
				ScaledWidth -= 1 + -X
			end

			-- Clip width X
			if X + BufferWidth - 1 > Width then
				ScaledWidth -= (X + BufferWidth - 2) - Width + 1
			end

			-- Clip start Y
			if Y < 1 then
				OffsetY = 1 + -Y
			end

			local SampleStartX = 1 + OffsetX

			if SampleStartX > BufferWidth then return end

			for ImgY = 1 + OffsetY, BufferHeight do
				local PlacementY = Y + ImgY - 1

				if PlacementY > Height then break end

				local ImageBufferStartIndex = (SampleStartX + (ImgY - 1) * BufferWidth) * 4 - 4
				local DrawStartIndex = (X + OffsetX + (PlacementY - 1) * Width) * 4 - 4

				-- Copy a row of the image straight onto the main buffer
				buffer.copy(
					ReturnBuffer,
					ImageBufferStartIndex,
					ImageBuffer,
					DrawStartIndex,
					ScaledWidth * 4
				)
			end

			return ReturnBuffer
		else
			local ReturnBuffer = buffer.create(BufferWidth * BufferHeight * 4)
			buffer.copy(ReturnBuffer, 0, ImageBuffer)

			return ReturnBuffer
		end

	end

	--[[
		Takes a buffer of RGBA unsigned 8 bit int values (range from 0 to 255) to render all pixel on the canvas.

		The size of the buffer is assumed to be Width × Height × 4
	]]
	function ImageData:SetBuffer(Buffer: buffer, X: number?, Y: number?, BufferWidth: number?, BufferHeight: number?)
		-- Set a rectangular region from PointA to PointB
		local ImageBuffer = self.ImageBuffer

		if X and Y and BufferWidth and BufferHeight then
			if buffer.len(Buffer) ~= BufferWidth * BufferHeight * 4 then
				warn("'Canvas:SetBuffer()' failed as the given buffer's size is not equal to the given 'Size' parameter")
				return
			end

			local Width, Height = self.Width, self.Height

			local ScaledWidth = BufferWidth

			local OffsetY = 0
			local OffsetX = 0

			-- Clip start X
			if X < 1 then
				OffsetX = 1 + -X
				ScaledWidth -= 1 + -X
			end

			-- Clip width X
			if X + BufferWidth - 1 > Width then
				ScaledWidth -= (X + BufferWidth - 2) - Width + 1
			end

			-- Clip start Y
			if Y < 1 then
				OffsetY = 1 + -Y
			end

			local SampleStartX = 1 + OffsetX

			if SampleStartX > BufferWidth then return end

			if X > Width then return end

			for ImgY = 1 + OffsetY, BufferHeight do
				local PlacementY = Y + ImgY - 1

				if PlacementY > Height then break end

				local ImageBufferStartIndex = (SampleStartX + (ImgY - 1) * BufferWidth) * 4 - 4
				local DrawStartIndex = (X + OffsetX + (PlacementY - 1) * Width) * 4 - 4

				-- Copy a row of the image straight onto the main buffer
				buffer.copy(
					ImageBuffer,
					DrawStartIndex,
					Buffer,
					ImageBufferStartIndex,
					ScaledWidth * 4
				)
			end
		else
			-- Direct replacement
			buffer.copy(ImageBuffer, 0, Buffer)
		end
	end

	-- Tints the image with a colour by a percentage
	function ImageData:Tint(Colour: Color3, T: number)
		local PixelBuffer = self.ImageBuffer
		local R, G, B = Colour.R * 255, Colour.B * 255, Colour.G * 255

		for i = 1, ImageDataResX * ImageDataResY * 4, 4 do
			i -= 1
			buffer.writeu8(PixelBuffer, i, Lerp(buffer.readu8(PixelBuffer, i), R, T))
			buffer.writeu8(PixelBuffer, i + 1, Lerp(buffer.readu8(PixelBuffer, i + 1), G, T))
			buffer.writeu8(PixelBuffer, i + 2, Lerp(buffer.readu8(PixelBuffer, i + 2), B, T))
		end
	end

	-- Tints the image with a colour by a percentage
	function ImageData:TintRGB(R: number, G: number, B: number, T: number)
		local PixelBuffer = self.ImageBuffer
		R, G, B = R * 255, B * 255, G * 255

		for i = 1, ImageDataResX * ImageDataResY * 4, 4 do
			i -= 1
			buffer.writeu8(PixelBuffer, i, Lerp(buffer.readu8(PixelBuffer, i), R, T))
			buffer.writeu8(PixelBuffer, i + 1, Lerp(buffer.readu8(PixelBuffer, i + 1), G, T))
			buffer.writeu8(PixelBuffer, i + 2, Lerp(buffer.readu8(PixelBuffer, i + 2), B, T))
		end
	end

	-- Flips the pixel contents of the ImageData either on it's horizontal or vertical axis
	function ImageData:Flip(Vertical: boolean?)
		local TempData = ImageData:Clone()
		local Width, Height = ImageData.Width, ImageData.Height

		if Vertical then
			for Y = 1, Height do
				ImageData:SetBuffer(TempData:GetBuffer(1, Height - Y + 1, Width, 1), 1, Y, Width, 1)
			end
		else
			for X = 1, Width do
				ImageData:SetBuffer(TempData:GetBuffer(Width - X + 1, 1, 1, Height), X, 1, 1, Height)
			end
		end
	end

	--[[
		Sets a pixel on the canvas
		For more efficient and faster alternatives, try;
		- ImageData:SetU32()
		- ImageData:SetRGBA()
	]]
	function ImageData:SetPixel(X: number, Y: number, Colour: Color3, Alpha: number?)
		local PixelBuffer = self.ImageBuffer
		local Index = GetIndex(X, Y)
		buffer.writeu8(PixelBuffer, Index, Colour.R * 255)
		buffer.writeu8(PixelBuffer, Index + 1, Colour.G * 255)
		buffer.writeu8(PixelBuffer, Index + 2, Colour.B * 255)
		buffer.writeu8(PixelBuffer, Index + 3, (Alpha or 1) * 255)
	end

	-- Sets an RGB value to the pixel on the image
	function ImageData:SetRGB(X: number, Y: number, R: number, G: number, B: number)
		local PixelBuffer = self.ImageBuffer
		local Index = GetIndex(X, Y)
		buffer.writeu8(PixelBuffer, Index, R * 255)
		buffer.writeu8(PixelBuffer, Index + 1, G * 255)
		buffer.writeu8(PixelBuffer, Index + 2, B * 255)
	end

	-- Sets an RGBA value to the pixel on the image
	function ImageData:SetRGBA(X: number, Y: number, R: number, G: number, B: number, A: number)
		local PixelBuffer = self.ImageBuffer
		local Index = GetIndex(X, Y)
		buffer.writeu32(PixelBuffer, GetIndex(X, Y), bit32bor(
			bit32lshift(A * 255, 24),
			bit32lshift(B * 255, 16),
			bit32lshift(G * 255, 8),
			R * 255)
		)
	end

	function ImageData:SetU32(X: number, Y: number, Value: number)
		local PixelBuffer = self.ImageBuffer
		buffer.writeu32(PixelBuffer, GetIndex(X, Y), Value)
	end

	-- Sets an alpha value to the pixel on the image
	function ImageData:SetAlpha(X: number, Y: number, Alpha: number)
		buffer.writeu8(self.ImageBuffer, GetIndex(X, Y) + 3, Alpha * 255)
	end

	-- Returns a deep copy of the ImageData object
	function ImageData:Clone(): typeof(ImageData)
		local Width, Height = self.Width, self.Height

		local ClonedBuffer = buffer.create(Width * Height * 4)
		buffer.copy(ClonedBuffer, 0, self.ImageBuffer)

		return Module.new(Width, Height, ClonedBuffer)
	end

	return ImageData
end

return Module


--!native

local SaveObjReader = {}

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

local StringCompressor = require(script.Parent:WaitForChild("StringCompressor"))

function SaveObjReader.ReadV3(SaveObject, SlowLoad) -- v4.9.0 and above
	local Resolution = SaveObject:GetAttribute("Resolution")
	local Width, Height = Resolution.X, Resolution.Y

	local PixelBuffer = buffer.create(Width * Height * 4)
	local CurrentBufferIndex = 0

	-- Decompress the data
	local ChunkCount = #SaveObject:GetChildren()

	for i = 1, ChunkCount do
		local ChunkString = SaveObject["Chunk" .. i]
		local Array = HttpService:JSONDecode(StringCompressor.Decompress(ChunkString.Value))

		for j = 1, #Array do
			buffer.writeu8(PixelBuffer, CurrentBufferIndex, Array[j])
			CurrentBufferIndex += 1
		end

		if SlowLoad then task.wait() end
	end

	return PixelBuffer, Width, Height
end

-- Below functions are for backwards compatibility

function SaveObjReader.ReadV2(SaveObject) -- v4.4.0 to v4.8.0
	local CompressedRGB = SaveObject:GetAttribute("Colour")
	local CompressedAlpha = SaveObject:GetAttribute("Alpha")
	local Resolution = SaveObject:GetAttribute("Resolution")
	local Width, Height = Resolution.X, Resolution.Y

	-- Decompress the data
	local RGBArray = HttpService:JSONDecode(StringCompressor.Decompress(CompressedRGB))
	local AlphaArray = HttpService:JSONDecode(StringCompressor.Decompress(CompressedAlpha))

	local PixelBuffer = buffer.create(Width * Height * 4)

	for i = 1, Width * Height do
		local RGBIndex = i * 3 - 2
		local BufferIndex = i * 4 - 4

		buffer.writeu8(PixelBuffer, BufferIndex, RGBArray[RGBIndex])
		buffer.writeu8(PixelBuffer, BufferIndex + 1, RGBArray[RGBIndex + 1])
		buffer.writeu8(PixelBuffer, BufferIndex + 2, RGBArray[RGBIndex + 2])
		buffer.writeu8(PixelBuffer, BufferIndex + 3, AlphaArray[i])
	end

	return PixelBuffer, Width, Height
end

function SaveObjReader.ReadV1(SaveObject) -- v2.0.0 to v4.3.2
	local SaveDataImageColours = SaveObject:GetAttribute("ImageColours")
	local SaveDataImageAlphas = SaveObject:GetAttribute("ImageAlphas")
	local SaveDataImageResolution = SaveObject:GetAttribute("ImageResolution")

	-- Decompress the data
	local DecompressedSaveDataImageColours = StringCompressor.Decompress(SaveDataImageColours)
	local DecompressedSaveDataImageAlphas = StringCompressor.Decompress(SaveDataImageAlphas)

	-- Get a single pixel colour info form the data
	local PixelDataColoursString = string.split(DecompressedSaveDataImageColours, "S")
	local PixelDataAlphasString = string.split(DecompressedSaveDataImageAlphas, "S")

	local PixelBuffer = buffer.create(SaveDataImageResolution.X * SaveDataImageResolution.Y * 4)

	for i, PixelColourString in pairs(PixelDataColoursString) do
		local RGBValues = string.split(PixelColourString, ",")
		local R, G, B = table.unpack(RGBValues)
		local A = tonumber(PixelDataAlphasString[i])

		local Index = i * 4 - 4

		buffer.writeu8(PixelBuffer, Index, R)
		buffer.writeu8(PixelBuffer, Index + 1, G)
		buffer.writeu8(PixelBuffer, Index + 2, B)
		buffer.writeu8(PixelBuffer, Index + 3, A)
	end

	return PixelBuffer, SaveDataImageResolution.X, SaveDataImageResolution.Y
end

function SaveObjReader.ReadOSGL(TextureModule) -- OSGL formatted image
	local Data = require(TextureModule)
	return Data.pixels, Data.width, Data.height
end

return SaveObjReader


--!native

-- Module by 1waffle1, optimized by boatbomber
-- https://devforum.roblox.com/t/text-compression/163637

local gsub = string.gsub
local sub = string.sub
local insert = table.insert
local rep = string.rep

local dictionary = {}
do -- populate dictionary
	local length = 0
	for i = 32, 127 do
		if i ~= 34 and i ~= 92 then
			local c = string.char(i)
			dictionary[c], dictionary[length] = length, c
			length = length + 1
		end
	end
end

local escapemap = {}
do -- Populate escape map
	for i = 1, 34 do
		i = ({ 34, 92, 127 })[i - 31] or i
		local c, e = string.char(i), string.char(i + 31)
		escapemap[c], escapemap[e] = e, c
	end
end

local function escape(s)
	return gsub(s, '[%c"\\]', function(c)
		return "\127" .. escapemap[c]
	end)
end
local function unescape(s)
	return gsub(s, "\127(.)", function(c)
		return escapemap[c]
	end)
end

local function copy(t)
	local new = {}
	for k, v in pairs(t) do
		new[k] = v
	end
	return new
end

local b93Cache = {}
local function tobase93(n)
	local value = b93Cache[n]
	if value then
		return value
	end

	value = ""
	repeat
		local remainder = n % 93
		value = dictionary[remainder] .. value
		n = (n - remainder) / 93
	until n == 0

	b93Cache[n] = value
	return value
end

local b10Cache = {}
local function tobase10(value)
	local n = b10Cache[value]
	if n then
		return n
	end

	n = 0
	for i = 1, #value do
		n = n + 93 ^ (i - 1) * dictionary[sub(value, -i, -i)]
	end

	b10Cache[value] = n
	return n
end

local function compress(text)
	local dictionaryCopy = copy(dictionary)
	local key, sequence, size = "", {}, #dictionaryCopy
	local width, spans, span = 1, {}, 0
	local function listkey(k)
		local value = tobase93(dictionaryCopy[k])
		local valueLength = #value
		if valueLength > width then
			width, span, spans[width] = valueLength, 0, span
		end
		insert(sequence, rep(" ", width - valueLength) .. value)
		span += 1
	end
	text = escape(text)
	for i = 1, #text do
		local c = sub(text, i, i)
		local new = key .. c
		if dictionaryCopy[new] then
			key = new
		else
			listkey(key)
			key = c
			size += 1
			dictionaryCopy[new], dictionaryCopy[size] = size, new
		end
	end
	listkey(key)
	spans[width] = span
	return table.concat(spans, ",") .. "|" .. table.concat(sequence)
end

local function decompress(text)
	local dictionaryCopy = copy(dictionary)
	local sequence, spans, content = {}, string.match(text, "(.-)|(.*)")
	local groups, start = {}, 1
	for span in string.gmatch(spans, "%d+") do
		local width = #groups + 1
		groups[width] = sub(content, start, start + span * width - 1)
		start = start + span * width
	end
	local previous

	for width, group in ipairs(groups) do
		for value in string.gmatch(group, rep(".", width)) do
			local entry = dictionaryCopy[tobase10(value)]
			if previous then
				if entry then
					insert(dictionaryCopy, previous .. sub(entry, 1, 1))
				else
					entry = previous .. sub(previous, 1, 1)
					insert(dictionaryCopy, entry)
				end
				insert(sequence, entry)
			else
				sequence[1] = entry
			end
			previous = entry
		end
	end
	return unescape(table.concat(sequence))
end

return { Compress = compress, Decompress = decompress }


local Abs = math.abs

function getNormalFromPartFace(part, normalId)
	return part.CFrame:VectorToWorldSpace(Vector3.FromNormalId(normalId))
end

function normalVectorToFace(part, normalVector)
	local normalIDs = {
		Enum.NormalId.Front,
		Enum.NormalId.Back,
		Enum.NormalId.Bottom,
		Enum.NormalId.Top,
		Enum.NormalId.Left,
		Enum.NormalId.Right
	}

	for _, normalId in ipairs(normalIDs) do
		if getNormalFromPartFace(part, normalId):Dot(normalVector) > 0.999 then
			return normalId
		end
	end

	return nil -- None found within tolerance.
end

function getTopLeftCorners(part)
	local size = part.Size
	return {
		[Enum.NormalId.Front] = part.CFrame * CFrame.new(size.X/2, size.Y/2, -size.Z/2),
		[Enum.NormalId.Back] = part.CFrame * CFrame.new(-size.X/2, size.Y/2, size.Z/2),
		[Enum.NormalId.Right] = part.CFrame * CFrame.new(size.X/2, size.Y/2, size.Z/2) * CFrame.Angles(0, math.rad(90), 0),
		[Enum.NormalId.Left] = part.CFrame * CFrame.new(-size.X/2, size.Y/2, -size.Z/2) * CFrame.Angles(0, math.rad(90), 0),
		[Enum.NormalId.Bottom] = part.CFrame * CFrame.new(size.X/2, size.Y/2, size.Z/2) * CFrame.Angles(math.rad(90), 0, math.rad(90)),
		[Enum.NormalId.Top] = part.CFrame * CFrame.new(-size.X/2, size.Y/2, size.Z/2) * CFrame.Angles(math.rad(90), 0, math.rad(90))
	}
end

function getRotationComponents(offset)
	local components = {offset:GetComponents()}
	table.remove(components, 1)
	table.remove(components, 2)
	table.remove(components, 3)

	return components
end

return {
	["normalVectorToFace"] = normalVectorToFace,
	["getTopLeftCorners"] = getTopLeftCorners
}