Help with not repeating myself

So I have these areas (that are each parts) where only members of a certain team can use F3X building tools. The brickcolor of each area determines which team is allowed to build in that area. Here are the scripts that do this (both are modulescripts):

Building Tools > Core > Security (edited):

-- Services
MarketplaceService = game:GetService 'MarketplaceService';
HttpService = game:GetService 'HttpService';

-- References
Tool = script.Parent.Parent
Libraries = Tool:WaitForChild 'Libraries'
Support = require(Libraries:WaitForChild 'SupportLibrary')
RegionModule = require(Libraries:WaitForChild 'Region')

-- Determine whether we're in tool or plugin mode
local ToolMode = (Tool.Parent:IsA 'Plugin') and 'Plugin' or 'Tool'

-- Initialize the security module
Security = {};

-- The distance above the area-defining part that counts as part of the area
Security.AreaHeight = 0;

-- Whether to allow building outside of private areas
Security.AllowPublicBuilding = false;

-- Allowed locations in the hierarchy (descendants of which are authorized)
Security.AllowedLocations = { workspace };

-- Track the enabling of areas
Security.Areas = workspace:FindFirstChild('PrivateBuildingAreas');
workspace.ChildAdded:Connect(function (Child)
	if not Security.Areas and Child.Name == 'PrivateBuildingAreas' then
		Security.Areas = Child;
	end;
end);
workspace.ChildRemoved:Connect(function (Child)
	if Security.Areas and Child.Name == 'PrivateBuildingAreas' then
		Security.Areas = nil;
	end;
end);

function Security.IsAreaAuthorizedForPlayer(Area, Player)
	-- Returns whether `Player` has permission to manipulate parts in this area

	-- Ensure area has permissions
	local Permissions = Area:FindFirstChild 'Permissions';
	if not Permissions then
		return;
	else
		Permissions = require(Permissions);
	end;

	-- Ensure permissions are set up
	if not Permissions then
		return;
	end;

	-- Search for authorizing permission
	for _, Permission in pairs(Permissions) do

		-- Check group permissions
		if Permission.Type == 'Group' then

			-- Check player's group membership
			local PlayerInGroup = Player:IsInGroup(Permission.GroupId);

			-- If no specific rank is required, authorize
			if PlayerInGroup and not Permission.Ranks then
				return true;

			-- If specific rank is required, check player rank
			elseif PlayerInGroup and Permission.Ranks then
				local Symbol, RankNumber = tostring(Permission.Ranks):match('([<>]?=?)([0-9]+)');
				local PlayerRank = Player:GetRankInGroup(Permission.GroupId);
				RankNumber = tonumber(RankNumber);

				-- Check the player rank
				if not Symbol and (PlayerRank == RankNumber) then
					return true;
				elseif Symbol == '=' and (PlayerRank == RankNumber) then
					return true;
				elseif Symbol == '>' and (PlayerRank > RankNumber) then
					return true;
				elseif Symbol == '<' and (PlayerRank < RankNumber) then
					return true;
				elseif Symbol == '>=' and (PlayerRank >= RankNumber) then
					return true;
				elseif Symbol == '<=' and (PlayerRank <= RankNumber) then
					return true;
				end;
			end;

		-- Check player permissions
		elseif Permission.Type == 'Player' then
			if (Player.userId == Permission.PlayerId) or (Player.Name == Permission.PlayerName) then
				return true;
			end;

		-- Check owner permissions
		elseif Permission.Type == 'Owner' then
			if (Player.userId == Permission.PlayerId) or (Player.Name == Permission.PlayerName) then
				return true;
			end;

		-- Check auto-permissions
		elseif Permission.Type == 'Anybody' then
			return true;

		-- Check friend permissions
		elseif Permission.Type == 'Friends' then
			if Player:IsFriendsWith(Permission.PlayerId) then
				return true;
			end;

		-- Check asset permissions
		elseif Permission.Type == 'Asset' then
			if MarketplaceService:PlayerOwnsAsset(Player, Permission.AssetId) then
				return true;
			end;

		-- Check team permissions
		elseif Permission.Type == 'Team' then
			if Permission.Team and Player.Team == Permission.Team then
				return true;
			elseif Permission.TeamColor and Player.Team and Player.Team.TeamColor == Permission.TeamColor then
				return true;
			elseif Permission.TeamName and Player.Team and Player.Team.Name == Permission.TeamName then
				return true;
			end;
		
		-- Check BC permissions
		elseif Permission.Type == 'NoBC' then
			if Player.MembershipType == Enum.MembershipType.None then
				return true;
			end;
		elseif Permission.Type == 'AnyBC' then
			if Player.MembershipType ~= Enum.MembershipType.None then
				return true;
			end;
		elseif Permission.Type == 'BC' then
			if Player.MembershipType == Enum.MembershipType.BuildersClub then
				return true;
			end;
		elseif Permission.Type == 'TBC' then
			if Player.MembershipType == Enum.MembershipType.TurboBuildersClub then
				return true;
			end;
		elseif Permission.Type == 'OBC' then
			if Player.MembershipType == Enum.MembershipType.OutrageousBuildersClub then
				return true;
			end;

		-- Check custom permissions
		elseif Permission.Type == 'Callback' then
			return Permission.Callback(Player);
		end;

	end;

	-- If the player passes none of these conditions, deny access
	return false;
end;

function Security.IsItemAllowed(Item, Player)
	-- Returns whether instance `Item` can be accessed

	-- Ensure `Item` is a part or a model
	local IsItemClassAllowed = (Item:IsA 'BasePart' and not Item:IsA 'Terrain') or
		(Item:IsA 'Model' and not Item:IsA 'Workspace') or
		Item:IsA 'Folder' or
		Item:IsA 'Smoke' or
		Item:IsA 'Fire' or
		Item:IsA 'Sparkles' or
		Item:IsA 'DataModelMesh' or
		Item:IsA 'Decal' or
		Item:IsA 'Texture' or
		Item:IsA 'Light'
	if not IsItemClassAllowed then
		return false
	end

	-- Check if `Item` descendants from any allowed location
	for _, AllowedLocation in pairs(Security.AllowedLocations) do
		if Item:IsDescendantOf(AllowedLocation) then
			return true
		end
	end

	-- Deny if `Item` is not a descendant of any allowed location
	return false

end

function Security.IsLocationAllowed(Location, Player)
	-- Returns whether location `Location` can be accessed

	-- Check if within allowed locations
	for _, AllowedLocation in pairs(Security.AllowedLocations) do
		if (AllowedLocation == Location) or Location:IsDescendantOf(AllowedLocation) then
			return true
		end
	end

	-- Deny if not within allowed locations
	return false
end

function Security.AreAreasEnabled()
	-- Returns whether areas are enabled

	-- Base whether areas are enabled depending on area container presence and tool mode
	if Security.Areas and (ToolMode == 'Tool') then
		return true;
	else
		return false;
	end;
end;

function Security.ArePartsViolatingAreas(Parts, Player, ExemptPartial, AreaPermissions)
	-- Returns whether the given parts are inside any unauthorized areas

	-- Make sure area security is being enforced
	if not Security.AreAreasEnabled() then
		return false;
	end;

	-- If no parts, no violations exist
	if not next(Parts) then
		return false
	end

	-- Make sure there is a permissions cache
	AreaPermissions = AreaPermissions or {};

	-- Check which areas the parts are in
	local Areas, RegionMap = Security.GetSelectionAreas(Parts, true);

	-- Check authorization for each relevant area
	for _, Area in pairs(Areas) do

		-- Determine authorization if not in given permissions cache
		if AreaPermissions[Area] == nil then
			AreaPermissions[Area] = Security.IsAreaAuthorizedForPlayer(Area, Player);
		end;

		-- If unauthorized and partial violations aren't exempt, declare violation
		if not ExemptPartial and AreaPermissions[Area] == false then
			return true;
		end;

		-- If authorized and partial violations are allowed, check if all parts match area
		if ExemptPartial and AreaPermissions[Area] then

			-- Get parts matched to this area
			for Region, RegionParts in pairs(RegionMap) do
				if Region.Area == Area then

					-- If all parts are on this authorized area, call off any violation
					if Support.CountKeys(Parts) == #RegionParts then
						return false
					end

				end
			end

		end;

	end;

	-- If not in a private area, determine violation based on public building policy
	if #Areas == 0 then
		return not Security.AllowPublicBuilding;

	-- If authorization for a partial violation-exempt check on an area failed, indicate a violation
	elseif ExemptPartial then
		return true;

	-- If in authorized areas, determine violation based on public building policy compliance
	elseif RegionMap and not Security.AllowPublicBuilding then

		-- Check area residence of each part's corner
		local PartCornerCompliance = {};
		for AreaRegion, Parts in pairs(RegionMap) do
			for _, Part in pairs(Parts) do
				PartCornerCompliance[Part] = PartCornerCompliance[Part] or 0;

				-- Track the number of corners that `Part` has in this region
				for _, Corner in pairs(Support.GetPartCorners(Part)) do
					if AreaRegion:CastPoint(Corner.p) then
						PartCornerCompliance[Part] = PartCornerCompliance[Part] + 1;
					end;
				end;

			end;
		end;

		-- Ensure all corners of the part are contained within areas
		for _, CornersContained in pairs(PartCornerCompliance) do
			if CornersContained ~= 8 then
				return true;
			end;
		end;

	end;

	-- If no violations occur, indicate no violations
	return false;
end;

function Security.GetSelectionAreas(Selection, ReturnMap)
	-- Returns a list of areas that the selection of parts is in

	-- Make sure areas are enabled
	if not Security.AreAreasEnabled() then
		return {};
	end;

	-- Start a map if requested
	local Map = ReturnMap and {} or nil;

	-- Check each area to find out if any of the parts are within
	local Areas = {};
	for _, Area in pairs(Security.Areas:GetChildren()) do

		-- Get all parts from the selection within this area
		local Region = RegionModule.new(
			Area.CFrame * CFrame.new(0, Security.AreaHeight / 2 - Area.Size.Y / 2, 0),
			Vector3.new(Area.Size.X, Security.AreaHeight + Area.Size.Y, Area.Size.Z)
		);
		Region.Area = Area
		local ContainedParts = Region:CastParts(Selection);

		-- If parts are in this area, remember the area
		if #ContainedParts > 0 then
			table.insert(Areas, Area);

			-- Map out the parts for each area region
			if Map then
				Map[Region] = ContainedParts;
			end;
		end;

	end;

	-- Return the areas where any of the given parts exist
	return Areas, Map;
end;

function Security.GetPermissions(Areas, Player)
	-- Returns a cache of the current player's authorization to the given areas

	-- Make sure security is enabled
	if not Security.AreAreasEnabled() then
		return;
	end;

	-- Build the cache of permissions for each area
	local Cache = {};
	for _, Area in pairs(Areas) do
		Cache[Area] = Security.IsAreaAuthorizedForPlayer(Area, Player);
	end;

	-- Return the permissions cache
	return Cache;
end;

return Security;

Workspace > PrivateBuildingAreas (which is a folder) > every single building area > Permissions:

return {
	{
		Type = "Team",
		TeamColor = script.Parent.BrickColor
	}
}

However, if I do it this way, I’d be making one of the most notorious development mistakes: not complying with the DRY principle (Don’t Repeat Yourself). If I had to go back and change something, I’d have to do it for every single Permissions script manually. Plus, overuse of the same code would cause the data to eventually add up to create lag, as I predict that I need a lot of these building areas for my game. So what I’m basically looking for is a way to control all the building areas with only one script.

I didn’t really find any solutions to my particular problem anywhere else. I also tried using tags and rearranging the code, but I never seemed to get it working correctly.

1 Like

Where are you repeating yourself, exactly? It seems to me like your data is stored once: inside the permission area where its relevant. That seems like a pretty decent design IMO.

edit: If you wanted, you could check out Tags/CollectionService for marking a part as a “private area” (instead of needing to place it in a dedicated folder), and Instance Attributes if you’re looking for a more roblox-y way to attach data to your areas, but using both of these will not look much different than the way you’re doing it with folders and module scripts.

1 Like

What I mean is that each and every area has its own permissions script which are all the same, and I just want one copy of the permissions script that runs all of the areas.

I see! You should try out CollectionService. Instead of putting areas in a folder like that and controlling behavior with child modulescripts, you can tag parts as Areas and control behavior by either (a) making different tags for “PermissionFriends”, “PermissionOwner”, etc. or (b) using a single tag for “PermissionArea” and using Attributes to differentiate the different types.

I show an example of using tags to control behavior of many parts here: I don't get why I should use Collection Service - #4 by nicemike40

Let me know if you need more help applying that example to your specific problem!

How would I apply the second option, because I tried using tags before I made this post, but it always just broke the whole system. One of the things I tried was this:

Security script:

-- Services
MarketplaceService = game:GetService 'MarketplaceService';
HttpService = game:GetService 'HttpService';

-- References
Tool = script.Parent.Parent
Libraries = Tool:WaitForChild 'Libraries'
Support = require(Libraries:WaitForChild 'SupportLibrary')
RegionModule = require(Libraries:WaitForChild 'Region')

-- Determine whether we're in tool or plugin mode
local ToolMode = (Tool.Parent:IsA 'Plugin') and 'Plugin' or 'Tool'

-- Initialize the security module
Security = {};

-- The distance above the area-defining part that counts as part of the area
Security.AreaHeight = 0;

-- Whether to allow building outside of private areas
Security.AllowPublicBuilding = false;

-- Allowed locations in the hierarchy (descendants of which are authorized)
Security.AllowedLocations = { workspace };

-- Track the enabling of areas
Security.Areas = workspace:FindFirstChild('PrivateBuildingAreas');
workspace.ChildAdded:Connect(function (Child)
	if not Security.Areas and Child.Name == 'PrivateBuildingAreas' then
		Security.Areas = Child;
	end;
end);
workspace.ChildRemoved:Connect(function (Child)
	if Security.Areas and Child.Name == 'PrivateBuildingAreas' then
		Security.Areas = nil;
	end;
end);

function Security.IsAreaAuthorizedForPlayer(Area, Player)
	-- Returns whether `Player` has permission to manipulate parts in this area

	-- Ensure area has permissions
	local Permissions = workspace:FindFirstChild 'Permissions';
	if not Permissions then
		return;
	else
		Permissions = require(Permissions);
	end;

	-- Ensure permissions are set up
	if not Permissions then
		return;
	end;

	-- Search for authorizing permission
	for _, Permission in pairs(Permissions) do

		-- Check group permissions
		if Permission.Type == 'Group' then

			-- Check player's group membership
			local PlayerInGroup = Player:IsInGroup(Permission.GroupId);

			-- If no specific rank is required, authorize
			if PlayerInGroup and not Permission.Ranks then
				return true;

				-- If specific rank is required, check player rank
			elseif PlayerInGroup and Permission.Ranks then
				local Symbol, RankNumber = tostring(Permission.Ranks):match('([<>]?=?)([0-9]+)');
				local PlayerRank = Player:GetRankInGroup(Permission.GroupId);
				RankNumber = tonumber(RankNumber);

				-- Check the player rank
				if not Symbol and (PlayerRank == RankNumber) then
					return true;
				elseif Symbol == '=' and (PlayerRank == RankNumber) then
					return true;
				elseif Symbol == '>' and (PlayerRank > RankNumber) then
					return true;
				elseif Symbol == '<' and (PlayerRank < RankNumber) then
					return true;
				elseif Symbol == '>=' and (PlayerRank >= RankNumber) then
					return true;
				elseif Symbol == '<=' and (PlayerRank <= RankNumber) then
					return true;
				end;
			end;

			-- Check player permissions
		elseif Permission.Type == 'Player' then
			if (Player.userId == Permission.PlayerId) or (Player.Name == Permission.PlayerName) then
				return true;
			end;

			-- Check owner permissions
		elseif Permission.Type == 'Owner' then
			if (Player.userId == Permission.PlayerId) or (Player.Name == Permission.PlayerName) then
				return true;
			end;

			-- Check auto-permissions
		elseif Permission.Type == 'Anybody' then
			return true;

			-- Check friend permissions
		elseif Permission.Type == 'Friends' then
			if Player:IsFriendsWith(Permission.PlayerId) then
				return true;
			end;

			-- Check asset permissions
		elseif Permission.Type == 'Asset' then
			if MarketplaceService:PlayerOwnsAsset(Player, Permission.AssetId) then
				return true;
			end;

			-- Check team permissions
		elseif Permission.Type == 'Team' then
			if Permission.Team and Player.Team == Permission.Team then
				return true;
			elseif Permission.TeamColor and Player.Team and Player.Team.TeamColor == Permission.TeamColor then
				return true;
			elseif Permission.TeamName and Player.Team and Player.Team.Name == Permission.TeamName then
				return true;
			end;

			-- Check BC permissions
		elseif Permission.Type == 'NoBC' then
			if Player.MembershipType == Enum.MembershipType.None then
				return true;
			end;
		elseif Permission.Type == 'AnyBC' then
			if Player.MembershipType ~= Enum.MembershipType.None then
				return true;
			end;
		elseif Permission.Type == 'BC' then
			if Player.MembershipType == Enum.MembershipType.BuildersClub then
				return true;
			end;
		elseif Permission.Type == 'TBC' then
			if Player.MembershipType == Enum.MembershipType.TurboBuildersClub then
				return true;
			end;
		elseif Permission.Type == 'OBC' then
			if Player.MembershipType == Enum.MembershipType.OutrageousBuildersClub then
				return true;
			end;

			-- Check custom permissions
		elseif Permission.Type == 'Callback' then
			return Permission.Callback(Player);
		end;

	end;

	-- If the player passes none of these conditions, deny access
	return false;
end;

function Security.IsItemAllowed(Item, Player)
	-- Returns whether instance `Item` can be accessed

	-- Ensure `Item` is a part or a model
	local IsItemClassAllowed = (Item:IsA 'BasePart' and not Item:IsA 'Terrain') or
		(Item:IsA 'Model' and not Item:IsA 'Workspace') or
		Item:IsA 'Folder' or
		Item:IsA 'Smoke' or
		Item:IsA 'Fire' or
		Item:IsA 'Sparkles' or
		Item:IsA 'DataModelMesh' or
		Item:IsA 'Decal' or
		Item:IsA 'Texture' or
		Item:IsA 'Light'
	if not IsItemClassAllowed then
		return false
	end

	-- Check if `Item` descendants from any allowed location
	for _, AllowedLocation in pairs(Security.AllowedLocations) do
		if Item:IsDescendantOf(AllowedLocation) then
			return true
		end
	end

	-- Deny if `Item` is not a descendant of any allowed location
	return false

end

function Security.IsLocationAllowed(Location, Player)
	-- Returns whether location `Location` can be accessed

	-- Check if within allowed locations
	for _, AllowedLocation in pairs(Security.AllowedLocations) do
		if (AllowedLocation == Location) or Location:IsDescendantOf(AllowedLocation) then
			return true
		end
	end

	-- Deny if not within allowed locations
	return false
end

function Security.AreAreasEnabled()
	-- Returns whether areas are enabled

	-- Base whether areas are enabled depending on area container presence and tool mode
	if Security.Areas and (ToolMode == 'Tool') then
		return true;
	else
		return false;
	end;
end;

function Security.ArePartsViolatingAreas(Parts, Player, ExemptPartial, AreaPermissions)
	-- Returns whether the given parts are inside any unauthorized areas

	-- Make sure area security is being enforced
	if not Security.AreAreasEnabled() then
		return false;
	end;

	-- If no parts, no violations exist
	if not next(Parts) then
		return false
	end

	-- Make sure there is a permissions cache
	AreaPermissions = AreaPermissions or {};

	-- Check which areas the parts are in
	local Areas, RegionMap = Security.GetSelectionAreas(Parts, true);

	-- Check authorization for each relevant area
	for _, Area in pairs(Areas) do

		-- Determine authorization if not in given permissions cache
		if AreaPermissions[Area] == nil then
			AreaPermissions[Area] = Security.IsAreaAuthorizedForPlayer(Area, Player);
		end;

		-- If unauthorized and partial violations aren't exempt, declare violation
		if not ExemptPartial and AreaPermissions[Area] == false then
			return true;
		end;

		-- If authorized and partial violations are allowed, check if all parts match area
		if ExemptPartial and AreaPermissions[Area] then

			-- Get parts matched to this area
			for Region, RegionParts in pairs(RegionMap) do
				if Region.Area == Area then

					-- If all parts are on this authorized area, call off any violation
					if Support.CountKeys(Parts) == #RegionParts then
						return false
					end

				end
			end

		end;

	end;

	-- If not in a private area, determine violation based on public building policy
	if #Areas == 0 then
		return not Security.AllowPublicBuilding;

		-- If authorization for a partial violation-exempt check on an area failed, indicate a violation
	elseif ExemptPartial then
		return true;

		-- If in authorized areas, determine violation based on public building policy compliance
	elseif RegionMap and not Security.AllowPublicBuilding then

		-- Check area residence of each part's corner
		local PartCornerCompliance = {};
		for AreaRegion, Parts in pairs(RegionMap) do
			for _, Part in pairs(Parts) do
				PartCornerCompliance[Part] = PartCornerCompliance[Part] or 0;

				-- Track the number of corners that `Part` has in this region
				for _, Corner in pairs(Support.GetPartCorners(Part)) do
					if AreaRegion:CastPoint(Corner.p) then
						PartCornerCompliance[Part] = PartCornerCompliance[Part] + 1;
					end;
				end;

			end;
		end;

		-- Ensure all corners of the part are contained within areas
		for _, CornersContained in pairs(PartCornerCompliance) do
			if CornersContained ~= 8 then
				return true;
			end;
		end;

	end;

	-- If no violations occur, indicate no violations
	return false;
end;

function Security.GetSelectionAreas(Selection, ReturnMap)
	-- Returns a list of areas that the selection of parts is in

	-- Make sure areas are enabled
	if not Security.AreAreasEnabled() then
		return {};
	end;

	-- Start a map if requested
	local Map = ReturnMap and {} or nil;

	-- Check each area to find out if any of the parts are within
	local Areas = {};
	for _, Area in pairs(Security.Areas:GetChildren()) do

		-- Get all parts from the selection within this area
		local Region = RegionModule.new(
			Area.CFrame * CFrame.new(0, Security.AreaHeight / 2 - Area.Size.Y / 2, 0),
			Vector3.new(Area.Size.X, Security.AreaHeight + Area.Size.Y, Area.Size.Z)
		);
		Region.Area = Area
		local ContainedParts = Region:CastParts(Selection);

		-- If parts are in this area, remember the area
		if #ContainedParts > 0 then
			table.insert(Areas, Area);

			-- Map out the parts for each area region
			if Map then
				Map[Region] = ContainedParts;
			end;
		end;

	end;

	-- Return the areas where any of the given parts exist
	return Areas, Map;
end;

function Security.GetPermissions(Areas, Player)
	-- Returns a cache of the current player's authorization to the given areas

	-- Make sure security is enabled
	if not Security.AreAreasEnabled() then
		return;
	end;

	-- Build the cache of permissions for each area
	local Cache = {};
	for _, Area in pairs(Areas) do
		Cache[Area] = Security.IsAreaAuthorizedForPlayer(Area, Player);
	end;

	-- Return the permissions cache
	return Cache;
end;

return Security;

Permissions script (single modulescript under workspace):

local CollectionService = game:GetService("CollectionService")

for i, zone in pairs(CollectionService:GetTagged("BuildZone")) do
	return {
		{
			Type = "Team",
			TeamColor = zone.BrickColor
		}
	}
end