diff --git a/Codes.xml b/Codes.xml index d4f8dbb..1009226 100644 --- a/Codes.xml +++ b/Codes.xml @@ -2780,8 +2780,8 @@ Customized Display カスタマイズ表示 sup39(サポミク) - 0.3 - Jan 28, 2023 + 0.4 + Jan 31, 2023 drawText Shows metadata at any given time. @@ -2800,6 +2800,9 @@ |`VSpd`|Vertical speed of Mario|float| |`QF`|QF offset|\{0,1,2,3}| |`CAngle`|Camera Angle|uint16| + |`invinc`|Invincibility Timer (frame)|int16| + |`goop`|Pollution Degree (<600 to complete SB6)|int32| + |`spin`|Whether satisfying spin jump condition|Show 🅐 if YES| For float data, you can set the *format* to `.{digit}` to specify how many digits to show. @@ -2831,6 +2834,9 @@ |`VSpd`|マリオのY速度|float| |`QF`|ずれたQFの数|\{0,1,2,3}| |`CAngle`|カメラの角度|uint16| + |`invinc`|無敵時間(フレーム数)|int16| + |`goop`|汚れの量(600未満でSB6クリア)|int32| + |`spin`|スピン入力の判定|条件を満たせば🅐を表示| float(小数)型に対して、「表示のフォーマット」を`.{桁数}`に設定して何桁まで表示するか指定できます。 @@ -2848,88 +2854,96 @@ #### プレビュー - C62069D4 817FA000 - 077FA000 00000084 - 806D98B8 C0230010 - C0430014 C0630018 - A0A30096 C08300B0 - C0A300A8 48000059 - 001000C8 00000014 - FFFFFFFF FFFFFFFF - 5820506F 7320252E - 30660A59 20506F73 - 20252E30 660A5A20 - 506F7320 252E3066 - 0A416E67 6C652025 - 68750A48 20537064 - 20252E32 660A5620 - 53706420 252E3266 - 00000000 7C6802A6 - 38830010 4BFF61BD - 4AA0C958 00000000 + C22069D4 00000014 + 9421FFF0 806D98B8 + C0230010 C0430014 + C0630018 A0A30096 + C08300B0 C0A300A8 + 48000015 001000C8 + 00000014 FFFFFFFF + FFFFFFFF 7C6802A6 + 48000049 5820506F + 7320252E 30660A59 + 20506F73 20252E30 + 660A5A20 506F7320 + 252E3066 0A416E67 + 6C652025 68750A48 + 20537064 20252E32 + 660A5620 53706420 + 252E3266 00000000 + 7C8802A6 3D80817F + 618C0238 7D8903A6 + 4E800421 38210010 + 60000000 00000000 - C6125540 817FA000 - 077FA000 00000084 - 806D9DE8 C0230010 - C0430014 C0630018 - A0A30096 C08300B0 - C0A300A8 48000059 - 001000C8 00000014 - FFFFFFFF FFFFFFFF - 5820506F 7320252E - 30660A59 20506F73 - 20252E30 660A5A20 - 506F7320 252E3066 - 0A416E67 6C652025 - 68750A48 20537064 - 20252E32 660A5620 - 53706420 252E3266 - 00000000 7C6802A6 - 38830010 4BFF61BD - 4A92B4C4 00000000 + C2125540 00000014 + 9421FFF0 806D9DE8 + C0230010 C0430014 + C0630018 A0A30096 + C08300B0 C0A300A8 + 48000015 001000C8 + 00000014 FFFFFFFF + FFFFFFFF 7C6802A6 + 48000049 5820506F + 7320252E 30660A59 + 20506F73 20252E30 + 660A5A20 506F7320 + 252E3066 0A416E67 + 6C652025 68750A48 + 20537064 20252E32 + 660A5620 53706420 + 252E3266 00000000 + 7C8802A6 3D80817F + 618C0238 7D8903A6 + 4E800421 38210010 + 60000000 00000000 - C61441B4 817FA000 - 077FA000 00000084 - 806D9F28 C0230010 - C0430014 C0630018 - A0A30096 C08300B0 - C0A300A8 48000059 - 001000C8 00000014 - FFFFFFFF FFFFFFFF - 5820506F 7320252E - 30660A59 20506F73 - 20252E30 660A5A20 - 506F7320 252E3066 - 0A416E67 6C652025 - 68750A48 20537064 - 20252E32 660A5620 - 53706420 252E3266 - 00000000 7C6802A6 - 38830010 4BFF61BD - 4A94A138 00000000 + C21441B4 00000014 + 9421FFF0 806D9F28 + C0230010 C0430014 + C0630018 A0A30096 + C08300B0 C0A300A8 + 48000015 001000C8 + 00000014 FFFFFFFF + FFFFFFFF 7C6802A6 + 48000049 5820506F + 7320252E 30660A59 + 20506F73 20252E30 + 660A5A20 506F7320 + 252E3066 0A416E67 + 6C652025 68750A48 + 20537064 20252E32 + 660A5620 53706420 + 252E3266 00000000 + 7C8802A6 3D80817F + 618C0238 7D8903A6 + 4E800421 38210010 + 60000000 00000000 - C6138DF0 817FA000 - 077FA000 00000084 - 806D9E50 C0230010 - C0430014 C0630018 - A0A30096 C08300B0 - C0A300A8 48000059 - 001000C8 00000014 - FFFFFFFF FFFFFFFF - 5820506F 7320252E - 30660A59 20506F73 - 20252E30 660A5A20 - 506F7320 252E3066 - 0A416E67 6C652025 - 68750A48 20537064 - 20252E32 660A5620 - 53706420 252E3266 - 00000000 7C6802A6 - 38830010 4BFF61BD - 4A93ED74 00000000 + C2138DF0 00000014 + 9421FFF0 806D9E50 + C0230010 C0430014 + C0630018 A0A30096 + C08300B0 C0A300A8 + 48000015 001000C8 + 00000014 FFFFFFFF + FFFFFFFF 7C6802A6 + 48000049 5820506F + 7320252E 30660A59 + 20506F73 20252E30 + 660A5A20 506F7320 + 252E3066 0A416E67 + 6C652025 68750A48 + 20537064 20252E32 + 660A5620 53706420 + 252E3266 00000000 + 7C8802A6 3D80817F + 618C0238 7D8903A6 + 4E800421 38210010 + 60000000 00000000 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]; +}