fix: 复用皮肤站逻辑并稳定玩家头像
Some checks failed
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
Some checks failed
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
This commit is contained in:
@ -320,6 +320,7 @@ fn main() {
|
||||
.commands(&[
|
||||
"get_available_capes",
|
||||
"get_available_skins",
|
||||
"get_default_skins",
|
||||
"add_and_equip_custom_skin",
|
||||
"equip_skin",
|
||||
"remove_custom_skin",
|
||||
|
||||
@ -10,6 +10,7 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_available_capes,
|
||||
get_available_skins,
|
||||
get_default_skins,
|
||||
add_and_equip_custom_skin,
|
||||
equip_skin,
|
||||
remove_custom_skin,
|
||||
@ -40,6 +41,12 @@ pub async fn get_available_skins() -> Result<Vec<Skin>> {
|
||||
Ok(minecraft_skins::get_available_skins().await?)
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|get_default_skins')`
|
||||
#[tauri::command]
|
||||
pub async fn get_default_skins() -> Result<Vec<Skin>> {
|
||||
Ok(minecraft_skins::get_default_skins())
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|add_and_equip_custom_skin', texture_blob, variant, cape)`
|
||||
///
|
||||
/// See also: [minecraft_skins::add_and_equip_custom_skin]
|
||||
|
||||
@ -15,6 +15,8 @@
|
||||
let pending
|
||||
const pendingLuck = new Map()
|
||||
const pendingPlayers = new Map()
|
||||
const pendingSkinUpdates = new Map()
|
||||
let knownPlayers = new Map()
|
||||
let snapshot = { status: 'checking', user: null }
|
||||
|
||||
function publish(status, user = null) {
|
||||
@ -40,6 +42,106 @@
|
||||
)
|
||||
}
|
||||
|
||||
function publishSkinUpdate(requestId, result) {
|
||||
if (!parentOrigin) return
|
||||
window.parent.postMessage(
|
||||
{ type: 'starlight-skin-update-result', requestId, ...result },
|
||||
parentOrigin,
|
||||
)
|
||||
}
|
||||
|
||||
// Keep this geometry identical to the skin site's SkinRender.renderHead.
|
||||
// In particular, the site does not infer texture dimensions before cropping.
|
||||
// Rendering here keeps the source on the skin site's origin and only exposes
|
||||
// the finished PNG to the launcher.
|
||||
function renderPlayerHead(skinSource) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image()
|
||||
const timeout = setTimeout(() => reject(new Error('Skin rendering timed out.')), 8_000)
|
||||
image.crossOrigin = 'anonymous'
|
||||
image.onerror = () => {
|
||||
clearTimeout(timeout)
|
||||
reject(new Error('Unable to load the skin texture.'))
|
||||
}
|
||||
image.onload = () => {
|
||||
clearTimeout(timeout)
|
||||
try {
|
||||
const buffer = document.createElement('canvas')
|
||||
buffer.width = 18
|
||||
buffer.height = 18
|
||||
const context = buffer.getContext('2d')
|
||||
if (!context) throw new Error('Unable to create the skin renderer.')
|
||||
context.imageSmoothingEnabled = false
|
||||
context.drawImage(image, 8, 8, 8, 8, 1, 1, 16, 16)
|
||||
context.globalCompositeOperation = 'source-over'
|
||||
context.drawImage(image, 40, 8, 8, 8, 0, 0, 18, 18)
|
||||
|
||||
const output = document.createElement('canvas')
|
||||
output.width = 36
|
||||
output.height = 36
|
||||
const outputContext = output.getContext('2d')
|
||||
if (!outputContext) throw new Error('Unable to create the skin renderer.')
|
||||
outputContext.imageSmoothingEnabled = false
|
||||
outputContext.drawImage(buffer, 0, 0, 18, 18, 0, 0, 36, 36)
|
||||
resolve(output.toDataURL('image/png'))
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
}
|
||||
image.src = skinSource
|
||||
})
|
||||
}
|
||||
|
||||
function serializePlayerSkin(skinSource) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image()
|
||||
const timeout = setTimeout(() => reject(new Error('Skin loading timed out.')), 8_000)
|
||||
image.crossOrigin = 'anonymous'
|
||||
image.onerror = () => {
|
||||
clearTimeout(timeout)
|
||||
reject(new Error('Unable to load the skin texture.'))
|
||||
}
|
||||
image.onload = () => {
|
||||
clearTimeout(timeout)
|
||||
try {
|
||||
const width = image.naturalWidth || image.width
|
||||
const height = image.naturalHeight || image.height
|
||||
if (!width || !height) throw new Error('The skin texture is empty.')
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
const context = canvas.getContext('2d')
|
||||
if (!context) throw new Error('Unable to create the skin renderer.')
|
||||
context.imageSmoothingEnabled = false
|
||||
context.drawImage(image, 0, 0)
|
||||
const dataUrl = canvas.toDataURL('image/png')
|
||||
if (dataUrl.length > 2_000_000) throw new Error('The skin texture is too large.')
|
||||
resolve(dataUrl)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
}
|
||||
image.src = skinSource
|
||||
})
|
||||
}
|
||||
|
||||
function sleep(delay) {
|
||||
return new Promise((resolve) => setTimeout(resolve, delay))
|
||||
}
|
||||
|
||||
function pngDataUrlToBlob(dataUrl) {
|
||||
if (
|
||||
typeof dataUrl !== 'string' ||
|
||||
dataUrl.length > 2_000_000 ||
|
||||
!/^data:image\/png;base64,[a-z0-9+/]+={0,2}$/i.test(dataUrl)
|
||||
)
|
||||
throw new Error('Invalid skin texture.')
|
||||
const bytes = atob(dataUrl.slice(dataUrl.indexOf(',') + 1))
|
||||
const buffer = new Uint8Array(bytes.length)
|
||||
for (let index = 0; index < bytes.length; index += 1) buffer[index] = bytes.charCodeAt(index)
|
||||
return new Blob([buffer], { type: 'image/png' })
|
||||
}
|
||||
|
||||
async function requestPlayers(requestId) {
|
||||
if (pendingPlayers.has(requestId)) return
|
||||
let token
|
||||
@ -57,7 +159,6 @@
|
||||
|
||||
const controller = new AbortController()
|
||||
pendingPlayers.set(requestId, controller)
|
||||
const timeout = setTimeout(() => controller.abort(), 10_000)
|
||||
try {
|
||||
const response = await fetch('/starlight/skin/player', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
@ -84,7 +185,59 @@
|
||||
return [{ uuid: player.uuid, name: player.name, isMojang: player.isMojang === true }]
|
||||
})
|
||||
if (response.ok && players) {
|
||||
publishPlayers(requestId, { ok: true, players })
|
||||
knownPlayers = new Map(players.map((player) => [player.uuid, player]))
|
||||
// Match the skin site: stagger texture requests by 150 ms so the
|
||||
// service is not hit with a burst that can drop individual players.
|
||||
const skinResults = await Promise.all(
|
||||
players.map(async (player, index) => {
|
||||
if (index > 0) await sleep(index * 150)
|
||||
try {
|
||||
const skinResponse = await fetch(
|
||||
`/starlight/skin/player/skin/${encodeURIComponent(player.uuid)}`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
signal: controller.signal,
|
||||
cache: 'no-store',
|
||||
redirect: 'error',
|
||||
},
|
||||
)
|
||||
const skinBody = await skinResponse.json().catch(() => null)
|
||||
if (
|
||||
!skinResponse.ok ||
|
||||
!skinBody?.payload ||
|
||||
typeof skinBody.payload !== 'object'
|
||||
)
|
||||
return { player, skinData: null }
|
||||
return { player, skinData: skinBody.payload }
|
||||
} catch {
|
||||
return { player, skinData: null }
|
||||
}
|
||||
}),
|
||||
)
|
||||
const playersWithSkins = await Promise.all(
|
||||
skinResults.map(async ({ player, skinData }) => {
|
||||
if (!skinData) return { ...player, skinState: 'error' }
|
||||
try {
|
||||
const skinSource = skinData.skin
|
||||
if (typeof skinSource !== 'string' || !skinSource.trim()) {
|
||||
return { ...player, skinState: 'empty' }
|
||||
}
|
||||
const headDataUrl = await renderPlayerHead(skinSource.trim())
|
||||
const skinDataUrl = await serializePlayerSkin(skinSource.trim()).catch(
|
||||
() => undefined,
|
||||
)
|
||||
const model = skinData.model === 'slim' ? 'slim' : 'default'
|
||||
return { ...player, skinState: 'ready', headDataUrl, skinDataUrl, model }
|
||||
} catch {
|
||||
return { ...player, skinState: 'error' }
|
||||
}
|
||||
}),
|
||||
)
|
||||
if (localStorage.getItem('loginToken') !== token) {
|
||||
publishPlayers(requestId, { ok: false, error: 'The skin site account changed.' })
|
||||
return
|
||||
}
|
||||
publishPlayers(requestId, { ok: true, players: playersWithSkins })
|
||||
} else {
|
||||
publishPlayers(requestId, {
|
||||
ok: false,
|
||||
@ -97,11 +250,79 @@
|
||||
} catch {
|
||||
publishPlayers(requestId, { ok: false, error: 'Unable to reach the player service.' })
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
pendingPlayers.delete(requestId)
|
||||
}
|
||||
}
|
||||
|
||||
async function updatePlayerSkin(requestId, playerId, textureDataUrl, model) {
|
||||
if (pendingSkinUpdates.has(requestId)) return
|
||||
const player = knownPlayers.get(playerId)
|
||||
if (!player || player.isMojang) {
|
||||
publishSkinUpdate(requestId, {
|
||||
ok: false,
|
||||
error: 'This skin site player cannot be changed.',
|
||||
})
|
||||
return
|
||||
}
|
||||
let token
|
||||
try {
|
||||
token = localStorage.getItem('loginToken') || ''
|
||||
} catch {
|
||||
publishSkinUpdate(requestId, { ok: false, error: 'Unable to read the skin site session.' })
|
||||
return
|
||||
}
|
||||
if (!token) {
|
||||
publish('signed-out')
|
||||
publishSkinUpdate(requestId, { ok: false, error: 'Sign in to the skin site first.' })
|
||||
return
|
||||
}
|
||||
|
||||
const controller = new AbortController()
|
||||
pendingSkinUpdates.set(requestId, controller)
|
||||
const timeout = setTimeout(() => controller.abort(), 18_000)
|
||||
try {
|
||||
const texture = pngDataUrlToBlob(textureDataUrl)
|
||||
const form = new FormData()
|
||||
form.append('file', texture, `${playerId}.png`)
|
||||
form.append('model', model)
|
||||
const response = await fetch(
|
||||
`/starlight/skin/player/skin/${encodeURIComponent(playerId)}/SKIN`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
body: form,
|
||||
signal: controller.signal,
|
||||
cache: 'no-store',
|
||||
redirect: 'error',
|
||||
},
|
||||
)
|
||||
const body = await response.json().catch(() => null)
|
||||
if (localStorage.getItem('loginToken') !== token) {
|
||||
publishSkinUpdate(requestId, { ok: false, error: 'The skin site account changed.' })
|
||||
return
|
||||
}
|
||||
if (response.status === 401 || response.status === 403) publish('signed-out')
|
||||
if (response.ok && body?.success !== false) publishSkinUpdate(requestId, { ok: true })
|
||||
else {
|
||||
publishSkinUpdate(requestId, {
|
||||
ok: false,
|
||||
error:
|
||||
typeof body?.errorMessage === 'string' && body.errorMessage
|
||||
? body.errorMessage.slice(0, 300)
|
||||
: 'The skin site rejected the skin update.',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
publishSkinUpdate(requestId, {
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.message : 'Unable to reach the skin service.',
|
||||
})
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
pendingSkinUpdates.delete(requestId)
|
||||
}
|
||||
}
|
||||
|
||||
async function requestLuck(requestId) {
|
||||
if (pendingLuck.has(requestId)) return
|
||||
let token
|
||||
@ -168,6 +389,7 @@
|
||||
pending?.abort()
|
||||
pending = undefined
|
||||
lastToken = token
|
||||
if (changed) knownPlayers = new Map()
|
||||
lastCheck = Date.now()
|
||||
if (!token) {
|
||||
publish('signed-out')
|
||||
@ -231,6 +453,23 @@
|
||||
/^skin-players-\d+-\d+$/.test(event.data.requestId)
|
||||
) {
|
||||
void requestPlayers(event.data.requestId)
|
||||
return
|
||||
}
|
||||
if (
|
||||
event.data?.type === 'starlight-skin-update-request' &&
|
||||
parentOrigin === event.origin &&
|
||||
typeof event.data.requestId === 'string' &&
|
||||
/^skin-update-\d+-\d+$/.test(event.data.requestId) &&
|
||||
typeof event.data.playerId === 'string' &&
|
||||
/^[0-9a-f-]{32,36}$/i.test(event.data.playerId) &&
|
||||
(event.data.model === 'default' || event.data.model === 'slim')
|
||||
) {
|
||||
void updatePlayerSkin(
|
||||
event.data.requestId,
|
||||
event.data.playerId,
|
||||
event.data.textureDataUrl,
|
||||
event.data.model,
|
||||
)
|
||||
}
|
||||
})
|
||||
// Storage events cover other tabs; polling also covers same-document SPA login/logout.
|
||||
@ -245,6 +484,8 @@
|
||||
pendingLuck.clear()
|
||||
for (const controller of pendingPlayers.values()) controller.abort()
|
||||
pendingPlayers.clear()
|
||||
for (const controller of pendingSkinUpdates.values()) controller.abort()
|
||||
pendingSkinUpdates.clear()
|
||||
})
|
||||
window.addEventListener('pageshow', (event) => {
|
||||
if (!event.persisted) return
|
||||
|
||||
@ -4,11 +4,14 @@ const vm = require('node:vm')
|
||||
const test = require('node:test')
|
||||
const source = fs.readFileSync(require('node:path').join(__dirname, 'skin_site_bridge.js'), 'utf8')
|
||||
|
||||
function harness(origin = 'https://skin.starlight.cool') {
|
||||
function harness(origin = 'https://skin.starlight.cool', skinSize = 64) {
|
||||
let token = null
|
||||
const listeners = {},
|
||||
messages = [],
|
||||
requests = []
|
||||
requests = [],
|
||||
drawCalls = []
|
||||
const skinWidth = typeof skinSize === 'number' ? skinSize : skinSize.width
|
||||
const skinHeight = typeof skinSize === 'number' ? skinSize : skinSize.height
|
||||
let tick
|
||||
let result = async () => ({
|
||||
ok: true,
|
||||
@ -38,7 +41,34 @@ function harness(origin = 'https://skin.starlight.cool') {
|
||||
return result(...args)
|
||||
},
|
||||
AbortController,
|
||||
Blob,
|
||||
Date,
|
||||
FormData,
|
||||
Uint8Array,
|
||||
atob,
|
||||
document: {
|
||||
createElement() {
|
||||
return {
|
||||
getContext() {
|
||||
return {
|
||||
drawImage(...args) {
|
||||
drawCalls.push(args)
|
||||
},
|
||||
}
|
||||
},
|
||||
toDataURL() {
|
||||
return 'data:image/png;base64,SEVBRERBVEE='
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
Image: class {
|
||||
naturalWidth = skinWidth
|
||||
naturalHeight = skinHeight
|
||||
set src(_value) {
|
||||
queueMicrotask(() => this.onload?.())
|
||||
}
|
||||
},
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
setInterval(callback) {
|
||||
@ -49,6 +79,7 @@ function harness(origin = 'https://skin.starlight.cool') {
|
||||
}
|
||||
vm.runInNewContext(source, context)
|
||||
return {
|
||||
drawCalls,
|
||||
messages,
|
||||
requests,
|
||||
listeners,
|
||||
@ -71,6 +102,19 @@ function harness(origin = 'https://skin.starlight.cool') {
|
||||
listeners.message?.({ source, origin, data })
|
||||
await new Promise(setImmediate)
|
||||
},
|
||||
async waitForMessage(type, requestId, timeout = 2_000) {
|
||||
const deadline = Date.now() + timeout
|
||||
while (Date.now() < deadline) {
|
||||
const message = messages.find(
|
||||
(entry) =>
|
||||
entry.data?.type === type &&
|
||||
(requestId === undefined || entry.data?.requestId === requestId),
|
||||
)
|
||||
if (message) return message
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
}
|
||||
throw new Error(`Timed out waiting for ${type}`)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -186,25 +230,130 @@ test('player requests return a sanitized complete player collection without expo
|
||||
const h = harness()
|
||||
h.token('test-token')
|
||||
await h.connect()
|
||||
h.result(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
payload: [
|
||||
{ uuid: '0123456789abcdef0123456789abcdef', name: 'PlayerOne', isMojang: false },
|
||||
{ uuid: 'fedcba9876543210fedcba9876543210', name: 'Official', isMojang: true },
|
||||
{ uuid: 'bad', name: 'Ignored', isMojang: false },
|
||||
],
|
||||
}),
|
||||
}))
|
||||
h.result(async (url) => {
|
||||
if (url.startsWith('/starlight/skin/player/skin/')) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
payload: {
|
||||
skin: url.endsWith('0123456789abcdef0123456789abcdef')
|
||||
? '/textures/player-one.png'
|
||||
: null,
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
payload: [
|
||||
{ uuid: '0123456789abcdef0123456789abcdef', name: 'PlayerOne', isMojang: false },
|
||||
{ uuid: 'fedcba9876543210fedcba9876543210', name: 'Official', isMojang: true },
|
||||
{ uuid: 'bad', name: 'Ignored', isMojang: false },
|
||||
],
|
||||
}),
|
||||
}
|
||||
})
|
||||
await h.message({ type: 'starlight-skin-players-request', requestId: 'skin-players-1-1' })
|
||||
const request = h.requests.at(-1)
|
||||
await h.waitForMessage('starlight-skin-players-result', 'skin-players-1-1')
|
||||
const request = h.requests.find(([url]) => url === '/starlight/skin/player')
|
||||
assert.equal(request[0], '/starlight/skin/player')
|
||||
assert.equal(request[1].headers.Authorization, 'Bearer test-token')
|
||||
assert.equal(
|
||||
h.requests.filter(([url]) => url.startsWith('/starlight/skin/player/skin/')).length,
|
||||
2,
|
||||
)
|
||||
const message = h.messages.at(-1)
|
||||
assert.equal(message.data.type, 'starlight-skin-players-result')
|
||||
assert.equal(message.data.ok, true)
|
||||
assert.equal(message.data.players.length, 2)
|
||||
assert.equal(message.data.players[1].isMojang, true)
|
||||
assert.equal(message.data.players[0].skinState, 'ready')
|
||||
assert.equal(message.data.players[0].headDataUrl, 'data:image/png;base64,SEVBRERBVEE=')
|
||||
assert.equal(message.data.players[0].skinDataUrl, 'data:image/png;base64,SEVBRERBVEE=')
|
||||
assert.equal(message.data.players[0].model, 'default')
|
||||
assert.equal(message.data.players[1].skinState, 'empty')
|
||||
assert.equal(message.data.players[1].headDataUrl, undefined)
|
||||
assert.ok(!JSON.stringify(h.messages).includes('test-token'))
|
||||
})
|
||||
|
||||
test('skin updates upload a PNG to the selected skin-site player without exposing the token', async () => {
|
||||
const h = harness()
|
||||
h.token('test-token')
|
||||
await h.connect()
|
||||
h.result(async (url) => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
payload:
|
||||
url === '/starlight/skin/player'
|
||||
? [
|
||||
{
|
||||
uuid: '0123456789abcdef0123456789abcdef',
|
||||
name: 'PlayerOne',
|
||||
isMojang: false,
|
||||
},
|
||||
]
|
||||
: { skin: null },
|
||||
}),
|
||||
}))
|
||||
await h.message({ type: 'starlight-skin-players-request', requestId: 'skin-players-2-1' })
|
||||
h.result(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ success: true, payload: 'updated' }),
|
||||
}))
|
||||
await h.message({
|
||||
type: 'starlight-skin-update-request',
|
||||
requestId: 'skin-update-1-1',
|
||||
playerId: '0123456789abcdef0123456789abcdef',
|
||||
textureDataUrl: 'data:image/png;base64,SEVBRERBVEE=',
|
||||
model: 'slim',
|
||||
})
|
||||
|
||||
const request = h.requests.at(-1)
|
||||
assert.equal(
|
||||
request[0],
|
||||
'/starlight/skin/player/skin/0123456789abcdef0123456789abcdef/SKIN',
|
||||
)
|
||||
assert.equal(request[1].method, 'PUT')
|
||||
assert.equal(request[1].headers.Authorization, 'Bearer test-token')
|
||||
assert.equal(request[1].body.get('model'), 'slim')
|
||||
assert.equal(request[1].body.get('file').type, 'image/png')
|
||||
assert.equal(h.messages.at(-1).data.type, 'starlight-skin-update-result')
|
||||
assert.equal(h.messages.at(-1).data.ok, true)
|
||||
assert.ok(!JSON.stringify(h.messages).includes('test-token'))
|
||||
})
|
||||
|
||||
test('skin rendering uses the skin site crop and preserves the source texture dimensions', async () => {
|
||||
const h = harness('https://skin.starlight.cool', { width: 64, height: 128 })
|
||||
h.token('test-token')
|
||||
await h.connect()
|
||||
h.result(async (url) => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
payload:
|
||||
url === '/starlight/skin/player'
|
||||
? [
|
||||
{
|
||||
uuid: '0123456789abcdef0123456789abcdef',
|
||||
name: 'HighResolutionPlayer',
|
||||
isMojang: false,
|
||||
},
|
||||
]
|
||||
: { skin: '/textures/hd.png', model: 'slim' },
|
||||
}),
|
||||
}))
|
||||
await h.message({ type: 'starlight-skin-players-request', requestId: 'skin-players-3-1' })
|
||||
|
||||
const player = h.messages.at(-1).data.players[0]
|
||||
assert.equal(player.skinState, 'ready')
|
||||
assert.equal(player.headDataUrl, 'data:image/png;base64,SEVBRERBVEE=')
|
||||
assert.equal(player.skinDataUrl, 'data:image/png;base64,SEVBRERBVEE=')
|
||||
assert.equal(player.model, 'slim')
|
||||
assert.deepEqual(h.drawCalls[0].slice(1), [8, 8, 8, 8, 1, 1, 16, 16])
|
||||
assert.deepEqual(h.drawCalls[1].slice(1), [40, 8, 8, 8, 0, 0, 18, 18])
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user