import { Steps } from "nextra/components" # Migrating from Remodel If you have used [Remodel](https://github.com/rojo-rbx/remodel) 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](#differences-between-lune--remodel) below. ### Step 1 Copy the source below and place it in a file named `remodel.luau`:
Click to expand ```lua copy --!strict local fs = require("@lune/fs") local net = require("@lune/net") local process = require("@lune/process") local roblox = require("@lune/roblox") local serde = require("@lune/serde") 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. Create request headers in advance, we might re-use them for CSRF challenges local headers = { ["User-Agent"] = "Roblox/WinInet", ["Content-Type"] = "application/octet-stream", Accept = "application/json", Cookie = cookie, } -- 3. Create and send a request to the upload url local uploadResponse = net.request({ url = `https://data.roblox.com/Data/Upload.ashx?assetid={assetId}`, body = contents, method = "POST", headers = headers, }) -- 4. Check if we got a valid response, we might have gotten a CSRF -- challenge and need to send the request with a token included if not uploadResponse.ok and uploadResponse.statusCode == 403 and uploadResponse.headers["x-csrf-token"] ~= nil then headers["X-CSRF-Token"] = uploadResponse.headers["x-csrf-token"] uploadResponse = net.request({ url = `https://data.roblox.com/Data/Upload.ashx?assetid={assetId}`, body = contents, method = "POST", headers = headers, }) end 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: ```lua copy 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 🚀 ```sh copy lune 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 - `remodel.readPlaceFile` âž¡ [`fs.readFile`](../api-reference/fs.md#readfile) & [`roblox.deserializePlace`](../api-reference/roblox.md#deserializeplace) - `remodel.readModelFile` âž¡ [`fs.readFile`](../api-reference/fs.md#readfile) & [`roblox.deserializeModel`](../api-reference/roblox.md#deserializemodel) - `remodel.readPlaceAsset` âž¡ [`net.request`](../api-reference/net.md#request) & [`roblox.deserializePlace`](../api-reference/roblox.md#deserializeplace) - `remodel.readModelAsset` âž¡ [`net.request`](../api-reference/net.md#request) & [`roblox.deserializeModel`](../api-reference/roblox.md#deserializemodel) - `remodel.writePlaceFile` âž¡ [`roblox.serializePlace`](../api-reference/roblox.md#serializeplace) & [`fs.writeFile`](../api-reference/fs.md#writefile) - `remodel.writeModelFile` âž¡ [`roblox.serializeModel`](../api-reference/roblox.md#serializemodel) & [`fs.writeFile`](../api-reference/fs.md#writefile) - `remodel.writeExistingPlaceAsset` âž¡ [`roblox.serializePlace`](../api-reference/roblox.md#serializeplace) & [`net.request`](../api-reference/net.md#request) - `remodel.writeExistingModelAsset` âž¡ [`roblox.serializeModel`](../api-reference/roblox.md#serializemodel) & [`net.request`](../api-reference/net.md#request) - `remodel.getRawProperty` âž¡ no equivalent, you can get properties directly by indexing - `remodel.setRawProperty` âž¡ no equivalent, you can set properties directly by indexing
Files & Directories - `remodel.readFile` âž¡ [`fs.readFile`](../api-reference/fs.md#readfile) - `remodel.readDir` âž¡ [`fs.readDir`](../api-reference/fs.md#readdir) - `remodel.writeFile` âž¡ [`fs.writeFile`](../api-reference/fs.md#writefile) - `remodel.createDirAll` âž¡ [`fs.writeDir`](../api-reference/fs.md#writedir) - `remodel.removeFile` âž¡ [`fs.removeFile`](../api-reference/fs.md#removefile) - `remodel.removeDir` âž¡ [`fs.removeDir`](../api-reference/fs.md#removedir) - `remodel.isFile` âž¡ [`fs.isFile`](../api-reference/fs.md#isfile) - `remodel.isDir` âž¡ [`fs.isDir`](../api-reference/fs.md#isdir)
JSON - `json.fromString` âž¡ [`serde.decode`](../api-reference/serde.md#decode) - `json.toString` âž¡ [`serde.encode`](../api-reference/serde.md#encode) - `json.toStringPretty` âž¡ [`serde.encode`](../api-reference/serde.md#encode)
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`](../api-reference/process.md#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](./4-api-status.md) 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!