Generally Useful functions inside one ModuleScript

For a while, I’ve been running into slight problems that, when I solve them, I put into a module script I like to call “ObjectFuncs.” The goal of this module script is to make generalized tasks, such as finding out if a chosen part is the descendant of another part that you know the name of, a heck of a lot easier with just one function. So, here is my module script for all o’ y’all to use:

local module = {}


local RunService = game:GetService("RunService")

-- Condenses all the things you could want about Instance.new() into a single line
function module.ShorthandNew(TypeName:string|Instance,Parent:Instance,ObjName:string,PresetValues:{[string]: any}?):Instance
	-- Create a new object, go through PresetValues list, and set the value of the new Object to that preset value
	local NewObj:Instance --= Instance.new(TypeName)
	if type(TypeName) == "string" then
		NewObj = Instance.new(TypeName)
	else
		NewObj = TypeName:Clone()
	end
	NewObj.Name = ObjName
	if PresetValues ~= nil then
		for i,v in pairs(PresetValues) do
			NewObj[i] = v
		end
	end
	NewObj.Parent = Parent

	return NewObj
end

-- Auto deletes the given Instance after WaitTime. Sets the parent of the AutoDeleteScript to the given Parent. It does not delete the script's parent if the parent is different from the target object
function module.CreateAutoDelete(Obj:Instance,Parent:Instance,WaitTime:number)
	local Scr = game.ReplicatedStorage.InGameStuff.AutoDeleteScript:Clone()
	Scr.Object.Value = Obj
	Scr.WaitTime.Value = WaitTime
	Scr.Parent = Parent
end

-- Does not overwrite original list. Goes through given list and removes from the list all 
-- items that are not parts of FilterClass or FilterName, or items that ARE parts of 
-- ExcludeClass and ExcludeName. Recommended to only use either: FilterClass, FilterName, FilterClass
-- and Exclude Name, or FilterName and ExcludeClass.
function module.FilterTable(Table:{Instance},FilterClass:{string}?,ExcludeClass:{string}?,FilterName:{string}?,ExcludeName:{string}?):{Instance}
	local List = table.clone(Table)
	local AmountRemoved = 0

	local function CheckClass(Obj:Instance)
		for i2,v2 in pairs(FilterClass) do
			if Obj:IsA(v2) then
				if ExcludeName then
					if not table.find(ExcludeName,Obj.Name) then
						return true
					else
						return false
					end
				end
				return true
			end
		end

		return false
	end

	local function CheckName(Obj:Instance)
		for i2,v2 in pairs(FilterName) do
			if Obj.Name == v2 then
				if ExcludeClass then
					if not table.find(ExcludeClass,Obj.Name) then
						return true
					else
						return false
					end
				end
				return true
			end
		end

		return false
	end


	for i,v in pairs(Table) do
		local ItemWanted = false

		if FilterClass ~= nil and FilterName == nil then
			-- Only check for class and not name
			if CheckClass(v) then
				ItemWanted = true
			end
		elseif FilterClass ~= nil and FilterName ~= nil then
			-- Check for both Class and Name
			if CheckClass(v) and CheckName(v) then
				ItemWanted = true
			end
		elseif FilterClass == nil and FilterName ~= nil then
			-- Only check for Name and not class
			if CheckName(v) then
				ItemWanted = true
			end
		end


		if ItemWanted == false then
			table.remove(List,i - AmountRemoved)
			AmountRemoved += 1
		end

	end

	return List
end

-- Calls Instance:Destroy() if the given Object exists
function module.SafeDestroy(Obj:Instance?)
	if Obj then
		Obj:Destroy()
	end
end

-- Combines SafelyGetProperty and SafelyGetChild
function module.SafelyGetPropertyFromChild(Parent:Instance,ChildName:string|{string},PropertyName:string,ReturnValIfNil:any?):any?
	if ReturnValIfNil == nil then
		ReturnValIfNil = nil
	end

	if not Parent then return ReturnValIfNil end

	local Child:Instance
	if type(ChildName) == "table" then
		local ParentOn:Instance = Parent
		for i,v in pairs(ChildName) do
			if i == #ChildName then
				-- On the last item
				Child = ParentOn:FindFirstChild(tostring(v))
				if not Child then
					return ReturnValIfNil
				end
				break
			else
				Child = ParentOn:FindFirstChild(tostring(v))
				if not Child then
					return ReturnValIfNil
				end
				ParentOn = Child
			end
		end
	else
		if Parent then
			Child = Parent:FindFirstChild(tostring(ChildName))
		else
			return ReturnValIfNil
		end

	end

	if Child then 
		local Val = pcall(function() if Child[PropertyName] then return Child[PropertyName] end end)
		if Val then
			return Child[PropertyName]
		else
			return ReturnValIfNil
		end
	else
		return ReturnValIfNil
	end
end

-- Returns the value of the property of the given object. Returns DefaultReturnVal if the object or property doesn't exist.
function module.SafelyGetProperty(Obj:Instance,PropertyName:string,DefaultReturnValIfNil:any?):any?
	if Obj == nil then 
		return DefaultReturnValIfNil 
	else
		local Val = Obj[PropertyName]
		if Val then
			return Val
		else
			return DefaultReturnValIfNil 
		end
	end
end

-- basically FindFirstChild():FindFirstChild():FindFirstChild but it doesn't break if one of them returns nil
function module.SafelyGetChild(Parent:Instance,ChildName:string|{string}):Instance
	local Child:Instance
	if not Parent then return end -- Parent didn't even exist, lol
	if type(ChildName) == "table" then
		local ParentOn:Instance = Parent
		for i,v in pairs(ChildName) do
			if i == #ChildName then
				-- On the last item
				Child = ParentOn:FindFirstChild(v)
				if not Child then
					return nil
				end
				break
			else
				Child = ParentOn:FindFirstChild(v)
				if not Child then
					return nil
				end
				ParentOn = Child
			end
		end
	else
		Child = Parent:FindFirstChild(ChildName)
	end
	return Child
end

-- Condenses raycasting to one line so your own code doesn't look so long.
-- ColGroup = name of the collision group the raycast is a part of
function module.ShorthandRaycast(StartPos:Vector3,Direction:Vector3,ExcludeGroup:{Instance},ColGroup:string,RespectCanCollide:boolean):RaycastResult
	local RayParams = RaycastParams.new()
	RayParams.CollisionGroup = ColGroup
	RayParams.FilterDescendantsInstances = ExcludeGroup
	RayParams.RespectCanCollide = RespectCanCollide
	local Cast = game.Workspace:Raycast(StartPos,Direction,RayParams)
	if Cast then
		return Cast
	else
		-- make my own RaycastResult
		local Result:RaycastResult = {}
		Result.Distance = Direction.Magnitude
		Result.Instance = nil
		Result.Material = Enum.Material.Air
		Result.Position = StartPos + Direction
		Result.Normal = (StartPos + Direction).Unit
		return Result
	end
end

-- Adds a space  for every for every capital letter. Ex: "HelloThereBro" becomes "Hello There Bro"
function module.ConvertStringToDisplay(Str:string):string
	local NewString = ""
	for i=1, #Str, 1 do
		local Letter = string.sub(Str,i,i)
		if Letter == string.upper(Letter) then
			-- Letter is a capital letter
			NewString = NewString .." " ..Letter
		else
			NewString = NewString ..Letter
		end
	end
	return NewString
end

-- If DecimalPlace = 0.1, then truncate Num to the 0.1s place. ex: 0.583 becomes 0.5
function module.CutoffDecimal(Num:number,DecimalPlace:number):number
	local NewNum:number = math.floor(Num * (1 / DecimalPlace)) / (1 / DecimalPlace)

	return NewNum
end

-- this function adds commas where there needs to be them
function module.BeautifyNumber(InputNum:number|string):string

	local Num = tostring(InputNum)
	local FinalStr_Backwards = ""
	local GroupNumOn = 0

	for i=0, #Num-1,1 do
		local char = string.sub(Num,#Num-i,#Num-i)
		GroupNumOn = GroupNumOn + 1
		if GroupNumOn >= 3 and i ~= #Num-1 then
			FinalStr_Backwards = FinalStr_Backwards ..char ..","
			GroupNumOn = 0
		else
			FinalStr_Backwards = FinalStr_Backwards ..char
		end
	end

	-- Now, swap all the characters backwards
	local FinalStr = ""
	for i=0,#FinalStr_Backwards-1,1 do
		FinalStr = FinalStr ..string.sub(FinalStr_Backwards,#FinalStr_Backwards-i,#FinalStr_Backwards-i)
	end

	return FinalStr
end

-- Asks if the given Object is a descendant of another object with the given name. If true, returns that parent Object.  MaxTries is the max number of Parent Tries that will occur to try to check the bool
function module.IsDescendantOfName(Obj:Instance,ParentName:string,MaxTries:number):Instance?
	local ObjOn:Instance = Obj
	if Obj == nil then return nil end
	for i=0,MaxTries,1 do
		if ObjOn["Parent"] == nil then
			return nil
		end

		ObjOn = Obj.Parent
		if ObjOn.Name == ParentName then
			return ObjOn
		elseif ObjOn == game.Workspace then
			return nil
		end
	end
	return nil
end

-- Useful if you want to get a list of, for example, all the positions of a list of parts
function module.GetPropertyValuesFromTable(List:{any},PropertyName:string):{any}
	local FinalList = {}
	for i,v in pairs(List) do
		local Val = v[PropertyName]
		if Val then
			FinalList[#FinalList + 1] = v[Val]
		end
	end

	return FinalList
end

-- Sets the given property of all Instances in the given table to the same given value
function module.MassSetProperty(Table:{Instance},PropName:string,Val:any)
	for i,v in pairs(Table) do
		if v[PropName] then
			v[PropName] = Val
		end
	end

end

--[[
	Turns a TweenInfo object into a table so it can be passed as an Event argument
]]
function module.CompressTweenInfo(Info:TweenInfo)
	local NewInfo = {}

	NewInfo.Time = Info.Time
	NewInfo.EasingDirection = Info.EasingDirection
	NewInfo.EasingStyle = Info.EasingStyle
	NewInfo.Reverses = Info.Reverses
	NewInfo.DelayTime = Info.DelayTime
	NewInfo.RepeatCount = Info.RepeatCount

	return NewInfo
end

-- Changes a single coordinate to the value of ChangeTo inside a Vector3. Does not overwrite original given Vector.
-- Instead, returns a copy of the given vector with the changed coordinate.
function module.ChangeCoordinateFromVector3(Vector:Vector3,ChangeTo:number,Coord:string)
	if not Vector then return end
	if Coord == "x" then
		return Vector3.new(ChangeTo,Vector.y,Vector.z)
	elseif Coord == "y" then
		return Vector3.new(Vector.x,ChangeTo,Vector.z)
	elseif Coord == "z" then
		return Vector3.new(Vector.x,Vector.y,ChangeTo)
	end
	return Vector
end

-- Will start at the given object, and keeps getting its parents until either it reaches the game, or it reaches an ancestor with EndParentName. Returns a list that does NOT include the EndParentName. MaxParentChecks defaults to 10. ReturnAsString defaults to false
function module.GetHierarchy(Obj:Instance,EndParentName:string,ReturnAsString:boolean,MaxParentChecks:number)
	MaxParentChecks = MaxParentChecks or 10
	local OverallTable:{string}|string = {Obj.Name}
	if ReturnAsString then
		OverallTable = Obj.Name
	end

	local LastObjOn:Instance = Obj
	for i=1, MaxParentChecks, 1 do
		if LastObjOn.Parent == nil then
			-- reached the end of the line. Presumably at game now
			if ReturnAsString == true then
				local StartPos = string.find(OverallTable,"Game/")
				local StringToAvoid = string.sub(OverallTable,StartPos,(StartPos + string.len("Game/")))
				OverallTable = string.sub(OverallTable,(StartPos + string.len("Game/")))
			else
				table.remove(OverallTable,table.find(OverallTable,"Game"))
			end

			break
		end

		if LastObjOn.Parent.Name == EndParentName then
			-- reached the end of the line
			break
		end

		LastObjOn = LastObjOn.Parent
		if ReturnAsString then
			OverallTable = LastObjOn.Name .. "/" ..OverallTable
		else
			table.insert(OverallTable,1,LastObjOn.Name)
		end

	end

	return OverallTable
end


-- Uses a descendant path starting at game and using "/" as the child/parent denominator. 
-- If total wait time exceeds MaxTimeout, returns nil. MaxTimeout defaults to 5 seconds.
-- Useful for when the client needs to wait for the entirey of a object hierarchy line
-- to exist.
function module.WaitToReplicateObj_WaitForChild(StartingParent:Instance,ObjPath:string,MaxTimeout:number)
	local ChildTable:{string} = string.split(ObjPath,"/")
	local ChildToReturn:Instance = nil
	MaxTimeout = MaxTimeout or 5
	local StartTime = time()
	local CurrentParent = StartingParent

	for i,v in pairs(ChildTable) do
		--print("Waiting for " ..v)
		ChildToReturn = CurrentParent:WaitForChild(v,MaxTimeout - (time() - StartTime))
		if not ChildToReturn then
			return nil
		end

		CurrentParent = ChildToReturn
	end

	print(ChildToReturn)
	return ChildToReturn
end

-- Waits until the Property of the given object, specified by PropName, becomes any value that is NOT ValToAvoid. 
-- Timeout defaults to -1 which is an indefinite wait.
function module.WaitUntilPropertyIsNotDefault(Obj:Instance,PropName:string,ValToAvoid:any,Timeout:number)
	if not Timeout then Timeout = -1 end
	local StartTime = time()
	repeat
		if Timeout > 0 then
			if time() - StartTime >= Timeout then
				break
			end
		end
		RunService.Stepped:Wait()
	until Obj[PropName] ~= ValToAvoid
end

-- Disconnects an event connection from a function. Does not break
-- if the script has already disconnected, or if the connection ceases
-- to exist.
function module.SafelyDisconnect(Con:RBXScriptConnection)
	if Con then
		if Con.Connected == true then
			Con:Disconnect()
		end
	end
end
-- Only runs on client. Relies on the Player:HasAppearanceLoaded() boolean function.
function module.WaitForPlayerCharacterToLoad(Player)
	if not Player and RunService:IsClient() then
		Player = game.Players.LocalPlayer
	end
	
	while not Player:HasAppearanceLoaded() do
		print("Player: " ..tostring(Player:HasAppearanceLoaded()))
		RunService.Stepped:Wait()
	end
	
	--if not Player:HasAppearanceLoaded() then
	--	Player.CharacterAppearanceLoaded:Wait()
	--end
end

return module