@quenty/playerthumbnailutils 10.20.0 → 10.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/deploy.nevermore.json +10 -0
- package/package.json +6 -3
- package/src/Shared/PlayerThumbnailUtils.lua +38 -3
- package/src/Shared/PlayerThumbnailUtils.spec.lua +81 -0
- package/src/jest.config.lua +3 -0
- package/test/default.project.json +17 -0
- package/test/scripts/Server/ServerMain.server.lua +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [10.21.1](https://github.com/Quenty/NevermoreEngine/compare/@quenty/playerthumbnailutils@10.21.0...@quenty/playerthumbnailutils@10.21.1) (2026-07-23)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @quenty/playerthumbnailutils
|
|
9
|
+
|
|
10
|
+
# [10.21.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/playerthumbnailutils@10.20.0...@quenty/playerthumbnailutils@10.21.0) (2026-07-23)
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
- Additional fixes ([8714d52](https://github.com/Quenty/NevermoreEngine/commit/8714d52ed996f2e41e6daa191554acca35860e36))
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
- Add baseline player-mock and support across Nevermore for mocked players. ([567d121](https://github.com/Quenty/NevermoreEngine/commit/567d121ffc014b42391554088189a1a6296dda83))
|
|
19
|
+
|
|
6
20
|
# [10.20.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/playerthumbnailutils@10.19.0...@quenty/playerthumbnailutils@10.20.0) (2026-07-18)
|
|
7
21
|
|
|
8
22
|
**Note:** Version bump only for package @quenty/playerthumbnailutils
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/playerthumbnailutils",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.21.1",
|
|
4
4
|
"description": "Reimplementation of Player:GetUserThumbnailAsync but as a promise with retry logic",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Roblox",
|
|
@@ -29,10 +29,13 @@
|
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@quenty/loader": "10.11.0",
|
|
32
|
-
"@quenty/
|
|
32
|
+
"@quenty/nevermore-test-runner": "1.5.0",
|
|
33
|
+
"@quenty/playermock": "1.1.1",
|
|
34
|
+
"@quenty/promise": "10.21.0",
|
|
35
|
+
"@quentystudios/jest-lua": "3.10.0-quenty.2"
|
|
33
36
|
},
|
|
34
37
|
"publishConfig": {
|
|
35
38
|
"access": "public"
|
|
36
39
|
},
|
|
37
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "5ea0583dedebba6492f9ac6d719d32b45c5543fa"
|
|
38
41
|
}
|
|
@@ -10,10 +10,19 @@ local require = require(script.Parent.loader).load(script)
|
|
|
10
10
|
|
|
11
11
|
local Players = game:GetService("Players")
|
|
12
12
|
|
|
13
|
+
local PlayerMock = require("PlayerMock")
|
|
13
14
|
local Promise = require("Promise")
|
|
14
15
|
|
|
15
16
|
local MAX_TRIES = 5
|
|
16
17
|
|
|
18
|
+
-- What each ThumbnailType is called in an rbxthumb:// content URL, which is the shape
|
|
19
|
+
-- GetUserThumbnailAsync itself resolves -- so a mock's thumbnail can derive from its UserId alone.
|
|
20
|
+
local THUMBNAIL_TYPE_TO_RBXTHUMB_TYPE = {
|
|
21
|
+
[Enum.ThumbnailType.HeadShot] = "AvatarHeadShot",
|
|
22
|
+
[Enum.ThumbnailType.AvatarBust] = "AvatarBust",
|
|
23
|
+
[Enum.ThumbnailType.AvatarThumbnail] = "Avatar",
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
local PlayerThumbnailUtils = {}
|
|
18
27
|
|
|
19
28
|
--[=[
|
|
@@ -35,17 +44,36 @@ function PlayerThumbnailUtils.promiseUserThumbnail(
|
|
|
35
44
|
thumbnailSize: Enum.ThumbnailSize?
|
|
36
45
|
): Promise.Promise<string>
|
|
37
46
|
assert(type(userId) == "number", "Bad userId")
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
local resolvedThumbnailType: Enum.ThumbnailType = thumbnailType or Enum.ThumbnailType.HeadShot
|
|
48
|
+
local resolvedThumbnailSize: Enum.ThumbnailSize = thumbnailSize or Enum.ThumbnailSize.Size100x100
|
|
40
49
|
|
|
41
50
|
local promise
|
|
42
51
|
promise = Promise.spawn(function(resolve, reject)
|
|
52
|
+
if PlayerMock.getMockByUserId(userId) ~= nil then
|
|
53
|
+
-- The engine call would reject a fake UserId; derive the rbxthumb content URL it
|
|
54
|
+
-- would resolve for a real one.
|
|
55
|
+
local width, height = string.match(resolvedThumbnailSize.Name, "^Size(%d+)x(%d+)$")
|
|
56
|
+
if width == nil or height == nil then
|
|
57
|
+
return reject(string.format("Failed to parse thumbnail size %q", resolvedThumbnailSize.Name))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
return resolve(
|
|
61
|
+
string.format(
|
|
62
|
+
"rbxthumb://type=%s&id=%d&w=%s&h=%s",
|
|
63
|
+
THUMBNAIL_TYPE_TO_RBXTHUMB_TYPE[resolvedThumbnailType],
|
|
64
|
+
userId,
|
|
65
|
+
width,
|
|
66
|
+
height
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
43
71
|
local tries = 0
|
|
44
72
|
repeat
|
|
45
73
|
tries = tries + 1
|
|
46
74
|
local content, isReady
|
|
47
75
|
local ok, err = pcall(function()
|
|
48
|
-
content, isReady = Players:GetUserThumbnailAsync(userId,
|
|
76
|
+
content, isReady = Players:GetUserThumbnailAsync(userId, resolvedThumbnailType, resolvedThumbnailSize)
|
|
49
77
|
end)
|
|
50
78
|
|
|
51
79
|
-- Don't retry if we immediately error (timeout exceptions!)
|
|
@@ -79,6 +107,13 @@ function PlayerThumbnailUtils.promiseUserName(userId: number): Promise.Promise<s
|
|
|
79
107
|
|
|
80
108
|
local promise
|
|
81
109
|
promise = Promise.spawn(function(resolve, reject)
|
|
110
|
+
local mockPlayer = PlayerMock.getMockByUserId(userId)
|
|
111
|
+
if mockPlayer ~= nil then
|
|
112
|
+
-- Resolved from the same user-info domain UserServiceUtils reads, so the two
|
|
113
|
+
-- packages' usernames can never disagree for a mock.
|
|
114
|
+
return resolve(PlayerMock.readLookup(mockPlayer, "UserService.GetUserInfosByUserIdsAsync", 0).Username)
|
|
115
|
+
end
|
|
116
|
+
|
|
82
117
|
local tries = 0
|
|
83
118
|
repeat
|
|
84
119
|
tries = tries + 1
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
@class PlayerThumbnailUtils.spec.lua
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local Workspace = game:GetService("Workspace")
|
|
9
|
+
|
|
10
|
+
local Jest = require("Jest")
|
|
11
|
+
local PlayerMock = require("PlayerMock")
|
|
12
|
+
local PlayerThumbnailUtils = require("PlayerThumbnailUtils")
|
|
13
|
+
local PromiseTestUtils = require("PromiseTestUtils")
|
|
14
|
+
|
|
15
|
+
local describe = Jest.Globals.describe
|
|
16
|
+
local expect = Jest.Globals.expect
|
|
17
|
+
local it = Jest.Globals.it
|
|
18
|
+
|
|
19
|
+
describe("PlayerThumbnailUtils.promiseUserThumbnail against a mock", function()
|
|
20
|
+
it("resolves the derived rbxthumb URL with defaulted type and size", function()
|
|
21
|
+
local player = PlayerMock.new({ UserId = 90081001 })
|
|
22
|
+
player.Parent = Workspace
|
|
23
|
+
|
|
24
|
+
local outcome, content = PromiseTestUtils.awaitOutcome(PlayerThumbnailUtils.promiseUserThumbnail(90081001))
|
|
25
|
+
|
|
26
|
+
expect(outcome).toBe("resolved")
|
|
27
|
+
expect(content).toBe("rbxthumb://type=AvatarHeadShot&id=90081001&w=100&h=100")
|
|
28
|
+
|
|
29
|
+
player:Destroy()
|
|
30
|
+
end)
|
|
31
|
+
|
|
32
|
+
it("derives the URL from the requested type and size", function()
|
|
33
|
+
local player = PlayerMock.new({ UserId = 90081002 })
|
|
34
|
+
player.Parent = Workspace
|
|
35
|
+
|
|
36
|
+
local outcome, content = PromiseTestUtils.awaitOutcome(
|
|
37
|
+
PlayerThumbnailUtils.promiseUserThumbnail(
|
|
38
|
+
90081002,
|
|
39
|
+
Enum.ThumbnailType.AvatarThumbnail,
|
|
40
|
+
Enum.ThumbnailSize.Size420x420
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
expect(outcome).toBe("resolved")
|
|
45
|
+
expect(content).toBe("rbxthumb://type=Avatar&id=90081002&w=420&h=420")
|
|
46
|
+
|
|
47
|
+
player:Destroy()
|
|
48
|
+
end)
|
|
49
|
+
end)
|
|
50
|
+
|
|
51
|
+
describe("PlayerThumbnailUtils.promiseUserName against a mock", function()
|
|
52
|
+
it("resolves the default Name-derived username", function()
|
|
53
|
+
local player = PlayerMock.new({ UserId = 90081003 })
|
|
54
|
+
player.Parent = Workspace
|
|
55
|
+
|
|
56
|
+
local outcome, name = PromiseTestUtils.awaitOutcome(PlayerThumbnailUtils.promiseUserName(90081003))
|
|
57
|
+
|
|
58
|
+
expect(outcome).toBe("resolved")
|
|
59
|
+
expect(name).toBe(player.Name)
|
|
60
|
+
|
|
61
|
+
player:Destroy()
|
|
62
|
+
end)
|
|
63
|
+
|
|
64
|
+
it("resolves an injected username from the shared user-info domain", function()
|
|
65
|
+
local player = PlayerMock.new({ UserId = 90081004 })
|
|
66
|
+
player.Parent = Workspace
|
|
67
|
+
PlayerMock.writeLookup(player, "UserService.GetUserInfosByUserIdsAsync", 0, {
|
|
68
|
+
Id = 90081004,
|
|
69
|
+
Username = "shared_domain_name",
|
|
70
|
+
DisplayName = "Shared",
|
|
71
|
+
HasVerifiedBadge = false,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
local outcome, name = PromiseTestUtils.awaitOutcome(PlayerThumbnailUtils.promiseUserName(90081004))
|
|
75
|
+
|
|
76
|
+
expect(outcome).toBe("resolved")
|
|
77
|
+
expect(name).toBe("shared_domain_name")
|
|
78
|
+
|
|
79
|
+
player:Destroy()
|
|
80
|
+
end)
|
|
81
|
+
end)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "PlayerThumbnailUtilsTest",
|
|
3
|
+
"tree": {
|
|
4
|
+
"$className": "DataModel",
|
|
5
|
+
"ServerScriptService": {
|
|
6
|
+
"$properties": {
|
|
7
|
+
"LoadStringEnabled": true
|
|
8
|
+
},
|
|
9
|
+
"playerthumbnailutils": {
|
|
10
|
+
"$path": ".."
|
|
11
|
+
},
|
|
12
|
+
"Script": {
|
|
13
|
+
"$path": "scripts/Server"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
--!nonstrict
|
|
2
|
+
local ServerScriptService = game:GetService("ServerScriptService")
|
|
3
|
+
|
|
4
|
+
local root = ServerScriptService.playerthumbnailutils
|
|
5
|
+
local loader = root:FindFirstChild("LoaderUtils", true).Parent
|
|
6
|
+
local require = require(loader).bootstrapGame(root)
|
|
7
|
+
|
|
8
|
+
local NevermoreTestRunnerUtils = require("NevermoreTestRunnerUtils")
|
|
9
|
+
|
|
10
|
+
if NevermoreTestRunnerUtils.runTestsIfNeededAsync(root) then
|
|
11
|
+
return
|
|
12
|
+
end
|