import { parseJSON } from '../codegen.js'; import { getFillRectParams } from '../asm'; import { int2hex } from '../utils'; export const lskey = 'config/qft'; export const getPreviewText = () => '0:00.000'; export const defaultConfig = { x: 16, y: 456, fontSize: 20, fgRGB: 0xffffff, fgA: 0xff, fgRGB2: null, fgA2: null, bgRGB: 0x000000, bgA: 0x80, bgLeft: 0, bgRight: 2, bgTop: 2, bgBot: 0, freezeDuration: 30, freeze: { yellowCoin: false, redCoin: true, blueCoin: true, item: true, talk: true, demo: true, cleaned: true, bowser: true, // onBathhubGripDestroyed yoshi: true, take: true, drop: true, put: true, tripleJump: true, spinJump: true, ledgeGrab: true, wallKick: true, ropeJump: true, bounce: true, }, }; /** @returns {typeof defaultConfig} */ export function getConfig() { const config = (typeof localStorage !== 'undefined' && parseJSON(localStorage.getItem(lskey))) || {}; return { ...defaultConfig, ...config, freeze: { ...defaultConfig.freeze, ...config.freeze, }, text: getPreviewText(), }; } /** @param {number} x */ const int2gecko = (x) => (x >>> 0).toString(16).toUpperCase().padStart(8, '0'); import * as GMSJ01 from './code/GMSJ01.js'; import * as GMSE01 from './code/GMSE01.js'; import * as GMSP01 from './code/GMSP01.js'; import * as GMSJ0A from './code/GMSJ0A.js'; export const codes = { GMSJ01, GMSE01, GMSP01, GMSJ0A }; import { measureText } from '../text.js'; import statusDB0 from './code/status.js'; const statusDB = /**@type{Record}*/ (statusDB0); export const statusKeys = Object.keys(statusDB0); /**** ## save freeze frame, load and save QF ## this function destroys r11 and r12 077F0348: lwz r11, gpMarDirector-_SDA_BASE_(r13) lis r12, 0x817F lwz r11, 0x5C(r11) stw r11, 0xB8(r12) li r11, freezeDuration stw r11, 0xBC(r12) blr ## for each hook (over a blr): b 817f0348 ****/ const freezeCodeAddr = 0x817f0348; /** * @param {keyof typeof codes} version * @param {string=} baseCode */ export default function codegen(version, baseCode) { if (!baseCode) return ''; const config = getConfig(); const { freezeCodeHooks, r13off, onChangeStatusAddr } = codes[version] ?? {}; let code = baseCode; const { freezeDuration: frame } = config; // freezing code const enabledFreezes = []; const statuses = []; if (frame > 0) { for (const [key, enabled] of Object.entries(config.freeze)) { if (!enabled) continue; // add status statuses.push(...(statusDB[key] ?? [])); // add hook const hook = freezeCodeHooks[key]; if (hook) { if (key === 'blueCoin') { const addr = /**@type{number}*/ (hook); // special: needs to adjust QF -> use separate C2 instead code += [ 0xc2000000 + (addr & 0x1ffffff), 0x00000005, 0x7c030378, 0x80a3005c, 0x38a50003, 0x54a0003a, 0x3ca0817f, 0x900500b8, 0x38000000 | (frame & 0xffff), 0x900500bc, 0x60000000, 0x00000000, ] .map(int2gecko) .join(''); } else if (typeof hook === 'number') { // handle regular freezing code later const addr = hook; enabledFreezes.push(addr); } else { // {addr: number, orig: number} // separate C2 code to handle orig const { addr, orig } = hook; code += [ 0xc2000000 + (addr & 0x1ffffff), 0x00000003, 0x3d800000 + (freezeCodeAddr >>> 16), // lis r12, freezeCodeAddr@h 0x618c0000 + (freezeCodeAddr & 0xffff), // ori r12, r12, freezeCodeAddr@l 0x7d8803a6, // mtlr r12 0x4e800021, // blrl orig, 0x00000000, ] .map(int2gecko) .join(''); } } } } // handle regular freezing code if (enabledFreezes.length <= 1 && statuses.length === 0) { // use C2 directly code += enabledFreezes .flatMap((addr) => [ 0xc2000000 + (addr & 0x1ffffff), 0x00000004, 0x816d0000 | (r13off & 0xffff), // lwz r11, r13off(r13) 0x3d80817f, // lis r12, 0x817F 0x816b005c, // lwz r11, 0x5C(r11) 0x916c00b8, // stw r11, 0xB8(r12) 0x39600000 | (frame & 0xffff), // li r11, frame 0x916c00bc, // stw r11, 0xBC(r12) 0x60000000, // nop 0x00000000, ]) .map(int2gecko) .join(''); } else { // could be shorter to turn this into a Gecko loop if enough freezes are enabled const hooks = enabledFreezes.flatMap((addr) => [ 0xc6000000 | (addr & 0x1ffffff), freezeCodeAddr, ]); const freezer = [ 0x06000000 | (freezeCodeAddr & 0x1ffffff), 0x0000001c, 0x816d0000 | (r13off & 0xffff), // lwz r11, r13off(r13) 0x3d80817f, // lis r12, 0x817F 0x816b005c, // lwz r11, 0x5C(r11) 0x916c00b8, // stw r11, 0xB8(r12) 0x39600000 | (frame & 0xffff), // li r11, frame 0x916c00bc, // stw r11, 0xBC(r12) 0x4e800020, // blr 0x00000000, ]; // apply code code += [...hooks, ...freezer].map(int2gecko).join(''); } // onChangeStatus hook if (statuses.length) { const c = [ // check each status ...statuses.flatMap((x, i) => { const cr = i > 0 ? 0x800000 : 0; // i>0 ? cr1 : cr0 const c = x < 0x10000 ? [ 0x281d0000 + cr + x, // cmplwi crX, r29, $x ] : [ 0x3c000000 + (x >>> 16), // lis r0, $x@h 0x60000000 + (x & 0xffff), // ori r0, r0, $x@l 0x7c1d0040 + cr, // cmplw crX, r29, r0 ]; if (i > 0) { // cror 4*cr0+eq, 4*cr0+eq, 4*cr1+eq c.push(0x4c423382); } return c; }), // freeze 0x3d800000 + (freezeCodeAddr >>> 16), // lis r12, freezeCodeAddr@h 0x618c0000 + (freezeCodeAddr & 0xffff), // ori r12, r12, freezeCodeAddr@l 0x7d8803a6, // mtlr r12 0x4d820021, // beqlrl // orig 0x38000000, // li r0, 0 ]; // pad nop if (c.length % 2 === 0) { c.push(0x60000000); } // end of C2 c.push(0x00000000); // apply code code += [ 0xc2000000 + (onChangeStatusAddr & 0x1ffffff), c.length >> 1, // line count ...c, ] .map(int2gecko) .join(''); } // ui /* bounds */ const { x, y, fontSize, bgLeft, bgRight, bgTop, bgBot } = config; const rect = getFillRectParams(config, measureText(getPreviewText(), version)); const [bgColor] = rect.splice(-1); const scale = fontSize / 20; code += '077F0094 0000001D'; code += rect.map(int2gecko).join(''); code += '25753a253032752e2530337500000000'; // fmt /** * 817F0110 drawTextOpt: {x, y, fontSize, colorTop, colorBot} * 817F0120 bgColor */ code += '077F0110 00000014'; const fgColor = (config.fgRGB & 0xffffff) * 256 + (config.fgA & 0xff); const fgColor2 = ((config.fgRGB2 ?? config.fgRGB) & 0xffffff) * 256 + ((config.fgA2 ?? config.fgA) & 0xff); code += [x, y].map((x) => int2hex(x, 2)).join(''); code += [fontSize, fgColor, fgColor2, bgColor].map(int2gecko).join(''); code += '00000000'; return code.replace(/\s/g, ''); }