diff --git a/Lync/Plugin.rbxm b/Lync/Plugin.rbxm index 055c57e..15dcce5 100644 Binary files a/Lync/Plugin.rbxm and b/Lync/Plugin.rbxm differ diff --git a/Lync/RobloxPluginSource/Plugin.lua b/Lync/RobloxPluginSource/Plugin.lua index 4f5cd71..c6a4f35 100644 --- a/Lync/RobloxPluginSource/Plugin.lua +++ b/Lync/RobloxPluginSource/Plugin.lua @@ -19,7 +19,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA ]] -local VERSION = "Alpha 27" +local VERSION = "Alpha 28" if not plugin or game:GetService("RunService"):IsRunning() and game:GetService("RunService"):IsClient() then return end @@ -28,7 +28,6 @@ if not plugin or game:GetService("RunService"):IsRunning() and game:GetService(" -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- local ChangeHistoryService = game:GetService("ChangeHistoryService") -local CollectionService = game:GetService("CollectionService") local CoreGui = game:GetService("CoreGui") local HttpService = game:GetService("HttpService") local RunService = game:GetService("RunService") @@ -53,6 +52,7 @@ local SUPPRESSED_CLASSES = { } -- Defines +local contentRoot = "" local debugPrints = false local theme: StudioTheme = settings().Studio.Theme :: StudioTheme local connected = false @@ -278,8 +278,8 @@ local function setTheme() updateChangedModelUi() end -local function getPort(): string - return portTextBox.Text ~= "" and portTextBox.Text or portTextBox.PlaceholderText +local function getHost(): string + return "http://localhost:" .. (portTextBox.Text ~= "" and portTextBox.Text or portTextBox.PlaceholderText) end local function terminate(errorMessage: string) @@ -364,6 +364,28 @@ local function listenForChanges(object: Instance) end end +local function setScriptSourceLive(container: LuaSourceContainer, lua: string) + local document = ScriptEditorService:FindScriptDocument(container) + local cursorLine: number, cursorChar: number, anchorLine: number, anchorChar: number; + if document then + cursorLine, cursorChar, anchorLine, anchorChar = document:GetSelection() + end + ScriptEditorService:UpdateSourceAsync(container, function(_oldContent: string) + return ({lua:gsub("\r", "")})[1] + end) + if document then + local maxLine = document:GetLineCount() + local maxCursorChar = document:GetLine(math.min(cursorLine, maxLine)):len() + 1 + local maxAnchorChar = document:GetLine(math.min(anchorLine, maxLine)):len() + 1 + document:ForceSetSelectionAsync( + math.min(cursorLine, maxLine), + math.min(cursorChar, maxCursorChar), + math.min(anchorLine, maxLine), + math.min(anchorChar, maxAnchorChar) + ) + end +end + --offline-start local function trim6(s: string): string @@ -454,7 +476,7 @@ local function setDetails(target: any, data: any) end) end if data.Properties then - local warning = if serverKey ~= "BuildScript" and (target.Parent == game or table.find(SUPPRESSED_CLASSES, target.ClassName)) then true else false + local warning = if target.Parent == game or table.find(SUPPRESSED_CLASSES, target.ClassName) then true else false for property, value in data.Properties do lpcall("Set Property " .. property, warning, function() if serverKey ~= "BuildScript" and target:IsA("Model") and property == "Scale" then @@ -475,34 +497,12 @@ local function setDetails(target: any, data: any) if data.Tags then for _, tag in data.Tags do lpcall("Set Tag", false, function() - CollectionService:AddTag(target, tag) + game:GetService("CollectionService"):AddTag(target, tag) end) end end end -local function setScriptSourceLive(container: LuaSourceContainer, lua: string) - local document = ScriptEditorService:FindScriptDocument(container) - local cursorLine: number, cursorChar: number, anchorLine: number, anchorChar: number; - if document then - cursorLine, cursorChar, anchorLine, anchorChar = document:GetSelection() - end - ScriptEditorService:UpdateSourceAsync(container, function(_oldContent: string) - return ({lua:gsub("\r", "")})[1] - end) - if document then - local maxLine = document:GetLineCount() - local maxCursorChar = document:GetLine(math.min(cursorLine, maxLine)):len() + 1 - local maxAnchorChar = document:GetLine(math.min(anchorLine, maxLine)):len() + 1 - document:ForceSetSelectionAsync( - math.min(cursorLine, maxLine), - math.min(cursorChar, maxCursorChar), - math.min(anchorLine, maxLine), - math.min(anchorChar, maxAnchorChar) - ) - end -end - local function buildJsonModel(target: any, data: any) data.Properties = data.properties data.Attributes = data.attributes @@ -600,7 +600,7 @@ local function buildPath(path: string) task.spawn(function() activeSourceRequests += 1 local success, result = pcall(function() - return HttpService:GetAsync("http://localhost:" .. getPort(), false, {Key = serverKey, Type = "Source", Path = data.Path}) + return HttpService:GetAsync(getHost(), false, {Key = serverKey, Type = "Source", Path = data.Path}) end) activeSourceRequests -= 1 if success then @@ -614,7 +614,7 @@ local function buildPath(path: string) end end) elseif data.Type == "Model" then - local objects = getObjects("rbxasset://lync/" .. data.Path) + local objects = getObjects("rbxasset://" .. contentRoot .. data.Path) if objects then if #objects == 1 then objects[1].Name = name @@ -634,7 +634,7 @@ local function buildPath(path: string) task.spawn(function() activeSourceRequests += 1 local success, result = pcall(function() - return HttpService:GetAsync("http://localhost:" .. getPort(), false, {Key = serverKey, Type = "Source", Path = data.Path, DataType = data.Type}) + return HttpService:GetAsync(getHost(), false, {Key = serverKey, Type = "Source", Path = data.Path, DataType = data.Type}) end) activeSourceRequests -= 1 if success then @@ -651,7 +651,7 @@ local function buildPath(path: string) task.spawn(function() activeSourceRequests += 1 local success, result = pcall(function() - return HttpService:GetAsync("http://localhost:" .. getPort(), false, {Key = serverKey, Type = "Source", Path = data.Path}) + return HttpService:GetAsync(getHost(), false, {Key = serverKey, Type = "Source", Path = data.Path}) end) activeSourceRequests -= 1 if success then @@ -679,7 +679,7 @@ local function buildPath(path: string) task.spawn(function() activeSourceRequests += 1 local success, result = pcall(function() - return HttpService:GetAsync("http://localhost:" .. getPort(), false, {Key = serverKey, Type = "Source", Path = data.Path}) + return HttpService:GetAsync(getHost(), false, {Key = serverKey, Type = "Source", Path = data.Path}) end) activeSourceRequests -= 1 if success then @@ -698,7 +698,7 @@ local function buildPath(path: string) task.spawn(function() activeSourceRequests += 1 local success, result = pcall(function() - return HttpService:GetAsync("http://localhost:" .. getPort(), false, {Key = serverKey, Type = "Source", Path = data.Path, DataType = "Localization"}) + return HttpService:GetAsync(getHost(), false, {Key = serverKey, Type = "Source", Path = data.Path, DataType = "Localization"}) end) activeSourceRequests -= 1 if success then @@ -718,7 +718,7 @@ local function buildPath(path: string) setDetails(target, data) if data.TerrainRegion then if target == workspace.Terrain then - local objects = getObjects("rbxasset://lync/" .. data.TerrainRegion[1]) + local objects = getObjects("rbxasset://" .. contentRoot .. data.TerrainRegion[1]) if objects then if #objects == 1 then lpcall("Set Terrain Region", false, function() @@ -787,13 +787,15 @@ local function setConnected(newConnected: boolean) if newConnected then if not map then local success, result = pcall(function() - local get = HttpService:GetAsync("http://localhost:" .. getPort(), false, {Key = serverKey, Type = "Map", Playtest = IS_PLAYTEST_SERVER}) + local get = HttpService:GetAsync(getHost(), false, {Key = serverKey, Type = "Map", Playtest = IS_PLAYTEST_SERVER}) return get ~= "{}" and HttpService:JSONDecode(get) or nil end) if success then if result.Version == VERSION then debugPrints = result.Debug + contentRoot = result.ContentRoot result.Debug = nil + result.ContentRoot = nil map = result if not IS_PLAYTEST_SERVER then if debugPrints then warn("[Lync] - Map:", result) end @@ -824,7 +826,7 @@ local function setConnected(newConnected: boolean) end else local success, result = pcall(function() - HttpService:GetAsync("http://localhost:" .. getPort(), false, {Key = serverKey, Type = "Resume"}) + HttpService:GetAsync(getHost(), false, {Key = serverKey, Type = "Resume"}) end) if not success then task.spawn(error, "[Lync] - " .. result) @@ -881,7 +883,7 @@ if not IS_PLAYTEST_SERVER then -- Port portTextBox.MouseEnter:Connect(function() - if portTextBox:IsFocused() then return end + if not portTextBox.Active or portTextBox:IsFocused() then return end mainWidgetFrame.Frame.UIStroke.Color = mainWidgetFrame.Frame:GetAttribute("BorderHover") end) @@ -891,6 +893,7 @@ if not IS_PLAYTEST_SERVER then end) portTextBox.Focused:Connect(function() + if not portTextBox.Active then return end mainWidgetFrame.Frame.UIStroke.Color = mainWidgetFrame.Frame:GetAttribute("BorderSelected") end) @@ -915,7 +918,7 @@ if not IS_PLAYTEST_SERVER then for _, data in map do if data.Instance == StudioService.ActiveScript then local success, result = pcall(function() - HttpService:PostAsync("http://localhost:" .. getPort(), ScriptEditorService:GetEditorSource(StudioService.ActiveScript :: LuaSourceContainer), Enum.HttpContentType.TextPlain, false, {Key = serverKey, Type = "ReverseSync", Path = data.Path}) + HttpService:PostAsync(getHost(), ScriptEditorService:GetEditorSource(StudioService.ActiveScript :: LuaSourceContainer), Enum.HttpContentType.TextPlain, false, {Key = serverKey, Type = "ReverseSync", Path = data.Path}) end) if success then print("[Lync] - Saved script:", data.Path) @@ -965,7 +968,7 @@ if not IS_PLAYTEST_SERVER then for _, data in map do if data.Instance == StudioService.ActiveScript then local success, result = pcall(function() - local source = HttpService:GetAsync("http://localhost:" .. getPort(), false, {Key = serverKey, Type = "Source", Path = data.Path}) + local source = HttpService:GetAsync(getHost(), false, {Key = serverKey, Type = "Source", Path = data.Path}) setScriptSourceLive(data.Instance, source) end) if success then @@ -1079,13 +1082,17 @@ end if workspace:GetAttribute("__lyncbuildfile") and not IS_PLAYTEST_SERVER or syncDuringTest and IS_PLAYTEST_SERVER and workspace:GetAttribute("__lyncactive") then if syncDuringTest and IS_PLAYTEST_SERVER then warn("[Lync] - Playtest Sync is active.") end + portTextBox.Text = "" + portTextBox.PlaceholderText = workspace:GetAttribute("__lyncbuildfile") + portTextBox.TextEditable = false + portTextBox.Active = false setConnected(true) end while task.wait(0.5) do if connected then local success, result = pcall(function() - local get = HttpService:GetAsync("http://localhost:" .. getPort(), false, {Key = serverKey, Type = "Modified", Playtest = IS_PLAYTEST_SERVER}) + local get = HttpService:GetAsync(getHost(), false, {Key = serverKey, Type = "Modified", Playtest = IS_PLAYTEST_SERVER}) return get ~= "{}" and HttpService:JSONDecode(get) or nil end) if success then diff --git a/Lync/index.js b/Lync/index.js index 92ab852..93b90b5 100644 --- a/Lync/index.js +++ b/Lync/index.js @@ -18,9 +18,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -const VERSION = 'Alpha 27' +const VERSION = 'Alpha 28' const { spawn, spawnSync } = require('child_process') +const crypto = require('crypto') const fs = require('fs') const os = require('os') const path = require('path') @@ -193,33 +194,40 @@ function localPathIsIgnored(localPath) { //------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ /** - * @param {string} existingPath - * @param {string} hardLinkPath + * @param {string} sourcePath + * @param {string} hardLinkRootPath */ -function hardLinkRecursive(existingPath, hardLinkPath) { - if (localPathIsIgnored(existingPath)) return - const stats = fs.statSync(existingPath) - const newPath = path.resolve(hardLinkPath, path.relative(process.cwd(), existingPath)) +function hardLinkRecursive(sourcePath, hardLinkRootPath) { + if (localPathIsIgnored(sourcePath)) return + const hardLinkPath = path.resolve(hardLinkRootPath, path.parse(process.cwd()).name, PROJECT_JSON, path.relative(process.cwd(), sourcePath)) + if (process.cwd() == sourcePath) { + try { + fs.rmSync(hardLinkPath, { force: true, recursive: true }) + } catch (err) { + console.error(red('Hard link error:'), yellow(err)) + } + } + const stats = fs.statSync(sourcePath) + const parentPath = path.resolve(hardLinkPath, '..') try { - const parentPath = path.resolve(newPath, '..') if (!fs.existsSync(parentPath)) { - fs.mkdirSync(parentPath) + fs.mkdirSync(parentPath, { recursive: true }) } if (stats.isDirectory()) { - if (!fs.existsSync(newPath)) { - fs.mkdirSync(newPath) + if (!fs.existsSync(hardLinkPath)) { + fs.mkdirSync(hardLinkPath) } - fs.readdirSync(existingPath).forEach((dirNext) => { - hardLinkRecursive(path.resolve(existingPath, dirNext), hardLinkPath) + fs.readdirSync(sourcePath).forEach((dirNext) => { + hardLinkRecursive(path.resolve(sourcePath, dirNext), hardLinkRootPath) }) } else { - if (fs.existsSync(newPath)) { - fs.unlinkSync(newPath) + if (fs.existsSync(hardLinkPath)) { + fs.unlinkSync(hardLinkPath) } - fs.linkSync(existingPath, newPath) + fs.linkSync(sourcePath, hardLinkPath) } } catch (err) { - if (DEBUG) console.error(red('Hard link error:'), yellow(err)) + console.error(red('Hard link error:'), yellow(err)) } } @@ -1023,14 +1031,11 @@ function runJobs(event, localPath) { // Write build script if (DEBUG) console.log('Writing build script . . .') let buildScript = fs.readFileSync(path.resolve(__dirname, 'luneBuildTemplate.luau')) - buildScript += `local game = Instance.new("DataModel")\n` - buildScript += 'local workspace = game:GetService("Workspace")\n' - buildScript += 'workspace:SetAttribute("__lyncbuildfile", true)\n' - buildScript += `${pluginSource}\n` - buildScript += `port = "${projectJson.port}"\n` - buildScript += `map = net.jsonDecode("${toEscapeSequence(JSON.stringify(map, null, '\t'))}")\n` - buildScript += `buildAll()\n` - buildScript += `fs.writeFile("${projectJson.build}", roblox.serializePlace(game))\n` + + `\nworkspace:SetAttribute("__lyncbuildfile", ${projectJson.port})\n` + + `${pluginSource}\n` + + `map = net.jsonDecode("${toEscapeSequence(JSON.stringify(map, null, '\t'))}")\n` + + `buildAll()\n` + + `fs.writeFile("${projectJson.build}", roblox.serializePlace(game))\n` if (fs.existsSync(buildScriptPath)) fs.rmSync(buildScriptPath) fs.writeFileSync(buildScriptPath, buildScript) @@ -1097,8 +1102,13 @@ function runJobs(event, localPath) { if (DEBUG) console.log('Creating folder', cyan(pluginsPath)) fs.mkdirSync(pluginsPath) } - if (DEBUG) console.log('Copying', cyan(path.resolve(__dirname, 'Plugin.rbxm')), '->', cyan(path.resolve(pluginsPath, 'Lync.rbxm'))) - fs.copyFileSync(path.resolve(__dirname, 'Plugin.rbxm'), path.resolve(pluginsPath, 'Lync.rbxm')) + const pluginPath = path.resolve(pluginsPath, 'Lync.rbxm') + const currentHash = fs.existsSync(pluginPath) && crypto.createHash('md5').update(fs.readFileSync(pluginPath)).digest('hex') + const newHash = crypto.createHash('md5').update(fs.readFileSync(path.resolve(__dirname, 'Plugin.rbxm'))).digest('hex') + if (currentHash != newHash) { + if (DEBUG) console.log('Copying', cyan(path.resolve(__dirname, 'Plugin.rbxm')), '->', cyan(pluginPath)) + fs.copyFileSync(path.resolve(__dirname, 'Plugin.rbxm'), pluginPath) + } // Sync file changes chokidar.watch('.', { @@ -1286,19 +1296,18 @@ function runJobs(event, localPath) { hardLinkPaths.push(hardLinkPath) } for (const hardLinkPath of hardLinkPaths) { - try { - fs.rmSync(hardLinkPath, { force: true, recursive: true }) - } catch (err) {} hardLinkRecursive(process.cwd(), hardLinkPath) } // Send map map.Version = VERSION map.Debug = DEBUG + map.ContentRoot = path.join('lync', path.parse(process.cwd()).name, PROJECT_JSON).replaceAll('\\', '/') + '/' map.ServePlaceIds = projectJson.servePlaceIds const mapJsonString = JSON.stringify(map) delete map['Version'] delete map['Debug'] + delete map['ContentRoot'] delete map['ServePlaceIds'] if ('playtest' in req.headers) { modified_playtest = {} diff --git a/Lync/luneBuildTemplate.luau b/Lync/luneBuildTemplate.luau index 0280672..0e45739 100644 --- a/Lync/luneBuildTemplate.luau +++ b/Lync/luneBuildTemplate.luau @@ -7,10 +7,16 @@ local roblox = require("@lune/roblox") local Enum = roblox.Enum local Instance = roblox.Instance +local game = Instance.new("DataModel") +local workspace = game:GetService("Workspace") + +local SUPPRESSED_CLASSES = {} + +local contentRoot = "" local serverKey = "BuildScript" -local port = "34873" local map = {} local activeSourceRequests = 0 +local changedModels = {} local terminate = error @@ -70,16 +76,18 @@ local function lpcall(_context, _warning, func, ...) end local function getObjects(url) - return roblox.deserializeModel(fs.readFile(url:sub(17))) + return roblox.deserializeModel(fs.readFile(url:sub(12))) end local function listenForChanges() end -local function getPort() - return port +local function getHost() + return "http://localhost:" .. workspace:GetAttribute("__lyncbuildfile") +end + +local function setScriptSourceLive() end -local function LuaCsv(_) - return "" +local function updateChangedModelUi() end diff --git a/Lync/validator/project.js b/Lync/validator/project.js index bf8b61d..2a06588 100644 --- a/Lync/validator/project.js +++ b/Lync/validator/project.js @@ -15,27 +15,6 @@ function scan(json, root, localPath) { if (key == '$className' && typeof json[key] != 'string') { console.error(jsonError(localPath, root, json, '$className'), yellow('Must be a string')) failed = true - - } else if (key == '$properties') { - if (!(typeof json[key] == 'object' && !Array.isArray(json[key]))) { - console.error(jsonError(localPath, root, json, '$properties'), yellow('Must be an object')) - failed = true - } else { - for (const property in json[key]) { - if (typeof json[key][property] == 'object' && Array.isArray(json[key][property]) && json[key][property].length > 1) { - console.error(jsonError(localPath, root, json, '$properties\\' + property), yellow('Array with size > 1; check property syntax')) - failed = true - } - } - } - - } else if (key == '$attributes' && !(typeof json[key] == 'object' && !Array.isArray(json[key]))) { - console.error(jsonError(localPath, root, json, '$attributes'), yellow('Must be an object')) - failed = true - - } else if (key == '$tags' && !(typeof json[key] == 'object' && Array.isArray(json[key]))) { - console.error(jsonError(localPath, root, json, '$tags'), yellow('Must be an array')) - failed = true } else if (key == '$path') { if (typeof(json[key]) == 'object') { @@ -65,6 +44,27 @@ function scan(json, root, localPath) { failed = true } + } else if (key == '$properties') { + if (!(typeof json[key] == 'object' && !Array.isArray(json[key]))) { + console.error(jsonError(localPath, root, json, '$properties'), yellow('Must be an object')) + failed = true + } else { + for (const property in json[key]) { + if (typeof json[key][property] == 'object' && Array.isArray(json[key][property]) && json[key][property].length > 1) { + console.error(jsonError(localPath, root, json, '$properties\\' + property), yellow('Array with size > 1; check property syntax')) + failed = true + } + } + } + + } else if (key == '$attributes' && !(typeof json[key] == 'object' && !Array.isArray(json[key]))) { + console.error(jsonError(localPath, root, json, '$attributes'), yellow('Must be an object')) + failed = true + + } else if (key == '$tags' && !(typeof json[key] == 'object' && Array.isArray(json[key]))) { + console.error(jsonError(localPath, root, json, '$tags'), yellow('Must be an array')) + failed = true + } else if (key == '$clearOnSync' && (typeof json[key] != 'boolean')) { console.error(jsonError(localPath, root, json, '$clearOnSync'), yellow('Must be a boolean')) failed = true @@ -132,6 +132,20 @@ module.exports.validate = function(type, json, localPath) { failed = true } + if ('servePlaceIds' in json) { + if (!(typeof json.servePlaceIds == 'object' && Array.isArray(json.servePlaceIds))) { + console.error(jsonError(localPath, json, json, 'servePlaceIds'), yellow('Must be an array')) + failed = true + } else { + for (const index in json.servePlaceIds) { + if (typeof json.servePlaceIds[index] != 'number') { + console.error(jsonError(localPath, json, json.servePlaceIds, index), yellow('Must be a number')) + failed = true + } + } + } + } + if ('globIgnorePaths' in json) { if (!(typeof json.globIgnorePaths == 'object' && Array.isArray(json.globIgnorePaths))) { console.error(jsonError(localPath, json, json, 'globIgnorePaths'), yellow('Must be an array')) diff --git a/Sample Project/default.project.json b/Sample Project/default.project.json index 6ab4075..3ba00cf 100644 --- a/Sample Project/default.project.json +++ b/Sample Project/default.project.json @@ -1,7 +1,7 @@ { "name": "Example Build", - "port": 34873, "build": "BuildScripts/Build/Build.rbxl", + "port": 34873, "sourcemapEnabled": { "RBXM": true, "RBXMX": true diff --git a/sourcemap.json b/sourcemap.json index bb986fa..749fc57 100644 --- a/sourcemap.json +++ b/sourcemap.json @@ -4,14 +4,5 @@ "filePaths": [ "Lync/RobloxPluginSource/Plugin.lua" ], - "children": [ - { - "name": "LuaCsv", - "className": "ModuleScript", - "filePaths": [ - "Lync/RobloxPluginSource/LuaCsv.lua" - ], - "children": [] - } - ] + "children": [] } \ No newline at end of file