· 6 years ago · Jul 16, 2019, 11:48 PM
1Tool = script.Parent;
2
3-- Await initialization
4repeat wait() until _G[Tool];
5Plugin = _G[Tool].Plugin;
6
7-- Detect mode
8Mode = Plugin and 'Plugin' or 'Tool';
9
10-- Load tool completely
11local Indicator = Tool:WaitForChild 'Loaded';
12while not Indicator.Value do
13 Indicator.Changed:wait();
14end;
15
16-- Libraries
17Security = require(Tool.SecurityModule);
18History = require(Tool.HistoryModule);
19Selection = require(Tool.SelectionModule);
20Targeting = require(Tool.TargetingModule);
21Region = require(Tool['Region by AxisAngle']);
22RbxUtility = LoadLibrary 'RbxUtility';
23Create = RbxUtility.Create;
24
25-- Load additional libraries
26while not _G.GetLibraries do wait() end
27Support, Cheer, Try = _G.GetLibraries(
28 'F3X/SupportLibrary@^1.0.0',
29 'F3X/Cheer@^0.0.0',
30 'F3X/Try@~1.0.0'
31);
32
33-- References
34Support.ImportServices();
35SyncAPI = Tool.SyncAPI;
36Player = Players.LocalPlayer;
37
38-- Preload assets
39Assets = require(Tool.Assets);
40ContentProvider:PreloadAsync(Support.Values(Assets));
41
42-- Core events
43ToolChanged = RbxUtility.CreateSignal();
44
45function EquipTool(Tool)
46 -- Equips and switches to the given tool
47
48 -- Unequip current tool
49 if CurrentTool and CurrentTool.Equipped then
50 CurrentTool.Unequip();
51 CurrentTool.Equipped = false;
52 end;
53
54 -- Set `Tool` as current
55 CurrentTool = Tool;
56 CurrentTool.Equipped = true;
57
58 -- Fire relevant events
59 ToolChanged:fire(Tool);
60
61 -- Equip the tool
62 Tool.Equip();
63
64end;
65
66function RecolorHandle(Color)
67 SyncAPI:Invoke('RecolorHandle', Color);
68end;
69
70-- Theme UI to current tool
71ToolChanged:connect(function (Tool)
72 coroutine.wrap(RecolorHandle)(Tool.Color);
73 coroutine.wrap(Selection.RecolorOutlines)(Tool.Color);
74end);
75
76-- Core hotkeys
77Hotkeys = {};
78
79function AssignHotkey(Hotkey, Callback)
80 -- Assigns the given hotkey to `Callback`
81
82 -- Standardize enum-described hotkeys
83 if type(Hotkey) == 'userdata' then
84 Hotkey = { Hotkey };
85
86 -- Standardize string-described hotkeys
87 elseif type(Hotkey) == 'string' then
88 Hotkey = { Enum.KeyCode[Hotkey] };
89
90 -- Standardize string table-described hotkeys
91 elseif type(Hotkey) == 'table' then
92 for Index, Key in ipairs(Hotkey) do
93 if type(Key) == 'string' then
94 Hotkey[Index] = Enum.KeyCode[Key];
95 end;
96 end;
97 end;
98
99 -- Register the hotkey
100 table.insert(Hotkeys, { Keys = Hotkey, Callback = Callback });
101
102end;
103
104function EnableHotkeys()
105 -- Begins to listen for hotkey triggering
106
107 -- Listen for pressed keys
108 Connections.Hotkeys = Support.AddUserInputListener('Began', 'Keyboard', false, function (Input)
109 local _PressedKeys = Support.GetListMembers(UserInputService:GetKeysPressed(), 'KeyCode');
110
111 -- Filter out problematic keys
112 local PressedKeys = {};
113 local FilteredKeys = Support.FlipTable { 'LeftAlt', 'W', 'S', 'A', 'D', 'Space' };
114 for _, Key in ipairs(_PressedKeys) do
115 if not FilteredKeys[Key.Name] then
116 table.insert(PressedKeys, Key);
117 end;
118 end;
119
120 -- Count pressed keys
121 local KeyCount = #PressedKeys;
122
123 -- Prioritize hotkeys based on # of required keys
124 table.sort(Hotkeys, function (A, B)
125 if #A.Keys > #B.Keys then
126 return true;
127 end;
128 end);
129
130 -- Identify matching hotkeys
131 for _, Hotkey in ipairs(Hotkeys) do
132 if KeyCount == #Hotkey.Keys then
133
134 -- Get the hotkey's key index
135 local Keys = Support.FlipTable(Hotkey.Keys)
136 local MatchingKeys = 0;
137
138 -- Check matching pressed keys
139 for _, PressedKey in pairs(PressedKeys) do
140 if Keys[PressedKey] then
141 MatchingKeys = MatchingKeys + 1;
142 end;
143 end;
144
145 -- Trigger the first matching hotkey's callback
146 if MatchingKeys == KeyCount then
147 Hotkey.Callback();
148 break;
149 end;
150
151 end;
152 end;
153 end);
154
155end;
156
157Enabling = RbxUtility.CreateSignal();
158Disabling = RbxUtility.CreateSignal();
159Enabled = RbxUtility.CreateSignal();
160Disabled = RbxUtility.CreateSignal();
161
162function Enable(Mouse)
163
164 -- Ensure tool is disabled or disabling, and not already enabling
165 if (IsEnabled and not IsDisabling) or IsEnabling then
166 return;
167
168 -- If tool is disabling, enable it once fully disabled
169 elseif IsDisabling then
170 Disabled:Wait();
171 return Enable(Mouse);
172 end;
173
174 -- Indicate that tool is enabling
175 IsEnabling = true;
176 Enabling:Fire();
177
178 -- Update the core mouse
179 getfenv(0).Mouse = Mouse;
180
181 -- Use default mouse behavior
182 UserInputService.MouseBehavior = Enum.MouseBehavior.Default;
183
184 -- Disable mouse lock in tool mode
185 if Mode == 'Tool' then
186 SyncAPI:Invoke('SetMouseLockEnabled', false);
187 end;
188
189 -- Wait for UI to initialize asynchronously
190 while not UI do
191 wait(0.1);
192 end;
193
194 -- Show UI
195 UI.Parent = UIContainer;
196
197 -- Display startup notifications
198 if not StartupNotificationsDisplayed then
199 Cheer(Tool.Interfaces.Notifications, UI).Start(getfenv(0));
200 StartupNotificationsDisplayed = true;
201 end;
202
203 -- Start systems
204 EnableHotkeys();
205 Targeting.EnableTargeting();
206 Selection.EnableOutlines();
207 Selection.EnableMultiselectionHotkeys();
208
209 -- Equip current tool
210 EquipTool(CurrentTool or require(Tool.Tools.MoveTool));
211
212 -- Indicate that tool is now enabled
213 IsEnabled = true;
214 IsEnabling = false;
215 Enabled:Fire();
216
217end;
218
219function Disable()
220
221 -- Ensure tool is enabled or enabling, and not already disabling
222 if (not IsEnabled and not IsEnabling) or IsDisabling then
223 return;
224
225 -- If tool is enabling, disable it once fully enabled
226 elseif IsEnabling then
227 Enabled:Wait();
228 return Disable();
229 end;
230
231 -- Indicate that tool is now disabling
232 IsDisabling = true;
233 Disabling:Fire();
234
235 -- Reenable mouse lock option in tool mode
236 if Mode == 'Tool' then
237 pcall(SyncAPI.Invoke, SyncAPI, 'SetMouseLockEnabled', true);
238 end;
239
240 -- Hide UI
241 if UI then
242 UI.Parent = script;
243 end;
244
245 -- Unequip current tool
246 if CurrentTool then
247 CurrentTool.Unequip();
248 CurrentTool.Equipped = false;
249 end;
250
251 -- Clear temporary connections
252 ClearConnections();
253
254 -- Indicate that tool is now disabled
255 IsEnabled = false;
256 IsDisabling = false;
257 Disabled:Fire();
258
259end;
260
261
262-- Core connections
263Connections = {};
264
265function ClearConnections()
266 -- Clears and disconnects temporary connections
267 for Index, Connection in pairs(Connections) do
268 Connection:disconnect();
269 Connections[Index] = nil;
270 end;
271end;
272
273function InitializeUI()
274 -- Sets up the UI
275
276 -- Ensure UI has not yet been initialized
277 if UI then
278 return;
279 end;
280
281 -- Create the root UI
282 UI = Create 'ScreenGui' {
283 Name = 'Building Tools by F3X (UI)'
284 };
285
286 -- Set up dock
287 Dock = Cheer(Tool.Interfaces.Dock, UI).Start(getfenv(0));
288
289end;
290
291-- Enable tool or plugin
292if Mode == 'Plugin' then
293
294 -- Set the UI root
295 UIContainer = CoreGui;
296
297 -- Create the toolbar button
298 PluginButton = Plugin:CreateToolbar('Building Tools by F3X'):CreateButton(
299 'Building Tools by F3X',
300 'Building Tools by F3X',
301 Assets.PluginIcon
302 );
303
304 -- Connect the button to the system
305 PluginButton.Click:connect(function ()
306 PluginEnabled = not PluginEnabled;
307 PluginButton:SetActive(PluginEnabled);
308
309 -- Toggle the tool
310 if PluginEnabled then
311 Plugin:Activate(true);
312 Enable(Plugin:GetMouse());
313 else
314 Disable();
315 end;
316 end);
317
318 -- Disable the tool upon plugin deactivation
319 Plugin.Deactivation:connect(Disable);
320
321 -- Sync Studio selection to internal selection
322 Selection.Changed:connect(function ()
323 SelectionService:Set(Selection.Items);
324 end);
325
326 -- Sync internal selection to Studio selection on enabling
327 Enabling:connect(function ()
328 Selection.Replace(SelectionService:Get());
329 end);
330
331 -- Roughly sync Studio history to internal history (API lacking necessary functionality)
332 History.Changed:connect(function ()
333 ChangeHistoryService:SetWaypoint 'Building Tools by F3X';
334 end);
335
336elseif Mode == 'Tool' then
337
338 -- Set the UI root
339 UIContainer = Player:WaitForChild 'PlayerGui';
340
341 -- Connect the tool to the system
342 Tool.Equipped:connect(Enable);
343 Tool.Unequipped:connect(Disable);
344
345 -- Disable the tool if not parented
346 if not Tool.Parent then
347 Disable();
348 end;
349
350 -- Disable the tool automatically if not equipped or in backpack
351 Tool.AncestryChanged:connect(function (Item, Parent)
352 if not Parent or not (Parent:IsA 'Backpack' or (Parent:IsA 'Model' and Players:GetPlayerFromCharacter(Parent))) then
353 Disable();
354 end;
355 end);
356
357end;
358
359-- Assign hotkeys for undoing (left or right shift + Z)
360AssignHotkey({ 'LeftShift', 'Z' }, History.Undo);
361AssignHotkey({ 'RightShift', 'Z' }, History.Undo);
362
363-- Assign hotkeys for redoing (left or right shift + Y)
364AssignHotkey({ 'LeftShift', 'Y' }, History.Redo);
365AssignHotkey({ 'RightShift', 'Y' }, History.Redo);
366
367-- If in-game, enable ctrl hotkeys for undoing and redoing
368if Mode == 'Tool' then
369 AssignHotkey({ 'LeftControl', 'Z' }, History.Undo);
370 AssignHotkey({ 'RightControl', 'Z' }, History.Undo);
371 AssignHotkey({ 'LeftControl', 'Y' }, History.Redo);
372 AssignHotkey({ 'RightControl', 'Y' }, History.Redo);
373end;
374
375function CloneSelection()
376 -- Clones selected parts
377
378 -- Make sure that there are items in the selection
379 if #Selection.Items == 0 then
380 return;
381 end;
382
383 -- Send the cloning request to the server
384 local Clones = SyncAPI:Invoke('Clone', Selection.Items);
385
386 -- Put together the history record
387 local HistoryRecord = {
388 Clones = Clones;
389
390 Unapply = function (HistoryRecord)
391 -- Reverts this change
392
393 -- Deselect the clones
394 Selection.Remove(HistoryRecord.Clones, false);
395
396 -- Remove the clones
397 SyncAPI:Invoke('Remove', HistoryRecord.Clones);
398
399 end;
400
401 Apply = function (HistoryRecord)
402 -- Reapplies this change
403
404 -- Restore the clones
405 SyncAPI:Invoke('UndoRemove', HistoryRecord.Clones);
406
407 end;
408
409 };
410
411 -- Register the history record
412 History.Add(HistoryRecord);
413
414 -- Select the clones
415 Selection.Replace(Clones);
416
417 -- Play a confirmation sound
418 PlayConfirmationSound();
419
420 -- Flash the outlines of the new parts
421 coroutine.wrap(Selection.FlashOutlines)();
422
423end;
424
425ConfirmationSound = Create 'Sound' {
426 Name = 'BTActionCompletionSound';
427 Pitch = 1.5;
428 SoundId = Assets.ActionCompletionSound;
429 Volume = 1;
430};
431
432function PlayConfirmationSound()
433 -- Plays a confirmation beep sound
434
435 -- Trigger the sound locally
436 SoundService:PlayLocalSound(ConfirmationSound);
437
438end;
439
440function DeleteSelection()
441 -- Deletes selected items
442
443 -- Put together the history record
444 local HistoryRecord = {
445 Parts = Support.CloneTable(Selection.Items);
446
447 Unapply = function (HistoryRecord)
448 -- Reverts this change
449
450 -- Restore the parts
451 SyncAPI:Invoke('UndoRemove', HistoryRecord.Parts);
452
453 -- Select the restored parts
454 Selection.Replace(HistoryRecord.Parts);
455
456 end;
457
458 Apply = function (HistoryRecord)
459 -- Applies this change
460
461 -- Deselect the parts
462 Selection.Remove(HistoryRecord.Parts, false);
463
464 -- Remove the parts
465 SyncAPI:Invoke('Remove', HistoryRecord.Parts);
466
467 end;
468
469 };
470
471 -- Deselect parts before deleting
472 Selection.Remove(HistoryRecord.Parts, false);
473
474 -- Perform the removal
475 SyncAPI:Invoke('Remove', HistoryRecord.Parts);
476
477 -- Register the history record
478 History.Add(HistoryRecord);
479
480end;
481
482-- Assign hotkeys for cloning (left or right shift + c)
483AssignHotkey({ 'LeftShift', 'C' }, CloneSelection);
484AssignHotkey({ 'RightShift', 'C' }, CloneSelection);
485
486-- Assign hotkeys for deletion (left or right shift + X)
487AssignHotkey({ 'LeftShift', 'X' }, DeleteSelection);
488AssignHotkey({ 'RightShift', 'X' }, DeleteSelection);
489
490-- If in-game, enable ctrl hotkeys for cloning and deleting
491if Mode == 'Tool' then
492 AssignHotkey({ 'LeftControl', 'C' }, CloneSelection);
493 AssignHotkey({ 'RightControl', 'C' }, CloneSelection);
494 AssignHotkey({ 'LeftControl', 'X' }, DeleteSelection);
495 AssignHotkey({ 'RightControl', 'X' }, DeleteSelection);
496end;
497
498-- Assign hotkeys for prism selection
499AssignHotkey({ 'LeftShift', 'K' }, Targeting.PrismSelect);
500AssignHotkey({ 'RightShift', 'K' }, Targeting.PrismSelect);
501
502-- If in-game, enable ctrl hotkeys for prism selection
503if Mode == 'Tool' then
504 AssignHotkey({ 'LeftControl', 'K' }, Targeting.PrismSelect);
505 AssignHotkey({ 'RightControl', 'K' }, Targeting.PrismSelect);
506end;
507
508-- Assign hotkeys for sibling selection
509AssignHotkey({ 'LeftBracket' }, Support.Call(Targeting.SelectSiblings, false, true));
510AssignHotkey({ 'LeftShift', 'LeftBracket' }, Support.Call(Targeting.SelectSiblings, false, false));
511AssignHotkey({ 'RightShift', 'LeftBracket' }, Support.Call(Targeting.SelectSiblings, false, false));
512
513-- Assign hotkeys for selection clearing
514AssignHotkey({ 'LeftShift', 'R' }, Support.Call(Selection.Clear, true));
515AssignHotkey({ 'RightShift', 'R' }, Support.Call(Selection.Clear, true));
516
517-- If in-game, enable ctrl hotkeys for sibling selection & selection clearing
518if Mode == 'Tool' then
519 AssignHotkey({ 'LeftControl', 'LeftBracket' }, Support.Call(Targeting.SelectSiblings, false, false));
520 AssignHotkey({ 'RightControl', 'LeftBracket' }, Support.Call(Targeting.SelectSiblings, false, false));
521 AssignHotkey({ 'LeftControl', 'R' }, Support.Call(Selection.Clear, true));
522 AssignHotkey({ 'RightControl', 'R' }, Support.Call(Selection.Clear, true));
523end;
524
525function IsSelectable(Object)
526 -- Returns whether `Object` can be selected
527
528 -- Check if `Object` exists, is not locked, and is not ignored
529 if not Object or not Object.Parent or not Object:IsA 'BasePart' or Object.Locked or IsIgnored(Object) then
530 return false;
531 end;
532
533 -- If areas are enabled, check if `Object` violates any areas
534 if Security.AreAreasEnabled() then
535 return not Security.ArePartsViolatingAreas({ Object }, Player, true, {});
536 end;
537
538 -- If no checks fail, `Object` is selectable
539 return Object;
540
541end;
542
543function IsIgnored(Object)
544 -- TODO: Add ignoring capability
545end;
546
547function SetParent(Parent)
548 -- Sets the current default parent for parts
549
550end;
551
552function ExportSelection()
553 -- Exports the selected parts
554
555 -- Make sure that there are items in the selection
556 if #Selection.Items == 0 then
557 return;
558 end;
559
560 -- Start an export dialog
561 local Dialog = Cheer(Tool.Interfaces.ExportDialog, UI).Start();
562
563 -- Send the exporting request to the server
564 Try(SyncAPI.Invoke, SyncAPI, 'Export', Selection.Items)
565
566 -- Display creation ID on success
567 :Then(function (CreationId)
568 Dialog.SetResult(CreationId);
569 PlayConfirmationSound();
570 print('[Building Tools by F3X] Uploaded Export:', CreationId);
571 end)
572
573 -- Display error messages on failure
574 :Catch('Http requests are not enabled', function ()
575 Dialog.SetError('Please enable HTTP requests');
576 end)
577 :Catch('Export failed due to server-side error', function ()
578 Dialog.SetError('An error occurred, try again');
579 end)
580 :Catch('Post data too large', function ()
581 Dialog.SetError('Try splitting up your build');
582 end)
583 :Catch(function (Error, Stack, Attempt)
584 Dialog.SetError('An unknown error occurred, try again')
585 warn('❌ [Building Tools by F3X] Failed to export selection', '\n\nError:\n', Error, '\n\nStack:\n', Stack);
586 end);
587
588end;
589
590-- Assign hotkey for exporting selection
591AssignHotkey({ 'LeftShift', 'P' }, ExportSelection);
592AssignHotkey({ 'RightShift', 'P' }, ExportSelection);
593
594-- If in-game, enable ctrl hotkeys for exporting
595if Mode == 'Tool' then
596 AssignHotkey({ 'LeftControl', 'P' }, ExportSelection);
597 AssignHotkey({ 'RightControl', 'P' }, ExportSelection);
598end;
599
600function IsVersionOutdated()
601 -- Returns whether this version of Building Tools is out of date
602
603 -- Check most recent version number
604 local AssetInfo = MarketplaceService:GetProductInfo(142785488, Enum.InfoType.Asset);
605 local LatestMajorVersion, LatestMinorVersion, LatestPatchVersion = AssetInfo.Description:match '%[Version: ([0-9]+)%.([0-9]+)%.([0-9]+)%]';
606 local CurrentMajorVersion, CurrentMinorVersion, CurrentPatchVersion = Tool.Version.Value:match '([0-9]+)%.([0-9]+)%.([0-9]+)';
607
608 -- Convert version data into numbers
609 local LatestMajorVersion, LatestMinorVersion, LatestPatchVersion =
610 tonumber(LatestMajorVersion), tonumber(LatestMinorVersion), tonumber(LatestPatchVersion);
611 local CurrentMajorVersion, CurrentMinorVersion, CurrentPatchVersion =
612 tonumber(CurrentMajorVersion), tonumber(CurrentMinorVersion), tonumber(CurrentPatchVersion);
613
614 -- Determine whether current version is outdated
615 if LatestMajorVersion > CurrentMajorVersion then
616 return true;
617 elseif LatestMajorVersion == CurrentMajorVersion then
618 if LatestMinorVersion > CurrentMinorVersion then
619 return true;
620 elseif LatestMinorVersion == CurrentMinorVersion then
621 return LatestPatchVersion > CurrentPatchVersion;
622 end;
623 end;
624
625 -- Return an up-to-date status if not oudated
626 return false;
627
628end;
629
630function ToggleSwitch(CurrentButtonName, SwitchContainer)
631 -- Toggles between the buttons in a switch
632
633 -- Reset all buttons
634 for _, Button in pairs(SwitchContainer:GetChildren()) do
635
636 -- Make sure to not mistake the option label for a button
637 if Button.Name ~= 'Label' then
638
639 -- Set appearance to disabled
640 Button.SelectedIndicator.BackgroundTransparency = 1;
641 Button.Background.Image = Assets.LightSlantedRectangle;
642
643 end;
644
645 end;
646
647 -- Make sure there's a new current button
648 if CurrentButtonName then
649
650 -- Get the current button
651 local CurrentButton = SwitchContainer[CurrentButtonName];
652
653 -- Set the current button's appearance to enabled
654 CurrentButton.SelectedIndicator.BackgroundTransparency = 0;
655 CurrentButton.Background.Image = Assets.DarkSlantedRectangle;
656
657 end;
658end;
659
660-- References to reduce indexing time
661local GetConnectedParts = Instance.new('Part').GetConnectedParts;
662local GetChildren = script.GetChildren;
663
664function GetPartJoints(Part, Whitelist)
665 -- Returns any manual joints involving `Part`
666
667 local Joints = {};
668
669 -- Get joints stored inside `Part`
670 for Joint, JointParent in pairs(SearchJoints(Part, Part, Whitelist)) do
671 Joints[Joint] = JointParent;
672 end;
673
674 -- Get joints stored inside connected parts
675 for _, ConnectedPart in pairs(GetConnectedParts(Part)) do
676 for Joint, JointParent in pairs(SearchJoints(ConnectedPart, Part, Whitelist)) do
677 Joints[Joint] = JointParent;
678 end;
679 end;
680
681 -- Return all found joints
682 return Joints;
683
684end;
685
686-- Types of joints to assume should be preserved
687local ManualJointTypes = Support.FlipTable { 'Weld', 'ManualWeld', 'ManualGlue', 'Motor', 'Motor6D' };
688
689function SearchJoints(Haystack, Part, Whitelist)
690 -- Searches for and returns manual joints in `Haystack` involving `Part` and other parts in `Whitelist`
691
692 local Joints = {};
693
694 -- Search the haystack for joints involving `Part`
695 for _, Item in pairs(GetChildren(Haystack)) do
696
697 -- Check if this item is a manual, intentional joint
698 if ManualJointTypes[Item.ClassName] and
699 (Whitelist[Item.Part0] and Whitelist[Item.Part1]) then
700
701 -- Save joint and state if intentional
702 Joints[Item] = Item.Parent;
703
704 end;
705
706 end;
707
708 -- Return the found joints
709 return Joints;
710
711end;
712
713function RestoreJoints(Joints)
714 -- Restores the joints from the given `Joints` data
715
716 -- Restore each joint
717 for Joint, JointParent in pairs(Joints) do
718 Joint.Parent = JointParent;
719 end;
720
721end;
722
723function PreserveJoints(Part, Whitelist)
724 -- Preserves and returns intentional joints of `Part` connecting parts in `Whitelist`
725
726 -- Get the part's joints
727 local Joints = GetPartJoints(Part, Whitelist);
728
729 -- Save the joints from being broken
730 for Joint in pairs(Joints) do
731 Joint.Parent = nil;
732 end;
733
734 -- Return the joints
735 return Joints;
736
737end;
738
739-- Initialize the UI
740InitializeUI();
741
742-- Return core
743return getfenv(0);