Breakpoints not activating in module script ran from a Script with Client Context

Breakpoints set inside a module script function ran from a Script with the Client Context option set are not pausing execution at all. Other breakpoints seem to work fine, even breakpoints in module scripts ran from a LocalScript.

Interestingly enough, these breakpoints were working when I first started the project. I cannot recall if I made any serious changes that made these errors.

Context:
The project I am running includes a Villager model inside of Replicated storage with 2 scripts as such:
-A Script with Client Context that is Enabled, called ‘Villager’
-A ModuleScript containing all my villager behaviours and data, called ‘VillagerAI’
01

Essentially, the Villager Client Script calls a function inside the module script called Run.
-The first 3 lines of code is to stop the script running inside the Rep Storage

This is the layout of the module scripts Run() function:
-An infinite loop spaced by task.wait() calls. Inside here I do all my logic and behaviours.

So in my mind this while loop is being executed inside the thread spawned by the Villager Client Script.

The functionality is perfect, it does what I want it to. I clone these villagers and put them into a folder in Workspace. Each VillagerAI module script contains data pertaining to each one and the Run() function runs (I assume every frame?) But no breakpoints inside the Run() function calls, even outside of the while loop. Breakpoints work inside all other scripts including the client script.

What I tried
-I tried took to disable the beta feature “Intuitive Debugger” but I couldnt find it. I guess that feature is already passed.
-Disabling faster Play Solo beta feature

I dont understand why it worked first few days with this same layout. I know Studio updated twice or more since I started and it may have been after an update. Nothing on Google helped, all were posts from a year ago and all were solved.

I think a bit more information is needed, especially since your images don’t show the breakpoints.

  • Can you confirm that the breakpoints are still there?
  • Are the breakpoints enabled? (solid red dot, not a red outlined circle)
  • Have you tried creating a smaller reproduction of the issue? (Just a single script with Client context with a breakpoint set somewhere)

Yes, I will provide better snippets


The breakpoints are indeed enabled (except there are some disabled breakpoints but those are not in use currently), the 2nd snapshots shows the config of each breakpoint. They’re all on ‘ALL CONTEXTS’ and no other conditions. I attempted to set contexts only to Client and Edit but that didn’t change anything so I changed it back to All.

To answer the last question, I just tried after you posted it. I created a 2nd module script with the same kind of layout :
test2

I added this function call to the Villager Client Script:

Works perfectly fine. Very strange. So the problem has to be with my module script Villager AI? I cannot find the difference. I will post it now:

--!strict
local ai = {}
local mt = {}
local Types = require(workspace.Utils.Types)

local main = require(workspace.Utils.Game)
local Pathfinder = require(workspace.Utils.Pathfinder)
local Timer = require(workspace.Utils.Timer)
local model = script.Parent
local humanoid = model.Humanoid
local entityId = model:GetAttribute("EntID")
local entity : Types.Villager= main.GetEntityById(entityId)
local state = "Idle"
local target = nil
local treeHarvestSpeed = 0.2
local buildSpeed = 1
local baseWalkSpeed = 16
local taskName : string? = nil
local taskPriority : number? = nil
local taskArgs : { [string] : any }?  = nil
local taskProgress : number? | string? = nil
local pathAgent : Types.Pathfinder = Pathfinder.new(entity, {
	AgentRadius = 4,
	AgentHeight = 9,
	AgentCanJump = true,
	AgentCanClimb = false,
	Costs = {
		Cobblestone = 0.25
	}
})

local PathfindingService = game:GetService("PathfindingService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local TraversedPath = Instance.new("BindableEvent")
local cleanup = {}
local timer : number? = nil

local canEat = true
local eatTimer = Timer.new(3, { OnElapsed = function() canEat = true end })
local actionTimer : {}? = nil

ai.TaskFinished = Instance.new("BindableEvent")

local function getEntryPoint(building : Types.Entity)
	local size = building.Inst:GetExtentsSize()
	return building.Inst:GetPivot():ToWorldSpace(CFrame.new(size.X / 2 + 2, 0, 0)).Position
end

local function findEntity(name : string) : Types.Entity?
	local list = main.GetEntitiesSortedDistance(name, model:GetPivot().Position)

	if list[1] then
		return list[1][1]
	end
	
	return nil
end

local function targetDeltaPosition()
	return (model:GetPivot().Position - target.Inst:GetPivot().Position).Magnitude
end


local function ListenEventOnce(event: BindableEvent, func : (...any) -> ())
	local conn = event.Event:Once(func)
	table.insert(cleanup, conn)
	return conn
end



function SetState(_state : string)
	state = _state
	
	if state == "Idle" or state == "" then
		if actionTimer then
			actionTimer:Stop(true)
			actionTimer = nil
		end
	end
	
	if state == "" then
		taskName = nil
		taskPriority = nil
		taskProgress = nil
		taskArgs = nil
		target = nil
		timer = nil
		--ResetPath()
		pathAgent:CancelPath()
		
		for _, v in ipairs(cleanup) do
			v:Disconnect()
		end

		table.clear(cleanup)
	end
end

function ai.GetState()
	return state
end

function ai.GetTask()
	return taskName, taskPriority, taskProgress, taskArgs
end

function ai.GetEntity()
	return entity
end

function ai.GetTaskPriority()
	return taskPriority
end

function ai.RunTask(name : string, priority : number, args : {}) : (boolean, string?)
	if state == "Task" then
		if taskPriority > priority then
			return false, "LOW_PRIORITY"
		else
			--Cancel logic
			SetState("Idle")
		end
	end
	
	
	taskName = name
	taskPriority = priority
	taskProgress = 0
	taskArgs = args

	
	print("Villager " .. entityId .. " has new task " .. taskName :: string)
	
	return true
end

local function CalculateTask()
	if entity.Inventory:GetWeight() > 0 then
		ai.RunTask("StoreItems", 100, {})
		return
	end
	
	if canEat and entity.Hunger < 50 then
		ai.RunTask("Eat", 100, {})
		eatTimer:Start()
		canEat = false
		return
	end	
end

function ai.EndTask()
	SetState("")
end

local function FinishTask()
	ai.TaskFinished:Fire(taskName)
	print("Finished " .. taskName .. " task")
	SetState("")
end

function ai.OnOccupationChange(oldOccupation : string, newOccupation : string)
	if entity.Inventory:GetWeight() > 0 then
		ai.RunTask("StoreItems", 100, {})
	else
		ai.EndTask()
	end
	
	print("Villager " .. entityId .. " reassigned from " .. oldOccupation .. " to " .. newOccupation)
end

--Is the AI available/willing to work?
function ai.AvailableWork()
	return state == "Idle" and not taskName 
end

local function OnPathSuccess(success : boolean, status : string?)
	if typeof(taskProgress) == "number" then
		taskProgress += 1
	end
end

local function consumeFood(name : string)
	local amountFilled = 20
	local amountToEat = math.min(entity.PersonalInventory:GetItemAmount(name), math.ceil((100  - entity.Hunger) / amountFilled) )

	if amountToEat > 0 then
		entity.PersonalInventory:RemoveItem(name, amountToEat)
		entity.Hunger += math.min(100, amountToEat * amountFilled)
	end
end

local function buildStructure(buildSite : Types.Entity)
	local queue : Types.BuildQueueEntry = buildSite.BuildQueue

	if queue.Stage == "Build" then
		queue.BuildProgress += 1

		if queue.BuildProgress >= queue.MaxBuildProgress then
			main.buildQueue:Remove(queue)
			main.DestroyEntity(buildSite)
			main.BuildBuilding(queue.BuildName, queue.Origin, queue.Size)
			FinishTask()
		end
	end
end

local function purchaseMarket(market : Types.WorkBuilding, itemName : string, quantity : number) : boolean
	local ppu = main.itemSettings[itemName].Price
	local totalPrice = ppu * quantity

	if entity.Gold >= totalPrice then
		entity.Gold -= totalPrice
		main.IncreaseGold(totalPrice)
		market.Inventory:TransferItem(itemName, quantity, entity.PersonalInventory)
		--Adjust item stocks for market price calcs
		main.itemStocks[itemName] -= quantity

		print("Entity " .. entityId .. " purchased " .. itemName .. " x " .. quantity .. " for PPU: " .. ppu)
		return true
	end

	return false
end

function ai.Run()
	--print("Hello " .. villager.Name .. " ent ID: " .. entityId)
	entity.Hunger = 70
	
	
	local heartbeat = RunService.Heartbeat:Connect(function(deltaTime: number) 
		local floor = humanoid.FloorMaterial
		local walkSpeedMofifier = 1.0
		
		if floor == Enum.Material.Cobblestone then
			walkSpeedMofifier += 1.0
		end
		
		--walkSpeedMofifier -= 0.5 * (entity.Inventory:GetWeight() / entity.CarryWeight)
		
		humanoid.WalkSpeed = baseWalkSpeed * walkSpeedMofifier * main.GetGameSpeed()
		
		local deltaHunger = if main.GetGameRunning() then 0.5 * deltaTime * main.GetGameSpeed() else 0
		entity.Hunger = math.max(0, entity.Hunger - deltaHunger)
	end)
	
	while task.wait() do
		if state == "" then
			CalculateTask()
			
			if taskName then
				SetState("Task")
			else
				SetState("Idle")
			end
		
		elseif state == "Idle" then
			--Try to resolve a task
			if taskName then
				state = "Task"
				--print("Switching to Task state")
				continue
			end
			
			CalculateTask()
			
		elseif state == "Task" then
			--Should always be the case. Repressing Luau type checker from crying about it
			if not taskArgs or not taskProgress then continue end
			
			if taskName == "GatherResource" then
				if taskProgress == 0 then
					--Walk to the resource
					
					if not pathAgent:HasMoveCommand() then
						local cframe :CFrame = taskArgs.Target.Inst:GetPivot()
						local pos = cframe:ToWorldSpace(CFrame.new(taskArgs.Target.Inst:GetExtentsSize().X / 2 + 4, 0, 0)).Position
						
						pathAgent:FollowPath(pos, function(success : boolean, status : string?)
							if success then
								taskProgress += 1
							else
								print("Could not traverse path " .. status)
							end
						end)
					end
					
				elseif taskProgress == 1 then
					if not actionTimer then
						local function OnElapsed()
							--Calculate what resource we're taking
							local res = nil
							for n, v in pairs(taskArgs.Target.Inventory.Items) do
								if v == 0 then continue end
								res = n
								break
							end

							local didTransfer = taskArgs.Target.Inventory:TransferItem(res, 1, entity.Inventory)
							local invFull = entity.Inventory:GetWeight() >= entity.CarryWeight
							
							if didTransfer then
								main.itemStocks[res] += 1
							end

							if (not didTransfer) or (didTransfer and invFull) then
								actionTimer:Stop(true)
								actionTimer = nil
								FinishTask()
							end
						end
						actionTimer = Timer.new(treeHarvestSpeed * entity.WorkSpeed, { OnElapsed = OnElapsed, Looped = true})
						actionTimer:Start()
					end
				end
				
			elseif taskName == "StoreItems" then
				if taskProgress == 0 then
					if not pathAgent:HasMoveCommand() then
						local _target = taskArgs.Target or findEntity("Stockyard")
						
						if not _target then
							continue
						end
						
						target = _target
						
						pathAgent:PathToBuilding(target, function(success: boolean, status: string?) 
							if success then
								taskProgress += 1
							end	
						end)
					end
					
				elseif taskProgress == 1 then
					--Give gold for their work
					local gold = 0
					for n, v in pairs(entity.Inventory.Items) do
						gold +=  main.itemSettings[n].EqPrice * v
					end
					
					entity.Gold += gold
					main.DecreaseGold(gold)					
					
					entity.Inventory:TransferAllItems(target.Inventory :: Types.Inventory)
					FinishTask()
					continue
				end
				
			elseif taskName == "StockBuilding" then
				local workplace = entity.Workplace :: Types.WorkBuilding
				
				if taskProgress == 0 then
					if not pathAgent:HasMoveCommand() then
						pathAgent:PathToBuilding(taskArgs.Target, function(success: boolean, status: string?) 
							if success then
								taskProgress += 1
							end	
						end)
					end
					
					
				elseif taskProgress == 1 then
					if taskArgs.Target.Inventory:TransferItem(taskArgs.Resource, taskArgs.Quantity, entity.Inventory) then
						taskProgress += 1
					end
					
				elseif taskProgress == 2 then
					
					if not pathAgent:HasMoveCommand() then
						pathAgent:PathToBuilding(workplace, function(success: boolean, status: string?) 
							if success then
								taskProgress += 1
							end	
						end)
					end
					
					
				elseif taskProgress == 3 then
					if entity.Inventory:TransferItem(taskArgs.Resource, taskArgs.Quantity, workplace.Inventory) then
						--Remove from overall item stocks (for correct market price calculations)
						if workplace.Inst.Name ~= "Marketplace" then
							main.itemStocks[taskArgs.Resource] -= taskArgs.Quantity
						end
						
						FinishTask()
						continue
					end
				end
				
			elseif taskName == "StockBuildSite" then
				local buildSite : Types.Entity = taskArgs.BuildSite
				local storage : Types.Entity = taskArgs.Storage
				local resource : string = taskArgs.Resource
				local quantity : number = taskArgs.Quantity

				if taskProgress == 0 then
					if not pathAgent:HasMoveCommand() then
						pathAgent:PathToBuilding(storage, function(success: boolean, status: string?) 
							if success then
								taskProgress += 1
							end	
						end)
					end


				elseif taskProgress == 1 then
					if storage.Inventory:TransferItem(resource, quantity, entity.Inventory) then
						taskProgress += 1
					end

				elseif taskProgress == 2 then

					if not pathAgent:HasMoveCommand() then
						pathAgent:PathToBuilding(buildSite, function(success: boolean, status: string?) 
							if success then
								taskProgress += 1
							end	
						end)
					end


				elseif taskProgress == 3 then
					if entity.Inventory:TransferItem(resource, quantity, buildSite.Inventory) then
						FinishTask()
						continue
					end
				end
				
			elseif taskName == "ProduceProduct" then
				local recipe : Types.Recipe = taskArgs.Recipe
				local workplace = entity.Workplace :: Types.WorkBuilding
				
				if taskProgress == 0 then
					
					if entity.ResidingBuilding == workplace then
						taskProgress += 1
						continue
					end
					
					if not pathAgent:HasMoveCommand() then
						pathAgent:PathToBuilding(workplace, function(success: boolean, status: string?) 
							if success then
								taskProgress += 1
							end	
						end)
					end
					
				elseif taskProgress == 1 then
					local stockedWell : boolean = true
					
					for _, v in ipairs(recipe.Ingridients) do
						local myAmount = entity.Inventory:GetItemAmount(v.Item)
						
						if myAmount < v.Quantity then
							if not workplace.Inventory:TransferItem(v.Item, v.Quantity - myAmount, entity.Inventory) then
								stockedWell = false
								continue
							end
						end
					end
					
					--TODO: FinishTask() with an error
					if not stockedWell then
						warn("Could not finish ProduceProduct task because not enough stock!")
						FinishTask()
						continue
					end
					
					--Fully stocked to work
					
					taskProgress += 1
					
				elseif taskProgress == 2 then
					if not actionTimer then
						local function OnElapsed()
							for _, ing in ipairs(recipe.Ingridients) do
								entity.Inventory:RemoveItem(ing.Item, ing.Quantity)
							end

							entity.Inventory:AddItem(recipe.Product, recipe.ProductAmount)
							actionTimer = nil
							FinishTask()
						end
						actionTimer = Timer.new(treeHarvestSpeed * entity.WorkSpeed, { OnElapsed = OnElapsed })
						actionTimer:Start()
					end
					
					
					--if not timer then
					--	timer = tick()
					--	continue
					--elseif tick() - timer > (3) then
					--	timer = nil
						
					--	for _, ing in ipairs(recipe.Ingridients) do
					--		entity.Inventory:RemoveItem(ing.Item, ing.Quantity)
					--	end
						
					--	entity.Inventory:AddItem(recipe.Product, recipe.ProductAmount)
					--	FinishTask()
					--	continue
					--end
				end
				
			elseif taskName == "BuildStructure" then
				local structure : Types.Entity? = taskArgs.Target:Resolve()
				
				--Cancel if the build site is gone (completed by other builders or canceled)
				if not structure then
					print("NO STRUCT")
					FinishTask()
					continue	
				end
				
				if taskProgress == 0 then
					if not pathAgent:HasMoveCommand() then
						pathAgent:FollowPath(structure.Inst:GetPivot().Position, OnPathSuccess)
					end
					
				elseif taskProgress == 1 then
					if not actionTimer then
						local function OnElapsed()
							print("BUild taskname: " .. taskName)
							buildStructure(structure)
						end
						
						actionTimer = Timer.new(buildSpeed * entity.WorkSpeed, { OnElapsed = OnElapsed, Looped = true })
						actionTimer:Start()
					end
				end
				
			elseif taskName == "Eat" then
				if taskProgress == 0 then
					if entity.PersonalInventory:GetItemAmount("Berry") > 0 then
						consumeFood("Berry")
						
						if entity.Hunger > 90 then
							FinishTask()
							continue
						end
					end
					
					--Go to store
					for _, v : Types.Entity in ipairs(main.GetEntities()) do
						if v.Type == "Building" and v.Inst.Name == "Marketplace" then
							local building : Types.WorkBuilding = v :: Types.WorkBuilding
							
							if building.Inventory:GetItemAmount("Berry") > 0 then
								target = building
								taskProgress = "GotoMarket"
								break
							end
						end
					end
					
					if not target then
						FinishTask()
						continue
					end
					
				elseif taskProgress == "GotoMarket" then
					if entity.ResidingBuilding == target then
						taskProgress = "ShopMarket"
						continue
					end
					
					if not pathAgent:HasMoveCommand() then
						pathAgent:PathToBuilding(target, function(success: boolean, status: string?) 
							if success then
								taskProgress = "ShopMarket"
							end	
						end)
					end
					
				elseif taskProgress == "ShopMarket" then
					local stock = target.Inventory:GetItemAmount("Berry")
					local amountFilled = 20
					local amountToEat = math.min(stock, math.ceil((100  - entity.Hunger) / amountFilled) )
					local pricePerUnit = main.itemSettings["Berry"].Price
					local amountToBuy = math.min(amountToEat, math.floor(entity.Gold / pricePerUnit))
					
					if amountToBuy > 0 then
						if purchaseMarket(target, "Berry", amountToBuy) then
							taskProgress = 0
						end
					end
				end
			end
		end
	end
	
	--Not running the AI anymore
	heartbeat:Disconnect()
end

print("Villager AI!")

return ai

Update: Copy pasting the script into a new module script re-enables breakpoint functionality. This is either my fault or a bug, but for now it is clearly a bug until I figure out what I did that caused it.

Update 2: I found the problem.
Each villager in my game is an entity, which is a table containing data about that villager, and this is a table external to the module script in question.

Observe this code in another module script called ‘Game’ (yes breakpoints in this one work). This code spawns a villager and create the entity table/object

-- Init ent	
	local ent : Types.Villager = InitEntity(villagerModel, "Villager", map.getCellCoordsFromWorldCoords(pos.X, pos.Z), Vector2.new(1,1))
	ent.Inventory = Inventory.new()
	ent.PersonalInventory = Inventory.new()
	ent.Occupation = "Labourer"
	ent.CarryWeight = 10
	ent.WorkSpeed = 1.0
	ent.Gold = 100
	ent.Hunger = 100
	ent.Sex = if numFemale > numMale then "Male" else "Female"
	ent.Relations = { Children = {}, Parents = {} }
	ent.AI = require(villagerModel:FindFirstChild("AI"))
	
	table.insert(villagers, ent)
	table.insert(occupations.Labourer, ent)
	
	villagerModel.Parent = workspace.Dynamic
	setModelCollisionGroup(villagerModel, "Villager")
	
	main.AssignHousing()

The line of code that breaks the breakpoint functionality is this one. If removed, it works fine.

ent.AI = require(villagerModel:FindFirstChild("AI"))

This key in the table, AI, stores the module script (VillagerAI)'s data so I can use functions such as GetTask() that gets data from the module script. I also use this to call functions in my module script from other scripts/module scripts. Why does this break it>?

Note:villagerModel:FindFirstChild(“AI”) refers to the copied module script “VillagerAI”. This is the one I copied to test it

Ok, I think its caused by cyclic module dependencies. I didn’t see a warning or an error for it, but apparently it silently disabled the breakpoints.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.