Roblox
Migrating from Remodel

Migrating from Remodel

If you have used Remodel (opens in a new tab) before to manipulate place and/or model files, this migration guide will help you get started with accomplishing the same tasks in Lune.

Drop-in Compatibility

This guide provides a module which translates all of the relevant Lune APIs to their Remodel equivalents. For more details or manual migration steps, check out Differences Between Lune & Remodel below.

Step 1

Copy the source below and place it in a file named remodel.luau:

Click to expand
--!strict
 
local fs = require("@lune/fs")
local net = require("@lune/net")
local serde = require("@lune/serde")
local process = require("@lune/process")
local roblox = require("@lune/roblox")
 
export type LuneDataModel = roblox.DataModel
export type LuneInstance = roblox.Instance
 
local function getAuthCookieWithFallbacks()
	local cookie = roblox.getAuthCookie()
	if cookie then
		return cookie
	end
 
	local cookieFromEnv = process.env.REMODEL_AUTH
	if cookieFromEnv and #cookieFromEnv > 0 then
		return `.ROBLOSECURITY={cookieFromEnv}`
	end
 
	for index, arg in process.args do
		if arg == "--auth" then
			local cookieFromArgs = process.args[index + 1]
			if cookieFromArgs and #cookieFromArgs > 0 then
				return `.ROBLOSECURITY={cookieFromArgs}`
			end
			break
		end
	end
 
	error([[
		Failed to find ROBLOSECURITY cookie for authentication!
		Make sure you have logged into studio, or set the ROBLOSECURITY environment variable.
	]])
end
 
local function downloadAssetId(assetId: number)
	-- 1. Try to find the auth cookie for the current user
	local cookie = getAuthCookieWithFallbacks()
 
	-- 2. Send a request to the asset delivery API,
	--    which will respond with cdn download link(s)
	local assetApiResponse = net.request({
		url = `https://assetdelivery.roblox.com/v2/assetId/{assetId}`,
		headers = {
			Accept = "application/json",
			Cookie = cookie,
		},
	})
	if not assetApiResponse.ok then
		error(
			string.format(
				"Failed to fetch asset download link for asset id %s!\n%s (%s)\n%s",
				tostring(assetId),
				tostring(assetApiResponse.statusCode),
				tostring(assetApiResponse.statusMessage),
				tostring(assetApiResponse.body)
			)
		)
	end
 
	-- 3. Make sure we got a valid response body
	local assetApiBody = serde.decode("json", assetApiResponse.body)
	if type(assetApiBody) ~= "table" then
		error(
			string.format(
				"Asset delivery API returned an invalid response body!\n%s",
				assetApiResponse.body
			)
		)
	elseif type(assetApiBody.locations) ~= "table" then
		error(
			string.format(
				"Asset delivery API returned an invalid response body!\n%s",
				assetApiResponse.body
			)
		)
	end
 
	-- 4. Grab the first asset download location - we only
	--    requested one in our query, so this will be correct
	local firstLocation = assetApiBody.locations[1]
	if type(firstLocation) ~= "table" then
		error(
			string.format(
				"Asset delivery API returned no download locations!\n%s",
				assetApiResponse.body
			)
		)
	elseif type(firstLocation.location) ~= "string" then
		error(
			string.format(
				"Asset delivery API returned no valid download locations!\n%s",
				assetApiResponse.body
			)
		)
	end
 
	-- 5. Fetch the place contents from the cdn
	local cdnResponse = net.request({
		url = firstLocation.location,
		headers = {
			Cookie = cookie,
		},
	})
	if not cdnResponse.ok then
		error(
			string.format(
				"Failed to download asset with id %s from the Roblox cdn!\n%s (%s)\n%s",
				tostring(assetId),
				tostring(cdnResponse.statusCode),
				tostring(cdnResponse.statusMessage),
				tostring(cdnResponse.body)
			)
		)
	end
 
	-- 6. The response body should now be the contents of the asset file
	return cdnResponse.body
end
 
local function uploadAssetId(assetId: number, contents: string)
	-- 1. Try to find the auth cookie for the current user
	local cookie = getAuthCookieWithFallbacks()
 
	-- 2. Use a different endpoint to fetch a valid CSRF token
	local csrfHeaders = {
		["User-Agent"] = "Roblox/WinInet",
		Accept = "application/json",
		Cookie = cookie,
	}
 
	local csrfResponse = net.request({
		url = `https://auth.roblox.com/`,
		body = contents,
		method = "POST",
		headers = csrfHeaders,
	})
 
	local csrfToken = csrfResponse.headers["x-csrf-token"]
	if csrfToken == nil then error('Failed to fetch CSRF token.') end
 
	-- 3. Upload the asset to Roblox
	local uploadHeaders = {
		["User-Agent"] = "Roblox/WinInet",
		["Content-Type"] = "application/octet-stream",
		['X-CSRF-Token'] = csrfToken,
		Accept = "application/json",
		Cookie = cookie,
	}
 
	local uploadResponse = net.request({
		url = `https://data.roblox.com/Data/Upload.ashx?assetid={assetId}`,
		body = contents,
		method = "POST",
		headers = uploadHeaders,
	})
 
	-- 4. Make sure it uploaded properly
	if not uploadResponse.ok then
		error(
			string.format(
				"Failed to upload asset with id %s to Roblox!\n%s (%s)\n%s",
				tostring(assetId),
				tostring(uploadResponse.statusCode),
				tostring(uploadResponse.statusMessage),
				tostring(uploadResponse.body)
			)
		)
	end
end
 
local remodel = {}
 
--[=[
	Load an `rbxl` or `rbxlx` file from the filesystem.
 
	Returns a `DataModel` instance, equivalent to `game` from within Roblox.
]=]
function remodel.readPlaceFile(filePath: string)
	local placeFile = fs.readFile(filePath)
	local place = roblox.deserializePlace(placeFile)
	return place
end
 
--[=[
	Load an `rbxm` or `rbxmx` file from the filesystem.
 
	Note that this function returns a **list of instances** instead of a single instance!
	This is because models can contain mutliple top-level instances.
]=]
function remodel.readModelFile(filePath: string)
	local modelFile = fs.readFile(filePath)
	local model = roblox.deserializeModel(modelFile)
	return model
end
 
--[=[
	Reads a place asset from Roblox, equivalent to `remodel.readPlaceFile`.
 
	***NOTE:** This function requires authentication using a ROBLOSECURITY cookie!*
]=]
function remodel.readPlaceAsset(assetId: number)
	local contents = downloadAssetId(assetId)
	local place = roblox.deserializePlace(contents)
	return place
end
 
--[=[
	Reads a model asset from Roblox, equivalent to `remodel.readModelFile`.
 
	***NOTE:** This function requires authentication using a ROBLOSECURITY cookie!*
]=]
function remodel.readModelAsset(assetId: number)
	local contents = downloadAssetId(assetId)
	local place = roblox.deserializeModel(contents)
	return place
end
 
--[=[
	Saves an `rbxl` or `rbxlx` file out of the given `DataModel` instance.
 
	If the instance is not a `DataModel`, this function will throw.
	Models should be saved with `writeModelFile` instead.
]=]
function remodel.writePlaceFile(filePath: string, dataModel: LuneDataModel)
	local asBinary = string.sub(filePath, -5) == ".rbxl"
	local asXml = string.sub(filePath, -6) == ".rbxlx"
	assert(asBinary or asXml, "File path must have .rbxl or .rbxlx extension")
	local placeFile = roblox.serializePlace(dataModel, asXml)
	fs.writeFile(filePath, placeFile)
end
 
--[=[
	Saves an `rbxm` or `rbxmx` file out of the given `Instance`.
 
	If the instance is a `DataModel`, this function will throw.
	Places should be saved with `writePlaceFile` instead.
]=]
function remodel.writeModelFile(filePath: string, instance: LuneInstance)
	local asBinary = string.sub(filePath, -5) == ".rbxm"
	local asXml = string.sub(filePath, -6) == ".rbxmx"
	assert(asBinary or asXml, "File path must have .rbxm or .rbxmx extension")
	local placeFile = roblox.serializeModel({ instance }, asXml)
	fs.writeFile(filePath, placeFile)
end
 
--[=[
	Uploads the given `DataModel` instance to Roblox, overwriting an existing place.
 
	If the instance is not a `DataModel`, this function will throw.
	Models should be uploaded with `writeExistingModelAsset` instead.
 
	***NOTE:** This function requires authentication using a ROBLOSECURITY cookie!*
]=]
function remodel.writeExistingPlaceAsset(dataModel: LuneDataModel, assetId: number)
	local placeFile = roblox.serializePlace(dataModel)
	uploadAssetId(assetId, placeFile)
end
 
--[=[
	Uploads the given instance to Roblox, overwriting an existing model.
 
	If the instance is a `DataModel`, this function will throw.
	Places should be uploaded with `writeExistingPlaceAsset` instead.
 
	***NOTE:** This function requires authentication using a ROBLOSECURITY cookie!*
]=]
function remodel.writeExistingModelAsset(instance: LuneInstance, assetId: number)
	local modelFile = roblox.serializeModel({ instance })
	uploadAssetId(assetId, modelFile)
end
 
remodel.readFile = fs.readFile
remodel.readDir = fs.readDir
remodel.writeFile = fs.writeFile
remodel.createDirAll = fs.writeDir
remodel.removeFile = fs.removeFile
remodel.removeDir = fs.removeDir
remodel.isFile = fs.isFile
remodel.isDir = fs.isDir
 
return remodel

This module is quite large, but you will not need to read through it unless you want to know about the internal details of how Remodel used to work.

Step 2

Next, create another script next to your remodel.luau. We will be naming it example.luau, but you can name it whatever you want. This example code is from one of the previous Remodel-native example scripts, with only the top line added:

local remodel = require("./remodel")
 
-- One use for Remodel is to move the terrain of one place into another place.
local inputGame = remodel.readPlaceFile("input-place.rbxlx")
local outputGame = remodel.readPlaceFile("output-place.rbxlx")
 
-- This isn't possible inside Roblox, but works just fine in Remodel!
outputGame.Workspace.Terrain:Destroy()
inputGame.Workspace.Terrain.Parent = outputGame.Workspace
 
remodel.writePlaceFile("output-place-updated.rbxlx", outputGame)

Step 3

Finally, run the script you've created by providing the script name to Lune, in our case example, without the luau file extension. Everything should work the same way it did when running natively in Remodel, now running in Lune 🚀

lune run example

Differences Between Lune & Remodel

Most APIs previously found in Remodel have direct equivalents in Lune, below are some direct links to APIs that are equivalent or very similar.

Places & Models
Files & Directories
JSON

Since Lune is meant to be a general-purpose Luau runtime, there are also some more general differences, and Lune takes a different approach from Remodel in certain areas:

  • Lune runs Luau instead of Lua 5.3.
  • APIs are more loosely coupled, meaning that a task may require more steps using Lune. This also means that Lune is more flexible and supports more use cases.
  • Built-in libraries are not accessible from global variables, you have to explicitly import them using require("@lune/library-name").
  • Arguments given to scripts are not available in ..., you have to use process.args instead.
  • Lune generally supports all of the Roblox datatypes that are gettable/settable on instance properties. For a full list of available datatypes, check out the API Status page.

There may be more differences than are listed here, and the Lune-specific guides and examples may provide more info, but this should be all you need to know to migrate from Remodel. Good luck!