-------------- CONSTANTS -------------- local STICK_ROTATION_MULTIPLIER = 3 local THUMBSTICK_DEADZONE = 0.2 -------------- SERVICES -------------- local ContextActionService = game:GetService("ContextActionService") local ReplicatedStorage = game:GetService('ReplicatedStorage') local CoreGui = game:GetService('CoreGui') ------------ MODULES ------------------- local GuiRoot = CoreGui:FindFirstChild('RobloxGui') local Modules = GuiRoot:FindFirstChild('Modules') local AppState = require(Modules.LuaApp.Legacy.AvatarEditor.AppState) local SetConsoleMenuLevel = require(Modules.LuaApp.Actions.SetConsoleMenuLevel) local ResetCategory = require(Modules.LuaApp.Actions.ResetCategory) local ToggleAvatarType = require(Modules.LuaApp.Actions.ToggleAvatarType) local ToggleAvatarEditorFullView = require(Modules.LuaApp.Actions.ToggleAvatarEditorFullView) local CreateCharacterManager = require(Modules.LuaApp.Legacy.AvatarEditor.CharacterManager) local CreatePageManager = require(Modules.LuaApp.Legacy.AvatarEditor.PageManagerConsole) local CreateCategoryMenu = require(Modules.LuaApp.Legacy.AvatarEditor.CategoryMenuConsole) local CreateTabList = require(Modules.LuaApp.Legacy.AvatarEditor.TabListConsole) local ConsoleButtonIndicators = require(Modules.LuaApp.Legacy.AvatarEditor.ConsoleButtonIndicators) local LayoutInfo = require(Modules.LuaApp.Legacy.AvatarEditor.LayoutInfoConsole) local Flags = require(Modules.LuaApp.Legacy.AvatarEditor.Flags) local XboxAppState = require(Modules.Shell.AppState) ----------- UTILITIES -------------- local Utilities = require(Modules.LuaApp.Legacy.AvatarEditor.Utilities) local TableUtilities = require(Modules.LuaApp.TableUtilities) ------------ SHELL MODULES ------------------- local ShellModules = script.Parent local CameraManager = require(ShellModules:FindFirstChild('CameraManager')) local EventHub = require(ShellModules:FindFirstChild('EventHub')) local LoadingWidget = require(ShellModules:FindFirstChild('LoadingWidget')) -------------- FFLAGS -------------- local SoundManager = require(ShellModules:FindFirstChild('SoundManager')) ------------ VARIABLES ------------------- local characterTemplates = { CharacterR6 = ReplicatedStorage:WaitForChild('CharacterR6'); CharacterR15 = ReplicatedStorage:WaitForChild('CharacterR15'); CharacterR15New = ReplicatedStorage:WaitForChild('CharacterR15New'); } local cameraTweenerObject = { tweenCamera = function(newCFrame, newFOV) CameraManager:UpdateAvatarEditorCamera(newCFrame, newFOV) end } ----------- CLASS DECLARATION -------------- local function createAvatarEditorView() AppState:Init() local this = {} local storeChangedCn = nil local scrollingFrameCn = nil local avatarEditorLoader = nil local savedSelectedGuiObject = nil local selectedGuiObjectEnterFullView = nil local toggleButtonDebounce = true local toggleViewDebounce = true local showCount = 0 local inShow = false local inFocus = false local lastCharacterSaveTime = nil local characterUpdated = false local characterEquipped = false local savingAvatarMutex = false local rotationInfo = { rotation = 0; lastRotation = 0; delta = 0; } local Container = Utilities.create'ScrollingFrame' { Name = 'AvatarEditorContainer'; Size = UDim2.new(1, 0, 1, 0); CanvasSize = UDim2.new(1, 0, 1, 0); BackgroundTransparency = 1; ScrollingEnabled = false; Selectable = false; Visible = true; BorderSizePixel = 0; ScrollBarThickness = 0; } local BackgroundOverlay = Utilities.create'ImageLabel' { Name = 'BackgroundOverlay'; Size = UDim2.new(1, 0, 1, 0); BackgroundTransparency = 1; Visible = true; Image = 'rbxasset://textures/ui/Shell/AvatarEditor/graphic/gr-background overlay merge.png'; ZIndex = LayoutInfo.BackgroundLayer; Parent = Container; } local frame = Utilities.create'Frame' { Position = UDim2.new(0, 0, 0, 270); Size = UDim2.new(1, 0, 1, 0); Name = "PageFrame"; BackgroundTransparency = 1; ClipsDescendants = true; Visible = true; Selectable = false; ZIndex = LayoutInfo.BasicLayer; Parent = Container; } local scrollingFrame = Utilities.create'ScrollingFrame' { AnchorPoint = Vector2.new(1, 0); Position = UDim2.new(1, -99, 0, 0); Size = UDim2.new(0, 491, 1, -270); Name = "ScrollingFrame"; BackgroundTransparency = 1; ClipsDescendants = true; Visible = true; ScrollingEnabled = false; ScrollBarThickness = 0; Selectable = false; ZIndex = LayoutInfo.BasicLayer; Parent = frame; } ConsoleButtonIndicators.init(Container) local warningFrame = Utilities.create'Frame' { AnchorPoint = Vector2.new(0.5, 0); Position = UDim2.new(0.5, 0, 0, 0); Size = UDim2.new(0, LayoutInfo.WarningMaxLength, 1, 0); Name = "WarningWidget"; BackgroundTransparency = 1; Visible = true; Selectable = false; ZIndex = LayoutInfo.IndicatorLayer; Parent = Container; } local CameraController = require(Modules.LuaApp.Legacy.AvatarEditor.CameraController)( cameraTweenerObject, LayoutInfo.CameraCenterScreenPosition) local characterManager = CreateCharacterManager( { get = Utilities.httpGet; post = Utilities.httpPost; }, characterTemplates, 1 ) local WarningWidget = require(Modules.LuaApp.Legacy.AvatarEditor.WarningWidget)(warningFrame, characterManager, true) local PageManager = CreatePageManager( XboxAppState.store:getState().RobloxUser.rbxuid, scrollingFrame, characterManager ) characterManager:setUpdateCameraCallback(CameraController.updateCamera) local CategoryMenu = CreateCategoryMenu(Container) local TabList = CreateTabList(Container, PageManager) local function initStickRotation() local gamepadInput = Vector2.new(0, 0) ContextActionService:UnbindCoreAction("StickRotation") ContextActionService:BindCoreAction("StickRotation", function(actionName, inputState, inputObject) if inputState == Enum.UserInputState.Change then gamepadInput = inputObject.Position or gamepadInput gamepadInput = Vector2.new(gamepadInput.X, gamepadInput.Y) if math.abs(gamepadInput.X) > THUMBSTICK_DEADZONE then rotationInfo.delta = STICK_ROTATION_MULTIPLIER * gamepadInput.X else rotationInfo.delta = 0 end end end, false, Enum.KeyCode.Thumbstick2) end local function initKeyControls() toggleButtonDebounce = true toggleViewDebounce = true ContextActionService:UnbindCoreAction("KeyControls") ContextActionService:BindCoreAction("KeyControls", function(actionName, inputState, inputObject) if inputState == Enum.UserInputState.Begin then if inputObject.KeyCode == Enum.KeyCode.ButtonSelect and not AppState.Store:getState().FullView then if toggleButtonDebounce then toggleButtonDebounce = false SoundManager:Play('ButtonPress') AppState.Store:dispatch(ToggleAvatarType()) toggleButtonDebounce = true end end if inputObject.KeyCode == Enum.KeyCode.ButtonR3 then if toggleViewDebounce then toggleViewDebounce = false SoundManager:Play('ScreenChange') AppState.Store:dispatch(ToggleAvatarEditorFullView()) toggleViewDebounce = true end end end end, false, Enum.KeyCode.ButtonR3, Enum.KeyCode.ButtonSelect) end local function initMenuActions() ContextActionService:UnbindCoreAction("AvatarEditorMenu") ContextActionService:BindCoreAction("AvatarEditorMenu", function(actionName, inputState, inputObject) if inputState == Enum.UserInputState.End then if inputObject.KeyCode == Enum.KeyCode.ButtonB then if AppState.Store:getState().ConsoleMenuLevel > LayoutInfo.ConsoleMenuLevel.CategoryMenu then local currentMenuLevel = AppState.Store:getState().ConsoleMenuLevel SoundManager:Play('PopUp') AppState.Store:dispatch(SetConsoleMenuLevel(currentMenuLevel - 1)) else -- Back to avatar page in AppShell AppState.Store:dispatch(ResetCategory()) AppState.Store:dispatch(SetConsoleMenuLevel(LayoutInfo.ConsoleMenuLevel.None)) return Enum.ContextActionResult.Pass end end elseif inputState == Enum.UserInputState.Begin then if inputObject.KeyCode == Enum.KeyCode.ButtonL2 then if AppState.Store:getState().ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.CategoryMenu then CategoryMenu:SelectPreviousPage() elseif AppState.Store:getState().ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.TabList then TabList:SelectPreviousPage() elseif AppState.Store:getState().ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.AssetsPage then PageManager:SelectPreviousPage() end elseif inputObject.KeyCode == Enum.KeyCode.ButtonR2 then if AppState.Store:getState().ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.CategoryMenu then CategoryMenu:SelectNextPage() elseif AppState.Store:getState().ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.TabList then TabList:SelectNextPage() elseif AppState.Store:getState().ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.AssetsPage then PageManager:SelectNextPage() end end end end, false, Enum.KeyCode.ButtonB, Enum.KeyCode.ButtonL2, Enum.KeyCode.ButtonR2) end local function updateFullViewCoreAction(fullView) ContextActionService:UnbindCoreAction("FullView") if fullView == true then ContextActionService:BindCoreAction("FullView", function(actionName, inputState, inputObject) if inputState == Enum.UserInputState.End then if inputObject.KeyCode == Enum.KeyCode.ButtonB then SoundManager:Play('ScreenChange') AppState.Store:dispatch(ToggleAvatarEditorFullView()) end return Enum.ContextActionResult.Sink end end, false, Enum.KeyCode.ButtonB, Enum.KeyCode.ButtonA, Enum.KeyCode.ButtonL2, Enum.KeyCode.ButtonR2) BackgroundOverlay.Visible = false elseif fullView == false then BackgroundOverlay.Visible = true end end local function FocusAll() initStickRotation() initKeyControls() initMenuActions() CameraController:Focus() CategoryMenu:Focus() TabList:Focus() PageManager:Focus() WarningWidget:Focus() if not savedSelectedGuiObject and not AppState.Store:getState().FullView then AppState.Store:dispatch(SetConsoleMenuLevel(LayoutInfo.ConsoleMenuLevel.CategoryMenu)) elseif savedSelectedGuiObject then Utilities.setSelectedCoreObject(savedSelectedGuiObject) end updateFullViewCoreAction(AppState.Store:getState().FullView) storeChangedCn = AppState.Store.Changed:Connect(function(newState, oldState) if newState.FullView ~= oldState.FullView then updateFullViewCoreAction(newState.FullView) if newState.FullView == true then local selectedCoreObject = Utilities.getSelectedCoreObject() if selectedCoreObject then selectedGuiObjectEnterFullView = selectedCoreObject end Utilities.setSelectedCoreObject(nil) else if selectedGuiObjectEnterFullView then Utilities.setSelectedCoreObject(selectedGuiObjectEnterFullView) end end end if not characterEquipped or not characterUpdated then local didUpdate = false local didEquip = false if newState.Character.AvatarType ~= oldState.Character.AvatarType then didUpdate = true end if newState.Character.Assets ~= oldState.Character.Assets then for assetType, assetList in pairs(oldState.Character.Assets) do if not newState.Character.Assets[assetType] and assetList then if next(assetList) ~= nil then didUpdate = true didEquip = true break end end end if not didUpdate or not didEquip then for assetType, _ in pairs(newState.Character.Assets) do if newState.Character.Assets[assetType] ~= oldState.Character.Assets[assetType] then local addTheseAssets = TableUtilities.ListDifference( newState.Character.Assets[assetType] or {}, oldState.Character.Assets[assetType] or {}) local removeTheseAssets = TableUtilities.ListDifference(oldState.Character.Assets[assetType] or {}, newState.Character.Assets[assetType] or {}) if next(addTheseAssets) ~= nil or next(removeTheseAssets) ~= nil then didUpdate = true didEquip = true break end end end end end if newState.Character.BodyColors ~= oldState.Character.BodyColors then local differentBodyColors = TableUtilities.TableDifference(newState.Character.BodyColors, oldState.Character.BodyColors) if next(differentBodyColors) ~= nil then didUpdate = true end end if newState.Character.Scales ~= oldState.Character.Scales then local differentScales = TableUtilities.TableDifference(newState.Character.Scales, oldState.Character.Scales) if next(differentScales) ~= nil then didUpdate = true end end if not characterEquipped then characterEquipped = didEquip end if not characterUpdated then characterUpdated = didUpdate end end end) scrollingFrameCn = scrollingFrame.Changed:connect( function(prop) if prop == 'CanvasPosition' then PageManager:updateListContent(scrollingFrame.CanvasPosition.Y) end end ) end local function RemoveFocusAll() CameraController:RemoveFocus() PageManager:RemoveFocus() CategoryMenu:RemoveFocus() TabList:RemoveFocus() WarningWidget:RemoveFocus() Utilities.disconnectEvent(storeChangedCn) Utilities.disconnectEvent(scrollingFrameCn) ContextActionService:UnbindCoreAction("FullView") ContextActionService:UnbindCoreAction("AvatarEditorMenu") ContextActionService:UnbindCoreAction("StickRotation") ContextActionService:UnbindCoreAction("KeyControls") end function this:Show(parent) local startCount = showCount inShow = true characterUpdated = false characterEquipped = false local prevAvatarEditorLoader = avatarEditorLoader Utilities.fastSpawn(function() if startCount == showCount then avatarEditorLoader = LoadingWidget( {Parent = parent}, {function() if prevAvatarEditorLoader then prevAvatarEditorLoader:AwaitFinished() prevAvatarEditorLoader:Cleanup() end if startCount == showCount then characterManager.initFromServer() characterManager.show() end end} ) avatarEditorLoader:AwaitFinished() if startCount == showCount then avatarEditorLoader:Cleanup() avatarEditorLoader = nil end end if startCount == showCount then Container.Parent = parent if inFocus then FocusAll() end while startCount == showCount do local deltaT = Utilities.renderWait() if startCount == showCount then rotationInfo.rotation = rotationInfo.rotation + deltaT * rotationInfo.delta characterManager.setRotation(rotationInfo.rotation) if not lastCharacterSaveTime or tick() - lastCharacterSaveTime > 5 then characterManager.saveToServer(false) lastCharacterSaveTime = tick() end end end end end) end function this:Hide() inShow = false showCount = showCount + 1 characterManager.hide() savedSelectedGuiObject = nil selectedGuiObjectEnterFullView = nil Container.Parent = nil if not avatarEditorLoader then RemoveFocusAll() CategoryMenu:Hide() WarningWidget:Hide() local savedCharacterUpdated = characterUpdated local savedCharacterEquipped = characterEquipped spawn(function() while savingAvatarMutex do wait() end savingAvatarMutex = true characterManager.saveToServer(true) savingAvatarMutex = false if savedCharacterUpdated then Utilities.fastSpawn(function() EventHub:dispatchEvent(EventHub.Notifications["CharacterUpdated"]) end) end if savedCharacterEquipped then Utilities.fastSpawn(function() EventHub:dispatchEvent(EventHub.Notifications["CharacterEquipped"], AppState.Store:getState().Character.Assets, savedCharacterUpdated) end) end end) end end --Rebind actions, make them in the right order function this:Focus() inFocus = true if inShow and not avatarEditorLoader then FocusAll() end end function this:RemoveFocus() inFocus = false if inShow and not avatarEditorLoader then savedSelectedGuiObject = Utilities.getSelectedCoreObject() RemoveFocusAll() end end function this:Destruct() this:RemoveFocus() this:Hide() Container:Destroy() characterManager.destroy() AppState:Destruct() end return this end return createAvatarEditorView