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. ### 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 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. 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 ```
## API Differences Since Lune is meant to be a general-purpose Luau runtime, it takes a different approach from Remodel in certain areas: - Lune uses Luau instead of Lua 5.3. - APIs are more loosely coupled, for example reading a Roblox place file is separated into two steps - reading the actual file using Lune's `fs` built-in library, and then deserializing that file using the `roblox` built-in library. - Lune tries to support many more formats and use cases - while Remodel has the `JSON` global for converting to/from JSON specifically, Lune has the `serde` built-in library which can convert to/from JSON, YAML, TOML, compress and decompress files, and more. - Built-in libraries are not accessible from global variables, you have to explicitly import them using `require("@lune/library-name")`. - Arguments given to the script are not available in `...`, you have to import them using the `process` library: ```lua copy local process = require("@lune/process") print(process.args) -- Same as print(...) in Remodel ``` There are many more subtle differences between the two, not all of which are listed here.