diff --git a/Codes.xml b/Codes.xml index d4f8dbb..1009226 100644 --- a/Codes.xml +++ b/Codes.xml @@ -2780,8 +2780,8 @@
diff --git a/changelog.md b/changelog.md
index d68968b..44009a4 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,4 +1,13 @@
# Changelog
+## Jan 31, 2023
+### Updated 'Customized Display'
+- Rewrote with C2
+- Implemented a more complex assembler/compiler to support function call
+- Add options
+ - Invincibility Timer
+ - Pollution Degree
+ - Spin Jump Condition Check
+
## Jan 28, 2023
### Rewrote 'drawText'
- Reduced parameters to struct pointer + format string + varargs
diff --git a/package-lock.json b/package-lock.json
index fe43257..f1e5ce1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,7 +17,6 @@
"@types/encoding-japanese": "^2.0.1",
"@vuepress/plugin-back-to-top": "1.9.7",
"@vuepress/plugin-medium-zoom": "1.9.7",
- "encoding-japanese": "^2.0.0",
"jsdom": "20.0.2",
"pre-commit": "1.2.2",
"prettier": "2.7.1",
@@ -6574,15 +6573,6 @@
"node": ">= 0.8"
}
},
- "node_modules/encoding-japanese": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.0.0.tgz",
- "integrity": "sha512-++P0RhebUC8MJAwJOsT93dT+5oc5oPImp1HubZpAuCZ5kTLnhuuBhKHj2jJeO/Gj93idPBWmIuQ9QWMe5rX3pQ==",
- "dev": true,
- "engines": {
- "node": ">=8.10.0"
- }
- },
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@@ -22759,12 +22749,6 @@
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"dev": true
},
- "encoding-japanese": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.0.0.tgz",
- "integrity": "sha512-++P0RhebUC8MJAwJOsT93dT+5oc5oPImp1HubZpAuCZ5kTLnhuuBhKHj2jJeO/Gj93idPBWmIuQ9QWMe5rX3pQ==",
- "dev": true
- },
"end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
diff --git a/package.json b/package.json
index 36b5eca..2e27ddc 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,6 @@
"@types/encoding-japanese": "^2.0.1",
"@vuepress/plugin-back-to-top": "1.9.7",
"@vuepress/plugin-medium-zoom": "1.9.7",
- "encoding-japanese": "^2.0.0",
"jsdom": "20.0.2",
"pre-commit": "1.2.2",
"prettier": "2.7.1",
diff --git a/site/.vuepress/components/Generator.vue b/site/.vuepress/components/Generator.vue
index d55e4d6..29d5553 100644
--- a/site/.vuepress/components/Generator.vue
+++ b/site/.vuepress/components/Generator.vue
@@ -132,13 +132,6 @@ export default {
codeConfigs: {},
};
},
- created() {
- this.codeConfigs = {
- qft: getConfigQFT(),
- PatternSelector: getConfigPS(),
- CustomizedDisplay: getConfigCD(this.version),
- };
- },
methods: {
getLabel(key) {
return translate(key, this.$lang);
@@ -166,6 +159,13 @@ export default {
JSON.stringify({ version: e }),
]);
} catch {}
+
+ // update config for preview
+ this.codeConfigs = {
+ qft: getConfigQFT(),
+ PatternSelector: getConfigPS(),
+ CustomizedDisplay: getConfigCD(e),
+ };
},
onFormatChanged(e) {
this.selectedFormat = e;
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 ccae03c..016262a 100644
--- a/site/.vuepress/components/codes/CustomizedDisplay/codegen.js
+++ b/site/.vuepress/components/codes/CustomizedDisplay/codegen.js
@@ -1,19 +1,19 @@
import { parseJSON } from '../codegen.js';
-import {
- ASM,
- makeInst,
- liDX,
- str2inst,
- makeProgram,
- inst2gecko,
- getFillRectParams,
-} 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';
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} */
@@ -27,423 +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 {string} version
- * @param {{
- * x: number
- * y: number
- * fontSize: number
- * fgRGB: number
- * fgA: number
- * fgRGB2: number | null
- * fgA2: number | null
- * }} drawTextOpt
- */
-export function prepareDrawText(version, { x, y, fontSize, fgRGB, fgA, fgRGB2, fgA2 }) {
- const colorTop = (fgRGB << 8) | fgA;
- const colorBot = fgRGB2 == null || fgA2 == null ? colorTop : (fgRGB2 << 8) | fgA;
-
- let gpr = 5;
- 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 = 11; // tmp GPR
- const fField = 9; // tmp FPR
- 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) ?? [],
- );
- }
- }
- }
- // r3 = opt // sizeof(opt) = 0x10
- // r4 = fmt
- const fmtbuf = str2inst(fmt, version);
- insts.push(
- // bl 4+sizeof(opt)+len4(fmt)
- ASM.b(0x14 + (fmtbuf.length << 2), true),
- // opt
- [((x & 0xffff) << 16) | (y & 0xffff), fontSize, colorTop, colorBot],
- // .string fmt
- fmtbuf,
- // mflr r3
- ASM.mflr(3),
- // addi r4, r3, sizeof(opt)
- ASM.addi(4, 3, 0x10),
- );
- // 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;
-}
-
-import addrs from '../addrs.js';
-const addrOrigOff = -0x2c; // drawWater - [-0x30, -0x18]
-const addrDst = 0x817fa000;
-
-/**
+ * @typedef {Parameters[0][number]} ASMInst
* @param {GameVersion} version
*/
export default function codegen(version) {
const configs = getConfig(version);
- let spOff = 0;
- const fcodes = /** @type {Inst[]} */ ([]);
- const bcodes = /** @type {Inst[]} */ ([]);
+ let stackFrameSize = 0;
+
+ /** @type {ASMInst[]} */
+ const asm = [];
for (const config of configs) {
- const { fontSize, fmt, bgA } = config;
- // prepare drawText
- const f = prepareDrawText(version, config);
- const text = format2previewText(fmt, version, f);
- // text code
- if (fmt.trim()) {
- // update code and sp
- const { code, spNeed } = f.makeCode();
- spOff = Math.max(spOff, spNeed);
- fcodes.push(code);
- }
- // background code
+ const { fmt, bgA } = config;
+ const { preview, format, fields } = parseFormat(fmt, version);
+
+ // fill_rect
if (bgA) {
- const { width, height } = measureText(text, version);
- const w = Math.ceil((width * fontSize) / 20);
- const h = Math.ceil((height * fontSize) / 20);
- bcodes.push(
- [
- // bl 4+sizeof(rect)+sizeof(color)
- ASM.b(0x18, true),
- // fill_rect params
- ...getFillRectParams(config, measureText(text, version)),
- // mflr r3
- ASM.mflr(3),
- // addi r4, r3, sizeof(rect)
- ASM.addi(4, 3, 0x10),
- ].flatMap((e) => e),
- );
+ 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 = addrs.drawWater[version] + addrOrigOff;
- const addrFillRect = addrs.fillRect[version];
+ let body = assemble(asm, stackFrameSize);
+ // align code
+ if (body.length % 16 === 0) body += '60000000';
+ body += '00000000';
- // program
- const program = makeProgram(addrDst);
- // la r3, ctx(r1)
- // program.push(ASM.addi(3, 1, addrs.ctxSpOff[version]));
- // addi r1, r1, -spOff
- if (spOff) program.push(ASM.addi(1, 1, -spOff));
- // bl J2DGrafContext::setup2D
- // program.bl(addrs.setup2D[version]);
- // (fill_rect)
- for (const code of bcodes) {
- program.push(code);
- program.bl(addrFillRect);
- }
- // (drawText)
- for (const code of fcodes) {
- program.push(code);
- program.bl(addrs.drawText);
- }
- // 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/configDB.js b/site/.vuepress/components/codes/CustomizedDisplay/configDB.js
index fd1c08d..a9293a4 100644
--- a/site/.vuepress/components/codes/CustomizedDisplay/configDB.js
+++ b/site/.vuepress/components/codes/CustomizedDisplay/configDB.js
@@ -35,7 +35,7 @@ V Spd `,
...base,
x: 16,
y: 192,
- fontSize: 18,
+ fontSize: 16,
fmt: `X
Y
Z
@@ -43,7 +43,10 @@ A
C
H
V
-QF `,
+QF
+I
+G
+Spin `,
},
rect: {
...base,
diff --git a/site/.vuepress/components/codes/CustomizedDisplay/fields.js b/site/.vuepress/components/codes/CustomizedDisplay/fields.js
new file mode 100644
index 0000000..b0ae70a
--- /dev/null
+++ b/site/.vuepress/components/codes/CustomizedDisplay/fields.js
@@ -0,0 +1,125 @@
+import { ASM } from '../asm.js';
+import { makeDirectLoaderASM, makeFunctionLoader, rTmp, fTmp } from './loader.js';
+import { addrs, r13offs } from '../addrs.js';
+
+/**
+ * @typedef {ReturnType} Loader
+ * @typedef {ReturnType[number]} ASMInst
+ * @typedef {Parameters[0]} GameVersion
+ *
+ * @typedef {Parameters[1]} DirectLoadFunc
+ * @typedef {{
+ * dtype: Loader['dtype'],
+ * base: keyof typeof bases,
+ * offset: number,
+ * postprocess?: (rT: number)=>ASMInst[]
+ * }} DirectLoader
+ */
+
+const r13bases = /**@type{{[k in keyof typeof r13offs]: DirectLoadFunc}}*/ (
+ Object.fromEntries(
+ Object.entries(r13offs).map(([k, ver2off]) => [
+ k,
+ /** @type {DirectLoadFunc} */
+ (rT, ver) => [ASM.lwz(rT, 13, ver2off[ver])],
+ ]),
+ )
+);
+export const bases = {
+ ...r13bases,
+};
+
+/** @type {Array<(Loader|DirectLoader) & {id:string} & (
+ * {fmt: string, preview: number} |
+ * {fmt: '%s', preview: (ver: GameVersion) => string}
+ * )>}
+ */
+export 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,
+ postprocess: (rT) => [ASM.rlwinm(rT, rT, 0, 30, 31, false)],
+ },
+ {
+ id: 'CAngle',
+ base: 'gpCamera',
+ dtype: 16,
+ offset: 0xa6,
+ fmt: '%hu',
+ preview: 9,
+ postprocess: (rT) => [ASM.addi(rT, rT, -0x8000)], // offset by 0x8000
+ },
+ {
+ id: 'invinc',
+ base: 'gpMarioOriginal',
+ dtype: 16,
+ offset: 0x14c,
+ fmt: '%hd',
+ preview: 30,
+ postprocess: (rT) => [ASM.rlwinm(rT, rT, 30, 2, 31, false)], // QF to frame (>>2)
+ },
+ {
+ id: 'goop',
+ fmt: '%d',
+ preview: 600,
+ ...makeFunctionLoader(32, (ver) => [
+ {
+ type: 'call',
+ addr: addrs.getPollutionDegree[ver],
+ prep: [ASM.lwz(3, 13, r13offs.gpPollution[ver])],
+ },
+ ]),
+ },
+ {
+ id: 'spin',
+ fmt: '%s',
+ // TODO better char mapping
+ preview: (ver) => String.fromCharCode(ver.startsWith('GMSJ') ? 0xff20 : 0x40),
+ dtype: 32,
+ calling: true,
+ asm: (ver, dst) => [
+ {
+ type: 'call',
+ addr: addrs.checkStickRotate[ver],
+ prep: [
+ ASM.lwz(3, 13, r13offs.gpMarioOriginal[ver]),
+ ASM.stwu(1, 1, -0x10),
+ ASM.addi(4, 1, 8),
+ ],
+ },
+ // 0 (A) 0
+ { type: 'struct', reg: rTmp, hex: ver.startsWith('GMSJ') ? '00819700' : '004000' },
+ ...(dst.type === 'stack'
+ ? [ASM.add(3, rTmp, 3), ASM.stw(3, 1, dst.off)]
+ : [ASM.add(dst.num, rTmp, 3)]),
+ // finalize
+ ASM.addi(1, 1, 0x10),
+ ],
+ },
+];
+
+export const fieldDB = Object.fromEntries(fields.map((o) => [o.id.toLowerCase(), o]));
diff --git a/site/.vuepress/components/codes/CustomizedDisplay/format.js b/site/.vuepress/components/codes/CustomizedDisplay/format.js
new file mode 100644
index 0000000..1f9ad5d
--- /dev/null
+++ b/site/.vuepress/components/codes/CustomizedDisplay/format.js
@@ -0,0 +1,92 @@
+import { fieldDB } from './fields.js';
+
+/**
+ * @typedef {Parameters[0]} GameVersion
+ */
+
+const dtype2fmtinfo = {
+ 8: { prefix: 'hh', mask: 0xff },
+ 16: { prefix: 'h', mask: 0xffff },
+ 32: { prefix: '', mask: 0xffffffff },
+};
+
+/**
+ * @param {string} input
+ * @param {GameVersion} version
+ */
+export function parseFormat(input, version) {
+ const regex = /<(.*?)>/g;
+ let preview = '';
+ let format = '';
+ /** @type {(typeof fieldDB)[string][]} */
+ const fields = [];
+ /** @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);
+ preview += text;
+ format += text.replace(/%/g, '%%');
+ // arg
+ const [fieldId, fmt0, pvw0] = m[1].split('|');
+ const field = fieldDB[fieldId.toLowerCase()];
+ if (field) {
+ const { dtype } = field;
+ let fmt;
+ let pvw;
+ if (typeof field.preview === 'function') {
+ // TODO preview of %s field
+ fmt = field.fmt;
+ pvw = field.preview(version);
+ } else {
+ const fmt2 = fmt0 || field.fmt;
+ let ipvw = +pvw0;
+ if (!pvw0 || !isFinite(ipvw)) ipvw = field.preview;
+ 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' : ' ');
+ }
+ preview += pvw;
+ format += fmt;
+ fields.push(field);
+ } else {
+ // fail to parse
+ preview += m[0];
+ format += m[0].replace(/%/g, '%%');
+ }
+ // next
+ i0 = i + m[0].length;
+ }
+ const text = input.slice(i0);
+ preview += text;
+ format += text.replace(/%/g, '%%');
+ // DONE
+ return { preview, format, fields };
+}
diff --git a/site/.vuepress/components/codes/CustomizedDisplay/functions.js b/site/.vuepress/components/codes/CustomizedDisplay/functions.js
new file mode 100644
index 0000000..35eb00d
--- /dev/null
+++ b/site/.vuepress/components/codes/CustomizedDisplay/functions.js
@@ -0,0 +1,140 @@
+import { ASM, $load, insts2hex, str2hex, getDrawTextOpt, getFillRectParams } from '../asm.js';
+import { addrs } from '../addrs.js';
+import { rTmp, fTmp } from './loader.js';
+import { bases } from './fields.js';
+
+/**
+ * @typedef {import('./loader.js').Loader} Loader
+ * @typedef {Parameters[0]} GameVersion
+ * @typedef {ReturnType[number]} ASMInst
+ * @typedef {(typeof import('./fields.js').fieldDB)[string]} Field
+ *
+ * @typedef {{type: 'reg', num: number}} LoadDstReg
+ * @typedef {{type: 'stack', off: number}} LoadDstStack
+ * @typedef {LoadDstReg|LoadDstStack} LoadDst
+ * @typedef {{
+ * dtype: Loader['dtype'],
+ * base: keyof typeof import('./fields').bases,
+ * offset: number,
+ * postprocess?: (rT: number)=>ASMInst[]
+ * }} DirectLoader
+ */
+
+/**
+ * @param {GameVersion} version
+ * @param {Parameters[0]} opt
+ * @param {string} fmt
+ * @param {Field[]} fields
+ */
+export function drawText(version, opt, fmt, fields) {
+ /** @type {ASMInst[]} */
+ const insts = [];
+
+ let gpr = 5;
+ let fpr = 1;
+ let sp = 8;
+ /** @type {Map)[]>} */
+ const simples = new Map();
+ /** @type {{asm: Loader['asm'], dst: LoadDstStack, dtype: Loader['dtype']}[]} */
+ const callingStacks = [];
+ /** @type {{asm: Loader['asm'], dst: LoadDstReg, dtype: Loader['dtype']}[]} */
+ const callingRegs = [];
+ /** @type {{asm: Loader['asm'], dst: LoadDst, dtype: Loader['dtype']}[]} */
+ const directs = [];
+ for (const entry of fields) {
+ const { dtype } = entry;
+ const isFloat = dtype === 'float';
+ /** @type {LoadDst} */
+ let dst;
+ if (isFloat && fpr <= 8) {
+ dst = { type: 'reg', num: fpr++ };
+ } else if (!isFloat && gpr <= 10) {
+ dst = { type: 'reg', num: gpr++ };
+ } else {
+ if (isFloat) sp = ((sp + 7) >> 3) << 3;
+ dst = { type: 'stack', off: sp };
+ sp += isFloat ? 8 : 4;
+ }
+ // push
+ if ('asm' in entry) {
+ const { asm, calling } = entry;
+ (calling ? (dst.type === 'stack' ? callingStacks : callingRegs) : directs).push({
+ dtype,
+ asm,
+ dst,
+ });
+ } else {
+ const { base, offset, postprocess } = entry;
+ const item = { dst, dtype, offset, postprocess };
+ const arr = simples.get(base);
+ if (arr == null) {
+ simples.set(base, [item]);
+ } else {
+ arr.push(item);
+ }
+ }
+ }
+
+ insts.push(...callingStacks.flatMap((inst) => inst.asm(version, inst.dst)));
+
+ const callingRegLast = callingRegs.pop();
+ /** @type {ASMInst[]} */
+ const instsLoadFromStack = [];
+ callingRegs.forEach((inst) => {
+ const isFloat = inst.dtype === 'float';
+ if (isFloat) sp = ((sp + 7) >> 3) << 3;
+ insts.push(...inst.asm(version, { type: 'stack', off: sp }));
+ instsLoadFromStack.push((isFloat ? ASM.lfd : ASM.lwz)(inst.dst.num, 1, sp));
+ sp += isFloat ? 8 : 4;
+ });
+ // last
+ if (callingRegLast) {
+ insts.push(...callingRegLast.asm(version, callingRegLast.dst));
+ }
+ // load from stack
+ insts.push(...instsLoadFromStack);
+
+ // directs
+ insts.push(...directs.flatMap((inst) => inst.asm(version, inst.dst)));
+
+ // simples
+ const rBase = 3;
+ for (const [base, items] of simples.entries()) {
+ // load base
+ insts.push(...bases[base](rBase, version));
+ // load all var
+ for (const { dtype, offset, dst, postprocess } of items) {
+ if (dst.type === 'stack') {
+ insts.push(
+ $load[dtype](rTmp, rBase, offset),
+ ...(postprocess?.(rTmp) ?? []),
+ (dtype === 'float' ? ASM.stfd : ASM.stw)(rTmp, 1, dst.off),
+ );
+ } else {
+ insts.push($load[dtype](dst.num, rBase, offset), ...(postprocess?.(dst.num) ?? []));
+ }
+ }
+ }
+
+ // r3 = drawTextOpt
+ insts.push({ type: 'struct', reg: 3, hex: insts2hex(getDrawTextOpt(opt)) });
+ // r4 = fmt
+ insts.push({ type: 'struct', reg: 4, hex: str2hex(fmt, version) });
+
+ // call
+ insts.push({ type: 'call', addr: addrs.drawText });
+ return { insts, sp };
+}
+
+/**
+ * @param {GameVersion} version
+ * @param {Parameters[0]} config
+ * @param {Parameters[1]} size
+ * @returns {ASMInst[]}
+ */
+export const fillRect = (version, config, size) => [
+ // r3, r4 = opt
+ { type: 'struct', reg: 3, hex: insts2hex(getFillRectParams(config, size)) },
+ // call
+ { type: 'call', addr: addrs.fillRect[version], prep: [ASM.addi(4, 3, 0x10)] },
+];
diff --git a/site/.vuepress/components/codes/CustomizedDisplay/loader.js b/site/.vuepress/components/codes/CustomizedDisplay/loader.js
new file mode 100644
index 0000000..8e3eaaf
--- /dev/null
+++ b/site/.vuepress/components/codes/CustomizedDisplay/loader.js
@@ -0,0 +1,68 @@
+import { ASM, $load, $store } from '../asm.js';
+import { assemble } from './assembler.js';
+
+export const rTmp = 12;
+export const fTmp = 12;
+
+/**
+ * @typedef {Parameters[0] extends (infer U)[] ? U : never} ASMInst
+ * @typedef {'GMSJ01'|'GMSJ0A'|'GMSE01'|'GMSP01'} GameVersion
+ * @typedef {{type: 'reg', num: number}} LoadDstReg
+ * @typedef {{type: 'stack', off: number}} LoadDstStack
+ * @typedef {LoadDstReg|LoadDstStack} LoadDst
+ * @typedef {{
+ * asm(version: GameVersion, dst: LoadDst): ASMInst[]
+ * dtype: 8|16|32|'float'
+ * calling: boolean
+ * }} Loader
+ */
+
+/**
+ * @param {Loader['dtype']} dtype
+ * @param {(reg: number, version: GameVersion)=>ASMInst[]} load
+ * @param {(rD: number, version: GameVersion)=>ASMInst[]} [postprocess]
+ * @returns {Loader['asm']}
+ */
+export const makeDirectLoaderASM = (dtype, load, postprocess) => (version, dst) => {
+ const { type } = dst;
+ if (type == 'reg') {
+ const { num } = dst;
+ return [...load(num, version), ...(postprocess?.(num, version) ?? [])];
+ } else {
+ const { num, st } =
+ dtype === 'float' ? { num: fTmp, st: ASM.stfd } : { num: rTmp, st: ASM.stw };
+ return [...load(num, version), ...(postprocess?.(num, version) ?? []), st(num, 1, dst.off)];
+ }
+};
+
+/**
+ * @param {Loader['dtype']} dtype
+ * @param {(version: GameVersion)=>ASMInst[]} load
+ * @returns {Loader['asm']}
+ */
+export const makeFunctionLoaderASM = (dtype, load) => (version, dst) => {
+ const { type } = dst;
+ const base = load(version);
+ if (type == 'reg') {
+ const { num } = dst;
+ if (dtype === 'float') {
+ return num === 1 ? base : [...base, ASM.fmr(num, 1)];
+ } else {
+ return num === 3 ? base : [...base, ASM.mr(num, 3)];
+ }
+ } else {
+ const { off } = dst;
+ return [...base, dtype === 'float' ? ASM.stfd(1, 1, off) : ASM.stw(3, 1, off)];
+ }
+};
+
+/**
+ * @param {Loader['dtype']} dtype
+ * @param {(version: GameVersion)=>ASMInst[]} load
+ * @returns {Loader}
+ */
+export const makeFunctionLoader = (dtype, load) => ({
+ dtype,
+ asm: makeFunctionLoaderASM(dtype, load),
+ calling: true,
+});
diff --git a/site/.vuepress/components/codes/PatternSelector/codegen.js b/site/.vuepress/components/codes/PatternSelector/codegen.js
index f3aeb98..9770105 100644
--- a/site/.vuepress/components/codes/PatternSelector/codegen.js
+++ b/site/.vuepress/components/codes/PatternSelector/codegen.js
@@ -2,7 +2,7 @@ import { parseJSON } from '../codegen.js';
import { ASM, liDX, str2hex, inst2gecko, getFillRectParams } from '../asm.js';
import { measureText } from '../text.js';
import { int2hex } from '../utils.js';
-import addrs from '../addrs.js';
+import { addrs } from '../addrs.js';
export const lskey = 'config/PatternSelector';
import * as GMSJ01 from './code/GMSJ01.js';
diff --git a/site/.vuepress/components/codes/addrs.js b/site/.vuepress/components/codes/addrs.js
index a5dd95a..04636a8 100644
--- a/site/.vuepress/components/codes/addrs.js
+++ b/site/.vuepress/components/codes/addrs.js
@@ -1,4 +1,4 @@
-export default {
+export const addrs = {
drawText: 0x817f0238,
drawWater: {
GMSJ01: 0x80206a00,
@@ -18,11 +18,51 @@ export default {
GMSE01: 0x802eb6bc,
GMSP01: 0x802e3864,
},
- // r1 offset of J2DGrafContext in TGCConsole2::perform()
- ctxSpOff: {
- GMSJ01: 0xe90,
- GMSJ0A: 0xbec,
- GMSE01: 0xbd0,
- GMSP01: 0xbe4,
+ 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 9bc30a4..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,13 +177,13 @@ 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)];
}
}
@@ -178,15 +192,12 @@ export function liDX(rT, D) {
* @param {string} version
*/
export function str2bytes(s, version) {
- const enc = version.startsWith('GMSJ') ? 'SJIS' : '';
- const fmtbuf = version.startsWith('GMSJ')
- ? Encoding.convert(Encoding.stringToCode(s), 'SJIS') // Shift-JIS
- : Array.from(s, (c) => {
- // latin1
- const x = c.charCodeAt(0);
- // replace the char with space if it is multi-byte
- return x >= 0x100 ? 0x20 : x;
- });
+ /** @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;
}
@@ -217,35 +228,49 @@ export function str2inst(s, version) {
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 {{
@@ -273,3 +298,20 @@ export const getFillRectParams = (
// 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/text.js b/site/.vuepress/components/codes/text.js
index c350c09..05193d2 100644
--- a/site/.vuepress/components/codes/text.js
+++ b/site/.vuepress/components/codes/text.js
@@ -11,13 +11,11 @@ const getFontInfo = (version) =>
// JP
charInfo: /**@type{Record}*/ (charInfoJP),
rowSize: 24, // how many char in a row of the img
- multibyte: true,
}
: {
// EU (TODO US)
charInfo: /**@type{Record}*/ (charInfoEU),
rowSize: 16, // how many char in a row of the img
- multibyte: false,
};
/**
@@ -25,7 +23,7 @@ const getFontInfo = (version) =>
* @param {string} version
*/
export function measureText(text, version) {
- const { charInfo, rowSize, multibyte } = getFontInfo(version);
+ const { charInfo, rowSize } = getFontInfo(version);
/** @type {{x: number, y: number, u: number, v: number}[]} */
const chars = [];
@@ -34,8 +32,7 @@ export function measureText(text, version) {
let w = 0;
let useKerning = false;
text.split('').forEach((c) => {
- const { index, kerning, width } =
- charInfo[c] ?? (multibyte && c.charCodeAt(0) >= 0x80 ? charInfo['?'] : charInfo[' ']);
+ const { index, kerning, width } = charInfo[c] ?? charInfo['?'];
if (c === '\n') {
useKerning = false;
x = 0;
diff --git a/site/.vuepress/components/codes/utils.js b/site/.vuepress/components/codes/utils.js
index e8943f5..c89b774 100644
--- a/site/.vuepress/components/codes/utils.js
+++ b/site/.vuepress/components/codes/utils.js
@@ -59,3 +59,20 @@ export const makeGetLabel =
}
return null;
};
+
+/**
+ * @template T
+ * @param {Iterable} arr
+ * @param {(val: T) => boolean} tester
+ * @returns {[positive: T[], negative: T[]]}
+ */
+export function splitArray(arr, tester) {
+ /** @type {T[]} */
+ const positive = [];
+ /** @type {T[]} */
+ const negative = [];
+ for (const val of arr) {
+ (tester(val) ? positive : negative).push(val);
+ }
+ return [positive, negative];
+}