This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,8 @@
{
"lint": {
"SameLineStatement": "fatal",
"LocalShadow": "fatal",
"FunctionUnused": "fatal",
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,309 @@
--[[
// FileName: ContextMenuGui.lua
// Written by: TheGamer101
// Description: Module for creating the context GUI.
]]
-- CONSTANTS
local BOTTOM_SCREEN_PADDING_PERCENT = 0.02
-- SERVICES
local CoreGuiService = game:GetService("CoreGui")
local GuiService = game:GetService("GuiService")
--- VARIABLES
local RobloxGui = CoreGuiService:WaitForChild("RobloxGui")
local CoreGuiModules = RobloxGui:WaitForChild("Modules")
local SettingsModules = CoreGuiModules:WaitForChild("Settings")
local AvatarMenuModules = CoreGuiModules:WaitForChild("AvatarContextMenu")
local PlayerCarousel = nil
local PlayerChangedEvent = Instance.new("BindableEvent")
--- Modules
local ContextMenuUtil = require(AvatarMenuModules:WaitForChild("ContextMenuUtil"))
local Utility = require(SettingsModules:WaitForChild("Utility"))
local ContextMenuGui = {}
ContextMenuGui.__index = ContextMenuGui
-- PRIVATE METHODS
function ContextMenuGui:CreateContextMenuHolder(player)
local contextMenuHolder = Instance.new("Frame")
contextMenuHolder.Name = "AvatarContextMenu"
contextMenuHolder.Position = UDim2.new(0, 0, 0, 0)
contextMenuHolder.Size = UDim2.new(1, 0, 1, 0)
contextMenuHolder.BackgroundTransparency = 1
contextMenuHolder.Parent = RobloxGui
contextMenuHolder.AutoLocalize = false
return contextMenuHolder
end
function ContextMenuGui:CreateLeaveMenuButton(frame, theme)
local function closeMenu()
self.CloseMenuFunc()
end
local closeMenuButton = Instance.new("ImageButton")
closeMenuButton.Name = "CloseMenuButton"
closeMenuButton.BackgroundTransparency = 1
closeMenuButton.AnchorPoint = Vector2.new(1, 0)
closeMenuButton.Position = UDim2.new(1, -10, 0, 10)
closeMenuButton.Size = UDim2.new(0.05, 0, 0.1, 0)
closeMenuButton.Image = theme.LeaveMenuImage
closeMenuButton.Selectable = false
closeMenuButton.Activated:Connect(closeMenu)
local aspectConstraint = Instance.new("UIAspectRatioConstraint")
aspectConstraint.AspectType = Enum.AspectType.FitWithinMaxSize
aspectConstraint.DominantAxis = Enum.DominantAxis.Height
aspectConstraint.AspectRatio = 1
aspectConstraint.Parent = closeMenuButton
closeMenuButton.Parent = frame
return closeMenuButton
end
-- PUBLIC METHODS
local function listenToViewportChange(functionToFire)
if functionToFire == nil then return end
local viewportChangedConnection = nil
local function updateCamera()
local newCamera = workspace.CurrentCamera
if viewportChangedConnection then
viewportChangedConnection:Disconnect()
end
viewportChangedConnection = newCamera:GetPropertyChangedSignal("ViewportSize"):Connect(functionToFire)
functionToFire()
end
workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(updateCamera)
updateCamera()
end
function ContextMenuGui:CreateMenuFrame(theme)
local contextMenuHolder = self:CreateContextMenuHolder()
local menu = Instance.new("ImageButton")
menu.Name = "Menu"
menu.AnchorPoint = theme.AnchorPoint
menu.Size = theme.Size
menu.Position = theme.OnScreenPosition
menu.BackgroundTransparency = theme.BackgroundTransparency
menu.BackgroundColor3 = theme.BackgroundColor
menu.Image = theme.BackgroundImage
menu.ScaleType = theme.BackgroundImageScaleType
menu.SliceCenter = theme.BackgroundImageSliceCenter
menu.AutoButtonColor = false
menu.BorderSizePixel = 0
menu.Selectable = false
menu.Visible = false
menu.Active = true
menu.ClipsDescendants = true
menu.Modal = true
GuiService:AddSelectionParent("AvatarContextMenuGroup", menu)
local aspectConstraint = Instance.new("UIAspectRatioConstraint")
aspectConstraint.AspectType = Enum.AspectType.ScaleWithParentSize
aspectConstraint.DominantAxis = Enum.DominantAxis.Height
aspectConstraint.Name = "MenuAspectRatio"
aspectConstraint.AspectRatio = theme.AspectRatio
aspectConstraint.Parent = menu
local function updateAspectRatioForViewport()
local viewportSize = workspace.CurrentCamera.ViewportSize
if viewportSize.x < viewportSize.y then
aspectConstraint.DominantAxis = Enum.DominantAxis.Width
else
aspectConstraint.DominantAxis = Enum.DominantAxis.Height
end
end
listenToViewportChange(updateAspectRatioForViewport)
local sizeConstraint = Instance.new("UISizeConstraint")
sizeConstraint.Name = "MenuSizeConstraint"
sizeConstraint.MaxSize = theme.MaxSize
sizeConstraint.MinSize = theme.MinSize
sizeConstraint.Parent = menu
local contentFrame = Instance.new("Frame")
contentFrame.Name = "Content"
contentFrame.Size = UDim2.new(1,0,1,0)
contentFrame.BackgroundTransparency = 1
contentFrame.Parent = menu
local contentListLayout = Instance.new("UIListLayout")
contentListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
contentListLayout.VerticalAlignment = Enum.VerticalAlignment.Top
contentListLayout.SortOrder = Enum.SortOrder.LayoutOrder
contentListLayout.Parent = contentFrame
local contextActionList = Instance.new("ScrollingFrame")
contextActionList.Name = "ContextActionList"
contextActionList.AnchorPoint = Vector2.new(0.5,1)
contextActionList.BackgroundColor3 = theme.ButtonFrameColor
contextActionList.BackgroundTransparency = theme.ButtonFrameTransparency
contextActionList.BorderSizePixel = 0
contextActionList.LayoutOrder = 2
contextActionList.Size = UDim2.new(1,-12,0.54,0)
contextActionList.CanvasSize = UDim2.new(0,0,0,208)
contextActionList.ScrollBarThickness = 4
contextActionList.Selectable = false
contextActionList.Parent = contentFrame
local contextActionListUIListLayout = Instance.new("UIListLayout")
contextActionListUIListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
contextActionListUIListLayout.SortOrder = Enum.SortOrder.LayoutOrder
contextActionListUIListLayout.VerticalAlignment = Enum.VerticalAlignment.Top
contextActionListUIListLayout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function()
contextActionList.CanvasSize = UDim2.new(0,0,0,contextActionListUIListLayout.AbsoluteContentSize.Y)
end)
contextActionListUIListLayout.Parent = contextActionList
local nameTag = Instance.new("TextButton")
nameTag.Name = "NameTag"
nameTag.AnchorPoint = Vector2.new(0.5,1)
nameTag.BackgroundColor3 = theme.NameTagColor
nameTag.AutoButtonColor = false
nameTag.BorderSizePixel = 0
nameTag.LayoutOrder = 1
nameTag.Size = UDim2.new(1,-12,0.16,0)
nameTag.Font = theme.Font
nameTag.TextColor3 = theme.TextColor
nameTag.TextSize = 24 * theme.TextScale
nameTag.Text = ""
nameTag.TextXAlignment = Enum.TextXAlignment.Center
nameTag.TextYAlignment = Enum.TextYAlignment.Center
nameTag.Selectable = false
nameTag.Parent = contentFrame
local underline = Instance.new("Frame")
underline.Name = "Underline"
underline.BackgroundColor3 = theme.NameUnderlineColor
underline.AnchorPoint = Vector2.new(0,1)
underline.BorderSizePixel = 0
underline.Position = UDim2.new(0,0,1,0)
underline.Size = UDim2.new(1,0,0,2)
underline.Parent = nameTag
self:CreateLeaveMenuButton(menu, theme)
menu.Parent = contextMenuHolder
self.ContextMenuFrame = menu
return menu
end
function ContextMenuGui:UpdateGuiTheme(theme)
self.ContextMenuFrame.Size = theme.Size
self.ContextMenuFrame.AnchorPoint = theme.AnchorPoint
self.ContextMenuFrame.BackgroundTransparency = theme.BackgroundTransparency
self.ContextMenuFrame.BackgroundColor3 = theme.BackgroundColor
self.ContextMenuFrame.Image = theme.BackgroundImage
self.ContextMenuFrame.ScaleType = theme.BackgroundImageScaleType
self.ContextMenuFrame.SliceCenter = theme.BackgroundImageSliceCenter
self.ContextMenuFrame.CloseMenuButton.Image = theme.LeaveMenuImage
self.ContextMenuFrame.MenuSizeConstraint.MaxSize = theme.MaxSize
self.ContextMenuFrame.MenuSizeConstraint.MinSize = theme.MinSize
self.ContextMenuFrame.MenuAspectRatio.AspectRatio = theme.AspectRatio
local contentFrame = self.ContextMenuFrame.Content
contentFrame.ContextActionList.BackgroundColor3 = theme.ButtonFrameColor
contentFrame.ContextActionList.BackgroundTransparency = theme.ButtonFrameTransparency
contentFrame.NameTag.BackgroundColor3 = theme.NameTagColor
contentFrame.NameTag.Font = theme.Font
contentFrame.NameTag.TextColor3 = theme.TextColor
contentFrame.NameTag.TextSize = 24 * theme.TextScale
contentFrame.NameTag.Underline.BackgroundColor3 = theme.NameUnderlineColor
if PlayerCarousel then
PlayerCarousel:UpdateGuiTheme(theme)
end
end
function ContextMenuGui:BuildPlayerCarousel(playersByProximity, theme)
if not PlayerCarousel then
local playerCarouselModule = require(AvatarMenuModules.PlayerCarousel)
PlayerCarousel = playerCarouselModule.new(theme)
PlayerCarousel.rbxGui.Parent = self.ContextMenuFrame.Content
end
PlayerCarousel:ClearPlayerEntries()
if self.PlayerChangedConnection then
self.PlayerChangedConnection:Disconnect()
end
self.PlayerChangedConnection = PlayerCarousel.PlayerChanged:Connect(function(player)
if player then
self.ContextMenuFrame.Content.NameTag.Text = player.Name
else
self.ContextMenuFrame.Content.NameTag.Text = ""
end
PlayerChangedEvent:Fire(player)
end)
for i = 1, #playersByProximity do
PlayerCarousel:CreatePlayerEntry(playersByProximity[i][1], playersByProximity[i][2])
end
PlayerCarousel:FadeTowardsEdges()
PlayerCarousel:AddCarouselDivider()
end
function ContextMenuGui:RemovePlayerEntry(player)
if not PlayerCarousel then return end
PlayerCarousel:RemovePlayerEntry(player)
end
function ContextMenuGui:GetBottomScreenPaddingConstant()
return BOTTOM_SCREEN_PADDING_PERCENT
end
function ContextMenuGui:SetCloseMenuFunc(closeMenuFunc)
self.CloseMenuFunc = closeMenuFunc
end
function ContextMenuGui:SwitchToPlayerEntry(player, dontTween)
if not PlayerCarousel then return end
PlayerCarousel:SwitchToPlayerEntry(player, dontTween)
end
function ContextMenuGui:OffsetPlayerEntry(offset)
if not PlayerCarousel then return end
PlayerCarousel:OffsetPlayerEntry(offset)
end
function ContextMenuGui:GetSelectedPlayer()
if not PlayerCarousel then return nil end
return PlayerCarousel:GetSelectedPlayer()
end
function ContextMenuGui.new()
local obj = setmetatable({}, ContextMenuGui)
obj.CloseMenuFunc = nil
obj.ContextMenuFrame = nil
obj.LastSetPlayerIcon = nil
obj.PlayerChangedConnection = nil
obj.SelectedPlayerChanged = PlayerChangedEvent.Event
return obj
end
return ContextMenuGui.new()
@@ -0,0 +1,442 @@
--[[
// FileName: ContextMenuItems.lua
// Written by: TheGamer101
// Description: Module for creating the context menu items for the menu and doing the actions when they are clicked.
]]
--- SERVICES
local PlayersService = game:GetService("Players")
local CoreGuiService = game:GetService("CoreGui")
local StarterGui = game:GetService("StarterGui")
local Chat = game:GetService("Chat")
local RunService = game:GetService("RunService")
local AnalyticsService = game:GetService("RbxAnalyticsService")
local GuiService = game:GetService("GuiService")
-- MODULES
local RobloxGui = CoreGuiService:WaitForChild("RobloxGui")
local CoreGuiModules = RobloxGui:WaitForChild("Modules")
local RobloxTranslator = require(RobloxGui.Modules.RobloxTranslator)
local GameTranslator = require(RobloxGui.Modules.GameTranslator)
local AvatarMenuModules = CoreGuiModules:WaitForChild("AvatarContextMenu")
local ContextMenuUtil = require(AvatarMenuModules:WaitForChild("ContextMenuUtil"))
local ThemeHandler = require(AvatarMenuModules.ThemeHandler)
local BlockingUtility = require(CoreGuiModules.BlockingUtility)
local PolicyService = require(RobloxGui.Modules.Common.PolicyService)
-- FLAGS
local FFlagInspectMenuSubjectToPolicy = require(CoreGuiModules.Flags.FFlagInspectMenuSubjectToPolicy)
-- VARIABLES
local LocalPlayer = PlayersService.LocalPlayer
while not LocalPlayer do
PlayersService.PlayerAdded:wait()
LocalPlayer = PlayersService.LocalPlayer
end
local EnabledContextMenuItems = {
[Enum.AvatarContextMenuOption.Chat] = true,
[Enum.AvatarContextMenuOption.Friend] = true,
[Enum.AvatarContextMenuOption.Emote] = true,
[Enum.AvatarContextMenuOption.InspectMenu] = true
}
local CustomContextMenuItems = {}
local CustomItemAddedOrder = 0
-- CONSTANTS
-- If Custom buttons exist these layout orders are offset by highest custom button layout order.
local FRIEND_LAYOUT_ORDER = 1
local CHAT_LAYOUT_ORDER = 3
local INSPECT_AND_BUY_LAYOUT_ORDER = 4
local WAVE_LAYOUT_ORDER = 5
local MENU_ITEM_SIZE_X = 0.96
local MENU_ITEM_SIZE_Y = 0
local MENU_ITEM_SIZE_Y_OFFSET = 52
local VIEW_KEY = "InGame.InspectMenu.Action.View"
local ContextMenuItems = {}
ContextMenuItems.__index = ContextMenuItems
-- PRIVATE METHODS
function ContextMenuItems:UpdateInspectMenuEnabled()
local enabled = GuiService:GetInspectMenuEnabled()
if FFlagInspectMenuSubjectToPolicy then
enabled = enabled and not PolicyService:IsSubjectToChinaPolicies()
end
if enabled ~= EnabledContextMenuItems[Enum.AvatarContextMenuOption.InspectMenu] then
EnabledContextMenuItems[Enum.AvatarContextMenuOption.InspectMenu] = enabled
end
end
function ContextMenuItems:ClearMenuItems()
local children = self.MenuItemFrame:GetChildren()
for i = 1, #children do
if children[i]:IsA("GuiObject") then
children[i]:Destroy()
end
end
end
function ContextMenuItems:AddCustomAvatarMenuItem(menuOption, bindableEvent)
CustomItemAddedOrder = CustomItemAddedOrder + 1
CustomContextMenuItems[menuOption] = {
event = bindableEvent,
layoutOrder = CustomItemAddedOrder,
}
end
function ContextMenuItems:RemoveCustomAvatarMenuItem(menuOption)
CustomContextMenuItems[menuOption] = nil
end
function ContextMenuItems:IsContextAvatarEnumItem(enumItem)
local enumItems = Enum.AvatarContextMenuOption:GetEnumItems()
for i = 1, #enumItems do
if enumItem == enumItems[i] then
return true
end
end
return false
end
function ContextMenuItems:EnableDefaultMenuItem(menuOption)
EnabledContextMenuItems[menuOption] = true
end
function ContextMenuItems:RemoveDefaultMenuItem(menuOption)
EnabledContextMenuItems[menuOption] = false
end
function ContextMenuItems:RegisterCoreMethods()
local function addMenuItemFunc(args) --[[ menuOption, bindableEvent]]
if type(args) == "table" then
local name = ""
if args[1] and type(args[1]) == "string" then
name = args[1]
else
error("AddAvatarContextMenuOption first argument must be a table or Enum.AvatarContextMenuOption")
end
if args[2] and typeof(args[2]) == "Instance" and args[2].ClassName == "BindableEvent" then
self:AddCustomAvatarMenuItem(name, args[2])
else
error("AddAvatarContextMenuOption second table entry must be a BindableEvent")
end
elseif typeof(args) == "EnumItem" then
if self:IsContextAvatarEnumItem(args) then
self:EnableDefaultMenuItem(args)
else
error("AddAvatarContextMenuOption given EnumItem is not valid")
end
else
error("AddAvatarContextMenuOption first argument must be a table or Enum.AvatarContextMenuOption")
end
end
StarterGui:RegisterSetCore("AddAvatarContextMenuOption", addMenuItemFunc)
local function removeMenuItemFunc(menuOption)
if type(menuOption) == "string" then
self:RemoveCustomAvatarMenuItem(menuOption)
elseif typeof(menuOption) == "EnumItem" then
if self:IsContextAvatarEnumItem(menuOption) then
self:RemoveDefaultMenuItem(menuOption)
else
error("RemoveAvatarContextMenuOption given EnumItem is not valid")
end
else
error("RemoveAvatarContextMenuOption first argument must be a string or Enum.AvatarContextMenuOption")
end
end
StarterGui:RegisterSetCore("RemoveAvatarContextMenuOption", removeMenuItemFunc)
end
function ContextMenuItems:CreateCustomMenuItems()
for buttonText, itemInfo in pairs(CustomContextMenuItems) do
AnalyticsService:TrackEvent("Game", "AvatarContextMenuCustomButton", "name: " .. tostring(buttonText))
local function customButtonFunc()
if self.CloseMenuFunc then self:CloseMenuFunc() end
itemInfo.event:Fire(self.SelectedPlayer)
end
buttonText = GameTranslator:TranslateGameText(self.MenuItemFrame, buttonText)
local customButton = ContextMenuUtil:MakeStyledButton(
"CustomButton",
buttonText,
UDim2.new(MENU_ITEM_SIZE_X, 0, MENU_ITEM_SIZE_Y, MENU_ITEM_SIZE_Y_OFFSET),
customButtonFunc,
ThemeHandler:GetTheme()
)
customButton.Name = "CustomButton"
customButton.LayoutOrder = itemInfo.layoutOrder
customButton.Parent = self.MenuItemFrame
end
end
-- PUBLIC METHODS
local addFriendString = "Add Friend"
local friendsString = "Friends"
local friendRequestPendingString = "Friend Request Pending"
local acceptFriendRequestString = "Accept Friend Request"
local blockedString = "Player Blocked"
local addFriendDisabledTransparency = 0.75
local friendStatusChangedConn = nil
function ContextMenuItems:CreateFriendButton(status, isBlocked)
addFriendString = RobloxTranslator:FormatByKey("Corescripts.AvatarContextMenu.AddFriend")
friendsString = RobloxTranslator:FormatByKey("Corescripts.AvatarContextMenu.Friends")
friendRequestPendingString = RobloxTranslator:FormatByKey("Corescripts.AvatarContextMenu.FriendRequestPending")
acceptFriendRequestString = RobloxTranslator:FormatByKey("Corescripts.AvatarContextMenu.AcceptFriendRequest")
blockedString = RobloxTranslator:FormatByKey("Corescripts.AvatarContextMenu.PlayerBlocked")
local friendLabel = self.MenuItemFrame:FindFirstChild("FriendStatus")
if friendLabel then
friendLabel:Destroy()
friendLabel = nil
end
if friendStatusChangedConn then
friendStatusChangedConn:disconnect()
end
local friendLabelText = nil
local addFriendFunc = function()
if friendLabelText and friendLabel.Selectable then
friendLabel.Selectable = false
friendLabelText.TextTransparency = addFriendDisabledTransparency
friendLabelText.Text = friendRequestPendingString
AnalyticsService:ReportCounter("AvatarContextMenu-RequestFriendship")
AnalyticsService:TrackEvent("Game", "RequestFriendship", "AvatarContextMenu")
LocalPlayer:RequestFriendship(self.SelectedPlayer)
end
end
friendLabel, friendLabelText = ContextMenuUtil:MakeStyledButton(
"FriendStatus",
addFriendString,
UDim2.new(MENU_ITEM_SIZE_X, 0, MENU_ITEM_SIZE_Y, MENU_ITEM_SIZE_Y_OFFSET),
addFriendFunc,
ThemeHandler:GetTheme()
)
if isBlocked then
friendLabel.Selectable = false
friendLabelText.TextTransparency = addFriendDisabledTransparency
friendLabelText.Text = blockedString
elseif status == Enum.FriendStatus.Friend then
friendLabel.Selectable = false
friendLabelText.TextTransparency = addFriendDisabledTransparency
friendLabelText.Text = friendsString
elseif status == Enum.FriendStatus.FriendRequestSent then
friendLabel.Selectable = false
friendLabelText.TextTransparency = addFriendDisabledTransparency
friendLabelText.Text = friendRequestPendingString
elseif status == Enum.FriendStatus.FriendRequestReceived then
friendLabelText.Text = acceptFriendRequestString
else
friendLabel.Selectable = true
friendLabelText.TextTransparency = 0
end
friendLabel.LayoutOrder = FRIEND_LAYOUT_ORDER + CustomItemAddedOrder
friendLabel.Parent = self.MenuItemFrame
end
function ContextMenuItems:UpdateFriendButton(status, isBlocked)
local friendLabel = self.MenuItemFrame:FindFirstChild("FriendStatus")
if friendLabel then
self:CreateFriendButton(status, isBlocked)
end
end
function ContextMenuItems:CreateInspectAndBuyButton()
local function browseItems()
if self.CloseMenuFunc then self:CloseMenuFunc() end
-- If the developer disables the menu while someone is already looking at the ACM
-- the button doesn't disappear, so we need to check again.
if not EnabledContextMenuItems[Enum.AvatarContextMenuOption.InspectMenu] then
warn("The Inspect Menu is not currently available.")
return
end
GuiService:InspectPlayerFromUserIdWithCtx(self.SelectedPlayer.UserId, "avatarContextMenu")
end
local browseItemsButton = ContextMenuUtil:MakeStyledButton(
"View",
RobloxTranslator:FormatByKey(VIEW_KEY),
UDim2.new(MENU_ITEM_SIZE_X, 0, MENU_ITEM_SIZE_Y, MENU_ITEM_SIZE_Y_OFFSET),
browseItems,
ThemeHandler:GetTheme()
)
browseItemsButton.LayoutOrder = INSPECT_AND_BUY_LAYOUT_ORDER + CustomItemAddedOrder
browseItemsButton.Parent = self.MenuItemFrame
end
function ContextMenuItems:CreateEmoteButton()
local function wave()
if self.CloseMenuFunc then self:CloseMenuFunc() end
AnalyticsService:ReportCounter("AvatarContextMenu-Wave")
AnalyticsService:TrackEvent("Game", "AvatarContextMenuWave", "placeId: " .. tostring(game.PlaceId))
PlayersService:Chat("/e wave")
end
local waveString = RobloxTranslator:FormatByKey("Corescripts.AvatarContextMenu.Wave")
local waveButton = ContextMenuUtil:MakeStyledButton(
"Wave",
waveString,
UDim2.new(MENU_ITEM_SIZE_X, 0, MENU_ITEM_SIZE_Y, MENU_ITEM_SIZE_Y_OFFSET),
wave,
ThemeHandler:GetTheme()
)
waveButton.LayoutOrder = WAVE_LAYOUT_ORDER + CustomItemAddedOrder
waveButton.Parent = self.MenuItemFrame
end
function ContextMenuItems:CreateChatButton()
local chatDisabled = false
local function chatFunc()
if chatDisabled then
return
end
if self.CloseMenuFunc then self:CloseMenuFunc() end
AnalyticsService:ReportCounter("AvatarContextMenu-Chat")
AnalyticsService:TrackEvent("Game", "AvatarContextMenuChat", "placeId: " .. tostring(game.PlaceId))
local ChatModule = require(RobloxGui.Modules.ChatSelector)
ChatModule:SetVisible(true)
local eventDidFire = ChatModule:EnterWhisperState(self.SelectedPlayer)
if not eventDidFire then
-- Fallback to the old version for backwards compatibility with old chat versions
local ChatBar = nil
pcall(function() ChatBar = LocalPlayer.PlayerGui.Chat.Frame.ChatBarParentFrame.Frame.BoxFrame.Frame.ChatBar end)
if ChatBar then
ChatBar.Text = "/w " .. self.SelectedPlayer.Name
end
ChatModule:FocusChatBar()
end
end
local chatString = RobloxTranslator:FormatByKey("Corescripts.AvatarContextMenu.Chat")
local chatButton, chatLabelText = ContextMenuUtil:MakeStyledButton(
"ChatStatus",
chatString,
UDim2.new(MENU_ITEM_SIZE_X, 0, MENU_ITEM_SIZE_Y, MENU_ITEM_SIZE_Y_OFFSET),
chatFunc,
ThemeHandler:GetTheme()
)
chatButton.LayoutOrder = CHAT_LAYOUT_ORDER + CustomItemAddedOrder
local success, canLocalUserChat = pcall(function() return Chat:CanUserChatAsync(LocalPlayer.UserId) end)
local canChat = success and (RunService:IsStudio() or canLocalUserChat)
if canChat then
chatButton.Parent = self.MenuItemFrame
local canChatWith = ContextMenuUtil:GetCanChatWith(self.SelectedPlayer)
if not canChatWith then
chatDisabled = true
chatButton.Selectable = false
chatLabelText.TextTransparency = addFriendDisabledTransparency
chatLabelText.Text = RobloxTranslator:FormatByKey("Corescripts.AvatarContextMenu.ChatDisabled")
end
else
chatButton.Parent = nil
end
end
function ContextMenuItems:RemoveLastButtonUnderline()
local buttons = self.MenuItemFrame:GetChildren()
local lastButton = nil
local highestLayoutOrder = -1
for _, button in pairs(buttons) do
if button:IsA("GuiObject") and button.LayoutOrder > highestLayoutOrder then
highestLayoutOrder = button.LayoutOrder
lastButton = button
end
end
if lastButton then
local underline = lastButton:FindFirstChild("Underline")
if underline then
underline:Destroy()
end
end
end
function ContextMenuItems:BuildContextMenuItems(player)
if not player then return end
local friendStatus = ContextMenuUtil:GetFriendStatus(player)
local isBlocked = BlockingUtility:IsPlayerBlockedByUserId(player.UserId)
self:ClearMenuItems()
self:SetSelectedPlayer(player)
if EnabledContextMenuItems[Enum.AvatarContextMenuOption.Friend] then
self:CreateFriendButton(friendStatus, isBlocked)
end
if EnabledContextMenuItems[Enum.AvatarContextMenuOption.Chat] then
self:CreateChatButton()
end
if EnabledContextMenuItems[Enum.AvatarContextMenuOption.Emote] then
self:CreateEmoteButton()
end
if EnabledContextMenuItems[Enum.AvatarContextMenuOption.InspectMenu] then
self:CreateInspectAndBuyButton()
end
self:CreateCustomMenuItems()
self:RemoveLastButtonUnderline()
end
function ContextMenuItems:SetSelectedPlayer(selectedPlayer)
self.SelectedPlayer = selectedPlayer
end
function ContextMenuItems:SetCloseMenuFunc(closeMenuFunc)
self.CloseMenuFunc = closeMenuFunc
end
function ContextMenuItems.new(menuItemFrame)
local obj = setmetatable({}, ContextMenuItems)
obj.MenuItemFrame = menuItemFrame
obj.SelectedPlayer = nil
obj:RegisterCoreMethods()
-- If disabled in a script, sometimes it registers before we can receive the signal.
ContextMenuItems:UpdateInspectMenuEnabled()
return obj
end
GuiService.InspectMenuEnabledChangedSignal:Connect(function(enabled)
if FFlagInspectMenuSubjectToPolicy then
enabled = enabled and PolicyService:IsSubjectToChinaPolicies()
end
if not enabled then
ContextMenuItems:RemoveDefaultMenuItem(Enum.AvatarContextMenuOption.InspectMenu)
else
ContextMenuItems:EnableDefaultMenuItem(Enum.AvatarContextMenuOption.InspectMenu)
end
end)
if FFlagInspectMenuSubjectToPolicy then
spawn(function()
-- Check whether InspectMenu is disabled by policy after PolicyService is finished initializing
PolicyService:InitAsync()
ContextMenuItems:UpdateInspectMenuEnabled()
end)
end
return ContextMenuItems
@@ -0,0 +1,311 @@
--[[
// FileName: ContextMenuUtil.lua
// Written by: TheGamer101
// Description: Module for utility funcitons of the avatar context menu.
]]
--[[
// FileName: ContextMenuGui.lua
// Written by: TheGamer101
// Description: Module for creating the context GUI.
]]
--- CONSTANTS
local STOP_MOVEMENT_ACTION_NAME = "AvatarContextMenuStopInput"
-- todo: remove with GetFFlagUseThumbnailUrl
local MAX_THUMBNAIL_RETRIES = 4
--- SERVICES
local CoreGuiService = game:GetService("CoreGui")
local PlayersService = game:GetService("Players")
local ContextActionService = game:GetService("ContextActionService")
local GuiService = game:GetService("GuiService")
local UserInputService = game:GetService("UserInputService")
local RobloxReplicatedStorage = game:GetService("RobloxReplicatedStorage")
--- VARIABLES
local RobloxGui = CoreGuiService:WaitForChild("RobloxGui")
local CoreGuiModules = RobloxGui:WaitForChild("Modules")
local BlockingUtility = require(CoreGuiModules.BlockingUtility)
local LocalPlayer = PlayersService.LocalPlayer
while not LocalPlayer do
PlayersService.PlayerAdded:wait()
LocalPlayer = PlayersService.LocalPlayer
end
-- FLAGS
local GetFFlagUseThumbnailUrl = require(CoreGuiModules.Common.Flags.GetFFlagUseThumbnailUrl)
local ContextMenuUtil = {}
ContextMenuUtil.__index = ContextMenuUtil
-- PUBLIC METHODS
function ContextMenuUtil:GetHeadshotForPlayer(player)
if GetFFlagUseThumbnailUrl() then
return "rbxthumb://type=AvatarHeadShot&id=" .. player.UserId .. "&w=150&h=150"
else
if self.HeadShotUrlCache[player] ~= nil and self.HeadShotUrlCache[player] ~= "" then
return self.HeadShotUrlCache[player]
end
if self.HeadShotUrlCache[player] == nil then
-- Mark that we are getting a headshot for this player.
self.HeadShotUrlCache[player] = ""
end
local startTime = tick()
local headshotUrl, isFinal = PlayersService:GetUserThumbnailAsync(player.UserId, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size180x180)
if not isFinal then
for i = 0, MAX_THUMBNAIL_RETRIES do
headshotUrl, isFinal = PlayersService:GetUserThumbnailAsync(player.UserId, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size180x180)
if isFinal then
break
end
wait(i ^ 2)
end
end
self.HeadShotUrlCache[player] = headshotUrl
return headshotUrl
end
end
function ContextMenuUtil:HasOrGettingHeadShot(player)
return self.HeadShotUrlCache[player] ~= nil
end
function ContextMenuUtil:FindPlayerFromPart(part)
if part and part.Parent then
local possibleCharacter = part
while possibleCharacter and not possibleCharacter:IsA("Model") do
possibleCharacter = possibleCharacter.Parent
end
if possibleCharacter then
return PlayersService:GetPlayerFromCharacter(possibleCharacter)
end
end
return nil
end
function ContextMenuUtil:GetPlayerPosition(player)
if player.Character then
local hrp = player.Character:FindFirstChild("HumanoidRootPart")
if hrp then
return hrp.Position
end
end
return nil
end
local playerMovementEnabled = true
function ContextMenuUtil:DisablePlayerMovement()
if not playerMovementEnabled then return end
playerMovementEnabled = false
local noOpFunc = function(actionName, actionState)
if actionState == Enum.UserInputState.End then
return Enum.ContextActionResult.Pass
end
return Enum.ContextActionResult.Sink
end
ContextActionService:BindCoreAction(STOP_MOVEMENT_ACTION_NAME, noOpFunc, false,
Enum.PlayerActions.CharacterForward,
Enum.PlayerActions.CharacterBackward,
Enum.PlayerActions.CharacterLeft,
Enum.PlayerActions.CharacterRight,
Enum.PlayerActions.CharacterJump,
Enum.UserInputType.Gamepad1, Enum.UserInputType.Gamepad2, Enum.UserInputType.Gamepad3, Enum.UserInputType.Gamepad4
)
end
function ContextMenuUtil:EnablePlayerMovement()
if playerMovementEnabled then return end
playerMovementEnabled = true
ContextActionService:UnbindCoreAction(STOP_MOVEMENT_ACTION_NAME)
end
function ContextMenuUtil:GetFriendStatus(player)
local success, result = pcall(function()
-- NOTE: Core script only
return LocalPlayer:GetFriendStatus(player)
end)
if success then
return result
else
return Enum.FriendStatus.NotFriend
end
end
local CanChatWithMap = {}
coroutine.wrap(function()
local RemoteEvent_CanChatWith = RobloxReplicatedStorage:WaitForChild("CanChatWith", math.huge)
RemoteEvent_CanChatWith.OnClientEvent:Connect(function(userId, canChat)
CanChatWithMap[userId] = canChat
end)
end)()
function ContextMenuUtil:GetCanChatWith(otherPlayer)
if BlockingUtility:IsPlayerBlockedByUserId(otherPlayer.UserId) then
-- This can be removed when Chat:CanUsersChatAsync() correctly respects blocked status.
return false
end
if CanChatWithMap[otherPlayer.UserId] ~= nil then
return CanChatWithMap[otherPlayer.UserId]
end
-- Assume we can chat if we have not received information from the server yet.
return true
end
local SelectionOverrideObject = Instance.new("ImageLabel")
SelectionOverrideObject.Image = ""
SelectionOverrideObject.BackgroundTransparency = 1
local function MakeDefaultButton(name, size, clickFunc, theme)
local button = Instance.new("ImageButton")
button.Name = name
button.Image = theme.ButtonImage
button.ScaleType = theme.ButtonImageScaleType
button.SliceCenter = theme.ButtonImageSliceCenter
button.BackgroundColor3 = theme.ButtonColor
button.BackgroundTransparency = theme.ButtonTransparency
button.AutoButtonColor = false
button.Size = size
button.ZIndex = 2
button.SelectionImageObject = SelectionOverrideObject
button.BorderSizePixel = 0
local underline = Instance.new("Frame")
underline.Name = "Underline"
underline.BackgroundColor3 = theme.ButtonUnderlineColor
underline.AnchorPoint = Vector2.new(0.5,1)
underline.BorderSizePixel = 0
underline.Position = UDim2.new(0.5,0,1,0)
underline.Size = UDim2.new(0.95,0,0,1)
underline.Parent = button
if clickFunc then
button.MouseButton1Click:Connect(function()
clickFunc(UserInputService:GetLastInputType())
end)
end
local function isPointerInput(inputObject)
return inputObject.UserInputType == Enum.UserInputType.MouseMovement or inputObject.UserInputType == Enum.UserInputType.Touch
end
local function selectButton()
button.BackgroundColor3 = theme.ButtonHoverColor
button.BackgroundTransparency = theme.ButtonHoverTransparency
end
local function deselectButton()
button.BackgroundColor3 = theme.ButtonColor
button.BackgroundTransparency = theme.ButtonTransparency
end
button.InputBegan:Connect(function(inputObject)
if button.Selectable and isPointerInput(inputObject) then
selectButton()
inputObject:GetPropertyChangedSignal("UserInputState"):connect(function()
if inputObject.UserInputState == Enum.UserInputState.End then
deselectButton()
end
end)
end
end)
button.InputEnded:Connect(function(inputObject)
if button.Selectable and GuiService.SelectedCoreObject ~= button and isPointerInput(inputObject) then
deselectButton()
end
end)
button.SelectionGained:Connect(function()
selectButton()
end)
button.SelectionLost:Connect(function()
deselectButton()
end)
local guiServiceCon = GuiService.Changed:Connect(function(prop)
if prop ~= "SelectedCoreObject" then return end
if GuiService.SelectedCoreObject == nil or GuiService.SelectedCoreObject ~= button then
deselectButton()
return
end
if button.Selectable then
selectButton()
end
end)
return button
end
local function getViewportSize()
while not workspace.CurrentCamera do
workspace.Changed:wait()
end
-- ViewportSize is initally set to 1, 1 in Camera.cpp constructor.
-- Also check against 0, 0 incase this is changed in the future.
while workspace.CurrentCamera.ViewportSize == Vector2.new(0,0) or
workspace.CurrentCamera.ViewportSize == Vector2.new(1,1) do
workspace.CurrentCamera.Changed:wait()
end
return workspace.CurrentCamera.ViewportSize
end
local function isSmallTouchScreen()
local viewportSize = getViewportSize()
return UserInputService.TouchEnabled and (viewportSize.Y < 500 or viewportSize.X < 700)
end
function ContextMenuUtil:MakeStyledButton(name, text, size, clickFunc, theme)
local button = MakeDefaultButton(name, size, clickFunc, theme)
local textLabel = Instance.new("TextLabel")
textLabel.Name = name .. "TextLabel"
textLabel.BackgroundTransparency = 1
textLabel.BorderSizePixel = 0
textLabel.Size = UDim2.new(1, 0, 1, -8)
textLabel.Position = UDim2.new(0,0,0,0)
textLabel.TextColor3 = Color3.fromRGB(255,255,255)
textLabel.TextYAlignment = Enum.TextYAlignment.Center
textLabel.Font = theme.Font
textLabel.TextSize = 24 * theme.TextScale
if isSmallTouchScreen() then
textLabel.TextSize = 18 * theme.TextScale
elseif GuiService:IsTenFootInterface() then
textLabel.TextSize = 36 * theme.TextScale
end
textLabel.Text = text
textLabel.TextScaled = true
textLabel.TextWrapped = true
textLabel.ZIndex = 2
textLabel.Parent = button
local constraint = Instance.new("UITextSizeConstraint", textLabel)
constraint.MaxTextSize = textLabel.TextSize
return button, textLabel
end
function ContextMenuUtil.new()
local obj = setmetatable({}, ContextMenuUtil)
-- todo: remove with GetFFlagUseThumbnailUrl
obj.HeadShotUrlCache = {}
return obj
end
return ContextMenuUtil.new()
@@ -0,0 +1,361 @@
--[[
// FileName: PlayerCarousel.lua
// Written by: darthskrill
// Description: Module for building the UI for the player selection carousel
]]
local PlayerCarousel = {}
PlayerCarousel.__index = PlayerCarousel
-- SERVICES
local GuiService = game:GetService("GuiService")
local CoreGuiService = game:GetService("CoreGui")
local TweenService = game:GetService("TweenService")
local UserInputService = game:GetService("UserInputService")
-- CONSTANTS
local BACKGROUND_SELECTED_COLOR = Color3.fromRGB(0,162,255)
local BACKGROUND_DEFAULT_COLOR = Color3.fromRGB(0,0,0)
local PAGE_LAYOUT_TWEEN_TIME = 0.25
local CAROUSEL_DIVIDER_NAME = "_CarouselDivider"
-- VARIABLES
local RobloxGui = CoreGuiService:WaitForChild("RobloxGui")
local CoreGuiModules = RobloxGui:WaitForChild("Modules")
local AvatarMenuModules = CoreGuiModules:WaitForChild("AvatarContextMenu")
local selectedPlayer = nil
local uiPageLayout = nil
local playerChangedEvent = nil
local buttonToPlayerMap = {}
local playerToButtonMap = {}
local function getSortedCarouselButtons()
local buttonChildIndexs = {}
local carouselButtons = {}
for childIndex, child in ipairs(selectedPlayer:GetChildren()) do
if child:IsA("GuiObject") and child.Name ~= CAROUSEL_DIVIDER_NAME then
table.insert(carouselButtons, child)
buttonChildIndexs[child] = childIndex
end
end
table.sort(carouselButtons, function(a, b)
if a.LayoutOrder == b.LayoutOrder then
return buttonChildIndexs[a] < buttonChildIndexs[b]
end
return a.LayoutOrder < b.LayoutOrder
end)
return carouselButtons
end
local function CreateMenuCarousel(theme)
local playerSelection = Instance.new("Frame")
playerSelection.Name = "PlayerCarousel"
playerSelection.AnchorPoint = Vector2.new(0.5, 0.5)
playerSelection.BackgroundTransparency = 1
playerSelection.Position = UDim2.new(0.5,0,0.5,0)
playerSelection.Size = UDim2.new(1, 0, 0.28, 0)
playerSelection.ClipsDescendants = true
local innerFrame = Instance.new("Frame")
innerFrame.Name = "InnerFrame"
innerFrame.AnchorPoint = Vector2.new(0.5, 0.5)
innerFrame.BackgroundTransparency = 1
innerFrame.Position = UDim2.new(0.5, 0, 0.5, 0)
innerFrame.Size = UDim2.new(0.8, 0, 1, 0)
innerFrame.ClipsDescendants = true
innerFrame.Active = true
innerFrame.Parent = playerSelection
selectedPlayer = Instance.new("Frame")
selectedPlayer.Name = "SelectedPlayer"
selectedPlayer.AnchorPoint = Vector2.new(0.5, 0.5)
selectedPlayer.BackgroundTransparency = 1
selectedPlayer.Position = UDim2.new(0.5, 0, 0.5, 0)
selectedPlayer.Size = UDim2.new(0, 100, 1, -10)
selectedPlayer.Parent = innerFrame
uiPageLayout = Instance.new("UIPageLayout")
uiPageLayout.EasingDirection = Enum.EasingDirection.Out
uiPageLayout.EasingStyle = Enum.EasingStyle.Quad
uiPageLayout.Padding = UDim.new(0, 5)
uiPageLayout.TweenTime = PAGE_LAYOUT_TWEEN_TIME
uiPageLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
uiPageLayout.VerticalAlignment = Enum.VerticalAlignment.Center
uiPageLayout.TouchInputEnabled = false
uiPageLayout.Circular = true
uiPageLayout.SortOrder = Enum.SortOrder.LayoutOrder
local aspectRatioConstraint = Instance.new("UIAspectRatioConstraint")
aspectRatioConstraint.DominantAxis = Enum.DominantAxis.Height
aspectRatioConstraint.Parent = selectedPlayer
playerChangedEvent = Instance.new("BindableEvent")
playerChangedEvent.Name = "PlayerChanged"
local lastPage = nil
uiPageLayout:GetPropertyChangedSignal("CurrentPage"):Connect(function()
if uiPageLayout.CurrentPage then
if uiPageLayout.CurrentPage.Name == CAROUSEL_DIVIDER_NAME then
local carouselButtons = getSortedCarouselButtons()
if lastPage == carouselButtons[1] then
uiPageLayout:Previous()
else
uiPageLayout:Next()
end
else
uiPageLayout.CurrentPage.BackgroundColor3 = BACKGROUND_DEFAULT_COLOR
lastPage = uiPageLayout.CurrentPage
end
end
if GuiService.SelectedCoreObject and GuiService.SelectedCoreObject.Parent == uiPageLayout.Parent then
GuiService.SelectedCoreObject.BackgroundColor3 = BACKGROUND_SELECTED_COLOR
end
GuiService.SelectedCoreObject = uiPageLayout.CurrentPage
if uiPageLayout.CurrentPage then playerChangedEvent:Fire(buttonToPlayerMap[uiPageLayout.CurrentPage]) end
end)
uiPageLayout.Parent = selectedPlayer
local nextButton = Instance.new("ImageButton")
nextButton.Name = "NextButton"
nextButton.Image = theme.ScrollRightImage
nextButton.BackgroundTransparency = 1
nextButton.AnchorPoint = Vector2.new(1,0.5)
nextButton.Position = UDim2.new(1,-5,0.5,0)
nextButton.Size = UDim2.new(0.3,0,0.3,0)
nextButton.Selectable = false
nextButton.Parent = playerSelection
local nextButtonAspectRatio = Instance.new("UIAspectRatioConstraint")
nextButtonAspectRatio.DominantAxis = Enum.DominantAxis.Width
nextButtonAspectRatio.Parent = nextButton
local prevButton = nextButton:Clone()
prevButton.Name = "PrevButton"
if theme.ScrollLeftImage ~= "" then
prevButton.Image = theme.ScrollLeftImage
else
prevButton.Rotation = 180
end
prevButton.AnchorPoint = Vector2.new(0, 0.5)
prevButton.Position = UDim2.new(0, 5, 0.5, 0)
prevButton.Selectable = false
prevButton.Parent = playerSelection
local function moveChangePage(goToNext)
if goToNext then
uiPageLayout:Next()
else
uiPageLayout:Previous()
end
end
nextButton.MouseButton1Click:Connect(function() moveChangePage(true) end)
prevButton.MouseButton1Click:Connect(function() moveChangePage(false) end)
local defaultChildrenSize = 3 -- aspectRatioConstraint, PageLayout, first button in pagelayout
local function checkButtonVisibility()
local lastInputIsTouch = UserInputService:GetLastInputType() == Enum.UserInputType.Touch
prevButton.Visible = not lastInputIsTouch and (#selectedPlayer:GetChildren() > defaultChildrenSize)
nextButton.Visible = not lastInputIsTouch and (#selectedPlayer:GetChildren() > defaultChildrenSize)
end
checkButtonVisibility()
UserInputService.LastInputTypeChanged:Connect(checkButtonVisibility)
return playerSelection
end
function PlayerCarousel:UpdateGuiTheme(theme)
self.rbxGui.NextButton.Image = theme.ScrollRightImage
if theme.ScrollLeftImage ~= "" then
self.rbxGui.PrevButton.Image = theme.ScrollLeftImage
else
self.rbxGui.PrevButton.Image = theme.ScrollRightImage
self.rbxGui.PrevButton.Rotation = 180
end
end
function PlayerCarousel:FadeTowardsEdges()
if not uiPageLayout.CurrentPage then
return
end
local carouselButtons = getSortedCarouselButtons()
local currentPageIndex = 0
for index, button in ipairs(carouselButtons) do
if button == uiPageLayout.CurrentPage then
currentPageIndex = index
break
end
end
for index, button in ipairs(carouselButtons) do
local distanceToLeft = (currentPageIndex - index) % #carouselButtons
local distanceToRight = (index - currentPageIndex) % #carouselButtons
local distance = math.min(distanceToLeft, distanceToRight)
if distance >= 2 then
button.ImageTransparency = 0.7
elseif distance == 1 then
button.ImageTransparency = 0.4
else
button.ImageTransparency = 0
end
end
end
function PlayerCarousel:AddCarouselDivider()
local carouselDivider = selectedPlayer:FindFirstChild(CAROUSEL_DIVIDER_NAME)
if carouselDivider then
carouselDivider:Destroy()
end
local buttonCount = #selectedPlayer:GetChildren() - 1
if buttonCount < 5 then
return
end
carouselDivider = Instance.new("Frame")
carouselDivider.Name = CAROUSEL_DIVIDER_NAME
carouselDivider.Size = UDim2.new(1, 0, 1, 0)
carouselDivider.BackgroundTransparency = 1
carouselDivider.Parent = selectedPlayer
carouselDivider.LayoutOrder = 1000000
local line = Instance.new("Frame")
line.Name = "line"
line.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
line.BorderSizePixel = 0
line.Position = UDim2.new(0.25, 0, 0, -3)
line.Size = UDim2.new(0, 2, 1, 6)
line.Parent = carouselDivider
local line2 = line:Clone()
line2.Position = UDim2.new(0.5, 0, 0, -3)
line2.Parent = carouselDivider
local line3 = line:Clone()
line3.Position = UDim2.new(0.75, 0, 0, -3)
line3.Parent = carouselDivider
end
function PlayerCarousel:RemovePlayerEntry(player)
local button = playerToButtonMap[player]
if button then
playerToButtonMap[player] = nil
buttonToPlayerMap[button] = nil
button:Destroy()
if uiPageLayout then
uiPageLayout:ApplyLayout()
end
self:FadeTowardsEdges()
end
end
function PlayerCarousel:ClearPlayerEntries()
for button in pairs(buttonToPlayerMap) do
button:Destroy()
end
buttonToPlayerMap = {}
playerToButtonMap = {}
end
function PlayerCarousel:CreatePlayerEntry(player, distanceToLocalPlayer)
local playerButton = playerToButtonMap[player]
if playerButton then
playerButton.LayoutOrder = distanceToLocalPlayer
return
end
local button = Instance.new("ImageButton")
button.Name = player.Name
button.BorderSizePixel = 0
button.LayoutOrder = distanceToLocalPlayer
button.BackgroundColor3 = Color3.fromRGB(0,0,0)
button.BackgroundTransparency = 0
button.Size = UDim2.new(1, 0, 1, 0)
button.SelectionLost:connect(function() button.BackgroundColor3 = BACKGROUND_DEFAULT_COLOR end)
button.SelectionGained:connect(function() button.BackgroundColor3 = BACKGROUND_SELECTED_COLOR end)
local tweenStyle = TweenInfo.new(1, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, -1, true)
local buttonLoadingTween
buttonLoadingTween = TweenService:Create(
button,
tweenStyle,
{BackgroundColor3 = Color3.fromRGB(255,255,255)}
)
buttonLoadingTween:Play()
buttonToPlayerMap[button] = player
playerToButtonMap[player] = button
button.MouseButton1Click:Connect(function()
uiPageLayout:JumpTo(button)
end)
button.Parent = selectedPlayer
spawn(function()
local ContextMenuUtil = require(AvatarMenuModules:WaitForChild("ContextMenuUtil"))
button.Image = ContextMenuUtil:GetHeadshotForPlayer(player)
buttonLoadingTween:Cancel()
buttonLoadingTween = nil
if button == GuiService.SelectedCoreObject then
button.BackgroundColor3 = BACKGROUND_SELECTED_COLOR
else
button.BackgroundColor3 = BACKGROUND_DEFAULT_COLOR
end
end)
end
function PlayerCarousel:SwitchToPlayerEntry(player, dontTween)
if not player then return end
local button = playerToButtonMap[player]
if not button then
self:CreatePlayerEntry(player, 0)
button = playerToButtonMap[player]
end
if dontTween then
uiPageLayout.TweenTime = 0
end
uiPageLayout:JumpTo(button)
spawn(function()
uiPageLayout.TweenTime = PAGE_LAYOUT_TWEEN_TIME
end)
end
function PlayerCarousel:OffsetPlayerEntry(offset)
if offset == 0 then return end
if offset > 0 then
uiPageLayout:Next()
else
uiPageLayout:Previous()
end
end
function PlayerCarousel:GetSelectedPlayer()
return buttonToPlayerMap[uiPageLayout.CurrentPage]
end
function PlayerCarousel.new(theme)
local obj = setmetatable({}, PlayerCarousel)
obj.rbxGui = CreateMenuCarousel(theme)
obj.PlayerChanged = playerChangedEvent.Event
playerChangedEvent.Event:Connect(function()
obj:FadeTowardsEdges()
end)
return obj
end
return PlayerCarousel
@@ -0,0 +1,118 @@
--[[
// FileName: SelectedCharacterIndicator.lua
// Written by: TheGamer101
// Description: Module for rendering an effect for the selected character .
]]
local SelectedCharacterIndicator = {}
SelectedCharacterIndicator.__index = SelectedCharacterIndicator
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")
local TweenService = game:GetService("TweenService")
local InsertService = game:GetService("InsertService")
local RENDER_ARROW_CONTEXT_ACTION = "ContextActionMenuRenderArrow"
local CurrentCamera = Workspace.CurrentCamera
-- This isn't currently necessary but done as a percaution in case
-- we move the selected character indicator elsewhere later.
local function removeScripts(object)
for _, descendant in ipairs(object:GetDescendants()) do
if descendant:IsA("LuaSourceContainer") then
descendant:Destroy()
end
end
end
local function ApplyArrow(character, theme)
local baseModel = Instance.new("Model")
baseModel.Name = "ContextMenuArrow"
baseModel.Parent = CurrentCamera
local humanoid = character:FindFirstChildOfClass("Humanoid")
if humanoid == nil then
humanoid = character:WaitForChild("Humanoid", 15)
if humanoid == nil then
return
end
end
local torso = character:WaitForChild("HumanoidRootPart")
local arrowPart = theme.SelectedCharacterIndicator:Clone()
removeScripts(arrowPart)
arrowPart.Anchored = true
arrowPart.Transparency = 0
arrowPart.CanCollide = false
arrowPart.Parent = baseModel
local arrowTween = TweenService:Create(
arrowPart,
TweenInfo.new(4, Enum.EasingStyle.Linear, Enum.EasingDirection.Out, -1, false),
{Orientation = Vector3.new(0, 360, 180)}
)
arrowTween:Play()
local function update()
arrowPart.Position = torso.Position + Vector3.new(0, 5, 0)
end
local isKilled = false
local function kill()
if isKilled then
return
end
isKilled = true
baseModel:Destroy()
arrowTween:Destroy()
RunService:UnbindFromRenderStep(RENDER_ARROW_CONTEXT_ACTION)
end
humanoid.Died:Connect(kill)
character.AncestryChanged:Connect(kill)
RunService:BindToRenderStep(RENDER_ARROW_CONTEXT_ACTION, Enum.RenderPriority.Camera.Value + 1 , update)
baseModel.Parent = CurrentCamera
return kill
end
function SelectedCharacterIndicator:ChangeSelectedPlayer(selectedPlayer, theme)
coroutine.wrap(function()
if self.SelectedPlayer then
self.SelectedPlayer = nil
self.CharacterAddedConn:Disconnect()
self.CharacterAddedConn = nil
if self.KillOldRenderFunction then
self.KillOldRenderFunction()
self.KillOldRenderFunction = nil
end
end
if selectedPlayer then
self.SelectedPlayer = selectedPlayer
self.CharacterAddedConn = selectedPlayer.CharacterAdded:Connect(function(character)
if self.KillOldRenderFunction then
self.KillOldRenderFunction()
end
self.KillOldRenderFunction = ApplyArrow(character, theme)
end)
if selectedPlayer.Character then
self.KillOldRenderFunction = ApplyArrow(selectedPlayer.Character, theme)
end
end
end)()
end
function SelectedCharacterIndicator.new()
local obj = setmetatable({}, SelectedCharacterIndicator)
obj.KillOldRenderFunction = nil
obj.SelectedPlayer = nil
obj.CharacterAddedConn = nil
return obj
end
return SelectedCharacterIndicator.new()
@@ -0,0 +1,105 @@
local InsertService = game:GetService("InsertService")
local StarterGui = game:GetService("StarterGui")
-- Keep around any entries removed from this table to avoid breaking
-- AvatarContextMenuTheme backwards compatibility.
local DEFAULT_THEME = {
BackgroundImage = "rbxasset://textures/blackBkg_round.png",
BackgroundImageScaleType = Enum.ScaleType.Slice,
BackgroundImageSliceCenter = Rect.new(12, 12, 12, 12),
BackgroundImageTransparency = 0,
BackgroundTransparency = 1,
BackgroundColor = Color3.fromRGB(31, 31, 31),
NameTagColor = Color3.fromRGB(79, 79, 79),
NameUnderlineColor = Color3.fromRGB(255, 255, 255),
ButtonFrameColor = Color3.fromRGB(79, 79, 79),
ButtonFrameTransparency = 0,
ButtonImage = "",
ButtonImageScaleType = Enum.ScaleType.Slice,
ButtonImageSliceCenter = Rect.new(8, 6, 46, 44),
ButtonColor = Color3.fromRGB(79, 79, 79),
ButtonTransparency = 1,
ButtonHoverColor = Color3.fromRGB(163, 162, 165),
ButtonHoverTransparency = 0.5,
ButtonUnderlineColor = Color3.fromRGB(137, 137, 137),
Font = Enum.Font.SourceSansBold,
TextColor = Color3.fromRGB(255, 255, 255),
TextScale = 1,
LeaveMenuImage = "rbxasset://textures/loading/cancelButton.png",
ScrollRightImage = "rbxasset://textures/ui/AvatarContextMenu_Arrow.png",
ScrollLeftImage = "", --If no ScrollLeftImage is provided, the right image is rotated 180 degrees.
SelectedCharacterIndicator = InsertService:LoadLocalAsset(
"rbxasset://models/AvatarContextMenu/AvatarContextArrow.rbxm"
),
Size = UDim2.new(0.95, 0, 0.9, 0),
MinSize = Vector2.new(200, 200),
MaxSize = Vector2.new(300, 300),
AspectRatio = 1.15,
AnchorPoint = Vector2.new(0.5, 1),
OnScreenPosition = UDim2.new(0.5, 0, 0.98, 0),
OffScreenPosition = UDim2.new(0.5, 0, 1, 300),
}
local ThemeHandler = {}
ThemeHandler.__index = ThemeHandler
function ThemeHandler:UpdateTheme(newThemeData)
local newTheme = {}
for key, value in pairs(DEFAULT_THEME) do
if typeof(newThemeData[key]) == typeof(value) then
newTheme[key] = newThemeData[key]
elseif newThemeData[key] == nil then
newTheme[key] = value
else
error(string.format(
"AvatarContextMenuTheme wrong type for key %s: %s. Expected type %s",
key,
typeof(newThemeData[key]),
typeof(value)
), 2)
end
end
self.Theme = newTheme
end
function ThemeHandler:RegisterCoreMethods()
local function setMenuTheme(newTheme)
if type(newTheme) == "table" then
for key in pairs(newTheme) do
if DEFAULT_THEME[key] == nil then
error(string.format("AvatarContextMenuTheme got invalid key: %s", key))
end
end
self:UpdateTheme(newTheme)
else
error("AvatarContextMenuTheme argument must be a table")
end
end
StarterGui:RegisterSetCore("AvatarContextMenuTheme", setMenuTheme)
end
-- PUBLIC METHODS
function ThemeHandler:GetTheme()
return self.Theme
end
function ThemeHandler.new()
local obj = setmetatable({}, ThemeHandler)
obj.Theme = DEFAULT_THEME
obj:RegisterCoreMethods()
return obj
end
return ThemeHandler.new()