local upsideEngine = script.Parent.Parent.Parent
local contentProvider = game:GetService("ContentProvider")
local colorProperties = { "ImageColor3", "TextColor3", "TextStrokeColor3" }

local sceneManager = require(script.Parent.Parent.Parent.Services.SceneManager)
local shader = require(script.Parent.Shader)
local baseObject = require(upsideEngine.Classes.Internal.BaseObject)
local util = require(upsideEngine.Lib.Util)

local staticObject = {}
staticObject.__index = staticObject

local function tryToReplicate(self, Property)
	local value = self.ChromaticAberration.R[Property]
	self.ChromaticAberration.G[Property] = value
	self.ChromaticAberration.B[Property] = value
end

function updateChromaticAberration(self, property)
	local index = 1
	local instance = self.Instance
	local distance = util.ToVector2(instance.Position, "Scale") - self.ChromaticAberrationPoint
	distance = Vector2.new(math.abs(distance.X), math.abs(distance.Y))

	if property ~= "Position" and table.find(colorProperties, property) == nil then
		pcall(tryToReplicate, self, property)
	end

	for colorName, c in self.ChromaticAberration do
		if colorName ~= "R" then
			local localDistance = distance * index * self.ChromaticAberrationDistance
			index += 1

			c.Position = self.ChromaticAberration.R.Position
				+ UDim2.fromScale(
					instance.Position.X.Scale > self.ChromaticAberrationPoint.X and -localDistance.X or localDistance.X,
					instance.Position.Y.Scale > self.ChromaticAberrationPoint.Y and -localDistance.Y or localDistance.Y
				)
		end
	end
end

local function setAberration(data)
	local transparencyProperty = data.property:split("Color3")[1] .. "Transparency"
	if data.color[transparencyProperty] > 1 then
		return
	end

	local Color = { R = 0, G = 0, B = 0 }
	Color[data.name] = 1

	data.color[data.property] = Color3.new(Color.R, Color.G, Color.B)
	data.color[transparencyProperty] = data.intensity
end

--[[={
	@link StaticObject.md
}=]]

function staticObject.new(): StaticObject
	local defaultShader = shader.new()
	defaultShader.Enabled = false

	local self = baseObject.new()
	self:SetClassName(script.Name)
	self.ChromaticAberrationPoint = Vector2.new(0.5, 0.5)
	self.ChromaticAberration = {}
	self.Shader = defaultShader

	self.ChromaticAberrationIntensity = 0
	self.ChromaticAberrationDistance = 0.1
	self.ChromaticAberrationConnection = false

	local instance = self.Instance
	instance.AnchorPoint = Vector2.new(0.5, 0.5)
	instance.Position = UDim2.fromOffset(100, 100)
	instance.Size = UDim2.fromOffset(200, 200)
	instance.BackgroundTransparency = 1
	instance.ResampleMode = Enum.ResamplerMode.Pixelated

	return setmetatable(self, staticObject)
end

--[[={
	@desc Sets the scene for this object
	@link void.link
}=]]

function staticObject:SetScene(scene: Scene)
	baseObject.SetScene(self, scene)
	local shader = self.Shader

	if shader.Enabled and shader.Source then
		self:SetShader(shader)
	end
end

--[[={
	@desc Sets the shader for this object
	@link void.link
}=]]

function staticObject:SetShader(shader: Shader)
	local scene = sceneManager.Scenes[self.Scene]
	if not scene then
		return
	elseif not shader then
		local shaderEnv = scene.ShaderEnvironment
		if shaderEnv:HasOne(self.Id) then
			shaderEnv:RemoveOne(self.Id)
		end

		self.Shader = {}
		return
	end

	local shaderEnv = scene.ShaderEnvironment
	shaderEnv:SetOne(self, self.Id)
	self.Shader = shader
end

--[[={
	@desc Loads an image for the current object and is set as the instance image
	@link void.link
}=]]

function staticObject:Load(url: string)
	contentProvider:PreloadAsync({ url })
	self.Instance.Image = url
end

--[[={
	@desc Sets chromatic aberration for the image using a center as reference, by default the center is the middle of the screen,
	to disable the chromatic aberration send 0 as first parameter
	@link void.link
}=]]

function staticObject:SetChromaticAberration(Intensity: number, Distance: number, Point: Vector2)
	assert(typeof(Intensity) == "number", "Invalid argument #1, it must be a number")
	assert(typeof(Point) == "Vector2" or not Point, "Invalid argument #2, it must be a Vector2")

	if Intensity == 0 then
		self.ChromaticAberration.ImageColor3 = Color3.new(1, 1, 1)
		self.ChromaticAberrationConnection:Disconnect()

		self.ChromaticAberration.R = nil
		self.ChromaticAberration.G:Destroy()
		self.ChromaticAberration.B:Destroy()
		return
	end

	self.ChromaticAberrationIntensity = Intensity
	self.ChromaticAberrationDistance = Distance or 0.1
	self.ChromaticAberrationPoint = Point or self.ChromaticAberrationPoint

	updateChromaticAberration(self, "Position")

	if not self.ChromaticAberrationConnection then
		self.ChromaticAberration = {
			R = self.Instance,
			G = self.Instance:Clone(),
			B = self.Instance:Clone(),
		}

		for colorName, color in self.ChromaticAberration do
			for _, property in colorProperties do
				pcall(setAberration, {
					name = colorName,
					color = color,
					property = property,
					intensity = self.ChromaticAberrationIntensity,
				})
			end
		end

		local chromaticAberration = self.ChromaticAberration
		local rParent = chromaticAberration.R.Parent
		chromaticAberration.G.Parent = rParent
		chromaticAberration.B.Parent = rParent
		self.ChromaticAberrationConnection = self.Instance.Changed:Connect(util.Wrap(updateChromaticAberration, self))
	end
end

--[[={
	@desc This class is used to create a basic object, without physics or animations
	@about
		@ChromaticAberrationPoint This is the point which is being used as reference for the chromatic aberration
		@ChromaticAberration The instances which makes the chromatic aberration effect
		@Shader Is the shader of the object
}=]]

return setmetatable(staticObject, baseObject)


local upsideEngine = script.Parent.Parent.Parent
local BaseClass = require(upsideEngine.Classes.Internal.BaseClass)
local TextTagService = require(upsideEngine.Services.TextTagService)
local TextTag = {}
TextTag.__index = TextTag

function TextTag.new()
	local self = BaseClass.new()
	self:SetClassName(script.Name)
	self.TagName = "default"
	self.DisplayMode = "letter"
	self.TypingSoundId = "rbxassetid://92143495194538"
	self.TypingSoundVolume = 1
	self.WordsPerSecond = 0
	self.IdleTime = 0

	return setmetatable(self, TextTag)
end

--[[={
	@desc Sets the tag name and updates its registration in TextTagService.
	@link void.md
}=]]
function TextTag:SetTagName(tagName)
	assert(type(tagName) == "string", "Tag name must be a string.")
	self.TagName = tagName
	TextTagService:RemoveTextTag(self)
	TextTagService:AddTextTag(self)
end

--[[={
	@desc Sets the display mode with validation.
	@link void.md
}=]]
function TextTag:SetDisplayMode(displayMode)
	assert(displayMode == "letter" or displayMode == "word", "Invalid display mode. Use 'letter' or 'word'.")
	self.displayMode = displayMode
end

--[[={
	@desc Assigns a custom render function.
	@link void.md
}=]]
function TextTag:SetRenderFunction(renderFunction)
	assert(type(renderFunction) == "function", "Render function must be a function.")
	self.renderFunction = renderFunction
end

--[[={
	@desc Removes the TextTag instance from the TextTagService.
	@link void.md
}=]]
function TextTag:Destroy()
	TextTagService:RemoveTextTag(self)
end

--[[={
	@desc Class representing a text tag for UpsideEngine.
	@about
		@TagName The tag name that defines a rendering style.
		@DisplayMode Display mode, either "letter" (character by character) or "word" (word by word).
		@TypingSoundId The sound asset ID played during typing.
		@TypingSoundVolume Volume of the typing sound.
		@WordsPerSecond Typing speed expressed in words per second.
		@IdleTime Idle time.
		@SetTagName Updates the tag name and re-registers with the TextTagService.
		@SetDisplayMode Sets the display mode with validation.
		@SetRenderFunction Assigns a custom render function.
}=]]

return setmetatable(TextTag, BaseClass)


local RunService = game:GetService("RunService")
local actor = script:GetActor()

if not RunService:IsRunning() or not actor then
	return
end

local data = {}
local running = {}
local tasks = {}

local module = nil
local runMode = "Default"
local ready = script:WaitForChild("ready")

local function addTask(f)
	return function(...)
		if runMode == "Default" then
			f(...)
			return
		end

		local args = { ... }
		table.insert(tasks, function()
			f(unpack(args))
		end)
	end
end

actor:BindToMessage("setRunMode", function(mode)
	runMode = mode
end)

actor:BindToMessage(
	"init",
	addTask(function(moduleScript)
		module = require(moduleScript)
	end)
)

actor:BindToMessage(
	"setVariable",
	addTask(function(name, value)
		data[name] = module.onSetData(data, name, value)
	end)
)

actor:BindToMessage(
	"updateVariable",
	addTask(function(name)
		data[name] = module.onSetData(data, name, data[name])
	end)
)

actor:BindToMessage(
	"execute",
	addTask(function(name)
		local func = module[name]
		func(data)
	end)
)

actor:BindToMessage(
	"executeEveryStep",
	addTask(function(name)
		if running[name] then
			return
		end

		local func = module[name]
		running[name] = true
		RunService.Heartbeat:ConnectParallel(function(deltaTime)
			func(data, deltaTime)
		end)
	end)
)

ready:Fire()

while task.wait() do
	for index, run in tasks do
		table.remove(tasks, index)
		run()
	end
end


return function(t1, t2)
	local result = t1
	for index, value in t2 do
		result[index] = value
	end

	return result
end


return function(f, ...)
	local args = { ... }
	return function(...)
		f(unpack(args), ...)
	end
end


--!native
--!optimize 2
local supportFn = require(script.Parent.Support)

local function getEdge(simplex)
	local minDistSq = math.huge
	local edgeNormal = Vector3.zero
	local index = -1
	local n = #simplex

	if n == 0 then
		return index, edgeNormal
	end

	for i = 1, n do
		local j = (i % n) + 1
		local edge = simplex[j] - simplex[i]
		local normal = Vector3.new(-edge.Y, edge.X)
		local magSq = normal.X * normal.X + normal.Y * normal.Y
		if magSq > 1e-12 then
			local dot = normal:Dot(simplex[i])
			local distSq = (dot * dot) / magSq
			if distSq < minDistSq then
				minDistSq = distSq
				edgeNormal = normal
				index = j
			end
		end
	end

	if index ~= -1 then
		local magSq = edgeNormal.X * edgeNormal.X + edgeNormal.Y * edgeNormal.Y
		if magSq > 1e-12 then
			local invMag = 1 / math.sqrt(magSq)
			edgeNormal = edgeNormal * invMag
		else
			edgeNormal = Vector3.new(1, 0, 0)
		end
	end

	return index, edgeNormal
end

return function(simplex, objectA, objectB)
	local eps = 1e-4
	for _ = 1, 64 do
		local index, edgeNormal = getEdge(simplex)
		if index == -1 then
			return Vector3.zero, 0
		end

		local supportPoint = supportFn(objectA, edgeNormal) - supportFn(objectB, -edgeNormal)
		local distance = edgeNormal:Dot(supportPoint)
		local edgePointDistance = edgeNormal:Dot(simplex[index])

		if distance - edgePointDistance < eps then
			return edgeNormal, distance
		end

		table.insert(simplex, index, supportPoint)
	end

	return Vector3.zero, 0
end


--!native
--!optimize 2
local handlers = {}

local function tripleProduct(a, b, c)
	return b * a:Dot(c) - a * b:Dot(c)
end

function handlers.point(direction, simplex)
	return -direction
end

@native
function handlers.line(direction, simplex)
	local b = simplex[2]
	local c = simplex[1]

	local cb = b - c
	local c0 = -c

	return tripleProduct(cb, c0, cb)
end

@native
function handlers.triangle(direction, simplex)
	local a = simplex[3]
	local b = simplex[2]
	local c = simplex[1]

	local a0 = -a
	local ab = b - a
	local ac = c - a

	local abPerp = tripleProduct(ac, ab, ab)
	local acPerp = tripleProduct(ab, ac, ac)

	if abPerp:Dot(a0) > 0 then
		table.remove(simplex, 1)
		return abPerp
	elseif acPerp:Dot(a0) > 0 then
		table.remove(simplex, 2)
		return acPerp
	end

	return direction, true
end

return handlers


--!native
--!optimize 2
return function(object, direction)
	if object.shape == "circle" then
		if direction.X == 0 and direction.Y == 0 then
			return object.centre
		end

		local unitDirection = direction.Unit
		if unitDirection.X == 0 and unitDirection.Y == 0 then
			return object.centre
		end

		return object.centre + object.radius * unitDirection
	end

	if not object.area or #object.area == 0 then
		return object.centre
	end

	local furthestDistance = -math.huge
	local furthestVertex = object.area[1]

	for i = 1, #object.area do
		local vertex = object.area[i]
		local distance = vertex:Dot(direction)
		if distance > furthestDistance then
			furthestDistance = distance
			furthestVertex = vertex
		end
	end

	return furthestVertex
end


--!native
--!optimize 2
local getCorrectionVector = require(script.GetCorrectionVector)
local support = require(script.Support)
local handlers = require(script.Handlers)

local handlersByLen = {
	handlers.point,
	handlers.line,
	handlers.triangle,
}

local EvolveResult = {
	NoIntersection = 1,
	Intersection = 2,
	Evolving = 3,
}

type EvolveResult = typeof(EvolveResult)
local function toVector2(v3)
	return Vector2.new(v3.X, v3.Y)
end

local function handleSimplex(data, objectA, objectB)
	local simplex = data.simplex
	local direction = data.direction
	local length = #simplex

	if length > 0 then
		local handler = handlersByLen[length]
		if handler then
			local newDir, collision = handler(direction, simplex)
			if newDir then
				direction = newDir
			end

			if collision then
				data.direction = direction
				return EvolveResult.Intersection
			end
		end
	end

	local newVertex = support(objectA, direction) - support(objectB, -direction)
	local isEvolving = direction:Dot(newVertex) > 0

	data.direction = direction
	table.insert(simplex, newVertex)

	if isEvolving then
		return EvolveResult.Evolving
	else
		return EvolveResult.NoIntersection
	end
end

return function(objectA, objectB)
	local state = EvolveResult.Evolving
	local direction = objectA.centre - objectB.centre

	if direction.X == 0 and direction.Y == 0 then
		direction = Vector3.new(1, 0, 0)
	end

	local data = {
		simplex = {},
		direction = direction,
	}

	local iterations = 0
	local maxIterations = 64

	while state == EvolveResult.Evolving and iterations < maxIterations do
		state = handleSimplex(data, objectA, objectB)
		iterations = iterations + 1
	end

	if state == EvolveResult.Intersection then
		local penetrationVector, penetrationDepth = getCorrectionVector(data.simplex, objectA, objectB)

		if typeof(penetrationDepth) ~= "number" then
			penetrationDepth = 0
		end

		if penetrationVector.X == 0 and penetrationVector.Y == 0 or penetrationDepth == 0 then
			return true, Vector2.zero
		end

		local unitDirection = penetrationVector.Unit
		if unitDirection.X == 0 and unitDirection.Y == 0 or unitDirection.X ~= unitDirection.X then
			unitDirection = Vector3.zero
		end

		local moveVector = unitDirection * penetrationDepth
		moveVector = toVector2(moveVector)

		return true, moveVector
	end

	return false
end


--!native
--!optimize 2
local checkCollision = require(script.Parent.CheckCollision)

local function isFar(objectA, objectB)
	local a = objectA.Instance
	local b = objectB.Instance
	local scaleA = objectA.HitboxScale
	local scaleB = objectB.HitboxScale

	local aw, ah = a.AbsoluteSize.X * scaleA, a.AbsoluteSize.Y * scaleA
	local bw, bh = b.AbsoluteSize.X * scaleB, b.AbsoluteSize.Y * scaleB

	local rA = math.sqrt(aw * aw + ah * ah) * 0.5
	local rB = math.sqrt(bw * bw + bh * bh) * 0.5

	local dx = (a.AbsolutePosition.X + a.AbsoluteSize.X * 0.5) - (b.AbsolutePosition.X + b.AbsoluteSize.X * 0.5)
	local dy = (a.AbsolutePosition.Y + a.AbsoluteSize.Y * 0.5) - (b.AbsolutePosition.Y + b.AbsoluteSize.Y * 0.5)
	local sumR = rA + rB

	return dx * dx + dy * dy > sumR * sumR
end

local function shouldSkip(object, other)
	return object.Id == other.Id
		or other.CollisionGroup ~= object.CollisionGroup
		or other.CollisionBlacklist[object.Id]
		or object.CollisionBlacklist[other.Id]
end

local function toVector3(v2)
	return Vector3.new(v2.X, v2.Y, 0)
end

local function getRotatedArea(instance, area, centre)
	if instance.Rotation == 0 then
		return area
	end

	local rotation = math.rad(instance.Rotation)
	local sin, cos = math.sin(rotation), math.cos(rotation)

	for i, vertex in area do
		local XDistance = vertex.X - centre.X
		local YDistance = vertex.Y - centre.Y

		-- stylua: ignore start
		vertex = Vector3.new(
			centre.X + XDistance * cos - YDistance * sin,
			centre.Y + XDistance * sin + YDistance * cos,
			0
		)
		-- stylua: ignore end
		area[i] = vertex
	end

	return area
end

local function getObjectData(object, velocity)
	local area = {}
	local instance = object.Instance

	local position = toVector3(instance.AbsolutePosition + velocity)
	local size = toVector3(instance.AbsoluteSize)

	local centre = position + size * 0.5
	local radius = if object.Shape == "circle" then (size.X / 2) * object.HitboxScale else nil

	local hitboxSize = size * object.HitboxScale
	local alignment = size * (1 - math.clamp(object.HitboxScale, 0, 1)) * 0.5
	local offset = position + alignment

	for i, vertex in object.Hitbox do
		area[i] = offset + hitboxSize * vertex
	end

	return {
		radius = radius,
		shape = object.Shape,
		centre = centre,
		area = getRotatedArea(instance, area, centre),
	}
end

return function(objects, object, velocity)
	local collisions = {}
	local objectData = getObjectData(object, velocity)

	for _, other in objects do
		if shouldSkip(object, other) or isFar(object, other) then
			continue
		end

		local otherData = getObjectData(other, Vector2.zero)
		local collision, moveVector = checkCollision(objectData, otherData)

		if not collision then
			continue
		end

		local totalMass = object.Mass + other.Mass
		local ratio = if other.Anchored then 1 elseif totalMass == 0 then 0.5 else other.Mass / totalMass
		moveVector *= ratio
		velocity -= moveVector
		objectData = getObjectData(object, velocity)

		table.insert(collisions, {
			object = other,
			moveVector = moveVector,
		})
	end

	return collisions
end


return function(object)
	local instance = object.Instance
	local size = instance.AbsoluteSize
	local position = instance.AbsolutePosition

	size = Vector3.new(size.X, size.Y, 0)
	position = Vector3.new(position.X, position.Y, 0)

	local hitboxSize = size * object.HitboxScale
	local alignment = size * (1 - math.clamp(object.HitboxScale, 0, 1)) * 0.5

	local center = position + alignment + hitboxSize * 0.5
	local corners = {}

	for i = 1, #object.Hitbox do
		local point = object.Hitbox[i]
		table.insert(corners, position + alignment + hitboxSize * point)
	end

	if instance.Rotation ~= 0 then
		local rotation = math.rad(instance.Rotation)
		local sin, cos = math.sin(rotation), math.cos(rotation)

		for i = 1, #corners do
			local corner = corners[i]
			local dx, dy = corner.X - center.X, corner.Y - center.Y

			-- stylua: ignore start
			corners[i] = Vector3.new(
				center.X + dx * cos - dy * sin,
				center.Y + dx * sin + dy * cos,
				0
			)
			-- stylua: ignore end
		end
	end

	return corners
end


--!native
--!optimize 2

local SpatialGrid = {}
SpatialGrid.__index = SpatialGrid

function SpatialGrid.new(cellSize: number)
	return setmetatable({
		_cellSize = cellSize,
		_cells = {},
	}, SpatialGrid)
end

function SpatialGrid:clear()
	for _, row in self._cells do
		for _, cell in row do
			table.clear(cell)
		end
	end
end

function SpatialGrid:insert(object)
	local pos = object.Instance.AbsolutePosition
	local size = object.Instance.AbsoluteSize
	local cs = self._cellSize

	local minCX = math.floor(pos.X / cs)
	local minCY = math.floor(pos.Y / cs)
	local maxCX = math.floor((pos.X + size.X) / cs)
	local maxCY = math.floor((pos.Y + size.Y) / cs)

	local cells = self._cells
	for cx = minCX, maxCX do
		local row = cells[cx]
		if not row then
			row = {}
			cells[cx] = row
		end
		for cy = minCY, maxCY do
			local cell = row[cy]
			if not cell then
				cell = {}
				row[cy] = cell
			end
			table.insert(cell, object)
		end
	end
end

function SpatialGrid:query(object, result, seen)
	table.clear(result)
	table.clear(seen)

	local pos = object.Instance.AbsolutePosition
	local size = object.Instance.AbsoluteSize
	local cs = self._cellSize

	local minCX = math.floor(pos.X / cs) - 1
	local minCY = math.floor(pos.Y / cs) - 1
	local maxCX = math.floor((pos.X + size.X) / cs) + 1
	local maxCY = math.floor((pos.Y + size.Y) / cs) + 1

	local cells = self._cells
	local n = 0

	for cx = minCX, maxCX do
		local row = cells[cx]
		if row then
			for cy = minCY, maxCY do
				local cell = row[cy]
				if cell then
					for _, other in cell do
						local id = other.Id
						if not seen[id] then
							seen[id] = true
							n += 1
							result[n] = other
						end
					end
				end
			end
		end
	end

	return result
end

return SpatialGrid


local ContentManager = require(script.Parent.ContentManager)
local cache = {}
local exports = {}
exports.__index = exports

local function createCacheableImage(url)
	local editableImage = ContentManager.createSafeEditableImage(url) :: EditableImage
	local imageData = {
		image = editableImage,
		isFree = false,
	}

	cache[url] = cache[url] or imageData
	return imageData
end

function exports.new(): canvas2d
	return setmetatable({
		loading = false,
		url = "",
		position = Vector2.zero,
		image = (nil :: any) :: EditableImage,
		containers = {},
		primaryContainer = nil,
	}, exports)
end

function exports:setUrl(url: string)
	self.url = url
	self.loading = true

	local imageData = cache[url]
	local cantUseCache = not imageData or not imageData.isFree

	if cantUseCache then
		imageData = createCacheableImage(url)
	end

	if imageData.image == nil then
		self.loading = false
		return
	end

	imageData.isFree = false
	self.loading = false
	self:setImage(imageData.image)
end

function exports:setImage(targetImage: EditableImage)
	local cacheData = cache[self.url]
	local isUsingCache = if cacheData then cacheData.image == self.image else nil
	local content = Content.fromObject(targetImage)

	if isUsingCache then
		cacheData.isFree = true
	end

	for container in self.containers do
		container.ImageContent = content
	end

	self.image = targetImage
end

function exports:preloadSprites(sprites)
	for name, data in sprites do
		if not data.ImageContent and data.Url then
			local imageData = cache[data.Url] or createCacheableImage(data.Url)
			data.ImageContent = imageData.image

			continue
		end

		local imageContentList = data.ImageContentList or {}
		data.ImageContentList = imageContentList

		for i, url in imageContentList do
			if imageContentList[i] and cache[url] then
				continue
			end

			local imageData = cache[url] or createCacheableImage(url)
			imageContentList[i] = imageData.image
		end
	end
end

function exports:setPrimaryContainer(container)
	self.primaryContainer = container
end

function exports:addContainer(container)
	local containers = self.containers
	local image = self.image

	if containers[container] then
		return
	elseif image then
		container.ImageContent = Content.fromObject(image)
	end

	containers[container] = true
end

function exports:hasContainer(container)
	return self.containers[container]
end

export type canvas2d = typeof(exports.new())
return exports


local AssetService = game:GetService("AssetService")
local classes = script.Parent.Parent.Parent.Parent.Classes
local EventEmitter = require(classes.Internal.EventEmitter)

local cache = {}
local loading = {}
local events = EventEmitter.new()
local exports = {
	cache = cache,
	events = events,
	loading = loading,
}

function exports.loadImage(id)
	if cache[id] then
		return cache[id]
	elseif loading[id] then
		return events:Once("imageLoaded:" .. id):Wait()
	end

	local image = exports.createSafeEditableImage(id)
	cache[id] = image

	return image
end

function exports.createSafeEditableImage(id)
	local canvas, sucess, message = nil, nil, nil
	local content = Content.fromUri(id)
	loading[id] = id

	for retries = 1, 10 do
		sucess, canvas = pcall(
			AssetService.CreateEditableImageAsync,
			AssetService,
			content --Target content
		)

		if typeof(canvas) == "string" then
			message = canvas:lower()
			canvas = nil
		end

		if sucess then
			events:Fire("imageLoaded:" .. id, canvas)
			loading[id] = nil
			break
		elseif message and message:match("memory") then
			warn(("[SHADER INFO] Maximum memory reached, the shader for %s will not be applied"):format(id))
			break
		elseif message and message:match("too many") then
			warn(("[SHADER INFO] Too many requests for %s retrying in 5s"):format(id))
			task.wait(5)
		else
			warn("[SHADER INFO] Unmanaged error: " .. message)
		end

		warn("[SHADER INFO] Retrying to load image: " .. id .. " (" .. retries .. "/10)")
		warn("[SHADER INFO] If you are loading shaders when the client is still loading, it's recommended to wait some seconds before trying to load shaders")
		task.wait(0.1)
	end

	return canvas
end

return exports


function copyTable(__table, copyIn)
	local copy = copyIn or {}

	for key, value in __table do
		local valueType = typeof(value)

		if valueType ~= "table" then
			copy[key] = value
		else
			copy[key] = copyTable(value, copy[key])
		end
	end

	return copy
end

return copyTable


local appData = script.Parent.Parent.Parent.Parent.AppData
local data = require(appData.Data)
local default = {}

function delete(obj)
	for key, val in (obj or default) do
		local valueType = typeof(val)
		if valueType ~= "table" and valueType ~= "Instance" then
			obj[key] = nil
		elseif valueType == "Instance" or val.Id and data.objects[val.Id] == val then
			val:Destroy()
		else
			delete(obj[key])
			obj[key] = nil
		end
	end
end

return delete


local function diff(newObj, oldObj)
	local changes = {}
	for key, newVal in newObj do
		local oldVal = oldObj[key]
		if type(newVal) == "table" and type(oldVal) == "table" then
			local subChanges = diff(newVal, oldVal)

			if next(subChanges) then
				changes[key] = subChanges
			end
		elseif newVal ~= oldVal then
			changes[key] = newVal
		end
	end

	return changes
end

return diff


--!nonstrict
local httpService = game:GetService("HttpService")
local encoder = {}

local encoders = {
	UDim2 = function(data)
		return {
			X = {
				Scale = data.X.Scale,
				Offset = data.X.Offset,
			},
			Y = {
				Scale = data.Y.Scale,
				Offset = data.Y.Offset,
			},
		}
	end,
	Vector2 = function(data)
		return {
			X = data.X,
			Y = data.Y,
		}
	end,
	Vector3 = function(data)
		return {
			X = data.X,
			Y = data.Y,
			Z = data.Y,
		}
	end,
	EnumItem = function(data)
		return {
			EnumType = tostring(data.EnumType),
			Name = data.Name,
		}
	end,
}

local decoders = {
	UDim2 = function(data)
		return UDim2.new(data.X.Scale, data.X.Offset, data.Y.Scale, data.Y.Offset)
	end,
	Vector2 = function(data)
		return Vector2.new(data.X, data.Y)
	end,
	Vector3 = function(data)
		return Vector3.new(data.X, data.Y)
	end,
	EnumItem = function(data: { EnumType: string, Name: string }): EnumItem
		local enum = Enum
		return enum[data.EnumType][data.Name]
	end,
}

local function encode(data)
	for index, value in data do
		local datatype = typeof(value)
		local encoder = encoders[datatype]

		if datatype == "table" and not encoder then
			data[index] = encode(value)
			continue
		elseif not encoder then
			continue
		end

		local recovered = encoder(value)
		recovered.datatype = datatype

		data[index] = recovered
	end

	return data
end

local function decode(data)
	for index, value in data do
		local datatype = typeof(value)

		if datatype == "table" and not value.datatype then
			data[index] = decode(value)
			continue
		elseif datatype ~= "table" or not value.datatype then
			continue
		end

		local decoder = decoders[value.datatype]
		data[index] = decoder(value)
	end

	return data
end

function encoder.Encode(data)
	return httpService:JSONEncode(encode(data))
end

function encoder.Decode(data)
	local result = httpService:JSONDecode(data)
	return decode(result)
end

return encoder


function isEqual(first, second)
	if first == second then
		return true
	elseif
		first == nil
		or second == nil
		or typeof(first) ~= typeof(second)
		or typeof(first) ~= "table" and typeof(second) ~= "table"
	then
		return false
	end

	local firstCount, secondCount = 0, 0
	for key, value in first do
		firstCount += 1
		if not isEqual(value, second[key]) then
			return false
		end
	end

	for _ in second do
		secondCount += 1
	end

	return firstCount == secondCount
end

return isEqual


local path = {}
function path.getRoute(object: Instance): string
	local route = {}
	local baseRoute = {}
	local parent = object

	while parent ~= game do
		table.insert(baseRoute, parent.Name)
		parent = parent.Parent
	end

	for i = #baseRoute, 1, -1 do
		table.insert(route, baseRoute[i])
	end

	return table.concat(route, "/", 1, #route)
end

function path.readRoute(url: string): Instance
	local route = url:split("/")
	local instance = game

	for _, index in route do
		instance = instance[index]
	end

	return instance
end

return path


return function(v1, v2)
	local direction = v1 - v2
	return direction:Dot(direction)
end


return function(position, size)
	local resolution = workspace.CurrentCamera.ViewportSize
	local fakeRes = resolution + size
    -- stylua: ignore start
	return position.X < -size.X
        or position.Y < -size.Y
        or position.X > fakeRes.X
        or position.Y > fakeRes.Y
	-- stylua: ignore end
end


return function(object: PhysicalObject, smoothness: number, target: UDim2)
	local alpha = 1 - math.clamp(smoothness, 0, 0.95)
	local instance = object.Instance :: ImageLabel

	local currentPosition = instance.Position
	instance.Position = currentPosition:Lerp(target, alpha)
end


local data = require(script.Parent.Parent.Parent.Parent.AppData.Data)
local SceneManager = require(script.Parent.Parent.Parent.Parent.Services.SceneManager)
local handlers = {}

local function getObjectScene(object)
	local sceneId = object and object.Scene or ""
	return SceneManager:Get(sceneId)
end

local function subjectObject(object)
	local subject = object.Subject
	local scene = getObjectScene(subject)

	local targetSubject = scene and scene.Objects[subject.Id]
	if not targetSubject then
		return
	end

	object:SetSubject(targetSubject)
end

function handlers.Scene(scene)
	local scenes = SceneManager.Scenes
	local isInScenes = scenes[scene.Id]

	if isInScenes then
		return
	end

	for index, sceneObj in scenes do
		if scene == sceneObj then
			data.objects[index] = nil
			scenes[index] = nil
		end
	end

	data.objects[scene.Id] = scene
	scenes[scene.Id] = scene
end

function handlers.BaseObject(object)
	local scene = getObjectScene(object)
	local skipObject = if scene then scene.Objects:HasOne(object.Id) else true

	if object:IsA("Scene") or skipObject then
		return
	end

	object:SetScene(scene)
end

function handlers.StaticObject(object)
	local shader = object.Shader
	local scene = getObjectScene(object)
	local shaderEnv = if scene --
		then scene.ShaderEnvironment
		else nil

	local isInShaderEnv = if shaderEnv --
		then shaderEnv:HasOne(object.Id)
		else false

	if shader.Path ~= "" and shader.Enabled and not isInShaderEnv then
		object:SetShader(object.Shader)
	elseif not shader.Enabled and isInShaderEnv then
		object:SetShader()
	end
end

handlers.Sound = subjectObject
handlers.Particle = subjectObject

return handlers


local modules = {}

for _, val in script:GetDescendants() do
	if not val.Name:match(".spec") and val.Parent:IsA("Folder") then
		modules[val.Name] = require(val)
	end
end

return modules


local RunService = game:GetService("RunService")
local TextTag = require(script.Parent.Parent.Classes.Public.TextTag)
local frameRate = 60

local function createRandomDecayRenderFunction(defaultIntensity)
	return function(label, i, args)
		local intensity = args.intensity or defaultIntensity
		local duration = args.duration or 1
		local steps = math.max(1, math.floor(duration * frameRate))
		for step = 0, steps do
			local currentIntensity = intensity * (1 - step / steps)
			label.Position = UDim2.new(
				0,
				math.random(-currentIntensity, currentIntensity),
				0,
				math.random(-currentIntensity, currentIntensity)
			)
			task.wait(duration / steps)
		end
		label.Position = UDim2.new(0, 0, 0, 0)
	end
end

local function createWaveRenderFunction(defaultAmplitude)
	return function(label, i, args)
		local amplitude = args.intensity or defaultAmplitude
		local duration = args.duration or 1
		local steps = math.max(1, math.floor(duration * frameRate))
		for step = 0, steps do
			local phase = (2 * math.pi) * (step / steps) + (i or 0)
			local offsetY = amplitude * math.sin(phase)
			label.Position = UDim2.new(0, 0, 0, offsetY)
			task.wait(duration / steps)
		end
		label.Position = UDim2.new(0, 0, 0, 0)
	end
end

local function createGradientRenderFunction(defaultStartColor, defaultEndColor)
	return function(label, index, args)
		local startColor = args.startColor and Color3.fromHex(args.startColor) or defaultStartColor
		local endColor = args.endColor and Color3.fromHex(args.endColor) or defaultEndColor
		local finalTextColor = args.finalTextColor and Color3.fromHex(args.finalTextColor) or Color3.new(1, 1, 1)

		local totalDuration = args.duration or 5
		local cycleDur = args.cycleDuration or 1
		local offsetFactor = args.offsetFactor or 0.1
		local baseOffset = 1 - ((index - 1) * offsetFactor)
		local startTime = os.clock()

		local connection
		connection = RunService.RenderStepped:Connect(function()
			local now = os.clock()
			local elapsed = now - startTime

			if elapsed >= totalDuration then
				connection:Disconnect()
				return
			end

			local cycleT = elapsed % cycleDur
			local shift = 0.4 * math.sin(cycleT * (math.pi * 2))
			local unsat = baseOffset + shift
			local fraction = 1 - math.abs((unsat % 2) - 1)

			local gradientColor = startColor:Lerp(endColor, fraction)
			local blendFactor = (totalDuration - elapsed <= cycleDur) and cycleT or 0
			label.TextColor3 = gradientColor:Lerp(finalTextColor, blendFactor)
		end)
	end
end

local angry = TextTag.new()
angry.TypingSoundId = "rbxassetid://110332722919137"
angry.WordsPerSecond = 2
angry:SetTagName("angry")
angry:SetDisplayMode("word")
angry:SetRenderFunction(createRandomDecayRenderFunction(2))

local shake = TextTag.new()
shake:SetTagName("shake")
shake:SetRenderFunction(createRandomDecayRenderFunction(5))

local wave = TextTag.new()
wave:SetTagName("wave")
wave:SetRenderFunction(createWaveRenderFunction(5))

local gradient = TextTag.new()
gradient:SetTagName("gradient")
gradient:SetRenderFunction(createGradientRenderFunction(Color3.new(1, 1, 1), Color3.new(0, 0, 0)))

return {
	angry = angry,
	shake = shake,
	wave = wave,
	gradient = gradient,
}


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

local runService = game:GetService("RunService")
local upsideEngine = script.Parent.Parent

local crossPlatformService = require(upsideEngine.Services).CrossPlatformService
local util = require(upsideEngine.Lib.Util)

local configs = crossPlatformService.Configs
local wrap = util.Wrap

local actions = {
	up = false,
	left = false,
	right = false,
	down = false,
}

local lastAction = ""

local function resetActions()
	for action in actions do
		actions[action] = false
	end

	lastAction = ""
end

local directions = {
	up = Vector2.new(0, -1),
	left = Vector2.new(-1, 0),
	right = Vector2.new(1, 0),
	down = Vector2.new(0, 1),
}

local function onUserInput(state, input)
	local controlsEnabled = crossPlatformService.DefaultControllersEnabled
	local character = crossPlatformService.Character
	local action = input.Action:lower()

	local isValid = not character or not character.Sprites or not directions[action]
	local begin = state == "Begin"

	if not controlsEnabled or isValid then
		return
	end

	actions[action] = begin
	return
end

local function getStickDirection(position, config)
	local action, smaller = "idle", 10
	local direction = position.Unit

	for _, keyword in config do
		keyword = keyword:lower()

		local vector = directions[keyword]
		local magnitude = (vector - direction).Magnitude

		if magnitude < smaller then
			smaller = magnitude
			action = keyword
		end
	end

	return action
end

local function inputDetector(input, gpe)
	local inputType = input.UserInputType.Name
	local keyName = input.KeyCode.Name

	local device = inputType:match("Gamepad") and "Gamepad" or inputType:match("InputMethod") and "Mobile" or "Keyboard"
	local config = configs[device][keyName]

	if not config or inputType == "Keyboard" and gpe then
		return
	end

	local sensibility = crossPlatformService.StickSensibility
	local inputState = "Input" .. input.UserInputState.Name

	local position = Vector2.new(input.Position.X, input.Position.Y)
	local action = typeof(config) ~= "table" and config or getStickDirection(position * sensibility, config)

	crossPlatformService:Fire(inputState, {
		Action = action,
		Position = position,
		KeyCode = input.KeyCode,
	})
end

local function inputSetup(obj, func)
	for _, event in { "Began", "Changed", "Ended" } do
		obj["Input" .. event]:Connect(func)
	end
end

local function mobileSetup()
	local playerGui = Players.LocalPlayer:WaitForChild("PlayerGui")
	local touchControlFrame = playerGui:WaitForChild("TouchGui"):WaitForChild("TouchControlFrame")
	local thumbstick = touchControlFrame:WaitForChild("ThumbstickFrame")

	local stick = thumbstick:WaitForChild("StickImage")
	local jumpButton = touchControlFrame:WaitForChild("JumpButton")

	local jumpKeyCode = {
		Name = "JumpButton",
		EnumType = "KeyCode",
		Value = 5,
	}

	inputSetup(jumpButton, function()
		inputDetector({
			Position = Vector2.zero,
			UserInputState = Enum.UserInputState.Begin,
			UserInputType = Enum.UserInputType.InputMethod,
			KeyCode = jumpKeyCode,
		})
	end)

	local changed = false
	local base = Vector2.new(17, 17)
	runService.Heartbeat:Connect(function()
		local position = Vector2.new(stick.Position.X.Offset, stick.Position.Y.Offset)
		position = position - base
		position = Vector2.new(position.X, -position.Y)

		if position.X == 0 and changed then
			changed = false
			inputDetector({
				Position = Vector2.zero,
				UserInputState = Enum.UserInputState.End,
				UserInputType = Enum.UserInputType.InputMethod,
				KeyCode = Enum.KeyCode.Thumbstick1,
			})
		elseif position.X ~= 0 then
			changed = true
			inputDetector({
				Position = position,
				UserInputState = Enum.UserInputState.Begin,
				UserInputType = Enum.UserInputType.InputMethod,
				KeyCode = Enum.KeyCode.Thumbstick1,
			})
		end
	end)
end

if uis.TouchEnabled then
	coroutine.wrap(pcall)(mobileSetup)
end

game.StarterPlayer.DevTouchMovementMode = Enum.DevTouchMovementMode.Thumbstick
inputSetup(uis, inputDetector)

crossPlatformService:On("InputBegin", wrap(onUserInput, "Begin"))
crossPlatformService:On("InputChange", wrap(onUserInput, "Begin"))
crossPlatformService:On("InputEnd", wrap(onUserInput, "Ended"))
crossPlatformService:On("CharacterDestroy", resetActions)

local function animationPlayer(character, sideView)
	local defaultIdle = if sideView then "idle_right" else "idle_down"
	local action = "none"
	local animation = "idle"

	for control, enabled in actions do
		if not enabled then
			continue
		end

		action = control
		animation = control
	end

	if action == lastAction then --or sideView and character.IsJumping and lastAction == "up" then
		return
	elseif action == "none" and not lastAction:match("idle") then
		animation = ("idle_%s"):format(lastAction)
	end

	animation = if animation == "idle" then defaultIdle else animation
	if lastAction == "left" or lastAction == "right" then
		animation = `idle_{lastAction}`
	end
	lastAction = action

	character:Play(animation)
end

local function characterController(character, sideView)
	local velocity = Vector2.zero
	if actions.up and sideView and character.IsGrounded then
		character:Jump()
	end

	for key, direction in directions do
		local active = actions[key]
		if not active or key == "up" and sideView then
			continue
		end

		velocity += direction
	end

	velocity = if velocity ~= Vector2.zero then velocity.Unit else velocity
	velocity = velocity * character.WalkSpeed * 5

	if velocity == Vector2.zero then
		return
	end

	character.Velocity = if not character.IsGrounded and sideView
		then Vector2.new(velocity.X, character.Velocity.Y)
		else velocity
end

RunService.Heartbeat:Connect(function()
	local character = crossPlatformService.Character
	local sideView = crossPlatformService.SideView

	if not character or not character.Instance then
		return
	end

	animationPlayer(character, sideView)
	characterController(character, sideView)
end)

return {}


local ToUDim2 = require(script.Parent.Parent.Parent.Lib.Util).ToUDim2

local function isOutsideLimits(target, limits, margin)
	return target.X < limits.X - margin
		or target.X > 1 - limits.X + margin
		or target.Y < limits.Y - margin
		or target.Y > 1 - limits.Y + margin
end

return function(scene, deltaTime)
	local camera = scene.Camera
	local subject = camera.Subject

	local resolution = workspace.CurrentCamera.ViewportSize
	if not subject or not camera.FollowSubject then
		return
	elseif camera.Limits == Vector2.zero then
		return camera:LookTo(subject)
	end

	local velocity = subject.Velocity * deltaTime
	local limits = (camera.Limits / 2)
	local target = (subject.Instance.AbsolutePosition + velocity) / resolution

	if not isOutsideLimits(target, limits, 0) then
		return
	elseif isOutsideLimits(target, limits, 0.05) then
		return camera:LookTo(subject)
	end

	local position = camera.LocalPosition - velocity
	return camera:SetPosition(ToUDim2(position))
end


local setSmoothPosition = require(script.Parent.Parent.Parent.Lib.Util.Math.SetSmoothPosition)
return function(scene: Scene)
	local camera = scene.Camera
	local subject = camera.Subject and camera.Subject.Instance
	if not subject then
		return
	end

	for _, parallax in scene.Objects.Content do
		if not parallax:IsA("Parallax") and not parallax.Track then
			continue
		end

		parallax:UpdateTiles()

		if not parallax.LockToCamera then
			continue
		end

		local position = subject.Position - camera.OffsetPosition
		setSmoothPosition(parallax, camera.Smoothness, position)
	end
end


return function(scene, deltaTime)
    local cl = os.clock()
    for _, particle in scene.ParticleEnvironment.Content do
        if cl - particle.Clock < particle.LifeTime/particle.Rate then
            continue
        end

        particle.Clock = cl
        particle:Emit(particle.Rate * deltaTime)
    end
end


--!native
--!optimize 2
local upsideEngine = script.Parent.Parent.Parent
local ToUDim2 = require(script.Parent.Parent.Parent.Lib.Util.DataType.ToUDim2)
local getCollidingObjects = require(script.Parent.Parent.Parent.Lib.Util.Geometry.GetCollidingObjects)
local SpatialGrid = require(script.Parent.Parent.Parent.Lib.Util.Geometry.SpatialGrid)
local crossPlatformService = require(script.Parent.Parent.Parent.Services.CrossPlatformService)
local util = require(upsideEngine.Lib.Util)

local MAX_ITERATIONS = 4
local grid = SpatialGrid.new(256)
local nearbyBuffer = {}
local seenBuffer = {}

local function emit(self, meta, active)
	local oldState = self.Collisions[meta.Id]
	if oldState == active then
		return
	end

	local eventName = if active then "Collision" else "CollisionEnd"
	self.Collisions[meta.Id] = active or nil
	self:Fire(eventName, meta)
end

function applyBuoyancy(scene, object, fluid)
	local height = object.Instance.AbsoluteSize.Y
	local bottom = object.Instance.AbsolutePosition.Y + height
	local top = fluid.Instance.AbsolutePosition.Y

	local submergedVolume = math.min(bottom - top, height)
	local submergedRatio = submergedVolume / height

	if object:IsA("Fluid") or object.Anchored then
		return
	end

	local tideEffect = fluid.WavesAmplitude * (math.sin(fluid.WavesSpeed * os.clock()))
	local buoyancyForce = fluid.Density * submergedRatio * height * scene.Gravity.Y + tideEffect
	local dragForce = -fluid.Viscosity * object.Velocity

	object:ApplyForce(Vector2.new(0, -buoyancyForce))
	object:ApplyForce(dragForce)
end

local GRAVITY_SCALE = 100
local FORCE_SCALE = 10

local function update(scene, object, deltaTime)
	if object.Anchored then
		return
	end

	local sideView = crossPlatformService.SideView
	local mass = math.max(object.Mass, 1)

	object.Acceleration = scene.Gravity * GRAVITY_SCALE * object.GravityScale + (object.Force * FORCE_SCALE) / mass
	object.Velocity += object.Acceleration * deltaTime

	local friction = math.max(1 - object.Friction * deltaTime, 0)
	local vx = object.Velocity.X * friction
	local vy = if sideView then object.Velocity.Y else object.Velocity.Y * friction
	object.Velocity = Vector2.new(vx, vy)

	object.Force = Vector2.zero

	return object.Velocity * deltaTime
end

local GRID_THRESHOLD = 30
local objects = {}
local freefall = {}
local collisionsBuffer = {}

return function(scene, deltaTime)
	table.clear(objects)
	table.clear(freefall)
	local objectCount = 0
	local useGrid = false

	for index, object in scene.Objects do
		if
			not object:IsA("PhysicalObject")
			or not object.TrackCollisions
			or not object.Instance.Visible
			or scene.OnlyTrackVisible
				and util.IsOutScreen(object.Instance.AbsolutePosition, object.Instance.AbsoluteSize)
		then
			continue
		elseif object.Anchored then
			objects[index] = object
			continue
		end

		objects[index] = object
		freefall[index] = object
		objectCount += 1
	end

	useGrid = objectCount >= GRID_THRESHOLD
	if useGrid then
		grid:clear()
		for _, object in objects do
			grid:insert(object)
		end
	end

	for _, object in freefall do
		local posX, negX, posY, negY = 0, 0, 0, 0
		local instance = object.Instance
		local velocity = update(scene, object, deltaTime)
		local absSize = instance.AbsoluteSize
		local sizeMax = math.max(absSize.X, absSize.Y, 1)
		local iterations = math.min(math.ceil(velocity.Magnitude / sizeMax), MAX_ITERATIONS)
		iterations = math.max(iterations, 1)

		table.clear(collisionsBuffer)
		local collisions = collisionsBuffer
		local colliding = {}
		local nearby = if useGrid then grid:query(object, nearbyBuffer, seenBuffer) else objects

		for i = 1, iterations do
			local vel = velocity * (i / iterations)
			colliding = getCollidingObjects(nearby, object, vel)

			if #colliding > 0 then
				velocity = vel
				break
			end
		end

		for _, collision in colliding do
			local other = collision.object
			local mv = collision.moveVector
			local isFluid = other:IsA("Fluid")

			collisions[collision.object.Id] = true
			emit(other, object, true)
			emit(object, other, true)

			if isFluid then
				applyBuoyancy(scene, object, other)
				continue
			elseif not object.CanCollide or not other.CanCollide then
				continue
			end

			if mv.X > 0 then posX = math.max(posX, mv.X) else negX = math.min(negX, mv.X) end
			if mv.Y > 0 then posY = math.max(posY, mv.Y) else negY = math.min(negY, mv.Y) end

			if object.AngularScale ~= 0 and math.abs(mv.X) > math.abs(mv.Y) * 0.25 then
				local vel = object.Velocity
				local size = math.max(absSize.X, absSize.Y, 1)
				local spin = (vel.X * mv.Y - vel.Y * mv.X) / size
				object.AngularVelocity += spin * object.AngularScale * deltaTime * 12
			end
		end

		for id in object.Collisions do
			local meta = scene.Objects:Get(id)
			if not meta then
				continue
			end

			emit(object, meta, collisions[id])
			emit(meta, object, collisions[id])
		end

		if object.Anchored then
			continue
		end

		local moveVector = Vector2.new(posX + negX, posY + negY)
		object.IsGrounded = moveVector.Y > 0
		instance.Position += ToUDim2(velocity - moveVector)

		local vx, vy = object.Velocity.X, object.Velocity.Y
		if moveVector.X * vx > 0 then vx = 0 end
		if moveVector.Y * vy > 0 then vy = 0 end
		object.Velocity = Vector2.new(vx, vy)

		local speed = object.Velocity.Magnitude
		if speed > 5000 then
			object.Velocity *= 5000 / speed
		end

		if object.AngularScale ~= 0 or object.Torque ~= 0 then
			local mass = math.max(object.Mass, 1)
			object.AngularVelocity += (object.Torque * FORCE_SCALE / mass) * deltaTime

			if object.IsGrounded then
				local target = math.round(instance.Rotation / 90) * 90
				local offset = instance.Rotation - target
				object.AngularVelocity -= offset * object.AngularStiffness * deltaTime
			end

			object.AngularVelocity *= math.max(1 - object.AngularFriction * deltaTime, 0)
			instance.Rotation += object.AngularVelocity * deltaTime
			object.Torque = 0
		end
	end
end


local TweenService = game:GetService("TweenService")
local getDistance = require(script.Parent.Parent.Parent.Lib.Util.Math.GetDistance)

local function setProximityPromptVisible(proximityPrompt, visible)
	local targetTransparency = if visible then 0 else 1
	local tweenInfo = TweenInfo.new(proximityPrompt.FadeDuration, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
	local elements = {
		proximityPrompt.Instance,
		proximityPrompt.Label,
	}

	if visible and not proximityPrompt.IsPlaying then
		proximityPrompt:Play("idle")
	end

	for _, element in elements do
		local tween = TweenService:Create(
			element,
			tweenInfo,
			if element:IsA("ImageLabel")
				then { ImageTransparency = targetTransparency }
				else { TextTransparency = targetTransparency }
		)

		tween:Play()
		tween.Completed:Once(function()
			tween:Destroy()
			if targetTransparency == 1 then
				proximityPrompt:Stop()
			end
		end)
	end
end

return function(scene)
	local subject = scene.Camera.Subject
	local maxDistance = math.huge
	local targetProximityPrompt = nil
	local proximityPrompts = {}

	if not subject or not subject.Instance then
		return
	end

	for _, object in scene.Objects do
		if not object:IsA("ProximityPrompt2D") or not object.Enabled or not object.Instance then
			continue
		end

		local subjectPos = subject.Instance.AbsolutePosition
		local objectPos = object.Instance and object.Instance.AbsolutePosition

		local range = object.Range and object.Range * object.Range
		local distance = getDistance(subjectPos, objectPos)

		local inRange = distance <= (range or math.huge)
		table.insert(proximityPrompts, {
			object = object,
			inRange = inRange,
		})

		if distance < maxDistance and inRange then
			maxDistance = distance
			targetProximityPrompt = object
		end
	end

	for _, data in proximityPrompts do
		local object = data.object
		local show = if object ~= targetProximityPrompt then not object.ShowIfClosest else true
		local closestChange = object.ShowIfClosest and object ~= targetProximityPrompt
		object.InRange = data.inRange
		object.IsClosest = object == targetProximityPrompt

		if object.inRange ~= data.inRange or closestChange then
			setProximityPromptVisible(object, object.InRange and show)
		end
	end
end


local util = require(script.Parent.Parent.Parent.Lib.Util)
local getDistance = require(script.Parent.Parent.Parent.Lib.Util.Math.GetDistance)
local normalize = util.Normalize

return function(scene)
	if not scene.Camera.Subject or not scene.Camera.Subject.Instance then
		return
	end

	for _, sound in scene.SoundEnvironment.Content do
		local soundSubject = sound.Subject and sound.Subject.Instance
		local soundInstance = sound.Instance

		local player = scene.Camera.Subject.Instance
		if not soundSubject then
			continue
		end

		local range = sound.Range * sound.Range
		local distance = getDistance(player.AbsolutePosition, soundSubject.AbsolutePosition)

		if distance <= range and sound.DistanceFading then
			soundInstance.Volume = sound.MaxVolume - normalize(distance, 0, range)
		elseif distance <= range then
			soundInstance.Volume = sound.MaxVolume
		elseif sound.Volume ~= 0 then
			soundInstance.Volume = 0
		end
	end
end


local IsOutScreen = require(script.Parent.Parent.Parent.Lib.Util).IsOutScreen
local function axeStep(active, axe, max)
	local index = active.CurrentFrame[axe]
	local value = index <= max - 1 and index + 1 or 0

	active.CurrentFrame =
		Vector2.new(axe == "X" and value or active.CurrentFrame["X"], axe == "Y" and value or active.CurrentFrame["Y"])
end

local function spriteListStep(self, sprite, seconds)
	local active = self.Active
	local instance = self.Instance
	local shader = sprite.Shader

	local list = sprite.List
	local currentFrame
	axeStep(active, "X", #list - 1)

	currentFrame = Vector2.new(active.CurrentFrame.X + 1, active.CurrentFrame.Y)
	self:Fire("FrameChanging", currentFrame)

	if not shader.Enabled then
		instance.Image = list[currentFrame.X]
		return
	end

	instance:SetAttribute("url", list[currentFrame.X])
end

local function SpriteSheetStep(self, sprite, active)
	local instance = self.Instance
	local frames = sprite.Frames

	if frames.X == active.CurrentFrame.X then
		axeStep(active, "Y", frames.Y)
	end

	axeStep(active, "X", frames.X)
	self:Fire("FrameChanging", active.CurrentFrame)
	instance.ImageRectOffset = instance.ImageRectSize * active.CurrentFrame
end

local function play(self, active)
	local sprite = self.Sprites[active.Name]
	local instance = self.Instance

	local shader = self.Shader
	local spriteStep = sprite.Url and SpriteSheetStep or spriteListStep
	active.NextStepReady = false

	if sprite.Url and shader.Enabled then
		instance:SetAttribute("url", sprite.Url)
	elseif sprite.Url and not shader.Enabled then
		instance.Image = sprite.Url
	end

	spriteStep(self, sprite, active)
	task.wait(active.SecondsPerFrame)

	active.NextStepReady = true
end

local function shouldSkip(object, active)
	return not (object:IsA("Sprite") and object.IsPlaying and active.NextStepReady and object.Sprites[active.Name])
		or IsOutScreen(object.Instance.AbsolutePosition, object.Instance.AbsoluteSize)
end

return function(scene, delta)
	for _, object in scene.Objects do
		local active = object.Active
		if shouldSkip(object, active) then
			continue
		end
		coroutine.wrap(play)(object, active)
	end
end


return function(scene)
	local lighting = scene.LightingEnvironment
	local shaders = scene.ShaderEnvironment
	lighting.__actrees:SetVariable("lights", lighting.Content)
	shaders.__actor:SetVariable("shadable", shaders.Content)
end


local upsideEngine = script.Parent.Parent
local runService = game:GetService("RunService")
local players = game:GetService("Players")

local request = require(script.Parent.Parent.Classes.Internal.Request)
local ToVector2 = require(script.Parent.Parent.Lib.Util.DataType.ToVector2)
local merge = require(script.Parent.Parent.Lib.Util.Generic.Merge)
local copyTable = require(script.Parent.Parent.Lib.Util.Internal.CopyTable)
local diff = require(script.Parent.Parent.Lib.Util.Internal.Diff)
local getDistance = require(script.Parent.Parent.Lib.Util.Math.GetDistance)
local authorityService = require(script.Parent.Parent.Services.AuthorityService)
local networkingService = require(upsideEngine.Services.NetworkingService)
local sceneManager = require(upsideEngine.Services.SceneManager)

local socket = game.ReplicatedStorage:WaitForChild("UpsideEngineSocket")
local isServer = runService:IsServer()
local isRunning = runService:IsRunning()

local requestsCache = networkingService.RequestsCache
local playersData = networkingService.PlayersData
local replicationTarget = networkingService.ReplicationTarget
local replicationTimer = 0

local event = isServer and "OnServerEvent" or "OnClientEvent"
local endpoints = {}

function endpoints.sync(client, pending)
	if typeof(pending) ~= "table" then
		return
	elseif not isServer then
		for _, requests in pending do
			buildRequests(requests)
		end

		return
	end

	-- This loop prevents the client from impersonating the server and therefore
	-- ensures that their requests are not automatically approved

	for _, request in pending do
		request.ClientId = client.UserId
	end

	buildRequests(pending)
end

function endpoints.requestServerData(client)
	for retries = 1, 4 do
		local data = table.clone(requestsCache)
		data[client.UserId] = nil

		socket:FireClient(client, "sync", data)
		task.wait(0.5)
	end
end

function buildRequests(requests)
	for _, data in requests do
		local content = data.Content :: RequestContent
		local clientId = data.ClientId

		if clientId == nil or content == nil then
			continue
		end

		local pendingRequest = request.new(clientId, content)
		playersData[clientId] = playersData[clientId] or {}

		local isNewObject = playersData[clientId][content.ObjectId] == nil
		local authorityType = authorityService:GetAuthority({
			Id = content.ObjectId,
		} :: any)

		if clientId == 0 then
			pendingRequest:Accept()
		elseif authorityType == "Client" or (isServer and isNewObject) then
			networkingService:Fire("ReplicationRequest", pendingRequest)
		end
	end
end

local function onEvent(client, requestType, pending)
	local handle = endpoints[requestType] :: () -> nil
	handle(client, pending)
end

local function onPlayerAdded(player)
	playersData[player.UserId] = {}
	requestsCache[player.UserId] = {}
end

local function onPlayerLeave(player)
	local skip = not networkingService.DestroyObjectsOnLeave
	local data = playersData[player.UserId]
	playersData[player.UserId] = nil
	requestsCache[player.UserId] = nil

	if skip or isServer then
		return
	end

	for _, object in data do
		replicationTarget[object] = nil
		object:Destroy()
	end
end

local function sendClientRequests(pending)
	networkingService.Pending = {}
	socket:FireServer("sync", pending)
end

local function sendServerRequests(requests)
	local toSend = {}
	for index, data in requests do
		local content = data.Content
		local objectId = content.ObjectId
		local clientId = data.ClientId
		local clientRequests = requestsCache[clientId]

		if clientRequests == nil then
			requests[index] = nil
			continue
		end

		local cachedRequest = clientRequests[objectId] or { Content = {} }
		local cachedContent = cachedRequest.Content

		if toSend[clientId] == nil then
			toSend[clientId] = {}
		end

		requests[index] = nil
		clientRequests[objectId] = copyTable(clientRequests[objectId] or {}, data)
		toSend[clientId][objectId] = {
			ClientId = clientId,
			Content = merge(data.Content, {
				Instance = diff(content.Instance, cachedContent.Instance or {}),
				ObjectProperties = diff(content.ObjectProperties, cachedContent.ObjectProperties or {}),
			}),
		}
	end

	for _, player in players:GetPlayers() do
		local playerData = playersData[player.UserId] or {}
		local data = table.clone(toSend)
		local serverData = table.clone(data[0] or {})
		data[player.UserId] = nil
		data[0] = serverData

		for objectId, request in serverData do
			local object = playerData[objectId]
			if object == nil then
				continue
			end

			local authorityType = authorityService:GetAuthority({
				Id = objectId,
			} :: any)

			if authorityType == "Client" then
				serverData[objectId] = nil
			end
		end

		socket:FireClient(player, "sync", data)
	end
end

local function replicateServer()
	for _, scene in sceneManager.Scenes do
		networkingService:Replicate(scene)
		for _, object in scene.Objects do
			networkingService:Replicate(object)
		end
	end
end

local function requestManager(deltaTime)
	local pending = networkingService.Pending
	local sendRequests = if isServer then sendServerRequests else sendClientRequests

	local serverReplication = networkingService.ServerReplication
	local replicationTicks = 1 / networkingService.ReplicationPerSecond
	replicationTimer += deltaTime

	while replicationTimer > replicationTicks do
		replicationTimer -= replicationTicks

		if isServer and isRunning and serverReplication then
			replicateServer()
		end

		if next(pending) then
			sendRequests(pending)
		end
	end
end

local function clientInterpolator(deltaTime)
	for object, goals in replicationTarget do
		local instance = object.Instance
		replicationTarget[object] = nil

		for property, value in goals do
			local currentValue = instance[property]
			local distance = getDistance(ToVector2(currentValue, "Offset"), ToVector2(value, "Offset"))

			if distance < 2 then
				instance[property] = value
				continue
			end

			local lerpSpeed = math.min(deltaTime * 8, 0.5)
			local iValue = currentValue:Lerp(value, lerpSpeed)
			instance[property] = iValue
		end
	end
end

for _, player in players:GetPlayers() do
	playersData[player.UserId] = {}
	requestsCache[player.UserId] = {}
end

if isServer then
	socket[event]:Connect(onEvent)
else
	socket[event]:Connect(function(...)
		onEvent(players.LocalPlayer, ...)
	end)
end

players.PlayerAdded:Connect(onPlayerAdded)
players.PlayerRemoving:Connect(onPlayerLeave)
runService.Heartbeat:Connect(requestManager)

if not isServer then
	runService.Heartbeat:Connect(clientInterpolator)
	socket:FireServer("requestServerData")
end

return {}


local CrossPlatformService = require(script.Parent.Parent.Services.CrossPlatformService)
local SceneManager = require(script.Parent.Parent.Services.SceneManager)

local function emitTriggerEvent(input, eventName)
	for _, scene in SceneManager.ActiveScenes do
		for _, object in scene.Objects do
			if not object:IsA("ProximityPrompt2D") or not object.Enabled then
				continue
			end

			if input.Action == object.ActionName and object.IsClosest then
				object:Fire(eventName)
			end
		end
	end
end

CrossPlatformService:On("InputBegin", function(input)
	emitTriggerEvent(input, "Triggered")
end)

CrossPlatformService:On("InputEnd", function(input)
	emitTriggerEvent(input, "TriggerEnded")
end)

return {}


--!native
local upsideEngine = script.Parent.Parent
local stepFolder = script:FindFirstAncestorOfClass("Folder").EveryStep

local runService = game:GetService("RunService")
local sceneManager = require(upsideEngine.Services.SceneManager)

local childs = stepFolder:GetChildren()
local trackers = {}

for _, tracker in childs do
	if not tracker.Name:match(".spec") then
		trackers[tracker.Name] = require(tracker)
	end
end

local t1 = os.clock()
local FIXED_DELTA_TIME = 1 / 250
local MAX_STEPS = 10

if runService:IsRunning() then
	local fixedNames = { Physics = true, CameraTracker = true, ParallaxTracker = true }
	local fixedTarget = {}
	local frameTarget = {}

	if runService:IsServer() then
		table.insert(fixedTarget, trackers.Physics)
	else
		for name, tracker in trackers do
			if fixedNames[name] then
				table.insert(fixedTarget, tracker)
			else
				table.insert(frameTarget, tracker)
			end
		end
	end

	runService.Heartbeat:Connect(function()
		local now = os.clock()
		local deltaTime = now - t1
		t1 = now

		local accumulator = math.min(deltaTime, FIXED_DELTA_TIME * MAX_STEPS)

		while accumulator >= FIXED_DELTA_TIME do
			for _, scene in sceneManager.ActiveScenes do
				for _, tracker in fixedTarget do
					tracker(scene, FIXED_DELTA_TIME)
				end
			end
			accumulator -= FIXED_DELTA_TIME
		end

		for _, scene in sceneManager.ActiveScenes do
			for _, tracker in frameTarget do
				tracker(scene, deltaTime)
			end
		end
	end)
end

return {}


local upsideEngine = script.Parent.Parent
local eventEmitter = require(upsideEngine.Classes.Internal.EventEmitter)

local authorityService = {}
authorityService.__index = authorityService

function authorityService.new()
	local self = eventEmitter.new()
	self:SetClassName(script.Name)
	self.AuthorityAssignments = {}

	return setmetatable(self, authorityService)
end

--[[={
	@desc Assigns authority for a specific object to a given authority type. This determines who has control
	over the object's state and behavior in the networked environment. AuthorityType can be "Client" or "Server".
}=]]
function authorityService:SetAuthority(object: BaseObject, authorityType: AuthorityType)
	self.AuthorityAssignments[object.Id] = authorityType
end

--[[={
	@desc Retrieves the current authority assignment for a specific object. Returns the authority type
	that has control over the object, defaulting to "Server" authority when not explicitly set.
}=]]
function authorityService:GetAuthority(object: BaseObject): AuthorityType
	return self.AuthorityAssignments[object.Id] or "Server"
end

--[[={
	@desc This service manages authority assignments for objects in the game. Authority determines which client or server
	has control over specific objects. This is essential for network synchronization and preventing conflicts in
	multiplayer environments. Only the server can manage authority assignments.

	Example usage:
```lua
-- Set authority for an object to server
AuthorityService:SetAuthority(myObject, "Server")

-- Check authority
local authority = AuthorityService:GetAuthority(myObject)
if authority then
	print("Authority is assigned to:", authority)
end
```

	@about
		@AuthorityAssignments A table that stores the authority assignments for objects, indexed by object Id
}=]]

return setmetatable(authorityService, eventEmitter).new() :: AuthorityService


local UserInputService = game:GetService("UserInputService")
local upsideEngine = script.Parent.Parent
local classes = upsideEngine.Classes

local character = require(script.Parent.Parent.Classes.Public.Character)
local eventEmitter = require(classes.Internal.EventEmitter)
local crossPlatformService = {}
crossPlatformService.__index = crossPlatformService

local function disconnectCharacter(self)
	if self.CharacterDestroyConnection then
		self.CharacterDestroyConnection:Disconnect()
		self.CharacterDestroyConnection = nil
	end
end

local function bindCharacter(self, targetCharacter)
	disconnectCharacter(self)
	self.Character = targetCharacter

	if not targetCharacter then
		return
	end

	self.CharacterDestroyConnection = targetCharacter:On("Destroy", function()
		if self.Character ~= targetCharacter then
			return
		end

		disconnectCharacter(self)
		self.Character = nil
		self:Fire("CharacterDestroy", targetCharacter)
	end)
end

function crossPlatformService.new()
	local self = eventEmitter.new()
	self:SetClassName("CrossPlatformService")

	self.DefaultControllersEnabled = true
	self.SideView = true
	self.StickSensibility = 1
	self.CharacterDestroyConnection = nil

	self.Configs = {
		Keyboard = {
			W = "Up",
			A = "Left",
			S = "Down",
			D = "Right",

			Up = "Up",
			Left = "Left",
			Down = "Down",
			Right = "Right",
			Space = "Up",
			E = "Interact",
		},
		Gamepad = {
			ButtonA = "Up",
			ButtonX = "Interact",
			Thumbstick1 = {
				Up = "Up",
				Left = "Left",
				Down = "Down",
				Right = "Right",
			},
		},
		Mobile = {
			JumpButton = "Up",
			Thumbstick1 = {
				Up = "Up",
				Left = "Left",
				Down = "Down",
				Right = "Right",
			},
		},
	}

	bindCharacter(self, character.new())

	return setmetatable(self, crossPlatformService)
end

--[[={
	@desc Detects the current device type based on user input capabilities
	@link string.link
}=]]

function crossPlatformService:DetectCurrentDevice(): string
	if UserInputService.TouchEnabled then
		return "Mobile"
	elseif UserInputService.GamepadEnabled then
		return "Gamepad"
	else
		return "Keyboard"
	end
end

--[[={
	@desc Returns the current key for a specific action based on the current device (Mobile, Gamepad, or Keyboard)
	@link string.link
}=]]

function crossPlatformService:GetTargetActionKey(action: string): string?
	local device = self:DetectCurrentDevice()
	for key, mappedAction in self.Configs[device] or {} do
		if mappedAction == action then
			return key
		end
	end

	return nil
end

--[[={
	@desc Assigns an action to a device key, example:
	```lua
	CrossPlatformService:SetDeviceKey("Keyboard", "Space", "Up")
	```
	@link void.link
}=]]

function crossPlatformService:SetDeviceKey(device: string, key: string, action: string)
	self.Configs[device][key] = action
end

--[[={
	@desc Sets the entire configuration of a device, example:
	```lua
	CrossPlatformService:SetDeviceConfig("Keyboard", {
		W = "Up",
		A = "Left",
		S = "Down",
		D = "Right",

		Up = "Up",
		Left = "Left",
		Down = "Down",
		Right = "Right",
		Space = "Up",
	})
	```
	@link void.link
}=]]

function crossPlatformService:SetDeviceConfig(device: string, controls: Dictionary<string, string>)
	self.Configs[device] = controls
end

--[[={
	@desc Sets the player character
	@link void.link
}=]]

function crossPlatformService:SetPlayerCharacter(character: Character)
	bindCharacter(self, character)
end

--[[={
	@desc This class is used to make the controls functional in any device (Keyboards, Mobiles, Gamepads), this service vinculate actions
	to specified keys, and also provides a movement system for the player character which can be disabled with the `DefaultControllersEnabled`
	property, here is an example to make our player jumps in every device:
```lua
--  								Device,	    Key, 	Action
CrossPlatformService:SetDeviceKey("Keyboard", "Space", "Up")
CrossPlatformService:SetDeviceKey("Mobile", "JumpButton", "Up")
CrossPlatformService:SetDeviceKey("Gamepad", "ButtonA", "Up")
```

But this is not limited only to movement actions, you can also assign other kind of actions, for example:

```lua
CrossPlatformService:SetDeviceKey("Keyboard", "E", "Collect")
CrossPlatformService:SetDeviceKey("Mobile", "JumpButton", "Collect")
CrossPlatformService:SetDeviceKey("Gamepad", "ButtonA", "Collect")
```

We assigned an action for our devices but how can we detect when an action is triggered? well we can listen to three events
"InputBegin", "InputChange", "InputEnd", example of use:

```lua
-- If the movement belongs to a stick, the second parameter will give the current position of the stick
CrossPlatformService:On("InputBegin", function(inputObject)
	local character = CrossPlatformService.Character

	if inputObject.Action == "Up" then
		character:Jump(150)
	end
end)
```

	@about
		@DefaultControllersEnabled Defines if the default movement system is enabled
		@SideView Defines if the character is going to be seen from the side or from the top
		@Character This is the character which is going to be tracked by the camera
		@StickSensibility This is the sensibility of the sticks in mobile and in game controllers
		@Configs This table stores the default controllers
	@events
		@InputBegin Params -> [UpsideEngineInput](/documentation/datatypes/UpsideEngineInput.html)
		Fired when one of the keys/sticks in the configuration is pressed/moved
		@InputChange Params -> [UpsideEngineInput](/documentation/datatypes/UpsideEngineInput.html)
		Fired when the an active input change its value, for example the position of a stick
		@InputEnd Params -> [UpsideEngineInput](/documentation/datatypes/UpsideEngineInput.html)
		Fired when one of the keys/sticks in the configuration finish to be pressed/moved
}=]]

return setmetatable(crossPlatformService, eventEmitter).new()


local upsideEngine = script.Parent.Parent
local copyTable = require(script.Parent.Parent.Lib.Util.Internal.CopyTable)
local diff = require(script.Parent.Parent.Lib.Util.Internal.Diff)
local eventEmitter = require(upsideEngine.Classes.Internal.EventEmitter)
local request = require(upsideEngine.Classes.Internal.Request)
local properties = require(upsideEngine.AppData.Properties)

local networkingService = {}
networkingService.__index = networkingService

function networkingService.new()
	local self = eventEmitter.new()
	self:SetClassName(script.Name)
	self.ReplicationPerSecond = 15
	self.DestroyObjectsOnLeave = true
	self.ServerReplication = true
	self.ReplicationTarget = {}
	self.RequestsCache = { [0] = {} }
	self.PlayersData = { [0] = {} }
	self.Pending = {}
	self.Cache = {}

	return setmetatable(self, networkingService)
end

local function read(object, index)
	return object[index]
end

local function getInstanceData(object)
	local instance = object.Instance
	local data = {}

	for _, index in properties.Instance do
		local sucess, value = pcall(read, instance, index)
		local isSpritePlaying = index == "ImageRectOffset" and object:IsA("Sprite") and object.IsPlaying

		if not sucess or index == "none" or isSpritePlaying then
			continue
		elseif index == "Image" and instance.ImageContent.Object ~= nil then
			value = instance:GetAttribute("url")
		end

		data[index] = value
	end

	return data
end

local function replicate(self, object)
	local instance = getInstanceData(object)
	local req = request.new()
	local props = {}

	if object:IsA("Sprite") then
		props.Active = {
			Name = object.Active.Name,
			SecondsPerFrame = object.Active.SecondsPerFrame,
		}
	end

	for key, value in object do
		if table.find(properties.NonReplicable, key) or typeof(value) == "Instance" then
			continue
		end

		props[key] = if typeof(value) == "table" --
			then copyTable(value)
			else value
	end

	local cached = self.Cache[object.Id] or {}
	local instanceData = diff(instance, cached.Instance or {})
	local objPropsData = diff(props, cached.ObjectProperties or {})

	if not (next(instanceData) or next(objPropsData)) then
		return
	end

	local content = {
		ClassName = object.ClassName,
		ObjectId = object.Id,
		Name = object.Name,
		Instance = instance,
		ObjectProperties = props,
	}

	self.PlayersData[0][object.Id] = object
	self.Cache[object.Id] = content
	req:Send(content)
	req:Destroy()
end

--[[={
	@desc Replicates an object to other clients
}=]]

function networkingService:Replicate(object: BaseObject)
	replicate(self, object :: Sprite)
end

--[[={
	@desc Replicates an object and each change on it, and returns the connections that detect each change of the object
	@link Connection.md
	@tsreturns LuaTuple<[Connection, Connection]>
}=]]

function networkingService:ReplicateOnChange(object: BaseObject): RBXScriptConnection
	local function wrapper(property)
		self:Replicate(object)
	end

	wrapper("none")
	return object.Instance.Changed:Connect(wrapper)
end

--[[={
	@desc This class is used to replicate objects to other clients, for example the player character
	@about
		@PlayersData Dictionary containing the replicated objects for each client (keyed by UserId)
		@DestroyObjectsOnLeave If true, destroys replicated objects from other clients when they leave
		@ReplicationPerSecond Specifies the number of replication requests per second to send when using ReplicateOnChange
		@ReplicationTarget Table of objects from other clients currently targeted for replication
		@Pending Table of pending replication requests
		@ServerReplication Boolean that determines if server-side replication is enabled, true by default
		@RequestsCache Cache storing replication requests indexed by client (UserId)
		@Cache Dictionary storing cached object data to optimize replication by detecting changes
	@events
		@ReplicationRequest Params -> [Request](Request.md)
		Fired when a request is received by the client
}=]]

return setmetatable(networkingService, eventEmitter).new()


local ue = script.Parent.Parent
local classes = ue.Classes

local objects = require(ue.AppData.Data).objects
local properties = require(ue.AppData.Properties)
local eventEmitter = require(classes.Internal.EventEmitter)

local encoder = require(ue.Lib.Util).Encoder
local runService = game:GetService("RunService")
local replicatedStorage = game:GetService("ReplicatedStorage")

local isStudio = runService:IsStudio()
local isClient = runService:IsClient()

local sceneManager = require(ue.Services.SceneManager)
local pluginSupportService = {}
pluginSupportService.__index = pluginSupportService

local classFixes = {
	Environment = function(scene, env)
		env:Run("SetScene", scene)
	end,
	Shader = function(scene, shader)
		for _, object in scene.Objects do
			if not object:IsA("StaticObject") then
				continue
			elseif object.Shader.Id ~= shader.Id then
				continue
			end

			object:SetShader(shader)
		end
	end,
}

function pluginSupportService.new()
	local self = eventEmitter.new()
	return setmetatable(self, pluginSupportService)
end

function getData(data)
	local result = {}

	for index, value in pairs(data) do
		local valueType = typeof(value)
		local isSimple = valueType ~= "table" and valueType ~= "Instance"

		if isSimple then
			result[index] = value
			continue
		elseif valueType == "Instance" then
			local instance = value
			value = {}

			for _, property in properties.Instance do
				local sucess, val = pcall(read, instance, property)
				if sucess then
					value[property] = val
				end
			end
		end

		result[index] = getData(value)
	end

	if data.Id then
		result.Id = data.Id
	end

	return result
end

local function setObjectIds(object, id, name)
	if object.ClassName == "Scene" then
		sceneManager.Scenes[object.Id] = nil
		sceneManager.ActiveScenes[object.Id] = nil

		sceneManager.Scenes[id] = object
		sceneManager.ActiveScenes[id] = object
	end

	objects[id] = objects[object.Id]
	objects[object.Id] = nil

	object.Id = id
	if object.Instance and object.Instance.Parent and object.Instance.Parent.ClassName ~= "Folder" then
		object:SetName(name)
	end
end

local function rebuild(parent, index, value)
	local object = parent[index]
	if index == "Hitbox" and typeof(value[1]) == "Vector2" then
		for i, v in value do
			value[i] = Vector3.new(v.X, v.Y)
		end
	end

	if value.ClassName and (not object or object.ClassName ~= value.ClassName) then
		local sucess, class = pcall(require, classes.Public[value.ClassName])
		if not sucess then
			return
		end

		object = class.new()
		parent[index] = object

		pcall(function()
			object:SetScene(parent)
		end)
	elseif not object then
		object = {}
	end

	if value.Id then
		setObjectIds(object, value.Id, value.Name)
	end

	for i, v in value do
		pcall(set, object, i, v)
	end

	return object
end

function set(object, index, value)
	object[index] = if typeof(value) ~= "table" then value else rebuild(object, index, value)
end

function read(object, index)
	return object[index]
end

--[[={
	@desc Loads the engine data stored in the "UpsideEngineDB" attribute of replicated storage, when it ends sets the attribute as an empty table
	@link void.link
}=]]

function pluginSupportService:LoadPluginContent()
	local db = encoder.Decode(game.ReplicatedStorage:GetAttribute("UpsideEngineDB") or "{}")
	for _, sceneData in pairs(db) do
		if not sceneData.Objects.Content then
			sceneData.Objects = {
				Content = sceneData.Objects,
			}
		end

		local scene = rebuild({}, 0, sceneData)
		scene.Camera.Scene = scene.Id
	end

	for _, scene in pairs(sceneManager.Scenes) do
		for _, object in scene.Objects do
			if not object.SetScene then
				continue
			end

			object.Instance.Parent = scene.Instance.GameFrame
			object:SetScene(scene)

			local fix = classFixes[object.ClassName]
			if not fix then
				continue
			end

			fix(scene, object)
		end
	end

	if isClient and runService:IsRunning() then
		game.ReplicatedStorage:SetAttribute("UpsideEngineDB", "{}")
	end
end

--[[={
	@desc Saves the engine data in replicated storage as attribute with the name "UpsideEngineDB" can be useful to create plugins
	@link void.link
}=]]

function pluginSupportService:SavePluginContent(content: {})
	if isStudio then
		local json = encoder.Encode(getData(content or {}))
		replicatedStorage:SetAttribute("UpsideEngineDB", json)
	end
end

--[[={
	@desc This class save and build the engine data
}=]]

return setmetatable(pluginSupportService, eventEmitter).new()


local upsideEngine = script.Parent.Parent
local eventEmitter = require(upsideEngine.Classes.Internal.EventEmitter)

local sceneManager = {}
sceneManager.__index = sceneManager

function sceneManager.new()
	local self = eventEmitter.new()
	self.Scenes = {}
	self.ActiveScenes = {}

	return setmetatable(self, sceneManager)
end

--[[={
	@desc Finds a scene by the name
	@link void.link
}=]]

function sceneManager:FindByName(name: string): Scene?
	for _, scene in self.Scenes do
		if scene.Name == name then
			return scene
		end
	end

	return
end

--[[={
	@desc Returns the scene with the specified Id
	@link Scene.md
}=]]

function sceneManager:Get(Id: string): Scene
	return self.Scenes[Id]
end

--[[={
	@desc This class save and build the engine data
	@about
		@Scenes	This dictionary stores all the scenes
		@ActiveScenes This dictionary stores all the active scenes
	@events
		@SceneLoaded Params -> [Scene](Scene.md)
		Fired when a scene is loaded
		@SceneUnloaded Params -> [Scene](Scene.md)
		Fired when a scene is unloaded
}=]]

return setmetatable(sceneManager, eventEmitter).new()


local BaseClass = require(script.Parent.Parent.Classes.Internal.BaseClass)
local TextTagService = {}
TextTagService.__index = TextTagService

function TextTagService.new()
	local self = BaseClass.new()
	self:SetClassName(script.Name)
	self.TextTags = {}

	return setmetatable(self, TextTagService)
end

function TextTagService:AddTextTag(textTag)
	self.TextTags[textTag.TagName] = textTag
end

function TextTagService:RemoveTextTag(textTag)
	self.TextTags[textTag.TagName] = nil
end

return setmetatable(TextTagService, BaseClass).new()


local module = {}

for _, Service in script:GetChildren() do
	if not Service.Name:match(".spec") then
		module[Service.Name] = require(Service)
	end
end

return module


local AssetService = game:GetService("AssetService")
local Canvas2d = require(script.Parent.Parent.Lib.Util.Graphics.Canvas2d)
local getDistance = require(script.Parent.Parent.Lib.Util.Math.GetDistance)
local normalize = require(script.Parent.Parent.Lib.Util.Math.Normalize)
local IsOutScreen = require(script.Parent.Parent.Lib.Util).IsOutScreen
local clock = os.clock
local ms = clock()
local axes = {
	Vector3.new(0, 0),
	Vector3.new(1, 0),
	Vector3.new(0, 1),
	Vector3.new(1, 1),
	Vector3.new(-1, 0),
	Vector3.new(0, -1),
	Vector3.new(-1, -1),
}

@native
local function lerpColor(color1, color2, factor)
	return Color3.new(
		color1.R * (1 - factor) + color2.R * factor,
		color1.G * (1 - factor) + color2.G * factor,
		color1.B * (1 - factor) + color2.B * factor
	)
end

@native
local function sortf(l1, l2)
	return l1.range > l2.range
end

@native
local function isCircleOutsideChunk(chk, light, range)
	return chk.position.X >= light.Position.X + range
		or chk.position.X + chk.size.X <= light.Position.X - range
		or chk.position.Y >= light.Position.Y + range
		or chk.position.Y + chk.size.Y <= light.Position.Y - range
end

--stylua: ignore start
@native
local function normalizeAngle(angle)
	local normalized = angle % (2 * math.pi)
	return normalized > math.pi
		and normalized - 2 * math.pi
		or normalized
end
--stylua: ignore end

@native
local function getRotatedAngle(light, origin)
	local dx = origin.X - light.Position.X
	local dy = origin.Y - light.Position.Y

	local angle = math.atan2(dy, dx)
	local rotatedAngle = angle - light.Rotation

	return normalizeAngle(rotatedAngle)
end

@native
local function isSpotOutsideChunk(chk, light, range)
	local lightAngle = math.clamp(light.Angle, 0, 360)
	local isOutsideChunk = isCircleOutsideChunk(chk, light, range)

	if isOutsideChunk then
		return true
	end

	for _, axe in axes do
		local position = chk.position + chk.size * axe
		local rotatedAngle = getRotatedAngle(light, position)

		local lowerBound = -lightAngle / 2
		local upperBound = lightAngle / 2

		local insideAngle = rotatedAngle >= lowerBound and rotatedAngle <= upperBound
			or math.abs(lowerBound) == upperBound

		if insideAngle then
			return false
		end
	end

	return true
end

@native
local function drawSpotLight(chkData, light, staticData)
	local ambientTransparency = staticData.ambientTransparency
	local absRange = light.AbsoluteRange
	local angle = light.Angle * 0.5
	local pixelCount = #chkData.content - 1

	if isSpotOutsideChunk(chkData, light, light.Range) then
		return
	end

	for i = 0, pixelCount do
		local px = chkData.content[i + 1]
		local position = px.position

		local distance = getDistance(position, light.Position + light.Size * 0.5)
		local rotatedAngle = getRotatedAngle(light, position)

		if distance > absRange or (rotatedAngle <= -angle or rotatedAngle >= angle) then
			continue
		end

		local color = px.color
		local transparency = px.transparency or ambientTransparency

		local nDistance = if light.Inverted then normalize(distance, 0, absRange) else math.log(distance, absRange)
		local pastDistance = px.pastDistance or nDistance

		local influence1 = math.clamp(1 - (distance / absRange) ^ 2, 0, 1)
		local influence2 = math.clamp(1 - (pastDistance / absRange) ^ 2, 0, 1)
		local totalInfluence = influence1 + influence2

		local newTransparency = transparency - nDistance + light.Brightness + 1
		local mixFactor = 1 - influence2 / totalInfluence
		local newColor = if color then lerpColor(color, light.Color, mixFactor) else light.Color

		px.pastDistance = distance
		px.transparency = newTransparency
		px.color = newColor
	end
end

@native
local function drawPointLight(chkData, light, staticData)
	local ambientTransparency = staticData.ambientTransparency
	local absRange = light.AbsoluteRange

	if isCircleOutsideChunk(chkData, light, light.Range) then
		return
	end

	local pixelCount = #chkData.content - 1
	for i = 0, pixelCount do
		local px = chkData.content[i + 1]
		local position = px.position

		local distance = getDistance(position, light.Position)
		if distance > absRange then
			continue
		end

		local color = px.color
		local transparency = px.transparency or ambientTransparency

		local nDistance = if light.Inverted then normalize(distance, 0, absRange) else math.log(distance, absRange)
		local pastDistance = px.pastDistance or nDistance
		local newTransparency = transparency - nDistance + light.Brightness + 1

		local influence1 = math.clamp(1 - (distance / absRange) ^ 2, 0, 1)
		local influence2 = math.clamp(1 - (pastDistance / absRange) ^ 2, 0, 1)
		local totalInfluence = influence1 + influence2

		local mixFactor = 1 - influence2 / totalInfluence
		local newColor = if color then lerpColor(color, light.Color, mixFactor) else light.Color

		px.pastDistance = distance
		px.transparency = newTransparency
		px.color = newColor
	end
end

@native
local function buildCanvas(position, size, data)
	local canvas = data.Canvas or Canvas2d.new()
	canvas:addContainer(data.instance)

	local resolution = data.resolution
	local pixels = table.create((resolution.Y - 1) * (resolution.X - 1))

	local oldImage = canvas.image
	local image = AssetService:CreateEditableImage({ Size = data.resolution })
	canvas:setImage(image)

	if oldImage then
		oldImage:Destroy()
	end

	for y = 0, resolution.Y - 1 do
		for x = 0, resolution.X - 1 do
			local normalPosition = Vector3.new(x / resolution.X, y / resolution.Y)
			table.insert(pixels, {
				position = position + normalPosition * size,
			})
		end
	end

	return canvas, pixels
end

@native
local function serializeChunk(chunks, chkData, lighting)
	local resolution = chkData.canvas.image.Size
	local bufferSize = resolution.Y * resolution.X

	local pxIndex = 0
	local chkPixels = buffer.create(bufferSize * 4)
	chunks[chkData] = chkPixels

	for _, px in chkData.content do
		local isDrawn = px.transparency ~= 0 and px.transparency ~= nil
		local transparency = if isDrawn
			then math.clamp(px.transparency, lighting.ambientTransparency, 1)
			else lighting.ambientTransparency

		local color = if isDrawn
			then lerpColor(
				px.color,
				lighting.ambientColor,
				math.clamp(lighting.lightIntensity - px.transparency, 0, lighting.lightIntensity)
			)
			else lighting.ambientColor

		buffer.writeu8(chkPixels, pxIndex, color.R * 255)
		buffer.writeu8(chkPixels, pxIndex + 1, color.G * 255)
		buffer.writeu8(chkPixels, pxIndex + 2, color.B * 255)
		buffer.writeu8(chkPixels, pxIndex + 3, 255 - transparency * 255)

		pxIndex += 4
		px.color = nil
		px.lightTransparency = nil
		px.transparency = 0
	end
end

local lightingSystem = {}

@native
function lightingSystem.onSetData(data, name, value)
	if name ~= "lights" or not value then
		return value
	end

	for index, light in value do
		local instance = light.Instance
		local range = light.Range * light.Range

		local v2Range = Vector2.new(range, range)
		local absolutePosition = instance.AbsolutePosition
		local absoluteSize = instance.AbsoluteSize

		if IsOutScreen(instance.AbsolutePosition, v2Range * 2) then
			continue
		end

		value[index] = {
			Position = Vector3.new(absolutePosition.X, absolutePosition.Y),
			Size = Vector3.new(absoluteSize.X, absoluteSize.Y),
			Rotation = math.rad(light.Rotation),
			Angle = math.rad(light.Angle),
			Shape = light.Shape:lower(),
			Instance = light.Instance,
			AbsoluteRange = range,
			Range = light.Range,
			Color = light.Color,
			Inverted = light.Inverted,
			Brightness = light.Brightness,
		}
	end

	table.sort(value, sortf)
	return value
end

@native
function lightingSystem.build(data)
	local details = data.details
	local chunk = details.chunk
	local chunkSize = details.chunkSize

	local chunkRows = math.floor(details.screenSize.Y / chunkSize)
	for row = 0, chunkRows do
		local instance = chunk:Clone()
		instance.Position = UDim2.fromOffset(details.col * chunkSize, row * chunkSize)
		instance.Parent = details.screen

		local size = Vector3.new(instance.AbsoluteSize.X, instance.AbsoluteSize.Y)
		local position = Vector3.new(instance.AbsolutePosition.X, instance.AbsolutePosition.Y)

		local canvas, pixels = buildCanvas(position, size, {
			instance = instance,
			resolution = details.chunkResolution,
		})

		table.insert(data.pixels, {
			instance = instance,
			canvas = canvas,
			position = position,
			size = size,
			content = pixels,
		})
	end
end

@native
function lightingSystem.draw(data)
	local current = clock()
	local lighting = data.lighting

	if current - ms < lighting.updateFrequency then
		return
	end

	local pixels = data.pixels
	ms = current

	local lights = data.lights
	local staticData = {
		ambientTransparency = lighting.ambientTransparency,
	}

	local chunks = {}
	for _, chkData in pixels do
		for _, light in lights do
			if light.Shape == "pointlight" then
				drawPointLight(chkData, light, staticData)
				continue
			end

			drawSpotLight(chkData, light, staticData)
		end

		serializeChunk(chunks, chkData, lighting)
	end

	task.synchronize()
	for _, chkData in pixels do
		local canvas = chkData.canvas :: Canvas2d.canvas2d
		local image = canvas.image
		image:WritePixelsBuffer(Vector2.zero, image.Size, chunks[chkData])
	end
end

@native
function lightingSystem.updateScreen(data)
	local lighting = data.lighting
	for _, chkData in data.pixels do
		local _, pixels = buildCanvas(chkData.position, chkData.size, {
			instance = chkData.instance,
			resolution = lighting.chunkResolution,
		})

		chkData.instance.ResampleMode = lighting.lightStyle
		chkData.content = pixels
	end
end

return lightingSystem


local path = require(script.Parent.Parent.Lib.Util.Internal.Path)
local isOutScreen = require(script.Parent.Parent.Lib.Util.Math.IsOutScreen)

local Canvas2d = require(script.Parent.Parent.Lib.Util.Graphics.Canvas2d)
local ContentManager = require(script.Parent.Parent.Lib.Util.Graphics.ContentManager)
local shadingSystem = {}

@native
local function clampV2(base, max)
	return Vector2.new(
		math.clamp(math.round(base.X), 0, max.X), -- X
		math.clamp(math.round(base.Y), 0, max.Y) -- Y
	)
end

@native
local function getPixel(bmp, idx)
	return buffer.readu8(bmp, idx) or 0,
		buffer.readu8(bmp, idx + 1) or 0,
		buffer.readu8(bmp, idx + 2) or 0,
		buffer.readu8(bmp, idx + 3) or 0
end

@native
local function isEmpty(pixels, iterations, position)
	local x = position.X
	local y = position.Y
	local width = iterations.X + 1

	if x < 0 or x > iterations.X or y < 0 or y > iterations.Y then
		return true
	end

	local index = ((y * width) + x) * 4
	local result = buffer.readu8(pixels, index + 3) == 0

	return result
end

@native
local function getAproximatedColor(pixels, position, iterations)
	local count = 0
	local sumR, sumG, sumB, sumA = 0, 0, 0, 0
	local width = iterations.X + 1

	for sx = -1, 1 do
		for sy = -1, 1 do
			local nx = position.X + sx
			local ny = position.Y + sy
			local nPosition = Vector3.new(nx, ny)

			if isEmpty(pixels, iterations, nPosition) then
				continue
			end

			local index = ((ny * width) + nx) * 4
			local r, g, b, a = getPixel(pixels, index)
			sumR += r
			sumG += g
			sumB += b
			sumA += a
			count += 1
		end
	end

	return sumR / count, sumG / count, sumB / count, sumA / count
end

@native
local function countPaintedNeighbors(pixels, iterations, position)
	local count = 0
	local range = 2

	for sx = -range, range do
		for sy = -range, range do
			local nx = position.X + sx
			local ny = position.Y + sy
			local nPosition = Vector3.new(nx, ny)

			if isEmpty(pixels, iterations, nPosition) then
				continue
			end

			count += 1
		end
	end

	return count
end

@native
local function scanGaps(pixels, iterations, preicision)
	local width = iterations.X + 1
	preicision = preicision * 16

	for y = 0, iterations.Y do
		for x = 0, iterations.X do
			local idx = ((y * width) + x) * 4
			if buffer.readu8(pixels, idx + 3) ~= 0 then
				continue
			end

			local position = Vector3.new(x, y)
			local count = countPaintedNeighbors(pixels, iterations, position)

			if count < preicision then
				continue
			end

			local r, g, b, a = getAproximatedColor(pixels, position, iterations)
			buffer.writeu8(pixels, idx, r)
			buffer.writeu8(pixels, idx + 1, g)
			buffer.writeu8(pixels, idx + 2, b)
			buffer.writeu8(pixels, idx + 3, a)
		end
	end
end

@native
local function texture(imageLabel: ImageLabel, position: Vector2 | Vector3)
	local id = imageLabel.Image
	local canvas = ContentManager.cache[id]
	local content = imageLabel.ImageContent

	if not canvas and content.Uri then
		task.synchronize()
		canvas = ContentManager.loadImage(id)
		task.desynchronize()
	elseif content.Object then
		canvas = content.Object
	end

	local size = canvas.Size
	local offset = imageLabel.ImageRectOffset
	local rectSize = imageLabel.ImageRectSize
	rectSize = if rectSize.X ~= 0 then rectSize else size
	rectSize = Vector2.new(
		math.round(math.min(size.X - offset.X, rectSize.X)),
		math.round(math.min(size.Y - offset.Y, rectSize.Y))
	)

	local x = math.floor(position.X - offset.X)
	local y = math.floor(position.Y - offset.Y)

	if x < 0 or x >= rectSize.X or y < 0 or y >= rectSize.Y then
		return 0, 0, 0, 0
	end

	local bmp = canvas:ReadPixelsBuffer(offset, rectSize)
	local idx = ((y * rectSize.X) + x) * 4

	return getPixel(bmp, idx)
end

@native
local function rotate(centre, position, degrees)
	local rotation = math.rad(degrees)
	local sin, cos = math.sin(rotation), math.cos(rotation)

	local XDistance = position.X - centre.X
	local YDistance = position.Y - centre.Y

	local x = centre.X + XDistance * cos - YDistance * sin
	local y = centre.Y + XDistance * sin + YDistance * cos

	return x, y
end

local function isOutsideBounds(size, cropArea)
	return size.X < cropArea.X or size.Y < cropArea.Y
end

local function shouldSkip(object, old, id)
	local oldCanvas: Canvas2d.canvas2d = old.Canvas
	local playingSprite = object.Sprites and object.Sprites[object.Active.Name]
	local isContainerAdded = oldCanvas and oldCanvas:hasContainer(object.Instance)

	local sameSize = oldCanvas and oldCanvas.image and oldCanvas.image.Size == old.ImageData.Size
	local sameUrl = if playingSprite and oldCanvas
		then playingSprite.Url == oldCanvas.url
		elseif oldCanvas then oldCanvas.url == id
		else nil

	return (sameUrl and sameSize) and isContainerAdded and oldCanvas
end

function shadingSystem.onSetData(data, name, value)
	local oldValue = data[name] or {}
	local result = {}

	for idx, object in value do
		local shader = object.Shader
		local mirrorSource = shader.MirrorSource
		local useMirror = shader.MirrorEnabled and mirrorSource ~= nil

		local index = if useMirror then shader.Id else idx
		local instance: ImageLabel = if useMirror then mirrorSource else object.Instance
		local old = oldValue[index] or {}

		local outScreen = isOutScreen(instance.AbsolutePosition, instance.AbsoluteSize)
		local id = instance:GetAttribute("url")
		id = if id ~= nil then id else instance.Image

		if outScreen and shouldSkip(object, old, id) then
			old.Track = not outScreen
			result[index] = old
			continue
		end

		local shaderSource = path.readRoute(object.Shader.Path)
		local shadingFunction = require(shaderSource)

		local imageData = ContentManager.loadImage(id)
		local canvas = old.Canvas or Canvas2d.new()
		canvas:addContainer(object.Instance)
		canvas:setPrimaryContainer(instance)

		if shader.PreloadSprites and object.Sprites then
			canvas:preloadSprites(object.Sprites)
		end

		if id ~= "" and canvas.url ~= id or not instance.ImageContent.Object then
			canvas:setUrl(id)
			instance:SetAttribute("url", id)
		end

		result[index] = {
			Instance = instance,
			Shader = shader,
			Precision = shader.Precision,
			Run = shadingFunction,
			Canvas = canvas,
			ImageData = imageData,
			Image = id,
			Track = true,
			Enabled = shader.Enabled,
		}
	end

	return result
end

@native
function shadingSystem.draw(details)
	local shadable = details.shadable or {}
	local shaderResults = {}

	for _, data in shadable do
		local canvas = data.Canvas :: Canvas2d.canvas2d
		local skipProcess = not data.Track or not data.Enabled
		local isLoading = not data.ImageData or not data.Canvas.image

		if skipProcess or isLoading then
			continue
		end

		local instance = canvas.primaryContainer
		local run = data.Run

		local sourceSize = data.ImageData.Size
		local canvasSize = canvas.image.Size

		local rectSize = clampV2(instance.ImageRectSize, canvasSize)
		local position = clampV2(instance.ImageRectOffset, canvasSize)

		local cropSize = if rectSize.X > 0 then rectSize else canvasSize
		local cropArea = position + cropSize

		if isOutsideBounds(sourceSize, cropArea) then
			continue
		end

		local pixels = buffer.create(cropSize.X * cropSize.Y * 4)
		local bmp = data.ImageData:ReadPixelsBuffer(position, cropSize)

		local iterations = Vector3.new(cropSize.X - 1, cropSize.Y - 1)
		local width, checkGaps = cropSize.X, false

		local shaderData = {
			x = 0,
			y = 0,
			red = 0,
			green = 0,
			blue = 0,
			opacity = 0,
			rotate = rotate,
			texture = texture,
		}

		for y = 0, iterations.Y do
			for x = 0, iterations.X do
				local idx = ((y * width) + x) * 4
				local sx, sy

				local r, g, b, a = getPixel(bmp, idx)
				shaderData.red = r
				shaderData.green = g
				shaderData.blue = b
				shaderData.opacity = a
				shaderData.x = x
				shaderData.y = y

				run(shaderData)
				sx = math.floor(shaderData.x)
				sy = math.floor(shaderData.y)

				if sx < 0 or sx > iterations.X or sy < 0 or sy > iterations.Y then
					continue
				end

				checkGaps = checkGaps or sx ~= x or sy ~= y
				idx = ((sy * cropSize.X) + sx) * 4

				buffer.writeu8(pixels, idx, shaderData.red or r)
				buffer.writeu8(pixels, idx + 1, shaderData.green or g)
				buffer.writeu8(pixels, idx + 2, shaderData.blue or b)
				buffer.writeu8(pixels, idx + 3, shaderData.opacity or a)
			end
		end

		if checkGaps then
			scanGaps(pixels, iterations, data.Precision)
		end

		shaderResults[canvas] = {
			i = instance,
			position = position,
			cropSize = cropSize,
			pixels = pixels,
		}
	end

	task.synchronize()
	for canvas, data in shaderResults do
		local cropArea = data.cropSize + data.position
		local image = canvas.image

		if isOutsideBounds(image.Size, cropArea) then
			continue
		end

		image:WritePixelsBuffer(data.position, data.cropSize, data.pixels)
	end
end

return shadingSystem


local parent = script.Parent
local classes = {}

local services = require(parent.Services)
local upsideWorkspace = require(parent.AppData.Data).workSpace

local upsideEngine = {}
upsideEngine.Version = "v3.3.0"
upsideEngine.Workspace = upsideWorkspace

function upsideEngine.new(name: string)
	return classes[name].new()
end

function upsideEngine.GetService(name: string)
	return services[name]
end

for _, class in parent.Classes.Public:GetDescendants() do
	if class.Name:match(".spec") or not class:IsA("ModuleScript") then
		continue
	end

	classes[class.Name] = require(class)
end

return upsideEngine