CoreScript 'MainBotChatScript2' Performs Unneeded Calls and Indexes With 0 Dialogs InGame

Reproduction Steps

  • Play a Roblox game with 0 Dialog instances.
  • Open Client Microprofiler and then pause to observe or dumb the frames to observe through the file.
  • Find “MainBotChatScript2” in Runservice.Heartbeat ‘section’ of the profiler dump.

Expected Behavior
MainBotChatScript2’s heartbeat function doesn’t run unneeded ‘calls and indexes’ to attempt a find on a closest dialog, if there are none in the game.

Actual Behavior
MainBotChatScript2’s heartbeat function runs unneeded ‘calls and indexes’ to attempt a find a closest dialog, when there are none in the game.

Microprofiler Dump - Default Baseplate, No Additions.

Log/dump files were attached to the internal ticket by a Roblox Staff (@Focia19), due to the sensitive information they may contain, will not be shared publicly!

3 Likes

We’ve filed a ticket to our internal database, and we’ll follow up when we have an update!

Thanks for the report!

1 Like

Any updates/ETA of a ‘patch’?

2 Likes

@Sctrx Is this issue still reproducible for you? Seems there has not been any updates regarding this report and our QA team is unable to reproduce the issue.

After multiple attempts, we’re unable to reproduce the reported bug. Any additional information would be valuable.

Still see it on a blank baseplate game.


image

1 Like


Yeah it’s just doing a bunch of useless stuff and wasting resources. (ScriptProfiler - Client). Game has 1 dialog object.

I’ve decided to fix some of it myself. (because why not) (character/hrp references, replaced some deprecated APIs/used some new ones, heartbeat runs at 8fps) Took like 10-15 minutes. Maybe that will help the staff, developers, or engineers out. @PadinFull89

--!nonstrict
--!nolint DeprecatedApi
local PURPOSE_DATA = {
	[Enum.DialogPurpose.Quest] = {
		"rbxasset://textures/ui/dialog_purpose_quest.png",
		Vector2.new(10, 34)
	},
	[Enum.DialogPurpose.Help] = {
		"rbxasset://textures/ui/dialog_purpose_help.png",
		Vector2.new(20, 35)
	},
	[Enum.DialogPurpose.Shop] = {
		"rbxasset://textures/ui/dialog_purpose_shop.png",
		Vector2.new(22, 43)
	},
}
local TEXT_HEIGHT = 24 -- Pixel height of one row
local FONT_SIZE = Enum.FontSize.Size24
local BAR_THICKNESS = 6
local STYLE_PADDING = 17
local CHOICE_PADDING = 6 * 2 -- (Added to vertical height)
local PROMPT_SIZE = Vector2.new(80, 90)
local FRAME_WIDTH = 350

local WIDTH_BONUS = (STYLE_PADDING * 2) - BAR_THICKNESS
local XPOS_OFFSET = -(STYLE_PADDING - BAR_THICKNESS)

local playerService = game:GetService("Players")
local contextActionService = game:GetService("ContextActionService")
local userInputService = game:GetService("UserInputService")
local guiService = game:GetService("GuiService")
local AnalyticsService = game:GetService("RbxAnalyticsService")
local YPOS_OFFSET = -math.floor(STYLE_PADDING * 0.5)
local usingGamepad = false

local FlagHasReportedPlace = false
local localPlayer = playerService.LocalPlayer
while localPlayer == nil do
	playerService.PlayerAdded:Wait()
	localPlayer = playerService.LocalPlayer
end

local character = localPlayer.Character
local humanoidRootPart

local function updateCharacter(newChar)
	character = newChar
	if newChar then
		humanoidRootPart = newChar:WaitForChild("HumanoidRootPart", 5)
	else
		humanoidRootPart = nil
	end
end

if character then task.spawn(updateCharacter, character) end
localPlayer.CharacterAdded:Connect(updateCharacter)

local USING_GAMEPAD = {
	[Enum.UserInputType.Gamepad1] = true,
	[Enum.UserInputType.Gamepad2] = true,
	[Enum.UserInputType.Gamepad3] = true,
	[Enum.UserInputType.Gamepad4] = true,
}
function setUsingGamepad(input, processed)
	usingGamepad = USING_GAMEPAD[input.UserInputType] or false
end

userInputService.InputBegan:Connect(setUsingGamepad)
userInputService.InputChanged:Connect(setUsingGamepad)

local goodbyeChoiceActiveFlagSuccess, goodbyeChoiceActiveFlagValue = pcall(function()
	return settings():GetFFlag("GoodbyeChoiceActiveProperty")
end)
local goodbyeChoiceActiveFlag = (goodbyeChoiceActiveFlagSuccess and goodbyeChoiceActiveFlagValue)

local mainFrame
local choices = {}
local lastChoice
local choiceMap = {}
local currentConversationDialog
local currentConversationPartner

local coroutineMap = {}
local currentDialogTimeoutCoroutine = nil

local tooFarAwayMessage =           "You are too far away to chat!"
local tooFarAwaySize = 300
local characterWanderedOffMessage = "Chat ended because you walked away"
local characterWanderedOffSize = 350
local conversationTimedOut =        "Chat ended because you didn't reply"
local conversationTimedOutSize = 350

local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local RobloxGui = CoreGui.RobloxGui
local RobloxReplicatedStorage = game:GetService('RobloxReplicatedStorage')
local setDialogInUseEvent = RobloxReplicatedStorage:WaitForChild("SetDialogInUse", math.huge)

local chatNotificationGui
local messageDialog
local dialogMap = {}
local dialogConnections = {}
local touchControlGui = nil

local gui = nil

local isTenFootInterface = require(RobloxGui.Modules.TenFootInterface):IsEnabled()
local utility = require(RobloxGui.Modules.Settings.Utility)
local GameTranslator = require(RobloxGui.Modules.GameTranslator)
local isSmallTouchScreen = utility:IsSmallTouchScreen()

if isTenFootInterface then
	FONT_SIZE = Enum.FontSize.Size36
	TEXT_HEIGHT = 36
	FRAME_WIDTH = 500
elseif isSmallTouchScreen then
	FONT_SIZE = Enum.FontSize.Size14
	TEXT_HEIGHT = 14
	FRAME_WIDTH = 250
end

if RobloxGui:FindFirstChild("ControlFrame") then
	gui = RobloxGui.ControlFrame
else
	gui = RobloxGui
end
local touchEnabled = userInputService.TouchEnabled

local function isDialogMultiplePlayers(dialog)
	local success, value = pcall(function() return dialog.BehaviorType == Enum.DialogBehaviorType.MultiplePlayers end)
	return success and value or false
end

function currentTone()
	if currentConversationDialog then
		return currentConversationDialog.Tone
	else
		return Enum.DialogTone.Neutral
	end
end


function createChatNotificationGui()
	chatNotificationGui = Instance.new("BillboardGui")
	chatNotificationGui.Name = "ChatNotificationGui"
	chatNotificationGui.ExtentsOffset = vector.create(0, 1, 0)
	chatNotificationGui.Size = UDim2.new(PROMPT_SIZE.X / 31.5, 0, PROMPT_SIZE.Y / 31.5, 0)
	chatNotificationGui.SizeOffset = Vector2.zero
	chatNotificationGui.StudsOffset = vector.create(0, 3.7, 0)
	chatNotificationGui.Enabled = true
	chatNotificationGui.RobloxLocked = true
	chatNotificationGui.Active = true

	local button = Instance.new("ImageButton")
	button.Name = "Background"
	button.Active = false
	button.BackgroundTransparency = 1
	button.Position = UDim2.new(0, 0, 0, 0)
	button.Size = UDim2.new(1, 0, 1, 0)
	button.Image = ""
	button.Parent = chatNotificationGui

	local icon = Instance.new("ImageLabel")
	icon.Name = "Icon"
	icon.Position = UDim2.new(0, 0, 0, 0)
	icon.Size = UDim2.new(1, 0, 1, 0)
	icon.Image = ""
	icon.BackgroundTransparency = 1
	icon.Parent = button

	local activationButton = Instance.new("ImageLabel")
	activationButton.Name = "ActivationButton"
	activationButton.Position = UDim2.new(-0.3, 0, -0.4, 0)
	activationButton.Size = UDim2.new(.8, 0, .8 * (PROMPT_SIZE.X / PROMPT_SIZE.Y), 0)
	activationButton.Image = userInputService:GetImageForKeyCode(Enum.KeyCode.ButtonX)
	activationButton.BackgroundTransparency = 1
	activationButton.Visible = false
	activationButton.Parent = button
end

local CHAT_COLOR_TONE_MAP = {
	[Enum.DialogTone.Neutral] = Enum.ChatColor.Blue,
	[Enum.DialogTone.Friendly] = Enum.ChatColor.Green,
	[Enum.DialogTone.Enemy] = Enum.ChatColor.Red
}
local STYLE_TONE_MAP = {
	[Enum.DialogTone.Neutral] = Enum.FrameStyle.ChatBlue,
	[Enum.DialogTone.Friendly] = Enum.FrameStyle.ChatGreen,
	[Enum.DialogTone.Enemy] = Enum.FrameStyle.ChatRed
}
local IMAGE_TONE_MAP = {
	[Enum.DialogTone.Neutral] = "rbxasset://textures/ui/chatBubble_blue_notify_bkg.png",
	[Enum.DialogTone.Friendly] = "rbxasset://textures/ui/chatBubble_green_notify_bkg.png",
	[Enum.DialogTone.Enemy] = "rbxasset://textures/ui/chatBubble_red_notify_bkg.png",
}


function styleChoices()
	for _, obj in choices do
		obj.BackgroundTransparency = 1
	end
	lastChoice.BackgroundTransparency = 1
end

function styleMainFrame(tone)
	mainFrame.Style = STYLE_TONE_MAP[tone]
	styleChoices()
end
function setChatNotificationTone(gui, purpose, tone)
	gui.Background.Image = IMAGE_TONE_MAP[tone]
	local newIcon, size = unpack(PURPOSE_DATA[purpose])
	local relativeSize = size / PROMPT_SIZE
	gui.Background.Icon.Size = UDim2.new(relativeSize.X, 0, relativeSize.Y, 0)
	gui.Background.Icon.Position = UDim2.new(0.5 - (relativeSize.X * 0.5), 0, 0.4 - (relativeSize.Y * 0.5), 0)
	gui.Background.Icon.Image = newIcon
end

function createMessageDialog()
	messageDialog = Instance.new("Frame");
	messageDialog.Name = "DialogScriptMessage"
	messageDialog.Style = Enum.FrameStyle.Custom
	messageDialog.BackgroundTransparency = 0.5
	messageDialog.BackgroundColor3 = Color3.new(31 / 255, 31 / 255, 31 / 255)
	messageDialog.Visible = false
	messageDialog.RobloxLocked = true

	local text = Instance.new("TextLabel")
	text.Name = "Text"
	text.Position = UDim2.new(0, 0, 0, -1)
	text.Size = UDim2.new(1, 0, 1, 0)
	text.FontSize = Enum.FontSize.Size14
	text.BackgroundTransparency = 1
	text.TextColor3 = Color3.new(1, 1, 1)
	text.Parent = messageDialog
end

function showMessage(msg, size)
	messageDialog.Text.Text = msg
	messageDialog.Size = UDim2.new(0, size, 0, 40)
	messageDialog.Position = UDim2.new(0.5, -size * 0.5, 0.5, -40)
	messageDialog.Visible = true
	task.wait(2)
	messageDialog.Visible = false
end

function variableDelay(str)
	local length = math.min(string.len(str), 100)
	task.wait(0.75 + ((length / 75) * 1.5))
end

function resetColor(frame)
	frame.BackgroundTransparency = 1
end

function wanderDialog()
	mainFrame.Visible = false
	endDialog()
	showMessage(characterWanderedOffMessage, characterWanderedOffSize)
end

function timeoutDialog()
	mainFrame.Visible = false
	endDialog()
	showMessage(conversationTimedOut, conversationTimedOutSize)
end

function normalEndDialog()
	endDialog()
end

function endDialog()
	if currentDialogTimeoutCoroutine then
		coroutineMap[currentDialogTimeoutCoroutine] = false
		currentDialogTimeoutCoroutine = nil
	end

	local dialog = currentConversationDialog
	currentConversationDialog = nil
	if dialog and dialog.InUse then
		-- Waits 5 seconds before setting InUse to false
		task.delay(5, function()
			setDialogInUseEvent:FireServer(dialog, false)
			dialog.InUse = false
		end)
	end

	for dialog, gui in dialogMap do
		if dialog and gui then
			gui.Enabled = not dialog.InUse
		end
	end

	contextActionService:UnbindCoreAction("Nothing")
	currentConversationPartner = nil

	if touchControlGui then
		touchControlGui.Visible = true
	end

	if guiService.SelectedCoreObject and
		guiService.SelectedCoreObject.Parent == mainFrame then
		guiService.SelectedCoreObject = nil
	end
end

function sanitizeMessage(msg)
	if string.len(msg) == 0 then
		return "..."
	else
		return msg
	end
end

local function chatFunc(dialog, ...)
	if isDialogMultiplePlayers(dialog) then
		game:GetService("Chat"):ChatLocal(...)
	else
		game:GetService("Chat"):Chat(...)
	end
end

function selectChoice(choice)
	if not currentConversationDialog then
		return
	end

	renewKillswitch(currentConversationDialog)

	if not character:FindFirstAncestorOfClass("Workspace") then
		return
	end

	--First hide the Gui
	mainFrame.Visible = false
	if choice == lastChoice then
		chatFunc(currentConversationDialog, character, lastChoice.UserPrompt.Text, CHAT_COLOR_TONE_MAP[currentTone()])

		normalEndDialog()
	else
		local dialogChoice = choiceMap[choice]

		chatFunc(currentConversationDialog, character, sanitizeMessage(dialogChoice.UserDialog), CHAT_COLOR_TONE_MAP[currentTone()])
		task.wait(1)

		if not currentConversationDialog then
			return
		end

		currentConversationDialog:SignalDialogChoiceSelected(localPlayer, dialogChoice)
		chatFunc(currentConversationDialog, currentConversationPartner, sanitizeMessage(dialogChoice.ResponseDialog), CHAT_COLOR_TONE_MAP[currentTone()])

		variableDelay(dialogChoice.ResponseDialog)
		presentDialogChoices(currentConversationPartner, dialogChoice)
	end
end

function newChoice()
	local dummyFrame = Instance.new("Frame")
	dummyFrame.Visible = false

	local frame = Instance.new("TextButton")
	frame.BackgroundColor3 = Color3.new(227 / 255, 227 / 255, 227 / 255)
	frame.BackgroundTransparency = 1
	frame.AutoButtonColor = false
	frame.BorderSizePixel = 0
	frame.Text = ""
	frame.MouseEnter:Connect(function()
		frame.BackgroundTransparency = 0
	end)
	frame.MouseLeave:Connect(function()
		frame.BackgroundTransparency = 1
	end)
	frame.SelectionImageObject = dummyFrame
	frame.Activated:Connect(function()
		selectChoice(frame)
	end)
	frame.RobloxLocked = true

	local prompt = Instance.new("TextLabel")
	-- User response text is manually translated using GameTranslator
	prompt.AutoLocalize = false
	prompt.Name = "UserPrompt"
	prompt.BackgroundTransparency = 1
	prompt.Font = Enum.Font.SourceSans
	prompt.FontSize = FONT_SIZE
	prompt.Position = UDim2.new(0, 40, 0, 0)
	prompt.Size = UDim2.new(1, -32 - 40, 1, 0)
	prompt.TextXAlignment = Enum.TextXAlignment.Left
	prompt.TextYAlignment = Enum.TextYAlignment.Center
	prompt.TextWrap = true
	prompt.Parent = frame

	local selectionButton = Instance.new("ImageLabel")
	selectionButton.Name = "RBXchatDialogSelectionButton"
	selectionButton.Position = UDim2.new(0, 0, 0.5, -33 * 0.5)
	selectionButton.Size = UDim2.new(0, 33, 0, 33)
	selectionButton.Image = userInputService:GetImageForKeyCode(Enum.KeyCode.ButtonA)
	selectionButton.BackgroundTransparency = 1
	selectionButton.Visible = false
	selectionButton.Parent = frame

	return frame
end
function initialize(parent)
	choices[1] = newChoice()
	choices[2] = newChoice()
	choices[3] = newChoice()
	choices[4] = newChoice()

	lastChoice = newChoice()
	lastChoice.UserPrompt.Text = "Goodbye!"
	lastChoice.Size = UDim2.new(1, WIDTH_BONUS, 0, TEXT_HEIGHT + CHOICE_PADDING)

	mainFrame = Instance.new("Frame")
	mainFrame.Name = "UserDialogArea"
	mainFrame.Size = UDim2.new(0, FRAME_WIDTH, 0, 200)
	mainFrame.Style = Enum.FrameStyle.ChatBlue
	mainFrame.Visible = false

	for n, obj in choices do
		obj.RobloxLocked = true
		obj.Parent = mainFrame
	end

	lastChoice.RobloxLocked = true
	lastChoice.Parent = mainFrame

	mainFrame.RobloxLocked = true
	mainFrame.Parent = parent
end

function presentDialogChoices(talkingPart, parentDialog)
	if not currentConversationDialog then
		return
	end

	currentConversationPartner = talkingPart
	local sortedDialogChoices = parentDialog:QueryDescendants(">DialogChoice")

	table.sort(sortedDialogChoices, function(a, b) return a.Name < b.Name end)

	if #sortedDialogChoices == 0 then
		normalEndDialog()
		return
	end

	local pos = 1
	local yPosition = 0
	choiceMap = {}
	for n, obj in choices do
		obj.Visible = false
	end

	for n, obj in sortedDialogChoices do
		if pos <= #choices then
			--3 lines is the maximum, set it to that temporarily
			choices[pos].Size = UDim2.new(1, WIDTH_BONUS, 0, TEXT_HEIGHT * 3)
			GameTranslator:TranslateAndRegister(choices[pos].UserPrompt, obj, obj.UserDialog)
			local height = (math.ceil(choices[pos].UserPrompt.TextBounds.Y / TEXT_HEIGHT) * TEXT_HEIGHT) + CHOICE_PADDING

			choices[pos].Position = UDim2.new(0, XPOS_OFFSET, 0, YPOS_OFFSET + yPosition)
			choices[pos].Size = UDim2.new(1, WIDTH_BONUS, 0, height)
			choices[pos].Visible = true

			choiceMap[choices[pos]] = obj

			yPosition += height + 1 -- The +1 makes highlights not overlap
			pos += 1
		end
	end

	lastChoice.Size = UDim2.new(1, WIDTH_BONUS, 0, TEXT_HEIGHT * 3)
	lastChoice.UserPrompt.Text = parentDialog.GoodbyeDialog == "" and "Goodbye!" or parentDialog.GoodbyeDialog
	local height = (math.ceil(lastChoice.UserPrompt.TextBounds.Y / TEXT_HEIGHT) * TEXT_HEIGHT) + CHOICE_PADDING
	lastChoice.Size = UDim2.new(1, WIDTH_BONUS, 0, height)
	lastChoice.Position = UDim2.new(0, XPOS_OFFSET, 0, YPOS_OFFSET + yPosition)
	lastChoice.Visible = true

	if goodbyeChoiceActiveFlag and not parentDialog.GoodbyeChoiceActive then
		lastChoice.Visible = false
		mainFrame.Size = UDim2.new(0, FRAME_WIDTH, 0, yPosition + (STYLE_PADDING * 2) + (YPOS_OFFSET * 2))
	else
		mainFrame.Size = UDim2.new(0, FRAME_WIDTH, 0, yPosition + lastChoice.AbsoluteSize.Y + (STYLE_PADDING * 2) + (YPOS_OFFSET * 2))
	end

	mainFrame.Position = UDim2.new(0, 20, 1.0, -mainFrame.Size.Y.Offset - 20)
	if isSmallTouchScreen then
		local touchScreenGui = localPlayer.PlayerGui:FindFirstChild("TouchGui")
		if touchScreenGui then
			touchControlGui = touchScreenGui:FindFirstChild("TouchControlFrame")
			if touchControlGui then
				touchControlGui.Visible = false
			end
		end
		mainFrame.Position = UDim2.new(0, 10, 1.0, -mainFrame.Size.Y.Offset)
	end
	styleMainFrame(currentTone())
	mainFrame.Visible = true

	if usingGamepad then
		game:GetService("GuiService").SelectedCoreObject = choices[1]
	end
end

function doDialog(dialog)
	if dialog.InitialPrompt == "" then
		warn("Can't start a dialog with an empty InitialPrompt")
		return
	end

	local isMultiplePlayers = isDialogMultiplePlayers(dialog)

	if dialog.InUse and not isMultiplePlayers then
		return
	else
		currentConversationDialog = dialog
		dialog.InUse = true
		-- only bind if we actual enter the dialog
		contextActionService:BindCoreAction("Nothing", function()
		end, false, Enum.UserInputType.Gamepad1, Enum.UserInputType.Gamepad2, Enum.UserInputType.Gamepad3, Enum.UserInputType.Gamepad4)
		-- Immediately sets InUse to true on the server
		setDialogInUseEvent:FireServer(dialog, true)
	end
	chatFunc(dialog, dialog.Parent, dialog.InitialPrompt, CHAT_COLOR_TONE_MAP[dialog.Tone])
	variableDelay(dialog.InitialPrompt)

	presentDialogChoices(dialog.Parent, dialog)
end

function renewKillswitch(dialog)
	if currentDialogTimeoutCoroutine then
		coroutineMap[currentDialogTimeoutCoroutine] = false
		currentDialogTimeoutCoroutine = nil
	end

	currentDialogTimeoutCoroutine = coroutine.create(function(thisCoroutine)
		task.wait(15)
		if thisCoroutine ~= nil then
			if coroutineMap[thisCoroutine] == nil then
				setDialogInUseEvent:FireServer(dialog, false)
				dialog.InUse = false
			end
			coroutineMap[thisCoroutine] = nil
		end
	end)
	coroutine.resume(currentDialogTimeoutCoroutine, currentDialogTimeoutCoroutine)
end

function checkForLeaveArea()
	while currentConversationDialog do
		if currentConversationDialog.Parent and (localPlayer:DistanceFromCharacter(currentConversationDialog.Parent.Position) >= currentConversationDialog.ConversationDistance) then
			wanderDialog()
		end
		task.wait(1)
	end
end

function startDialog(dialog)
	if dialog.Parent and dialog.Parent:IsA("BasePart") then
		AnalyticsService:TrackEvent("Dialogue", "Old Dialogue", "Conversation Initiated")

		if localPlayer:DistanceFromCharacter(dialog.Parent.Position) >= dialog.ConversationDistance then
			showMessage(tooFarAwayMessage, tooFarAwaySize)
			return
		end

		for dialog, gui in dialogMap do
			if dialog and gui then
				gui.Enabled = false
			end
		end

		renewKillswitch(dialog)

		task.delay(1, checkForLeaveArea)
		doDialog(dialog)
	end
end

function removeDialog(dialog)
	if dialogMap[dialog] then
		dialogMap[dialog]:Destroy()
		dialogMap[dialog] = nil
	end
	if dialogConnections[dialog] then
		dialogConnections[dialog]:disconnect()
		dialogConnections[dialog] = nil
	end
end

function addDialog(dialog)
	if dialog.Parent then
		if dialog.Parent:IsA("BasePart") and dialog:IsDescendantOf(workspace) then
			FlagHasReportedPlace = true
			AnalyticsService:TrackEvent("Dialogue", "Old Dialogue", "Used In Place", nil, game.PlaceId)

			local chatGui = chatNotificationGui:clone()
			chatGui.Adornee = dialog.Parent
			chatGui.RobloxLocked = true
			chatGui.Enabled = not dialog.InUse or isDialogMultiplePlayers(dialog)
			chatGui.Parent = CoreGui

			chatGui.Background.MouseButton1Click:Connect(function()
				startDialog(dialog)
			end)
			setChatNotificationTone(chatGui, dialog.Purpose, dialog.Tone)

			if game:GetEngineFeature("ClosestVisibleDialogAPI") then
				dialog:SetGuiObject(chatGui)
			end
			dialogMap[dialog] = chatGui

			dialogConnections[dialog] = dialog.Changed:Connect(function(prop)
				if prop == "Parent" and dialog.Parent then
					--This handles the reparenting case, seperate from removal case
					removeDialog(dialog)
					addDialog(dialog)
				elseif prop == "InUse" then
					if not isDialogMultiplePlayers(dialog) then
						chatGui.Enabled = (currentConversationDialog == nil) and not dialog.InUse
					else
						chatGui.Enabled = (currentConversationDialog ~= dialog)
					end

					if not dialog.InUse and not isDialogMultiplePlayers(dialog) and dialog == currentConversationDialog then
						timeoutDialog()
					end
				elseif prop == "Tone" or prop == "Purpose" then
					setChatNotificationTone(chatGui, dialog.Purpose, dialog.Tone)
				end
			end)
		else -- still need to listen to parent changes even if current parent is not a BasePart
			dialogConnections[dialog] = dialog.Changed:Connect(function(prop)
				if prop == "Parent" and dialog.Parent then
					--This handles the reparenting case, seperate from removal case
					removeDialog(dialog)
					addDialog(dialog)
				end
			end)
		end
	end
end

function onLoad()
	createChatNotificationGui()

	createMessageDialog()
	messageDialog.RobloxLocked = true
	messageDialog.Parent = gui

	gui:WaitForChild("BottomLeftControl")

	local frame = Instance.new("Frame")
	frame.Name = "DialogFrame"
	frame.Position = UDim2.new(0, 0, 0, 0)
	frame.Size = UDim2.new(0, 0, 0, 0)
	frame.BackgroundTransparency = 1
	frame.RobloxLocked = true
	-- AddSelectionParent is deprecated
	(game:GetService("GuiService") :: any):AddSelectionParent("RBXDialogGroup", frame)

	if (touchEnabled and not isSmallTouchScreen) then
		frame.Position = UDim2.new(0, 20, 0.5, 0)
		frame.Size = UDim2.new(0.25, 0, 0.1, 0)
		frame.Parent = gui
	elseif isSmallTouchScreen then
		frame.Position = UDim2.new(0, 0, .9, -10)
		frame.Size = UDim2.new(0.25, 0, 0.1, 0)
		frame.Parent = gui
	else
		frame.Parent = gui.BottomLeftControl
	end
	initialize(frame)

	game:GetService("CollectionService").ItemAdded:Connect(function(obj)
		if obj:IsA("Dialog") then
			addDialog(obj)
		end
	end)
	game:GetService("CollectionService").ItemRemoved:Connect(function(obj)
		if obj:IsA("Dialog") then
			removeDialog(obj)
		end
	end)
	for i, obj in game:GetService("CollectionService"):GetCollection("Dialog") do
		if obj:IsA("Dialog") then
			addDialog(obj)
		end
	end
end

function dialogIsValid(dialog)
	return dialog and dialog.Parent and dialog.Parent:IsA("BasePart")
end

local lastClosestDialog = nil
local getClosestDialogToPosition = guiService.GetClosestDialogToPosition

local timeAccumulator = 0
local TIMER_INTERVAL = 1/8
game:GetService("RunService").Heartbeat:Connect(function(dt)
	timeAccumulator += dt
	if timeAccumulator < TIMER_INTERVAL then return end
	timeAccumulator = 0
	local closestDistance = math.huge
	local closestDialog = nil

	if humanoidRootPart then
		local characterPosition = humanoidRootPart.Position
		closestDialog = getClosestDialogToPosition(guiService, characterPosition)
	end

	if humanoidRootPart and dialogIsValid(closestDialog) and currentConversationDialog == nil then

		local dialogTriggerDistance = closestDialog.TriggerDistance
		local dialogTriggerOffset = closestDialog.TriggerOffset

		local distanceFromCharacterWithOffset = localPlayer:DistanceFromCharacter(
			closestDialog.Parent.Position + dialogTriggerOffset
		)

		if dialogTriggerDistance ~= 0 and
			distanceFromCharacterWithOffset < closestDialog.ConversationDistance and
			distanceFromCharacterWithOffset < dialogTriggerDistance then

			startDialog(closestDialog)
		end
	end

	if usingGamepad == true then
		if humanoidRootPart and game:GetEngineFeature("ClosestVisibleDialogAPI") then
			local characterPosition = humanoidRootPart.Position
			closestDialog = guiService.GetClosestVisibleDialogToPosition(guiService, characterPosition)
		end
		if closestDialog ~= lastClosestDialog then
			if dialogMap[lastClosestDialog] then
				dialogMap[lastClosestDialog].Background.ActivationButton.Visible = false
			end
			lastClosestDialog = closestDialog
			contextActionService:UnbindCoreAction("StartDialogAction")
			if closestDialog ~= nil then
				contextActionService:BindCoreAction("StartDialogAction", function(actionName, userInputState, inputObject)
					if userInputState == Enum.UserInputState.Begin then
						if closestDialog and closestDialog.Parent then
							startDialog(closestDialog)
						end
					end
				end, false, Enum.KeyCode.ButtonX)
				if dialogMap[closestDialog] then
					dialogMap[closestDialog].Background.ActivationButton.Visible = true
				end
			end -- closestDialog ~= nil
		end -- closestDialog ~= lastClosestDialog
	end -- usingGamepad == true
end)

local lastSelectedChoice = nil

guiService.Changed:Connect(function(property)
	if property == "SelectedCoreObject" then
		if lastSelectedChoice and lastSelectedChoice:FindFirstChild("RBXchatDialogSelectionButton") then
			lastSelectedChoice:FindFirstChild("RBXchatDialogSelectionButton").Visible = false
			lastSelectedChoice.BackgroundTransparency = 1
		end
		lastSelectedChoice = guiService.SelectedCoreObject
		if lastSelectedChoice and lastSelectedChoice:FindFirstChild("RBXchatDialogSelectionButton") then
			lastSelectedChoice:FindFirstChild("RBXchatDialogSelectionButton").Visible = true
			lastSelectedChoice.BackgroundTransparency = 0
		end
	end
end)

onLoad()

3 Likes