Do subroutines run correctly inside modules?

Hi, there. Thanks for reading in advance. I’ve created a module that handles kill rewards / credits in a small fighting game I’m making. This subroutine exists within the main death processing function, and handles the timeout of the the killer’s [k] killstreak. The timeout loop stops itself if the player has nine or more kills in the streak (the game has a 10 player limit) or if the player has gone 10 seconds [streakreq] without killing another target. However, with no outside influence, the subroutine shown here never ends. The loop itself stops running after about 2 seconds of runtime, and the ‘streak ended’ print never reaches the output.

I’ve double-checked countless times; there aren’t any outside influences on the variables contained within that could cause the routine to hang. After reading a few articles, I’ve learned that some applications of subroutines can exhibit odd behavior inside modules. Is this one of those cases, and if so, what must I do to remedy it?

local module = {}

local damage = {}
local recent = {}
local kills = {}

local streak = {}
local stime = {}
local ktime = {}

--
function wfc(p, o, t)
	return p:WaitForChild(o, t)
end

function newC3(r, g, b)
	return Color3.new(r/255, g/255, b/255)
end

--
i_n = Instance.new
v3 = Vector3.new
cfn = CFrame.new
pn = PhysicalProperties.new
bcn = BrickColor.new
c3 = Color3.new

rand = math.random

--
sss = script.Parent
data = require(wfc(sss, "DataModule"))

ps = game:GetService("PhysicsService")
ss = game:GetService("ServerStorage")
rs = game:GetService("ReplicatedStorage")

remotes = wfc(rs, "Remotes")
smsg = wfc(remotes, "ServerMessage")

gfolder = wfc(workspace, "Global")
ding = wfc(gfolder, "Died")

--
streakreq = 20
killwait = 30

jargon = {
	{"DOUBLE KILL!", newC3(255, 50, 0)},
	{"TRIPLE KILL!!", newC3(255, 150, 0)},
	{"QUADRA-KILL!!!", newC3(255, 255, 0)},
	{"PENTA-KILL!!!", newC3(150, 255, 0)},
	{"HEXA-KILL!!!", newC3(50, 255, 0)},
	{"SEPTA-KILL!!!", newC3(50, 255, 0)},
	{"OCTA-KILL!!!", newC3(0, 255, 0)},
	{"ACE!!!", newC3(0, 255, 255)},
}

--
function message(text, col)
	smsg:FireAllClients(text or "NULL", col or c3(1, 1, 1))
end

function getKiller(p)
	local highest = 0
	local k = nil
	
	if damage[p] then
		for n,v in pairs(damage[p]) do
			if v > highest then k = n; highest = v end
		end
	end
	
	return k
end

function getReward(p)
	local e = p:FindFirstChild("KillReward", true)
	if e and e:IsA("BindableEvent") then return e end
	return nil
end

function processKill(p, t, r, rq)
	local k = r or p
	
	local typ = 1
	local bonus = 10
	
	local reward = getReward(k)
	local class = data:Check(k, "Game", "Settings", "Class")
	local tclass = data:Check(t, "Game", "Settings", "Class")
	local pclass = data:Check(p, "Game", "Settings", "Class")
	
	kills[k] = kills[k] or {}
	kills[t] = kills[t] or {}
	
	local kp = kills[k]; kp[t] = (kp[t] or 0) + 1
	local kt = kp[t]
	
	local tp = kills[t]; tp[k] = tp[k] or 0
	local tk = tp[k]
	
	local ms = data:Check(k, "Game", "Stats", "Streak")
	local ks = streak[k]; stime[k] = tick()
	
	local GK = data:Check(k, "Game", "Stats", "KOs") + 1
	local GW = data:Check(k, "Game", "Stats", "WOs")
	GW = math.max(GW, 1)
	
	local CK = data:Check(k, "Class", class, "KOs") + 1
	local CW = data:Check(k, "Class", class, "WOs")
	CW = math.max(CW, 1)
	
	local GKD = math.floor((GK/GW) * 100)
	local CKD = math.floor((CK/CW) * 100)
	
	data:editGlobal(class, "KOs", 1)
	
	data:GameEdit(k, "Stats", "KOs", 1)
	data:ClassEdit(k, class, "KOs", 1)
	
	data:GameEdit(k, "Stats", "KDR", tostring(GKD/100))
	data:ClassEdit(k, class, "KDR", tostring(CKD/100))
	
	if not ks or ks <= 0 then ks = 1
		spawn(function() ------------------------------- << SUBROUTINE STARTS HERE
			print('streak started')
			while (tick()-stime[k]) < streakreq do wait()
				print(tick()-stime[k] < streakreq)
				if streak[k] >= 9 then print('overflow') break end
			end
			
			print('streak ended')
			streak[k] = 0
		end)
		
	else ks = ks + 1
		if ks > ms then data:GameEdit(k, "Stats", "Streak", ks - ms) end
	end
	
	if k ~= p then typ = 2
		message(k.Name.." ("..class..") stole "..t.Name.." ("..tclass..") from "..p.Name.." ("..pclass..")!", newC3(255, 0, 150))
	
	elseif rq then message(k.Name.." ("..class..") made "..t.Name.." ("..tclass..") ragequit!", newC3(255, 0, 0))
	else message(k.Name.." ("..class..") destroyed "..t.Name.." ("..tclass..")!", newC3(255, 0, 0))
	end
	
	if tk > 2 then bonus = 30
		message(k.Name.." ("..class..") got REVENGE on "..t.Name.." ("..tclass..")!", newC3(255, 150, 0))
		
	elseif kt > 2 then bonus = bonus + (5 * (kt-2))
		message(k.Name.." ("..class..") is DOMINATING (x"..(kt-2)..") "..t.Name.." ("..tclass..")!", newC3(255, 150, 0)) 
	end
	
	if ks >= 2 then local j = jargon[ks-1] or jargon[#jargon]
		typ = 3; bonus = bonus * (ks - 1)
		message(k.Name.." "..j[1], j[2])
	end
	
	reward:Fire(typ)
	data:GameEdit(k, "Stats", "Points", bonus)
	
	tp[k] = 0; streak[k] = ks
end

--
function module:AddDamage(p1, p2, amt)
	local class = data:Check(p1, "Game", "Settings", "Class")
	
	if amt < 0 then amt = math.abs(amt)
		data:GameEdit(p1, "Stats", "Healing", amt)
		data:ClassEdit(p1, class, "Healing", amt)
		
	elseif p1 ~= p2 then recent[p2] = p1
		damage[p2] = damage[p2] or {}
		ktime[p2] = ktime[p2] or {}
		
		local d = damage[p2]
		d[p1] = (d[p1] or 0) + amt
		
		local dt = ktime[p2]; local bool = (dt[p1] ~= nil)
		dt[p1] = tick()
		
		if not bool then spawn(function()
				while (tick() - dt[p1]) <= killwait do wait() end
				d[p1] = 0; dt[p1] = nil
			end)
		end
		
		data:GameEdit(p1, "Stats", "Damage", amt)
		data:ClassEdit(p1, class, "Damage", amt)
	end
end

function module:ProcessDeath(p, rq)
	local class = data:Check(p, "Game", "Settings", "Class")
	local killer = getKiller(p)
	
	local GK = data:Check(p, "Game", "Stats", "KOs")
	local GW = data:Check(p, "Game", "Stats", "WOs") + 1
	
	local CK = data:Check(p, "Class", class, "KOs")
	local CW = data:Check(p, "Class", class, "WOs") + 1
	
	local GKD = math.floor((GK/GW) * 100)
	local CKD = math.floor((CK/CW) * 100)
	
	data:editGlobal(class, "WOs", 1)
	
	data:GameEdit(p, "Stats", "WOs", 1)
	data:ClassEdit(p, class, "WOs", 1)
	
	data:GameEdit(p, "Stats", "KDR", tostring(GKD/100))
	data:ClassEdit(p, class, "KDR", tostring(CKD/100))
	
	ding:Play()
	
	if killer and killer ~= p then
		processKill(killer, p, recent[p], rq)
		else message(p.Name.." died!", newC3(255, 0, 0))
	end
	
	damage[p] = {}; recent[p] = nil
end

function module:reset()
	print('reset')
	damage = {}; recent = {}
	streak = {}; stime = {}
end

--
return module

I am a little confused by the lack of context in your code sample.
For instance where is the streakreq set and is this for a single player game?

It isn’t, I’ll edit with the entire script and mark the sample I’ve shown above.

Ok, thanks for that.
I am finding it a bit difficult reading the code, for example is p the player?
and if so what is r and what are you trying to achieve with the line ‘local k = r or p’ in processKill function which I am assuming is called from the client.

Nope, all of this is server-sided. ProcessDeath sends the ProcessKill function the person who died [t], the person who did the most damage to them [p], and the person who most recently attacked the newly dead person [r]. There’s also a supplemental argument [rq] that determines whether or not the death was achieved by the target leaving the game (it stands for ragequit, har-har).

I wouldn’t know that right away by reading it, which is a problem :grimacing: you have a lot of poorly named variables that only you would know what they meant at a glance (and probably not right away if you looked at it again in the future). This is a problem when you require others to read your code, such as asking for help debugging…so not to gripe or get off topic, but I think that’s something you could improve on

I’m going to come off as snarky, but:

  1. I don’t see how variable names could pose an issue to anyone with more than layperson’s experience in coding - they don’t trip me up, and unless I was designing a module to be used by anyone other than myself regardless of background, I don’t see any reason to change habits.

  2. I posed the question I was asking directly in the title, with a relevant snippet that may contain the problem within. Anyone responding who already knew the answer to that question would have no need to examine my code, unless there was any due uncertainty.

1 Like

Well yes usually variable names aren’t a problem but when they are one or two characters long that can strip them of context/meaning. This is bad naming convention practice.

Yes my comment wasn’t directly aimed toward the problem at hand but if you were just looking for an answer to the question posed in the title, then…

:grimacing:

So the code does hold more or less relevance here. The problem with the variable names was exhibited when you had to explain your variable names to dude above. Just throwing a tip out there :+1:

Happy coding!

Without reading your code, to answer your title question:

Do subroutines run correctly inside modules?

Yes modules are no different than a normal script once they start executing.
But “correctly” is of course from the engines perspective and not yours.
Keep debugging :slightly_smiling_face:

After reading a few articles, I’ve learned that some applications of subroutines can exhibit odd behavior inside modules

If that’s the case, then by all means share them, otherwise it’s not much help to us.

There is a little-understood Roblox feature called a “Script Context.” This context, in the background, keeps track of which event connections and task scheduler calls belong to which scripts, so that when a script is deleted, the game engine can disconnect or stop them, such as by forgetting about in-progress wait calls.

Because you are directly calling your ModuleScript’s functions from another script, the game engine thinks your wait calls belong that script instead of the ModuleScript.

It is possible that the script that is supposed to start this loop gets deleted or disabled sometime after starting it, causing the loop’s wait calls to be silently-aborted.

My suggestion is to have your ModuleScript handle function calls through a BindableFunction so that the script context is the ModuleScript itself.