Making a module script loader that does not miss dependencies because of loading order

I know it’s possible if in your table of modules you have pointers to the modules and just call require(loader.Module) from the individual modules but is it possible to do with the module importing code to just be (and assuming yielding of the modules is allowed): local module = loader.Module

Edit I would like to avoid returning an empty table and then linking it with the original table

3 Likes

The root of the problem is that your modules are yielding, can you change that?

I have :WaitForChilds which yield, I could wrap every single module except for the table they return but that seems to be a messy solution. Also I’m looking for a solution that just requires change in either the loader or the proxy so I won’t have to change the 100+ modules that I use (again - since I previously just did require(loader.Module))

This is why :WaitForChild is bad, what are you using WaitForChild for?

1 Like

Making sure I don’t get errors when I index items in the game if they don’t exist already - I had to add them because I was getting errors, but that was when I had my bootstrap script in ReplicatedFirst (decided to move it out because it seemed to be causing more lag)

You shouldn’t need to WaitForChild anything in ReplicatedStorage if your startup script is in PlayerScripts, PlayerGui, or ServerScriptsService. Yeah I keep everything in ReplicatedFirst self-contained.

I think WaitForChild is only necessary inside of GUI’s because the Roblox engine runs scripts before it finishes replicating (which is broken IMO.)

If you’re expecting it to be there but want to try yielding just in case, you could do something hacky like this:

local instance = parent:FindFirstChild("Name") or parent:WaitForChild("Name")

Do I need WFC recursively for the PlayerGui? Or just the ScreenGuis/just PlayerGui?

Also do you know if:

  1. When something replicates to the client, all of its descendants will automatically be replicated as well (or is there a delay and a wfc is necessary)?
  2. When the client :Clones() something, once the base item that was cloned is returned, are all of the descendents created as well or is a wfc necessary?

(Testing this stuff won’t suffice because my computer is just 1 out of millions)

Yes I believe the PlayerGui needs to be waited for, although you could wait for it at the top of your bootstrap script instead of inside of a module that gets loaded via __index. You could also try creating a screenGui/folder, return it, then insert the folder into the PlayerGui in a separate thread by doing something like this:

local screenGui = Instance.new("ScreenGui")
coroutine.wrap(function()
	screenGui.Parent = game:GetService("Players").LocalPlayer:WaitForChild("PlayerGui")
end)()
return screenGui

It’s probably best to only create GUI’s once PlayerGui is loaded though.

I believe all of its descendants are replicated, unless your script is one of those descendants and it’s being replicated to the playerGui.

All of its descendants are definitely already created on :Clone and WFC is unnecessary.

2 Likes

Ok I just want to do some extra clarifications to be 100% sure before I change everything:

Do you mean you have to recursively wfc all of its descendants or just the PlayerGui?

I mean after the game has loaded, like say the server parents something from a non replicated spot to a replicated spot and passes a reference to the client for it, once the client receives that reference is it safe to assume the entire object with all of its descendants have loaded? What about if no reference is passed and you’re using replicatedLocation.ChildAdded?

You probably need to wfc it’s descendants, unless you store your ScreenGui’s in ReplicatedStorage and clone them in.
It’s a little intense, but most of my gui’s are created programmatically.

The root of your issue is that you’re using ModuleScripts wrong.

ModuleScripts generally shouldn’t do things, they should allow you to do things. That is, you shouldn’t do this in your ModuleScript:

local gui = {}
local playerGui = -- wait for the player GUI
gui.Instance = makeGui(playerGui)
function gui:DoStuff()
    ... do stuff with gui.Instance
end
return gui

You should do this:

local gui = {}
function gui:Initialize(playerGui) 
    gui.Instance = makeGui(playerGui)
end
function gui:DoStuff()
    ... do stuff with gui.Instance
end
return gui

That way there’s no yields in your Modules, and they can all be initialized cleanly in whatever order the dependency tree mandates. Instead, in your root Script, you call off to the actual initialization that does the work of setting things up. The ModuleScripts just provide the interface for it to do so.

There are obviously exceptions to this (For instance, it’s plenty clean enough to have a Logging / error handling module connect to the LogService as part of it being required), but I can almost guarantee you that if you have to potentially yield, then your case isn’t one of those exceptions.

2 Likes

A little background on how my framework is structured (tell me if I’m doing something completely wrong/if there is a better way to do something please)

--[[
	Framework:
	
	Based on the MVC architectural pattern:
		https://en.wikipedia.org/wiki/Model–view–controller
		
--]]

--[[
	Organization:
	
	Separated into 3 categories:
		- Client: Stuff only the client uses (game.ReplicatedStorage)
		- Shared: Stuff both the client and server use (game.ReplicatedStorage)
		- Server: Stuff only the server uses (game.ServerStorage)
		
	Each of these 3 categories is further divided into 3 parts:
		- Classes: Abstract(ish) modules that are often reused
		- Model: Model handles the game logic and interacts with View (UI)
		- View:	View handles the UI and updates according to Model
		- Priority: A module that contains the Model and View modules orderered in the desired order to load them
	
--]]

--[[
	Loading Order:
	
	Modules are loaded in the following order:
		1. All Shared.Classes
		2. All Local.Classes
		3. Shared.Model and Shared.View loaded by order in Shared.Priority
		4. Local.Model and Local.View loaded by order in Local.Priority
	
--]]

--[[
	Shared state:
	
	If a module would like to use the shared environment between all other modules, it must add this line in its headers:
		local shared = require(game.Shared)
	
	
	Modules can then require each other like so:
		local otherModule = shared.OtherModule
	
--]]

Also modules do stuff themselves, there is no root script that controls and only calls other modules’ functions

So back to what you were saying,

I don’t do Initialize functions, I just have the initialization be automatically done when the script is required, should I be doing initialization?

And if so, whats the advantage?

Also if I were to do it, would the bootstrap script call the initialization after all of the modules (I assume its only called once)? (if so, when would the calling (or potentially the requiring other modules) be done though in order to avoid require collisions with uninitialized modules)

A possible solution to part of this I guess could be modules have two functions:
Initialize and Run

Initialize for all of the modules is called first and then run is, but modules would never be allowed to yield in their initialize stage if they are allowed to require each other in the initialize stage

Yes, this is a very bad idea.

  • One, because of the yielding issues that you’re seeing. If you yield in one of your ModuleScripts then you immediately hold up every single Module which directly or indirectly depends on that one until the yield completes, even if they could actually have run their initialization simultaneously.

  • Two, because there is no enforced initialization order. If you change what a given ModuleScript requires you may change your initialization order. You may change it even change it indirectly by changing some include further up the chain!

  • Three, because it cuts off the ability to use some parts of your system during your initialization. When you’re initializing your objects now only half of the code in your place has actually run so far, and it’s not even entirely obvious which half.

If your ModuleScript requires any non-trivial initialization (Basically any initialization which adds objects to the game hierarchy itself or touches objects in the hierarchy that some other script/module added), then you should have an explicit “initialization” function on that Module to do that initialization.

Ideally you have just one script which sets off the initialization process. That way you exactly control the initialization order. For the most part you call all the initializes from that script, but you don’t necessarily have to directly call all the initializes in that one script, it may be more clear / straight forwards to have some modules initialize call other modules initializes if there’s a really close dependency between them.

Also keep in mind that most modules shouldn’t need an initialize. Most of your modules should be providing pure utility code / Lua defined objects that / etc and not need any initialization of the Module itself.

Yes. You can even add an “initialized” variable in the Module as a sanity check to make sure you don’t accidentally misuse it.

There is no issue as long as you follow the rule that I posted: “Don’t have modulescripts do things, only have them allow you to do things.” Then none of the ModuleScripts will try to do anything until you tell them to do something (and you’ll only tell them to do something after you’ve called all the initialization code in the desired order)

2 Likes

I don’t have ANY non module scripts except for the bootstrap ones for server and client, should I still follow this rule (some of my modulescripts probably work the way non modulescripts work because they return nil)? - and should I abandon return nil modulescripts?

Also if modulescripts are running loops, (you probably are going to say they shouldn’t be, but for the sake of the question, please still assume they are going to LOL) and the loops need the module’s initialize function to have been called, should the loops be placed in the initialize function (and be ran on a separate thread to not infinite yield the function) or should they be wrapped in a separate thread in the module’s base environment (not in any function) and wait until the conditions necessary which will be fulfilled by the Initialize function have occurred or should there be a separate Run function that is called?

I do it this way too… which makes it a bit more nuanced than the dogmatic rule would suggest. Using an Object Oriented approach really helps in this regard, since it allows you to have a bunch of extensive well defined blocks of functionality in classes, and then “hook them together” using your one bootstrap script.

Here's the "main" class for my current project for an idea of how I do it:
local GameState = require(game.ReplicatedStorage.GameState)
local GameController = require(game.ReplicatedStorage.GameController)
local GameView = require(game.ReplicatedStorage.GameView)
local NetmapView = require(game.ReplicatedStorage.NetmapView)
local UnitInfoView = require(game.ReplicatedStorage.UnitInfoView)
local LocalPlayerData = require(game.ReplicatedStorage.LocalPlayerData)
local Places = require(game.ReplicatedStorage.Places)
local Netmap = require(game.ReplicatedStorage.Netmap)
local SoundManager = require(game.ReplicatedStorage.SoundManager)
local DialogueView = require(game.ReplicatedStorage.DialogueView)
local WarezView = require(game.ReplicatedStorage.WarezView)
local Scripts = require(game.ReplicatedStorage.Scripts)
local WindowsButton = require(game.ReplicatedStorage.WindowsButton)
local MainMenuView = require(game.ReplicatedStorage.MainMenuView)

local UserInputService = game:GetService('UserInputService')

local UglyTutorialNonsense = require(script.UglyTutorialNonsense)

local MainView = {}

-- Can't use the proper API because I don't want to wait... ARGHH
local ExtraY = 36 --game:GetService('GuiService'):GetGuiInset()

function MainView.getSize()
	local Mouse = game.Players.LocalPlayer:GetMouse()
	--if UserInputService.TouchEnabled then
	return UDim2.new(0, math.min(800, Mouse.ViewSizeX), 0, math.min(600, Mouse.ViewSizeY + 2*ExtraY))
end

function MainView.new()
	local this = {}
	
	-- Make root GUI
	local mGui = Instance.new('Frame')
	mGui.BackgroundColor3 = Color3.new(0, 0, 0)
	mGui.Size = MainView.getSize()
	mGui.Position = UDim2.new(0.5, 0, 0.5, -ExtraY/2)
	mGui.AnchorPoint = Vector2.new(0.5, 0.5)
	mGui.BorderSizePixel = 0
	local topFill = Instance.new('Frame')
	topFill.Name = 'TopFill'
	topFill.BackgroundColor3 = Color3.new(0, 0, 0)
	topFill.Position = UDim2.new(0, 0, 0, -ExtraY)
	topFill.Size = UDim2.new(1, 0, 0, ExtraY)
	topFill.BorderSizePixel = 0	
	--topFill.Parent = mGui
	function this:GetGui()
		return mGui;
	end
	
	-- Make menu
	local mMainMenu = MainMenuView.new(mGui)
	local mMenuButton = script.MenuButton:Clone()
	WindowsButton.new(mMenuButton)
	mMenuButton.Parent = mGui
	mMenuButton.MouseButton1Click:connect(function()
		print("Open main menu")
		mMainMenu:Show()
	end)
	
	-- Make netmap
	local mNetmapView = NetmapView.new()
	mNetmapView:GetGui().Position = UDim2.new(0.5, 0, 0.5, 0)
	mNetmapView:GetGui().Parent = mGui

	-- Give the netmap a unitInfoView
	local mUnitInfo = UnitInfoView.new(mNetmapView:GetGui(), LocalPlayerData:GetProgramList())
	mUnitInfo:SetProgramListVisible(false)
	mUnitInfo:ClearSelectedUnit()
	
	-- Dialogue
	local mDialogue = DialogueView.new()
	mDialogue:SetVisible(false)
	mDialogue:GetGui().Parent = mGui
	
	-- Setup events
	mNetmapView.NodeSelected:connect(function(nodeId)
		if LocalPlayerData:CanAccessNode(nodeId) then
			local node = Netmap.ById[nodeId]
			if node.Id == 'hq' then
				-- Special behavior
				
			elseif node.Warez then
				-- Visit warez node
				this:VisitWarez(nodeId)
			else
				-- Do the battle
				this:PlayGame(node.PlaceId, nodeId)
			end
		end
	end)
	
	-- Warez node
	function this:VisitWarez(nodeId)
		this:ProcessWonBattle(nodeId, 0) -- make it visible / beaten
		
		-- Show the GUI and handle the events for it
		local warezGui = WarezView.new(nodeId, Netmap.ById[nodeId].Warez)
		warezGui.MadePurchase:connect(function(id)
			mNetmapView:UpdateCreditDisplay()
			this:ShowNotification("Acquired program "..Scripts[id].Name)
		end)
		warezGui.Done:connect(function()
			warezGui:GetGui().Parent = nil
		end)
		warezGui:GetGui().Parent = mGui
	end
	
	-- Play the tutorial dialogue and tutorial
	function this:PlayTutorial()
		UglyTutorialNonsense:PlayTutorial(mGui, mNetmapView, mDialogue)
		this:ProcessWonBattle('hq', 1000)
		game.ReplicatedStorage.Remotes.BeatTutorial:FireServer()
	end
	
	-- Play a game at a place
	function this:PlayGame(placeId, nodeId)
		local placeData = Places[placeId]
		if not placeData then
			error("Missing place "..placeId)
		end
		local gameState = GameState.new(placeData, LocalPlayerData:GetProgramList(), GameState.ClientDelayFunc)
		local gameController = GameController.new(gameState)		
		local gameView = GameView.new(gameState, gameController)
		
		-- Restore the main state when the game is over
		gameView.CloseGame:connect(function(didWin, replay)
			mNetmapView:GetGui().Visible = true
			SoundManager:Play('MainBackgroundLoop')
			gameView:Destroy()
			
			-- Did we win?
			if didWin then
				this:ProcessWonBattle(nodeId, gameState:GetCreditsEarned())
			end
		end)
		
		gameView:getGui().Position = UDim2.new(0.5, 0, 0.5, 0)
		gameView:getGui().Parent = mGui
		mNetmapView:GetGui().Visible = false
		SoundManager:Stop('MainBackgroundLoop')
	end
	
	-- We won a battle, process the node revealing, and
	-- play through the win-triggers for that node
	function this:ProcessWonBattle(nodeId, creditsCollected)
		-- Get credits
		LocalPlayerData:AddCredits(creditsCollected)
		mNetmapView:UpdateCreditDisplay()
		
		-- Did we already beat this node?
		if LocalPlayerData:HasBeatenNode(nodeId) then
			return -- Don't update the node or play the win triggers again
		end		
		
		-- Update the state
		LocalPlayerData:SetNodeBeaten(nodeId)
		mNetmapView:SetNodeBeaten(nodeId)
		
		-- Handle the win-triggers
		local conversation = Netmap.ById[nodeId].Conversation
		if conversation then
			-- Show it
			mDialogue:SetVisible(true)
			mDialogue:ExecuteConversation(conversation)
			mDialogue:SetVisible(false)
			
			-- Process the result
			if conversation.Function then
				local f = conversation.Function
				if f.Type == 'revealNode' then
					LocalPlayerData:RevealNode(f.Id)
					mNetmapView:SetNodeVisible(f.Id)
					mNetmapView:HighlightNode(f.Id)
				elseif f.Type == 'upgradeSecurity' then
					LocalPlayerData:SetSecurityLevel(f.Level)
					-- We need to call again to make sure that the nodes that are now accessible appear that way
					mNetmapView:SetNodeBeaten(nodeId)
					this:ShowNotification("Upgraded security level to "..f.Level)
				elseif f.Type == 'getProgram' then
					LocalPlayerData:AddUnit(f.Id)
					this:ShowNotification("Received program: "..Scripts[f.Id].Name)
				elseif f.Type == 'getCredits' then
					LocalPlayerData:AddCredits(f.Amount)
					mNetmapView:UpdateCreditDisplay()
					this:ShowNotification("Received credits: "..f.Amount)
				elseif f.Type == 'beginNightfall' then
					LocalPlayerData:SetSecurityLevel(5)
					mNetmapView:SetNodeBeaten('ph45') -- Special case, do this to show the access to the boss node
					-- TODO:
				elseif f.Type == 'endNightfall' then
					-- TODO:
				else
					error("Bad function type: "..tostring(f.Type))
				end
			end
		end
	end
	
	local NotificationBoxTween = TweenInfo.new(1.3, Enum.EasingStyle.Quint, Enum.EasingDirection.Out, 0, true)
	function this:ShowNotification(text)
		local box = script.NotificationBox:Clone()
		box.Inset.Content.Text = text
		box.Parent = mNetmapView:GetGui()
		local TweenService = game:GetService('TweenService')
		local inAnim = TweenService:Create(box, NotificationBoxTween, {
			Position = UDim2.new(1, -24, 1, -10);
		})
		inAnim:Play()
		inAnim.Completed:connect(function()
			box:Destroy()
		end)
	end
	
	SoundManager:Play('MainBackgroundLoop')
	
	local function handleError(title, body)
		local gui = script.ErrorBox:Clone()
		gui.Content.Text = title.."\n"..body
		gui.CloseButton.MouseButton1Click:connect(function()
			gui:Destroy()
		end)
		gui.Parent = mGui
	end
	game:GetService('LogService').MessageOut:connect(function(message, messageType)
		if messageType == Enum.MessageType.MessageError then
			handleError("A CLIENT ERROR OCURRED - Please screenshot this and send it to Stravant", message)
		end
	end)
	game.ReplicatedStorage.Remotes.ServerError.OnClientEvent:connect(function(message)
		handleError("A SERVER ERROR OCURRED - Please screenshot this and send it to Stravant", message)
	end)
	
	if not LocalPlayerData:HasBeatenNode('hq') then
		spawn(function()
			this:PlayTutorial()
		end)
	end
	
	return this
end

return MainView

Absolutely. These are the worst thing I routinely see people doing on Roblox, it totally violates the contract of what a ModuleScript is supposed to be, there is no reason for these to not either use Initialization functions or just be normal scripts.

Yes, you should obviously start them in the initialize function using Spawn(). You could even have an entirely separate “initialization step” of Run() calls on your Modules that need it for starting your long running loops.

1 Like

With regards to when :WaitForChild is necessary, I bookmarked a post by which cleared up so much stuff.

https://devforum.roblox.com/t/multiple-levels-in-waitforchild-looking-for-feedback/90528/41?u=xaxa
1 Like