mirror of
https://github.com/lune-org/docs.git
synced 2024-12-12 04:50:36 +00:00
420 lines
14 KiB
Text
420 lines
14 KiB
Text
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.
|
|
|
|
<Steps>
|
|
|
|
### Step 1
|
|
|
|
Copy the source below and place it in a file named `remodel.luau`:
|
|
|
|
<details>
|
|
<summary>Click to expand</summary>
|
|
|
|
```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. 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
|
|
```
|
|
|
|
</details>
|
|
|
|
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 run example
|
|
```
|
|
|
|
</Steps>
|
|
|
|
## 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.
|
|
|
|
<details>
|
|
<summary>Places & Models</summary>
|
|
|
|
- `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
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary>Files & Directories</summary>
|
|
|
|
- `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)
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary>JSON</summary>
|
|
|
|
- `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)
|
|
|
|
</details>
|
|
|
|
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!
|