├── .github └── workflows │ └── luacheck.yml ├── .gitignore ├── .luacheckrc ├── LICENSE.md ├── buildPlugin.rbxl ├── classConverter.sublime-project ├── default.project.json ├── images ├── ConverterLogo.png └── screenshots │ ├── UIExample.PNG │ └── UIExampleFilter.png ├── readme.md └── src ├── Converter.lua ├── IconHandler.lua ├── Maid.lua ├── ScrollingFrame.lua ├── Signal.lua ├── Spring.lua ├── StringMatcher.lua ├── ThemeSwitcher.lua ├── UI.lua ├── ValueObject.lua └── init.server.lua /.github/workflows/luacheck.yml: -------------------------------------------------------------------------------- 1 | name: luacheck 2 | on: [push, pull_request] 3 | jobs: 4 | lint: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@master 8 | - name: lint 9 | uses: Roang-zero1/factorio-mod-luacheck@master 10 | with: 11 | luacheckrc_url: https://raw.githubusercontent.com/Quenty/ClassConverterPlugin/master/.luacheckrc 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sublime-workspace 2 | *.rbxl.lock 3 | 4 | archive/*.* -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | local empty = {} 2 | local read_write = { read_only = false } 3 | local read_write_class = { read_only = false, other_fields = true } 4 | local read_only = { read_only = true } 5 | 6 | local function def_fields(field_list) 7 | local fields = {} 8 | 9 | for _, field in ipairs(field_list) do 10 | fields[field] = empty 11 | end 12 | 13 | return { fields = fields } 14 | end 15 | 16 | local enum = def_fields({"Value", "Name"}) 17 | 18 | local function def_enum(field_list) 19 | local fields = {} 20 | 21 | for _, field in ipairs(field_list) do 22 | fields[field] = enum 23 | end 24 | 25 | fields["GetEnumItems"] = read_only 26 | 27 | return { fields = fields } 28 | end 29 | 30 | stds.roblox = { 31 | globals = { 32 | script = { 33 | other_fields = true, 34 | fields = { 35 | Source = read_write; 36 | GetHash = read_write; 37 | Disabled = read_write; 38 | LinkedSource = read_write; 39 | CurrentEditor = read_write_class; 40 | IsDifferentFromFileSystem = read_write; 41 | Archivable = read_write; 42 | ClassName = read_only; 43 | Name = read_write; 44 | Parent = read_write_class; 45 | RobloxLocked = read_write; 46 | ClearAllChildren = read_write; 47 | Clone = read_write; 48 | Destroy = read_write; 49 | FindFirstAncestor = read_write; 50 | FindFirstAncestorOfClass = read_write; 51 | FindFirstAncestorWhichIsA = read_write; 52 | FindFirstChild = read_write; 53 | FindFirstChildOfClass = read_write; 54 | FindFirstChildWhichIsA = read_write; 55 | GetAttribute = read_write; 56 | GetAttributeChangedSignal = read_write; 57 | GetAttributes = read_write; 58 | GetChildren = read_write; 59 | GetDebugId = read_write; 60 | GetDescendants = read_write; 61 | GetFullName = read_write; 62 | GetPropertyChangedSignal = read_write; 63 | IsA = read_write; 64 | IsAncestorOf = read_write; 65 | IsDescendantOf = read_write; 66 | SetAttribute = read_write; 67 | WaitForChild = read_write; 68 | AncestryChanged = read_write; 69 | AttributeChanged = read_write; 70 | Changed = read_write; 71 | ChildAdded = read_write; 72 | ChildRemoved = read_write; 73 | DescendantAdded = read_write; 74 | DescendantRemoving = read_write; 75 | } 76 | }, 77 | game = { 78 | other_fields = true, 79 | fields = { 80 | CreatorId = read_only; 81 | CreatorType = read_only; 82 | GameId = read_only; 83 | Genre = read_only; 84 | IsSFFlagsLoaded = read_only; 85 | JobId = read_only; 86 | PlaceId = read_only; 87 | PlaceVersion = read_only; 88 | PrivateServerId = read_only; 89 | PrivateServerOwnerId = read_only; 90 | Workspace = read_only; 91 | BindToClose = read_write; 92 | DefineFastFlag = read_write; 93 | DefineFastInt = read_write; 94 | DefineFastString = read_write; 95 | GetFastFlag = read_write; 96 | GetFastInt = read_write; 97 | GetFastString = read_write; 98 | GetJobIntervalPeakFraction = read_write; 99 | GetJobTimePeakFraction = read_write; 100 | GetJobsExtendedStats = read_write; 101 | GetJobsInfo = read_write; 102 | GetObjects = read_write; 103 | GetObjectsList = read_write; 104 | IsLoaded = read_write; 105 | Load = read_write; 106 | OpenScreenshotsFolder = read_write; 107 | OpenVideosFolder = read_write; 108 | ReportInGoogleAnalytics = read_write; 109 | SetFastFlagForTesting = read_write; 110 | SetFastIntForTesting = read_write; 111 | SetFastStringForTesting = read_write; 112 | SetPlaceId = read_write; 113 | SetUniverseId = read_write; 114 | Shutdown = read_write; 115 | GetObjectsAsync = read_write; 116 | HttpGetAsync = read_write; 117 | HttpPostAsync = read_write; 118 | InsertObjectsAndJoinIfLegacyAsync = read_write; 119 | GraphicsQualityChangeRequest = read_write; 120 | Loaded = read_write; 121 | ScreenshotReady = read_write; 122 | FindService = read_write; 123 | GetService = read_write; 124 | Close = read_write; 125 | CloseLate = read_write; 126 | ServiceAdded = read_write; 127 | ServiceRemoving = read_write; 128 | Archivable = read_write; 129 | ClassName = read_only; 130 | Name = read_write; 131 | Parent = read_write_class; 132 | RobloxLocked = read_write; 133 | ClearAllChildren = read_write; 134 | Clone = read_write; 135 | Destroy = read_write; 136 | FindFirstAncestor = read_write; 137 | FindFirstAncestorOfClass = read_write; 138 | FindFirstAncestorWhichIsA = read_write; 139 | FindFirstChild = read_write; 140 | FindFirstChildOfClass = read_write; 141 | FindFirstChildWhichIsA = read_write; 142 | GetAttribute = read_write; 143 | GetAttributeChangedSignal = read_write; 144 | GetAttributes = read_write; 145 | GetChildren = read_write; 146 | GetDebugId = read_write; 147 | GetDescendants = read_write; 148 | GetFullName = read_write; 149 | GetPropertyChangedSignal = read_write; 150 | IsA = read_write; 151 | IsAncestorOf = read_write; 152 | IsDescendantOf = read_write; 153 | SetAttribute = read_write; 154 | WaitForChild = read_write; 155 | AncestryChanged = read_write; 156 | AttributeChanged = read_write; 157 | Changed = read_write; 158 | ChildAdded = read_write; 159 | ChildRemoved = read_write; 160 | DescendantAdded = read_write; 161 | DescendantRemoving = read_write; 162 | } 163 | }, 164 | workspace = { 165 | other_fields = true, 166 | fields = { 167 | AllowThirdPartySales = read_write; 168 | CurrentCamera = read_write_class; 169 | DistributedGameTime = read_write; 170 | FallenPartsDestroyHeight = read_write; 171 | FilteringEnabled = read_write; 172 | Gravity = read_write; 173 | StreamingEnabled = read_write; 174 | StreamingMinRadius = read_write; 175 | StreamingPauseMode = read_write; 176 | StreamingTargetRadius = read_write; 177 | TemporaryLegacyPhysicsSolverOverride = read_write; 178 | Terrain = read_only; 179 | BreakJoints = read_write; 180 | CalculateJumpPower = read_write; 181 | ExperimentalSolverIsEnabled = read_write; 182 | GetNumAwakeParts = read_write; 183 | GetPhysicsThrottling = read_write; 184 | GetRealPhysicsFPS = read_write; 185 | JoinToOutsiders = read_write; 186 | MakeJoints = read_write; 187 | PGSIsEnabled = read_write; 188 | SetPhysicsThrottleEnabled = read_write; 189 | UnjoinFromOutsiders = read_write; 190 | ZoomToExtents = read_write; 191 | FindPartOnRay = read_write; 192 | FindPartOnRayWithIgnoreList = read_write; 193 | FindPartOnRayWithWhitelist = read_write; 194 | FindPartsInRegion3 = read_write; 195 | FindPartsInRegion3WithIgnoreList = read_write; 196 | FindPartsInRegion3WithWhiteList = read_write; 197 | IKMoveTo = read_write; 198 | IsRegion3Empty = read_write; 199 | IsRegion3EmptyWithIgnoreList = read_write; 200 | Raycast = read_write; 201 | PrimaryPart = read_write_class; 202 | BreakJoints = read_write; 203 | GetBoundingBox = read_write; 204 | GetExtentsSize = read_write; 205 | GetPrimaryPartCFrame = read_write; 206 | MakeJoints = read_write; 207 | MoveTo = read_write; 208 | SetPrimaryPartCFrame = read_write; 209 | TranslateBy = read_write; 210 | Archivable = read_write; 211 | ClassName = read_only; 212 | Name = read_write; 213 | Parent = read_write_class; 214 | RobloxLocked = read_write; 215 | ClearAllChildren = read_write; 216 | Clone = read_write; 217 | Destroy = read_write; 218 | FindFirstAncestor = read_write; 219 | FindFirstAncestorOfClass = read_write; 220 | FindFirstAncestorWhichIsA = read_write; 221 | FindFirstChild = read_write; 222 | FindFirstChildOfClass = read_write; 223 | FindFirstChildWhichIsA = read_write; 224 | GetAttribute = read_write; 225 | GetAttributeChangedSignal = read_write; 226 | GetAttributes = read_write; 227 | GetChildren = read_write; 228 | GetDebugId = read_write; 229 | GetDescendants = read_write; 230 | GetFullName = read_write; 231 | GetPropertyChangedSignal = read_write; 232 | IsA = read_write; 233 | IsAncestorOf = read_write; 234 | IsDescendantOf = read_write; 235 | SetAttribute = read_write; 236 | WaitForChild = read_write; 237 | AncestryChanged = read_write; 238 | AttributeChanged = read_write; 239 | Changed = read_write; 240 | ChildAdded = read_write; 241 | ChildRemoved = read_write; 242 | DescendantAdded = read_write; 243 | DescendantRemoving = read_write; 244 | } 245 | }, 246 | }, 247 | read_globals = { 248 | -- Methods 249 | delay = empty; 250 | settings = empty; 251 | spawn = empty; 252 | tick = empty; 253 | time = empty; 254 | typeof = empty; 255 | version = empty; 256 | wait = empty; 257 | warn = empty; 258 | 259 | -- Libraries 260 | math = def_fields({"abs", "acos", "asin", "atan", "atan2", "ceil", "clamp", "cos", "cosh", 261 | "deg", "exp", "floor", "fmod", "frexp", "ldexp", "log", "log10", "max", "min", "modf", 262 | "noise", "pow", "rad", "random", "randomseed", "sign", "sin", "sinh", "sqrt", "tan", 263 | "tanh", "huge", "pi"}), 264 | 265 | table = def_fields({"concat", "foreach", "foreachi", "getn", "insert", "remove", "sort", 266 | "pack", "unpack", "move", "create", "find"}), 267 | 268 | os = def_fields({"time", "difftime", "date"}), 269 | 270 | debug = def_fields({"traceback", "profilebegin", "profileend"}), 271 | 272 | utf8 = def_fields({"char", "codes", "codepoint", "len", "offset", "graphemes", 273 | "nfcnormalize", "nfdnormalize", "charpattern"}), 274 | 275 | bit32 = def_fields({"arshift", "band", "bnot", "bor", "btest", "bxor", "extract", 276 | "replace", "lrotate", "lshift", "rrotate", "rshift"}), 277 | 278 | string = def_fields({"byte", "char", "find", "format", "gmatch", "gsub", "len", "lower", 279 | "match", "rep", "reverse", "split"}), 280 | 281 | -- Types 282 | Axes = def_fields({"new"}), 283 | 284 | BrickColor = def_fields({"new", "palette", "random", "White", "Gray", "DarkGray", "Black", 285 | "Red", "Yellow", "Green", "Blue"}), 286 | 287 | CFrame = def_fields({"new", "fromEulerAnglesXYZ", "Angles", "fromOrientation", 288 | "fromAxisAngle", "fromMatrix"}), 289 | 290 | Color3 = def_fields({"new", "fromRGB", "fromHSV", "toHSV"}), 291 | 292 | ColorSequence = def_fields({"new"}), 293 | 294 | ColorSequenceKeypoint = def_fields({"new"}), 295 | 296 | DockWidgetPluginGuiInfo = def_fields({"new"}), 297 | 298 | Enums = def_fields({"GetEnums"}), 299 | 300 | Faces = def_fields({"new"}), 301 | 302 | Instance = def_fields({"new"}), 303 | 304 | NumberRange = def_fields({"new"}), 305 | 306 | NumberSequence = def_fields({"new"}), 307 | 308 | NumberSequenceKeypoint = def_fields({"new"}), 309 | 310 | PhysicalProperties = def_fields({"new"}), 311 | 312 | Random = def_fields({"new"}), 313 | 314 | Ray = def_fields({"new"}), 315 | 316 | Rect = def_fields({"new"}), 317 | 318 | Region3 = def_fields({"new"}), 319 | 320 | Region3int16 = def_fields({"new"}), 321 | 322 | TweenInfo = def_fields({"new"}), 323 | 324 | UDim = def_fields({"new"}), 325 | 326 | UDim2 = def_fields({"new", "fromScale", "fromOffset"}), 327 | 328 | Vector2 = def_fields({"new"}), 329 | 330 | Vector2int16 = def_fields({"new"}), 331 | 332 | Vector3 = def_fields({"new", "FromNormalId", "FromAxis"}), 333 | 334 | Vector3int16 = def_fields({"new"}), 335 | 336 | -- Enums 337 | Enum = { 338 | readonly = true, 339 | fields = { 340 | ActionType = def_enum({"Nothing", "Pause", "Lose", "Draw", "Win"}), 341 | ActuatorRelativeTo = def_enum({"Attachment0", "Attachment1", "World"}), 342 | ActuatorType = def_enum({"None", "Motor", "Servo"}), 343 | AlignType = def_enum({"Parallel", "Perpendicular"}), 344 | AlphaMode = def_enum({"Overlay", "Transparency"}), 345 | AnimationPriority = def_enum({"Idle", "Movement", "Action", "Core"}), 346 | AppShellActionType = def_enum({"None", "OpenApp", "TapChatTab", 347 | "TapConversationEntry", "TapAvatarTab", "ReadConversation", "TapGamePageTab", 348 | "TapHomePageTab", "GamePageLoaded", "HomePageLoaded", "AvatarEditorPageLoaded"}), 349 | AspectType = def_enum({"FitWithinMaxSize", "ScaleWithParentSize"}), 350 | AssetFetchStatus = def_enum({"Success", "Failure"}), 351 | AssetType = def_enum({"Image", "TeeShirt", "Audio", "Mesh", "Lua", "Hat", "Place", 352 | "Model", "Shirt", "Pants", "Decal", "Head", "Face", "Gear", "Badge", 353 | "Animation", "Torso", "RightArm", "LeftArm", "LeftLeg", "RightLeg", "Package", 354 | "GamePass", "Plugin", "MeshPart", "HairAccessory", "FaceAccessory", 355 | "NeckAccessory", "ShoulderAccessory", "FrontAccessory", "BackAccessory", 356 | "WaistAccessory", "ClimbAnimation", "DeathAnimation", "FallAnimation", 357 | "IdleAnimation", "JumpAnimation", "RunAnimation", "SwimAnimation", 358 | "WalkAnimation", "PoseAnimation", "EarAccessory", "EyeAccessory", 359 | "EmoteAnimation"}), 360 | AvatarContextMenuOption = def_enum({"Friend", "Chat", "Emote", "InspectMenu"}), 361 | AvatarJointPositionType = def_enum({"Fixed", "ArtistIntent"}), 362 | Axis = def_enum({"X", "Y", "Z"}), 363 | BinType = def_enum({"Script", "GameTool", "Grab", "Clone", "Hammer"}), 364 | BodyPart = def_enum({"Head", "Torso", "LeftArm", "RightArm", "LeftLeg", "RightLeg"}), 365 | BodyPartR15 = def_enum({"Head", "UpperTorso", "LowerTorso", "LeftFoot", 366 | "LeftLowerLeg", "LeftUpperLeg", "RightFoot", "RightLowerLeg", "RightUpperLeg", 367 | "LeftHand", "LeftLowerArm", "LeftUpperArm", "RightHand", "RightLowerArm", 368 | "RightUpperArm", "RootPart", "Unknown"}), 369 | BorderMode = def_enum({"Outline", "Middle", "Inset"}), 370 | BreakReason = def_enum({"Other", "Error", "UserBreakpoint", "SpecialBreakpoint"}), 371 | Button = def_enum({"Jump", "Dismount"}), 372 | ButtonStyle = def_enum({"Custom", "RobloxButtonDefault", "RobloxButton", 373 | "RobloxRoundButton", "RobloxRoundDefaultButton", "RobloxRoundDropdownButton"}), 374 | CameraMode = def_enum({"Classic", "LockFirstPerson"}), 375 | CameraPanMode = def_enum({"Classic", "EdgeBump"}), 376 | CameraType = def_enum({"Fixed", "Watch", "Attach", "Track", "Follow", "Custom", 377 | "Scriptable", "Orbital"}), 378 | CellBlock = def_enum({"Solid", "VerticalWedge", "CornerWedge", 379 | "InverseCornerWedge", "HorizontalWedge"}), 380 | CellMaterial = def_enum({"Empty", "Grass", "Sand", "Brick", "Granite", "Asphalt", 381 | "Iron", "Aluminum", "Gold", "WoodPlank", "WoodLog", "Gravel", "CinderBlock", 382 | "MossyStone", "Cement", "RedPlastic", "BluePlastic", "Water"}), 383 | CellOrientation = def_enum({"NegZ", "X", "Z", "NegX"}), 384 | CenterDialogType = def_enum({"UnsolicitedDialog", "PlayerInitiatedDialog", 385 | "ModalDialog", "QuitDialog"}), 386 | ChatCallbackType = def_enum({"OnCreatingChatWindow", "OnClientSendingMessage", 387 | "OnClientFormattingMessage", "OnServerReceivingMessage"}), 388 | ChatColor = def_enum({"Blue", "Green", "Red", "White"}), 389 | ChatMode = def_enum({"Menu", "TextAndMenu"}), 390 | ChatPrivacyMode = def_enum({"AllUsers", "NoOne", "Friends"}), 391 | ChatStyle = def_enum({"Classic", "Bubble", "ClassicAndBubble"}), 392 | CollisionFidelity = def_enum({"Default", "Hull", "Box", 393 | "PreciseConvexDecomposition"}), 394 | ComputerCameraMovementMode = def_enum({"Default", "Follow", "Classic", "Orbital", 395 | "CameraToggle"}), 396 | ComputerMovementMode = def_enum({"Default", "KeyboardMouse", "ClickToMove"}), 397 | ConnectionError = def_enum({"OK", "DisconnectErrors", "DisconnectBadhash", 398 | "DisconnectSecurityKeyMismatch", "DisconnectNewSecurityKeyMismatch", 399 | "DisconnectProtocolMismatch", "DisconnectReceivePacketError", 400 | "DisconnectReceivePacketStreamError", "DisconnectSendPacketError", 401 | "DisconnectIllegalTeleport", "DisconnectDuplicatePlayer", 402 | "DisconnectDuplicateTicket", "DisconnectTimeout", "DisconnectLuaKick", 403 | "DisconnectOnRemoteSysStats", "DisconnectHashTimeout", 404 | "DisconnectCloudEditKick", "DisconnectPlayerless", "DisconnectEvicted", 405 | "DisconnectDevMaintenance", "DisconnectRobloxMaintenance", "DisconnectRejoin", 406 | "DisconnectConnectionLost", "DisconnectIdle", "DisconnectRaknetErrors", 407 | "DisconnectWrongVersion", "DisconnectBySecurityPolicy", "DisconnectBlockedIP", 408 | "PlacelaunchErrors", "PlacelaunchDisabled", "PlacelaunchError", 409 | "PlacelaunchGameEnded", "PlacelaunchGameFull", "PlacelaunchUserLeft", 410 | "PlacelaunchRestricted", "PlacelaunchUnauthorized", "PlacelaunchFlooded", 411 | "PlacelaunchHashExpired", "PlacelaunchHashException", 412 | "PlacelaunchPartyCannotFit", "PlacelaunchHttpError", 413 | "PlacelaunchCustomMessage", "PlacelaunchOtherError", "TeleportErrors", 414 | "TeleportFailure", "TeleportGameNotFound", "TeleportGameEnded", 415 | "TeleportGameFull", "TeleportUnauthorized", "TeleportFlooded", 416 | "TeleportIsTeleporting"}), 417 | ConnectionState = def_enum({"Connected", "Disconnected"}), 418 | ContextActionPriority = def_enum({"Low", "Medium", "Default", "High"}), 419 | ContextActionResult = def_enum({"Pass", "Sink"}), 420 | ControlMode = def_enum({"MouseLockSwitch", "Classic"}), 421 | CoreGuiType = def_enum({"PlayerList", "Health", "Backpack", "Chat", "All", 422 | "EmotesMenu"}), 423 | CreatorType = def_enum({"User", "Group"}), 424 | CurrencyType = def_enum({"Default", "Robux", "Tix"}), 425 | CustomCameraMode = def_enum({"Default", "Follow", "Classic"}), 426 | DataStoreRequestType = def_enum({"GetAsync", "SetIncrementAsync", "UpdateAsync", 427 | "GetSortedAsync", "SetIncrementSortedAsync", "OnUpdate"}), 428 | DevCameraOcclusionMode = def_enum({"Zoom", "Invisicam"}), 429 | DevComputerCameraMovementMode = def_enum({"UserChoice", "Classic", "Follow", 430 | "Orbital", "CameraToggle"}), 431 | DevComputerMovementMode = def_enum({"UserChoice", "KeyboardMouse", "ClickToMove", 432 | "Scriptable"}), 433 | DevTouchCameraMovementMode = def_enum({"UserChoice", "Classic", "Follow", 434 | "Orbital"}), 435 | DevTouchMovementMode = def_enum({"UserChoice", "Thumbstick", "DPad", "Thumbpad", 436 | "ClickToMove", "Scriptable", "DynamicThumbstick"}), 437 | DeveloperMemoryTag = def_enum({"Internal", "HttpCache", "Instances", "Signals", 438 | "LuaHeap", "Script", "PhysicsCollision", "PhysicsParts", "GraphicsSolidModels", 439 | "GraphicsMeshParts", "GraphicsParticles", "GraphicsParts", 440 | "GraphicsSpatialHash", "GraphicsTerrain", "GraphicsTexture", 441 | "GraphicsTextureCharacter", "Sounds", "StreamingSounds", "TerrainVoxels", 442 | "Gui", "Animation", "Navigation"}), 443 | DeviceType = def_enum({"Unknown", "Desktop", "Tablet", "Phone"}), 444 | DialogBehaviorType = def_enum({"SinglePlayer", "MultiplePlayers"}), 445 | DialogPurpose = def_enum({"Quest", "Help", "Shop"}), 446 | DialogTone = def_enum({"Neutral", "Friendly", "Enemy"}), 447 | DominantAxis = def_enum({"Width", "Height"}), 448 | DraftStatusCode = def_enum({"OK", "DraftOutdated", "ScriptRemoved", 449 | "DraftCommitted"}), 450 | EasingDirection = def_enum({"In", "Out", "InOut"}), 451 | EasingStyle = def_enum({"Linear", "Sine", "Back", "Quad", "Quart", "Quint", 452 | "Bounce", "Elastic", "Exponential", "Circular", "Cubic"}), 453 | ElasticBehavior = def_enum({"WhenScrollable", "Always", "Never"}), 454 | EnviromentalPhysicsThrottle = def_enum({"DefaultAuto", "Disabled", "Always", 455 | "Skip2", "Skip4", "Skip8", "Skip16"}), 456 | ExplosionType = def_enum({"NoCraters", "Craters"}), 457 | FillDirection = def_enum({"Horizontal", "Vertical"}), 458 | FilterResult = def_enum({"Rejected", "Accepted"}), 459 | Font = def_enum({"Legacy", "Arial", "ArialBold", "SourceSans", "SourceSansBold", 460 | "SourceSansSemibold", "SourceSansLight", "SourceSansItalic", "Bodoni", 461 | "Garamond", "Cartoon", "Code", "Highway", "SciFi", "Arcade", "Fantasy", 462 | "Antique", "Gotham", "GothamSemibold", "GothamBold", "GothamBlack"}), 463 | FontSize = def_enum({"Size8", "Size9", "Size10", "Size11", "Size12", "Size14", 464 | "Size18", "Size24", "Size36", "Size48", "Size28", "Size32", "Size42", "Size60", 465 | "Size96"}), 466 | FormFactor = def_enum({"Symmetric", "Brick", "Plate", "Custom"}), 467 | FrameStyle = def_enum({"Custom", "ChatBlue", "RobloxSquare", "RobloxRound", 468 | "ChatGreen", "ChatRed", "DropShadow"}), 469 | FramerateManagerMode = def_enum({"Automatic", "On", "Off"}), 470 | FriendRequestEvent = def_enum({"Issue", "Revoke", "Accept", "Deny"}), 471 | FriendStatus = def_enum({"Unknown", "NotFriend", "Friend", "FriendRequestSent", 472 | "FriendRequestReceived"}), 473 | FunctionalTestResult = def_enum({"Passed", "Warning", "Error"}), 474 | GameAvatarType = def_enum({"R6", "R15", "PlayerChoice"}), 475 | GearGenreSetting = def_enum({"AllGenres", "MatchingGenreOnly"}), 476 | GearType = def_enum({"MeleeWeapons", "RangedWeapons", "Explosives", "PowerUps", 477 | "NavigationEnhancers", "MusicalInstruments", "SocialItems", "BuildingTools", 478 | "Transport"}), 479 | Genre = def_enum({"All", "TownAndCity", "Fantasy", "SciFi", "Ninja", "Scary", 480 | "Pirate", "Adventure", "Sports", "Funny", "WildWest", "War", "SkatePark", 481 | "Tutorial"}), 482 | GraphicsMode = def_enum({"Automatic", "Direct3D9", "Direct3D11", "OpenGL", "Metal", 483 | "Vulkan", "NoGraphics"}), 484 | HandlesStyle = def_enum({"Resize", "Movement"}), 485 | HorizontalAlignment = def_enum({"Center", "Left", "Right"}), 486 | HoverAnimateSpeed = def_enum({"VerySlow", "Slow", "Medium", "Fast", "VeryFast"}), 487 | HttpCachePolicy = def_enum({"None", "Full", "DataOnly", "Default", 488 | "InternalRedirectRefresh"}), 489 | HttpContentType = def_enum({"ApplicationJson", "ApplicationXml", 490 | "ApplicationUrlEncoded", "TextPlain", "TextXml"}), 491 | HttpError = def_enum({"OK", "InvalidUrl", "DnsResolve", "ConnectFail", 492 | "OutOfMemory", "TimedOut", "TooManyRedirects", "InvalidRedirect", "NetFail", 493 | "Aborted", "SslConnectFail", "Unknown"}), 494 | HttpRequestType = def_enum({"Default", "MarketplaceService", "Players", "Chat", 495 | "Avatar", "Analytics", "Localization"}), 496 | HumanoidCollisionType = def_enum({"OuterBox", "InnerBox"}), 497 | HumanoidDisplayDistanceType = def_enum({"Viewer", "Subject", "None"}), 498 | HumanoidHealthDisplayType = def_enum({"DisplayWhenDamaged", "AlwaysOn", 499 | "AlwaysOff"}), 500 | HumanoidRigType = def_enum({"R6", "R15"}), 501 | HumanoidStateType = def_enum({"FallingDown", "Running", "RunningNoPhysics", 502 | "Climbing", "StrafingNoPhysics", "Ragdoll", "GettingUp", "Jumping", "Landed", 503 | "Flying", "Freefall", "Seated", "PlatformStanding", "Dead", "Swimming", 504 | "Physics", "None"}), 505 | IKCollisionsMode = def_enum({"NoCollisions", "OtherMechanismsAnchored", 506 | "IncludeContactedMechanisms"}), 507 | InOut = def_enum({"Edge", "Inset", "Center"}), 508 | InfoType = def_enum({"Asset", "Product", "GamePass", "Subscription", "Bundle"}), 509 | InitialDockState = def_enum({"Top", "Bottom", "Left", "Right", "Float"}), 510 | InlineAlignment = def_enum({"Bottom", "Center", "Top"}), 511 | InputType = def_enum({"NoInput", "Constant", "Sin"}), 512 | JointCreationMode = def_enum({"All", "Surface", "None"}), 513 | KeyCode = def_enum({"Unknown", "Backspace", "Tab", "Clear", "Return", "Pause", 514 | "Escape", "Space", "QuotedDouble", "Hash", "Dollar", "Percent", "Ampersand", 515 | "Quote", "LeftParenthesis", "RightParenthesis", "Asterisk", "Plus", "Comma", 516 | "Minus", "Period", "Slash", "Zero", "One", "Two", "Three", "Four", "Five", 517 | "Six", "Seven", "Eight", "Nine", "Colon", "Semicolon", "LessThan", "Equals", 518 | "GreaterThan", "Question", "At", "LeftBracket", "BackSlash", "RightBracket", 519 | "Caret", "Underscore", "Backquote", "A", "B", "C", "D", "E", "F", "G", "H", 520 | "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", 521 | "Y", "Z", "LeftCurly", "Pipe", "RightCurly", "Tilde", "Delete", "KeypadZero", 522 | "KeypadOne", "KeypadTwo", "KeypadThree", "KeypadFour", "KeypadFive", 523 | "KeypadSix", "KeypadSeven", "KeypadEight", "KeypadNine", "KeypadPeriod", 524 | "KeypadDivide", "KeypadMultiply", "KeypadMinus", "KeypadPlus", "KeypadEnter", 525 | "KeypadEquals", "Up", "Down", "Right", "Left", "Insert", "Home", "End", 526 | "PageUp", "PageDown", "LeftShift", "RightShift", "LeftMeta", "RightMeta", 527 | "LeftAlt", "RightAlt", "LeftControl", "RightControl", "CapsLock", "NumLock", 528 | "ScrollLock", "LeftSuper", "RightSuper", "Mode", "Compose", "Help", "Print", 529 | "SysReq", "Break", "Menu", "Power", "Euro", "Undo", "F1", "F2", "F3", "F4", 530 | "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "F13", "F14", "F15", 531 | "World0", "World1", "World2", "World3", "World4", "World5", "World6", "World7", 532 | "World8", "World9", "World10", "World11", "World12", "World13", "World14", 533 | "World15", "World16", "World17", "World18", "World19", "World20", "World21", 534 | "World22", "World23", "World24", "World25", "World26", "World27", "World28", 535 | "World29", "World30", "World31", "World32", "World33", "World34", "World35", 536 | "World36", "World37", "World38", "World39", "World40", "World41", "World42", 537 | "World43", "World44", "World45", "World46", "World47", "World48", "World49", 538 | "World50", "World51", "World52", "World53", "World54", "World55", "World56", 539 | "World57", "World58", "World59", "World60", "World61", "World62", "World63", 540 | "World64", "World65", "World66", "World67", "World68", "World69", "World70", 541 | "World71", "World72", "World73", "World74", "World75", "World76", "World77", 542 | "World78", "World79", "World80", "World81", "World82", "World83", "World84", 543 | "World85", "World86", "World87", "World88", "World89", "World90", "World91", 544 | "World92", "World93", "World94", "World95", "ButtonX", "ButtonY", "ButtonA", 545 | "ButtonB", "ButtonR1", "ButtonL1", "ButtonR2", "ButtonL2", "ButtonR3", 546 | "ButtonL3", "ButtonStart", "ButtonSelect", "DPadLeft", "DPadRight", "DPadUp", 547 | "DPadDown", "Thumbstick1", "Thumbstick2"}), 548 | KeywordFilterType = def_enum({"Include", "Exclude"}), 549 | Language = def_enum({"Default"}), 550 | LanguagePreference = def_enum({"SystemDefault", "English", "SimplifiedChinese", 551 | "Korean"}), 552 | LeftRight = def_enum({"Left", "Center", "Right"}), 553 | LevelOfDetailSetting = def_enum({"High", "Medium", "Low"}), 554 | Limb = def_enum({"Head", "Torso", "LeftArm", "RightArm", "LeftLeg", "RightLeg", 555 | "Unknown"}), 556 | ListDisplayMode = def_enum({"Horizontal", "Vertical"}), 557 | ListenerType = def_enum({"Camera", "CFrame", "ObjectPosition", "ObjectCFrame"}), 558 | Material = def_enum({"Plastic", "Wood", "Slate", "Concrete", "CorrodedMetal", 559 | "DiamondPlate", "Foil", "Grass", "Ice", "Marble", "Granite", "Brick", "Pebble", 560 | "Sand", "Fabric", "SmoothPlastic", "Metal", "WoodPlanks", "Cobblestone", "Air", 561 | "Water", "Rock", "Glacier", "Snow", "Sandstone", "Mud", "Basalt", "Ground", 562 | "CrackedLava", "Neon", "Glass", "Asphalt", "LeafyGrass", "Salt", "Limestone", 563 | "Pavement", "ForceField"}), 564 | MembershipType = def_enum({"None", "BuildersClub", "TurboBuildersClub", 565 | "OutrageousBuildersClub", "Premium"}), 566 | MeshType = def_enum({"Head", "Torso", "Wedge", "Prism", "Pyramid", "ParallelRamp", 567 | "RightAngleRamp", "CornerWedge", "Brick", "Sphere", "Cylinder", "FileMesh"}), 568 | MessageType = def_enum({"MessageOutput", "MessageInfo", "MessageWarning", 569 | "MessageError"}), 570 | ModifierKey = def_enum({"Alt", "Ctrl", "Meta", "Shift"}), 571 | MouseBehavior = def_enum({"Default", "LockCenter", "LockCurrentPosition"}), 572 | MoveState = def_enum({"Stopped", "Coasting", "Pushing", "Stopping", "AirFree"}), 573 | NameOcclusion = def_enum({"OccludeAll", "EnemyOcclusion", "NoOcclusion"}), 574 | NetworkOwnership = def_enum({"Automatic", "Manual", "OnContact"}), 575 | NormalId = def_enum({"Top", "Bottom", "Back", "Front", "Right", "Left"}), 576 | OutputLayoutMode = def_enum({"Horizontal", "Vertical"}), 577 | OverrideMouseIconBehavior = def_enum({"None", "ForceShow", "ForceHide"}), 578 | PacketPriority = def_enum({"IMMEDIATE_PRIORITY", "HIGH_PRIORITY", 579 | "MEDIUM_PRIORITY", "LOW_PRIORITY"}), 580 | PartType = def_enum({"Ball", "Block", "Cylinder"}), 581 | PathStatus = def_enum({"Success", "ClosestNoPath", "ClosestOutOfRange", 582 | "FailStartNotEmpty", "FailFinishNotEmpty", "NoPath"}), 583 | PathWaypointAction = def_enum({"Walk", "Jump"}), 584 | PermissionLevelShown = def_enum({"Game", "RobloxGame", "RobloxScript", "Studio", 585 | "Roblox"}), 586 | Platform = def_enum({"Windows", "OSX", "IOS", "Android", "XBoxOne", "PS4", "PS3", 587 | "XBox360", "WiiU", "NX", "Ouya", "AndroidTV", "Chromecast", "Linux", "SteamOS", 588 | "WebOS", "DOS", "BeOS", "UWP", "None"}), 589 | PlaybackState = def_enum({"Begin", "Delayed", "Playing", "Paused", "Completed", 590 | "Cancelled"}), 591 | PlayerActions = def_enum({"CharacterForward", "CharacterBackward", "CharacterLeft", 592 | "CharacterRight", "CharacterJump"}), 593 | PlayerChatType = def_enum({"All", "Team", "Whisper"}), 594 | PoseEasingDirection = def_enum({"Out", "InOut", "In"}), 595 | PoseEasingStyle = def_enum({"Linear", "Constant", "Elastic", "Cubic", "Bounce"}), 596 | PrivilegeType = def_enum({"Owner", "Admin", "Member", "Visitor", "Banned"}), 597 | ProductPurchaseDecision = def_enum({"NotProcessedYet", "PurchaseGranted"}), 598 | QualityLevel = def_enum({"Automatic", "Level01", "Level02", "Level03", "Level04", 599 | "Level05", "Level06", "Level07", "Level08", "Level09", "Level10", "Level11", 600 | "Level12", "Level13", "Level14", "Level15", "Level16", "Level17", "Level18", 601 | "Level19", "Level20", "Level21"}), 602 | R15CollisionType = def_enum({"OuterBox", "InnerBox"}), 603 | RaycastFilterType = def_enum({"Blacklist", "Whitelist"}), 604 | RenderFidelity = def_enum({"Automatic", "Precise"}), 605 | RenderPriority = def_enum({"First", "Input", "Camera", "Character", "Last"}), 606 | RenderingTestComparisonMethod = def_enum({"psnr", "diff"}), 607 | ReturnKeyType = def_enum({"Default", "Done", "Go", "Next", "Search", "Send"}), 608 | ReverbType = def_enum({"NoReverb", "GenericReverb", "PaddedCell", "Room", 609 | "Bathroom", "LivingRoom", "StoneRoom", "Auditorium", "ConcertHall", "Cave", 610 | "Arena", "Hangar", "CarpettedHallway", "Hallway", "StoneCorridor", "Alley", 611 | "Forest", "City", "Mountains", "Quarry", "Plain", "ParkingLot", "SewerPipe", 612 | "UnderWater"}), 613 | RibbonTool = def_enum({"Select", "Scale", "Rotate", "Move", "Transform", 614 | "ColorPicker", "MaterialPicker", "Group", "Ungroup", "None"}), 615 | RollOffMode = def_enum({"Inverse", "Linear", "InverseTapered", "LinearSquare"}), 616 | RotationType = def_enum({"MovementRelative", "CameraRelative"}), 617 | RuntimeUndoBehavior = def_enum({"Aggregate", "Snapshot", "Hybrid"}), 618 | SaveFilter = def_enum({"SaveAll", "SaveWorld", "SaveGame"}), 619 | SavedQualitySetting = def_enum({"Automatic", "QualityLevel1", "QualityLevel2", 620 | "QualityLevel3", "QualityLevel4", "QualityLevel5", "QualityLevel6", 621 | "QualityLevel7", "QualityLevel8", "QualityLevel9", "QualityLevel10"}), 622 | ScaleType = def_enum({"Stretch", "Slice", "Tile", "Fit", "Crop"}), 623 | ScreenOrientation = def_enum({"LandscapeLeft", "LandscapeRight", "LandscapeSensor", 624 | "Portrait", "Sensor"}), 625 | ScrollBarInset = def_enum({"None", "ScrollBar", "Always"}), 626 | ScrollingDirection = def_enum({"X", "Y", "XY"}), 627 | ServerAudioBehavior = def_enum({"Enabled", "Muted", "OnlineGame"}), 628 | SizeConstraint = def_enum({"RelativeXY", "RelativeXX", "RelativeYY"}), 629 | SortOrder = def_enum({"LayoutOrder", "Name", "Custom"}), 630 | SoundType = def_enum({"NoSound", "Boing", "Bomb", "Break", "Click", "Clock", 631 | "Slingshot", "Page", "Ping", "Snap", "Splat", "Step", "StepOn", "Swoosh", 632 | "Victory"}), 633 | SpecialKey = def_enum({"Insert", "Home", "End", "PageUp", "PageDown", "ChatHotkey"}), 634 | StartCorner = def_enum({"TopLeft", "TopRight", "BottomLeft", "BottomRight"}), 635 | Status = def_enum({"Poison", "Confusion"}), 636 | StreamingPauseMode = def_enum({"Default", "Disabled", "ClientPhysicsPause"}), 637 | StudioDataModelType = def_enum({"Edit", "PlayClient", "PlayServer", "RobloxPlugin", 638 | "UserPlugin", "None"}), 639 | StudioStyleGuideColor = def_enum({"MainBackground", "Titlebar", "Dropdown", 640 | "Tooltip", "Notification", "ScrollBar", "ScrollBarBackground", "TabBar", "Tab", 641 | "RibbonTab", "RibbonTabTopBar", "Button", "MainButton", "RibbonButton", 642 | "ViewPortBackground", "InputFieldBackground", "Item", "TableItem", 643 | "CategoryItem", "GameSettingsTableItem", "GameSettingsTooltip", "EmulatorBar", 644 | "EmulatorDropDown", "ColorPickerFrame", "CurrentMarker", "Border", "Shadow", 645 | "Light", "Dark", "Mid", "MainText", "SubText", "TitlebarText", "BrightText", 646 | "DimmedText", "LinkText", "WarningText", "ErrorText", "InfoText", 647 | "SensitiveText", "ScriptSideWidget", "ScriptBackground", "ScriptText", 648 | "ScriptSelectionText", "ScriptSelectionBackground", 649 | "ScriptFindSelectionBackground", "ScriptMatchingWordSelectionBackground", 650 | "ScriptOperator", "ScriptNumber", "ScriptString", "ScriptComment", 651 | "ScriptPreprocessor", "ScriptKeyword", "ScriptBuiltInFunction", 652 | "ScriptWarning", "ScriptError", "ScriptWhitespace", "DebuggerCurrentLine", 653 | "DebuggerErrorLine", "DiffFilePathText", "DiffTextHunkInfo", 654 | "DiffTextNoChange", "DiffTextAddition", "DiffTextDeletion", 655 | "DiffTextSeparatorBackground", "DiffTextNoChangeBackground", 656 | "DiffTextAdditionBackground", "DiffTextDeletionBackground", "DiffLineNum", 657 | "DiffLineNumSeparatorBackground", "DiffLineNumNoChangeBackground", 658 | "DiffLineNumAdditionBackground", "DiffLineNumDeletionBackground", 659 | "DiffFilePathBackground", "DiffFilePathBorder", "Separator", "ButtonBorder", 660 | "ButtonText", "InputFieldBorder", "CheckedFieldBackground", 661 | "CheckedFieldBorder", "CheckedFieldIndicator", "HeaderSection", "Midlight", 662 | "StatusBar", "DialogButton", "DialogButtonText", "DialogButtonBorder", 663 | "DialogMainButton", "DialogMainButtonText"}), 664 | StudioStyleGuideModifier = def_enum({"Default", "Selected", "Pressed", "Disabled", 665 | "Hover"}), 666 | Style = def_enum({"AlternatingSupports", "BridgeStyleSupports", "NoSupports"}), 667 | SurfaceConstraint = def_enum({"None", "Hinge", "SteppingMotor", "Motor"}), 668 | SurfaceGuiSizingMode = def_enum({"FixedSize", "PixelsPerStud"}), 669 | SurfaceType = def_enum({"Smooth", "Glue", "Weld", "Studs", "Inlet", "Universal", 670 | "Hinge", "Motor", "SteppingMotor", "SmoothNoOutlines"}), 671 | SwipeDirection = def_enum({"Right", "Left", "Up", "Down", "None"}), 672 | TableMajorAxis = def_enum({"RowMajor", "ColumnMajor"}), 673 | Technology = def_enum({"Compatibility", "Voxel", "ShadowMap", "Legacy"}), 674 | TeleportResult = def_enum({"Success", "Failure", "GameNotFound", "GameEnded", 675 | "GameFull", "Unauthorized", "Flooded", "IsTeleporting"}), 676 | TeleportState = def_enum({"RequestedFromServer", "Started", "WaitingForServer", 677 | "Failed", "InProgress"}), 678 | TeleportType = def_enum({"ToPlace", "ToInstance", "ToReservedServer"}), 679 | TextFilterContext = def_enum({"PublicChat", "PrivateChat"}), 680 | TextInputType = def_enum({"Default", "NoSuggestions", "Number", "Email", "Phone", 681 | "Password"}), 682 | TextTruncate = def_enum({"None", "AtEnd"}), 683 | TextXAlignment = def_enum({"Left", "Center", "Right"}), 684 | TextYAlignment = def_enum({"Top", "Center", "Bottom"}), 685 | TextureMode = def_enum({"Stretch", "Wrap", "Static"}), 686 | TextureQueryType = def_enum({"NonHumanoid", "NonHumanoidOrphaned", "Humanoid", 687 | "HumanoidOrphaned"}), 688 | ThreadPoolConfig = def_enum({"Auto", "PerCore1", "PerCore2", "PerCore3", 689 | "PerCore4", "Threads1", "Threads2", "Threads3", "Threads4", "Threads8", 690 | "Threads16"}), 691 | ThrottlingPriority = def_enum({"Extreme", "ElevatedOnServer", "Default"}), 692 | ThumbnailSize = def_enum({"Size48x48", "Size180x180", "Size420x420", "Size60x60", 693 | "Size100x100", "Size150x150", "Size352x352"}), 694 | ThumbnailType = def_enum({"HeadShot", "AvatarBust", "AvatarThumbnail"}), 695 | TickCountSampleMethod = def_enum({"Fast", "Benchmark", "Precise"}), 696 | TopBottom = def_enum({"Top", "Center", "Bottom"}), 697 | TouchCameraMovementMode = def_enum({"Default", "Follow", "Classic", "Orbital"}), 698 | TouchMovementMode = def_enum({"Default", "Thumbstick", "DPad", "Thumbpad", 699 | "ClickToMove", "DynamicThumbstick"}), 700 | TweenStatus = def_enum({"Canceled", "Completed"}), 701 | UITheme = def_enum({"Light", "Dark"}), 702 | UiMessageType = def_enum({"UiMessageError", "UiMessageInfo"}), 703 | UploadSetting = def_enum({"Never", "Ask", "Always"}), 704 | UserCFrame = def_enum({"Head", "LeftHand", "RightHand"}), 705 | UserInputState = def_enum({"Begin", "Change", "End", "Cancel", "None"}), 706 | UserInputType = def_enum({"MouseButton1", "MouseButton2", "MouseButton3", 707 | "MouseWheel", "MouseMovement", "Touch", "Keyboard", "Focus", "Accelerometer", 708 | "Gyro", "Gamepad1", "Gamepad2", "Gamepad3", "Gamepad4", "Gamepad5", "Gamepad6", 709 | "Gamepad7", "Gamepad8", "TextInput", "InputMethod", "None"}), 710 | VRTouchpad = def_enum({"Left", "Right"}), 711 | VRTouchpadMode = def_enum({"Touch", "VirtualThumbstick", "ABXY"}), 712 | VerticalAlignment = def_enum({"Center", "Top", "Bottom"}), 713 | VerticalScrollBarPosition = def_enum({"Left", "Right"}), 714 | VibrationMotor = def_enum({"Large", "Small", "LeftTrigger", "RightTrigger", 715 | "LeftHand", "RightHand"}), 716 | VideoQualitySettings = def_enum({"LowResolution", "MediumResolution", 717 | "HighResolution"}), 718 | VirtualInputMode = def_enum({"Recording", "Playing", "None"}), 719 | WaterDirection = def_enum({"NegX", "X", "NegY", "Y", "NegZ", "Z"}), 720 | WaterForce = def_enum({"None", "Small", "Medium", "Strong", "Max"}), 721 | ZIndexBehavior = def_enum({"Global", "Sibling"}), 722 | } 723 | } 724 | }, 725 | } 726 | 727 | stds.testez = { 728 | read_globals = { 729 | "describe", 730 | "it", "itFOCUS", "itSKIP", 731 | "FOCUS", "SKIP", "HACK_NO_XPCALL", 732 | "expect", 733 | } 734 | } 735 | 736 | stds.plugin = { 737 | read_globals = { 738 | "plugin", 739 | "DebuggerManager", 740 | } 741 | } 742 | 743 | ignore = { 744 | "212", -- unused arguments 745 | } 746 | 747 | std = "lua51+roblox" 748 | 749 | files["**/*.spec.lua"] = { 750 | std = "+testez", 751 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2014-2018 Quenty 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | 24 | 'StringMatcher.lua' contains code with the following copyright: 25 | 26 | Copyright 2011-2012 Nils Nordman 27 | 28 | with the license being the same as the above. 29 | 30 | -------------------------------------------------------------------------------- /buildPlugin.rbxl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quenty/ClassConverterPlugin/3208f88ecfae57abf0b1219a88ac31d4ea4d1631/buildPlugin.rbxl -------------------------------------------------------------------------------- /classConverter.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": "." 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TradeEnterprise", 3 | "tree": { 4 | "$className": "DataModel", 5 | "StarterGui": { 6 | "$className": "StarterGui", 7 | "$properties": { 8 | "HttpEnabled": true 9 | }, 10 | "QuentyClassConverter": { 11 | "$className": "Folder", 12 | "Plugin": { 13 | "$path": "src" 14 | } 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /images/ConverterLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quenty/ClassConverterPlugin/3208f88ecfae57abf0b1219a88ac31d4ea4d1631/images/ConverterLogo.png -------------------------------------------------------------------------------- /images/screenshots/UIExample.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quenty/ClassConverterPlugin/3208f88ecfae57abf0b1219a88ac31d4ea4d1631/images/screenshots/UIExample.PNG -------------------------------------------------------------------------------- /images/screenshots/UIExampleFilter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quenty/ClassConverterPlugin/3208f88ecfae57abf0b1219a88ac31d4ea4d1631/images/screenshots/UIExampleFilter.png -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

Quenty's Class Converter

2 | 3 | Convert between classes, loading from Anaminus's API dump. Inspired by Stravant's ClassChanger. With help from badcc, AxisAngles, Anaminus and Stravant. 4 | 5 | You can find the published version [here](https://www.roblox.com/library/906681627/Quentys-Class-Converter). 6 | 7 | 8 | ### Installing 9 | 10 | 1. Open up Roblox Studio 11 | 2. Open up a new place, go to the Plugins tab, then manage plugins 12 | 3. Navigate to https://www.roblox.com/library/906681627/Quentys-Class-Converter 13 | 4. Press Install 14 | 15 | ### Features 16 | 17 | * Always up to date API 18 | * Fuzzy searching 19 | * Nice UI 20 | * Converts CollectionService tags 21 | 22 | ### Caveats 23 | 24 | * Requires HttpEnabled to be on 25 | 26 | ### UI 27 | 28 | Here's an example of the UI 29 | 30 | ![alt text](images/screenshots/UIExampleFilter.png "Example of plugin interface while selecting a new class") 31 | 32 | ### Building the plugin 33 | This plugin is built with [Rojo](https://github.com/LPGhatguy/rojo) (Version 0.6.0) 34 | 35 | Run 36 | 37 | ```rojo serve``` 38 | 39 | from the root directory and then sync using the Rojo plugin. 40 | 41 | 42 | ### Syntax checking 43 | Syntax checking is done with LuaCheck 44 | 45 | ### Dependencies 46 | This plugin uses http://anaminus.github.io/rbx/json/api/latest.json to grab updates 47 | -------------------------------------------------------------------------------- /src/Converter.lua: -------------------------------------------------------------------------------- 1 | local HttpService = game:GetService("HttpService") 2 | local CollectionService = game:GetService("CollectionService") 3 | 4 | local Signal = require(script.Parent:WaitForChild("Signal")) 5 | local StringMatcher = require(script.Parent:WaitForChild("StringMatcher")) 6 | 7 | local Converter = {} 8 | Converter.ClassName = "Converter" 9 | Converter.__index = Converter 10 | Converter.ApiSettingCacheName = "AnaminusAPICache" 11 | Converter.MaxCacheSettingsTime = 60*60*24 -- 1 day 12 | Converter.SearchCache = false -- TODO: Try enabling cache 13 | Converter.ServiceNameMap = setmetatable({}, { 14 | __index = function(self, className) 15 | local isService = className:find("Service$") 16 | or className:find("Provider$") 17 | or className:find("Settings$") 18 | 19 | if not isService then 20 | -- Try to find the service 21 | pcall(function() 22 | isService = game:FindService(className) 23 | end) 24 | end 25 | 26 | self[className] = isService 27 | return isService 28 | end 29 | }) 30 | 31 | 32 | Converter.BaseGroups = { 33 | Container = { 34 | 'Folder', 35 | 'Model', 36 | 'Configuration', 37 | 'Backpack', 38 | }, 39 | Part = { 40 | 'Part', 41 | 'WedgePart', 42 | 'MeshPart', 43 | 'TrussPart', 44 | 'Seat', 45 | 'VehicleSeat', 46 | 'SpawnLocation', 47 | 'CornerWedgePart', 48 | }, 49 | Event = { 50 | 'RemoteEvent', 51 | 'RemoteFunction', 52 | 'BindableEvent', 53 | 'BindableFunction' 54 | }, 55 | Particle = { 56 | 'Smoke', 57 | 'Fire', 58 | 'Sparkles', 59 | 'Trail', 60 | 'ParticleEmitter', 61 | 'Explosion', 62 | }, 63 | Value = '^.*Value$', 64 | Mesh = '^.*Mesh$' 65 | } 66 | 67 | 68 | for _, item in pairs({ 69 | "Workspace", 70 | "Debris", 71 | "Players", 72 | "Lighting", 73 | "ReplicatedFirst", 74 | "ReplicatedStorage", 75 | "StarterGui", 76 | "StarterPack", 77 | "Teams", 78 | "Chat"}) do 79 | Converter.ServiceNameMap[item] = true 80 | end 81 | 82 | function Converter.new(IS_DEBUG_MODE) 83 | local self = setmetatable({}, Converter) 84 | 85 | self.IS_DEBUG_MODE = IS_DEBUG_MODE 86 | self.HttpNotEnabled = false 87 | self.HttpNotEnabledChanged = Signal.new() 88 | 89 | return self 90 | end 91 | 92 | --[[ 93 | function Converter:GetPriority(ClassName) 94 | if IconHandler:HasIcon(ClassName) then 95 | return 5 96 | end 97 | 98 | return 0 99 | end--]] 100 | 101 | function Converter:CanConvert(selection) 102 | local classes = self:GetClassesMap() 103 | if not classes then 104 | return false 105 | end 106 | 107 | local commonAncestorsAndGroupsMap 108 | for _, item in pairs(selection) do 109 | local Class = classes[item.ClassName] 110 | if not Class then 111 | return false 112 | end 113 | 114 | if not commonAncestorsAndGroupsMap then 115 | commonAncestorsAndGroupsMap = {} 116 | -- Make a copy because original table is cached 117 | for child, _ in pairs(Class:GetAncestorsAndGroups()) do 118 | commonAncestorsAndGroupsMap[child] = true 119 | end 120 | else 121 | -- Make sure we've got something in common with (set intersection) 122 | local newCommon = {} 123 | for child, _ in pairs(Class:GetAncestorsAndGroups()) do 124 | if commonAncestorsAndGroupsMap[child] then 125 | newCommon[child] = true 126 | end 127 | end 128 | commonAncestorsAndGroupsMap = newCommon 129 | end 130 | end 131 | 132 | return next(commonAncestorsAndGroupsMap) ~= nil 133 | end 134 | 135 | -- We use the plugin to cache content 136 | function Converter:WithPluginForCache(pluginForCache) 137 | self._pluginForCache = pluginForCache or error("No pluginForCache") 138 | 139 | return self 140 | end 141 | 142 | function Converter:IsNoHttp() 143 | return self.HttpNotEnabled 144 | end 145 | 146 | function Converter:GetAPIAsync(isHttpEnabledRetry) 147 | if not self.API then 148 | if self.SearchCache and self._pluginForCache and self:IsNoHttp() then 149 | warn("[Converter] - No HTTP, searching for cache!") 150 | 151 | local cacheResult = self._pluginForCache:GetSetting(self.ApiSettingCacheName) 152 | if cacheResult then 153 | local Age = os.time() - cacheResult.CacheTime 154 | if Age <= self.MaxCacheSettingsTime then 155 | local Hours = math.floor((Age / 60 / 60)) 156 | local Minutes = math.floor((Age/60) % 60) 157 | warn(("[Converter] - Restoring from cache! Results may be out of date. Age: %d:%d"):format(Hours, Minutes)) 158 | self.API = cacheResult.Data 159 | return 160 | else 161 | -- Wipe cache! 162 | self._pluginForCache:SetSetting(self.ApiSettingCacheName, nil) 163 | end 164 | end 165 | end 166 | 167 | 168 | if self.APIPending then 169 | return self.APIPending:wait() 170 | end 171 | 172 | self.APIPending = Signal.new() 173 | delay(5, function() 174 | if self.APIPending then 175 | self.APIPending:fire(nil, "Timed out") 176 | end 177 | end) 178 | 179 | spawn(function() 180 | if isHttpEnabledRetry then 181 | print("[Converter] - API request sent") 182 | end 183 | local ok, err = pcall(function() 184 | self.API = HttpService:JSONDecode(HttpService:GetAsync('http://anaminus.github.io/rbx/json/api/latest.json')) 185 | end) 186 | if isHttpEnabledRetry then 187 | print(("[Converter] - Done retrieving API (%s)"):format(ok and "Success" or ("Failed, '%s'"):format(tostring(err)))) 188 | end 189 | 190 | if self.API then 191 | -- Cache result for no-http points! 192 | self._pluginForCache:SetSetting(self.ApiSettingCacheName, { 193 | CacheTime = os.time(); 194 | Data = self.API; 195 | }) 196 | end 197 | 198 | if ok then 199 | self.HttpNotEnabled = false 200 | self.HttpNotEnabledChanged:fire() 201 | else 202 | self.HttpNotEnabled = (err or ""):find("not enabled") 203 | self.HttpNotEnabledChanged:fire() 204 | err = err or "Error, no extra data" 205 | 206 | if self.IS_DEBUG_MODE then 207 | warn(("[Converter] - Failed to retrieve API due to '%s'"):format(tostring(err))) 208 | end 209 | end 210 | 211 | self.APIPending:fire(self.API, err) 212 | self.APIPending = nil 213 | end) 214 | 215 | self.APIPending:wait() 216 | end 217 | 218 | return self.API 219 | end 220 | 221 | 222 | function Converter:GetSuggested(selection, settings) 223 | local classes = self:GetClassesMap() 224 | if not classes then 225 | return nil 226 | end 227 | 228 | local rankMap = {} 229 | if settings.Filter then 230 | local matches = self.StringMatcher:match(settings.Filter) 231 | for index, Match in pairs(matches) do 232 | rankMap[classes[Match]] = #matches - index 233 | end 234 | else 235 | local function Explore(baseClass) 236 | local queue = {{baseClass, 0}} 237 | 238 | -- Breadth first search 239 | while #queue > 0 do 240 | local Class, Rank = unpack(table.remove(queue, 1)) 241 | if not rankMap[Class] then 242 | local ExtraRank = 0 243 | for _, Group in pairs(Class.Groups) do 244 | if baseClass.Groups[Group.Name] then 245 | ExtraRank = ExtraRank + 10000 246 | end 247 | end 248 | rankMap[Class] = Rank + ExtraRank 249 | 250 | for _, Item in pairs(Class.Children) do 251 | table.insert(queue, {Item, Rank + 10}) 252 | end 253 | 254 | if Class.Superclass then 255 | table.insert(queue, {Class.Superclass, Rank - 100}) 256 | end 257 | end 258 | end 259 | 260 | rankMap[baseClass] = nil 261 | end 262 | 263 | -- Exploration 264 | local _, selected = next(selection) 265 | if selected then 266 | local class = classes[selected.ClassName] 267 | if not class then 268 | warn(("[Converter] - Bad class name '%s'"):format(selected.ClassName)) 269 | return nil 270 | end 271 | Explore(class) 272 | else 273 | Explore(classes["Instance"]) 274 | end 275 | end 276 | 277 | local services = self.ServiceNameMap 278 | 279 | local function doInclude(class) 280 | return (not class.Tags.notbrowsable or settings.IncludeNotBrowsable) 281 | and (not class.Tags.notCreatable or settings.IncludeNotCreatable) 282 | and (not services[class.ClassName] or settings.IncludeServices) 283 | --[[ 284 | if not Settings.IncludeNotBrowsable then 285 | 286 | end--]] 287 | end 288 | 289 | -- Remove current class from thing 290 | -- rankMap[Class] = nil 291 | 292 | local options = {} 293 | for _, Class in pairs(classes) do 294 | if rankMap[Class] and doInclude(Class) then 295 | table.insert(options, Class) 296 | end 297 | end 298 | table.sort(options, function(A, B) 299 | if rankMap[A] == rankMap[B] then 300 | return A.ClassName < B.ClassName 301 | end 302 | return rankMap[A] > rankMap[B] 303 | end) 304 | return options 305 | end 306 | 307 | local ClassMetatable = {} 308 | ClassMetatable.__index = ClassMetatable 309 | 310 | function ClassMetatable:IsA(type) 311 | if not self then 312 | return false 313 | elseif self.ClassName == type then 314 | return true 315 | elseif self.Superclass then 316 | return self.Superclass:IsA(type) 317 | else 318 | return false 319 | end 320 | end 321 | 322 | function ClassMetatable:GetAllProperties() 323 | if self.AllProperties then 324 | return self.AllProperties 325 | end 326 | 327 | local Properties = {} 328 | local Current = self 329 | 330 | while Current do 331 | for _, Property in pairs(Current.Properties) do 332 | Properties[Property.Name] = Property 333 | end 334 | Current = Current.Superclass 335 | end 336 | self.AllProperties = Properties 337 | return Properties 338 | end 339 | 340 | function ClassMetatable:GetAncestorsAndGroups() 341 | if self.AncestorsAndGroups then 342 | return self.AncestorsAndGroups 343 | end 344 | local Map = {} 345 | for _, Group in pairs(self.Groups) do 346 | Map[Group] = true 347 | end 348 | 349 | local Current = self 350 | while Current and Current.ClassName ~= "Instance" do 351 | for _, Group in pairs(Current.Groups) do 352 | Map[Group] = true 353 | end 354 | Map[Current] = true 355 | Current = Current.Superclass 356 | end 357 | self.AncestorsAndGroups = Map 358 | return Map 359 | end 360 | 361 | function Converter:GetLoadedText() 362 | return self.LoadedText or "Not loaded" 363 | end 364 | 365 | function Converter:GetClassesMap() 366 | if self._classes then 367 | return self._classes 368 | end 369 | 370 | local API = self:GetAPIAsync() 371 | if not API then 372 | return nil 373 | end 374 | 375 | local Classes = {} 376 | local Properties = {} 377 | local ClassCount = 0 378 | local PropertyCount = 0 379 | for _, Data in pairs(self.API) do 380 | if Data.type == "Class" then 381 | ClassCount = ClassCount + 1 382 | local Class = setmetatable({ 383 | ClassName = Data.Name; 384 | Children = {}; 385 | OriginalData = Data; 386 | Tags = {}; 387 | Properties = {}; 388 | -- Parent = nil; 389 | Groups = {}; 390 | }, ClassMetatable) 391 | 392 | if Data.tags then 393 | for _, Tag in pairs(Data.tags) do 394 | Class.Tags[Tag] = true 395 | end 396 | end 397 | Classes[Data.Name] = Class 398 | elseif Data.type == "Property" then 399 | PropertyCount = PropertyCount + 1 400 | Properties[Data] = { 401 | Name = Data.Name; 402 | OriginalData = Data; 403 | Classes = {}; 404 | -- Class = nil; 405 | } 406 | end 407 | end 408 | self.LoadedText = ("Loaded " .. ClassCount .. " classes and " .. PropertyCount .. " properties") 409 | 410 | for _, Property in pairs(Properties) do 411 | local Class = Classes[Property.OriginalData.Class] 412 | if Class then 413 | Class.Properties[Property.Name] = Property; 414 | table.insert(Property.Classes, Class) 415 | else 416 | warn("No class found for property '%s'"):format(tostring(Property.Name)) 417 | end 418 | end 419 | 420 | -- Calculate Superclass and Children 421 | for _, Class in pairs(Classes) do 422 | local SuperclassName = Class.OriginalData.Superclass 423 | if SuperclassName and Classes[SuperclassName] then 424 | local Superclass = Classes[SuperclassName] 425 | table.insert(Superclass.Children, Class) 426 | Class.Superclass = Superclass; 427 | end 428 | end 429 | self._classes = Classes 430 | 431 | -- Calculate groups 432 | local Groups = {} 433 | 434 | local function AddToGroup(Group, GroupData) 435 | if type(GroupData) == "string" then 436 | if self._classes[GroupData] then 437 | AddToGroup(Group, self._classes[GroupData]) 438 | else 439 | assert(GroupData:sub(#GroupData,#GroupData) == "$") 440 | 441 | for _, Class in pairs(self._classes) do 442 | if Class.ClassName:find(GroupData) then 443 | AddToGroup(Group, Class) 444 | end 445 | end 446 | end 447 | elseif type(GroupData) == "table" then 448 | if not getmetatable(GroupData) then 449 | for _, Data in pairs(GroupData) do 450 | AddToGroup(Group, Data) 451 | end 452 | else 453 | -- Class 454 | if not GroupData.Groups[Group.Name] then 455 | table.insert(Group.Classes, GroupData) 456 | GroupData.Groups[Group.Name] = Group 457 | end 458 | end 459 | else 460 | error("Bad group type") 461 | end 462 | end 463 | 464 | for GroupName, GroupData in pairs(self.BaseGroups) do 465 | local Group = { 466 | Name = GroupName; 467 | Classes = {}; 468 | } 469 | AddToGroup(Group, GroupData) 470 | table.insert(Groups, Group) 471 | end 472 | self.Groups = Groups 473 | 474 | 475 | local Options = {} 476 | for _, Class in pairs(Classes) do 477 | table.insert(Options, Class.ClassName) 478 | end 479 | self.StringMatcher = StringMatcher.new(Options, true, true) 480 | 481 | return self._classes 482 | end 483 | 484 | function Converter:ChangeClass(object, ClassName) 485 | local Classes = self:GetClassesMap() 486 | if not Classes then 487 | warn("[Converter] - No API loaded") 488 | return nil 489 | end 490 | 491 | local newObject 492 | local ok, err = pcall(function() 493 | newObject = Instance.new(ClassName) 494 | end) 495 | if not ok then 496 | warn(("[Converter] - Failed to make new instance '%s' due to '%s'"):format(tostring(ClassName), tostring(err))) 497 | return 498 | end 499 | if not newObject then 500 | warn(("[Converter] - Failed to instantiate '%s'"):format(tostring(ClassName))) 501 | return nil 502 | end 503 | 504 | --[[ 505 | local function Recurse(ClassName, object, newObject) 506 | for _,v in next, Classes do 507 | if (v['type'] == 'Class' and v['Name'] == ClassName and v['Superclass']) then 508 | Recurse(v['Superclass'], object, newObject) 509 | elseif (v['type'] == 'Property' and v['Class'] == ClassName) then 510 | if Changed[newObject] then 511 | pcall(function() -- If property is not allowed to be changed, do not error. 512 | newObject[v.Name] = object[v.Name] 513 | end) 514 | end 515 | end 516 | end 517 | end 518 | 519 | Recurse(object.ClassName, object, newObject)--]] 520 | 521 | 522 | local currentClass = Classes[object.ClassName] 523 | local newClass = Classes[ClassName] 524 | 525 | if not currentClass then 526 | warn(("[Converter] - Failed to find class for '%s'"):format(tostring(object.ClassName))) 527 | return nil 528 | end 529 | if not newClass then 530 | warn(("[Converter] - Failed to find class for '%s'"):format(tostring(ClassName))) 531 | return nil 532 | end 533 | 534 | local currentProperties = currentClass:GetAllProperties() 535 | local newProperties = newClass:GetAllProperties() 536 | for propertyName, _ in pairs(currentProperties) do 537 | if propertyName ~= "Parent" and newProperties[propertyName] then 538 | pcall(function() -- If property is not allowed to be changed, do not error. 539 | newObject[propertyName] = object[propertyName] 540 | end) 541 | end 542 | end 543 | 544 | -- Tag instance 545 | for _, Tag in pairs(CollectionService:GetTags(object)) do 546 | CollectionService:AddTag(newObject, Tag) 547 | end 548 | 549 | -- Go through each child and identify properties that point towards the parent that's getting replaced 550 | local oldParent = object.Parent 551 | local descendantList = object:GetChildren() 552 | local descendantPropertyMap = self:GetDescendantPropertyMap(descendantList, object, newObject) 553 | 554 | for _, descendant in pairs(descendantList) do 555 | if descendantPropertyMap[descendant] then 556 | for Property, NewValue in pairs(descendantPropertyMap[descendant]) do 557 | pcall(function() 558 | descendant[Property] = NewValue 559 | end) 560 | end 561 | end 562 | end 563 | 564 | -- Reparent children 565 | for _, child in pairs(object:GetChildren()) do 566 | child.Parent = newObject 567 | end 568 | 569 | object:remove() 570 | newObject.Parent = oldParent 571 | 572 | return newObject 573 | end 574 | 575 | --- Map oldParent to NewParent to handle welds in children 576 | function Converter:GetDescendantPropertyMap(childrenList, object, newObject) 577 | assert(newObject) 578 | assert(object) 579 | 580 | local classes = self:GetClassesMap() 581 | if not classes then 582 | warn("[Converter][GetDescendantPropertyMap] - No API loaded") 583 | return nil 584 | end 585 | 586 | local propertyMap = {} -- [Child] = { [Property] = NewValue } 587 | for _, child in pairs(childrenList) do 588 | local class = classes[child.ClassName] 589 | for propertyName, data in pairs(class:GetAllProperties()) do 590 | --print(propertyName, data.OriginalData.ValueType, "--") 591 | 592 | if propertyName ~= "Parent" and data.OriginalData.ValueType == "Object" then 593 | local propertyValue 594 | 595 | -- Reading certain properties can error 596 | local ok = pcall(function() 597 | propertyValue = child[propertyName] 598 | end) 599 | 600 | --print("propertyValue", propertyValue, propertyValue == object, object) 601 | 602 | if ok and propertyValue == object then 603 | propertyMap[child] = propertyMap[child] or {} 604 | propertyMap[child][propertyName] = newObject 605 | end 606 | end 607 | end 608 | end 609 | return propertyMap 610 | end 611 | 612 | return Converter -------------------------------------------------------------------------------- /src/IconHandler.lua: -------------------------------------------------------------------------------- 1 | local ICON_SHEET = 'rbxassetid://129293660' 2 | 3 | -- Credit: Stravant 4 | -- luacheck: ignore 5 | 6 | ---- IconMap ---- 7 | -- Image size: 256px x 256px 8 | -- Icon size: 16px x 16px 9 | -- Padding between each icon: 2px 10 | -- Padding around image edge: 1px 11 | -- Total icons: 14 x 14 (196) 12 | local ICON_INDEX = { 13 | ['Accoutrement'] = 32; ['Animation'] = 60; ['AnimationTrack'] = 60; ['ArcHandles'] = 56; ['Backpack'] = 20; ['BillboardGui'] = 64; ['BindableEvent'] = 67; 14 | ['BindableFunction'] = 66; ['BlockMesh'] = 8; ['BodyAngularVelocity'] = 14; ['BodyForce'] = 14; ['BodyGyro'] = 14; ['BodyPosition'] = 14; ['BodyThrust'] = 14; 15 | ['BodyVelocity'] = 14; ['BoolValue'] = 4; ['BrickColorValue'] = 4; ['Camera'] = 5; ['CFrameValue'] = 4; ['CharacterMesh'] = 60; ['ClickDetector'] = 41; ['Color3Value'] = 4; 16 | ['Configuration'] = 58; ['CoreGui'] = 46; ['CornerWedgePart'] = 1; ['CustomEvent'] = 4; ['CustomEventReceiver'] = 4; ['CylinderMesh'] = 8; ['Debris'] = 30; ['Decal'] = 7; 17 | ['Dialog'] = 62; ['DialogChoice'] = 63; ['DoubleConstrainedValue'] = 4; ['Explosion'] = 36; ['Fire'] = 61; ['Flag'] = 38; ['FlagStand'] = 39; ['FloorWire'] = 4; 18 | ['ForceField'] = 37; ['Frame'] = 48; ['GuiButton'] = 52; ['GuiMain'] = 47; ['Handles'] = 53; ['Hat'] = 45; ['Hint'] = 33; ['HopperBin'] = 22; ['Humanoid'] = 9; 19 | ['ImageButton'] = 52; ['ImageLabel'] = 49; ['IntConstrainedValue'] = 4; ['IntValue'] = 4; ['JointInstance'] = 34; ['Keyframe'] = 60; ['Lighting'] = 13; ['LocalScript'] = 18; 20 | ['MarketplaceService'] = 46; ['Message'] = 33; ['Model'] = 2; ['NetworkClient'] = 16; ['NetworkReplicator'] = 29; ['NetworkServer'] = 15; ['NumberValue'] = 4; 21 | ['ObjectValue'] = 4; ['Pants'] = 44; ['ParallelRampPart'] = 1; ['Part'] = 1; ['PartPairLasso'] = 57; ['Platform'] = 35; ['Player'] = 12; ['PlayerGui'] = 46; ['Players'] = 21; 22 | ['PointLight'] = 13; ['Pose'] = 60; ['PrismPart'] = 1; ['PyramidPart'] = 1; ['RayValue'] = 4; ['ReplicatedStorage'] = 0; ['RightAngleRampPart'] = 1; ['RocketPropulsion'] = 14; 23 | ['ScreenGui'] = 47; ['Script'] = 6; ['Seat'] = 35; ['SelectionBox'] = 54; ['SelectionPartLasso'] = 57; ['SelectionPointLasso'] = 57; ['ServerScriptService'] = 0; 24 | ['ServerStorage'] = 0; ['Shirt'] = 43; ['ShirtGraphic'] = 40; ['SkateboardPlatform'] = 35; ['Sky'] = 28; ['Smoke'] = 59; ['Sound'] = 11; ['SoundService'] = 31; ['Sparkles'] = 42; 25 | ['SpawnLocation'] = 25; ['SpecialMesh'] = 8; ['SpotLight'] = 13; ['StarterGear'] = 20; ['StarterGui'] = 46; ['StarterPack'] = 20; ['Status'] = 2; ['StringValue'] = 4; 26 | ['SurfaceSelection'] = 55; ['Team'] = 24; ['Teams'] = 23; ['Terrain'] = 65; ['TestService'] = 68; ['TextBox'] = 51; ['TextButton'] = 51; ['TextLabel'] = 50; ['Texture'] = 10; 27 | ['TextureTrail'] = 4; ['Tool'] = 17; ['TouchTransmitter'] = 37; ['TrussPart'] = 1; ['Vector3Value'] = 4; ['VehicleSeat'] = 35; ['WedgePart'] = 1; ['Weld'] = 34; ['Workspace'] = 19; 28 | } 29 | local Icon do 30 | game:GetService('ContentProvider'):Preload(ICON_SHEET) 31 | local iconDehash do 32 | -- 14 x 14, 0-based input, 0-based output 33 | local f = math.floor 34 | function iconDehash(h) 35 | return f(h/14 % 14), f(h % 14) 36 | end 37 | end 38 | function Icon(_, className) 39 | local Index = ICON_INDEX[className] or 0 40 | local row, col = iconDehash(Index) 41 | local pad, border = 2, 1 42 | local iconSize = 16 43 | -- 44 | --local imageLabel = Instance.new('ImageLabel') 45 | --imageLabel.Size = UDim2.new(0, iconSize, 0, iconSize) 46 | --imageLabel.BorderSizePixel = 0 47 | --imageLabel.BackgroundTransparency = 1 48 | --imageLabel.Image = ICON_SHEET 49 | --imageLabel.ImageRectSize = Vector2.new(iconSize, iconSize) 50 | --imageLabel.ImageRectOffset = Vector2.new(border + col*iconSize + pad*(col+1), border + row*iconSize + pad*(row+1)) 51 | -- 52 | --return imageLabel 53 | return 54 | { 55 | Image = ICON_SHEET; 56 | ImageRectSize = Vector2.new(iconSize, iconSize); 57 | ImageRectOffset = Vector2.new(border + col*iconSize + pad*(col+1), border + row*iconSize + pad*(row+1)); 58 | } 59 | end 60 | end 61 | 62 | return { 63 | GetIcon = Icon; 64 | HasIcon = function(self, ClassName) 65 | return ICON_INDEX[ClassName] 66 | end; 67 | } 68 | -------------------------------------------------------------------------------- /src/Maid.lua: -------------------------------------------------------------------------------- 1 | --- Manages the cleaning of events and other things. 2 | -- Useful for encapsulating state and make deconstructors easy 3 | -- @classmod Maid 4 | -- @see Signal 5 | 6 | local Maid = {} 7 | Maid.ClassName = "Maid" 8 | 9 | --- Returns a new Maid object 10 | -- @constructor Maid.new() 11 | -- @treturn Maid 12 | function Maid.new() 13 | local self = {} 14 | 15 | self._tasks = {} 16 | 17 | return setmetatable(self, Maid) 18 | end 19 | 20 | --- Returns Maid[key] if not part of Maid metatable 21 | -- @return Maid[key] value 22 | function Maid:__index(index) 23 | if Maid[index] then 24 | return Maid[index] 25 | else 26 | return self._tasks[index] 27 | end 28 | end 29 | 30 | --- Add a task to clean up 31 | -- @usage 32 | -- Maid[key] = (function) Adds a task to perform 33 | -- Maid[key] = (event connection) Manages an event connection 34 | -- Maid[key] = (Maid) Maids can act as an event connection, allowing a Maid to have other maids to clean up. 35 | -- Maid[key] = (Object) Maids can cleanup objects with a `Destroy` method 36 | -- Maid[key] = nil Removes a named task. If the task is an event, it is disconnected. If it is an object, 37 | -- it is destroyed. 38 | function Maid:__newindex(index, newTask) 39 | if Maid[index] ~= nil then 40 | error(("'%s' is reserved"):format(tostring(index)), 2) 41 | end 42 | 43 | local tasks = self._tasks 44 | local oldTask = tasks[index] 45 | tasks[index] = newTask 46 | 47 | if oldTask then 48 | if type(oldTask) == "function" then 49 | oldTask() 50 | elseif typeof(oldTask) == "RBXScriptConnection" then 51 | oldTask:Disconnect() 52 | elseif oldTask.Destroy then 53 | oldTask:Destroy() 54 | end 55 | end 56 | end 57 | 58 | --- Same as indexing, but uses an incremented number as a key. 59 | -- @param task An item to clean 60 | -- @treturn number taskId 61 | function Maid:GiveTask(task) 62 | assert(task, "Task cannot be false or nil") 63 | 64 | local taskId = #self._tasks+1 65 | self[taskId] = task 66 | 67 | if type(task) == "table" and (not task.Destroy) then 68 | warn("[Maid.GiveTask] - Gave table task without .Destroy\n\n" .. debug.traceback()) 69 | end 70 | 71 | return taskId 72 | end 73 | 74 | function Maid:GivePromise(promise) 75 | if not promise:IsPending() then 76 | return promise 77 | end 78 | 79 | local newPromise = promise.resolved(promise) 80 | local id = self:GiveTask(newPromise) 81 | 82 | -- Ensure GC 83 | newPromise:Finally(function() 84 | self[id] = nil 85 | end) 86 | 87 | return newPromise 88 | end 89 | 90 | --- Cleans up all tasks. 91 | -- @alias Destroy 92 | function Maid:DoCleaning() 93 | local tasks = self._tasks 94 | 95 | -- Disconnect all events first as we know this is safe 96 | for index, task in pairs(tasks) do 97 | if typeof(task) == "RBXScriptConnection" then 98 | tasks[index] = nil 99 | task:Disconnect() 100 | end 101 | end 102 | 103 | -- Clear out tasks table completely, even if clean up tasks add more tasks to the maid 104 | local index, task = next(tasks) 105 | while task ~= nil do 106 | tasks[index] = nil 107 | if type(task) == "function" then 108 | task() 109 | elseif typeof(task) == "RBXScriptConnection" then 110 | task:Disconnect() 111 | elseif task.Destroy then 112 | task:Destroy() 113 | end 114 | index, task = next(tasks) 115 | end 116 | end 117 | 118 | --- Alias for DoCleaning() 119 | -- @function Destroy 120 | Maid.Destroy = Maid.DoCleaning 121 | 122 | return Maid 123 | -------------------------------------------------------------------------------- /src/ScrollingFrame.lua: -------------------------------------------------------------------------------- 1 | local UserInputService = game:GetService("UserInputService") 2 | local RunService = game:GetService("RunService") 3 | 4 | local MakeMaid = require(script.Parent:WaitForChild("Maid")).new 5 | local Signal = require(script.Parent:WaitForChild("Signal")) 6 | local Spring = require(script.Parent:WaitForChild("Spring")) 7 | 8 | -- luacheck: ignore 9 | 10 | local Scroller = {} 11 | Scroller.ClassName = "Scroller" 12 | Scroller.__index = Scroller 13 | Scroller._Position = 0 14 | Scroller._Min = 0 15 | Scroller._Max = 100 16 | Scroller._ViewSize = 50 17 | 18 | function Scroller.new() 19 | local self = setmetatable({}, Scroller) 20 | 21 | self.Spring = Spring.new(0) 22 | self.Spring.Speed = 20 23 | 24 | return self 25 | end 26 | 27 | function Scroller:GetTimesOverBounds(Position) 28 | return self:GetDisplacementPastBounds(Position) / self.BackBounceInputRange 29 | end 30 | 31 | function Scroller:GetDisplacementPastBounds(Position) 32 | if Position > self.ContentMax then 33 | return Position - self.ContentMax 34 | elseif Position < self.ContentMin then 35 | return Position 36 | else 37 | return 0 38 | end 39 | end 40 | 41 | function Scroller:GetScale(TimesOverBounds) 42 | return (1 - 0.5 ^ math.abs(TimesOverBounds)) 43 | end 44 | 45 | function Scroller:__index(Index) 46 | if Index == "TotalContentLength" then 47 | return self._Max - self._Min 48 | elseif Index == "ViewSize" then 49 | return self._ViewSize 50 | elseif Index == "Max" then 51 | return self._Max 52 | elseif Index == "ContentMax" then 53 | if self._Max <= self.ContentMin + self._ViewSize then 54 | return self.ContentMin 55 | else 56 | return self._Max - self._ViewSize -- Compensate for AnchorPoint = 0 57 | end 58 | elseif Index == "Min" or Index == "ContentMin" then 59 | return self._Min 60 | elseif Index == "Position" then 61 | return self.Spring.Position 62 | elseif Index == "BackBounceInputRange" then 63 | return self._ViewSize -- Maximum distance we can drag past the end 64 | elseif Index == "BackBounceRenderRange" then 65 | return self._ViewSize 66 | elseif Index == "ContentScrollPercentSize" then 67 | if self.TotalContentLength == 0 then 68 | return 0 69 | end 70 | 71 | return (self._ViewSize / self.TotalContentLength) 72 | elseif Index == "RenderedContentScrollPercentSize" then 73 | local Position = self.Position 74 | return self.ContentScrollPercentSize * (1-self:GetScale(self:GetTimesOverBounds(Position))) 75 | elseif Index == "ContentScrollPercent" then 76 | return (self.Position - self._Min) / (self.TotalContentLength - self._ViewSize) 77 | elseif Index == "RenderedContentScrollPercent" then 78 | local Percent = self.ContentScrollPercent 79 | if Percent < 0 then 80 | return 0 81 | elseif Percent > 1 then 82 | return 1 83 | else 84 | return Percent 85 | end 86 | elseif Index == "BoundedRenderPosition" then 87 | local Position = self.Position 88 | local TimesOverBounds = self:GetTimesOverBounds(Position) 89 | local Scale = self:GetScale(TimesOverBounds) 90 | if TimesOverBounds > 0 then 91 | return -self.ContentMax - Scale*self.BackBounceRenderRange 92 | elseif TimesOverBounds < 0 then 93 | return self.ContentMin + Scale*self.BackBounceRenderRange 94 | else 95 | return -Position 96 | end 97 | elseif Index == "Velocity" then 98 | return self.Spring.Velocity 99 | elseif Index == "Target" then 100 | return self.Spring.Target 101 | elseif Index == "AtRest" then 102 | return math.abs(self.Spring.Target - self.Spring.Position) < 1e-5 and math.abs(self.Spring.Velocity) < 1e-5 103 | elseif Scroller[Index] then 104 | return Scroller[Index] 105 | else 106 | error(("[Scroller] - '%s' is not a valid member"):format(tostring(Index))) 107 | end 108 | end 109 | 110 | function Scroller:__newindex(Index, Value) 111 | if Scroller[Index] or Index == "Spring" then 112 | rawset(self, Index, Value) 113 | elseif Index == "Min" or Index == "ContentMin" then 114 | self._Min = Value 115 | elseif Index == "Max" then 116 | self._Max = Value 117 | self.Target = self.Target -- Force update! 118 | elseif Index == "TotalContentLength" then 119 | self.Max = self._Min + Value 120 | elseif Index == "ViewSize" then 121 | self._ViewSize = Value 122 | elseif Index == "Position" then 123 | self.Spring.Position = Value 124 | elseif Index == "TargetContentScrollPercent" then 125 | self.Target = self._Min + Value * (self.TotalContentLength - self._ViewSize) 126 | elseif Index == "ContentScrollPercent" then 127 | self.Position = self._Min + Value * (self.TotalContentLength - self._ViewSize) 128 | elseif Index == "Target" then 129 | if Value > self.ContentMax then 130 | Value = self.ContentMax 131 | elseif Value < self.ContentMin then 132 | Value = self.ContentMin 133 | end 134 | self.Spring.Target = Value 135 | elseif Index == "Velocity" then 136 | self.Spring.Velocity = Value 137 | else 138 | error(("[Scroller] - '%s' is not a valid member"):format(tostring(Index))) 139 | end 140 | end 141 | 142 | 143 | local BaseScroller = {} 144 | BaseScroller.ClassName = "Base" 145 | BaseScroller.__index = BaseScroller 146 | 147 | function BaseScroller.new(Gui) 148 | local self = setmetatable({}, BaseScroller) 149 | 150 | self.Maid = MakeMaid() 151 | self.Gui = Gui or error("No Gui") 152 | self.Container = self.Gui.Parent or error("No container") 153 | 154 | return self 155 | end 156 | 157 | function BaseScroller:Destroy() 158 | self.Maid:DoCleaning() 159 | self.Maid = nil 160 | 161 | setmetatable(self, nil) 162 | end 163 | 164 | local Scrollbar = setmetatable({}, BaseScroller) 165 | Scrollbar.ClassName = "Scrollbar" 166 | Scrollbar.__index = Scrollbar 167 | 168 | function Scrollbar.new(Gui, ScrollingFrame) 169 | local self = setmetatable(BaseScroller.new(Gui), Scrollbar) 170 | 171 | self.ScrollingFrame = ScrollingFrame or error("No ScrollingFrame") 172 | self.ParentScroller = self.ScrollingFrame:GetScroller() 173 | self:UpdateRender() 174 | 175 | self.DraggingBegan = Signal.new() 176 | 177 | self.Maid.InputBeganGui = self.Gui.InputBegan:connect(function(InputObject) 178 | if InputObject.UserInputType == Enum.UserInputType.MouseButton1 then 179 | self:InputBegan(InputObject) 180 | end 181 | end) 182 | 183 | self.Maid.InputBeganContainer = self.Container.InputBegan:connect(function(InputObject) 184 | if InputObject.UserInputType == Enum.UserInputType.MouseButton1 then 185 | self.LastContainerInputObject = InputObject 186 | end 187 | end) 188 | 189 | self.Maid.InputEndedContainer = self.Container.InputEnded:connect(function(InputObject) 190 | if InputObject == self.LastContainerInputObject then 191 | local ScrollbarSize = self.Container.AbsoluteSize.Y * self.ParentScroller.ContentScrollPercentSize 192 | local Offset = InputObject.Position.Y - self.Container.AbsolutePosition.Y - ScrollbarSize/2 -- In the middle of the bar 193 | local Percent = Offset / (self.Container.AbsoluteSize.Y * (1 - self.ParentScroller.ContentScrollPercentSize)) 194 | 195 | self.ParentScroller.TargetContentScrollPercent = Percent 196 | self.ParentScroller.Velocity = 0 197 | self.ScrollingFrame:FreeScroll() 198 | end 199 | end) 200 | 201 | return self 202 | end 203 | 204 | function Scrollbar:StopDrag() 205 | self.ScrollingFrame:StopDrag() 206 | end 207 | 208 | function Scrollbar:InputBegan(InputBeganObject) 209 | local Maid = MakeMaid() 210 | 211 | local StartPosition = InputBeganObject.Position 212 | local StartPercent = self.ParentScroller.ContentScrollPercent 213 | local UpdateVelocity = self.ScrollingFrame:GetVelocityTracker(0.25) 214 | 215 | Maid.InputChanged = UserInputService.InputChanged:connect(function(InputObject) 216 | if InputObject.UserInputType == Enum.UserInputType.MouseMovement then 217 | local Offset = (InputObject.Position - StartPosition).y 218 | local Percent = Offset / (self.Container.AbsoluteSize.Y * (1 - self.ParentScroller.ContentScrollPercentSize)) 219 | self.ParentScroller.ContentScrollPercent = StartPercent + Percent 220 | self.ParentScroller.TargetContentScrollPercent = self.ParentScroller.ContentScrollPercent 221 | 222 | self.ScrollingFrame:UpdateRender() 223 | UpdateVelocity() 224 | end 225 | end) 226 | 227 | Maid.InputEnded = UserInputService.InputEnded:connect(function(InputObject) 228 | if InputObject == InputBeganObject then 229 | self:StopDrag() 230 | end 231 | end) 232 | 233 | self.Maid.UpdateMaid = Maid 234 | self.ScrollingFrame.Maid.UpdateMaid = Maid 235 | end 236 | 237 | function Scrollbar:UpdateRender() 238 | if self.ParentScroller.TotalContentLength > self.ParentScroller.ViewSize then 239 | local RenderedContentScrollPercentSize = self.ParentScroller.RenderedContentScrollPercentSize 240 | self.Gui.Size = UDim2.new(self.Gui.Size.X, UDim.new(RenderedContentScrollPercentSize, 0)) 241 | self.Gui.Position = UDim2.new(self.Gui.Position.X, UDim.new((1-RenderedContentScrollPercentSize) * self.ParentScroller.RenderedContentScrollPercent, 0)) 242 | self.Gui.Visible = true 243 | else 244 | self.Gui.Visible = false 245 | end 246 | end 247 | 248 | local ScrollingFrame = setmetatable({}, BaseScroller) 249 | ScrollingFrame.ClassName = "ScrollingFrame" 250 | ScrollingFrame.__index = ScrollingFrame 251 | 252 | function ScrollingFrame.new(Gui) 253 | local self = setmetatable(BaseScroller.new(Gui), ScrollingFrame) 254 | 255 | self.Scrollbars = {} 256 | self.Scroller = Scroller.new() 257 | 258 | self:BindInput(Gui) 259 | self:BindInput(self.Container) 260 | 261 | self.Maid.ContainerChanged = self.Container.Changed:connect(function(Property) 262 | if Property == "AbsoluteSize" then 263 | self:UpdateScroller() 264 | self:FreeScroll(true) 265 | end 266 | end) 267 | 268 | self.Maid.GuiChanged = self.Gui.Changed:connect(function(Property) 269 | if Property == "AbsoluteSize" then 270 | self:UpdateScroller() 271 | self:FreeScroll(true) 272 | end 273 | end) 274 | 275 | self:UpdateScroller() 276 | self:UpdateRender() 277 | 278 | return self 279 | end 280 | 281 | function ScrollingFrame:GetScroller() 282 | return self.Scroller 283 | end 284 | 285 | function ScrollingFrame:AddScrollbar(Gui) 286 | local Bar = Scrollbar.new(Gui, self) 287 | table.insert(self.Scrollbars, Bar) 288 | 289 | self.Maid[Gui] = Bar 290 | end 291 | 292 | function ScrollingFrame:AddScrollbarFromContainer(Container) 293 | local ScrollBar = Instance.new("ImageButton") 294 | ScrollBar.Size = UDim2.new(1, 0, 0, 100) 295 | ScrollBar.Name = "ScrollBar" 296 | ScrollBar.BorderSizePixel = 0 297 | ScrollBar.Image = "" 298 | ScrollBar.Parent = Container 299 | ScrollBar.AutoButtonColor = false 300 | ScrollBar.ZIndex = Container.ZIndex 301 | ScrollBar.Parent = Container 302 | 303 | return self:AddScrollbar(ScrollBar) 304 | end 305 | 306 | function ScrollingFrame:UpdateScroller() 307 | self.Scroller.TotalContentLength = self.Gui.AbsoluteSize.y 308 | self.Scroller.ViewSize = self.Container.AbsoluteSize.y 309 | end 310 | 311 | function ScrollingFrame:UpdateRender() 312 | self.Gui.Position = UDim2.new(self.Gui.Position.X, UDim.new(0, self.Scroller.BoundedRenderPosition)) 313 | for _, Scrollbar in pairs(self.Scrollbars) do 314 | Scrollbar:UpdateRender() 315 | end 316 | end 317 | 318 | function ScrollingFrame:StopUpdate() 319 | self.Maid.UpdateMaid = nil 320 | end 321 | 322 | function ScrollingFrame:StopDrag() 323 | local Position = self.Scroller.Position 324 | 325 | if self.Scroller:GetDisplacementPastBounds(self.Scroller.Position) == 0 then 326 | if self.Scroller.Velocity > 0 then 327 | self.Scroller.Target = math.max(self.Scroller.Target, self.Scroller.Position + self.Scroller.Velocity * 0.5) 328 | else 329 | self.Scroller.Target = math.min(self.Scroller.Target, self.Scroller.Position + self.Scroller.Velocity * 0.5) 330 | end 331 | end 332 | 333 | self:FreeScroll() 334 | end 335 | 336 | function ScrollingFrame:FreeScroll(LowPriority) 337 | if LowPriority and self.Maid.UpdateMaid then 338 | return 339 | end 340 | 341 | local Maid = MakeMaid() 342 | 343 | self:UpdateRender() 344 | Maid.RenderStepped = RunService.RenderStepped:connect(function() 345 | self:UpdateRender() 346 | if self.Scroller.AtRest then 347 | self:StopUpdate() 348 | end 349 | end) 350 | 351 | self.Maid.UpdateMaid = Maid 352 | end 353 | 354 | function ScrollingFrame:GetVelocityTracker(Strength) 355 | Strength = Strength or 1 356 | self.Scroller.Velocity = 0 357 | 358 | local LastUpdate = tick() 359 | local LastPosition = self.Scroller.Position 360 | 361 | return function() 362 | local Position = self.Scroller.Position 363 | 364 | local Elapsed = tick() - LastUpdate 365 | LastUpdate = tick() 366 | local Delta = LastPosition - Position 367 | LastPosition = Position 368 | self.Scroller.Velocity = self.Scroller.Velocity - (Delta / (0.0001 + Elapsed)) * Strength 369 | end 370 | end 371 | 372 | function ScrollingFrame:GetProcessInput(InputBeganObject) 373 | local Start = self.Scroller.Position 374 | local UpdateVelocity = self:GetVelocityTracker() 375 | local OriginalPosition = InputBeganObject.Position 376 | 377 | return function(InputObject) 378 | local Distance = (InputObject.Position - OriginalPosition).y 379 | local Position = Start - Distance 380 | self.Scroller.Position = Position 381 | self.Scroller.Target = Position 382 | 383 | self:UpdateRender() 384 | UpdateVelocity() 385 | 386 | return Distance 387 | end 388 | end 389 | 390 | function ScrollingFrame:ScrollTo(Position, DoNotAnimate) 391 | self.Scroller.Target = Position 392 | if DoNotAnimate then 393 | self.Scroller.Position = self.Scroller.Target 394 | self.Scroller.Velocity = 0 395 | end 396 | end 397 | 398 | function ScrollingFrame:ScrollToTop(DoNotAnimate) 399 | self:ScrollTo(self.Scroller.Min, DoNotAnimate) 400 | end 401 | 402 | function ScrollingFrame:ScrollToBottom(DoNotAnimate) 403 | self:ScrollTo(self.Scroller.Max, DoNotAnimate) 404 | end 405 | 406 | function ScrollingFrame:BindInput(Gui, Options) 407 | local Maid = MakeMaid() 408 | 409 | Maid.GuiInputBegan = Gui.InputBegan:connect(function(InputObject) 410 | self:InputBegan(InputObject, Options) 411 | end) 412 | 413 | Maid.GuiInputChanged = Gui.InputChanged:connect(function(InputObject) 414 | if InputObject.UserInputType == Enum.UserInputType.MouseWheel and Gui.Active then 415 | self.Scroller.Target = self.Scroller.Target + -InputObject.Position.z * 80 -- We have to be active to avoid scrolling 416 | self:FreeScroll() 417 | end 418 | end) 419 | 420 | return Maid 421 | end 422 | 423 | function ScrollingFrame:InputBegan(InputBeganObject, Options) 424 | if InputBeganObject.UserInputType == Enum.UserInputType.MouseButton1 or InputBeganObject.UserInputType == Enum.UserInputType.Touch then 425 | local Maid = MakeMaid() 426 | 427 | local StartTime = tick() 428 | local TotalScrollDistance = 0 429 | local ProcessInput = self:GetProcessInput(InputBeganObject) 430 | 431 | if InputBeganObject.UserInputType == Enum.UserInputType.MouseButton1 then 432 | Maid.InputChanged = UserInputService.InputChanged:connect(function(InputObject, GameProcessed) 433 | if InputObject.UserInputType == Enum.UserInputType.MouseMovement then 434 | TotalScrollDistance = TotalScrollDistance + math.abs(ProcessInput(InputObject)) 435 | end 436 | end) 437 | elseif InputBeganObject.UserInputType == Enum.UserInputType.Touch then 438 | local function Update(InputObject, GameProcessed) 439 | if InputObject.UserInputType == Enum.UserInputType.Touch then 440 | TotalScrollDistance = TotalScrollDistance + math.abs(ProcessInput(InputObject)) 441 | end 442 | end 443 | 444 | Maid.InputChanged = UserInputService.InputChanged:connect(Update) 445 | end 446 | 447 | Maid.Cleanup = function() 448 | self:UpdateRender() 449 | if Options and Options.OnClick then 450 | local ElapsedTime = tick() - StartTime 451 | local ConsideredClick = (ElapsedTime <= 0.05) or (TotalScrollDistance < 1) 452 | if ConsideredClick then 453 | Options.OnClick(InputBeganObject) 454 | end 455 | end 456 | end 457 | 458 | Maid.InputEnded = UserInputService.InputEnded:connect(function(InputObject, GameProcessed) 459 | if InputObject == InputBeganObject then 460 | self:StopDrag() 461 | end 462 | end) 463 | 464 | Maid.WindowFocusReleased = UserInputService.WindowFocusReleased:connect(function() 465 | self:StopDrag() 466 | end) 467 | 468 | self.Maid.UpdateMaid = Maid 469 | end 470 | end 471 | 472 | return ScrollingFrame -------------------------------------------------------------------------------- /src/Signal.lua: -------------------------------------------------------------------------------- 1 | --- Lua-side duplication of the API of events on Roblox objects. 2 | -- Signals are needed for to ensure that for local events objects are passed by 3 | -- reference rather than by value where possible, as the BindableEvent objects 4 | -- always pass signal arguments by value, meaning tables will be deep copied. 5 | -- Roblox's deep copy method parses to a non-lua table compatable format. 6 | -- @classmod Signal 7 | 8 | local Signal = {} 9 | Signal.__index = Signal 10 | Signal.ClassName = "Signal" 11 | 12 | --- Constructs a new signal. 13 | -- @constructor Signal.new() 14 | -- @treturn Signal 15 | function Signal.new() 16 | local self = setmetatable({}, Signal) 17 | 18 | self._bindableEvent = Instance.new("BindableEvent") 19 | self._argData = nil 20 | self._argCount = nil 21 | 22 | return self 23 | end 24 | 25 | --- Fire the event with the given arguments. All handlers will be invoked. Handlers follow 26 | -- Roblox signal conventions. 27 | -- @param ... Variable arguments to pass to handler 28 | -- @treturn nil 29 | function Signal:Fire(...) 30 | self._argData = {...} 31 | self._argCount = select("#", ...) 32 | self._bindableEvent:Fire() 33 | end 34 | Signal.fire = Signal.Fire 35 | 36 | --- Connect a new handler to the event. Returns a connection object that can be disconnected. 37 | -- @tparam function handler Function handler called with arguments passed when `:Fire(...)` is called 38 | -- @treturn Connection Connection object that can be disconnected 39 | function Signal:Connect(handler) 40 | if not (typeof(handler) == "function") then 41 | error(("connect(%s)"):format(typeof(handler)), 2) 42 | end 43 | 44 | return self._bindableEvent.Event:Connect(function() 45 | handler(unpack(self._argData, 1, self._argCount)) 46 | end) 47 | end 48 | Signal.connect = Signal.Connect 49 | 50 | --- Wait for fire to be called, and return the arguments it was given. 51 | -- @treturn ... Variable arguments from connection 52 | function Signal:Wait() 53 | self._bindableEvent.Event:wait() 54 | assert(self._argData, "Missing arg data, likely due to :TweenSize/Position corrupting threadrefs.") 55 | return unpack(self._argData, 1, self._argCount) 56 | end 57 | Signal.wait = Signal.Wait 58 | 59 | --- Disconnects all connected events to the signal. Voids the signal as unusable. 60 | -- @treturn nil 61 | function Signal:Destroy() 62 | if self._bindableEvent then 63 | self._bindableEvent:Destroy() 64 | self._bindableEvent = nil 65 | end 66 | 67 | self._argData = nil 68 | self._argCount = nil 69 | end 70 | 71 | return Signal -------------------------------------------------------------------------------- /src/Spring.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | class Spring 3 | 4 | Description: 5 | A physical model of a spring, useful in many applications. Properties only evaluate 6 | upon index making this model good for lazy applications 7 | 8 | API: 9 | Spring = Spring.new(number position) 10 | Creates a new spring in 1D 11 | Spring = Spring.new(Vector3 position) 12 | Creates a new spring in 3D 13 | 14 | Spring.Position 15 | Returns the current position 16 | Spring.Velocity 17 | Returns the current velocity 18 | Spring.Target 19 | Returns the target 20 | Spring.Damper 21 | Returns the damper 22 | Spring.Speed 23 | Returns the speed 24 | 25 | Spring.Target = number/Vector3 26 | Sets the target 27 | Spring.Position = number/Vector3 28 | Sets the position 29 | Spring.Velocity = number/Vector3 30 | Sets the velocity 31 | Spring.Damper = number [0, 1] 32 | Sets the spring damper, defaults to 1 33 | Spring.Speed = number [0, infinity) 34 | Sets the spring speed, defaults to 1 35 | 36 | Spring:TimeSkip(number DeltaTime) 37 | Instantly skips the spring forwards by that amount of time 38 | Spring:Impulse(number/Vector3 velocity) 39 | Impulses the spring, increasing velocity by the amount given 40 | ]] 41 | 42 | 43 | local Spring = {} 44 | 45 | --- Creates a new spring 46 | -- @param initial A number or Vector3 (anything with * number and addition/subtraction defined) 47 | function Spring.new(initial) 48 | local self = setmetatable({}, Spring) 49 | 50 | local target = initial or 0 51 | rawset(self, "_time0", tick()) 52 | rawset(self, "_position0", target) 53 | rawset(self, "_velocity0", 0*target) 54 | rawset(self, "_target", target) 55 | rawset(self, "_damper", 1) 56 | rawset(self, "_speed", 1) 57 | 58 | return self 59 | end 60 | 61 | --- Impulse the spring with a change in velocity 62 | -- @param velocity The velocity to impulse with 63 | function Spring:Impulse(velocity) 64 | self.Velocity = self.Velocity + velocity 65 | end 66 | 67 | --- Skip forwards in time 68 | -- @param delta Time to skip forwards 69 | function Spring:TimeSkip(delta) 70 | local time = tick() 71 | local position, velocity = self:_positionVelocity(time+delta) 72 | rawset(self, "_position0", position) 73 | rawset(self, "_velocity0", velocity) 74 | rawset(self, "_time0", time) 75 | end 76 | 77 | function Spring:__index(index) 78 | if Spring[index] then 79 | return Spring[index] 80 | elseif index == "Value" or index == "Position" or index == "p" then 81 | local position, _ = self:_positionVelocity(tick()) 82 | return position 83 | elseif index == "Velocity" or index == "v" then 84 | local _, velocity = self:_positionVelocity(tick()) 85 | return velocity 86 | elseif index == "Target" or index == "t" then 87 | return rawget(self, "_target") 88 | elseif index == "Damper" or index == "d" then 89 | return rawget(self, "_damper") 90 | elseif index == "Speed" or index == "s" then 91 | return rawget(self, "_speed") 92 | else 93 | error(("%q is not a valid member of Spring"):format(tostring(index)), 2) 94 | end 95 | end 96 | 97 | function Spring:__newindex(index, value) 98 | local time = tick() 99 | 100 | if index == "Value" or index == "Position" or index == "p" then 101 | local _, velocity = self:_positionVelocity(time) 102 | rawset(self, "_position0", value) 103 | rawset(self, "_velocity0", velocity) 104 | elseif index == "Velocity" or index == "v" then 105 | local position, _ = self:_positionVelocity(time) 106 | rawset(self, "_position0", position) 107 | rawset(self, "_velocity0", value) 108 | elseif index == "Target" or index == "t" then 109 | local position, velocity = self:_positionVelocity(time) 110 | rawset(self, "_position0", position) 111 | rawset(self, "_velocity0", velocity) 112 | rawset(self, "_target", value) 113 | elseif index == "Damper" or index == "d" then 114 | local position, velocity = self:_positionVelocity(time) 115 | rawset(self, "_position0", position) 116 | rawset(self, "_velocity0", velocity) 117 | rawset(self, "_damper", math.clamp(value, 0, 1)) 118 | elseif index == "Speed" or index == "s" then 119 | local position, velocity = self:_positionVelocity(time) 120 | rawset(self, "_position0", position) 121 | rawset(self, "_velocity0", velocity) 122 | rawset(self, "_speed", value < 0 and 0 or value) 123 | else 124 | error(("%q is not a valid member of Spring"):format(tostring(index)), 2) 125 | end 126 | 127 | rawset(self, "_time0", time) 128 | end 129 | 130 | function Spring:_positionVelocity(time) 131 | local dt = time - rawget(self, "_time0") 132 | local p0 = rawget(self, "_position0") 133 | local v0 = rawget(self, "_velocity0") 134 | local t = rawget(self, "_target") 135 | local d = rawget(self, "_damper") 136 | local s = rawget(self, "_speed") 137 | 138 | local c0 = p0-t 139 | if s == 0 then 140 | return p0, 0 141 | elseif d<1 then 142 | local c = (1-d*d)^0.5 143 | local c1 = (v0/s+d*c0)/c 144 | local co = math.cos(c*s*dt) 145 | local si = math.sin(c*s*dt) 146 | local e = 2.718281828459045^(d*s*dt) 147 | return t+(c0*co+c1*si)/e, 148 | s*((c*c1-d*c0)*co-(c*c0+d*c1)*si)/e 149 | else 150 | local c1 = v0/s+c0 151 | local e = 2.718281828459045^(s*dt) 152 | return t+(c0+c1*s*dt)/e, 153 | s*(c1-c0-c1*s*dt)/e 154 | end 155 | end 156 | 157 | return Spring 158 | -------------------------------------------------------------------------------- /src/StringMatcher.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | The matcher module provides easy and advanced matching of strings. 3 | 4 | @author Nils Nordman 5 | @copyright 2011-2012 6 | @license MIT (see LICENSE) 7 | @module _M.textui.util.matcher 8 | ]] 9 | 10 | -- luacheck: push ignore 11 | 12 | local _G, string, table, math = _G, string, table, math 13 | local ipairs, type, setmetatable, tostring, append = 14 | ipairs, type, setmetatable, tostring, table.insert 15 | 16 | local matcher = {} 17 | local _ENV = matcher 18 | if setfenv then setfenv(1, _ENV) end 19 | 20 | --[[ Constructs a new matcher. 21 | @param candidates The candidates to consider for matching. A table of either 22 | string, or tables containing strings. 23 | @param search_case_insensitive Whether searches are case insensitive or not. 24 | Defaults to `true`. 25 | @param search_fuzzy Whether fuzzy searching should be used in addition to 26 | explicit matching. Defaults to `true`. 27 | ]] 28 | function new(candidates, search_case_insensitive, search_fuzzy) 29 | local m = { 30 | search_case_insensitive = search_case_insensitive, 31 | search_fuzzy = search_fuzzy 32 | } 33 | setmetatable(m, { __index = matcher }) 34 | m:_set_candidates(candidates) 35 | return m 36 | end 37 | 38 | -- Applies search matchers on a line. 39 | -- @param line The line to match 40 | -- @param matchers The search matchers to apply 41 | -- @return A numeric score if the line matches or nil otherwise. For scoring, 42 | -- lower is better. 43 | local function match_score(line, matchers) 44 | local score = 0 45 | 46 | for _, matcher in ipairs(matchers) do 47 | local matcher_score = matcher(line) 48 | if not matcher_score then return nil end 49 | score = score + matcher_score 50 | end 51 | return score 52 | end 53 | 54 | --[[ Explains the match for a given search. 55 | @param search The search string to match 56 | @param text The text to match against 57 | @return A list of explanation tables. Each explanation table contains the 58 | following fields: 59 | `score`: The score for the match 60 | `start_pos`: The start position of the best match 61 | `end_pos`: The end position of the best match 62 | `1..n`: Tables of matching positions with the field start_pos and length 63 | ]] 64 | function matcher:explain(search, text) 65 | if not search or #search == 0 then return {} end 66 | if self.search_case_insensitive then 67 | search = search:lower() 68 | text = text:lower() 69 | end 70 | local matchers = self:_matchers_for_search(search) 71 | local explanations = {} 72 | 73 | for _, matcher in ipairs(matchers) do 74 | local score, start_pos, end_pos, search = matcher(text) 75 | if not score then return {} end 76 | local explanation = { score = score, start_pos = start_pos, end_pos = end_pos } 77 | local s_start, s_index = 1, 1 78 | local l_start, l_index = start_pos, start_pos 79 | while s_index <= #search do 80 | repeat 81 | s_index = s_index + 1 82 | l_index = l_index + 1 83 | until search:sub(s_index, s_index) ~= text:sub(l_index, l_index) or s_index > #search 84 | append(explanation, { start_pos = l_start, length = l_index - l_start }) 85 | if s_index > #search then break end 86 | repeat 87 | l_index = l_index + 1 88 | until search:sub(s_index, s_index) == text:sub(l_index, l_index) or l_index > end_pos 89 | l_start = l_index 90 | end 91 | append(explanations, explanation) 92 | end 93 | 94 | return explanations 95 | end 96 | 97 | -- Matches search against the candidates. 98 | -- @param search The search string to match 99 | -- @return A table of matching candidates, ordered by relevance. 100 | function matcher:match(search) 101 | if not search or #search == 0 then return self.candidates end 102 | local cache = self.cache 103 | if self.search_case_insensitive then search = search:lower() end 104 | local matches = cache.matches[search] or {} 105 | if #matches > 0 then return matches end 106 | local lines = cache.lines[string.sub(search, 1, -2)] or self.lines 107 | local matchers = self:_matchers_for_search(search) 108 | 109 | local matching_lines = {} 110 | for i, line in ipairs(lines) do 111 | local score = match_score(line.text, matchers) 112 | if score then 113 | matches[#matches + 1] = { index = line.index, score = score } 114 | matching_lines[#matching_lines + 1] = line 115 | end 116 | end 117 | cache.lines[search] = matching_lines 118 | 119 | table.sort(matches, function(a ,b) return a.score < b.score end) 120 | local matching_candidates = {} 121 | for _, match in ipairs(matches) do 122 | matching_candidates[#matching_candidates + 1] = self.candidates[match.index] 123 | end 124 | self.cache.matches[search] = matching_candidates 125 | return matching_candidates 126 | end 127 | 128 | function matcher:_set_candidates(candidates) 129 | self.candidates = candidates 130 | self.cache = { 131 | lines = {}, 132 | matches = {} 133 | } 134 | local lines = {} 135 | local fuzzy_score_penalty = 0 136 | 137 | for i, candidate in ipairs(candidates) do 138 | if type(candidate) ~= 'table' then candidate = { candidate } end 139 | local text = table.concat(candidate, ' ') 140 | if self.search_case_insensitive then text = text:lower() end 141 | lines[#lines + 1] = { 142 | text = text, 143 | index = i 144 | } 145 | fuzzy_score_penalty = math.max(fuzzy_score_penalty, #text) 146 | end 147 | self.lines = lines 148 | self.fuzzy_score_penalty = fuzzy_score_penalty 149 | end 150 | 151 | local pattern_escapes = {} 152 | for c in string.gmatch('^$()%.[]*+-?', '.') do pattern_escapes[c] = '%' .. c end 153 | 154 | local function fuzzy_search_pattern(search) 155 | local pattern = '' 156 | for i = 1, #search do 157 | local c = search:sub(i, i) 158 | c = pattern_escapes[c] or c 159 | pattern = pattern .. c .. '.-' 160 | end 161 | return pattern 162 | end 163 | 164 | --- Creates matches for the specified search 165 | -- @param search_string The search string 166 | -- @return A table of matcher functions, each taking a line as parameter and 167 | -- returning a score (or nil for no match). 168 | function matcher:_matchers_for_search(search_string) 169 | local fuzzy = self.search_fuzzy 170 | local fuzzy_penalty = self.fuzzy_score_penalty 171 | local groups = {} 172 | for part in search_string:gmatch('%S+') do groups[#groups + 1] = part end 173 | local matchers = {} 174 | 175 | for _, search in ipairs(groups) do 176 | local fuzzy_pattern = fuzzy and fuzzy_search_pattern(search) 177 | matchers[#matchers + 1] = function(line) 178 | local start_pos, end_pos = line:find(search, 1, true) 179 | local score = start_pos 180 | if not start_pos and fuzzy then 181 | start_pos, end_pos = line:find(fuzzy_pattern) 182 | if start_pos then 183 | score = (end_pos - start_pos) + fuzzy_penalty 184 | end 185 | end 186 | if score then 187 | return score + #line, start_pos, end_pos, search 188 | end 189 | end 190 | end 191 | return matchers 192 | end 193 | 194 | return matcher -------------------------------------------------------------------------------- /src/ThemeSwitcher.lua: -------------------------------------------------------------------------------- 1 | -- Theme Switcher for Quenty's Class Converter Plugin 2 | -- provides support for Roblox Studio's Themes 3 | -- defaults to light theme if Roblox Studio ever supports more themes 4 | -- @author presssssure 5 | 6 | local ThemeSwitcher = {} 7 | 8 | local Studio = settings().Studio 9 | 10 | local DockWidget = nil 11 | 12 | local LightColors = { 13 | Background = Color3.fromRGB(255, 255, 255), 14 | BackgroundOnHover = Color3.fromRGB(242, 242, 242), 15 | 16 | Text = Color3.fromRGB(40, 40, 40), 17 | DropDownText = Color3.fromRGB(27, 42, 53), 18 | DropDownMouseOverLerp = Color3.fromRGB(0, 0, 0), 19 | TextBoxText = Color3.fromRGB(49, 49, 49), 20 | 21 | Line = Color3.fromRGB(230, 230, 230), 22 | ScrollBar = Color3.fromRGB(230, 230, 230), 23 | ScrollBarOnHover = Color3.fromRGB(161, 161, 161), 24 | Selected = Color3.fromRGB(90, 142, 243), 25 | 26 | ButtonStyle = Enum.ButtonStyle.RobloxRoundDropdownButton, 27 | } 28 | local DarkColors = { 29 | Background = Color3.fromRGB(46, 46, 46), 30 | BackgroundOnHover = Color3.fromRGB(56, 56, 56), 31 | 32 | Text = Color3.fromRGB(204, 204, 204), 33 | DropDownText = Color3.fromRGB(191, 206, 217), 34 | DropDownMouseOverLerp = Color3.fromRGB(255, 255, 255), 35 | TextBoxText = Color3.fromRGB(213, 213, 213), 36 | 37 | Line = Color3.fromRGB(21, 21, 21), 38 | ScrollBar = Color3.fromRGB(76, 76, 76), 39 | ScrollBarOnHover = Color3.fromRGB(96, 96, 96), 40 | Selected = Color3.fromRGB(90, 142, 243), 41 | 42 | ButtonStyle = Enum.ButtonStyle.RobloxButton, 43 | } 44 | local ConvertButtonTextColor = Color3.fromRGB(255, 255, 255) 45 | 46 | 47 | -- find theme colors (defaults to light if other themes are available besides light and dark) 48 | local function GetColorPalette(theme) 49 | if theme == Enum.UITheme.Dark then 50 | return DarkColors 51 | end 52 | return LightColors 53 | end 54 | 55 | 56 | -- determines if a button is a button in the dropdown menu 57 | local function isInDropDown(obj) 58 | return (obj.Parent.Parent.Parent.Parent and obj.Parent.Parent.Parent.Parent.Name == "DropDown") 59 | end 60 | 61 | 62 | -- determines if something is a "Line" (really skinny Frame that stretches across the page) 63 | local function isLine(obj) 64 | return obj.AbsoluteSize.Y == 1 or obj.AbsoluteSize.Y == 2 65 | end 66 | 67 | 68 | -- changes the theme of a given ui element 69 | function ThemeSwitcher.SwitchObject(obj, theme) 70 | if not obj:IsA("GuiBase") then return end 71 | theme = theme or Studio["UI Theme"] 72 | 73 | local NewPalette = GetColorPalette(theme) 74 | 75 | -- handle special cases first 76 | if isLine(obj) then -- lines in the main view 77 | obj.BackgroundColor3 = NewPalette.Line 78 | return 79 | elseif obj.Name == "Scrollbar" then -- scroll bar in the drop down 80 | obj.BackgroundColor3 = NewPalette.ScrollBar 81 | return 82 | elseif obj.Name == "CheckButton" then -- check boxes in the main view 83 | obj.Style = NewPalette.ButtonStyle 84 | -- no return on purpose, text color still needs to be changed 85 | elseif obj.Name == "ConvertButton" then -- ConvertButton's text color is always the same 86 | obj.TextColor3 = ConvertButtonTextColor 87 | elseif obj.Name == "Checkbox" or obj.Name == "CheckboxTemplate" then 88 | obj.BackgroundColor3 = NewPalette.BackgroundOnHover 89 | return 90 | end 91 | 92 | -- then change the text 93 | if obj.ClassName:find("Text") then 94 | local NewTextColor 95 | 96 | if obj:IsA("TextBox") then 97 | NewTextColor = NewPalette.TextBoxText 98 | elseif isInDropDown(obj) then 99 | NewTextColor = NewPalette.DropDownText 100 | else 101 | NewTextColor = NewPalette.Text 102 | end 103 | 104 | obj.TextColor3 = NewTextColor 105 | end 106 | 107 | -- lastly change the background 108 | obj.BackgroundColor3 = NewPalette.Background 109 | end 110 | 111 | 112 | -- switches every ui element within 113 | local function SwitchAllObjects(NewTheme) 114 | for _, obj in pairs(DockWidget:GetDescendants()) do 115 | ThemeSwitcher.SwitchObject(obj, NewTheme) 116 | end 117 | end 118 | 119 | 120 | -- for when the user changes theme while having the window open 121 | Studio.ThemeChanged:Connect(function() 122 | SwitchAllObjects(Studio["UI Theme"]) 123 | end) 124 | 125 | 126 | -- switches the plugin dock widget and all the objects within it 127 | function ThemeSwitcher.SetDockWidget(NewDockWidget) 128 | DockWidget = NewDockWidget 129 | 130 | SwitchAllObjects(Studio["UI Theme"]) 131 | DockWidget.DescendantAdded:Connect(function(obj) 132 | ThemeSwitcher.SwitchObject(obj, Studio["UI Theme"]) 133 | end) 134 | end 135 | 136 | 137 | -- allow other scripts to access colors 138 | function ThemeSwitcher.GetColorFor(ElementString) 139 | local Palette = GetColorPalette(Studio["UI Theme"]) 140 | return Palette[ElementString] 141 | end 142 | 143 | return ThemeSwitcher -------------------------------------------------------------------------------- /src/UI.lua: -------------------------------------------------------------------------------- 1 | local MakeMaid = require(script.Parent:WaitForChild("Maid")).new 2 | local Signal = require(script.Parent:WaitForChild("Signal")) 3 | local ScrollingFrame = require(script.Parent:WaitForChild("ScrollingFrame")) 4 | local ValueObject = require(script.Parent:WaitForChild("ValueObject")) 5 | local IconHandler = require(script.Parent:WaitForChild("IconHandler")) 6 | local ThemeSwitcher = require(script.Parent:WaitForChild("ThemeSwitcher")) 7 | local HttpService = game:GetService("HttpService") 8 | 9 | local function TrimString(str, pattern) 10 | pattern = pattern or "%s"; 11 | -- %S is whitespaces 12 | -- When we find the first non space character defined by ^%s 13 | -- we yank out anything in between that and the end of the string 14 | -- Everything else is replaced with %1 which is essentially nothing 15 | 16 | -- Credit Sorcus, Modified by Quenty 17 | return (str:gsub("^"..pattern.."*(.-)"..pattern.."*$", "%1")) 18 | end 19 | 20 | local UIBase = {} 21 | UIBase.ClassName = "UIBase" 22 | UIBase.__index = UIBase 23 | 24 | function UIBase.new(Gui) 25 | local self = setmetatable({}, UIBase) 26 | 27 | self.Maid = MakeMaid() 28 | 29 | self.Gui = Gui or error("No GUI") 30 | self.Gui.Visible = true 31 | self.Maid.Gui = Gui 32 | 33 | self.VisibleChanged = Signal.new() 34 | 35 | self.Visible = true 36 | self:Hide(true) 37 | 38 | return self 39 | end 40 | 41 | function UIBase:IsVisible() 42 | return self.Visible 43 | end 44 | 45 | function UIBase:Show(DoNotAnimate) 46 | if not self:IsVisible() then 47 | self.Visible = true 48 | self.VisibleChanged:fire(self:IsVisible(), DoNotAnimate) 49 | end 50 | end 51 | 52 | function UIBase:Hide(DoNotAnimate) 53 | if self:IsVisible() then 54 | self.Visible = false 55 | self.VisibleChanged:fire(self:IsVisible(), DoNotAnimate) 56 | end 57 | end 58 | 59 | function UIBase:Toggle(DoNotAnimate) 60 | self:SetVisible(not self:IsVisible(), DoNotAnimate) 61 | end 62 | 63 | function UIBase:SetVisible(IsVisible, DoNotAnimate) 64 | if IsVisible then 65 | self:Show(DoNotAnimate) 66 | else 67 | self:Hide(DoNotAnimate) 68 | end 69 | end 70 | 71 | function UIBase:Destroy() 72 | self.Maid:DoCleaning() 73 | end 74 | 75 | 76 | 77 | 78 | local Checkbox = setmetatable({}, UIBase) 79 | Checkbox.__index = Checkbox 80 | Checkbox.ClassName = "Checkbox" 81 | 82 | function Checkbox.new(Gui) 83 | local self = setmetatable(UIBase.new(Gui), Checkbox) 84 | 85 | self.Checked = Instance.new("BoolValue") 86 | self.Checked.Value = false 87 | 88 | self.CheckButton = self.Gui.CheckButton 89 | self.TextLabel = self.Gui.TextLabel 90 | self.TextLabel.Text = "???" 91 | 92 | self.Maid.Click = self.Gui.MouseButton1Click:connect(function() 93 | self.Checked.Value = not self.Checked.Value 94 | end) 95 | 96 | self.Maid.ButtonClick = self.CheckButton.MouseButton1Click:connect(function() 97 | self.Checked.Value = not self.Checked.Value 98 | end) 99 | 100 | self.Maid.Changed = self.Checked.Changed:connect(function() 101 | self:UpdateRender() 102 | end) 103 | self:UpdateRender() 104 | 105 | self.Maid:GiveTask(self.Gui.InputBegan:Connect(function(inputObject) 106 | if inputObject.UserInputType == Enum.UserInputType.MouseMovement then 107 | self.Gui.BackgroundTransparency = 0 108 | end 109 | end)) 110 | 111 | self.Maid:GiveTask(self.Gui.InputEnded:Connect(function(inputObject) 112 | if inputObject.UserInputType == Enum.UserInputType.MouseMovement then 113 | self.Gui.BackgroundTransparency = 1 114 | end 115 | end)) 116 | 117 | return self 118 | end 119 | 120 | function Checkbox:GetBoolValue() 121 | return self.Checked 122 | end 123 | 124 | function Checkbox:WithData(Data) 125 | self.Data = Data or error("No data") 126 | 127 | return self 128 | end 129 | 130 | function Checkbox:WithRenderData(RenderData) 131 | self.RenderData = RenderData or error("No RenderData") 132 | self.TextLabel.Text = tostring(self.RenderData.Name) 133 | 134 | return self 135 | end 136 | 137 | function Checkbox:UpdateRender() 138 | if self.Checked.Value then 139 | self.CheckButton.Text = "X" 140 | else 141 | self.CheckButton.Text = "" 142 | end 143 | end 144 | 145 | 146 | 147 | local DropDownButton = setmetatable({}, UIBase) 148 | DropDownButton.__index = DropDownButton 149 | DropDownButton.ClassName = "DropDownButton" 150 | 151 | function DropDownButton.new(Gui) 152 | local self = setmetatable(UIBase.new(Gui), DropDownButton) 153 | 154 | self.TextLabel = self.Gui.TextLabel 155 | self.IconLabel = self.Gui.IconLabel 156 | 157 | self.Selected = Signal.new() 158 | self.IsSelected = Instance.new("BoolValue") 159 | self.IsSelected.Value = false 160 | 161 | self.Maid.IsSelectedChanged = self.IsSelected.Changed:connect(function() 162 | self:UpdateRender() 163 | end) 164 | self.Maid.VisibleChanged = self.VisibleChanged:connect(function(IsVisible, DoNotAnimate) 165 | self.Gui.Visible = IsVisible 166 | if not IsVisible then 167 | self.MouseOver = false 168 | end 169 | self:UpdateRender() 170 | end) 171 | 172 | self.MouseOver = false 173 | self.Maid.InputBeganEnter = self.Gui.InputBegan:connect(function(InputObject) 174 | if InputObject.UserInputType == Enum.UserInputType.MouseMovement then 175 | self.MouseOver = true 176 | self:UpdateRender() 177 | end 178 | end) 179 | 180 | self.Maid.InputEnded = self.Gui.InputEnded:connect(function(InputObject) 181 | if InputObject.UserInputType == Enum.UserInputType.MouseMovement then 182 | self.MouseOver = false 183 | self:UpdateRender() 184 | end 185 | end) 186 | 187 | return self 188 | end 189 | 190 | function DropDownButton:GetData() 191 | return self.Data 192 | end 193 | 194 | function DropDownButton:UpdateRender() 195 | local Desired = ThemeSwitcher.GetColorFor("Background") 196 | if self.IsSelected.Value then 197 | Desired = ThemeSwitcher.GetColorFor("Selected") 198 | end 199 | 200 | if self.MouseOver then 201 | Desired = Desired:lerp(ThemeSwitcher.GetColorFor("DropDownMouseOverLerp"), 0.05) 202 | end 203 | 204 | self.Gui.BackgroundColor3 = Desired 205 | end 206 | 207 | function DropDownButton:WithScroller(Scroller) 208 | self.Scroller = Scroller or error("No scroller") 209 | 210 | self.Maid.InputBeganScroller = self.Scroller:BindInput(self.Gui, { 211 | OnClick = function(InputObject) 212 | self.Selected:fire() 213 | end; 214 | }) 215 | 216 | return self 217 | end 218 | 219 | function DropDownButton:WithData(Data) 220 | self.Data = Data or error("No data") 221 | 222 | return self 223 | end 224 | 225 | function DropDownButton:WithRenderData(RenderData) 226 | self.RenderData = RenderData or error("No RenderData") 227 | 228 | self.TextLabel.Text = tostring(RenderData.Name) 229 | 230 | if RenderData.Image and RenderData.Image.Image then 231 | self.IconLabel.Image = RenderData.Image.Image 232 | else 233 | self.IconLabel.Image = "rbxassetid://133293265" 234 | end 235 | 236 | if RenderData.Image and RenderData.Image.ImageRectSize then 237 | self.IconLabel.ImageRectSize = RenderData.Image.ImageRectSize 238 | else 239 | self.IconLabel.ImageRectSize = Vector2.new() 240 | end 241 | 242 | if RenderData.Image and RenderData.Image.ImageRectOffset then 243 | self.IconLabel.ImageRectOffset = RenderData.Image.ImageRectOffset 244 | else 245 | self.IconLabel.ImageRectOffset = Vector2.new() 246 | end 247 | 248 | return self 249 | end 250 | 251 | function DropDownButton:GetRenderData() 252 | return self.RenderData 253 | end 254 | 255 | 256 | 257 | 258 | 259 | local DropDownFilter = setmetatable({}, UIBase) 260 | DropDownFilter.__index = DropDownFilter 261 | DropDownFilter.ClassName = "DropDownFilter" 262 | DropDownFilter.DefaultFilterText = "Filter..." 263 | 264 | function DropDownFilter.new(Gui) 265 | local self = setmetatable(UIBase.new(Gui), DropDownFilter) 266 | 267 | self.CurrentText = ValueObject.new() 268 | self.ClearButton = self.Gui.ClearButton 269 | self.Background = self.Gui.Background 270 | 271 | self.AutoselectTop = Signal.new() 272 | 273 | self.Maid.VisibleChanged = self.VisibleChanged:connect(function(IsVisible, DoNotAnimate) 274 | if not IsVisible then 275 | self.Gui:ReleaseFocus(false) 276 | end 277 | end) 278 | 279 | self.Maid.Focused = self.Gui.Focused:connect(function() 280 | self.Gui.Text = "" 281 | end) 282 | 283 | self.Maid.FocusLost = self.Gui.FocusLost:connect(function(EnterPressed, InputObject) 284 | local Trimmed = TrimString(self.Gui.Text) 285 | if #Trimmed == 0 then 286 | self.Gui.Text = self.DefaultFilterText 287 | end 288 | 289 | if EnterPressed then 290 | self.AutoselectTop:fire() 291 | end 292 | end) 293 | 294 | self.Maid.FilterTextBoxChanged = self.Gui.Changed:connect(function(Property) 295 | if Property == "Text" then 296 | self:UpdateRender() 297 | end 298 | end) 299 | 300 | self.MouseOver = false 301 | self.Maid.InputBeganEnter = self.Background.InputBegan:connect(function(InputObject) 302 | if InputObject.UserInputType == Enum.UserInputType.MouseMovement then 303 | self.MouseOver = true 304 | self:UpdateRender() 305 | end 306 | end) 307 | 308 | self.Maid.InputEnded = self.Background.InputEnded:connect(function(InputObject) 309 | if InputObject.UserInputType == Enum.UserInputType.MouseMovement then 310 | self.MouseOver = false 311 | self:UpdateRender() 312 | end 313 | end) 314 | 315 | 316 | self.Maid.ClearButtonClick = self.ClearButton.MouseButton1Click:connect(function() 317 | self.Gui.Text = self.DefaultFilterText 318 | self.Gui:ReleaseFocus(false) 319 | end) 320 | 321 | return self 322 | end 323 | 324 | function DropDownFilter:GetText() 325 | local Trimmed = TrimString(self.Gui.Text) 326 | if #Trimmed == 0 then 327 | return self.DefaultFilterText 328 | end 329 | return Trimmed 330 | end 331 | 332 | function DropDownFilter:UpdateRender() 333 | if self.MouseOver then 334 | self.Background.BackgroundColor3 = ThemeSwitcher.GetColorFor("BackgroundOnHover") 335 | else 336 | self.Background.BackgroundColor3 = ThemeSwitcher.GetColorFor("Background") 337 | end 338 | 339 | if not self.Gui:IsFocused() then 340 | local Text = self:GetText() 341 | if self.Gui.Text ~= Text then 342 | self.Gui.Text = self:GetText() 343 | end 344 | end 345 | 346 | if self:IsFiltered() or self.Gui:IsFocused() then 347 | self.ClearButton.Visible = true 348 | else 349 | self.ClearButton.Visible = false 350 | end 351 | 352 | if self:IsFiltered() then 353 | self.CurrentText.Value = self:GetText() 354 | else 355 | self.CurrentText.Value = nil 356 | end 357 | end 358 | 359 | function DropDownFilter:IsFiltered() 360 | return self.Gui.Text ~= self.DefaultFilterText and #TrimString(self.Gui.Text) > 0 361 | end 362 | 363 | 364 | local DropDownPane = setmetatable({}, UIBase) 365 | DropDownPane.__index = DropDownPane 366 | DropDownPane.ClassName = "DropDownPane" 367 | DropDownPane.MaxHeight = 400 368 | 369 | function DropDownPane.new(Gui) 370 | local self = setmetatable(UIBase.new(Gui), DropDownPane) 371 | 372 | self.Selected = ValueObject.new() 373 | 374 | self.ButtonCache = {} 375 | self.Buttons = {} 376 | self.ScrollingFrame = self.Gui.ScrollingFrame 377 | self.Container = self.ScrollingFrame.Container 378 | self.Template = self.Container.Template 379 | self.Template.Visible = false 380 | 381 | 382 | self.Scroller = ScrollingFrame.new(self.Container) 383 | self.Maid.Scroller = self.Scroller 384 | 385 | self.ScrollbarContainer = self.Gui.ScrollbarContainer 386 | self.Scroller:AddScrollbar(self.ScrollbarContainer.Scrollbar) 387 | 388 | do 389 | local ButtonGui = self.Template:Clone() 390 | ButtonGui.Visible = true 391 | ButtonGui.Parent = self.Gui 392 | 393 | self.SelectedRenderButton = DropDownButton.new(ButtonGui) 394 | self.Maid.SelectedRenderButton = self.SelectedRenderButton 395 | 396 | self.SelectedRenderButton:Show() 397 | 398 | self.Maid.SelectedRenderButtonClick = self.SelectedRenderButton.Selected:connect(function() 399 | self:Toggle() 400 | end) 401 | end 402 | 403 | self.FilterBox = DropDownFilter.new(self.Gui.FilterTextBox) 404 | 405 | self.Maid.VisibleChanged = self.VisibleChanged:connect(function(IsVisible, DoNotAnimate) 406 | self:UpdateRender() 407 | 408 | self.FilterBox:SetVisible(IsVisible, DoNotAnimate) 409 | --[[ 410 | if self.Selected.Value then 411 | local ScrollPosition = self.Selected.Value.Gui.AbsolutePosition.Y - self.ScrollingFrame.AbsolutePosition.Y 412 | self.Scroller:ScrollTo(ScrollPosition, true) 413 | end--]] 414 | end) 415 | self:UpdateRender() 416 | 417 | self.Maid.SelectedChanged = self.Selected.Changed:connect(function(NewValue, OldValue) 418 | if OldValue then 419 | OldValue.IsSelected.Value = false 420 | end 421 | if NewValue then 422 | NewValue.IsSelected.Value = true 423 | end 424 | 425 | self:UpdateRender() 426 | end) 427 | 428 | 429 | self.Maid.AutoselectTop = self.FilterBox.AutoselectTop:connect(function() 430 | if self.Buttons[1] then 431 | self.Selected.Value = self.Buttons[1] 432 | self:Hide() 433 | end 434 | end) 435 | 436 | return self 437 | end 438 | 439 | 440 | function DropDownPane:UpdateRender() 441 | if self.Selected.Value then 442 | self.SelectedRenderButton:WithRenderData(self.Selected.Value:GetRenderData()) 443 | else 444 | self.SelectedRenderButton:WithRenderData({ 445 | Name = "Select a class"; 446 | }) 447 | end 448 | 449 | local YHeight = 0 450 | for _, Button in pairs(self.Buttons) do 451 | Button.Gui.Position = UDim2.new(0, 0, 0, YHeight) 452 | YHeight = YHeight + Button.Gui.Size.Y.Offset 453 | end 454 | 455 | if self:IsVisible() then 456 | self.Gui.Size = UDim2.new(self.Gui.Size.X, UDim.new(0, math.min(self.MaxHeight, YHeight + 80))) 457 | else 458 | self.Gui.Size = UDim2.new(self.Gui.Size.X, UDim.new(1, 0)) 459 | end 460 | self.Container.Size = UDim2.new(self.Gui.Size.X, UDim.new(0, YHeight)) 461 | end 462 | 463 | function DropDownPane:GetButtonFromData(Data) 464 | if self.ButtonCache[Data] then 465 | return self.ButtonCache[Data] 466 | end 467 | 468 | local ButtonMaid = MakeMaid() 469 | 470 | local Gui = self.Template:Clone() 471 | Gui.Visible = false 472 | Gui.Name = tostring(Data.ClassName) .. "Button" 473 | Gui.Parent = self.Template.Parent 474 | 475 | local Button = DropDownButton.new(Gui) 476 | :WithData(Data) 477 | :WithRenderData({ 478 | Name = Data.ClassName; 479 | Image = IconHandler:GetIcon(Data.ClassName); 480 | }) 481 | :WithScroller(self.Scroller) 482 | 483 | ButtonMaid.Button = Button 484 | ButtonMaid.Selected = Button.Selected:connect(function() 485 | self.Selected.Value = Button 486 | self:Hide() 487 | end) 488 | 489 | 490 | self.ButtonCache[Data] = Button 491 | self.Maid[Button] = ButtonMaid 492 | 493 | return Button 494 | end 495 | 496 | 497 | function DropDownPane:UpdateButtons(Suggested) 498 | for _, Item in pairs(self.Buttons) do 499 | Item:Hide(true) 500 | end 501 | 502 | self.Buttons = {} 503 | if Suggested then 504 | for _, Data in pairs(Suggested) do 505 | local Button = self:GetButtonFromData(Data) 506 | Button:Show(true) 507 | table.insert(self.Buttons, Button) 508 | end 509 | end 510 | 511 | self:UpdateRender() 512 | end 513 | 514 | 515 | 516 | local DropDown = setmetatable({}, UIBase) 517 | DropDown.__index = DropDown 518 | DropDown.ClassName = "DropDown" 519 | 520 | function DropDown.new(Gui) 521 | local self = setmetatable(UIBase.new(Gui), DropDown) 522 | 523 | self.Pane = DropDownPane.new(self.Gui.Pane) 524 | self.Maid.Pane = self.Pane 525 | 526 | self.Maid.Click = self.Gui.InputBegan:connect(function(InputObject) 527 | if InputObject.UserInputType == Enum.UserInputType.MouseButton1 then 528 | self.Pane:Toggle() 529 | end 530 | end) 531 | 532 | return self 533 | end 534 | 535 | 536 | local CheckboxPane = setmetatable({}, UIBase) 537 | CheckboxPane.ClassName = "CheckboxPane" 538 | CheckboxPane.__index = CheckboxPane 539 | 540 | function CheckboxPane.new(Gui) 541 | local self = setmetatable(UIBase.new(Gui), CheckboxPane) 542 | 543 | self.Checkboxes = {} 544 | self.SettingsChanged = Signal.new() 545 | 546 | self.CheckboxTemplate = self.Gui.CheckboxTemplate 547 | self.CheckboxTemplate.Visible = false 548 | 549 | return self 550 | end 551 | 552 | function CheckboxPane:AddCheckbox(Data) 553 | assert(Data.SerializeName) 554 | 555 | local Gui = self.CheckboxTemplate:Clone() 556 | Gui.Visible = true 557 | Gui.Parent = self.CheckboxTemplate.Parent 558 | 559 | local CheckboxMaid = MakeMaid() 560 | 561 | local checkbox = Checkbox.new(Gui) 562 | :WithData(Data) 563 | :WithRenderData({ 564 | Name = Data.Name; 565 | }) 566 | CheckboxMaid:GiveTask(checkbox) 567 | 568 | CheckboxMaid:GiveTask(checkbox.Checked.Changed:connect(function() 569 | self.SettingsChanged:fire() 570 | end)) 571 | 572 | if Data.DefaultValue then 573 | checkbox.Checked.Value = Data.DefaultValue 574 | end 575 | 576 | checkbox:Show() 577 | 578 | self.Maid[checkbox] = CheckboxMaid 579 | table.insert(self.Checkboxes, checkbox) 580 | 581 | self:UpdateRender() 582 | 583 | return checkbox 584 | end 585 | 586 | function CheckboxPane:GetSettings() 587 | local Settings = {} 588 | for _, checkbox in pairs(self.Checkboxes) do 589 | Settings[checkbox.Data.SerializeName] = checkbox.Checked.Value 590 | end 591 | return Settings 592 | end 593 | 594 | function CheckboxPane:UpdateRender() 595 | local YHeight = 0 596 | for _, Button in pairs(self.Checkboxes) do 597 | Button.Gui.Position = UDim2.new(0, 0, 0, YHeight) 598 | YHeight = YHeight + Button.Gui.Size.Y.Offset 599 | end 600 | end 601 | 602 | 603 | 604 | local Pane = setmetatable({}, UIBase) 605 | Pane.__index = Pane 606 | Pane.ClassName = "Pane" 607 | 608 | function Pane.new(Gui, Selection) 609 | local self = setmetatable(UIBase.new(Gui), Pane) 610 | 611 | -- self.Done = Signal.new() 612 | self.ConversionStarting = Signal.new() 613 | self.ConversionEnding = Signal.new() 614 | 615 | self.Selection = Selection or error("No selection") 616 | self.Content = self.Gui.Content 617 | self.WarningPane = self.Gui.WarningPane 618 | self.RetryButton = self.WarningPane.RetryButton 619 | 620 | self.Buttons = self.Content.Buttons 621 | self.ConvertButton = self.Buttons.ConvertButton 622 | self.StatusLabel = self.Buttons.StatusLabel 623 | self.LoadedStatusLabel = self.Buttons.LoadedStatusLabel 624 | 625 | self.DropDown = DropDown.new(self.Content.DropDown) 626 | self.Maid.DropDown = self.DropDown 627 | 628 | self.CheckboxPane = CheckboxPane.new(self.Content.Checkboxes) 629 | self.Maid.CheckboxPane = self.CheckboxPane 630 | 631 | self.CheckboxPane:AddCheckbox({ 632 | Name = "Include not browsable"; 633 | SerializeName = "IncludeNotBrowsable"; 634 | DefaultValue = false; 635 | }) 636 | self.CheckboxPane:AddCheckbox({ 637 | Name = "Include not creatable"; 638 | SerializeName = "IncludeNotCreatable"; 639 | DefaultValue = false; 640 | }) 641 | self.CheckboxPane:AddCheckbox({ 642 | Name = "Include services"; 643 | SerializeName = "IncludeServices"; 644 | DefaultValue = false; 645 | }) 646 | 647 | self.Maid.SettingsChanged = self.CheckboxPane.SettingsChanged:connect(function() 648 | self:UpdateRender() 649 | end) 650 | 651 | -- self.Maid.ScreenGui = self.Gui.Parent 652 | -- self.Maid.CloseButtonClick = self.Gui.Header.CloseButton.MouseButton1Click:connect(function() 653 | -- self.Done:fire() 654 | -- end) 655 | 656 | 657 | self.Maid.RetryButton = self.RetryButton.MouseButton1Click:connect(function() 658 | self:SelectHttpService() 659 | self.Converter:GetAPIAsync(true) 660 | self:UpdateRender() 661 | end) 662 | 663 | self.Maid:GiveTask(self.DropDown.Pane.Selected.Changed:connect(function() 664 | self:UpdateRender() 665 | end)) 666 | 667 | self.Maid.ConvertButtonClick = self.ConvertButton.MouseButton1Click:connect(function() 668 | self:UpdateRender() 669 | if self.IsAvailable then 670 | self:DoConversion() 671 | end 672 | end) 673 | 674 | self.Maid.VisibleChanged = self.VisibleChanged:connect(function(IsVisible, DoNotAnimate) 675 | self.CheckboxPane:SetVisible(IsVisible) 676 | self.Gui.Visible = IsVisible 677 | 678 | self.CheckboxPane:SetVisible(IsVisible) 679 | self.DropDown:SetVisible(IsVisible) 680 | 681 | self:UpdateRender() 682 | 683 | if IsVisible then 684 | self.Maid.SelectionChangedEvent = self.Selection.SelectionChanged:connect(function() 685 | if self:IsVisible() then 686 | self:UpdateRender() 687 | end 688 | end) 689 | else 690 | self.Maid.SelectionChangedEvent = nil 691 | end 692 | end) 693 | 694 | self.Maid.FilterChanged = self.DropDown.Pane.FilterBox.CurrentText.Changed:connect(function(CurrentText) 695 | self:UpdateRender() 696 | end) 697 | 698 | 699 | 700 | return self 701 | end 702 | 703 | function Pane:SelectHttpService() 704 | if not (#self.Selection:Get() == 1 and self.Selection:Get()[1] == HttpService) then 705 | self.Selection:Set({HttpService}) 706 | end 707 | end 708 | 709 | function Pane:UpdateRender() 710 | local Selection = self.Selection:Get() 711 | local IsAvailable = true 712 | 713 | self.WarningPane.Visible = false 714 | self.Content.Visible = true 715 | self.LoadedStatusLabel.Text = self.Converter:GetLoadedText() 716 | 717 | if self.Converter:IsNoHttp() then 718 | self.StatusLabel.Text = "No HTTP" 719 | self.WarningPane.Visible = true 720 | self.Content.Visible = false 721 | 722 | -- if not self.WasNoHttp then 723 | -- self.WasNoHttp = true 724 | -- self:SelectHttpService() 725 | -- end 726 | 727 | IsAvailable = false 728 | else 729 | self.WasNoHttp = false 730 | end 731 | 732 | if IsAvailable then 733 | if #Selection == 0 then 734 | self.StatusLabel.Text = "Nothing selected" 735 | IsAvailable = false 736 | elseif not self:GetClassName() then 737 | self.StatusLabel.Text = ("No class picked to convert to") 738 | IsAvailable = false 739 | elseif not self.Converter:CanConvert(Selection) then 740 | self.StatusLabel.Text = ("%d item%s are not similar"):format(#Selection, #Selection == 1 and "" or "s") 741 | else 742 | self.StatusLabel.Text = ("%d item%s selected"):format(#Selection, #Selection == 1 and "" or "s") 743 | end 744 | end 745 | 746 | if IsAvailable then 747 | self.ConvertButton.AutoButtonColor = true 748 | self.ConvertButton.TextTransparency = 0 749 | self.ConvertButton.Active = true 750 | self.ConvertButton.Style = Enum.ButtonStyle.RobloxRoundDefaultButton 751 | else 752 | self.ConvertButton.AutoButtonColor = false 753 | self.ConvertButton.Active = false 754 | self.ConvertButton.TextTransparency = 0.7 755 | self.ConvertButton.Style = Enum.ButtonStyle.RobloxRoundButton 756 | end 757 | 758 | self.IsAvailable = IsAvailable 759 | 760 | if self:IsVisible() then 761 | local Settings = self.CheckboxPane:GetSettings() 762 | Settings.Filter = self.DropDown.Pane.FilterBox.CurrentText.Value 763 | 764 | local Suggested = self.Converter:GetSuggested(Selection, Settings) 765 | self.DropDown.Pane:UpdateButtons(Suggested) 766 | else 767 | self.DropDown.Pane:UpdateButtons(nil) 768 | end 769 | end 770 | 771 | function Pane:WithConverter(Converter) 772 | self.Converter = Converter or error("No converter") 773 | 774 | self.Maid.HttpNotEnabledChanged = self.Converter.HttpNotEnabledChanged:connect(function() 775 | self:UpdateRender() 776 | end) 777 | 778 | return self 779 | end 780 | 781 | function Pane:GetClassName() 782 | local Selected = self.DropDown.Pane.Selected.Value 783 | if Selected then 784 | return Selected:GetData().ClassName 785 | end 786 | 787 | return nil 788 | end 789 | 790 | function Pane:DoConversion() 791 | local ClassName = self:GetClassName() 792 | local Selection = self.Selection:Get() 793 | if Selection and ClassName then 794 | print(("[Converter] - Converting selection to '%s'"):format(tostring(ClassName))) 795 | self.ConversionStarting:fire() 796 | 797 | local NewSelection = {} 798 | for _, Object in pairs(Selection) do 799 | local Result = self.Converter:ChangeClass(Object, ClassName) or Object 800 | table.insert(NewSelection, Result) 801 | end 802 | 803 | self.Selection:Set(NewSelection) 804 | self.ConversionEnding:fire() 805 | else 806 | print("[Converter] - No selection or class name") 807 | end 808 | end 809 | 810 | return Pane -------------------------------------------------------------------------------- /src/ValueObject.lua: -------------------------------------------------------------------------------- 1 | --- To work like value objects in ROBLOX and track a single item, 2 | -- with `.Changed` events 3 | -- @classmod ValueObject 4 | 5 | local Signal = require(script.Parent:WaitForChild("Signal")) 6 | local Maid = require(script.Parent:WaitForChild("Maid")) 7 | 8 | local ValueObject = {} 9 | ValueObject.ClassName = "ValueObject" 10 | 11 | --- The value of the ValueObject 12 | -- @tfield Variant Value 13 | 14 | --- Event fires when the value's object value change 15 | -- @signal Changed 16 | -- @tparam Variant newValue The new value 17 | -- @tparam Variant oldValue The old value 18 | 19 | 20 | --- Constructs a new value object 21 | -- @constructor 22 | -- @treturn ValueObject 23 | function ValueObject.new(baseValue) 24 | local self = {} 25 | 26 | rawset(self, "_value", baseValue) 27 | 28 | self._maid = Maid.new() 29 | 30 | self.Changed = Signal.new() -- :Fire(newValue, oldValue, maid) 31 | self._maid:GiveTask(self.Changed) 32 | 33 | return setmetatable(self, ValueObject) 34 | end 35 | 36 | function ValueObject:__index(index) 37 | if index == "Value" then 38 | return self._value 39 | elseif ValueObject[index] then 40 | return ValueObject[index] 41 | elseif index == "_value" then 42 | return nil -- Edge case 43 | else 44 | error(("%q is not a member of ValueObject"):format(tostring(index))) 45 | end 46 | end 47 | 48 | function ValueObject:__newindex(index, value) 49 | if index == "Value" then 50 | local previous = rawget(self, "_value", value) 51 | if previous ~= value then 52 | rawset(self, "_value", value) 53 | 54 | local maid = Maid.new() 55 | self._maid._valueMaid = maid 56 | self.Changed:Fire(value, previous, maid) 57 | end 58 | else 59 | error(("%q is not a member of ValueObject"):format(tostring(index))) 60 | end 61 | end 62 | 63 | --- Forces the value to be nil on cleanup, cleans up the Maid 64 | function ValueObject:Destroy() 65 | self.Value = nil 66 | self._maid:DoCleaning() 67 | end 68 | 69 | return ValueObject 70 | -------------------------------------------------------------------------------- /src/init.server.lua: -------------------------------------------------------------------------------- 1 | --- Class conversion plugin 2 | -- @author Quenty 3 | -- With help from: Badcc, Stravant, TreyReynolds 4 | 5 | -- luacheck: globals plugin 6 | 7 | local Players = game:GetService("Players") 8 | local UserInputService = game:GetService("UserInputService") 9 | local HttpService = game:GetService("HttpService") 10 | 11 | local IS_DEBUG_MODE = script:IsDescendantOf(game) 12 | if IS_DEBUG_MODE then 13 | warn("[Converter] - Starting plugin in debug mode") 14 | while not Players.LocalPlayer do 15 | wait(0.05) 16 | end 17 | end 18 | 19 | local Converter = require(script:WaitForChild("Converter")) 20 | local UI = require(script:WaitForChild("UI")) 21 | local Signal = require(script:WaitForChild("Signal")) 22 | local ThemeSwitcher = require(script:WaitForChild("ThemeSwitcher")) 23 | 24 | local Selection do 25 | if not IS_DEBUG_MODE then 26 | Selection = game.Selection 27 | else 28 | -- The things I do for testing... 29 | Selection = {} 30 | Selection.Items = {} 31 | Selection.SelectionChanged = Signal.new() 32 | 33 | function Selection:Get() 34 | return Selection.Items 35 | end 36 | function Selection:Set(Items) 37 | self.Items = Items 38 | self.SelectionChanged:fire() 39 | end 40 | 41 | local Mouse = Players.LocalPlayer:GetMouse() 42 | Mouse.Button1Down:connect(function() 43 | local New = {} 44 | if Mouse.Target and Mouse.Target:IsA("BasePart") then 45 | if UserInputService:IsKeyDown(Enum.KeyCode.LeftControl) then 46 | for _, Item in pairs(Selection:Get()) do 47 | table.insert(New, Item) 48 | end 49 | end 50 | if not Mouse.Target.Locked then 51 | table.insert(New, Mouse.Target) 52 | end 53 | end 54 | Selection:Set(New) 55 | end) 56 | end 57 | end 58 | 59 | local plugin = plugin 60 | if IS_DEBUG_MODE then 61 | local FakeMetatable = {} 62 | function FakeMetatable.new(OldPlugin) 63 | local self = setmetatable({}, FakeMetatable) 64 | 65 | self.Settings = {} 66 | self.OldPlugin = OldPlugin 67 | return self 68 | end 69 | 70 | function FakeMetatable:__index(Index) 71 | local Result = rawget(FakeMetatable, Index) 72 | if Result then 73 | return Result 74 | end 75 | 76 | return self.OldPlugin[Index] 77 | end 78 | 79 | function FakeMetatable:GetSetting(Key) 80 | return self.Settings[Key] 81 | end 82 | 83 | function FakeMetatable:SetSetting(Key, Value) 84 | self.Settings[Key] = Value 85 | end 86 | 87 | -- Override plugin settings (get/set) 88 | plugin = FakeMetatable.new(plugin) 89 | end 90 | 91 | local converter = Converter.new(IS_DEBUG_MODE) 92 | :WithPluginForCache(plugin) 93 | 94 | local screenGui 95 | do 96 | -- Activates the plugin 97 | if IS_DEBUG_MODE then 98 | screenGui = Instance.new("ScreenGui") 99 | screenGui.Name = "Converter" 100 | screenGui.Parent = Players.LocalPlayer.PlayerGui 101 | screenGui.Enabled = false 102 | else 103 | local info = DockWidgetPluginGuiInfo.new( 104 | Enum.InitialDockState.Float, 105 | false, 106 | true, 107 | 250, 108 | 320, 109 | 200, 110 | 240 111 | ) 112 | screenGui = plugin:CreateDockWidgetPluginGui("Quenty_Class_Converter", info) 113 | ThemeSwitcher.SetDockWidget(screenGui) 114 | screenGui.Title = "Quenty's Class Converter Plugin" 115 | end 116 | screenGui:BindToClose(function() 117 | screenGui.Enabled = false 118 | end) 119 | local function initializeGui() 120 | local main = script.Parent.ScreenGui.Main:Clone() 121 | main.Parent = screenGui 122 | 123 | local ui = UI.new(main, Selection) 124 | :WithConverter(converter) 125 | 126 | screenGui:GetPropertyChangedSignal("Enabled"):Connect(function() 127 | ui:SetVisible(screenGui.Enabled) 128 | if not screenGui.Enabled then 129 | ui.DropDown:SetVisible(false) 130 | end 131 | end) 132 | ui:SetVisible(screenGui.Enabled) 133 | 134 | if not IS_DEBUG_MODE then 135 | local ChangeHistoryService = game:GetService("ChangeHistoryService") 136 | ui.ConversionStarting:connect(function() 137 | ChangeHistoryService:SetWaypoint("Conversion_" .. HttpService:GenerateGUID(true)) 138 | end) 139 | ui.ConversionEnding:connect(function() 140 | ChangeHistoryService:SetWaypoint("Conversion_" .. HttpService:GenerateGUID(true)) 141 | end) 142 | 143 | 144 | screenGui.WindowFocusReleased:Connect(function() 145 | ui.DropDown:SetVisible(false) 146 | end) 147 | end 148 | end 149 | 150 | if screenGui.Enabled then 151 | spawn(function() 152 | initializeGui() 153 | end) 154 | else 155 | -- Wait to load GUI 156 | local connection 157 | connection = screenGui:GetPropertyChangedSignal("Enabled"):Connect(function() 158 | connection:disconnect() 159 | initializeGui() 160 | end) 161 | end 162 | end 163 | 164 | if not IS_DEBUG_MODE then 165 | local toolbar = plugin:CreateToolbar("Object") 166 | 167 | local button = toolbar:CreateButton( 168 | "Class converter", 169 | "Converts classes from one item to another", 170 | "rbxassetid://906772526" 171 | ) 172 | 173 | screenGui:GetPropertyChangedSignal("Enabled"):Connect(function() 174 | button:SetActive(screenGui.Enabled) 175 | end) 176 | 177 | button.Click:connect(function() 178 | screenGui.Enabled = not screenGui.Enabled 179 | end) 180 | else 181 | screenGui.Enabled = true 182 | end --------------------------------------------------------------------------------