mirror of
https://github.com/lune-org/lune.git
synced 2024-12-12 13:00:37 +00:00
Write remodel migration module, improve roblox builtin typings
This commit is contained in:
parent
2297350c6e
commit
83303cf5e6
2 changed files with 330 additions and 4 deletions
277
docs/modules/remodel.luau
Normal file
277
docs/modules/remodel.luau
Normal file
|
@ -0,0 +1,277 @@
|
|||
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.Instance
|
||||
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.com, equivalent to `remodel.readPlaceFile`.
|
||||
|
||||
This method requires web authentication!
|
||||
]=]
|
||||
function remodel.readPlaceAsset(assetId: number)
|
||||
local contents = downloadAssetId(assetId)
|
||||
local place = roblox.deserializePlace(contents)
|
||||
return place
|
||||
end
|
||||
|
||||
--[=[
|
||||
Reads a model asset from Roblox.com, equivalent to `remodel.readModelFile`.
|
||||
|
||||
This method requires web authentication!
|
||||
]=]
|
||||
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 asXml = string.sub(filePath, -6) == ".rbxlx"
|
||||
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 asXml = string.sub(filePath, -6) == ".rbxmx"
|
||||
local placeFile = roblox.serializeModel({ instance }, asXml)
|
||||
fs.writeFile(filePath, placeFile)
|
||||
end
|
||||
|
||||
--[=[
|
||||
Uploads the given `DataModel` instance to Roblox.com, overwriting an existing place.
|
||||
|
||||
If the instance is not a `DataModel`, this function will throw.
|
||||
Models should be uploaded with `writeExistingModelAsset` instead.
|
||||
|
||||
This method requires web authentication!
|
||||
]=]
|
||||
function remodel.writeExistingPlaceAsset(dataModel: LuneDataModel, assetId: number)
|
||||
local placeFile = roblox.serializePlace(dataModel)
|
||||
uploadAssetId(assetId, placeFile)
|
||||
end
|
||||
|
||||
--[=[
|
||||
Uploads the given instance to Roblox.com, overwriting an existing model.
|
||||
|
||||
If the instance is a `DataModel`, this function will throw.
|
||||
Places should be uploaded with `writeExistingPlaceAsset` instead.
|
||||
|
||||
This method requires web authentication!
|
||||
]=]
|
||||
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
|
|
@ -1,5 +1,54 @@
|
|||
-- TODO: Autogenerate this somehow ...
|
||||
export type Instance = {}
|
||||
type InstanceProperties = {
|
||||
Parent: Instance?,
|
||||
ClassName: string,
|
||||
Name: string,
|
||||
[string]: any,
|
||||
}
|
||||
|
||||
type InstanceMetatable = {
|
||||
Clone: (self: Instance) -> Instance,
|
||||
Destroy: (self: Instance) -> (),
|
||||
ClearAllChildren: (self: Instance) -> (),
|
||||
|
||||
GetChildren: (self: Instance) -> { Instance },
|
||||
GetDescendants: (self: Instance) -> { Instance },
|
||||
GetFullName: (self: Instance) -> string,
|
||||
|
||||
FindFirstAncestor: (self: Instance, name: string) -> Instance?,
|
||||
FindFirstAncestorOfClass: (self: Instance, className: string) -> Instance?,
|
||||
FindFirstAncestorWhichIsA: (self: Instance, className: string) -> Instance?,
|
||||
FindFirstChild: (self: Instance, name: string, recursive: boolean?) -> Instance?,
|
||||
FindFirstChildOfClass: (self: Instance, className: string, recursive: boolean?) -> Instance?,
|
||||
FindFirstChildWhichIsA: (self: Instance, className: string, recursive: boolean?) -> Instance?,
|
||||
|
||||
IsA: (self: Instance, className: string) -> boolean,
|
||||
IsAncestorOf: (self: Instance, descendant: Instance) -> boolean,
|
||||
IsDescendantOf: (self: Instance, ancestor: Instance) -> boolean,
|
||||
|
||||
GetAttribute: (self: Instance, name: string) -> any,
|
||||
GetAttributes: (self: Instance) -> { [string]: any },
|
||||
SetAttribute: (self: Instance, name: string, value: any) -> (),
|
||||
|
||||
GetTags: (self: Instance) -> { string },
|
||||
HasTag: (self: Instance, name: string) -> boolean,
|
||||
AddTag: (self: Instance, name: string) -> (),
|
||||
RemoveTag: (self: Instance, name: string) -> (),
|
||||
}
|
||||
|
||||
export type Instance = typeof(setmetatable(
|
||||
(nil :: any) :: InstanceProperties,
|
||||
(nil :: any) :: { __index: InstanceMetatable }
|
||||
))
|
||||
|
||||
type DataModelMetatable = {
|
||||
GetService: (self: DataModel, name: string) -> Instance,
|
||||
FindService: (self: DataModel, name: string) -> Instance?,
|
||||
}
|
||||
|
||||
export type DataModel = typeof(setmetatable(
|
||||
(nil :: any) :: InstanceProperties,
|
||||
(nil :: any) :: { __index: InstanceMetatable & DataModelMetatable }
|
||||
))
|
||||
|
||||
--[=[
|
||||
@class Roblox
|
||||
|
@ -50,7 +99,7 @@ return {
|
|||
|
||||
@param contents The contents of the place to read
|
||||
]=]
|
||||
deserializePlace = function(contents: string): Instance
|
||||
deserializePlace = function(contents: string): DataModel
|
||||
return nil :: any
|
||||
end,
|
||||
--[=[
|
||||
|
@ -99,7 +148,7 @@ return {
|
|||
@param dataModel The DataModel for the place to serialize
|
||||
@param xml If the place should be serialized as xml or not. Defaults to `false`, meaning the place gets serialized using the binary format and not xml.
|
||||
]=]
|
||||
serializePlace = function(dataModel: Instance, xml: boolean?): string
|
||||
serializePlace = function(dataModel: DataModel, xml: boolean?): string
|
||||
return nil :: any
|
||||
end,
|
||||
--[=[
|
||||
|
|
Loading…
Reference in a new issue