diff --git a/src/main.js b/src/main.js index 3f82268..a374cc2 100644 --- a/src/main.js +++ b/src/main.js @@ -1788,10 +1788,25 @@ ipcMain.handle('skin:modelFor', async (_e, uuid) => { } }); +// Zeitpunkt der letzten eigenen Skin-Änderung: danach hinkt der Sitzungsserver +// (= was im Spiel zu sehen ist) noch kurz hinterher +let letzterSkinWechsel = 0; +const SKIN_NACHLAUF_MS = 3 * 60 * 1000; + +// Kontoprofil + der Skin, den andere im Spiel sehen (siehe skins.waehleAnzeigeSkin) +async function profilMitSpielSkin(token) { + const p = await skins.holeProfil(token); + if (!p.ok) return p; + const kuerzlich = Date.now() - letzterSkinWechsel < SKIN_NACHLAUF_MS; + let spiel = null; + if (!kuerzlich) { try { spiel = await skins.holeSpielTexturen(p.id); } catch { /* dann eben das Kontoprofil */ } } + return skins.waehleAnzeigeSkin(p, spiel, kuerzlich); +} + ipcMain.handle('skin:profile', async () => { const token = await aktivesToken(); if (!token) return { ok: false, message: 'Kein angemeldetes Konto.' }; - try { return await skins.holeProfil(token); } + try { return await profilMitSpielSkin(token); } catch (err) { return { ok: false, message: 'Profil konnte nicht geladen werden: ' + err.message }; } }); @@ -1819,7 +1834,7 @@ ipcMain.handle('skin:apply', async (_e, dateiPfad, variante) => { if (!token) return { ok: false, message: 'Kein angemeldetes Konto.' }; try { const res = await skins.setzeSkin(token, dateiPfad, variante); - if (res && res.ok) { armformCache.clear(); broadcast('skin:changed'); } + if (res && res.ok) { letzterSkinWechsel = Date.now(); armformCache.clear(); broadcast('skin:changed'); } return res; } catch (err) { return { ok: false, message: 'Skin konnte nicht gesetzt werden: ' + err.message }; } @@ -1830,7 +1845,7 @@ ipcMain.handle('skin:reset', async () => { if (!token) return { ok: false, message: 'Kein angemeldetes Konto.' }; try { const res = await skins.setzeSkinZurueck(token); - if (res && res.ok) { armformCache.clear(); broadcast('skin:changed'); } + if (res && res.ok) { letzterSkinWechsel = Date.now(); armformCache.clear(); broadcast('skin:changed'); } return res; } catch (err) { return { ok: false, message: 'Zurücksetzen fehlgeschlagen: ' + err.message }; } @@ -1852,7 +1867,7 @@ ipcMain.handle('skin:currentData', async () => { const token = await aktivesToken(); if (!token) return { ok: false, message: 'Kein angemeldetes Konto.' }; try { - const p = await skins.holeProfil(token); + const p = await profilMitSpielSkin(token); if (!p.ok) return p; if (!p.skinUrl) return { ok: false, standard: true, variante: p.variante, message: 'Das Konto nutzt den Standard-Skin.' }; const res = await fetch(p.skinUrl); @@ -2063,7 +2078,7 @@ ipcMain.handle('skin:applyData', async (_e, base64, variante) => { try { fs.writeFileSync(tmp, daten); const res = await skins.setzeSkin(token, tmp, variante); - if (res && res.ok) { armformCache.clear(); broadcast('skin:changed', { quelle: 'editor' }); } + if (res && res.ok) { letzterSkinWechsel = Date.now(); armformCache.clear(); broadcast('skin:changed', { quelle: 'editor' }); } return res; } catch (err) { return { ok: false, message: 'Skin konnte nicht gesetzt werden: ' + err.message }; diff --git a/src/skins.js b/src/skins.js index cb2b5b9..b1d8349 100644 --- a/src/skins.js +++ b/src/skins.js @@ -141,4 +141,39 @@ async function setzeUmhang(accessToken, capeId) { return { ok: true }; } -module.exports = { setzeSkin, setzeSkinZurueck, holeProfil, pruefeDatei, aufHttps, setzeUmhang }; +/* + * Texturen, die andere Spieler im Spiel sehen (öffentliches Profil beim + * Sitzungsserver). Das kann vom Kontoprofil oben abweichen: Dort liefert + * Mojang bei manchen Konten einen Standard-Skin (z. B. Efe) als "ACTIVE", + * obwohl im Spiel längst ein eigener Skin getragen wird. + */ +async function holeSpielTexturen(uuid) { + const id = String(uuid || '').replace(/-/g, '').toLowerCase(); + if (!/^[0-9a-f]{32}$/.test(id)) return null; + const res = await fetch('https://sessionserver.mojang.com/session/minecraft/profile/' + id, { + headers: { 'User-Agent': UA, Accept: 'application/json' }, + }); + if (!res.ok) return null; + const profil = await res.json(); + const eigenschaft = (profil.properties || []).find((p) => p.name === 'textures'); + if (!eigenschaft) return null; + const texturen = JSON.parse(Buffer.from(eigenschaft.value, 'base64').toString('utf8')).textures || {}; + const skin = texturen.SKIN; + return { + skinUrl: skin && skin.url ? aufHttps(skin.url) : '', + variante: skin && skin.metadata && skin.metadata.model === 'slim' ? 'slim' : 'classic', + }; +} + +/* + * Welcher Skin wird als "aktuell" gezeigt? Grundsätzlich der aus dem Spiel – + * nur direkt nach einer eigenen Änderung das Kontoprofil, weil der + * Sitzungsserver dann noch kurz den alten Stand liefert. + */ +function waehleAnzeigeSkin(kontoProfil, spiel, kuerzlichGeaendert) { + if (!kontoProfil || !kontoProfil.ok || kuerzlichGeaendert) return kontoProfil; + if (!spiel || !spiel.skinUrl || spiel.skinUrl === kontoProfil.skinUrl) return kontoProfil; + return Object.assign({}, kontoProfil, { skinUrl: spiel.skinUrl, variante: spiel.variante, skinAusSpiel: true }); +} + +module.exports = { setzeSkin, setzeSkinZurueck, holeProfil, pruefeDatei, aufHttps, setzeUmhang, holeSpielTexturen, waehleAnzeigeSkin };