mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-22 04:51:47 +00:00
Merge branch 'master' of https://github.com/boatbomber/rojo into headless-api
This commit is contained in:
commit
1ff018a2ce
35 changed files with 571 additions and 212 deletions
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
|
|
@ -116,7 +116,7 @@ jobs:
|
||||||
version: 'v0.2.6'
|
version: 'v0.2.6'
|
||||||
|
|
||||||
- name: Build Release
|
- name: Build Release
|
||||||
run: cargo build --release --locked --verbose
|
run: cargo build --release --locked --verbose --target ${{ matrix.target }}
|
||||||
env:
|
env:
|
||||||
# Build into a known directory so we can find our build artifact more
|
# Build into a known directory so we can find our build artifact more
|
||||||
# easily.
|
# easily.
|
||||||
|
|
@ -132,11 +132,11 @@ jobs:
|
||||||
mkdir staging
|
mkdir staging
|
||||||
|
|
||||||
if [ "${{ matrix.host }}" = "windows" ]; then
|
if [ "${{ matrix.host }}" = "windows" ]; then
|
||||||
cp "output/release/$BIN.exe" staging/
|
cp "output/${{ matrix.target }}/release/$BIN.exe" staging/
|
||||||
cd staging
|
cd staging
|
||||||
7z a ../release.zip *
|
7z a ../release.zip *
|
||||||
else
|
else
|
||||||
cp "output/release/$BIN" staging/
|
cp "output/${{ matrix.target }}/release/$BIN" staging/
|
||||||
cd staging
|
cd staging
|
||||||
zip ../release.zip *
|
zip ../release.zip *
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
10
.gitmodules
vendored
10
.gitmodules
vendored
|
|
@ -1,15 +1,15 @@
|
||||||
[submodule "plugin/Packages/Roact"]
|
[submodule "plugin/Packages/Roact"]
|
||||||
path = plugin/Packages/Roact
|
path = plugin/Packages/Roact
|
||||||
url = git@github.com:roblox/roact
|
url = https://github.com/roblox/roact.git
|
||||||
[submodule "plugin/Packages/Flipper"]
|
[submodule "plugin/Packages/Flipper"]
|
||||||
path = plugin/Packages/Flipper
|
path = plugin/Packages/Flipper
|
||||||
url = git@github.com:reselim/flipper
|
url = https://github.com/reselim/flipper.git
|
||||||
[submodule "plugin/Packages/Promise"]
|
[submodule "plugin/Packages/Promise"]
|
||||||
path = plugin/Packages/Promise
|
path = plugin/Packages/Promise
|
||||||
url = git@github.com:evaera/roblox-lua-promise
|
url = https://github.com/evaera/roblox-lua-promise.git
|
||||||
[submodule "plugin/Packages/t"]
|
[submodule "plugin/Packages/t"]
|
||||||
path = plugin/Packages/t
|
path = plugin/Packages/t
|
||||||
url = git@github.com:osyrisrblx/t
|
url = https://github.com/osyrisrblx/t.git
|
||||||
[submodule "plugin/Packages/TestEZ"]
|
[submodule "plugin/Packages/TestEZ"]
|
||||||
path = plugin/Packages/TestEZ
|
path = plugin/Packages/TestEZ
|
||||||
url = git@github.com:roblox/testez
|
url = https://github.com/roblox/testez.git
|
||||||
|
|
|
||||||
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -3,9 +3,19 @@
|
||||||
## Unreleased Changes
|
## Unreleased Changes
|
||||||
* Added headless API for Studio companion plugins. ([#631])
|
* Added headless API for Studio companion plugins. ([#631])
|
||||||
* Significantly improved performance of `rojo sourcemap`. ([#668])
|
* Significantly improved performance of `rojo sourcemap`. ([#668])
|
||||||
|
* Fixed the diff visualizer of connected sessions. ([#674])
|
||||||
|
* Fixed disconnected session activity. ([#675])
|
||||||
|
* Skip confirming patches that contain only a datamodel name change. ([#688])
|
||||||
|
* Added sync reminder notification. ([#689])
|
||||||
|
* Added protection against syncing a model to a place. ([#691])
|
||||||
|
|
||||||
[#631]: https://github.com/rojo-rbx/rojo/pull/631
|
[#631]: https://github.com/rojo-rbx/rojo/pull/631
|
||||||
[#668]: https://github.com/rojo-rbx/rojo/pull/668
|
[#668]: https://github.com/rojo-rbx/rojo/pull/668
|
||||||
|
[#674]: https://github.com/rojo-rbx/rojo/pull/674
|
||||||
|
[#675]: https://github.com/rojo-rbx/rojo/pull/675
|
||||||
|
[#688]: https://github.com/rojo-rbx/rojo/pull/688
|
||||||
|
[#689]: https://github.com/rojo-rbx/rojo/pull/689
|
||||||
|
[#691]: https://github.com/rojo-rbx/rojo/pull/691
|
||||||
|
|
||||||
## [7.3.0] - April 22, 2023
|
## [7.3.0] - April 22, 2023
|
||||||
* Added `$attributes` to project format. ([#574])
|
* Added `$attributes` to project format. ([#574])
|
||||||
|
|
|
||||||
2
build.rs
2
build.rs
|
|
@ -54,7 +54,7 @@ fn main() -> Result<(), anyhow::Error> {
|
||||||
});
|
});
|
||||||
|
|
||||||
let out_path = Path::new(&out_dir).join("plugin.bincode");
|
let out_path = Path::new(&out_dir).join("plugin.bincode");
|
||||||
let out_file = File::create(&out_path)?;
|
let out_file = File::create(out_path)?;
|
||||||
|
|
||||||
bincode::serialize_into(out_file, &snapshot)?;
|
bincode::serialize_into(out_file, &snapshot)?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,12 @@ impl InMemoryFs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for InMemoryFs {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct InMemoryFsInner {
|
struct InMemoryFsInner {
|
||||||
entries: HashMap<PathBuf, Entry>,
|
entries: HashMap<PathBuf, Entry>,
|
||||||
|
|
|
||||||
|
|
@ -194,11 +194,8 @@ impl VfsInner {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commit_event(&mut self, event: &VfsEvent) -> io::Result<()> {
|
fn commit_event(&mut self, event: &VfsEvent) -> io::Result<()> {
|
||||||
match event {
|
if let VfsEvent::Remove(path) = event {
|
||||||
VfsEvent::Remove(path) => {
|
let _ = self.backend.unwatch(path);
|
||||||
let _ = self.backend.unwatch(&path);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -74,3 +74,9 @@ impl VfsBackend for NoopBackend {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for NoopBackend {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,3 +108,9 @@ impl VfsBackend for StdBackend {
|
||||||
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
|
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for StdBackend {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ function ApiContext.new(baseUrl)
|
||||||
__sessionId = nil,
|
__sessionId = nil,
|
||||||
__messageCursor = -1,
|
__messageCursor = -1,
|
||||||
__connected = true,
|
__connected = true,
|
||||||
|
__activeRequests = {},
|
||||||
}
|
}
|
||||||
|
|
||||||
return setmetatable(self, ApiContext)
|
return setmetatable(self, ApiContext)
|
||||||
|
|
@ -113,6 +114,11 @@ end
|
||||||
|
|
||||||
function ApiContext:disconnect()
|
function ApiContext:disconnect()
|
||||||
self.__connected = false
|
self.__connected = false
|
||||||
|
for request in self.__activeRequests do
|
||||||
|
Log.trace("Cancelling request {}", request)
|
||||||
|
request:cancel()
|
||||||
|
end
|
||||||
|
self.__activeRequests = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
function ApiContext:setMessageCursor(index)
|
function ApiContext:setMessageCursor(index)
|
||||||
|
|
@ -204,7 +210,7 @@ function ApiContext:retrieveMessages()
|
||||||
local url = ("%s/api/subscribe/%s"):format(self.__baseUrl, self.__messageCursor)
|
local url = ("%s/api/subscribe/%s"):format(self.__baseUrl, self.__messageCursor)
|
||||||
|
|
||||||
local function sendRequest()
|
local function sendRequest()
|
||||||
return Http.get(url)
|
local request = Http.get(url)
|
||||||
:catch(function(err)
|
:catch(function(err)
|
||||||
if err.type == Http.Error.Kind.Timeout then
|
if err.type == Http.Error.Kind.Timeout then
|
||||||
if self.__connected then
|
if self.__connected then
|
||||||
|
|
@ -216,6 +222,15 @@ function ApiContext:retrieveMessages()
|
||||||
|
|
||||||
return Promise.reject(err)
|
return Promise.reject(err)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
Log.trace("Tracking request {}", request)
|
||||||
|
self.__activeRequests[request] = true
|
||||||
|
|
||||||
|
return request:finally(function(...)
|
||||||
|
Log.trace("Cleaning up request {}", request)
|
||||||
|
self.__activeRequests[request] = nil
|
||||||
|
return ...
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return sendRequest()
|
return sendRequest()
|
||||||
|
|
|
||||||
|
|
@ -40,11 +40,6 @@ end
|
||||||
local DomLabel = Roact.Component:extend("DomLabel")
|
local DomLabel = Roact.Component:extend("DomLabel")
|
||||||
|
|
||||||
function DomLabel:init()
|
function DomLabel:init()
|
||||||
self.maxElementHeight = 0
|
|
||||||
if self.props.changeList then
|
|
||||||
self.maxElementHeight = math.clamp(#self.props.changeList * 30, 30, 30 * 6)
|
|
||||||
end
|
|
||||||
|
|
||||||
local initHeight = self.props.elementHeight:getValue()
|
local initHeight = self.props.elementHeight:getValue()
|
||||||
self.expanded = initHeight > 30
|
self.expanded = initHeight > 30
|
||||||
|
|
||||||
|
|
@ -118,7 +113,8 @@ function DomLabel:render()
|
||||||
Size = UDim2.new(1, 0, 1, 0),
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
[Roact.Event.Activated] = function()
|
[Roact.Event.Activated] = function()
|
||||||
self.expanded = not self.expanded
|
self.expanded = not self.expanded
|
||||||
self.motor:setGoal(Flipper.Spring.new((self.expanded and self.maxElementHeight or 0) + 30, {
|
local goalHeight = 30 + (if self.expanded then math.clamp(#self.props.changeList * 30, 30, 30 * 6) else 0)
|
||||||
|
self.motor:setGoal(Flipper.Spring.new(goalHeight, {
|
||||||
frequency = 5,
|
frequency = 5,
|
||||||
dampingRatio = 1,
|
dampingRatio = 1,
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
|
|
@ -99,18 +99,30 @@ function VirtualScroller:refresh()
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function VirtualScroller:didUpdate(previousProps)
|
||||||
|
if self.props.count ~= previousProps.count then
|
||||||
|
-- Items have changed, so we need to refresh
|
||||||
|
self:refresh()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function VirtualScroller:render()
|
function VirtualScroller:render()
|
||||||
local props, state = self.props, self.state
|
local props, state = self.props, self.state
|
||||||
|
|
||||||
local items = {}
|
local items = {}
|
||||||
for i = state.Start, state.End do
|
for i = state.Start, state.End do
|
||||||
|
local content = props.render(i)
|
||||||
|
if content == nil then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
items["Item" .. i] = e("Frame", {
|
items["Item" .. i] = e("Frame", {
|
||||||
LayoutOrder = i,
|
LayoutOrder = i,
|
||||||
Size = props.getHeightBinding(i):map(function(height)
|
Size = props.getHeightBinding(i):map(function(height)
|
||||||
return UDim2.new(1, 0, 0, height)
|
return UDim2.new(1, 0, 0, height)
|
||||||
end),
|
end),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}, props.render(i))
|
}, content)
|
||||||
end
|
end
|
||||||
|
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ local Packages = Rojo.Packages
|
||||||
|
|
||||||
local Roact = require(Packages.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
local Flipper = require(Packages.Flipper)
|
local Flipper = require(Packages.Flipper)
|
||||||
|
local Log = require(Packages.Log)
|
||||||
|
|
||||||
local bindingUtil = require(script.Parent.bindingUtil)
|
local bindingUtil = require(script.Parent.bindingUtil)
|
||||||
|
|
||||||
|
|
@ -14,6 +15,7 @@ local Theme = require(Plugin.App.Theme)
|
||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
|
|
||||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
|
local TextButton = require(Plugin.App.Components.TextButton)
|
||||||
|
|
||||||
local baseClock = DateTime.now().UnixTimestampMillis
|
local baseClock = DateTime.now().UnixTimestampMillis
|
||||||
|
|
||||||
|
|
@ -28,10 +30,8 @@ function Notification:init()
|
||||||
self.lifetime = self.props.timeout
|
self.lifetime = self.props.timeout
|
||||||
|
|
||||||
self.motor:onStep(function(value)
|
self.motor:onStep(function(value)
|
||||||
if value <= 0 then
|
if value <= 0 and self.props.onClose then
|
||||||
if self.props.onClose then
|
self.props.onClose()
|
||||||
self.props.onClose()
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
@ -86,10 +86,9 @@ function Notification:willUnmount()
|
||||||
end
|
end
|
||||||
|
|
||||||
function Notification:render()
|
function Notification:render()
|
||||||
local time = DateTime.fromUnixTimestampMillis(self.props.timestamp)
|
local transparency = self.binding:map(function(value)
|
||||||
local infoText =
|
return 1 - value
|
||||||
time:FormatLocalTime("LTS", "en-us") ..
|
end)
|
||||||
(if self.props.source then " • From: " .. self.props.source else "")
|
|
||||||
|
|
||||||
local textBounds = TextService:GetTextSize(
|
local textBounds = TextService:GetTextSize(
|
||||||
self.props.text,
|
self.props.text,
|
||||||
|
|
@ -98,23 +97,43 @@ function Notification:render()
|
||||||
Vector2.new(350, 700)
|
Vector2.new(350, 700)
|
||||||
)
|
)
|
||||||
|
|
||||||
local infoTextBounds = TextService:GetTextSize(
|
local actionButtons = {}
|
||||||
infoText,
|
local buttonsX = 0
|
||||||
12,
|
if self.props.actions then
|
||||||
Enum.Font.Gotham,
|
local count = 0
|
||||||
Vector2.new(350, 14)
|
for key, action in self.props.actions do
|
||||||
)
|
actionButtons[key] = e(TextButton, {
|
||||||
|
text = action.text,
|
||||||
|
style = action.style,
|
||||||
|
onClick = function()
|
||||||
|
local success, err = pcall(action.onClick, self)
|
||||||
|
if not success then
|
||||||
|
Log.warn("Error in notification action: " .. tostring(err))
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
layoutOrder = -action.layoutOrder,
|
||||||
|
transparency = transparency,
|
||||||
|
})
|
||||||
|
|
||||||
local textWidth = math.max(infoTextBounds.X, textBounds.X)
|
buttonsX += TextService:GetTextSize(
|
||||||
|
action.text, 18, Enum.Font.GothamMedium,
|
||||||
|
Vector2.new(math.huge, math.huge)
|
||||||
|
).X + 30
|
||||||
|
|
||||||
local transparency = self.binding:map(function(value)
|
count += 1
|
||||||
return 1 - value
|
end
|
||||||
end)
|
|
||||||
|
buttonsX += (count - 1) * 5
|
||||||
|
end
|
||||||
|
|
||||||
|
local paddingY, logoSize = 20, 32
|
||||||
|
local actionsY = if self.props.actions then 35 else 0
|
||||||
|
local contentX = math.max(textBounds.X, buttonsX)
|
||||||
|
|
||||||
local size = self.binding:map(function(value)
|
local size = self.binding:map(function(value)
|
||||||
return UDim2.fromOffset(
|
return UDim2.fromOffset(
|
||||||
(35+40+textWidth)*value,
|
(35 + 40 + contentX) * value,
|
||||||
math.max(16+20+textBounds.Y, 54)
|
5 + actionsY + paddingY + math.max(logoSize, textBounds.Y)
|
||||||
)
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -134,18 +153,18 @@ function Notification:render()
|
||||||
transparency = transparency,
|
transparency = transparency,
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
}, {
|
}, {
|
||||||
TextContainer = e("Frame", {
|
Contents = e("Frame", {
|
||||||
Size = UDim2.new(1, 0, 1, -20),
|
Size = UDim2.new(0, 35 + contentX, 1, -paddingY),
|
||||||
Position = UDim2.new(0, 0, 0, 10),
|
Position = UDim2.new(0, 0, 0, paddingY / 2),
|
||||||
BackgroundTransparency = 1
|
BackgroundTransparency = 1
|
||||||
}, {
|
}, {
|
||||||
Logo = e("ImageLabel", {
|
Logo = e("ImageLabel", {
|
||||||
ImageTransparency = transparency,
|
ImageTransparency = transparency,
|
||||||
Image = if self.props.thirdParty then Assets.Images.ThirdPartyPlugin else Assets.Images.PluginButton,
|
Image = if self.props.thirdParty then Assets.Images.ThirdPartyPlugin else Assets.Images.PluginButton,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Size = UDim2.new(0, 32, 0, 32),
|
Size = UDim2.new(0, logoSize, 0, logoSize),
|
||||||
Position = UDim2.new(0, 0, 0.5, 0),
|
Position = UDim2.new(0, 0, 0, 0),
|
||||||
AnchorPoint = Vector2.new(0, 0.5),
|
AnchorPoint = Vector2.new(0, 0),
|
||||||
}),
|
}),
|
||||||
Message = e("TextLabel", {
|
Message = e("TextLabel", {
|
||||||
Text = self.props.text,
|
Text = self.props.text,
|
||||||
|
|
@ -162,20 +181,21 @@ function Notification:render()
|
||||||
LayoutOrder = 1,
|
LayoutOrder = 1,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}),
|
}),
|
||||||
Info = e("TextLabel", {
|
Actions = if self.props.actions then e("Frame", {
|
||||||
Text = infoText,
|
Size = UDim2.new(1, -40, 0, 35),
|
||||||
Font = Enum.Font.Gotham,
|
Position = UDim2.new(1, 0, 1, 0),
|
||||||
TextSize = 12,
|
AnchorPoint = Vector2.new(1, 1),
|
||||||
TextColor3 = theme.Notification.InfoColor,
|
|
||||||
TextTransparency = transparency,
|
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
|
||||||
|
|
||||||
Size = UDim2.new(1, -38, 0, 14),
|
|
||||||
Position = UDim2.new(0, 38, 1, -14),
|
|
||||||
|
|
||||||
LayoutOrder = 1,
|
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}),
|
}, {
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
Padding = UDim.new(0, 5),
|
||||||
|
}),
|
||||||
|
Buttons = Roact.createFragment(actionButtons),
|
||||||
|
}) else nil,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Padding = e("UIPadding", {
|
Padding = e("UIPadding", {
|
||||||
|
|
@ -192,19 +212,20 @@ local Notifications = Roact.Component:extend("Notifications")
|
||||||
function Notifications:render()
|
function Notifications:render()
|
||||||
local notifs = {}
|
local notifs = {}
|
||||||
|
|
||||||
for index, notif in ipairs(self.props.notifications) do
|
for id, notif in self.props.notifications do
|
||||||
local props = {
|
notifs["NotifID_" .. id] = e(Notification, {
|
||||||
soundPlayer = self.props.soundPlayer,
|
soundPlayer = self.props.soundPlayer,
|
||||||
|
text = notif.text,
|
||||||
|
timestamp = notif.timestamp,
|
||||||
|
timeout = notif.timeout,
|
||||||
|
actions = notif.actions,
|
||||||
|
source = notif.source,
|
||||||
|
thirdParty = notif.thirdParty,
|
||||||
layoutOrder = (notif.timestamp - baseClock),
|
layoutOrder = (notif.timestamp - baseClock),
|
||||||
onClose = function()
|
onClose = function()
|
||||||
self.props.onClose(index)
|
self.props.onClose(id)
|
||||||
end,
|
end,
|
||||||
}
|
})
|
||||||
for key, value in notif do
|
|
||||||
props[key] = value
|
|
||||||
end
|
|
||||||
|
|
||||||
notifs[notif] = e(Notification, props)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return Roact.createFragment(notifs)
|
return Roact.createFragment(notifs)
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,11 @@ function timeSinceText(elapsed: number): string
|
||||||
|
|
||||||
local ageText = string.format("%d seconds ago", elapsed)
|
local ageText = string.format("%d seconds ago", elapsed)
|
||||||
|
|
||||||
for _,UnitData in ipairs(AGE_UNITS) do
|
for _, UnitData in ipairs(AGE_UNITS) do
|
||||||
local UnitSeconds, UnitName = UnitData[1], UnitData[2]
|
local UnitSeconds, UnitName = UnitData[1], UnitData[2]
|
||||||
if elapsed > UnitSeconds then
|
if elapsed > UnitSeconds then
|
||||||
local c = math.floor(elapsed/UnitSeconds)
|
local c = math.floor(elapsed / UnitSeconds)
|
||||||
ageText = string.format("%d %s%s ago", c, UnitName, c>1 and "s" or "")
|
ageText = string.format("%d %s%s ago", c, UnitName, c > 1 and "s" or "")
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -38,45 +38,53 @@ function timeSinceText(elapsed: number): string
|
||||||
return ageText
|
return ageText
|
||||||
end
|
end
|
||||||
|
|
||||||
local function ChangesDrawer(props)
|
local ChangesDrawer = Roact.Component:extend("ConnectedPage")
|
||||||
if props.rendered == false then
|
|
||||||
|
function ChangesDrawer:init()
|
||||||
|
-- Hold onto the serve session during the lifecycle of this component
|
||||||
|
-- so that it can still render during the fade out after disconnecting
|
||||||
|
self.serveSession = self.props.serveSession
|
||||||
|
end
|
||||||
|
|
||||||
|
function ChangesDrawer:render()
|
||||||
|
if self.props.rendered == false or self.serveSession == nil then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
return e(BorderedContainer, {
|
return e(BorderedContainer, {
|
||||||
transparency = props.transparency,
|
transparency = self.props.transparency,
|
||||||
size = props.height:map(function(y)
|
size = self.props.height:map(function(y)
|
||||||
return UDim2.new(1, 0, y, -180 * y)
|
return UDim2.new(1, 0, y, -180 * y)
|
||||||
end),
|
end),
|
||||||
position = UDim2.new(0, 0, 1, 0),
|
position = UDim2.new(0, 0, 1, 0),
|
||||||
anchorPoint = Vector2.new(0, 1),
|
anchorPoint = Vector2.new(0, 1),
|
||||||
layoutOrder = props.layoutOrder,
|
layoutOrder = self.props.layoutOrder,
|
||||||
}, {
|
}, {
|
||||||
Close = e(IconButton, {
|
Close = e(IconButton, {
|
||||||
icon = Assets.Images.Icons.Close,
|
icon = Assets.Images.Icons.Close,
|
||||||
iconSize = 24,
|
iconSize = 24,
|
||||||
color = theme.ConnectionDetails.DisconnectColor,
|
color = theme.ConnectionDetails.DisconnectColor,
|
||||||
transparency = props.transparency,
|
transparency = self.props.transparency,
|
||||||
|
|
||||||
position = UDim2.new(1, 0, 0, 0),
|
position = UDim2.new(1, 0, 0, 0),
|
||||||
anchorPoint = Vector2.new(1, 0),
|
anchorPoint = Vector2.new(1, 0),
|
||||||
|
|
||||||
onClick = props.onClose,
|
onClick = self.props.onClose,
|
||||||
}, {
|
}, {
|
||||||
Tip = e(Tooltip.Trigger, {
|
Tip = e(Tooltip.Trigger, {
|
||||||
text = "Close the patch visualizer"
|
text = "Close the patch visualizer",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
PatchVisualizer = e(PatchVisualizer, {
|
PatchVisualizer = e(PatchVisualizer, {
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
transparency = props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 3,
|
layoutOrder = 3,
|
||||||
|
|
||||||
columnVisibility = {true, false, true},
|
columnVisibility = { true, false, true },
|
||||||
patch = props.patchInfo:getValue().patch,
|
patch = self.props.patch,
|
||||||
instanceMap = props.serveSession.__instanceMap,
|
instanceMap = self.serveSession.__instanceMap,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
|
|
@ -91,7 +99,7 @@ local function ConnectionDetails(props)
|
||||||
}, {
|
}, {
|
||||||
TextContainer = e("Frame", {
|
TextContainer = e("Frame", {
|
||||||
Size = UDim2.new(1, 0, 1, 0),
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
BackgroundTransparency = 1
|
BackgroundTransparency = 1,
|
||||||
}, {
|
}, {
|
||||||
ProjectName = e("TextLabel", {
|
ProjectName = e("TextLabel", {
|
||||||
Text = props.projectName,
|
Text = props.projectName,
|
||||||
|
|
@ -141,7 +149,7 @@ local function ConnectionDetails(props)
|
||||||
onClick = props.onDisconnect,
|
onClick = props.onDisconnect,
|
||||||
}, {
|
}, {
|
||||||
Tip = e(Tooltip.Trigger, {
|
Tip = e(Tooltip.Trigger, {
|
||||||
text = "Disconnect from the Rojo sync server"
|
text = "Disconnect from the Rojo sync server",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
@ -155,6 +163,18 @@ end
|
||||||
|
|
||||||
local ConnectedPage = Roact.Component:extend("ConnectedPage")
|
local ConnectedPage = Roact.Component:extend("ConnectedPage")
|
||||||
|
|
||||||
|
function ConnectedPage:getChangeInfoText()
|
||||||
|
local patchData = self.props.patchData
|
||||||
|
if patchData == nil then
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
|
||||||
|
local elapsed = os.time() - patchData.timestamp
|
||||||
|
local changes = PatchSet.countChanges(patchData.patch)
|
||||||
|
|
||||||
|
return string.format("<i>Synced %d change%s %s</i>", changes, changes == 1 and "" or "s", timeSinceText(elapsed))
|
||||||
|
end
|
||||||
|
|
||||||
function ConnectedPage:init()
|
function ConnectedPage:init()
|
||||||
self.changeDrawerMotor = Flipper.SingleMotor.new(0)
|
self.changeDrawerMotor = Flipper.SingleMotor.new(0)
|
||||||
self.changeDrawerHeight = bindingUtil.fromMotor(self.changeDrawerMotor)
|
self.changeDrawerHeight = bindingUtil.fromMotor(self.changeDrawerMotor)
|
||||||
|
|
@ -176,6 +196,34 @@ function ConnectedPage:init()
|
||||||
self:setState({
|
self:setState({
|
||||||
renderChanges = false,
|
renderChanges = false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
self.changeInfoText, self.setChangeInfoText = Roact.createBinding("")
|
||||||
|
|
||||||
|
self.changeInfoTextUpdater = task.defer(function()
|
||||||
|
while true do
|
||||||
|
self.setChangeInfoText(self:getChangeInfoText())
|
||||||
|
|
||||||
|
local elapsed = os.time() - self.props.patchData.timestamp
|
||||||
|
local updateInterval = 1
|
||||||
|
|
||||||
|
-- Update timestamp text as frequently as currently needed
|
||||||
|
for _, UnitData in ipairs(AGE_UNITS) do
|
||||||
|
local UnitSeconds = UnitData[1]
|
||||||
|
if elapsed >= UnitSeconds then
|
||||||
|
updateInterval = UnitSeconds
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
task.wait(updateInterval)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ConnectedPage:willUnmount()
|
||||||
|
if self.changeInfoTextUpdater then
|
||||||
|
task.cancel(self.changeInfoTextUpdater)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function ConnectedPage:render()
|
function ConnectedPage:render()
|
||||||
|
|
@ -208,15 +256,7 @@ function ConnectedPage:render()
|
||||||
}),
|
}),
|
||||||
|
|
||||||
ChangeInfo = e("TextButton", {
|
ChangeInfo = e("TextButton", {
|
||||||
Text = self.props.patchInfo:map(function(info)
|
Text = self.changeInfoText,
|
||||||
local changes = PatchSet.countChanges(info.patch)
|
|
||||||
return string.format(
|
|
||||||
"<i>Synced %d change%s %s</i>",
|
|
||||||
changes,
|
|
||||||
changes == 1 and "" or "s",
|
|
||||||
timeSinceText(os.time() - info.timestamp)
|
|
||||||
)
|
|
||||||
end),
|
|
||||||
Font = Enum.Font.Gotham,
|
Font = Enum.Font.Gotham,
|
||||||
TextSize = 14,
|
TextSize = 14,
|
||||||
TextWrapped = true,
|
TextWrapped = true,
|
||||||
|
|
@ -249,7 +289,7 @@ function ConnectedPage:render()
|
||||||
ChangesDrawer = e(ChangesDrawer, {
|
ChangesDrawer = e(ChangesDrawer, {
|
||||||
rendered = self.state.renderChanges,
|
rendered = self.state.renderChanges,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
patchInfo = self.props.patchInfo,
|
patch = self.props.patchData.patch,
|
||||||
serveSession = self.props.serveSession,
|
serveSession = self.props.serveSession,
|
||||||
height = self.changeDrawerHeight,
|
height = self.changeDrawerHeight,
|
||||||
layoutOrder = 4,
|
layoutOrder = 4,
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ function Setting:render()
|
||||||
LayoutOrder = self.props.layoutOrder,
|
LayoutOrder = self.props.layoutOrder,
|
||||||
ZIndex = -self.props.layoutOrder,
|
ZIndex = -self.props.layoutOrder,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
|
Visible = self.props.visible,
|
||||||
|
|
||||||
[Roact.Change.AbsoluteSize] = function(object)
|
[Roact.Change.AbsoluteSize] = function(object)
|
||||||
self.setContainerSize(object.AbsoluteSize)
|
self.setContainerSize(object.AbsoluteSize)
|
||||||
|
|
|
||||||
|
|
@ -88,19 +88,36 @@ function SettingsPage:render()
|
||||||
layoutOrder = 0,
|
layoutOrder = 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
OpenScriptsExternally = e(Setting, {
|
ShowNotifications = e(Setting, {
|
||||||
id = "openScriptsExternally",
|
id = "showNotifications",
|
||||||
name = "Open Scripts Externally",
|
name = "Show Notifications",
|
||||||
description = "Attempt to open scripts in an external editor",
|
description = "Popup notifications in viewport",
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 1,
|
layoutOrder = 1,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
SyncReminder = e(Setting, {
|
||||||
|
id = "syncReminder",
|
||||||
|
name = "Sync Reminder",
|
||||||
|
description = "Notify to sync when opening a place that has previously been synced",
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
visible = Settings:getBinding("showNotifications"),
|
||||||
|
layoutOrder = 2,
|
||||||
|
}),
|
||||||
|
|
||||||
|
PlaySounds = e(Setting, {
|
||||||
|
id = "playSounds",
|
||||||
|
name = "Play Sounds",
|
||||||
|
description = "Toggle sound effects",
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
layoutOrder = 3,
|
||||||
|
}),
|
||||||
|
|
||||||
Permissions = e(Setting, {
|
Permissions = e(Setting, {
|
||||||
name = "Third Party Permissions",
|
name = "Third Party Permissions",
|
||||||
description = "Manage permissions for third party plugins",
|
description = "Manage permissions for third party plugins",
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 2,
|
layoutOrder = 4,
|
||||||
customInput = e("TextButton", {
|
customInput = e("TextButton", {
|
||||||
Text = "",
|
Text = "",
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
|
|
@ -136,20 +153,12 @@ function SettingsPage:render()
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
ShowNotifications = e(Setting, {
|
OpenScriptsExternally = e(Setting, {
|
||||||
id = "showNotifications",
|
id = "openScriptsExternally",
|
||||||
name = "Show Notifications",
|
name = "Open Scripts Externally",
|
||||||
description = "Popup notifications in viewport",
|
description = "EXPERIMENTAL! Attempt to open scripts in an external editor",
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 3,
|
layoutOrder = 5,
|
||||||
}),
|
|
||||||
|
|
||||||
PlaySounds = e(Setting, {
|
|
||||||
id = "playSounds",
|
|
||||||
name = "Play Sounds",
|
|
||||||
description = "Toggle sound effects",
|
|
||||||
transparency = self.props.transparency,
|
|
||||||
layoutOrder = 4,
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
TwoWaySync = e(Setting, {
|
TwoWaySync = e(Setting, {
|
||||||
|
|
@ -157,7 +166,7 @@ function SettingsPage:render()
|
||||||
name = "Two-Way Sync",
|
name = "Two-Way Sync",
|
||||||
description = "EXPERIMENTAL! Editing files in Studio will sync them into the filesystem",
|
description = "EXPERIMENTAL! Editing files in Studio will sync them into the filesystem",
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 5,
|
layoutOrder = 6,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
LogLevel = e(Setting, {
|
LogLevel = e(Setting, {
|
||||||
|
|
@ -165,7 +174,7 @@ function SettingsPage:render()
|
||||||
name = "Log Level",
|
name = "Log Level",
|
||||||
description = "Plugin output verbosity level",
|
description = "Plugin output verbosity level",
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 6,
|
layoutOrder = 100,
|
||||||
|
|
||||||
options = invertedLevels,
|
options = invertedLevels,
|
||||||
showReset = Settings:getBinding("logLevel"):map(function(value)
|
showReset = Settings:getBinding("logLevel"):map(function(value)
|
||||||
|
|
@ -181,7 +190,7 @@ function SettingsPage:render()
|
||||||
name = "Typechecking",
|
name = "Typechecking",
|
||||||
description = "Toggle typechecking on the API surface",
|
description = "Toggle typechecking on the API surface",
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 7,
|
layoutOrder = 101,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Layout = e("UIListLayout", {
|
Layout = e("UIListLayout", {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
local Players = game:GetService("Players")
|
local Players = game:GetService("Players")
|
||||||
local ServerStorage = game:GetService("ServerStorage")
|
local ServerStorage = game:GetService("ServerStorage")
|
||||||
|
local RunService = game:GetService("RunService")
|
||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
|
|
@ -20,6 +21,7 @@ local HeadlessAPI = require(Plugin.HeadlessAPI)
|
||||||
local PatchSet = require(Plugin.PatchSet)
|
local PatchSet = require(Plugin.PatchSet)
|
||||||
local preloadAssets = require(Plugin.preloadAssets)
|
local preloadAssets = require(Plugin.preloadAssets)
|
||||||
local soundPlayer = require(Plugin.soundPlayer)
|
local soundPlayer = require(Plugin.soundPlayer)
|
||||||
|
local ignorePlaceIds = require(Plugin.ignorePlaceIds)
|
||||||
local Theme = require(script.Theme)
|
local Theme = require(script.Theme)
|
||||||
|
|
||||||
local Page = require(script.Page)
|
local Page = require(script.Page)
|
||||||
|
|
@ -55,17 +57,18 @@ function App:init()
|
||||||
self.host, self.setHost = Roact.createBinding(priorHost or "")
|
self.host, self.setHost = Roact.createBinding(priorHost or "")
|
||||||
self.port, self.setPort = Roact.createBinding(priorPort or "")
|
self.port, self.setPort = Roact.createBinding(priorPort or "")
|
||||||
|
|
||||||
self.patchInfo, self.setPatchInfo = Roact.createBinding({
|
|
||||||
patch = PatchSet.newEmpty(),
|
|
||||||
timestamp = os.time(),
|
|
||||||
})
|
|
||||||
self.confirmationBindable = Instance.new("BindableEvent")
|
self.confirmationBindable = Instance.new("BindableEvent")
|
||||||
self.confirmationEvent = self.confirmationBindable.Event
|
self.confirmationEvent = self.confirmationBindable.Event
|
||||||
|
self.notifId = 0
|
||||||
|
|
||||||
self:setState({
|
self:setState({
|
||||||
appStatus = AppStatus.NotConnected,
|
appStatus = AppStatus.NotConnected,
|
||||||
guiEnabled = false,
|
guiEnabled = false,
|
||||||
confirmData = {},
|
confirmData = {},
|
||||||
|
patchData = {
|
||||||
|
patch = PatchSet.newEmpty(),
|
||||||
|
timestamp = os.time(),
|
||||||
|
},
|
||||||
notifications = {},
|
notifications = {},
|
||||||
toolbarIcon = Assets.Images.PluginButton,
|
toolbarIcon = Assets.Images.PluginButton,
|
||||||
popups = {},
|
popups = {},
|
||||||
|
|
@ -114,47 +117,90 @@ function App:init()
|
||||||
-- selene: allow(global_usage)
|
-- selene: allow(global_usage)
|
||||||
_G.Rojo = self.readOnlyHeadlessAPI -- Expose headless to other plugins and command bar
|
_G.Rojo = self.readOnlyHeadlessAPI -- Expose headless to other plugins and command bar
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if
|
||||||
|
RunService:IsEdit()
|
||||||
|
and self.serveSession == nil
|
||||||
|
and Settings:get("syncReminder")
|
||||||
|
and self:getLastSyncTimestamp()
|
||||||
|
then
|
||||||
|
self:addNotification("You've previously synced this place. Would you like to reconnect?", 300, {
|
||||||
|
Connect = {
|
||||||
|
text = "Connect",
|
||||||
|
style = "Solid",
|
||||||
|
layoutOrder = 1,
|
||||||
|
onClick = function(notification)
|
||||||
|
notification:dismiss()
|
||||||
|
self:startSession()
|
||||||
|
end
|
||||||
|
},
|
||||||
|
Dismiss = {
|
||||||
|
text = "Dismiss",
|
||||||
|
style = "Bordered",
|
||||||
|
layoutOrder = 2,
|
||||||
|
onClick = function(notification)
|
||||||
|
notification:dismiss()
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:addNotification(text: string, timeout: number?)
|
function App:addNotification(text: string, timeout: number?, actions: { [string]: {text: string, style: string, layoutOrder: number, onClick: (any) -> ()} }?)
|
||||||
if not Settings:get("showNotifications") then
|
if not Settings:get("showNotifications") then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
self.notifId += 1
|
||||||
|
local id = self.notifId
|
||||||
|
|
||||||
local notifications = table.clone(self.state.notifications)
|
local notifications = table.clone(self.state.notifications)
|
||||||
table.insert(notifications, {
|
notifications[id] = {
|
||||||
text = text,
|
text = text,
|
||||||
timestamp = DateTime.now().UnixTimestampMillis,
|
timestamp = DateTime.now().UnixTimestampMillis,
|
||||||
timeout = timeout or 3,
|
timeout = timeout or 3,
|
||||||
})
|
actions = actions,
|
||||||
|
}
|
||||||
|
|
||||||
self:setState({
|
self:setState({
|
||||||
notifications = notifications,
|
notifications = notifications,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return function()
|
||||||
|
self:closeNotification(id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:addThirdPartyNotification(source: string, text: string, timeout: number?)
|
function App:addThirdPartyNotification(source: string, text: string, timeout: number?, actions: { [string]: {text: string, style: string, layoutOrder: number, onClick: (any) -> ()} }?)
|
||||||
if not Settings:get("showNotifications") then
|
if not Settings:get("showNotifications") then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
self.notifId += 1
|
||||||
|
local id = self.notifId
|
||||||
|
|
||||||
local notifications = table.clone(self.state.notifications)
|
local notifications = table.clone(self.state.notifications)
|
||||||
table.insert(notifications, {
|
notifications[id] = {
|
||||||
text = text,
|
text = text,
|
||||||
timestamp = DateTime.now().UnixTimestampMillis,
|
timestamp = DateTime.now().UnixTimestampMillis,
|
||||||
timeout = timeout or 3,
|
timeout = timeout or 3,
|
||||||
|
actions = actions,
|
||||||
thirdParty = true,
|
thirdParty = true,
|
||||||
source = source,
|
source = source,
|
||||||
})
|
}
|
||||||
|
|
||||||
self:setState({
|
self:setState({
|
||||||
notifications = notifications,
|
notifications = notifications,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return function()
|
||||||
|
self:closeNotification(id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:closeNotification(index: number)
|
function App:closeNotification(id: number)
|
||||||
local notifications = table.clone(self.state.notifications)
|
local notifications = table.clone(self.state.notifications)
|
||||||
table.remove(notifications, index)
|
notifications[id] = nil
|
||||||
|
|
||||||
self:setState({
|
self:setState({
|
||||||
notifications = notifications,
|
notifications = notifications,
|
||||||
|
|
@ -165,12 +211,28 @@ function App:getPriorEndpoint()
|
||||||
local priorEndpoints = Settings:get("priorEndpoints")
|
local priorEndpoints = Settings:get("priorEndpoints")
|
||||||
if not priorEndpoints then return end
|
if not priorEndpoints then return end
|
||||||
|
|
||||||
local place = priorEndpoints[tostring(game.PlaceId)]
|
local id = tostring(game.PlaceId)
|
||||||
|
if ignorePlaceIds[id] then return end
|
||||||
|
|
||||||
|
local place = priorEndpoints[id]
|
||||||
if not place then return end
|
if not place then return end
|
||||||
|
|
||||||
return place.host, place.port
|
return place.host, place.port
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function App:getLastSyncTimestamp()
|
||||||
|
local priorEndpoints = Settings:get("priorEndpoints")
|
||||||
|
if not priorEndpoints then return end
|
||||||
|
|
||||||
|
local id = tostring(game.PlaceId)
|
||||||
|
if ignorePlaceIds[id] then return end
|
||||||
|
|
||||||
|
local place = priorEndpoints[id]
|
||||||
|
if not place then return end
|
||||||
|
|
||||||
|
return place.timestamp
|
||||||
|
end
|
||||||
|
|
||||||
function App:setPriorEndpoint(host: string, port: string)
|
function App:setPriorEndpoint(host: string, port: string)
|
||||||
local priorEndpoints = Settings:get("priorEndpoints")
|
local priorEndpoints = Settings:get("priorEndpoints")
|
||||||
if not priorEndpoints then
|
if not priorEndpoints then
|
||||||
|
|
@ -185,17 +247,16 @@ function App:setPriorEndpoint(host: string, port: string)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if host == Config.defaultHost and port == Config.defaultPort then
|
local id = tostring(game.PlaceId)
|
||||||
-- Don't save default
|
if ignorePlaceIds[id] then return end
|
||||||
priorEndpoints[tostring(game.PlaceId)] = nil
|
|
||||||
else
|
priorEndpoints[id] = {
|
||||||
priorEndpoints[tostring(game.PlaceId)] = {
|
host = if host ~= Config.defaultHost then host else nil,
|
||||||
host = host ~= Config.defaultHost and host or nil,
|
port = if port ~= Config.defaultPort then port else nil,
|
||||||
port = port ~= Config.defaultPort and port or nil,
|
timestamp = os.time(),
|
||||||
timestamp = os.time(),
|
}
|
||||||
}
|
Log.trace("Saved last used endpoint for {}", game.PlaceId)
|
||||||
Log.trace("Saved last used endpoint for {}", game.PlaceId)
|
|
||||||
end
|
|
||||||
|
|
||||||
Settings:set("priorEndpoints", priorEndpoints)
|
Settings:set("priorEndpoints", priorEndpoints)
|
||||||
end
|
end
|
||||||
|
|
@ -330,21 +391,25 @@ function App:startSession(host: string?, port: string?)
|
||||||
|
|
||||||
local now = os.time()
|
local now = os.time()
|
||||||
|
|
||||||
local old = self.patchInfo:getValue()
|
local old = self.state.patchData
|
||||||
if now - old.timestamp < 2 then
|
if now - old.timestamp < 2 then
|
||||||
-- Patches that apply in the same second are
|
-- Patches that apply in the same second are
|
||||||
-- considered to be part of the same change for human clarity
|
-- considered to be part of the same change for human clarity
|
||||||
local merged = PatchSet.newEmpty()
|
local merged = PatchSet.newEmpty()
|
||||||
PatchSet.assign(merged, old.patch, patch)
|
PatchSet.assign(merged, old.patch, patch)
|
||||||
|
|
||||||
self.setPatchInfo({
|
self:setState({
|
||||||
patch = merged,
|
patchData = {
|
||||||
timestamp = now,
|
patch = merged,
|
||||||
|
timestamp = now,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
else
|
else
|
||||||
self.setPatchInfo({
|
self:setState({
|
||||||
patch = patch,
|
patchData = {
|
||||||
timestamp = now,
|
patch = patch,
|
||||||
|
timestamp = now,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
@ -404,9 +469,28 @@ function App:startSession(host: string?, port: string?)
|
||||||
|
|
||||||
serveSession:setConfirmCallback(function(instanceMap, patch, serverInfo)
|
serveSession:setConfirmCallback(function(instanceMap, patch, serverInfo)
|
||||||
if PatchSet.isEmpty(patch) then
|
if PatchSet.isEmpty(patch) then
|
||||||
|
Log.trace("Accepting patch without confirmation because it is empty")
|
||||||
return "Accept"
|
return "Accept"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- The datamodel name gets overwritten by Studio, making confirmation of it intrusive
|
||||||
|
-- and unnecessary. This special case allows it to be accepted without confirmation.
|
||||||
|
if
|
||||||
|
PatchSet.hasAdditions(patch) == false
|
||||||
|
and PatchSet.hasRemoves(patch) == false
|
||||||
|
and PatchSet.containsOnlyInstance(patch, instanceMap, game)
|
||||||
|
then
|
||||||
|
local datamodelUpdates = PatchSet.getUpdateForInstance(patch, instanceMap, game)
|
||||||
|
if
|
||||||
|
datamodelUpdates ~= nil
|
||||||
|
and next(datamodelUpdates.changedProperties) == nil
|
||||||
|
and datamodelUpdates.changedClassName == nil
|
||||||
|
then
|
||||||
|
Log.trace("Accepting patch without confirmation because it only contains a datamodel name change")
|
||||||
|
return "Accept"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
self:setState({
|
self:setState({
|
||||||
appStatus = AppStatus.Confirming,
|
appStatus = AppStatus.Confirming,
|
||||||
confirmData = {
|
confirmData = {
|
||||||
|
|
@ -431,16 +515,6 @@ function App:startSession(host: string?, port: string?)
|
||||||
serveSession:start()
|
serveSession:start()
|
||||||
|
|
||||||
self.serveSession = serveSession
|
self.serveSession = serveSession
|
||||||
|
|
||||||
task.defer(function()
|
|
||||||
while self.serveSession == serveSession do
|
|
||||||
-- Trigger rerender to update timestamp text
|
|
||||||
local patchInfo = table.clone(self.patchInfo:getValue())
|
|
||||||
self.setPatchInfo(patchInfo)
|
|
||||||
local elapsed = os.time() - patchInfo.timestamp
|
|
||||||
task.wait(elapsed < 60 and 1 or elapsed / 5)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:endSession()
|
function App:endSession()
|
||||||
|
|
@ -574,7 +648,7 @@ function App:render()
|
||||||
Connected = createPageElement(AppStatus.Connected, {
|
Connected = createPageElement(AppStatus.Connected, {
|
||||||
projectName = self.state.projectName,
|
projectName = self.state.projectName,
|
||||||
address = self.state.address,
|
address = self.state.address,
|
||||||
patchInfo = self.patchInfo,
|
patchData = self.state.patchData,
|
||||||
serveSession = self.serveSession,
|
serveSession = self.serveSession,
|
||||||
|
|
||||||
onDisconnect = function()
|
onDisconnect = function()
|
||||||
|
|
@ -631,7 +705,11 @@ function App:render()
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
RojoNotifications = e("ScreenGui", {}, {
|
RojoNotifications = e("ScreenGui", {
|
||||||
|
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||||
|
ResetOnSpawn = false,
|
||||||
|
DisplayOrder = 100,
|
||||||
|
}, {
|
||||||
layout = e("UIListLayout", {
|
layout = e("UIListLayout", {
|
||||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||||
|
|
@ -647,8 +725,8 @@ function App:render()
|
||||||
notifs = e(Notifications, {
|
notifs = e(Notifications, {
|
||||||
soundPlayer = self.props.soundPlayer,
|
soundPlayer = self.props.soundPlayer,
|
||||||
notifications = self.state.notifications,
|
notifications = self.state.notifications,
|
||||||
onClose = function(index)
|
onClose = function(id)
|
||||||
self:closeNotification(index)
|
self:closeNotification(id)
|
||||||
end,
|
end,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,113 @@ function PatchSet.hasUpdates(patchSet)
|
||||||
return next(patchSet.updated) ~= nil
|
return next(patchSet.updated) ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Tells whether the given PatchSet contains changes to the given instance id
|
||||||
|
]]
|
||||||
|
function PatchSet.containsId(patchSet, instanceMap, id)
|
||||||
|
if patchSet.added[id] ~= nil then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, idOrInstance in patchSet.removed do
|
||||||
|
local removedId = if Types.RbxId(idOrInstance) then idOrInstance else instanceMap.fromInstances[idOrInstance]
|
||||||
|
if removedId == id then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, update in patchSet.updated do
|
||||||
|
if update.id == id then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Tells whether the given PatchSet contains changes to the given instance.
|
||||||
|
If the given InstanceMap does not contain the instance, this function always returns false.
|
||||||
|
]]
|
||||||
|
function PatchSet.containsInstance(patchSet, instanceMap, instance)
|
||||||
|
local id = instanceMap.fromInstances[instance]
|
||||||
|
if id == nil then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
return PatchSet.containsId(patchSet, instanceMap, id)
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Tells whether the given PatchSet contains changes to nothing but the given instance id
|
||||||
|
]]
|
||||||
|
function PatchSet.containsOnlyId(patchSet, instanceMap, id)
|
||||||
|
if not PatchSet.containsId(patchSet, instanceMap, id) then
|
||||||
|
-- Patch doesn't contain the id at all
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
for addedId in patchSet.added do
|
||||||
|
if addedId ~= id then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, idOrInstance in patchSet.removed do
|
||||||
|
local removedId = if Types.RbxId(idOrInstance) then idOrInstance else instanceMap.fromInstances[idOrInstance]
|
||||||
|
if removedId ~= id then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, update in patchSet.updated do
|
||||||
|
if update.id ~= id then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Tells whether the given PatchSet contains changes to nothing but the given instance.
|
||||||
|
If the given InstanceMap does not contain the instance, this function always returns false.
|
||||||
|
]]
|
||||||
|
function PatchSet.containsOnlyInstance(patchSet, instanceMap, instance)
|
||||||
|
local id = instanceMap.fromInstances[instance]
|
||||||
|
if id == nil then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
return PatchSet.containsOnlyId(patchSet, instanceMap, id)
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Returns the update to the given instance id, or nil if there aren't any
|
||||||
|
]]
|
||||||
|
function PatchSet.getUpdateForId(patchSet, id)
|
||||||
|
for _, update in patchSet.updated do
|
||||||
|
if update.id == id then
|
||||||
|
return update
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Returns the update to the given instance, or nil if there aren't any.
|
||||||
|
If the given InstanceMap does not contain the instance, this function always returns nil.
|
||||||
|
]]
|
||||||
|
function PatchSet.getUpdateForInstance(patchSet, instanceMap, instance)
|
||||||
|
local id = instanceMap.fromInstances[instance]
|
||||||
|
if id == nil then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return PatchSet.getUpdateForId(patchSet, id)
|
||||||
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Tells whether the given PatchSets are equal.
|
Tells whether the given PatchSets are equal.
|
||||||
]]
|
]]
|
||||||
|
|
@ -150,7 +257,7 @@ function PatchSet.humanSummary(instanceMap, patchSet)
|
||||||
for _, idOrInstance in ipairs(patchSet.removed) do
|
for _, idOrInstance in ipairs(patchSet.removed) do
|
||||||
local instance, id
|
local instance, id
|
||||||
|
|
||||||
if type(idOrInstance) == "string" then
|
if Types.RbxId(idOrInstance) then
|
||||||
id = idOrInstance
|
id = idOrInstance
|
||||||
instance = instanceMap.fromIds[id]
|
instance = instanceMap.fromIds[id]
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -232,6 +232,22 @@ function ServeSession:__initialSync(serverInfo)
|
||||||
Log.error("Could not compute a diff to catch up to the Rojo server: {:#?}", catchUpPatch)
|
Log.error("Could not compute a diff to catch up to the Rojo server: {:#?}", catchUpPatch)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
for _, update in catchUpPatch.updated do
|
||||||
|
if
|
||||||
|
update.id == self.__instanceMap.fromInstances[game]
|
||||||
|
and update.changedClassName ~= nil
|
||||||
|
then
|
||||||
|
-- Non-place projects will try to update the classname of game from DataModel to
|
||||||
|
-- something like Folder, ModuleScript, etc. This would fail, so we exit with a clear
|
||||||
|
-- message instead of crashing.
|
||||||
|
return Promise.reject(
|
||||||
|
"Cannot sync a model as a place."
|
||||||
|
.. "\nEnsure Rojo is serving a project file that has a DataModel at the root of its tree and try again."
|
||||||
|
.. "\nSee project file docs: https://rojo.space/docs/v7/project-format/"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
Log.trace("Computed hydration patch: {:#?}", debugPatch(catchUpPatch))
|
Log.trace("Computed hydration patch: {:#?}", debugPatch(catchUpPatch))
|
||||||
|
|
||||||
local userDecision = "Accept"
|
local userDecision = "Accept"
|
||||||
|
|
@ -285,6 +301,8 @@ end
|
||||||
function ServeSession:__mainSyncLoop()
|
function ServeSession:__mainSyncLoop()
|
||||||
return self.__apiContext:retrieveMessages()
|
return self.__apiContext:retrieveMessages()
|
||||||
:andThen(function(messages)
|
:andThen(function(messages)
|
||||||
|
Log.trace("Serve session {} retrieved {} messages", tostring(self), #messages)
|
||||||
|
|
||||||
for _, message in ipairs(messages) do
|
for _, message in ipairs(messages) do
|
||||||
local unappliedPatch = self.__reconciler:applyPatch(message)
|
local unappliedPatch = self.__reconciler:applyPatch(message)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ local defaultSettings = {
|
||||||
openScriptsExternally = false,
|
openScriptsExternally = false,
|
||||||
twoWaySync = false,
|
twoWaySync = false,
|
||||||
showNotifications = true,
|
showNotifications = true,
|
||||||
|
syncReminder = true,
|
||||||
playSounds = true,
|
playSounds = true,
|
||||||
typecheckingEnabled = false,
|
typecheckingEnabled = false,
|
||||||
logLevel = "Info",
|
logLevel = "Info",
|
||||||
|
|
|
||||||
27
plugin/src/ignorePlaceIds.lua
Normal file
27
plugin/src/ignorePlaceIds.lua
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
--[[
|
||||||
|
These are place ids that will not have metadata saved for them,
|
||||||
|
such as last sync address or time. This is because they are not unique
|
||||||
|
so storing metadata for them does not make sense as these ids are reused.
|
||||||
|
--]]
|
||||||
|
|
||||||
|
return {
|
||||||
|
["0"] = true, -- Local file
|
||||||
|
["95206881"] = true, -- Baseplate
|
||||||
|
["6560363541"] = true, -- Classic Baseplate
|
||||||
|
["95206192"] = true, -- Flat Terrain
|
||||||
|
["13165709401"] = true, -- Modern City
|
||||||
|
["520390648"] = true, -- Village
|
||||||
|
["203810088"] = true, -- Castle
|
||||||
|
["366130569"] = true, -- Suburban
|
||||||
|
["215383192"] = true, -- Racing
|
||||||
|
["264719325"] = true, -- Pirate Island
|
||||||
|
["203812057"] = true, -- Obby
|
||||||
|
["379736082"] = true, -- Starting Place
|
||||||
|
["301530843"] = true, -- Line Runner
|
||||||
|
["92721754"] = true, -- Capture The Flag
|
||||||
|
["301529772"] = true, -- Team/FFA Arena
|
||||||
|
["203885589"] = true, -- Combat
|
||||||
|
["10275826693"] = true, -- Concert
|
||||||
|
["5353920686"] = true, -- Move It Simulator
|
||||||
|
["6936227200"] = true, -- Mansion Of Wonder
|
||||||
|
}
|
||||||
|
|
@ -251,7 +251,7 @@ fn on_vfs_event(
|
||||||
|
|
||||||
let mut current_path = path.as_path();
|
let mut current_path = path.as_path();
|
||||||
let affected_ids = loop {
|
let affected_ids = loop {
|
||||||
let ids = tree.get_ids_at_path(¤t_path);
|
let ids = tree.get_ids_at_path(current_path);
|
||||||
|
|
||||||
log::trace!("Path {} affects IDs {:?}", current_path.display(), ids);
|
log::trace!("Path {} affects IDs {:?}", current_path.display(), ids);
|
||||||
|
|
||||||
|
|
@ -267,7 +267,7 @@ fn on_vfs_event(
|
||||||
};
|
};
|
||||||
|
|
||||||
for id in affected_ids {
|
for id in affected_ids {
|
||||||
if let Some(patch) = compute_and_apply_changes(&mut tree, &vfs, id) {
|
if let Some(patch) = compute_and_apply_changes(&mut tree, vfs, id) {
|
||||||
if !patch.is_empty() {
|
if !patch.is_empty() {
|
||||||
applied_patches.push(patch);
|
applied_patches.push(patch);
|
||||||
}
|
}
|
||||||
|
|
@ -301,7 +301,7 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option<
|
||||||
// path still exists. We can generate a snapshot starting at
|
// path still exists. We can generate a snapshot starting at
|
||||||
// that path and use it as the source for our patch.
|
// that path and use it as the source for our patch.
|
||||||
|
|
||||||
let snapshot = match snapshot_from_vfs(&metadata.context, &vfs, &path) {
|
let snapshot = match snapshot_from_vfs(&metadata.context, vfs, path) {
|
||||||
Ok(snapshot) => snapshot,
|
Ok(snapshot) => snapshot,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("Snapshot error: {:?}", err);
|
log::error!("Snapshot error: {:?}", err);
|
||||||
|
|
@ -309,7 +309,7 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option<
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let patch_set = compute_patch_set(snapshot, &tree, id);
|
let patch_set = compute_patch_set(snapshot, tree, id);
|
||||||
apply_patch_set(tree, patch_set)
|
apply_patch_set(tree, patch_set)
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
|
|
@ -337,10 +337,10 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option<
|
||||||
|
|
||||||
let snapshot_result = snapshot_project_node(
|
let snapshot_result = snapshot_project_node(
|
||||||
&metadata.context,
|
&metadata.context,
|
||||||
&project_path,
|
project_path,
|
||||||
instance_name,
|
instance_name,
|
||||||
project_node,
|
project_node,
|
||||||
&vfs,
|
vfs,
|
||||||
parent_class.as_ref().map(|name| name.as_str()),
|
parent_class.as_ref().map(|name| name.as_str()),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -352,7 +352,7 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option<
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let patch_set = compute_patch_set(snapshot, &tree, id);
|
let patch_set = compute_patch_set(snapshot, tree, id);
|
||||||
apply_patch_set(tree, patch_set)
|
apply_patch_set(tree, patch_set)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ impl FmtProjectCommand {
|
||||||
let serialized = serde_json::to_string_pretty(&project)
|
let serialized = serde_json::to_string_pretty(&project)
|
||||||
.context("could not re-encode project file as JSON")?;
|
.context("could not re-encode project file as JSON")?;
|
||||||
|
|
||||||
fs_err::write(&project.file_location, &serialized)
|
fs_err::write(&project.file_location, serialized)
|
||||||
.context("could not write back to project file")?;
|
.context("could not write back to project file")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,7 @@ fn try_git_init(path: &Path, git_ignore: &str) -> Result<(), anyhow::Error> {
|
||||||
/// already inside a Git repository.
|
/// already inside a Git repository.
|
||||||
fn should_git_init(path: &Path) -> bool {
|
fn should_git_init(path: &Path) -> bool {
|
||||||
let result = Command::new("git")
|
let result = Command::new("git")
|
||||||
.args(&["rev-parse", "--is-inside-work-tree"])
|
.args(["rev-parse", "--is-inside-work-tree"])
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::null())
|
.stderr(Stdio::null())
|
||||||
.current_dir(path)
|
.current_dir(path)
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ impl ServeCommand {
|
||||||
|
|
||||||
let vfs = Vfs::new_default();
|
let vfs = Vfs::new_default();
|
||||||
|
|
||||||
let session = Arc::new(ServeSession::new(vfs, &project_path)?);
|
let session = Arc::new(ServeSession::new(vfs, project_path)?);
|
||||||
|
|
||||||
let ip = self
|
let ip = self
|
||||||
.address
|
.address
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@ fn do_upload_open_cloud(
|
||||||
|
|
||||||
log::debug!("Uploading to Roblox...");
|
log::debug!("Uploading to Roblox...");
|
||||||
let response = client
|
let response = client
|
||||||
.post(&url)
|
.post(url)
|
||||||
.header("x-api-key", api_key)
|
.header("x-api-key", api_key)
|
||||||
.header(CONTENT_TYPE, "application/xml")
|
.header(CONTENT_TYPE, "application/xml")
|
||||||
.header(ACCEPT, "application/json")
|
.header(ACCEPT, "application/json")
|
||||||
|
|
|
||||||
|
|
@ -219,12 +219,30 @@ fn is_valid_ident_char(value: char) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_keyword(value: &str) -> bool {
|
fn is_keyword(value: &str) -> bool {
|
||||||
match value {
|
matches!(
|
||||||
"and" | "break" | "do" | "else" | "elseif" | "end" | "false" | "for" | "function"
|
value,
|
||||||
| "if" | "in" | "local" | "nil" | "not" | "or" | "repeat" | "return" | "then" | "true"
|
"and"
|
||||||
| "until" | "while" => true,
|
| "break"
|
||||||
_ => false,
|
| "do"
|
||||||
}
|
| "else"
|
||||||
|
| "elseif"
|
||||||
|
| "end"
|
||||||
|
| "false"
|
||||||
|
| "for"
|
||||||
|
| "function"
|
||||||
|
| "if"
|
||||||
|
| "in"
|
||||||
|
| "local"
|
||||||
|
| "nil"
|
||||||
|
| "not"
|
||||||
|
| "or"
|
||||||
|
| "repeat"
|
||||||
|
| "return"
|
||||||
|
| "then"
|
||||||
|
| "true"
|
||||||
|
| "until"
|
||||||
|
| "while"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tells whether the given string is a valid Lua identifier.
|
/// Tells whether the given string is a valid Lua identifier.
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ where
|
||||||
.as_os_str()
|
.as_os_str()
|
||||||
.to_str()
|
.to_str()
|
||||||
.expect("Invalid Unicode in file path, cannot serialize");
|
.expect("Invalid Unicode in file path, cannot serialize");
|
||||||
let replaced = as_str.replace("\\", "/");
|
let replaced = as_str.replace('\\', "/");
|
||||||
|
|
||||||
serializer.serialize_str(&replaced)
|
serializer.serialize_str(&replaced)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -127,11 +127,10 @@ impl Project {
|
||||||
contents: &[u8],
|
contents: &[u8],
|
||||||
project_file_location: &Path,
|
project_file_location: &Path,
|
||||||
) -> Result<Self, ProjectError> {
|
) -> Result<Self, ProjectError> {
|
||||||
let mut project: Self =
|
let mut project: Self = serde_json::from_slice(contents).map_err(|source| Error::Json {
|
||||||
serde_json::from_slice(&contents).map_err(|source| Error::Json {
|
source,
|
||||||
source,
|
path: project_file_location.to_owned(),
|
||||||
path: project_file_location.to_owned(),
|
})?;
|
||||||
})?;
|
|
||||||
|
|
||||||
project.file_location = project_file_location.to_path_buf();
|
project.file_location = project_file_location.to_path_buf();
|
||||||
project.check_compatibility();
|
project.check_compatibility();
|
||||||
|
|
@ -197,8 +196,8 @@ pub enum PathNode {
|
||||||
impl PathNode {
|
impl PathNode {
|
||||||
pub fn path(&self) -> &Path {
|
pub fn path(&self) -> &Path {
|
||||||
match self {
|
match self {
|
||||||
PathNode::Required(pathbuf) => &pathbuf,
|
PathNode::Required(pathbuf) => pathbuf,
|
||||||
PathNode::Optional(OptionalPathNode { optional }) => &optional,
|
PathNode::Optional(OptionalPathNode { optional }) => optional,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,12 +102,11 @@ impl ServeSession {
|
||||||
|
|
||||||
log::trace!("Starting new ServeSession at path {}", start_path.display());
|
log::trace!("Starting new ServeSession at path {}", start_path.display());
|
||||||
|
|
||||||
let project_path;
|
let project_path = if Project::is_project_file(start_path) {
|
||||||
if Project::is_project_file(start_path) {
|
Cow::Borrowed(start_path)
|
||||||
project_path = Cow::Borrowed(start_path);
|
|
||||||
} else {
|
} else {
|
||||||
project_path = Cow::Owned(start_path.join("default.project.json"));
|
Cow::Owned(start_path.join("default.project.json"))
|
||||||
}
|
};
|
||||||
|
|
||||||
log::debug!("Loading project file from {}", project_path.display());
|
log::debug!("Loading project file from {}", project_path.display());
|
||||||
|
|
||||||
|
|
@ -127,7 +126,7 @@ impl ServeSession {
|
||||||
let instance_context = InstanceContext::default();
|
let instance_context = InstanceContext::default();
|
||||||
|
|
||||||
log::trace!("Generating snapshot of instances from VFS");
|
log::trace!("Generating snapshot of instances from VFS");
|
||||||
let snapshot = snapshot_from_vfs(&instance_context, &vfs, &start_path)?;
|
let snapshot = snapshot_from_vfs(&instance_context, &vfs, start_path)?;
|
||||||
|
|
||||||
log::trace!("Computing initial patch set");
|
log::trace!("Computing initial patch set");
|
||||||
let patch_set = compute_patch_set(snapshot, &tree, root_id);
|
let patch_set = compute_patch_set(snapshot, &tree, root_id);
|
||||||
|
|
|
||||||
|
|
@ -97,8 +97,7 @@ fn finalize_patch_application(context: PatchApplyContext, tree: &mut RojoTree) -
|
||||||
|
|
||||||
for value in instance.properties_mut().values_mut() {
|
for value in instance.properties_mut().values_mut() {
|
||||||
if let Variant::Ref(referent) = value {
|
if let Variant::Ref(referent) = value {
|
||||||
if let Some(&instance_referent) = context.snapshot_id_to_instance_id.get(&referent)
|
if let Some(&instance_referent) = context.snapshot_id_to_instance_id.get(referent) {
|
||||||
{
|
|
||||||
*value = Variant::Ref(instance_referent);
|
*value = Variant::Ref(instance_referent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,8 @@ pub fn compute_patch_set(snapshot: Option<InstanceSnapshot>, tree: &RojoTree, id
|
||||||
// for all of the IDs that we know about so far.
|
// for all of the IDs that we know about so far.
|
||||||
rewrite_refs_in_updates(&context, &mut patch_set.updated_instances);
|
rewrite_refs_in_updates(&context, &mut patch_set.updated_instances);
|
||||||
rewrite_refs_in_additions(&context, &mut patch_set.added_instances);
|
rewrite_refs_in_additions(&context, &mut patch_set.added_instances);
|
||||||
} else {
|
} else if id != tree.get_root_id() {
|
||||||
if id != tree.get_root_id() {
|
patch_set.removed_instances.push(id);
|
||||||
patch_set.removed_instances.push(id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
patch_set
|
patch_set
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,8 @@ pub struct RojoTree {
|
||||||
|
|
||||||
impl RojoTree {
|
impl RojoTree {
|
||||||
pub fn new(snapshot: InstanceSnapshot) -> RojoTree {
|
pub fn new(snapshot: InstanceSnapshot) -> RojoTree {
|
||||||
let root_builder = InstanceBuilder::new(snapshot.class_name.to_owned())
|
let root_builder = InstanceBuilder::new(snapshot.class_name)
|
||||||
.with_name(snapshot.name.to_owned())
|
.with_name(snapshot.name)
|
||||||
.with_properties(snapshot.properties);
|
.with_properties(snapshot.properties);
|
||||||
|
|
||||||
let mut tree = RojoTree {
|
let mut tree = RojoTree {
|
||||||
|
|
@ -245,7 +245,7 @@ impl<'a> InstanceWithMeta<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn metadata(&self) -> &'a InstanceMetadata {
|
pub fn metadata(&self) -> &'a InstanceMetadata {
|
||||||
&self.metadata
|
self.metadata
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -294,6 +294,6 @@ impl InstanceWithMetaMut<'_> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn metadata(&self) -> &InstanceMetadata {
|
pub fn metadata(&self) -> &InstanceMetadata {
|
||||||
&self.metadata
|
self.metadata
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ pub fn snapshot_dir_no_meta(
|
||||||
|
|
||||||
let relevant_paths = vec![
|
let relevant_paths = vec![
|
||||||
path.to_path_buf(),
|
path.to_path_buf(),
|
||||||
meta_path.clone(),
|
meta_path,
|
||||||
// TODO: We shouldn't need to know about Lua existing in this
|
// TODO: We shouldn't need to know about Lua existing in this
|
||||||
// middleware. Should we figure out a way for that function to add
|
// middleware. Should we figure out a way for that function to add
|
||||||
// relevant paths to this middleware?
|
// relevant paths to this middleware?
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ where
|
||||||
.and_then(|name| name.to_str())
|
.and_then(|name| name.to_str())
|
||||||
.with_context(|| format!("Path did not have a file name: {}", path.display()))?;
|
.with_context(|| format!("Path did not have a file name: {}", path.display()))?;
|
||||||
|
|
||||||
match_trailing(&file_name, suffix)
|
match_trailing(file_name, suffix)
|
||||||
.with_context(|| format!("Path did not end in {}: {}", suffix, path.display()))
|
.with_context(|| format!("Path did not end in {}: {}", suffix, path.display()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -145,11 +145,7 @@ fn property_filter(value: Option<&Variant>) -> bool {
|
||||||
|
|
||||||
// Lua can't do anything with SharedString values. They also can't be
|
// Lua can't do anything with SharedString values. They also can't be
|
||||||
// serialized directly by Serde!
|
// serialized directly by Serde!
|
||||||
if ty == Some(VariantType::SharedString) {
|
ty != Some(VariantType::SharedString)
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response body from /api/rojo
|
/// Response body from /api/rojo
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue