// Narrow, reproducible fixes to the pinned upstream, applied in memory by Vite. // Keep the upstream checkout untouched and fail loudly if its source no longer matches. import type { Plugin } from 'vite'; export function upstreamFixes(): Plugin { return { name: 'shoal-sanguosha-rules-fixes', enforce: 'pre', transform(code, id) { if (id.endsWith('/src/engine/rules/身份局.ts')) { const before = 'if (aliveRenegade) return { gameOver: true, winner: aliveRenegade.index };'; if (!code.includes(before)) throw new Error('The pinned identity rule changed; review the Shoal rule fix.'); return code.replace(before, 'if (aliveRenegade && state.players.filter((p) => p.alive).length === 1) return { gameOver: true, winner: aliveRenegade.index };'); } if (id.endsWith('/src/engine/core/skill.ts')) { const before = ' if (state.pendingSlots.size === 1) {'; if (!code.includes(before)) throw new Error('The pinned pending-slot lookup changed; review the extra-phase fix.'); // A previous turn's paused execute can await the next player's extra phase. The new // player's play slot must still be paused/resolved by dispatch, even with two map entries. return code.replace(before, ' if (direct && !direct.isPaused && !direct.isBlocking) return direct;\n' + before); } if (id.endsWith('/src/engine/view/buildView.ts')) { const before = 'atom: slot.atom,\n prompt: { type: \'confirm\', title: \'等待回应\', cancelLabel: \'\' },'; if (!code.includes(before)) throw new Error('The pinned observer view changed; review the private-prompt fix.'); code = code.replace(before, "atom: { type: slot.atom.type, target, ...('requestType' in slot.atom ? { requestType: slot.atom.requestType } : {}) },\n prompt: { type: 'confirm', title: '等待回应', cancelLabel: '' },"); const priority = 'const ownOrBroadcastSlot = ownSlot && !ownSlot.isPaused ? ownSlot : broadcastSlot;'; if (!code.includes(priority)) throw new Error('The pinned pending priority changed; review the broadcast fix.'); // Match findPendingSlot: a blocking response outranks a nonblocking play window. // Extra play phases (界当先) otherwise leave the active player unable to answer/pass // a broadcast, while every attempted play is rejected by hasBlockingPending. return code.replace(priority, 'const ownOrBroadcastSlot = ownSlot && !ownSlot.isPaused && ownSlot.isBlocking ? ownSlot : broadcastSlot ?? (ownSlot && !ownSlot.isPaused ? ownSlot : undefined);'); } return null; }, }; }