From dbc1d4b444dfec3abdd34be7fa569c00f4d402a3 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Mon, 5 Oct 2026 22:45:56 -0700 Subject: [PATCH] =?UTF-8?q?Equip=20Jeffrey=E2=80=99s=20saved=20fighter=20b?= =?UTF-8?q?y=20default=20in=20local=20practice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oskiewar-default-fighter.browser.mjs | 49 +++++++++++++++++++ system/tests/oskiewar-generation.test.mjs | 5 +- xbox/live/account.mjs | 1 + xbox/live/mac-test.html | 19 +++++++ xbox/live/oskiewar-wizard.mjs | 41 +++++++++++++--- xbox/live/oskiewar.js | 4 +- 6 files changed, 110 insertions(+), 9 deletions(-) create mode 100644 system/tests/oskiewar-default-fighter.browser.mjs diff --git a/system/tests/oskiewar-default-fighter.browser.mjs b/system/tests/oskiewar-default-fighter.browser.mjs new file mode 100644 index 0000000000..5f1c58c11a --- /dev/null +++ b/system/tests/oskiewar-default-fighter.browser.mjs @@ -0,0 +1,49 @@ +// Poorslice regression: defaults restore live account authority without granting +// or accepting anything. Delayed requests cannot outlive a sign-out. +import assert from 'node:assert/strict'; +import {createServer} from 'node:http'; +import {readFile} from 'node:fs/promises'; +import puppeteer from 'puppeteer'; +let mode='saved',requests=[],pending; +const fighter={version:1,recipe:'oskiewar-capsule-fighter-v1',hash:'a'.repeat(64),appearance:{skin:'#f1d9c9',hair:'#4a3c31',shirt:'#f0f0f0',pants:'#1c4ea6',shoes:'#000000',hairStyle:'long',beard:false,glasses:false,sleeves:'long'}}; +const result=()=>({status:'accepted',handle:mode==='other'?'@other':'@jeffrey',validUntil:mode==='expired'?1:Date.now()+600000,fighter}); +const server=createServer(async(req,res)=>{ + if(req.url==='/') {res.setHeader('Content-Type','text/html');res.end(``);return;} + if(req.url.endsWith('.mjs')){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('../../xbox/live'+req.url,import.meta.url)));return;} + if(req.url==='/api/oskiewar-generation'){ + let body='';for await(const c of req)body+=c;requests.push(JSON.parse(body).action); + const send=()=>{res.setHeader('Content-Type','application/json');res.end(JSON.stringify(result()));}; + if(mode==='delayed'){pending=send;return;} + if(mode==='error'){res.statusCode=403;res.end(JSON.stringify({code:'handle_required'}));return;} + if(mode==='empty'){res.end(JSON.stringify({status:'empty',handle:'@jeffrey'}));return;} + send();return; + } + res.statusCode=404;res.end(); +}); +await new Promise(r=>server.listen(0,'127.0.0.1',r)); +const browser=await puppeteer.launch({headless:true,executablePath:process.env.CHROME_BIN||'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'}); +const page=await browser.newPage(),errors=[];page.on('pageerror',e=>errors.push(e.message)); +const selected=()=>page.evaluate(()=>globalThis.__oskiewarFighterAppearance?.handle||null); +const change=async signedIn=>page.evaluate(signedIn=>{window.signedIn=signedIn;dispatchEvent(new CustomEvent('oskiewar:account-change',{detail:{signedIn}}));},signedIn); +try{ + await page.goto('http://127.0.0.1:'+server.address().port);await page.waitForFunction(()=>window.ready); + assert.equal(await selected(),'@jeffrey');assert.equal(await page.$eval('#wizard-panel',e=>e.hidden),true); + await page.reload();await page.waitForFunction(()=>window.ready);assert.equal(await selected(),'@jeffrey'); + await change(false);assert.equal(await selected(),null); + await change(true);await page.waitForFunction(()=>globalThis.__oskiewarFighterAppearance?.handle==='@jeffrey'); + for(const state of ['other','expired','empty','error']){ + mode=state;await page.evaluate(()=>window.wizard.restoreSaved());assert.equal(await selected(),null); + assert.equal(await page.$eval('#wizard-panel',e=>e.hidden),true); + assert.equal(await page.evaluate(()=>globalThis.__oskiewarAccountDoor),undefined,'background restoration does not open a handle dialog'); + } + mode='delayed';await change(true); + for(let i=0;i<100&&!pending;i++)await new Promise(r=>setTimeout(r,10));assert.ok(pending); + await change(false);assert.equal(await selected(),null);pending();pending=null; + await new Promise(r=>setTimeout(r,100));assert.equal(await selected(),null); + assert.ok(requests.length>=8);assert.ok(requests.every(action=>action==='account'));assert.deepEqual(errors,[]); + console.log('PASS: default saved fighter on entry/reload/login; immediate sign-out; foreign handle, expired, missing and unavailable results stay unequipped; late response cannot re-equip; no acceptance or grant.'); +}finally{await browser.close();await new Promise(r=>server.close(r));} diff --git a/system/tests/oskiewar-generation.test.mjs b/system/tests/oskiewar-generation.test.mjs index 4495aa1ca9..588d457736 100644 --- a/system/tests/oskiewar-generation.test.mjs +++ b/system/tests/oskiewar-generation.test.mjs @@ -132,11 +132,14 @@ test('generated appearance is local practice presentation only, and expires', as const source = await readFile(new URL('../../xbox/live/oskiewar.js', import.meta.url), 'utf8'); const code = source.slice(source.indexOf('function generatedAppearance('), source.indexOf('function generatedPartColor(')); const read = Function('globalThis','netSession','roundViewer','versusLane','survivalActive','shellMode', code+';return generatedAppearance;'); - const selected={__oskiewarFighterAppearance:{appearance,validUntil:Date.now()+60000}}; + const selected={__oskiewarLocalPractice:true,__oskiewarFighterAppearance:{appearance,validUntil:Date.now()+60000}}; assert.equal(read(selected,null,false,()=>false,()=>false,'GAME')({pad:0}),appearance); for(const [net,viewer,versus,survival,mode] of [[{},false,false,false,'GAME'],[null,true,false,false,'GAME'],[null,false,true,false,'GAME'],[null,false,false,true,'GAME'],[null,false,false,false,'MENU']]) assert.equal(read(selected,net,viewer,()=>versus,()=>survival,mode)({pad:0}),null); assert.equal(read(selected,null,false,()=>false,()=>false,'GAME')({pad:1}),null); + selected.__oskiewarLocalPractice=false; + assert.equal(read(selected,null,false,()=>false,()=>false,'GAME')({pad:0}),null,'online park never uses the private fighter'); + selected.__oskiewarLocalPractice=true; selected.__oskiewarFighterAppearance.validUntil=1; assert.equal(read(selected,null,false,()=>false,()=>false,'GAME')({pad:0}),null); }); diff --git a/xbox/live/account.mjs b/xbox/live/account.mjs index 804e9244fe..86bc0ef35c 100644 --- a/xbox/live/account.mjs +++ b/xbox/live/account.mjs @@ -210,6 +210,7 @@ export default function mountAccount({ sfx = () => {}, probe = true } = {}) { handleLabel.textContent = handle ? "@" + handle : ""; handleLabel.hidden = !handle; logout.textContent = signedIn ? "log out" : "log in"; + dispatchEvent(new CustomEvent("oskiewar:account-change", { detail: { signedIn } })); } // 🤚 Who this sub is, as the rest of AC already answers it. Two requests diff --git a/xbox/live/mac-test.html b/xbox/live/mac-test.html index c4e0db9e0b..8f69a3af01 100644 --- a/xbox/live/mac-test.html +++ b/xbox/live/mac-test.html @@ -1784,16 +1784,34 @@ // social-preview render for the same reason the account probe is: that pass // draws a picture of the title and must not grow furniture a viewer of the // picture could never press. + const defaultCharacterEntry = location.pathname === "/" && !location.search && !location.hash; if (!socialPreview && !inIOSApp) globalThis.__oskiewarWizard = mountWizard({ sfx: (name, amount, pan) => oskiewarSfx.drum(name, amount, pan), bearer: () => globalThis.__oskiewarAccount.bearer(), + defaultHandle: "@jeffrey", + onDefault: () => { + if (defaultCharacterEntry && !globalThis.__oskiewarLocalPractice) { + // During initial loading, select the private lane before the game + // or a room bridge boots. A later sign-in starts a fresh page. + if (globalThis.__oskiewarTouch?.screen === "game") { + location.replace("/?practice"); + return; + } + globalThis.__oskiewarLocalPractice = true; + globalThis.__oskiewarVersusCapable = false; + globalThis.__oskiewarOpponent = "dummy"; + history.replaceState(null, "", "/?practice"); + } + }, enterPractice: () => { if (globalThis.__oskiewarLocalPractice) return; sessionStorage.setItem("oskiewar-enter-practice", "1"); location.assign("/?practice"); }, }); + const defaultFighterReady = Promise.resolve(accountDoor?.ready) + .then(() => globalThis.__oskiewarWizard?.restoreSaved()); if (globalThis.__oskiewarLocalPractice && sessionStorage.getItem("oskiewar-enter-practice")) { sessionStorage.removeItem("oskiewar-enter-practice"); Promise.resolve(accountDoor?.ready).then(() => globalThis.__oskiewarWizard?.open({ useSaved: true })); @@ -1882,6 +1900,7 @@ // Finish a web sign-in before matchmaking or room URL updates can consume // its callback URL. Ordinary anonymous visits still boot immediately. if (accountDoor?.redirectPending) await accountDoor.ready; + if (defaultCharacterEntry) await defaultFighterReady; const workshopRoute = /^\/workshop\/?$/.test(location.pathname); const roundName = roundNameFromPath(location.pathname) || (workshopRoute ? roundNameFromPath("/" + (new URLSearchParams(location.search).get("room") || "")) : ""); diff --git a/xbox/live/oskiewar-wizard.mjs b/xbox/live/oskiewar-wizard.mjs index 9f23ab5109..370642a7d4 100644 --- a/xbox/live/oskiewar-wizard.mjs +++ b/xbox/live/oskiewar-wizard.mjs @@ -101,7 +101,7 @@ const STYLE = ` @media(prefers-reduced-motion:reduce) { #wizard-panel button { transition:none; } } `; -export default function mountWizard({ sfx = () => {}, bearer = async () => null, enterPractice = () => {} } = {}) { +export default function mountWizard({ sfx = () => {}, bearer = async () => null, enterPractice = () => {}, defaultHandle = null, onDefault = () => {} } = {}) { const style = document.createElement("style"); style.textContent = STYLE; document.head.append(style); @@ -138,7 +138,8 @@ export default function mountWizard({ sfx = () => {}, bearer = async () => null, let ownerHandle = null; let requestId = null; let reviewSession = 0; - const clearFighter = () => { accepted = null; globalThis.__oskiewarFighterAppearance = null; }; + let authority = 0; + const clearFighter = () => { authority++; accepted = null; globalThis.__oskiewarFighterAppearance = null; }; const upload = document.createElement("div"); upload.id = "wizard-upload"; sections.after(upload); @@ -379,14 +380,14 @@ export default function mountWizard({ sfx = () => {}, bearer = async () => null, }); - async function generationRequest(token, action, source = submitted) { + async function generationRequest(token, action, source = submitted, { silent = false } = {}) { const response = await fetch("/api/oskiewar-generation", { method: "POST", headers: { "Content-Type": "application/json", authorization: "Bearer " + token }, body: JSON.stringify({ ...source, action }), signal: AbortSignal.timeout(100000), }); const result = await response.json(); if (!response.ok) { - if (result.code === "handle_required") { + if (!silent && result.code === "handle_required") { close(); globalThis.__oskiewarAccountDoor = "handle"; } throw new Error(result.message || "Fighter generation failed."); @@ -439,6 +440,34 @@ export default function mountWizard({ sfx = () => {}, bearer = async () => null, say("Withdrawn. Your stored material is removed and future use is blocked.", "settled"); } catch (error) { working(false); say(error.message, "trouble"); } }); + // A default is an already accepted account result, never a new grant or + // acceptance. Fetch live authority before equipping; don't retain a photo or + // fighter across pages. Only the explicitly configured pilot handle opts in. + async function restoreSaved() { + if (!defaultHandle) return false; + const revision = ++authority; + try { + const token = await bearer(); + if (revision !== authority) return false; + if (!token) { clearFighter(); return false; } + const result = await generationRequest(token, "account", {}, { silent: true }); + if (revision !== authority) return false; + if (result.status !== "accepted" || result.handle !== defaultHandle || + !Number.isFinite(result.validUntil) || result.validUntil <= Date.now()) { + clearFighter(); return false; + } + const appearance = validateFighter(result.fighter); + accepted = { ...result, token }; + globalThis.__oskiewarFighterAppearance = { appearance, + handle: result.handle, validUntil: result.validUntil }; + onDefault(result.handle); + return true; + } catch { if (revision === authority) clearFighter(); return false; } + } + addEventListener("oskiewar:account-change", event => { + clearFighter(); + if (event.detail?.signedIn) void restoreSaved(); + }); let checking = false; setInterval(async () => { if (!accepted || checking) return; @@ -448,7 +477,7 @@ export default function mountWizard({ sfx = () => {}, bearer = async () => null, try { const token = await bearer(); if (!token) { clearFighter(); return; } - const result = await generationRequest(token, "account", {}); + const result = await generationRequest(token, "account", {}, { silent: true }); if (accepted !== selected) return; if (result.status !== "accepted" || result.fighter?.hash !== selected.fighter.hash) { clearFighter(); return; } accepted = { ...result, token }; @@ -459,5 +488,5 @@ export default function mountWizard({ sfx = () => {}, bearer = async () => null, }, 15000); addEventListener("pagehide", clearFighter); - return { open, close, get isOpen() { return !panel.hidden; } }; + return { open, close, restoreSaved, get isOpen() { return !panel.hidden; } }; } diff --git a/xbox/live/oskiewar.js b/xbox/live/oskiewar.js index af77b69f15..8253b9dda2 100644 --- a/xbox/live/oskiewar.js +++ b/xbox/live/oskiewar.js @@ -102,7 +102,7 @@ if (hostAnalytics) const hostAc = typeof ac === "function" ? ac : null; // Monotonic count of committed revisions to this piece (next revision included). -const buildVersion = 261; +const buildVersion = 262; const parkDecalResolution=Number(globalThis.decalSurfaceSize)||2048; const floorY = 1800; // Oskiewar now opens as a versus game. An ordinary web visit hosts a room — @@ -15847,7 +15847,7 @@ function damagedPartColor(color, player, part) { function generatedAppearance(player) { const selected = globalThis.__oskiewarFighterAppearance; - if (!selected || Date.now() >= selected.validUntil || player?.pad !== 0 || player.npc || + if (!globalThis.__oskiewarLocalPractice || !selected || Date.now() >= selected.validUntil || player?.pad !== 0 || player.npc || netSession || roundViewer || versusLane() || survivalActive() || shellMode !== "GAME") return null; return selected.appearance; } -- 2.51.2