Check Badge Awards in Batches with CheckUserBadgesAsync

it did make my badge checking system much faster though ty :slight_smile:

1 Like

Wow this is really nice to see! Would something like this ever come out for MarketPlaceService for things like gamepasses?

2 Likes

Wow! This is a nice feature to have. The current game I co-own has 30 badges, so this could simplify things.

I can check up to 50 badges per minute per player based on rate limits. If combined with checking individual badges, you could do 50 + 35 = 85 badge checks per minute per player (In theory).

However, I do believe the limit of 10 should be increased to a number that popular games tend to have, which is at least 30 - 50 badges. This would also make it friendlier for new developers with limited scripting experience :slight_smile:

3 Likes

Win Update we created a custom system through our VPS so that’s one lesser costs. If we didn’t use it for other things as well. But i’m so happy with this. As our games are well known in the badge community for having a high amount of badges but also often troubles with loading it in with games that don’t have our custom system yet.

Edit: Seems like we can’t request multiple badge infos at once as well, so we can’t use it (according to the developer who handles this)

1 Like

meshpart users when they have to change MeshID using a script:

1 Like

Thank you! The fact that something like this didn’t exist before was definitely surprising, but better late than never. Excited to try out this new API!

On another note, can we expect later support for getting badge information and maybe higher request limits?

1 Like

I’m slightly annoyed I just rebuilt part of a game I had that used UserHasBadgeAsync to check for owned badges, but oh well. 10 is definitely an improvement, considering they all check simultaneously, but I hope it increases in the future…many badge hunt games, if not also using datastores, won’t find this very helpful.

1 Like

Haha I just added a feature like that to one of my games; I’m glad to see other people doing it, it seemed like a sensible idea :sweat_smile:

2 Likes

I just tested this, and I found that the UserHasBadgeAsync limit is 100 per minute, while AwardBadge is 85. I’m also pretty sure that each rate-limit is separate to the other.

I was pretty upset after seeing the maximum ID count, so I went ahead and did a “page” method as a workaround… Hoping this gets increased in the future.

For those who are curious, here you go :point_down:

local BadgeService = game:GetService("BadgeService")

local TargetUserId = 00000 -- The user Id that you want to scan for badges
local BadgeIdList = {00000,00000,00000,00000,00000} -- Insert badge IDs here

local OwnedBadgesResult = {} -- Final table that returns the status of the badges from the player

-- Generate pages for the function to successfully go through

local Pages = {}
local BadgeTableIndex = 0
local PageIndex = 1

for _,IndexBadgeId in pairs(BadgeIdList) do
	
	if Pages[PageIndex] == nil then -- Create new page
		Pages[PageIndex] = {}
	end
	
	OwnedBadgesResult[IndexBadgeId] = false
	table.insert(Pages[PageIndex],IndexBadgeId)
	
	if #Pages[PageIndex] >= 10 then -- Max page size
		PageIndex += 1
	end
	
end

-- Scan through the pages and return results

for _,Page in pairs(Pages) do
	
	local PageResult = BadgeService:CheckUserBadgesAsync(TargetUserId, Page)
	
	for _,IndexResult in pairs(PageResult) do
		OwnedBadgesResult[IndexResult] = true
	end
	
end

-- Finished! Check "OwnedBadgesResult"
1 Like

Yeah, when I think about it, it’s probably an even better solution. You can just load the UserData normally and include an extra BadgesOwned dictionary within the UserData. This way, there’s no need to make 5-100 API calls when a player joins or for certain Actions, you’d need zero. To award a badge, I simply check if the player doesn’t already own it by looking at the UserData, and directly after awarding a badge I update the UserData and mark the badge as owned

1 Like

Please keep in mind that your data may become stale if users delete badges or API calls fail as mentioned in my reply here so having a fail-safe is highly advised still.

1 Like

Thanks alot for the input!

- BadgeService / DatastoreService can go down. If this happens, you’ll be left with stale data.
I actually think this makes relying more on Datastore even more reasonable, as it reduces dependency on BadgeService. I’ve implemented a fallback in case :AwardBadge fails (as shown in my BadgeHandler function). If BadgeService were to go down in my game, only a few minor features, like a [BETA] chat tag, might be affected if I relied on it. However, if DatastoreService fails, players can’t load their owned items, which makes the game almost unplayable. In that scenario, even a fully functional BadgeService wouldn’t do much to help.

- Players can delete badges from their inventory. Ideally, you should be able to react to this and allow the player to re-earn the badge if they so wish.
I agree that this is an edge case not covered by saving badges in the datastore. But for example, in our game, badges aren’t critical, and we haven’t received any complaints about this issue. I understand that some players might delete badges for cosmetic reasons, to hide that they played a particular game, or maybe for other personal reasons?, which I don’t personally relate to. But there’s even a strange upside to stale data: if someone deletes a badge that can’t be earned anymore (whether by accident or because someone else messed with their account e.g. siblings), they won’t lose the benefits tied to that badge since it’s still marked as earned in the datastore. Another quirky upside is that if players delete badges to hide that they’ve played a particular game, they could re-enter the game without the badges awarding and having to be deleted again. :thinking:

As for the issue you mentioned in another experience, it might have been a logic mistake by the developer, perhaps they marked the badge as owned before the AwardBadge call was actually fired.

My Badgehandler Function (it’s not that cleaned up tbh):

local function handle_Badge(Plr, Type, Badge_Id)
	local UserId = Plr.UserId
	local Saved_Data = ProfileService:Get(Plr, "Badges")

	local Badge_Key = tostring(Badge_Id)

	if Saved_Data == nil then
		warn(script.Name..": SAVED DATA NIL") -- triggers when leaving game in certain moments
        return false
	end

	-- has_Badge
	if Type == "has_Badge" then
		local Saved_Value = Saved_Data[Badge_Key]

		if Saved_Value == nil then
			local Result = Retry(function()
				return BadgeService:UserHasBadgeAsync(UserId, Badge_Id)
			end)

			if not Plr.Parent then -- the above yields, so check if ingame
				return false
			end

			if Result == nil then
				warn(script.Name.." 1: RESULT == NIL")
				return false
			end

			-- Yield -> Racing Conditions
			local Saved_Data = ProfileService:Get(Plr, "Badges")
			Saved_Data[Badge_Key] = Result

			ProfileService:Set(Plr, "Badges", Saved_Data)
			return Result
		else
			--print("already known")
			return Saved_Value
		end

	elseif Type == "AwardBadge" then
		local Result = Retry(function()
			return BadgeService:AwardBadge(Plr.UserId, Badge_Id)
		end)

		--print(Result)
		if Result == nil then
			warn(script.Name.." 2: RESULT == NIL FOR AWARDING")
		end

		if not Plr.Parent then -- the above yields, so check if ingame
			return false
		end

		local Saved_Data = ProfileService:Get(Plr, "Badges")
		Saved_Data[Badge_Key] = Result -- If successful -> returns true

		ProfileService:Set(Plr, "Badges", Saved_Data)
	end

	-- check if success if already owns badge

	-- set_BadgeOwned
end
1 Like

I’ve just realized that my code might have a potential flaw. The AwardBadge method returns whether the badge was awarded successfully, so I should be also checking that return value.

However, I’m not entirely sure if this scenario ever occurs, since I remember AwardBadge typically throwing an error if it fails. But I’m not certain.

Nevermind, I just realized that by pure coincidence that my Retry function, already handles that flaw that I thought my code had:

I believe the game you had the problem in, could’ve also just wrapped it in a pcall and checked whether it failed without checking if the actual AwardBadge Method returned “true”

local function Retry(Function, MaxTries, WaitTime)
	local Tries = 0
	MaxTries = MaxTries or 5 -- Maximum Retries
	WaitTime = WaitTime or 5 -- Wait Time Between Retries

	local Success, Value

	repeat
		Success, Value = pcall(Function)

		if not Success then 
			warn(Value)
			Tries = Tries + 1 
			task.wait(WaitTime) 
		end

	until Tries >= MaxTries or Success

	if Success then 
		return Value
	else 
		return nil 
	end
end

Also I forgot to include the snippet that awards Badges via BindableEvents:

local function has_Badge(player, badgeId)
	-- Check if the player has the badge
	return Badge_Handler_BindableFunction:Invoke(player, "has_Badge", badgeId) 
end

local function awardBadge(player, badgeId)
	-- Check if the player already has the badge
	local hasBadge = has_Badge(player, badgeId)

	if hasBadge then
		return
	end
	
	Badge_Handler_BindableFunction:Invoke(player, "AwardBadge", badgeId)
end
1 Like

That’s an extremely niche use case and if you’re constantly doing that… then that’s probably bad practice. Store your assets in ReplicatedStorage.

I don’t think you’re aware of the ApplyMesh method:

2 Likes

Does this work too for making a player click another player that open the gui showing the badges of the player that have got?

1 Like