Customized Display 0.4

- Rewrote with C2
- Implemented a more complex assembler/compiler to support function call
- Add options
  - Invincibility Timer
  - Pollution Degree
  - Spin Jump Condition Check
This commit is contained in:
sup39 2023-01-31 22:06:41 +09:00
parent 69e8f17a24
commit 3a730390ac
17 changed files with 969 additions and 601 deletions

170
Codes.xml
View file

@ -2780,8 +2780,8 @@
<title lang="en-US">Customized Display</title>
<title lang="ja-JP">カスタマイズ表示</title>
<author>sup39(サポミク)</author>
<version>0.3</version>
<date>Jan 28, 2023</date>
<version>0.4</version>
<date>Jan 31, 2023</date>
<dependencies>drawText</dependencies>
<description lang="en-US">
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 (&lt;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 @@
#### プレビュー
</description>
<source version="GMSJ01">
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
</source>
<source version="GMSJ0A">
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
</source>
<source version="GMSE01">
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
</source>
<source version="GMSP01">
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
</source>
</code>
<code>

View file

@ -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

16
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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;

View file

@ -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<number, Map<string, number>>} */
const callCounts = new Map(); // [addr][prep] = count
/** @type {Set<string>} */
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<number, {fallback: number, preps: Map<string, number>}>} */
const offFuncs = new Map();
/** @type {number|null} */
let offCall = null;
/** @type {Map<string, number>} */
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<string, number>} */
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);
}

View file

@ -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<typeof parseFormat>) => 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<typeof prepareDrawText>|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<assemble>[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
);
}

View file

@ -35,7 +35,7 @@ V Spd <VSpd|.2|-31.17>`,
...base,
x: 16,
y: 192,
fontSize: 18,
fontSize: 16,
fmt: `X <x|.0|39.39>
Y <y|.0|1207.39>
Z <z|.0|-4193.6>
@ -43,7 +43,10 @@ A <angle||65535>
C <CAngle||9>
H <HSpd|.2|15.15>
V <VSpd|.2|-31.17>
QF <QF||0>`,
QF <QF||0>
I <invinc||30>
G <goop||36368>
Spin <spin||>`,
},
rect: {
...base,

View file

@ -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<import('./loader.js').makeFunctionLoader>} Loader
* @typedef {ReturnType<Loader['asm']>[number]} ASMInst
* @typedef {Parameters<Loader['asm']>[0]} GameVersion
*
* @typedef {Parameters<makeDirectLoaderASM>[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]));

View file

@ -0,0 +1,92 @@
import { fieldDB } from './fields.js';
/**
* @typedef {Parameters<import('./loader.js').Loader['asm']>[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 };
}

View file

@ -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<Loader['asm']>[0]} GameVersion
* @typedef {ReturnType<Loader['asm']>[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<getDrawTextOpt>[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<DirectLoader['base'], ({dst: LoadDst} & Pick<DirectLoader, 'dtype'|'offset'|'postprocess'>)[]>} */
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<getFillRectParams>[0]} config
* @param {Parameters<getFillRectParams>[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)] },
];

View file

@ -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<import('./assembler.js').assemble>[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,
});

View file

@ -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';

View file

@ -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,
};

View file

@ -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<string, (typeof charInfoJP)[' ']>} */
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];
}

View file

@ -11,13 +11,11 @@ const getFontInfo = (version) =>
// JP
charInfo: /**@type{Record<string, CharInfo>}*/ (charInfoJP),
rowSize: 24, // how many char in a row of the img
multibyte: true,
}
: {
// EU (TODO US)
charInfo: /**@type{Record<string, CharInfo>}*/ (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;

View file

@ -59,3 +59,20 @@ export const makeGetLabel =
}
return null;
};
/**
* @template T
* @param {Iterable<T>} 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];
}