Quick actions

cmd+k|ctrl+k

Navigation

Languages

SMD_Loader

Snippet info

Language

Lua

Visibility

public

Author

zen

Created

2024-12-21T19:26:15.983805Z

Updated

2024-12-21T19:26:15.983805Z

--!strict

local AssetService = game:GetService("AssetService")
local CollectionService = game:GetService("CollectionService")
local HTTP = game:GetService("HttpService")

local Compression

if not script:FindFirstChild("Compression") and HTTP.HttpEnabled then
	Compression = loadstring(HTTP:GetAsync("https://pastebin.com/raw/Lxk41Jq0",true))()
else
	require(script.Compression)
end

local SMD_DATA_ATTRIB = "SMD_Data"
local SMD_TAG = "SMD_MeshPart"

local main = {}
main.Compression = Compression
main.Util = {}

export type DataBlock = "version" | "nodes" | "skeleton" | "triangles"
export type SMD_Model = {
	Data: {number} -- This array stores the Vertices in this order [VERT_X,VERT_Y,VERT_Z,NORMAL_X,NORMAL_Y,NORMAL_Z,U,V]
}

-- this function converts a string to base64
local function ToBase64(data: string): string
	local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
	return ((data:gsub('.', function(x) 
		local r,b='',x:byte()
		for i=8,1,-1 do r=r..(b%2^i-b%2^(i-1)>0 and '1' or '0') end
		return r;
	end)..'0000'):gsub('%d%d%d?%d?%d?%d?', function(x)
		if (#x < 6) then return '' end
		local c=0
		for i=1,6 do c=c+(x:sub(i,i)=='1' and 2^(6-i) or 0) end
		return b:sub(c+1,c+1)
	end)..({ '', '==', '=' })[#data%3+1])
end

-- this function converts base64 to string
local function FromBase64(data: string): string
	local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
	data = string.gsub(data, '[^'..b..'=]', '')
	return (data:gsub('.', function(x)
		if (x == '=') then return '' end
		local r,f='',(b:find(x)::number-1)
		for i=6,1,-1 do r=r..(f%2^i-f%2^(i-1)>0 and '1' or '0') end
		return r;
	end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x)
		if (#x ~= 8) then return '' end
		local c=0
		for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end
		return string.char(c)
	end))
end

local function GetLine(str: string,find: string,start: number?): number
	-- TOTALLY scuffed implementation and can prob be optimized, but idrc so uhh yeah
	start = start or 1
	
	local lines = string.split(str,"\n")
	
	for i = start :: number,#lines do
		local line = lines[i]
		if line == find or string.match(line,find) then
			return i
		end
	end
	
	return -1
end

local function GetDataBlockContent(source: string, block: DataBlock): {string}
	-- Oh boy this has so much potentional to break
	local lines = string.split(source,"\n")
	
	local startLine = GetLine(source,block) + 1
	local endLine = GetLine(source,"end",startLine) - 1
	
	local contentLines: {string} = {}
	
	local chunkSize = math.round((endLine - startLine) / 5)
	
	for chunk = startLine,endLine,chunkSize do
		local chunkStart = chunk
		local chunkEnd = math.min(endLine,chunk + chunkSize)

		print("Reading chunk " .. math.round(chunk / chunkSize))

		for line = chunkStart,chunkEnd do
			task.spawn(function()
				local relLineNumber = 1 + (line - startLine)
				contentLines[relLineNumber] = lines[line]
			end)
		end

		task.wait()
	end
	
	return contentLines
end

local function GetTriangles(source: string): {{number}}
	-- I hate myself for this
	local verts: {{number}} = {}
	
	local dataBlock = GetDataBlockContent(source,"triangles")
	
	for i = 1,#dataBlock do
		local vertRaw = dataBlock[i]
		
		local nums = string.split(vertRaw," ")
		if #nums < 2 then continue end

		local data: {number} = {}

		local index = 1
		for i,strNum in pairs(nums) do
			local num = tonumber(strNum)
			if num then
				table.insert(data,index,num)
				index += 1
			end
		end

		table.insert(verts,data)
	end
	
	print(verts[1])

	return verts
end

function main.Load(source: string): SMD_Model
	local startTime = time()
	
	local model: SMD_Model = {
		Data = {}
	}
	
	local data = GetTriangles(source)
	
	for i,triangle in pairs(data) do
		-- Vertex Position
		model.Data[#model.Data + 1] = triangle[3]
		model.Data[#model.Data + 1] = triangle[4]
		model.Data[#model.Data + 1] = triangle[2]
		-- Vertex Normal
		model.Data[#model.Data + 1] = triangle[6]
		model.Data[#model.Data + 1] = triangle[7]
		model.Data[#model.Data + 1] = triangle[5]
		-- Vertex UV
		model.Data[#model.Data + 1] = triangle[8]
		model.Data[#model.Data + 1] = -triangle[9]
	end
	
	if #model.Data % 3 ~= 0 then
		error("Why isn't your model triangles? For what reason man.")
	end
	
	print(string.format("Model loaded after %s seconds",tostring(time() - startTime)))

	return model
end

function main.LoadFromURI(uri: string): SMD_Model
	local uriSplit = string.split(uri,"/")
	local fileName = uriSplit[#uriSplit]
	
	print("Loading " .. fileName)
	return main.Util.ModelFromData(HTTP:GetAsync(uri,true))
end

function main.MeshFromModel(model: SMD_Model,textureId: number?): MeshPart
	local editableMesh = AssetService:CreateEditableMesh()

	local colorId = editableMesh:AddColor(Color3.new(1,1,1),1)
	
	local vertexIds: {number} = {}
	local normalIds: {number} = {}
	local uvIds: {number} = {}
	
	local vertIndex = 1
	for i = 1,#model.Data,8 do
		local vertex = model.Data[i]
		table.insert(vertexIds,vertIndex,editableMesh:AddVertex(Vector3.new(model.Data[i+0],model.Data[i+1],model.Data[i+2])))
		table.insert(normalIds,vertIndex,editableMesh:AddNormal(Vector3.new(model.Data[i+3],model.Data[i+4],model.Data[i+5])))
		table.insert(uvIds,vertIndex,editableMesh:AddUV(Vector2.new(model.Data[i+6],model.Data[i+7])))
		vertIndex += 1
	end
	
	for i = 1,#vertexIds,3 do
		local faceId = editableMesh:AddTriangle(vertexIds[i],vertexIds[i+1],vertexIds[i+2])
		editableMesh:SetFaceNormals(faceId,{normalIds[i],normalIds[i+1],normalIds[i+2]})
		editableMesh:SetFaceUVs(faceId,{uvIds[i],uvIds[i+1],uvIds[i+2]})
	end
	
	print(editableMesh:RemoveUnused())

	local meshPart = AssetService:CreateMeshPartAsync(Content.fromObject(editableMesh))
	if textureId then
		meshPart.TextureContent = Content.fromAssetId(textureId)
	end
	
	meshPart:SetAttribute(SMD_DATA_ATTRIB,main.Util.ModelToString(model))
	
	CollectionService:AddTag(meshPart,SMD_TAG)
	
	--editableMesh:Destroy() -- To prevent memory leaks -- Message from future Zen : this was a stupid f-ing bug
	
	return meshPart
end

function main.ReloadAllMeshParts()
	for _,meshPart in CollectionService:GetTagged(SMD_TAG) do
		if not meshPart:IsA("MeshPart") then continue end
		main.ReloadMeshPart(meshPart)
	end
end

function main.ReloadMeshPart(meshPart: MeshPart)
	if not meshPart:GetAttribute(SMD_DATA_ATTRIB) then return end
	
	local data = meshPart:GetAttribute(SMD_DATA_ATTRIB) :: string
	local tempMeshPart = main.MeshFromModel(main.Util.ModelFromData(data))
	meshPart:ApplyMesh(tempMeshPart)
	tempMeshPart:Destroy()
end

function main.Util.ModelToString(model: SMD_Model): string
	local vertices = table.concat(model.Data,",")
	
	local compressed = ToBase64(Compression.Zlib.Compress(vertices,{
		level = 9,
		strategy = "huffman_only"
	}))
	
	return compressed
end

function main.Util.ModelFromData(data: string): SMD_Model
	local model: SMD_Model = {
		Data = {}
	}
	local rawData = string.split(main.Util.DecodeData(data),",")
	
	for i = 1,#rawData do
		model.Data[i] = tonumber(rawData[i]) :: number
	end
	
	return model
end

function main.Util.DecodeData(data: string)
	return Compression.Zlib.Decompress(FromBase64(data))
end

return main
INFO