-
-
-
+
diff --git a/site/.vuepress/components/codes/CustomizedDisplay/assembler.js b/site/.vuepress/components/codes/CustomizedDisplay/assembler.js
new file mode 100644
index 0000000..37ab83c
--- /dev/null
+++ b/site/.vuepress/components/codes/CustomizedDisplay/assembler.js
@@ -0,0 +1,214 @@
+import { ASM, liDX, makeProgram, insts2hex } from '../asm.js';
+import { splitArray } from '../utils.js';
+
+/**
+ * @typedef {number} Inst
+ * @typedef {{type: 'call', addr: number, prep?: Inst[]}} CallInst -- function call instruction
+ * @typedef {{type: 'struct', reg: number, hex: string}} LoadStructInst -- struct pointer load instruction
+ * @typedef {Inst|CallInst|LoadStructInst} ASMInst
+ */
+
+/**
+ * @param {ASMInst[]} insts
+ * @param {number} stackFrameSize
+ */
+export function assemble(insts, stackFrameSize) {
+ const rAddr = 12;
+ const rData = 31;
+
+ /**
+ * [0]: data
+ * [.]: body
+ */
+ const p = makeProgram(0);
+
+ /** @type {Map
>} */
+ const callCounts = new Map(); // [addr][prep] = count
+ /** @type {Set} */
+ const structDB = new Set();
+
+ // summarize data
+ let hasRepeatCall = false;
+ let loadStructCount = 0;
+ let sizeWithoutBLTrick = 0; // # of instruction
+ for (const inst of insts) {
+ if (typeof inst === 'number') continue;
+ const { type } = inst;
+ if (type === 'call') {
+ const { addr, prep = [] } = inst;
+ const prepKey = insts2hex(prep);
+ const prepCounts = callCounts.get(addr);
+ if (prepCounts == null) {
+ callCounts.set(addr, new Map([[prepKey, 1]]));
+ } else {
+ prepCounts.set(prepKey, (prepCounts.get(prepKey) ?? 0) + 1);
+ hasRepeatCall = true;
+ }
+ } else if (type === 'struct') {
+ const { hex } = inst;
+ structDB.add(hex);
+ loadStructCount++;
+ sizeWithoutBLTrick += ((hex.length + 7) >>> 3) + 2; // bl L; mflr
+ }
+ }
+
+ const useSharedCall = hasRepeatCall || callCounts.size > 2;
+ const sizeWithBLTrick =
+ ((Array.from(structDB.entries()).reduce((a, [hex]) => a + hex.length, 0) + 7) >>> 3) +
+ loadStructCount + // addi rReg, rData, off
+ (stackFrameSize > 0 ? 0 : 2) +
+ (useSharedCall ? 0 : 1) +
+ 3; // stw, mflr, lwz
+ const useBLTrick = sizeWithBLTrick < sizeWithoutBLTrick;
+
+ /** @type {Map}>} */
+ const offFuncs = new Map();
+ /** @type {number|null} */
+ let offCall = null;
+ /** @type {Map} */
+ const offStructs = new Map();
+
+ if (useSharedCall) {
+ let off = 0;
+ /** @type {string[]} */
+ const funcHexs = [];
+ for (const [addr, prepCounts] of callCounts.entries()) {
+ // put repeated call only
+ if (Array.from(prepCounts).reduce((a, [k, c]) => a + c, 0) <= 1) continue;
+ /** @type {Map} */
+ const preps = new Map();
+ const prepKeys = Array.from(prepCounts.keys()).filter((k) => k !== '');
+ let hex = '';
+ // TODO optimize only when one prep (excluding '') is used
+ if (prepKeys.length === 1) {
+ const [prep] = prepKeys;
+ preps.set(prep, off);
+ hex += prep;
+ }
+ offFuncs.set(addr, { fallback: off + hex.length / 2, preps });
+ // liDX addr
+ hex += insts2hex(liDX(rAddr, addr));
+ // b call
+ off += 4 + hex.length / 2;
+ // push
+ funcHexs.push(hex);
+ }
+ const offDst = Math.max(0, off - 4);
+ offCall = offDst;
+
+ // make data
+ /** callX: */
+ funcHexs.forEach((hex, i, arr) => {
+ p.pushHex(hex);
+ // b call # except the last 1
+ if (i < arr.length - 1) {
+ p.b(offDst);
+ }
+ });
+ /** call: */
+ p.push(ASM.mtctr(rAddr), ASM.bctr());
+ }
+
+ // add struct data
+ if (useBLTrick) {
+ // [4-byte aligned data, chars data]
+ const [aligned, chars] = splitArray(structDB, (hex) => hex.length % 8 === 0);
+ for (const data of [aligned, chars]) {
+ for (const hex of data) {
+ offStructs.set(hex, p.pc);
+ p.pushHex(hex);
+ }
+ }
+ // make 4-byte alignment
+ p.align();
+ }
+
+ // make body
+ const dataSize = p.pc;
+ /** mflr rData */
+ if (useBLTrick) p.push(ASM.mflr(rData));
+ /** for each ASM instructions */
+ for (const inst of insts) {
+ if (typeof inst === 'number') {
+ p.push(inst);
+ } else if (inst.type === 'call') {
+ const { prep = [], addr } = inst;
+ const prepKey = insts2hex(prep);
+ const off = offFuncs.get(addr);
+ if (off == null) {
+ // fallback to prepare and load addr manually
+ p.push(...prep, ...liDX(rAddr, addr));
+ // call
+ if (offCall == null) {
+ p.push(
+ // mtctr rAddr
+ ASM.mtctr(rAddr),
+ // bctrl
+ ASM.bctr(true),
+ );
+ } else {
+ p.bl(offCall);
+ }
+ } else {
+ // bl to callX directly
+ const { fallback, preps } = off;
+ const dst = preps.get(prepKey);
+ if (dst == null) {
+ // fallback to prepare manually
+ p.pushHex(prepKey);
+ p.bl(fallback);
+ } else {
+ // bl to dst directly
+ p.bl(dst);
+ }
+ }
+ } else if (inst.type === 'struct') {
+ const { reg, hex } = inst;
+ const off = offStructs.get(hex);
+ if (off == null) {
+ // fallback to use BL trick here
+ /** bl L */
+ const d = ((hex.length + 7) >> 3) << 2;
+ p.push(ASM.b(4 + d, true));
+ /** (data) */
+ p.pushHex(hex);
+ p.align();
+ /** L: mflr rReg */
+ p.push(ASM.mflr(reg));
+ } else {
+ // use addi directly
+ p.push(ASM.addi(reg, rData, off));
+ }
+ }
+ }
+
+ /** @type {Inst[]} */
+ const prologue = [];
+ if (useBLTrick) stackFrameSize += 4; // for r31
+ if (stackFrameSize > 0) {
+ // stack frame size: 16-byte align
+ stackFrameSize = ((stackFrameSize + 0xf) >> 4) << 4;
+ // stwu r1
+ prologue.push(ASM.stwu(1, 1, -stackFrameSize));
+ if (useBLTrick) {
+ // stw r31
+ prologue.push(ASM.stw(31, 1, stackFrameSize - 4));
+ }
+ }
+ if (useSharedCall || useBLTrick) {
+ prologue.push(ASM.b(4 + dataSize, true));
+ }
+
+ /** @type {Inst[]} */
+ const epilogue = [];
+ if (stackFrameSize > 0) {
+ if (useBLTrick) {
+ // lwz r31
+ epilogue.push(ASM.lwz(31, 1, stackFrameSize - 4));
+ }
+ // addi r1
+ epilogue.push(ASM.addi(1, 1, stackFrameSize));
+ }
+
+ return insts2hex(prologue) + p.hex + insts2hex(epilogue);
+}
diff --git a/site/.vuepress/components/codes/CustomizedDisplay/codegen.js b/site/.vuepress/components/codes/CustomizedDisplay/codegen.js
index e34297e..016262a 100644
--- a/site/.vuepress/components/codes/CustomizedDisplay/codegen.js
+++ b/site/.vuepress/components/codes/CustomizedDisplay/codegen.js
@@ -1,31 +1,25 @@
import { parseJSON } from '../codegen.js';
-import { ASM, makeInst, liDX, str2inst, makeProgram, inst2gecko } from '../asm.js';
+import { ASM, liDX, str2inst, makeProgram, inst2gecko, getFillRectParams } from '../asm.js';
+import { measureText } from '../text.js';
+import { addrs } from '../addrs.js';
+import { parseFormat } from './format.js';
+import { assemble } from './assembler.js';
+import { drawText, fillRect } from './functions.js';
export const lskey = 'config/CustomizedDisplay';
-export const defaultConfig = [
- {
- x: 16,
- y: 192,
- fontSize: 18,
- fgRGB: 0xffffff,
- fgA: 0xff,
- fgRGB2: null,
- fgA2: null,
- fmt: `X
-Y
-Z
-A
-H
-V
-QF `,
- },
-];
+import configDB from './configDB.js';
+export const defaultConfig = [configDB.PAS];
+/** @type {(...args: Parameters) => string} */
+export const format2previewText = (input, version) => parseFormat(input, version).preview;
+
+/** @typedef {'GMSJ01'|'GMSJ0A'|'GMSE01'|'GMSP01'} GameVersion */
/** @param {GameVersion} version */
export function getConfig(version) {
/** @type {typeof defaultConfig} */
const config = typeof localStorage !== 'undefined' && parseJSON(localStorage.getItem(lskey));
return (config instanceof Array ? config : defaultConfig).map(({ fmt, ...o }) => ({
+ ...defaultConfig[0],
...o,
fmt,
text: format2previewText(fmt, version),
@@ -33,412 +27,47 @@ export function getConfig(version) {
}
/**
- * @typedef {number[]} Inst
- * @typedef {8|16|32|'float'} DataType
- * @typedef {(gpr: number)=>Inst} GPRHandler -- (src=gpr, dst=gpr)
- * @typedef {{type: 'gpr'|'fpr'|'sp', index: number}} Dst
- * @typedef {'GMSJ01'|'GMSJ0A'|'GMSE01'|'GMSP01'} GameVersion
- *
- * @typedef {{
- * offset: number
- * dtype: DataType
- * post?: GPRHandler
- * }} FieldInfo
- *
- * @typedef {{
- * id: string
- * pre: GPRHandler
- * }} Base
- *
- * @typedef {{
- * pre: GPRHandler
- * fields: {info: FieldInfo, dst: Dst}[]
- * }} Entry
- */
-
-/** @typedef {{[ver in GameVersion]: GPRHandler}} VBase */
-const bases = {
- gpMarioOriginal: /**@type{VBase}*/ ({
- GMSJ01: (rT) => ASM.lwz(rT, 13, -0x6748),
- GMSE01: (rT) => ASM.lwz(rT, 13, -0x60d8),
- GMSP01: (rT) => ASM.lwz(rT, 13, -0x61b0),
- GMSJ0A: (rT) => ASM.lwz(rT, 13, -0x6218),
- }),
- gpMarDirector: /**@type{VBase}*/ ({
- GMSJ01: (rT) => ASM.lwz(rT, 13, -0x6818),
- GMSE01: (rT) => ASM.lwz(rT, 13, -0x6048),
- GMSP01: (rT) => ASM.lwz(rT, 13, -0x6120),
- GMSJ0A: (rT) => ASM.lwz(rT, 13, -0x6188),
- }),
- gpCamera: /**@type{VBase}*/ ({
- GMSJ01: (rT) => ASM.lwz(rT, 13, -0x5750),
- GMSE01: (rT) => ASM.lwz(rT, 13, -0x7118),
- GMSP01: (rT) => ASM.lwz(rT, 13, -0x7158),
- GMSJ0A: (rT) => ASM.lwz(rT, 13, -0x5768),
- }),
-};
-/** @typedef {keyof typeof bases} BaseId */
-
-/** @type {({id: string, base: BaseId, fmt: string, preview: number} & FieldInfo)[]} */
-const fields = [
- { id: 'x', base: 'gpMarioOriginal', dtype: 'float', offset: 0x10, fmt: '%.0f', preview: 426.39 },
- { id: 'y', base: 'gpMarioOriginal', dtype: 'float', offset: 0x14, fmt: '%.0f', preview: -427.39 },
- { id: 'z', base: 'gpMarioOriginal', dtype: 'float', offset: 0x18, fmt: '%.0f', preview: 428.39 },
- { id: 'angle', base: 'gpMarioOriginal', dtype: 16, offset: 0x96, fmt: '%hu', preview: 1207 },
- {
- id: 'HSpd',
- base: 'gpMarioOriginal',
- dtype: 'float',
- offset: 0xb0,
- fmt: '%.2f',
- preview: 15.15,
- },
- {
- id: 'VSpd',
- base: 'gpMarioOriginal',
- dtype: 'float',
- offset: 0xa8,
- fmt: '%.2f',
- preview: -31.17,
- },
- {
- id: 'QF',
- base: 'gpMarDirector',
- dtype: 32,
- offset: 0x58,
- fmt: '%u',
- preview: 0,
- post: (rT) => ASM.rlwinm(rT, rT, 0, 30, 31, false),
- },
- {
- id: 'CAngle',
- base: 'gpCamera',
- dtype: 16,
- offset: 0xa6,
- fmt: '%hu',
- preview: 9,
- post: (rT) => ASM.addi(rT, rT, -0x8000), // offset by 0x8000
- },
-];
-const fieldDB = Object.fromEntries(
- fields.map(({ id, base, fmt, preview, ...info }) => [
- id.toLowerCase(),
- { base, info, fmt, preview },
- ]),
-);
-
-const store = {
- 8: ASM.stb,
- 16: ASM.sth,
- 32: ASM.stw,
- float: ASM.stfs,
- double: ASM.stfd,
-};
-const load = {
- 8: ASM.lbz,
- 16: ASM.lhz,
- 32: ASM.lwz,
- float: ASM.lfs,
-};
-
-/**
- * @param {number} x
- * @param {number} y
- * @param {number} fontSize
- * @param {number} colorTop
- * @param {number} colorBot
- */
-export function prepareDrawText(x, y, fontSize, colorTop, colorBot) {
- let gpr = 9;
- let fpr = 1;
- let sp = 8;
- let fmt = '';
- let hasFloat = false;
- /** @type {{[id: string]: Entry}} */
- const entries = {};
-
- /** @returns {Dst} */
- function allocInt() {
- if (gpr <= 10) {
- return { type: 'gpr', index: gpr++ };
- } else {
- /** @type {Dst} */
- const dst = { type: 'sp', index: sp };
- sp += 4;
- return dst;
- }
- }
- /** @returns {Dst} */
- function allocFloat() {
- hasFloat = true;
- if (fpr <= 8) {
- return { type: 'fpr', index: fpr++ };
- } else {
- sp += sp & 4; // align 8
- /** @type {Dst} */
- const dst = { type: 'sp', index: sp };
- sp += 8;
- return dst;
- }
- }
- /** @param {Base} base */
- const getEntry = (base) =>
- entries[base.id] ??
- (entries[base.id] = {
- pre: base.pre,
- fields: [],
- });
-
- return {
- /**
- * @param {string} format
- * @param {Base} base
- * @param {FieldInfo} field
- */
- pushValue(format, base, field) {
- fmt += format;
- getEntry(base).fields.push({
- info: field,
- dst: (field.dtype === 'float' ? allocFloat : allocInt)(),
- });
- },
- /**
- * @param {string} text
- */
- pushText(text) {
- fmt += text.replace(/%/g, '%%');
- },
- makeCode() {
- /** @type {Inst[]} */
- const insts = [];
- // sp
- const spAdd = sp === 8 ? 0 : ((sp >> 4) + (sp & 0xf ? 1 : 0)) << 4;
- // params
- for (const { pre, fields: params } of Object.values(entries)) {
- // load base to gpr
- const rBase = 3;
- insts.push(pre(rBase));
- // load all params
- const rField = 5;
- const fField = 9;
- for (const {
- info: { offset: srcoff, dtype, post },
- dst,
- } of params) {
- if (dst.type === 'sp') {
- const dstoff = dst.index;
- if (dtype === 'float') {
- insts.push(
- // lfs fField, offset(rBase)
- load.float(fField, rBase, srcoff),
- // post
- post?.(fField) ?? [],
- // stfd fField, dst.index(r1)
- store.double(fField, 1, dstoff),
- );
- } else {
- insts.push(
- // load rField, offset(rBase)
- load[dtype](rField, rBase, srcoff),
- // post
- post?.(rField) ?? [],
- // stw rField, dst.index(r1)
- store[32](rField, 1, dstoff),
- );
- }
- } else {
- // load to register
- const { index: rDst } = dst;
- insts.push(
- // load rDst
- load[dtype](rDst, rBase, srcoff),
- // post
- post?.(rDst) ?? [],
- );
- }
- }
- }
- // r8 = fmt
- const fmtbuf = str2inst(fmt);
- insts.push(
- // bl 4+len4(fmt)
- ASM.b(4 + (fmtbuf.length << 2), true),
- // .string fmt
- fmtbuf,
- // mflr r8
- ASM.mflr(8),
- );
- /*
- * r3 = x
- * r4 = y
- * r5 = fontSize
- * r6 = colorTop
- * r7 = colorBot
- */
- insts.push(
- liDX(3, x),
- liDX(4, y),
- liDX(5, fontSize),
- liDX(6, colorTop),
- colorTop === colorBot ? ASM.mr(7, 6) : liDX(7, colorBot),
- );
- // cr{set|clr} 6
- insts.push((hasFloat ? ASM.crset : ASM.crclr)(6));
- // DONE
- return { code: insts.flatMap((e) => e), spNeed: spAdd };
- },
- };
-}
-
-const dtype2fmtinfo = {
- 8: { prefix: 'hh', mask: 0xff },
- 16: { prefix: 'h', mask: 0xffff },
- 32: { prefix: '', mask: 0xffffffff },
-};
-
-/**
- * @param {string} input
- * @param {GameVersion} version
- * @param {ReturnType|null} f
- */
-export function format2previewText(input, version, f = null) {
- const regex = /<(.*?)>/g;
- let preview = '';
- /** @type {RegExpExecArray|null} */
- let m = null;
- let i0 = 0;
- while ((m = regex.exec(input))) {
- const { index: i } = m;
- // text
- const text = input.slice(i0, i);
- f?.pushText(text);
- preview += text;
- // arg
- const [fieldId, fmt0, pvw0] = m[1].split('|');
- const field = fieldDB[fieldId.toLowerCase()];
- if (field) {
- const { base: baseId, info, fmt: fmt1, preview: pvw1 } = field;
- const { dtype } = info;
- const fmt2 = fmt0 || fmt1;
- let ipvw = +pvw0;
- if (!pvw0 || !isFinite(ipvw)) ipvw = pvw1;
- let fmt;
- let pvw;
- let padfmt = '';
- if (dtype === 'float') {
- const m = fmt2.trim().match(/^(?:%?(\d*)\.)?(\d+)([eEf]?)$/);
- padfmt = m?.[1] || '';
- const digit = +(m?.[2] || 0);
- const suffix = m?.[3] || 'f';
- fmt = `%${padfmt}.${digit}${suffix}`;
- pvw = ipvw[suffix === 'f' ? 'toFixed' : 'toExponential'](digit);
- if (suffix === 'E') pvw = pvw.toUpperCase();
- } else {
- const { prefix, mask } = dtype2fmtinfo[dtype];
- ipvw &= mask;
- const m = fmt2.trim().match(/^%?(\d*)h{,2}([dioxXu])$/);
- padfmt = m?.[1] || '';
- const t = m?.[2] || 'u';
- fmt = `%${padfmt}${prefix}${t}`;
- if ('di'.includes(t)) {
- if (ipvw > mask >>> 1) ipvw -= mask;
- pvw = ipvw.toString(10);
- } else if (t === 'o') {
- pvw = (ipvw >>> 0).toString(8);
- } else if ('xX'.includes(t)) {
- pvw = (ipvw >>> 0).toString(16);
- } else {
- pvw = (ipvw >>> 0).toString(10);
- }
- }
- pvw = pvw.padStart(+padfmt, padfmt[0] === '0' ? '0' : ' ');
- f?.pushValue(fmt, { id: baseId, pre: bases[baseId][version] }, info);
- preview += pvw;
- } else {
- // fail to parse
- f?.pushText(m[0]);
- preview += m[0];
- }
- // next
- i0 = i + m[0].length;
- }
- const text = input.slice(i0);
- f?.pushText(text);
- preview += text;
- // DONE
- return preview;
-}
-
-const addrsOrig = {
- GMSJ01: 0x80206a00 - 0x2c,
- GMSJ0A: 0x8012556c - 0x2c,
- GMSE01: 0x801441e0 - 0x2c,
- GMSP01: 0x80138e1c - 0x2c,
-};
-const addrsSetup2D = {
- GMSJ01: 0x80035228,
- GMSJ0A: 0x802caecc,
- GMSE01: 0x802eb6bc,
- GMSP01: 0x802e3864,
-};
-const addrDrawText = 0x817f0238;
-const addrDst = 0x817fa000;
-
-/**
+ * @typedef {Parameters[0][number]} ASMInst
* @param {GameVersion} version
*/
export default function codegen(version) {
- const config = getConfig(version);
+ const configs = getConfig(version);
- let spOff = 0;
- const fcodes = /** @type {Inst[]} */ ([]);
+ let stackFrameSize = 0;
- for (const { x, y, fontSize, fgRGB, fgA, fgRGB2, fgA2, fmt } of config) {
- // color
- const colorTop = (fgRGB << 8) | fgA;
- const colorBot = fgRGB2 == null || fgA2 == null ? colorTop : (fgRGB2 << 8) | fgA;
- // prepare drawText
- const f = prepareDrawText(x, y, fontSize, colorTop, colorBot);
- format2previewText(fmt, version, f);
- // update code and sp
- const { code, spNeed } = f.makeCode();
- spOff = Math.max(spOff, spNeed);
- fcodes.push(code);
+ /** @type {ASMInst[]} */
+ const asm = [];
+
+ for (const config of configs) {
+ const { fmt, bgA } = config;
+ const { preview, format, fields } = parseFormat(fmt, version);
+
+ // fill_rect
+ if (bgA) {
+ asm.push(...fillRect(version, config, measureText(preview, version)));
+ }
+
+ // drawText
+ if (fmt.trim()) {
+ const { insts, sp } = drawText(version, config, format, fields);
+ stackFrameSize = Math.max(stackFrameSize, sp);
+ asm.push(...insts);
+ }
}
- const addrOrig = addrsOrig[version];
- const addrSetup2D = addrsSetup2D[version];
+ let body = assemble(asm, stackFrameSize);
+ // align code
+ if (body.length % 16 === 0) body += '60000000';
+ body += '00000000';
- // program
- const program = makeProgram(addrDst);
- // addi r3, r1, 0xE90
- program.push(ASM.addi(3, 1, 0xe90));
- // addi r1, r1, -spOff
- if (spOff) program.push(ASM.addi(1, 1, -spOff));
- // bl setup
- program.bl(addrSetup2D);
- // (drawText)
- for (const code of fcodes) {
- program.push(code);
- program.bl(addrDrawText);
- }
- // addi r1, r1, spOff
- if (spOff) program.push(ASM.addi(1, 1, spOff));
- // b orig+4
- program.b(addrOrig + 4);
-
- // dump code
- const pcode = program.dump();
- const psize = pcode.length;
- return [
- makeInst((0xc6 << 24) | (addrOrig & 0x1ffffff)),
- makeInst(addrDst),
- makeInst((0x06 << 24) | (addrDst & 0x1fffffff)),
- makeInst(psize << 2),
- pcode,
- psize & 1 ? [0] : [],
- ]
- .flatMap((e) => e)
- .map(inst2gecko)
- .join('');
+ const addrDst = addrs.drawWater[version] - 0x2c; // [-0x30, -0x18]
+ return (
+ [
+ 0xc2000000 | (addrDst & 0x1fffffff),
+ body.length >>> 4, // 16 hex-digits per line
+ ]
+ .flatMap((e) => e)
+ .map(inst2gecko)
+ .join('') + body
+ );
}
diff --git a/site/.vuepress/components/codes/CustomizedDisplay/config.vue b/site/.vuepress/components/codes/CustomizedDisplay/config.vue
index 19a095b..99becb1 100644
--- a/site/.vuepress/components/codes/CustomizedDisplay/config.vue
+++ b/site/.vuepress/components/codes/CustomizedDisplay/config.vue
@@ -2,19 +2,26 @@
-
+
config.splice(i, 1, $event)" :version="version" />
|
-
-
+
+
+
+
+
diff --git a/site/.vuepress/components/codes/TextConfig.vue b/site/.vuepress/components/codes/TextConfig.vue
index c420645..e54d120 100644
--- a/site/.vuepress/components/codes/TextConfig.vue
+++ b/site/.vuepress/components/codes/TextConfig.vue
@@ -15,6 +15,23 @@
{{l.fgColor2}}
{{l.alpha}}/255={{(fgA2/2.55).toFixed(1)}}%
+
@@ -41,6 +58,7 @@ export default {
},
...Object.fromEntries([
'x', 'y', 'fontSize', 'fgRGB', 'fgA', 'fgRGB2', 'fgA2',
+ 'bgRGB', 'bgA', 'bgLeft', 'bgRight', 'bgTop', 'bgBot',
].map(k => [k, makeField(k)])),
},
methods: {
@@ -72,7 +90,7 @@ input[type=number], td.right {
text-align: right;
}
input[type="number"] {
- width: 3em;
+ width: 2em;
margin: 0 2px;
}
.appearance > div {
diff --git a/site/.vuepress/components/codes/addrs.js b/site/.vuepress/components/codes/addrs.js
new file mode 100644
index 0000000..04636a8
--- /dev/null
+++ b/site/.vuepress/components/codes/addrs.js
@@ -0,0 +1,68 @@
+export const addrs = {
+ drawText: 0x817f0238,
+ drawWater: {
+ GMSJ01: 0x80206a00,
+ GMSJ0A: 0x8012556c,
+ GMSE01: 0x801441e0,
+ GMSP01: 0x80138e1c,
+ },
+ fillRect: {
+ GMSJ01: 0x80201ea8,
+ GMSJ0A: 0x80121660,
+ GMSE01: 0x80140390,
+ GMSP01: 0x80134f0c,
+ },
+ setup2D: {
+ GMSJ01: 0x80035228,
+ GMSJ0A: 0x802caecc,
+ GMSE01: 0x802eb6bc,
+ GMSP01: 0x802e3864,
+ },
+ getPollutionDegree: {
+ GMSJ01: 0x801ef6b8,
+ GMSE01: 0x8019db20,
+ GMSP01: 0x801963a8,
+ GMSJ0A: 0x8017e26c,
+ },
+ checkStickRotate: {
+ GMSJ01: 0x80130758,
+ GMSE01: 0x80251304,
+ GMSP01: 0x80249090,
+ GMSJ0A: 0x80231054,
+ },
+};
+
+export const r13offs = {
+ gpMarioOriginal: {
+ GMSJ01: -0x6748,
+ GMSE01: -0x60d8,
+ GMSP01: -0x61b0,
+ GMSJ0A: -0x6218,
+ },
+ gpMarDirector: {
+ GMSJ01: -0x6818,
+ GMSE01: -0x6048,
+ GMSP01: -0x6120,
+ GMSJ0A: -0x6188,
+ },
+ gpCamera: {
+ GMSJ01: -0x5750,
+ GMSE01: -0x7118,
+ GMSP01: -0x7158,
+ GMSJ0A: -0x5768,
+ },
+ gpPollution: {
+ GMSJ01: -0x6518,
+ GMSE01: -0x62f0,
+ GMSP01: -0x63c8,
+ GMSJ0A: -0x6430,
+ },
+};
+
+// r1 offset of J2DGrafContext in TGCConsole2::perform()
+export const ctxSpOff = {
+ GMSJ01: 0xe90,
+ GMSJ0A: 0xbec,
+ GMSE01: 0xbd0,
+ GMSP01: 0xbe4,
+};
diff --git a/site/.vuepress/components/codes/asm.js b/site/.vuepress/components/codes/asm.js
index 6117066..c404c92 100644
--- a/site/.vuepress/components/codes/asm.js
+++ b/site/.vuepress/components/codes/asm.js
@@ -1,7 +1,8 @@
-import * as Encoding from 'encoding-japanese';
+import charInfoJP from '../../data/charInfo-JP.json';
+import charInfoEU from '../../data/charInfo-EU.json';
/**
- * @typedef {number[]} Inst
+ * @typedef {number} Inst
*
* @typedef {(
* rT: number,
@@ -27,7 +28,7 @@ import * as Encoding from 'encoding-japanese';
* rT: number,
* rA: number,
* rB: number,
- * Rc: number|boolean,
+ * Rc?: number|boolean,
* ) => Inst} InstX
* @typedef {(
* rS: number,
@@ -43,13 +44,6 @@ import * as Encoding from 'encoding-japanese';
* ) => Inst} InstI
*/
-/** @param {number} inst */
-export const makeInst = (inst) => {
- // const buf = Inst.alloc(4);
- // buf.writeUint32BE(inst >>> 0);
- // return buf;
- return [inst];
-};
/**
* @param {number} op
* @param {number} rT
@@ -57,7 +51,7 @@ export const makeInst = (inst) => {
* @param {number} D
*/
const InstD = (op, rT, rA, D) =>
- makeInst(((op & 0x3f) << 26) | ((rT & 0x1f) << 21) | ((rA & 0x1f) << 16) | (D & 0xffff));
+ ((op & 0x3f) << 26) | ((rT & 0x1f) << 21) | ((rA & 0x1f) << 16) | (D & 0xffff);
/**
* @param {number} op
* @param {number} rT
@@ -67,14 +61,12 @@ const InstD = (op, rT, rA, D) =>
* @param {number} Rc
*/
const InstX = (op, rT, rA, rB, op2, Rc) =>
- makeInst(
- ((op & 0x3f) << 26) |
- ((rT & 0x1f) << 21) |
- ((rA & 0x1f) << 16) |
- ((rB & 0x1f) << 11) |
- ((op2 & 0x3ff) << 1) |
- Rc,
- );
+ ((op & 0x3f) << 26) |
+ ((rT & 0x1f) << 21) |
+ ((rA & 0x1f) << 16) |
+ ((rB & 0x1f) << 11) |
+ ((op2 & 0x3ff) << 1) |
+ Rc;
/**
* @param {number} op
* @param {number} RS
@@ -85,15 +77,13 @@ const InstX = (op, rT, rA, rB, op2, Rc) =>
* @param {number} Rc
*/
const InstM = (op, RA, RS, SH, MB, ME, Rc) =>
- makeInst(
- ((op & 0x3f) << 26) |
- ((RS & 0x1f) << 21) |
- ((RA & 0x1f) << 16) |
- ((SH & 0x1f) << 11) |
- ((MB & 0x1f) << 6) |
- ((ME & 0x1f) << 1) |
- Rc,
- );
+ ((op & 0x3f) << 26) |
+ ((RS & 0x1f) << 21) |
+ ((RA & 0x1f) << 16) |
+ ((SH & 0x1f) << 11) |
+ ((MB & 0x1f) << 6) |
+ ((ME & 0x1f) << 1) |
+ Rc;
/**
* @param {number} op
* @param {number} LL
@@ -101,14 +91,17 @@ const InstM = (op, RA, RS, SH, MB, ME, Rc) =>
* @param {number} LK
*/
const InstI = (op, LL, AA, LK) =>
- makeInst(((op & 0x3f) << 26) | ((LL & 0xffffff) << 2) | ((AA & 1) << 1) | (LK & 1));
+ ((op & 0x3f) << 26) | ((LL & 0xffffff) << 2) | ((AA & 1) << 1) | (LK & 1);
/** @type {(op: number) => InstD} */
const makeInstD = (op) => (rT, rA, D) => InstD(op, rT, rA, D);
/** @type {(op: number) => InstDS} */
const makeInstDS = (op) => (rA, rS, D) => InstD(op, rA, rS, D);
/** @type {(op: number, op2: number) => InstX} */
-const makeInstX = (op, op2) => (rT, rA, rB, Rc) => InstX(op, rT, rA, rB, op2, +Rc);
+const makeInstX =
+ (op, op2) =>
+ (rT, rA, rB, Rc = 0) =>
+ InstX(op, rT, rA, rB, op2, +Rc);
/** @type {(op: number, op2: number) => InstXS} */
const makeInstXS = (op, op2) => (rA, rS, rB, Rc) => InstX(op, rA, rS, rB, op2, +Rc);
/** @type {(op: number) => InstM} */
@@ -132,6 +125,9 @@ export const ASM = {
lhz: makeInstD(40),
lwz: makeInstD(32),
lfs: makeInstD(48),
+ lfd: makeInstD(50),
+ // add
+ add: makeInstX(31, 266),
// li rT, D
addi: makeInstD(14),
li: (/**@type{number}*/ rT, /**@type{number}*/ D) => InstD(14, rT, 0, D),
@@ -144,8 +140,10 @@ export const ASM = {
mr: (/**@type{number}*/ rT, /**@type{number}*/ rA, flag = 0) => InstX(31, rA, rT, rA, 444, flag),
// mask
rlwinm: makeInstM(21),
+ rlwimi: makeInstM(20),
// b
b: makeInstI(18),
+ bctr: (/**@type{number|boolean}*/ LK = 0) => InstX(19, 0b10100, 0, 0, 528, LK ? 1 : 0),
// mflr
mflr: (/**@type{number}*/ rT) => InstX(31, rT, 8, 0, 339, 0),
mfctr: (/**@type{number}*/ rT) => InstX(31, rT, 9, 0, 339, 0),
@@ -155,6 +153,22 @@ export const ASM = {
// cr
crset: (/**@type{number}*/ B) => InstX(19, B, B, B, 289, 0),
crclr: (/**@type{number}*/ B) => InstX(19, B, B, B, 193, 0),
+ // float
+ fmr: (/**@type{number}*/ fT, /**@type{number}*/ fB, Rc = 0) => InstX(63, fT, 0, fB, 72, Rc),
+};
+
+export const $load = {
+ 8: ASM.lbz,
+ 16: ASM.lhz,
+ 32: ASM.lwz,
+ float: ASM.lfs,
+};
+export const $store = {
+ 8: ASM.stb,
+ 16: ASM.sth,
+ 32: ASM.stw,
+ float: ASM.stfs,
+ double: ASM.stfd,
};
/**
@@ -163,26 +177,46 @@ export const ASM = {
*/
export function liDX(rT, D) {
if (-0x8000 <= D && D < 0x8000) {
- return ASM.li(rT, D);
+ return [ASM.li(rT, D)];
} else if ((D & 0xffff) === 0) {
- return ASM.lis(rT, D >>> 16);
+ return [ASM.lis(rT, D >>> 16)];
} else {
const h = D >>> 16;
const l = D & 0xffff;
- return [...ASM.lis(rT, h), ...ASM.ori(rT, rT, l)];
+ return [ASM.lis(rT, h), ASM.ori(rT, rT, l)];
}
}
-/** @param {string} s */
-export function strlen(s) {
- const fmtbuf = Encoding.convert(Encoding.stringToCode(s), 'SJIS');
- return fmtbuf.length; // not NUL terminated
+/**
+ * @param {string} s
+ * @param {string} version
+ */
+export function str2bytes(s, version) {
+ /** @type {Record
} */
+ const charInfo = version.startsWith('GMSJ') ? charInfoJP : charInfoEU; // TODO US
+ const fmtbuf = Array.from(s).flatMap((c) => {
+ const code = charInfo[c]?.code ?? c.charCodeAt(0);
+ return code >= 0x100 ? [code >> 16, code & 0xff] : [code];
+ });
+ fmtbuf.push(0); // NUL terminated
+ return fmtbuf;
}
-/** @param {string} s */
-export function str2inst(s) {
- const fmtbuf = Encoding.convert(Encoding.stringToCode(s), 'SJIS');
- fmtbuf.push(0); // NUL terminated
+/**
+ * @param {string} s
+ * @param {string} version
+ */
+export const str2hex = (s, version) =>
+ str2bytes(s, version)
+ .map((x) => x.toString(16).toUpperCase().padStart(2, '0'))
+ .join('');
+
+/**
+ * @param {string} s
+ * @param {string} version
+ */
+export function str2inst(s, version) {
+ const fmtbuf = str2bytes(s, version);
const fmtlen = fmtbuf.length;
const fmtlen3 = fmtlen & 3;
const pad = fmtlen3 ? 4 - fmtlen3 : 0;
@@ -194,32 +228,90 @@ export function str2inst(s) {
return insts;
}
-/** @param {number} pc */
-export function makeProgram(pc) {
- /** @type {Inst[]} */
- const bufs = [];
- return {
- pc,
- /**
- * @param {number} dst
- * @param {boolean} LL
- */
- b(dst, LL = false) {
- // TODO check overflow
- this.push(ASM.b(dst - this.pc, LL));
- },
- /** @param {number} dst */
- bl(dst) {
- this.b(dst, true);
- },
- /** @param {Inst[]} codes */
- push(...codes) {
- bufs.push(...codes);
- this.pc += codes.reduce((a, e) => a + e.length, 0) << 2;
- },
- dump: () => bufs.flatMap((e) => e),
- };
-}
+/**
+ * @param {number} pc
+ * @param {string} [hex]
+ */
+export const makeProgram = (pc, hex = '') => ({
+ pc,
+ hex,
+ /**
+ * @param {number} dst
+ * @param {boolean} LL
+ */
+ b(dst, LL = false) {
+ // TODO check overflow
+ this.push(ASM.b(dst - this.pc, LL));
+ },
+ /** @param {number} dst */
+ bl(dst) {
+ this.b(dst, true);
+ },
+ /** @param {Inst[]} codes */
+ push(...codes) {
+ this.hex += codes.map(inst2gecko).join('');
+ this.pc += codes.length << 2;
+ },
+ /** @param {string} data */
+ pushHex(data) {
+ this.hex += data;
+ this.pc += data.length >> 1;
+ },
+ align() {
+ const l4 = this.pc % 4;
+ if (l4) {
+ const diff = 4 - l4;
+ this.hex += ''.padEnd(diff << 1, '0');
+ this.pc += diff;
+ }
+ },
+});
/** @param {number} x */
export const inst2gecko = (x) => (x >>> 0).toString(16).toUpperCase().padStart(8, '0');
+/** @param {Inst[]} insts */
+export const insts2hex = (insts) => insts.map(inst2gecko).join('');
+
+/**
+ * @param {{
+ * x: number,
+ * y: number,
+ * fontSize: number,
+ * bgRGB: number,
+ * bgA: number,
+ * bgLeft: number,
+ * bgRight: number,
+ * bgTop: number,
+ * bgBot: number
+ * }} opt
+ * @param {{width: number, height: number}} size
+ **/
+export const getFillRectParams = (
+ { x, y, fontSize, bgRGB, bgA, bgLeft, bgRight, bgTop, bgBot },
+ { width, height },
+) => [
+ // rect
+ x - bgLeft, // x0
+ y - fontSize - bgTop, // y0
+ x + Math.ceil((width * fontSize) / 20) + bgRight, // x1
+ y - fontSize + Math.ceil((height * fontSize) / 20) + bgBot, // y1
+ // color
+ (bgRGB << 8) | bgA,
+];
+
+/**
+ * @param {{
+ * x: number,
+ * y: number,
+ * fontSize: number,
+ * fgRGB: number
+ * fgA: number
+ * fgRGB2: number | null
+ * fgA2: number | null
+ * }} config
+ */
+export function getDrawTextOpt({ x, y, fontSize, fgRGB, fgA, fgRGB2, fgA2 }) {
+ const colorTop = (fgRGB << 8) | fgA;
+ const colorBot = fgRGB2 == null || fgA2 == null ? colorTop : (fgRGB2 << 8) | fgA;
+ return [((x & 0xffff) << 16) | (y & 0xffff), fontSize, colorTop, colorBot];
+}
diff --git a/site/.vuepress/components/codes/labels.json b/site/.vuepress/components/codes/labels.json
index 7ea4e0b..458b625 100644
--- a/site/.vuepress/components/codes/labels.json
+++ b/site/.vuepress/components/codes/labels.json
@@ -6,7 +6,13 @@
"fgColorGrad": "グラデーション",
"fgColor1": "文字色(上):",
"fgColor2": "文字色(下):",
- "alpha": "不透明度="
+ "alpha": "不透明度=",
+ "bgColor": "背景色:",
+ "bgOffset": "背景位置:",
+ "left": "左",
+ "right": "右",
+ "top": "上",
+ "bottom": "下"
},
"en-US": {
"location": "Location: ",
@@ -15,6 +21,22 @@
"fgColorGrad": "Gradient",
"fgColor1": "Font color(Top): ",
"fgColor2": "Font color(Bottom): ",
- "alpha": "Alpha="
+ "alpha": "Alpha=",
+ "bgColor": "Background color: ",
+ "bgOffset": "Background offset: ",
+ "left": "Left",
+ "right": "Right",
+ "top": "Top",
+ "bottom": "Bottom"
+ },
+ "fr-FR": {
+ "location": "Position : ",
+ "fontSize": "Taille de police : ",
+ "fgColor": "Couleur du texte : ",
+ "fgColorGrad": "Dégradé",
+ "fgColor1": "Couleur du texte (haut) : ",
+ "fgColor2": "Couleur du texte (bas) : ",
+ "bgColor": "Couleur de fond : ",
+ "alpha": "Alpha = "
}
}
diff --git a/site/.vuepress/components/codes/qft/codegen.js b/site/.vuepress/components/codes/qft/codegen.js
index 88a286f..ad66eba 100644
--- a/site/.vuepress/components/codes/qft/codegen.js
+++ b/site/.vuepress/components/codes/qft/codegen.js
@@ -1,10 +1,13 @@
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,
- width: 112,
fontSize: 20,
fgRGB: 0xffffff,
fgA: 0xff,
@@ -12,6 +15,10 @@ export const defaultConfig = {
fgA2: null,
bgRGB: 0x000000,
bgA: 0x80,
+ bgLeft: 0,
+ bgRight: 2,
+ bgTop: 2,
+ bgBot: 0,
freezeDuration: 30,
freeze: {
yellowCoin: false,
@@ -35,6 +42,7 @@ export const defaultConfig = {
},
};
+/** @returns {typeof defaultConfig} */
export function getConfig() {
const config =
(typeof localStorage !== 'undefined' && parseJSON(localStorage.getItem(lskey))) || {};
@@ -45,6 +53,7 @@ export function getConfig() {
...defaultConfig.freeze,
...config.freeze,
},
+ text: getPreviewText(),
};
}
@@ -57,8 +66,10 @@ import * as GMSP01 from './code/GMSP01.js';
import * as GMSJ0A from './code/GMSJ0A.js';
export const codes = { GMSJ01, GMSE01, GMSP01, GMSJ0A };
-import statusDB from './code/status.js';
-export const statusKeys = Object.keys(statusDB);
+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
@@ -100,7 +111,7 @@ export default function codegen(version, baseCode) {
const hook = freezeCodeHooks[key];
if (hook) {
if (key === 'blueCoin') {
- const addr = hook;
+ const addr = /**@type{number}*/ (hook);
// special: needs to adjust QF -> use separate C2 instead
code += [
0xc2000000 + (addr & 0x1ffffff),
@@ -234,25 +245,24 @@ export default function codegen(version, baseCode) {
// ui
/* bounds */
- const { x, y, fontSize, width } = config;
+ 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 += [
- x, // x1
- y - fontSize - 2, // y1
- x + width * scale, // x2
- y, // y2
- ]
- .map(int2gecko)
- .join('');
+ code += rect.map(int2gecko).join('');
code += '25753a253032752e2530337500000000'; // fmt
- /* fontSize, fgColor, bgColor */
- code += '077F0110 00000010';
- const bgColor = (config.bgRGB & 0xffffff) * 256 + (config.bgA & 0xff);
+ /**
+ * 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, '');
}
diff --git a/site/.vuepress/components/codes/qft/config.vue b/site/.vuepress/components/codes/qft/config.vue
index cc0437c..b1017c0 100644
--- a/site/.vuepress/components/codes/qft/config.vue
+++ b/site/.vuepress/components/codes/qft/config.vue
@@ -2,17 +2,7 @@
{{ l.freeze.h3 }}
@@ -35,31 +25,12 @@