mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-21 12:01:38 +00:00
Merge branch 'master' of https://github.com/boatbomber/rojo into headless-api
This commit is contained in:
commit
2e0f3dc011
42 changed files with 1054 additions and 426 deletions
|
|
@ -1 +1 @@
|
|||
7.7.0-prealpha
|
||||
7.7.0-rc.1
|
||||
|
|
@ -1,139 +1,10 @@
|
|||
-- Thanks to Tiffany352 for this base64 implementation!
|
||||
|
||||
local floor = math.floor
|
||||
local char = string.char
|
||||
|
||||
local function encodeBase64(str)
|
||||
local out = {}
|
||||
local nOut = 0
|
||||
local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
local strLen = #str
|
||||
|
||||
-- 3 octets become 4 hextets
|
||||
for i = 1, strLen - 2, 3 do
|
||||
local b1, b2, b3 = str:byte(i, i + 3)
|
||||
local word = b3 + b2 * 256 + b1 * 256 * 256
|
||||
|
||||
local h4 = word % 64 + 1
|
||||
word = floor(word / 64)
|
||||
local h3 = word % 64 + 1
|
||||
word = floor(word / 64)
|
||||
local h2 = word % 64 + 1
|
||||
word = floor(word / 64)
|
||||
local h1 = word % 64 + 1
|
||||
|
||||
out[nOut + 1] = alphabet:sub(h1, h1)
|
||||
out[nOut + 2] = alphabet:sub(h2, h2)
|
||||
out[nOut + 3] = alphabet:sub(h3, h3)
|
||||
out[nOut + 4] = alphabet:sub(h4, h4)
|
||||
nOut = nOut + 4
|
||||
end
|
||||
|
||||
local remainder = strLen % 3
|
||||
|
||||
if remainder == 2 then
|
||||
-- 16 input bits -> 3 hextets (2 full, 1 partial)
|
||||
local b1, b2 = str:byte(-2, -1)
|
||||
-- partial is 4 bits long, leaving 2 bits of zero padding ->
|
||||
-- offset = 4
|
||||
local word = b2 * 4 + b1 * 4 * 256
|
||||
|
||||
local h3 = word % 64 + 1
|
||||
word = floor(word / 64)
|
||||
local h2 = word % 64 + 1
|
||||
word = floor(word / 64)
|
||||
local h1 = word % 64 + 1
|
||||
|
||||
out[nOut + 1] = alphabet:sub(h1, h1)
|
||||
out[nOut + 2] = alphabet:sub(h2, h2)
|
||||
out[nOut + 3] = alphabet:sub(h3, h3)
|
||||
out[nOut + 4] = "="
|
||||
elseif remainder == 1 then
|
||||
-- 8 input bits -> 2 hextets (2 full, 1 partial)
|
||||
local b1 = str:byte(-1, -1)
|
||||
-- partial is 2 bits long, leaving 4 bits of zero padding ->
|
||||
-- offset = 16
|
||||
local word = b1 * 16
|
||||
|
||||
local h2 = word % 64 + 1
|
||||
word = floor(word / 64)
|
||||
local h1 = word % 64 + 1
|
||||
|
||||
out[nOut + 1] = alphabet:sub(h1, h1)
|
||||
out[nOut + 2] = alphabet:sub(h2, h2)
|
||||
out[nOut + 3] = "="
|
||||
out[nOut + 4] = "="
|
||||
end
|
||||
-- if the remainder is 0, then no work is needed
|
||||
|
||||
return table.concat(out, "")
|
||||
end
|
||||
|
||||
local function decodeBase64(str)
|
||||
local out = {}
|
||||
local nOut = 0
|
||||
local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
local strLen = #str
|
||||
local acc = 0
|
||||
local nAcc = 0
|
||||
|
||||
local alphabetLut = {}
|
||||
for i = 1, #alphabet do
|
||||
alphabetLut[alphabet:sub(i, i)] = i - 1
|
||||
end
|
||||
|
||||
-- 4 hextets become 3 octets
|
||||
for i = 1, strLen do
|
||||
local ch = str:sub(i, i)
|
||||
local byte = alphabetLut[ch]
|
||||
if byte then
|
||||
acc = acc * 64 + byte
|
||||
nAcc = nAcc + 1
|
||||
end
|
||||
|
||||
if nAcc == 4 then
|
||||
local b3 = acc % 256
|
||||
acc = floor(acc / 256)
|
||||
local b2 = acc % 256
|
||||
acc = floor(acc / 256)
|
||||
local b1 = acc % 256
|
||||
|
||||
out[nOut + 1] = char(b1)
|
||||
out[nOut + 2] = char(b2)
|
||||
out[nOut + 3] = char(b3)
|
||||
nOut = nOut + 3
|
||||
nAcc = 0
|
||||
acc = 0
|
||||
end
|
||||
end
|
||||
|
||||
if nAcc == 3 then
|
||||
-- 3 hextets -> 16 bit output
|
||||
acc = acc * 64
|
||||
acc = floor(acc / 256)
|
||||
local b2 = acc % 256
|
||||
acc = floor(acc / 256)
|
||||
local b1 = acc % 256
|
||||
|
||||
out[nOut + 1] = char(b1)
|
||||
out[nOut + 2] = char(b2)
|
||||
elseif nAcc == 2 then
|
||||
-- 2 hextets -> 8 bit output
|
||||
acc = acc * 64
|
||||
acc = floor(acc / 256)
|
||||
acc = acc * 64
|
||||
acc = floor(acc / 256)
|
||||
local b1 = acc % 256
|
||||
|
||||
out[nOut + 1] = char(b1)
|
||||
elseif nAcc == 1 then
|
||||
error("Base64 has invalid length")
|
||||
end
|
||||
|
||||
return table.concat(out, "")
|
||||
end
|
||||
local EncodingService = game:GetService("EncodingService")
|
||||
|
||||
return {
|
||||
decode = decodeBase64,
|
||||
encode = encodeBase64,
|
||||
decode = function(input: string)
|
||||
return buffer.tostring(EncodingService:Base64Decode(buffer.fromstring(input)))
|
||||
end,
|
||||
encode = function(input: string)
|
||||
return buffer.tostring(EncodingService:Base64Encode(buffer.fromstring(input)))
|
||||
end,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -208,4 +208,30 @@ return {
|
|||
end,
|
||||
},
|
||||
},
|
||||
StyleRule = {
|
||||
PropertiesSerialize = {
|
||||
read = function(instance: StyleRule)
|
||||
return true, instance:GetProperties()
|
||||
end,
|
||||
write = function(instance: StyleRule, _, value: { [any]: any })
|
||||
if typeof(value) ~= "table" then
|
||||
return false, Error.new(Error.Kind.CannotParseBinaryString)
|
||||
end
|
||||
|
||||
local existing = instance:GetProperties()
|
||||
|
||||
for itemName, itemValue in pairs(value) do
|
||||
instance:SetProperty(itemName, itemValue)
|
||||
end
|
||||
|
||||
for existingItemName in pairs(existing) do
|
||||
if value[existingItemName] == nil then
|
||||
instance:SetProperty(existingItemName, nil)
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -290,31 +290,39 @@ function ApiContext:open(id)
|
|||
end
|
||||
|
||||
function ApiContext:serialize(ids: { string })
|
||||
local url = ("%s/api/serialize/%s"):format(self.__baseUrl, table.concat(ids, ","))
|
||||
local url = ("%s/api/serialize"):format(self.__baseUrl)
|
||||
local request_body = Http.jsonEncode({ sessionId = self.__sessionId, ids = ids })
|
||||
|
||||
return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
|
||||
if body.sessionId ~= self.__sessionId then
|
||||
return Promise.reject("Server changed ID")
|
||||
end
|
||||
return Http.post(url, request_body)
|
||||
:andThen(rejectFailedRequests)
|
||||
:andThen(Http.Response.json)
|
||||
:andThen(function(response_body)
|
||||
if response_body.sessionId ~= self.__sessionId then
|
||||
return Promise.reject("Server changed ID")
|
||||
end
|
||||
|
||||
assert(validateApiSerialize(body))
|
||||
assert(validateApiSerialize(response_body))
|
||||
|
||||
return body
|
||||
end)
|
||||
return response_body
|
||||
end)
|
||||
end
|
||||
|
||||
function ApiContext:refPatch(ids: { string })
|
||||
local url = ("%s/api/ref-patch/%s"):format(self.__baseUrl, table.concat(ids, ","))
|
||||
local url = ("%s/api/ref-patch"):format(self.__baseUrl)
|
||||
local request_body = Http.jsonEncode({ sessionId = self.__sessionId, ids = ids })
|
||||
|
||||
return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
|
||||
if body.sessionId ~= self.__sessionId then
|
||||
return Promise.reject("Server changed ID")
|
||||
end
|
||||
return Http.post(url, request_body)
|
||||
:andThen(rejectFailedRequests)
|
||||
:andThen(Http.Response.json)
|
||||
:andThen(function(response_body)
|
||||
if response_body.sessionId ~= self.__sessionId then
|
||||
return Promise.reject("Server changed ID")
|
||||
end
|
||||
|
||||
assert(validateApiRefPatch(body))
|
||||
assert(validateApiRefPatch(response_body))
|
||||
|
||||
return body
|
||||
end)
|
||||
return response_body
|
||||
end)
|
||||
end
|
||||
|
||||
return ApiContext
|
||||
|
|
|
|||
|
|
@ -19,9 +19,15 @@ local FullscreenNotification = Roact.Component:extend("FullscreeFullscreenNotifi
|
|||
function FullscreenNotification:init()
|
||||
self.transparency, self.setTransparency = Roact.createBinding(0)
|
||||
self.lifetime = self.props.timeout
|
||||
self.dismissed = false
|
||||
end
|
||||
|
||||
function FullscreenNotification:dismiss()
|
||||
if self.dismissed then
|
||||
return
|
||||
end
|
||||
self.dismissed = true
|
||||
|
||||
if self.props.onClose then
|
||||
self.props.onClose()
|
||||
end
|
||||
|
|
@ -59,7 +65,7 @@ function FullscreenNotification:didMount()
|
|||
end
|
||||
|
||||
function FullscreenNotification:willUnmount()
|
||||
if self.timeout and coroutine.status(self.timeout) ~= "dead" then
|
||||
if self.timeout and coroutine.status(self.timeout) == "suspended" then
|
||||
task.cancel(self.timeout)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ function Notification:init()
|
|||
self.binding = bindingUtil.fromMotor(self.motor)
|
||||
|
||||
self.lifetime = self.props.timeout
|
||||
self.dismissed = false
|
||||
|
||||
self.motor:onStep(function(value)
|
||||
if value <= 0 and self.props.onClose then
|
||||
|
|
@ -35,6 +36,11 @@ function Notification:init()
|
|||
end
|
||||
|
||||
function Notification:dismiss()
|
||||
if self.dismissed then
|
||||
return
|
||||
end
|
||||
self.dismissed = true
|
||||
|
||||
self.motor:setGoal(Flipper.Spring.new(0, {
|
||||
frequency = 5,
|
||||
dampingRatio = 1,
|
||||
|
|
@ -76,7 +82,7 @@ function Notification:didMount()
|
|||
end
|
||||
|
||||
function Notification:willUnmount()
|
||||
if self.timeout and coroutine.status(self.timeout) ~= "dead" then
|
||||
if self.timeout and coroutine.status(self.timeout) == "suspended" then
|
||||
task.cancel(self.timeout)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -391,6 +391,19 @@ function App:setPriorSyncInfo(host: string, port: string, projectName: string)
|
|||
Settings:set("priorEndpoints", priorSyncInfos)
|
||||
end
|
||||
|
||||
function App:forgetPriorSyncInfo()
|
||||
local priorSyncInfos = Settings:get("priorEndpoints")
|
||||
if not priorSyncInfos then
|
||||
priorSyncInfos = {}
|
||||
end
|
||||
|
||||
local id = tostring(game.PlaceId)
|
||||
priorSyncInfos[id] = nil
|
||||
Log.trace("Erased last used endpoint for {}", game.PlaceId)
|
||||
|
||||
Settings:set("priorEndpoints", priorSyncInfos)
|
||||
end
|
||||
|
||||
function App:getHostAndPort(): (string, string)
|
||||
local host = self.host:getValue()
|
||||
local port = self.port:getValue()
|
||||
|
|
@ -578,7 +591,8 @@ function App:checkSyncReminder()
|
|||
self:findActiveServer()
|
||||
:andThen(function(serverInfo, host, port)
|
||||
self:sendSyncReminder(
|
||||
`Project '{serverInfo.projectName}' is serving at {host}:{port}.\nWould you like to connect?`
|
||||
`Project '{serverInfo.projectName}' is serving at {host}:{port}.\nWould you like to connect?`,
|
||||
{ "Connect", "Dismiss" }
|
||||
)
|
||||
end)
|
||||
:catch(function()
|
||||
|
|
@ -589,7 +603,8 @@ function App:checkSyncReminder()
|
|||
|
||||
local timeSinceSync = timeUtil.elapsedToText(os.time() - priorSyncInfo.timestamp)
|
||||
self:sendSyncReminder(
|
||||
`You synced project '{priorSyncInfo.projectName}' to this place {timeSinceSync}.\nDid you mean to run 'rojo serve' and then connect?`
|
||||
`You synced project '{priorSyncInfo.projectName}' to this place {timeSinceSync}.\nDid you mean to run 'rojo serve' and then connect?`,
|
||||
{ "Connect", "Forget", "Dismiss" }
|
||||
)
|
||||
end
|
||||
end)
|
||||
|
|
@ -629,12 +644,16 @@ function App:stopSyncReminderPolling()
|
|||
end
|
||||
end
|
||||
|
||||
function App:sendSyncReminder(message: string)
|
||||
function App:sendSyncReminder(message: string, shownActions: { string })
|
||||
local syncReminderMode = Settings:get("syncReminderMode")
|
||||
if syncReminderMode == "None" then
|
||||
return
|
||||
end
|
||||
|
||||
local connectIndex = table.find(shownActions, "Connect")
|
||||
local forgetIndex = table.find(shownActions, "Forget")
|
||||
local dismissIndex = table.find(shownActions, "Dismiss")
|
||||
|
||||
self.dismissSyncReminder = self:addNotification({
|
||||
text = message,
|
||||
timeout = 120,
|
||||
|
|
@ -643,24 +662,39 @@ function App:sendSyncReminder(message: string)
|
|||
self.dismissSyncReminder = nil
|
||||
end,
|
||||
actions = {
|
||||
Connect = {
|
||||
text = "Connect",
|
||||
style = "Solid",
|
||||
layoutOrder = 1,
|
||||
onClick = function()
|
||||
self:startSession()
|
||||
end,
|
||||
},
|
||||
Dismiss = {
|
||||
text = "Dismiss",
|
||||
style = "Bordered",
|
||||
layoutOrder = 2,
|
||||
onClick = function()
|
||||
-- If the user dismisses the reminder,
|
||||
-- then we don't need to remind them again
|
||||
self:stopSyncReminderPolling()
|
||||
end,
|
||||
},
|
||||
Connect = if connectIndex
|
||||
then {
|
||||
text = "Connect",
|
||||
style = "Solid",
|
||||
layoutOrder = connectIndex,
|
||||
onClick = function()
|
||||
self:startSession()
|
||||
end,
|
||||
}
|
||||
else nil,
|
||||
Forget = if forgetIndex
|
||||
then {
|
||||
text = "Forget",
|
||||
style = "Bordered",
|
||||
layoutOrder = forgetIndex,
|
||||
onClick = function()
|
||||
-- The user doesn't want to be reminded again about this sync
|
||||
self:forgetPriorSyncInfo()
|
||||
end,
|
||||
}
|
||||
else nil,
|
||||
Dismiss = if dismissIndex
|
||||
then {
|
||||
text = "Dismiss",
|
||||
style = "Bordered",
|
||||
layoutOrder = dismissIndex,
|
||||
onClick = function()
|
||||
-- If the user dismisses the reminder,
|
||||
-- then we don't need to remind them again
|
||||
self:stopSyncReminderPolling()
|
||||
end,
|
||||
}
|
||||
else nil,
|
||||
},
|
||||
})
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue