Merge pull request #147 from BitPatty/drawText

Rewrote drawText and related codes
This commit is contained in:
サポミク 2023-07-15 13:06:00 +09:00 committed by GitHub
commit cd7639b665
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 4144 additions and 2173 deletions

1281
Codes.xml

File diff suppressed because it is too large Load diff

View file

@ -533,4 +533,424 @@
60000000 00000000
</source>
</code>
<code>
<category>metadata</category>
<id>PASDisplay</id>
<title lang="en-US">Position/angle/speed display</title>
<title lang="de-CH">Position/Winkel/Geschw. Display</title>
<title lang="fr-FR">Affichage de position/angle/vitesse</title>
<title lang="ja-JP">位置/角度/速度表示</title>
<author>Noki Doki, sup39(サポミク)</author>
<version>1.4</version>
<date>Mar 24, 2022</date>
<dependencies version="GMSJ01">drawText</dependencies>
<dependencies version="GMSJ0A">drawText</dependencies>
<description lang="en-US">
Shows Mario's position, angle and speed at any given time.
::: warning
This code is not compatible with the Speed Display code.
:::
</description>
<description lang="de-CH">
Zeigt Mario's Position, Winkel und Geschwindigkeit jederzeit auf dem Bildschirm an.
::: warning
Dieser Code ist nicht kompatibel mit dem Geschwindigkeits-Display Code
:::
</description>
<description lang="fr-FR">Affiche la position, l'angle et la vitesse de Mario à tout moment.</description>
<description lang="ja-JP">常に「マリオの位置」「マリオの角度」「マリオの速度」を表示します。</description>
<source version="GMSE01">
062A6160 00000010
49553F19 60000000
60000000 60000000
042998B8 49560749
04143F14 496B6209
077FA000 000001E8
9421FFE0 7C0802A6
90010024 93E1001C
4AABD6E5 38E00200
38C00320 38A0FFD8
3880000A 38610008
4AAD01B9 814D9FC8
3FE08180 3BFFA1EC
39200000 39000002
3CE08180 38E7A164
80CA0048 38A10008
38800000 7FE3FB78
4AAD67F1 39200001
913F01B0 80010024
7C0803A6 83E1001C
38210020 4E800020
9421FFE8 7C0802A6
9001001C 93A1000C
93C10010 93E10014
7C7F1B78 81230000
83A90064 3D208180
8129A39C 2F890000
409E002C 7FE3FB78
7FA903A6 4E800421
8001001C 7C0803A6
83A1000C 83C10010
83E10014 38210018
4E800020 3D208040
6129E0E8 83C90000
3C608180 3863A1EC
4AAD6B79 C0BE00A8
C09E00B0 A0DE0096
C07E0018 C05E0014
C03E0010 3CA08180
38A5A164 38800080
4CC63242 4AB3F691
4BFFFF94 9421FFF0
7C0802A6 90010014
93E1000C 7C7F1B78
4AAF158D 38E00081
7FE6FB78 38A00000
38800000 3C608180
3863A1EC 4AAD5C5D
80010014 7C0803A6
83E1000C 38210010
4E800020 5820506F
7320252E 30660A59
20506F73 20252E30
660A5A20 506F7320
252E3066 0A416E67
6C652025 68750A48
20537064 20252E32
660A5620 53706420
252E3266 20202020
20202020 20202020
20202020 20202020
20202020 20202020
20202020 20202020
20202020 20202020
20202020 20202020
20202020 20202020
20202000 00000000
C22A66F4 00000003
38600000 3D808180
906CA39C 807F0040
60000000 00000000
</source>
<source version="GMSJ01">
04206734 495F38CD
077FA000 000000B5
9421FFF0 7C0802A6
90010014 93E1000C
7C7F1B78 4A83B215
3D208041 8149A378
3D008180 38600010
3908A074 C0AA00A8
38E0FFFF C08A00B0
38C0FFFF A12A0096
38A00014 C06A0018
388000C8 C04A0014
C02A0010 4CC63242
4BFF61E1 80010014
7FE3FB78 83E1000C
7C0803A6 38210010
4A83B1B8 5820506F
7320252E 30660A59
20506F73 20252E30
660A5A20 506F7320
252E3066 0A416E67
6C652025 68750A48
20537064 20252E32
660A5620 53706420
252E3266 00000000
</source>
<source version="GMSJ0A">
041252A0 496D4D61
077FA000 000000B5
9421FFF0 7C0802A6
90010014 93E1000C
7C7F1B78 4AAD0EB9
3D208040 8149EF88
3D008180 38600010
3908A074 C0AA00A8
38E0FFFF C08A00B0
38C0FFFF A12A0096
38A00014 C06A0018
388000C8 C04A0014
C02A0010 4CC63242
4BFF61E1 80010014
7FE3FB78 83E1000C
7C0803A6 38210010
4AAD0E5C 5820506F
7320252E 30660A59
20506F73 20252E30
660A5A20 506F7320
252E3066 0A416E67
6C652025 68750A48
20537064 20252E32
660A5620 53706420
252E326600000000
</source>
<source version="GMSP01">
0629E070 00000010
4955C009 60000000
60000000 60000000
04291750 495688B1
04138B50 496C15CD
077FA000 000001E8
9421FFE0 7C0802A6
90010024 93E1001C
4AAB56B5 38E00200
38C00320 38A0FFD8
3880000A 38610008
4AAC824D 814D9EF0
3FE08180 3BFFA1EC
39200000 39000002
3CE08180 38E7A164
80CA0048 38A10008
38800000 7FE3FB78
4AACE999 39200001
913F01B0 80010024
7C0803A6 83E1001C
38210020 4E800020
9421FFE8 7C0802A6
9001001C 93A1000C
93C10010 93E10014
7C7F1B78 81230000
83A90064 3D208180
8129A39C 2F890000
409E002C 7FE3FB78
7FA903A6 4E800421
8001001C 7C0803A6
83A1000C 83C10010
83E10014 38210018
4E800020 3D208040
612957B0 83C90000
3C608180 3863A1EC
4AACED21 C0BE00A8
C09E00B0 A0DE0096
C07E0018 C05E0014
C03E0010 3CA08180
38A5A164 38800080
4CC63242 4AB37811
4BFFFF94 9421FFF0
7C0802A6 90010014
93E1000C 7C7F1B78
4AAE9735 38E00081
7FE6FB78 38A00000
38800000 3C608180
3863A1EC 4AACDE05
80010014 7C0803A6
83E1000C 38210010
4E800020 5820506F
7320252E 30660A59
20506F73 20252E30
660A5A20 506F7320
252E3066 0A416E67
6C652025 68750A48
20537064 20252E32
660A5620 53706420
252E3266 20202020
20202020 20202020
20202020 20202020
20202020 20202020
20202020 20202020
20202020 20202020
20202020 20202020
20202020 20202020
20202000 00000000
C229E64C 00000003
38600000 3D808180
906CA39C 807F0040
60000000 00000000
</source>
</code>
<code>
<category>metadata</category>
<id>SpeedDisplay</id>
<title lang="en-US">Speed display</title>
<title lang="de-CH">Geschwindigkeits-Display</title>
<title lang="fr-FR">Affichage de vitesse</title>
<title lang="ja-JP">速度表示</title>
<author>Noki Doki, sup39(サポミク)</author>
<version>1.5</version>
<date>Mar 24, 2022</date>
<dependencies version="GMSJ01">drawText</dependencies>
<dependencies version="GMSJ0A">drawText</dependencies>
<description lang="en-US">
Shows Mario's speed at any given time.
::: warning
This code is not compatible with the Position/Angle/Speed Display code.
:::
</description>
<description lang="de-CH">
Zeigt Mario's Geschwindigkeit jederzeit auf dem Bildschirm an.
::: warning
Dieser Code ist nicht kompatibel mit dem Position/Winkel/Gewschw. Display Code
:::
</description>
<description lang="fr-FR">Affiche la vitesse de Mario à tout moment.</description>
<description lang="ja-JP">常に「マリオの速度」を表示します。</description>
<source version="GMSE01">
062A6160 00000010
49553F19 60000000
60000000 60000000
042998B8 49560749
04143F14 496B61F9
077FA000 00000188
9421FFE0 7C0802A6
90010024 93E1001C
4AABD6E5 38E00200
38C00320 38A0FFD8
3880000A 38610008
4AAD01B9 814D9FC8
3FE08180 3BFFA214
39200000 39000002
3CE08180 38E7A154
80CA0048 38A10008
38800000 7FE3FB78
4AAD67F1 39200001
913F01B0 80010024
7C0803A6 83E1001C
38210020 4E800020
9421FFE8 7C0802A6
9001001C 93C10010
93E10014 7C7F1B78
81230000 83C90064
3D208180 8129A3C4
2C090000 40820028
7FE3FB78 7FC903A6
4E800421 8001001C
7C0803A6 83C10010
83E10014 38210018
4E800020 93A1000C
3D208040 6129E0E8
83A90000 3C608180
3863A214 4AAD6B7D
C05D00A8 C03D00B0
3CA08180 38A5A154
38800030 4CC63242
4AB3F6A5 83A1000C
4BFFFFA0 9421FFF0
7C0802A6 90010014
93E1000C 7C7F1B78
4AAF159D 38E00081
7FE6FB78 38A00000
38800000 3C608180
3863A214 4AAD5C6D
80010014 7C0803A6
83E1000C 38210010
4E800020 48205370
6420252E 32660A56
20537064 20252E32
66202020 20202020
20202020 20202020
20202020 20202020
20202000 00000000
C22A66F4 00000003
38600000 3D808180
906CA3C4 807F0040
60000000 00000000
</source>
<source version="GMSJ01">
04206734 495F38CD
077FA000 0000007A
9421FFF0 7C0802A6
90010014 93E1000C
7C7F1B78 4A83B215
3D208041 8129A378
3D008180 38600010
3908A064 C04900A8
38E0FFFF C02900B0
38C0FFFF 38A00014
388000F0 4CC63242
4BFF61F1 80010014
7FE3FB78 83E1000C
7C0803A6 38210010
4A83B1C8 48205370
6420252E 32660A56
20537064 20252E32
66000000 00000000
</source>
<source version="GMSJ0A">
041252A0 496D4D61
077FA000 0000007A
9421FFF0 7C0802A6
90010014 93E1000C
7C7F1B78 4AAD0EB9
3D208040 8129EF88
3D008180 38600010
3908A064 C04900A8
38E0FFFF C02900B0
38C0FFFF 38A00014
388000F0 4CC63242
4BFF61F1 80010014
7FE3FB78 83E1000C
7C0803A6 38210010
4AAD0E6C 48205370
6420252E 32660A56
20537064 20252E32
66000000 00000000
</source>
<source version="GMSP01">
0629E070 00000010
4955C009 60000000
60000000 60000000
04291750 495688B1
04138B50 496C15BD
077FA000 00000188
9421FFE0 7C0802A6
90010024 93E1001C
4AAB56B5 38E00200
38C00320 38A0FFD8
3880000A 38610008
4AAC824D 814D9EF0
3FE08180 3BFFA214
39200000 39000002
3CE08180 38E7A154
80CA0048 38A10008
38800000 7FE3FB78
4AACE999 39200001
913F01B0 80010024
7C0803A6 83E1001C
38210020 4E800020
9421FFE8 7C0802A6
9001001C 93C10010
93E10014 7C7F1B78
81230000 83C90064
3D208180 8129A3C4
2C090000 40820028
7FE3FB78 7FC903A6
4E800421 8001001C
7C0803A6 83C10010
83E10014 38210018
4E800020 93A1000C
3D208040 612957B0
83A90000 3C608180
3863A214 4AACED25
C05D00A8 C03D00B0
3CA08180 38A5A154
38800030 4CC63242
4AB37825 83A1000C
4BFFFFA0 9421FFF0
7C0802A6 90010014
93E1000C 7C7F1B78
4AAE9745 38E00081
7FE6FB78 38A00000
38800000 3C608180
3863A214 4AACDE15
80010014 7C0803A6
83E1000C 38210010
4E800020 48205370
6420252E 32660A56
20537064 20252E32
66202020 20202020
20202020 20202020
20202020 20202020
20202000 00000000
C229E64C 00000003
38600000 3D808180
906CA3C4 807F0040
60000000 00000000
</source>
</code>
</codes>

View file

@ -29,6 +29,27 @@ Disable Z menu for Instant Level Select to prevent broken texture
### Updated 'Instant Level Select'
Overwrite button input value to prevent Level Select being activated when AreaLock is enabled
## Jan 31, 2023
### Updated 'Pattern Selector'
- Rewrote with C2 + separated config 06
- Remove label option
### 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
- Rewrote QFT, Pattern Selector, Customized Display with the new drawText function
### Improved Preview
- Added PAL font (TODO: NTSC-U)
- Merged P/A/S Display and Speed Display to Customized Display
- Provided background options to Pattern Selector and Customized Display
## Jan 10, 2023
### Updated 'Quarterframe Timer'
Added the following options to freeze QFT:

29
package-lock.json generated
View file

@ -14,10 +14,8 @@
"devDependencies": {
"@sup39/markdown-it-attr": "1.2.2",
"@sup39/markdown-it-inline-tag": "1.0.1",
"@types/encoding-japanese": "^2.0.1",
"@vuepress/plugin-back-to-top": "1.9.8",
"@vuepress/plugin-medium-zoom": "1.9.8",
"encoding-japanese": "^2.0.0",
"jsdom": "21.1.0",
"pre-commit": "1.2.2",
"prettier": "2.8.3",
@ -1896,12 +1894,6 @@
"@types/node": "*"
}
},
"node_modules/@types/encoding-japanese": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/encoding-japanese/-/encoding-japanese-2.0.1.tgz",
"integrity": "sha512-JaCXs2HLniKY8xXeWlg8MAtd4iKhNh8LwutW3yDMWY4usEdTZ2va1x9kd8V3179OAIUTgGQVA63XJrHettpVFQ==",
"dev": true
},
"node_modules/@types/express": {
"version": "4.17.13",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz",
@ -6604,15 +6596,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",
@ -19065,12 +19048,6 @@
"@types/node": "*"
}
},
"@types/encoding-japanese": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/encoding-japanese/-/encoding-japanese-2.0.1.tgz",
"integrity": "sha512-JaCXs2HLniKY8xXeWlg8MAtd4iKhNh8LwutW3yDMWY4usEdTZ2va1x9kd8V3179OAIUTgGQVA63XJrHettpVFQ==",
"dev": true
},
"@types/express": {
"version": "4.17.13",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz",
@ -22969,12 +22946,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

@ -22,10 +22,8 @@
"devDependencies": {
"@sup39/markdown-it-attr": "1.2.2",
"@sup39/markdown-it-inline-tag": "1.0.1",
"@types/encoding-japanese": "^2.0.1",
"@vuepress/plugin-back-to-top": "1.9.8",
"@vuepress/plugin-medium-zoom": "1.9.8",
"encoding-japanese": "^2.0.0",
"jsdom": "21.1.0",
"pre-commit": "1.2.2",
"prettier": "2.8.3",

View file

@ -132,15 +132,6 @@ export default {
codeConfigs: {},
};
},
created() {
this.codeConfigs = {
qft: getConfigQFT(),
PatternSelector: getConfigPS(),
SpeedDisplay: {},
PASDisplay: {},
CustomizedDisplay: getConfigCD(this.version),
};
},
methods: {
getLabel(key) {
return translate(key, this.$lang);
@ -168,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;
@ -222,7 +220,10 @@ export default {
.filter(code => !(code.category === category && exclusive))
.map(code => code.id));
ids.add(id);
return Object.fromEntries(Object.entries(this.codeConfigs).filter(([id]) => ids.has(id)));
return {
...Object.fromEntries(Object.entries(this.codeConfigs).filter(([id]) => ids.has(id))),
_version: this.selectedVersion,
};
},
},
};

View file

@ -1,82 +1,30 @@
<template>
<div class="preview-root">
<div class="preview-ctn">
<div v-if="qft">
<div :style="qft.bgStyle" />
<PreviewString :x="qft.x" :y="qft.y" :size="qft.fontSize" :color="qft.color" text="0:00:00" />
</div>
<div v-if="mdps">
<PreviewString v-for="mdp,i in mdps" :key="i"
:x="mdp.x" :y="mdp.y" :size="mdp.fontSize" :color="mdp.color" :text="mdp.text" />
</div>
<PreviewString v-if="ps" :x="ps.x" :y="ps.y" :size="ps.fontSize" :color="ps.color" :text="ps.text" />
<PreviewString :config="config.qft" :version="_version" />
<PreviewString v-for="mdp,i in (config.CustomizedDisplay || [])"
:key="'mdp'+i" :config="mdp" :version="_version" />
<PreviewString :config="config.PatternSelector" :version="_version" />
</div>
</div>
</template>
<script>
import { rgbaI2S, fg2Style } from './codes/utils.js';
export default {
props: {
config: {type: Object},
},
computed: {
mdps() {
const {config} = this;
if (config.PASDisplay) return [{
x: 16,
y: 200,
fontSize: 20,
color: '#fff',
text: 'X Pos -39\nY Pos 1207\nZ Pos -4193\nAngle 65535\nH Spd 15.15\nV Spd -31.17',
}];
if (config.SpeedDisplay) return [{
x: 16,
y: 240,
fontSize: 20,
color: '#fff',
text: 'H Spd 15.15\nV Spd -31.17',
}];
if (config.CustomizedDisplay) {
return config.CustomizedDisplay.map(({fgRGB, fgA, fgRGB2, fgA2, ...o}) => ({
...o,
color: fg2Style(fgRGB, fgA, fgRGB2, fgA2),
}));
}
},
qft() {
const {config: {qft}} = this;
if (qft == null) return;
const {x, y, fontSize, fgRGB, fgA, fgRGB2, fgA2, bgRGB, bgA, width} = qft;
const bg = rgbaI2S(bgRGB, bgA);
return {
x, y, fontSize,
color: fg2Style(fgRGB, fgA, fgRGB2, fgA2),
bgStyle: {
left: x+'px',
top: (y-fontSize)+'px',
width: (width*fontSize/20)+'px',
height: fontSize+'px',
background: bg,
},
};
},
ps() {
const {config: {PatternSelector: ps}} = this;
if (ps == null) return;
const {x, y, fontSize, fgRGB, fgA, fgRGB2, fgA2, label} = ps;
return {
x, y, fontSize,
color: fg2Style(fgRGB, fgA, fgRGB2, fgA2),
text: label+'#0 0 0',
};
_version() {
const {_version} = this.config
return _version;
},
},
}
</script>
<style scoped>
div.preview-root {
.preview-root {
position: relative;
width: 600px;
height: 448px;
@ -84,11 +32,8 @@ div.preview-root {
padding: 0;
overflow: hidden;
}
div.preview-ctn {
.preview-ctn {
position: absolute;
top: -16px;
}
div.preview-ctn * {
position: absolute;
}
</style>

View file

@ -1,47 +1,34 @@
<template>
<div v-if="text" class="preview-str" :style="styles.root" >
<div v-if="config">
<div :style="styles.bg" />
<div :class="previewCssClass" :style="styles.root" >
<div v-for="style, i in styles.chars" :key="i" class="char-ctn" :style="style.ctn">
<div class="char-bg" :style="style.bg" />
<div class="char-mask" :style="style.mask" />
</div>
</div>
</div>
</template>
<script>
import charInfo from '../data/font-jp.json';
import {rgbaI2S} from './codes/utils.js';
import {measureText} from './codes/text.js';
export default {
props: {
x: {type: Number},
y: {type: Number},
size: {type: Number},
color: {type: String},
text: {type: String},
config: {type: Object},
version: {type: String},
},
computed: {
previewCssClass() {
// TODO US
return `preview-str preview-${['GMSJ01', 'GMSJ0A'].includes(this.version) ? 'JP' : 'EU'}`;
},
styles() {
const {x: x0, y: y0, size: fontSize, color, text} = this;
/** @type {{x: number, y: number, u: number, v: number}[]} */
const chars = [];
let x = 0;
let y = 0;
let useKerning = false;
text.split('').forEach(c => {
const {index, kerning, width} = charInfo[c] ?? charInfo[' '];
if (c === '\n') {
useKerning = false;
x = 0;
y += 20;
return;
}
if (useKerning) x -= kerning;
useKerning = true;
// uv
const [u, v] = [index%25*20, (index/25|0)*20];
chars.push({x, y, u, v});
// next
x += width + kerning;
});
const {config, version} = this;
const {x: x0, y: y0, fontSize, fgRGB, fgA, fgRGB2, fgA2, bgRGB, bgA, bgLeft, bgRight, bgTop, bgBot, text} = config;
const fgColor = rgbaI2S(fgRGB, fgA);
const {width, height, chars} = measureText(text, version);
return {
root: {
@ -60,10 +47,18 @@ export default {
mask: {
'mask-position': offset,
'-webkit-mask-position': offset,
background: color,
background: fgRGB2 == null || fgA2 == null ? fgColor :
`linear-gradient(180deg, ${fgColor}, ${rgbaI2S(fgRGB2, fgA2)})`,
},
};
}),
bg: {
left: x0 - bgLeft + 'px',
top: y0 - fontSize - bgTop + 'px',
width: (width * fontSize) / 20 + bgLeft + bgRight + 'px',
height: (height * fontSize) / 20 + bgTop + bgBot + 'px',
background: rgbaI2S(bgRGB, bgA),
},
};
},
},
@ -71,27 +66,42 @@ export default {
</script>
<style scoped>
* {
position: absolute;
}
.preview-str {
position: relative;
}
.preview-str * {
position: absolute;
}
div.char-ctn {
.char-ctn {
isolation: isolate;
}
div.char-ctn > div {
.char-ctn > div {
width: 20px;
height: 20px;
}
div.char-bg {
background: url(/img/preview/font-jp.png);
}
div.char-mask {
mask-image: url(/img/preview/font-jp.png);
-webkit-mask-image: url(/img/preview/font-jp.png);
.char-mask {
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mix-blend-mode: multiply;
}
.preview-JP .char-bg {
background: url(/img/preview/font-JP.png);
}
.preview-JP .char-mask {
mask-image: url(/img/preview/font-JP.png);
-webkit-mask-image: url(/img/preview/font-JP.png);
}
.preview-EU .char-bg {
background: url(/img/preview/font-EU.png);
}
.preview-EU .char-mask {
mask-image: url(/img/preview/font-EU.png);
-webkit-mask-image: url(/img/preview/font-EU.png);
}
/* TODO US */
</style>

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,31 +1,25 @@
import { parseJSON } from '../codegen.js';
import { ASM, makeInst, liDX, str2inst, makeProgram, inst2gecko } from '../asm.js';
import { ASM, liDX, str2inst, makeProgram, inst2gecko, getFillRectParams } from '../asm.js';
import { measureText } from '../text.js';
import { addrs } from '../addrs.js';
import { parseFormat } from './format.js';
import { assemble } from './assembler.js';
import { drawText, fillRect } from './functions.js';
export const lskey = 'config/CustomizedDisplay';
export const defaultConfig = [
{
x: 16,
y: 192,
fontSize: 18,
fgRGB: 0xffffff,
fgA: 0xff,
fgRGB2: null,
fgA2: null,
fmt: `X <x|.0|39.39>
Y <y|.0|1207.39>
Z <z|.0|-4193.6>
A <angle||65535>
H <HSpd|.2|15.15>
V <VSpd|.2|-31.17>
QF <QF||0>`,
},
];
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} */
const config = typeof localStorage !== 'undefined' && parseJSON(localStorage.getItem(lskey));
return (config instanceof Array ? config : defaultConfig).map(({ fmt, ...o }) => ({
...defaultConfig[0],
...o,
fmt,
text: format2previewText(fmt, version),
@ -33,412 +27,47 @@ export function getConfig(version) {
}
/**
* @typedef {number[]} Inst
* @typedef {8|16|32|'float'} DataType
* @typedef {(gpr: number)=>Inst} GPRHandler -- (src=gpr, dst=gpr)
* @typedef {{type: 'gpr'|'fpr'|'sp', index: number}} Dst
* @typedef {'GMSJ01'|'GMSJ0A'|'GMSE01'|'GMSP01'} GameVersion
*
* @typedef {{
* offset: number
* dtype: DataType
* post?: GPRHandler
* }} FieldInfo
*
* @typedef {{
* id: string
* pre: GPRHandler
* }} Base
*
* @typedef {{
* pre: GPRHandler
* fields: {info: FieldInfo, dst: Dst}[]
* }} Entry
*/
/** @typedef {{[ver in GameVersion]: GPRHandler}} VBase */
const bases = {
gpMarioOriginal: /**@type{VBase}*/ ({
GMSJ01: (rT) => ASM.lwz(rT, 13, -0x6748),
GMSE01: (rT) => ASM.lwz(rT, 13, -0x60d8),
GMSP01: (rT) => ASM.lwz(rT, 13, -0x61b0),
GMSJ0A: (rT) => ASM.lwz(rT, 13, -0x6218),
}),
gpMarDirector: /**@type{VBase}*/ ({
GMSJ01: (rT) => ASM.lwz(rT, 13, -0x6818),
GMSE01: (rT) => ASM.lwz(rT, 13, -0x6048),
GMSP01: (rT) => ASM.lwz(rT, 13, -0x6120),
GMSJ0A: (rT) => ASM.lwz(rT, 13, -0x6188),
}),
gpCamera: /**@type{VBase}*/ ({
GMSJ01: (rT) => ASM.lwz(rT, 13, -0x5750),
GMSE01: (rT) => ASM.lwz(rT, 13, -0x7118),
GMSP01: (rT) => ASM.lwz(rT, 13, -0x7158),
GMSJ0A: (rT) => ASM.lwz(rT, 13, -0x5768),
}),
};
/** @typedef {keyof typeof bases} BaseId */
/** @type {({id: string, base: BaseId, fmt: string, preview: number} & FieldInfo)[]} */
const fields = [
{ id: 'x', base: 'gpMarioOriginal', dtype: 'float', offset: 0x10, fmt: '%.0f', preview: 426.39 },
{ id: 'y', base: 'gpMarioOriginal', dtype: 'float', offset: 0x14, fmt: '%.0f', preview: -427.39 },
{ id: 'z', base: 'gpMarioOriginal', dtype: 'float', offset: 0x18, fmt: '%.0f', preview: 428.39 },
{ id: 'angle', base: 'gpMarioOriginal', dtype: 16, offset: 0x96, fmt: '%hu', preview: 1207 },
{
id: 'HSpd',
base: 'gpMarioOriginal',
dtype: 'float',
offset: 0xb0,
fmt: '%.2f',
preview: 15.15,
},
{
id: 'VSpd',
base: 'gpMarioOriginal',
dtype: 'float',
offset: 0xa8,
fmt: '%.2f',
preview: -31.17,
},
{
id: 'QF',
base: 'gpMarDirector',
dtype: 32,
offset: 0x58,
fmt: '%u',
preview: 0,
post: (rT) => ASM.rlwinm(rT, rT, 0, 30, 31, false),
},
{
id: 'CAngle',
base: 'gpCamera',
dtype: 16,
offset: 0xa6,
fmt: '%hu',
preview: 9,
post: (rT) => ASM.addi(rT, rT, -0x8000), // offset by 0x8000
},
];
const fieldDB = Object.fromEntries(
fields.map(({ id, base, fmt, preview, ...info }) => [
id.toLowerCase(),
{ base, info, fmt, preview },
]),
);
const store = {
8: ASM.stb,
16: ASM.sth,
32: ASM.stw,
float: ASM.stfs,
double: ASM.stfd,
};
const load = {
8: ASM.lbz,
16: ASM.lhz,
32: ASM.lwz,
float: ASM.lfs,
};
/**
* @param {number} x
* @param {number} y
* @param {number} fontSize
* @param {number} colorTop
* @param {number} colorBot
*/
export function prepareDrawText(x, y, fontSize, colorTop, colorBot) {
let gpr = 9;
let fpr = 1;
let sp = 8;
let fmt = '';
let hasFloat = false;
/** @type {{[id: string]: Entry}} */
const entries = {};
/** @returns {Dst} */
function allocInt() {
if (gpr <= 10) {
return { type: 'gpr', index: gpr++ };
} else {
/** @type {Dst} */
const dst = { type: 'sp', index: sp };
sp += 4;
return dst;
}
}
/** @returns {Dst} */
function allocFloat() {
hasFloat = true;
if (fpr <= 8) {
return { type: 'fpr', index: fpr++ };
} else {
sp += sp & 4; // align 8
/** @type {Dst} */
const dst = { type: 'sp', index: sp };
sp += 8;
return dst;
}
}
/** @param {Base} base */
const getEntry = (base) =>
entries[base.id] ??
(entries[base.id] = {
pre: base.pre,
fields: [],
});
return {
/**
* @param {string} format
* @param {Base} base
* @param {FieldInfo} field
*/
pushValue(format, base, field) {
fmt += format;
getEntry(base).fields.push({
info: field,
dst: (field.dtype === 'float' ? allocFloat : allocInt)(),
});
},
/**
* @param {string} text
*/
pushText(text) {
fmt += text.replace(/%/g, '%%');
},
makeCode() {
/** @type {Inst[]} */
const insts = [];
// sp
const spAdd = sp === 8 ? 0 : ((sp >> 4) + (sp & 0xf ? 1 : 0)) << 4;
// params
for (const { pre, fields: params } of Object.values(entries)) {
// load base to gpr
const rBase = 3;
insts.push(pre(rBase));
// load all params
const rField = 5;
const fField = 9;
for (const {
info: { offset: srcoff, dtype, post },
dst,
} of params) {
if (dst.type === 'sp') {
const dstoff = dst.index;
if (dtype === 'float') {
insts.push(
// lfs fField, offset(rBase)
load.float(fField, rBase, srcoff),
// post
post?.(fField) ?? [],
// stfd fField, dst.index(r1)
store.double(fField, 1, dstoff),
);
} else {
insts.push(
// load rField, offset(rBase)
load[dtype](rField, rBase, srcoff),
// post
post?.(rField) ?? [],
// stw rField, dst.index(r1)
store[32](rField, 1, dstoff),
);
}
} else {
// load to register
const { index: rDst } = dst;
insts.push(
// load rDst
load[dtype](rDst, rBase, srcoff),
// post
post?.(rDst) ?? [],
);
}
}
}
// r8 = fmt
const fmtbuf = str2inst(fmt);
insts.push(
// bl 4+len4(fmt)
ASM.b(4 + (fmtbuf.length << 2), true),
// .string fmt
fmtbuf,
// mflr r8
ASM.mflr(8),
);
/*
* r3 = x
* r4 = y
* r5 = fontSize
* r6 = colorTop
* r7 = colorBot
*/
insts.push(
liDX(3, x),
liDX(4, y),
liDX(5, fontSize),
liDX(6, colorTop),
colorTop === colorBot ? ASM.mr(7, 6) : liDX(7, colorBot),
);
// cr{set|clr} 6
insts.push((hasFloat ? ASM.crset : ASM.crclr)(6));
// DONE
return { code: insts.flatMap((e) => e), spNeed: spAdd };
},
};
}
const dtype2fmtinfo = {
8: { prefix: 'hh', mask: 0xff },
16: { prefix: 'h', mask: 0xffff },
32: { prefix: '', mask: 0xffffffff },
};
/**
* @param {string} input
* @param {GameVersion} version
* @param {ReturnType<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;
}
const addrsOrig = {
GMSJ01: 0x80206a00 - 0x2c,
GMSJ0A: 0x8012556c - 0x2c,
GMSE01: 0x801441e0 - 0x2c,
GMSP01: 0x80138e1c - 0x2c,
};
const addrsSetup2D = {
GMSJ01: 0x80035228,
GMSJ0A: 0x802caecc,
GMSE01: 0x802eb6bc,
GMSP01: 0x802e3864,
};
const addrDrawText = 0x817f0238;
const addrDst = 0x817fa000;
/**
* @typedef {Parameters<assemble>[0][number]} ASMInst
* @param {GameVersion} version
*/
export default function codegen(version) {
const config = getConfig(version);
const configs = getConfig(version);
let spOff = 0;
const fcodes = /** @type {Inst[]} */ ([]);
let stackFrameSize = 0;
for (const { x, y, fontSize, fgRGB, fgA, fgRGB2, fgA2, fmt } of config) {
// color
const colorTop = (fgRGB << 8) | fgA;
const colorBot = fgRGB2 == null || fgA2 == null ? colorTop : (fgRGB2 << 8) | fgA;
// prepare drawText
const f = prepareDrawText(x, y, fontSize, colorTop, colorBot);
format2previewText(fmt, version, f);
// update code and sp
const { code, spNeed } = f.makeCode();
spOff = Math.max(spOff, spNeed);
fcodes.push(code);
/** @type {ASMInst[]} */
const asm = [];
for (const config of configs) {
const { fmt, bgA } = config;
const { preview, format, fields } = parseFormat(fmt, version);
// fill_rect
if (bgA) {
asm.push(...fillRect(version, config, measureText(preview, version)));
}
const addrOrig = addrsOrig[version];
const addrSetup2D = addrsSetup2D[version];
// program
const program = makeProgram(addrDst);
// addi r3, r1, 0xE90
program.push(ASM.addi(3, 1, 0xe90));
// addi r1, r1, -spOff
if (spOff) program.push(ASM.addi(1, 1, -spOff));
// bl setup
program.bl(addrSetup2D);
// (drawText)
for (const code of fcodes) {
program.push(code);
program.bl(addrDrawText);
// drawText
if (fmt.trim()) {
const { insts, sp } = drawText(version, config, format, fields);
stackFrameSize = Math.max(stackFrameSize, sp);
asm.push(...insts);
}
}
// 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] : [],
let body = assemble(asm, stackFrameSize);
// align code
if (body.length % 16 === 0) body += '60000000';
body += '00000000';
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('');
.join('') + body
);
}

View file

@ -2,19 +2,26 @@
<div>
<Preview :config="previewConfig" />
<div v-for="c,i in config" :key="i" class="textcell">
<button @click="config.splice(i, 1)" class="textcell-remove">&#215;</button>
<button class="textcell-remove" @click="deletionConfirm(i)">&#215;</button>
<Cell :value="c" @input="$event => config.splice(i, 1, $event)" :version="version" />
</div>
<div>
<button @click="config.push(defaultConfigCell)" class="textcell-add">+</button>
<div class="btn-ctn">
<button @click="config.push(db.PAS)">{{l('add.PAS')}}</button>
<button @click="config.push(db.speed)">{{l('add.speed')}}</button>
<button @click="config.push(db.detailed)">{{l('add.detailed')}}</button>
<button @click="config.push(db.rect)">{{l('add.rect')}}</button>
</div>
</div>
</template>
<script>
import labels from './labels.json';
import { defaultConfig, getConfig, lskey, format2previewText } from './codegen.js';
import configDB from './configDB.js';
import { makeUpdateConfig, makeGetLabel } from '../utils.js';
import Cell from './Cell.vue';
/** @typedef {'GMSJ01'|'GMSJ0A'|'GMSE01'|'GMSP01'} GameVersion */
export default {
components: {
Cell,
@ -23,21 +30,35 @@ export default {
version: {type: String},
previewConfig: {type: Object},
},
computed: {
l() {
return makeGetLabel(labels, this.$lang);
},
db() {
const version = /**@type{GameVersion}*/(this.version);
return Object.fromEntries(Object.entries(configDB).map(([k, v]) => [
k,
{...v, text: format2previewText(v.fmt, version)},
]));
},
},
data() {
const config = getConfig();
const version = /**@type{GameVersion}*/(this.version);
const config = getConfig(version);
const defaultConfigCell = {
text: format2previewText(defaultConfig[0].fmt, this.version),
text: format2previewText(defaultConfig[0].fmt, version),
...defaultConfig[0],
};
return {config, defaultConfigCell};
},
watch: {
config(config) {
// save config
const sconf = config.map(({text, ...o}) => ({...o}));
localStorage.setItem(lskey, JSON.stringify(sconf));
// emit
this.$emit('config', config);
config: makeUpdateConfig(lskey, defaultConfig),
},
methods: {
/** @param {number} i */
deletionConfirm(i) {
// if (window.confirm(this.l('deletionConfirm'))) {
this.config.splice(i, 1);
},
},
}
@ -57,4 +78,7 @@ export default {
color: red;
cursor: pointer;
}
.btn-ctn button {
display: block;
}
</style>

View file

@ -0,0 +1,61 @@
const base = {
fontSize: 20,
fgRGB: 0xffffff,
fgA: 0xff,
fgRGB2: null,
fgA2: null,
bgRGB: 0,
bgA: 0,
bgLeft: 0,
bgRight: 0,
bgTop: 0,
bgBot: 0,
};
export default {
PAS: {
...base,
x: 16,
y: 200,
fmt: `X Pos <x|.0|39.39>
Y Pos <y|.0|1207.39>
Z Pos <z|.0|-4193.6>
Angle <angle||65535>
H Spd <HSpd|.2|15.15>
V Spd <VSpd|.2|-31.17>`,
},
speed: {
...base,
x: 16,
y: 240,
fmt: `H Spd <HSpd|.2|15.15>
V Spd <VSpd|.2|-31.17>`,
},
detailed: {
...base,
x: 16,
y: 192,
fontSize: 16,
fmt: `X <x|.0|39.39>
Y <y|.0|1207.39>
Z <z|.0|-4193.6>
A <angle||65535>
C <CAngle||9>
H <HSpd|.2|15.15>
V <VSpd|.2|-31.17>
QF <QF||0>
I <invinc||30>
G <goop||36368>
Spin <spin||>`,
},
rect: {
...base,
x: 32,
y: 48,
fontSize: 0,
fmt: '',
bgRight: 536,
bgBot: 384,
bgA: 0x7f,
},
};

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

@ -1,8 +1,22 @@
{
"ja-JP": {
"format": "フォーマット:"
"format": "フォーマット:",
"deletionConfirm": "本当に削除しますか?",
"add": {
"PAS": "+ 位置/角度/速度表示",
"speed": "+ 速度表示",
"detailed": "+ 欲張りセット",
"rect": "+ 長方形"
}
},
"en-US": {
"format": "Format:"
"format": "Format:",
"deletionConfirm": "Are you sure to delete?",
"add": {
"PAS": "+ Position/Angle/Speed Display",
"speed": "+ Speed Display",
"detailed": "+ Detailed Display",
"rect": "+ Rectangle"
}
}
}

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

@ -22,15 +22,13 @@
</template>
<script>
import {getConfig, lskey, buttonValues} from './codegen.js';
import { makeUpdateConfig } from '../utils.js';
import {getConfig, defaultConfig, lskey, buttonValues} from './codegen.js';
import labels from './labels.json';
import {getLabels} from '../codegen.js';
export default {
methods: {
updateConfig() {
localStorage.setItem(lskey, JSON.stringify({button: this.button}));
},
updateConfig: makeUpdateConfig(lskey, defaultConfig),
toggleButton(event, value) {
this.button = event.target.checked ?
this.button | value : // ON

View file

@ -0,0 +1,46 @@
export const code04 = `
0403B6FC 497BD905
041441BC 496B4ED8
`;
export const codeBase = `
801F0000 3D00817F
61089147 3D40817F
614A0024 3D80803B
618C6178 7C006040
41820048 3D80803B
618C3F88 7C006040
40820034 819F0004
898C0013 508C442E
39280031 88080007
7CEA00AE A0080005
7C006040 41820024
39080007 7C084840
4180FFE4 4A854070
2C05FFFF 4082FFF8
88EA0000 39080031
7CE73B79 4182FFE8
7C0838AE 280000FF
4182FFDC 7C030378
4E800020 3C808040
A4044486 3C60817F
85630024 70000040
41A20050 88840019
548007FE 7D605850
5480FFFE 7D6B0214
556B07BE 5480F7BF
41A20030 280B0003
41A00008 39600000
5480EFFE 5080077A
7D8358AE 7D8C0214
280C0005 41A00008
398CFFFB 7D8359AE
99630003 3C002020
60002023 556C183E
5C00603E 5405063E
5407C63E 5409863E
88C30000 89030001
89430002 3C60817F
60639138 38830045
4BFF7109 4A94B08C
`;

View file

@ -1,11 +1,11 @@
export const addrDraw2D = 0x802069dc;
export const code04 = `
0424F32C 495A9CD5
042069DC 495F26B8
`;
export const codes = [
/* 077F9000 000001AF */ `
3D40817F
export const codeBase = `
801F0000 3D00817F
61089147 3D40817F
614A0024 3D80803D
618CA9C0 7C006040
41820048 3D80803D
@ -22,38 +22,25 @@ export const codes = [
7CE73B79 4182FFE8
7C0838AE 280000FF
4182FFDC 7C030378
4E800020 38610E90
4A83C191 3C808040
4E800020 3C808040
A4040D82 3C60817F
84A30024 70000040
85630024 70000040
41A20050 88840019
548007FE 7CA02850
5480FFFE 7CA50214
54A507BE 5480F7BF
41A20030 28050003
41A00008 38A00000
548007FE 7D605850
5480FFFE 7D6B0214
556B07BE 5480F7BF
41A20030 280B0003
41A00008 39600000
5480EFFE 5080077A
7D8328AE 7D8C0214
7D8358AE 7D8C0214
280C0005 41A00008
398CFFFB 7D8329AE
98A30003 3821FFF0
`,
/* li r8 */ `
3C002020 60002023
54AC183E 5C00603E
`,
/* stb r0, fmtCS0+fmtCSD*i(r8) */ `
89230000
89430001 89630002
91610008
`,
/* li r3~r7, drawText, b */ `
452020FF 213200FF
621CFF1D 32005025
25252630 01FF4520
20213001 FFFF621C
1D300151 0707FF08
3102FF36 01FF0231
021E6E20 FF
` /* fmt */,
];
398CFFFB 7D8359AE
99630003 3C002020
60002023 556C183E
5C00603E 5405063E
5407C63E 5409863E
88C30000 89030001
89430002 3C60817F
60639138 38830045
4BFF7109 4AA0D8AC
`;

View file

@ -1,12 +1,11 @@
export const addrDraw2D = 0x80125548;
export const code04 = `
04027900 497D1701
04125548 496D3B4C
`;
export const codes = [
`
3D40817F
export const codeBase = `
801F0000 3D00817F
61089147 3D40817F
614A0024 3D80803A
618C6D70 7C006040
41820048 3D80803A
@ -23,38 +22,25 @@ export const codes = [
7CE73B79 4182FFE8
7C0838AE 280000FF
4182FFDC 7C030378
4E800020 38610E90
4AAD1E35 3C80803F
4E800020 3C80803F
A404545A 3C60817F
84A30024 70000040
85630024 70000040
41A20050 88840019
548007FE 7CA02850
5480FFFE 7CA50214
54A507BE 5480F7BF
41A20030 28050003
41A00008 38A00000
548007FE 7D605850
5480FFFE 7D6B0214
556B07BE 5480F7BF
41A20030 280B0003
41A00008 39600000
5480EFFE 5080077A
7D8328AE 7D8C0214
7D8358AE 7D8C0214
280C0005 41A00008
398CFFFB 7D8329AE
98A30003 3821FFF0
`,
`
3C002020 60002023
54AC183E 5C00603E
`,
`
89230000
89430001 89630002
91610008
`,
`
452020FF 213200FF
621CFF1D 32005025
25252630 01FF4520
20213001 FFFF621C
1D300151 0707FF08
3102FF36 01FF0231
021E6E20 FF
`,
];
398CFFFB 7D8359AE
99630003 3C002020
60002023 556C183E
5C00603E 5405063E
5407C63E 5409863E
88C30000 89030001
89430002 3C60817F
60639138 38830045
4BFF7109 4A92C418
`;

View file

@ -0,0 +1,46 @@
export const code04 = `
0403B54C 497BDAB5
04138DF8 496C029C
`;
export const codeBase = `
801F0000 3D00817F
61089147 3D40817F
614A0024 3D80803A
618CDF98 7C006040
41820048 3D80803A
618CBDA8 7C006040
40820034 819F0004
898C0013 508C442E
39280031 88080007
7CEA00AE A0080005
7C006040 41820024
39080007 7C084840
4180FFE4 4A8531C4
2C05FFFF 4082FFF8
88EA0000 39080031
7CE73B79 4182FFE8
7C0838AE 280000FF
4182FFDC 7C030378
4E800020 3C808040
A404BC26 3C60817F
85630024 70000040
41A20050 88840019
548007FE 7D605850
5480FFFE 7D6B0214
556B07BE 5480F7BF
41A20030 280B0003
41A00008 39600000
5480EFFE 5080077A
7D8358AE 7D8C0214
280C0005 41A00008
398CFFFB 7D8359AE
99630003 3C002020
60002023 556C183E
5C00603E 5405063E
5407C63E 5409863E
88C30000 89030001
89430002 3C60817F
60639138 38830045
4BFF7109 4A93FCC8
`;

View file

@ -1,109 +1,65 @@
import { parseJSON } from '../codegen.js';
import { ASM, liDX, strlen, str2inst, inst2gecko } from '../asm.js';
import { ASM, liDX, str2hex, insts2hex, getFillRectParams, getDrawTextOpt } from '../asm.js';
import { measureText } from '../text.js';
import { int2hex } from '../utils.js';
import { addrs } from '../addrs.js';
export const lskey = 'config/PatternSelector';
import * as GMSJ01 from './code/GMSJ01.js';
import * as GMSJ0A from './code/GMSJ0A.js';
// import * as GMSE01 from './code/GMSE01.js';
// import * as GMSP01 from './code/GMSP01.js';
const codes = { GMSJ01, GMSJ0A };
import * as GMSP01 from './code/GMSP01.js';
const codes = { GMSJ01, GMSJ0A, GMSP01 };
export const defaultConfig = {
x: 16,
y: 320,
fontSize: 20,
x: 498,
y: 462,
fontSize: 14,
fgRGB: 0xffffff,
fgA: 0xff,
fgRGB2: null,
fgA2: null,
label: 'Pattern ',
bgRGB: 0,
bgA: 128,
bgLeft: 2,
bgRight: 4,
bgTop: 2,
bgBot: 2,
};
/** @returns {typeof defaultConfig} */
export function getConfig() {
const config =
(typeof localStorage !== 'undefined' && parseJSON(localStorage.getItem(lskey))) || {};
return { ...defaultConfig, ...config };
const o = { ...defaultConfig, ...config };
return { ...o, text: getPreviewText() };
}
const addrDrawText = 0x817f0238;
const addrCodeBase = 0x817f9000;
const addrPV1Data1 = 0x817f9167;
const addrFmt0 = 0x817f919d;
export const getPreviewText = () => '#0 0 0';
/** @param {keyof typeof codes} version */
export default function codegen(version) {
const { x, y, fontSize, fgRGB, fgA, fgRGB2, fgA2, label } = getConfig();
const colorTop = (fgRGB << 8) | fgA;
const colorBot = fgRGB2 == null || fgA2 == null ? colorTop : (fgRGB2 << 8) | fgA;
const text = label + '>%X>%X>%X';
const fmtCS0 = strlen(label);
const fmtCSD = 3;
/**
* @param {keyof typeof codes} version
* @param {string} baseCode
*/
export default function codegen(version, baseCode) {
const config = getConfig();
let code = baseCode;
code += '077F0410 00000066';
// fillRectOpt
code += insts2hex(getFillRectParams(config, measureText(getPreviewText(), version)));
// drawTextOpt
code += insts2hex(getDrawTextOpt(config));
// fmt
code += '25632558256325582563255800';
// pattern data
code += `452020 FF213200
FF621CFF 1D320050
25252526 3001FF45
20202130 01FFFF62
1C1D3001 510707FF
083102FF 3601FF02
31021E6E 20FF0000
`;
const { code04, addrDraw2D, codes: cs0 } = codes[version];
const cs = cs0.map((e) => e.replace(/\s+/g, ''));
const params = [
liDX(3, x),
liDX(4, y),
liDX(5, fontSize),
liDX(6, colorTop),
colorTop === colorBot ? ASM.mr(7, 6) : liDX(7, colorBot),
].flatMap((e) => e);
const extraOffset = (params.length - 5) << 2; // default: 5 inst
let pc;
// 07
let code07 = '801F0000';
/* li32 rEntry, .data.patterns.PV1-1 */
code07 += liDX(8, addrPV1Data1 + extraOffset)
.map(inst2gecko)
.join('');
code07 += cs[0];
/* li r8 */
code07 += liDX(8, addrFmt0 + extraOffset)
.map(inst2gecko)
.join('');
code07 += cs[1];
/* stb r0/12/12, fmtCS0+fmtCSD*i(r8) */
code07 += [
ASM.stb(0, 8, fmtCS0),
0x540cc63e,
ASM.stb(12, 8, fmtCS0 + fmtCSD),
0x540c863e,
ASM.stb(12, 8, fmtCS0 + fmtCSD * 2),
]
.flatMap((e) => e)
.map(inst2gecko)
.join('');
/* (code) */
code07 += cs[2];
/* r3~r7 */
code07 += params.map(inst2gecko).join('');
code07 += '4CC63182'; // crclr 6
/* bl drawText */
pc = addrCodeBase + (code07.length >> 1);
code07 += ASM.b(addrDrawText - pc, true)
.map(inst2gecko)
.join('');
/* addi */
code07 += '38210010';
/* b 4+$b$.draw2d */
pc = addrCodeBase + (code07.length >> 1);
code07 += ASM.b(4 + addrDraw2D - pc, false)
.map(inst2gecko)
.join('');
/* (code) */
code07 += cs[3];
/* fmt */
// prepend 1 dummy char as 'FF' in code
code07 += str2inst('.' + text)
.map(inst2gecko)
.join('')
.slice(2);
const head07 = [0x06000000 | (addrCodeBase & 0x1ffffff), code07.length >> 1]
.map(inst2gecko)
.join('');
// align code 07 (8 digit = 4 byte)
if (code07.length & 8) code07 += '00000000';
return (code04 + head07 + code07).replace(/\s+/g, '');
return code.replace(/\s/g, '');
}

View file

@ -4,26 +4,16 @@
<Preview :config="previewConfig" />
<div class="config-spacer" />
<TextConfig v-model="textConfig" />
<div>
{{l.label}} <input v-model="label">
</div>
</section>
</template>
<script>
import {getConfig, lskey} from './codegen.js';
import {getConfig, defaultConfig, lskey, getPreviewText} from './codegen.js';
import labels from './labels.json';
import TextConfig from '../TextConfig.vue';
import { makeUpdateConfig } from '../utils.js';
function updateConfig() {
const {x, y, fontSize, fgRGB, fgA, fgRGB2, fgA2, label} = this;
const config = {
x, y, fontSize, fgRGB, fgA, fgRGB2, fgA2, label,
};
localStorage.setItem(lskey, JSON.stringify(config));
this.$emit('config', config);
}
const updateConfig = makeUpdateConfig(lskey, defaultConfig, getPreviewText);
export default {
components: {
TextConfig,
@ -53,9 +43,6 @@ export default {
},
},
},
watch: {
label: updateConfig,
},
};
</script>

View file

@ -15,6 +15,23 @@
<span>{{l.fgColor2}}</span><input type="color" :value="rgbI2S(fgRGB2)" @change="fgRGB2 = rgbS2I($event.target.value)">
<span>{{l.alpha}}</span><input type="number" min="0" max="255" v-model.number="fgA2"><span>/255={{(fgA2/2.55).toFixed(1)}}%</span>
</div>
<div>
<div>
<span>{{ l.bgColor }}</span
><input type="color" :value="rgbI2S(bgRGB)" @change="bgRGB = rgbS2I($event.target.value)" />
<span>{{ l.alpha }}</span
><input type="number" min="0" max="254" v-model.number="bgA" /><span
>/255={{ (bgA / 2.55).toFixed(1) }}%</span
>
</div>
<div>
<span>{{ l.bgOffset }}</span>
<span>{{ l.left }}</span><input type="number" v-model.number="bgLeft" />
<span>{{ l.right }}</span><input type="number" v-model.number="bgRight" />
<span>{{ l.top }}</span><input type="number" v-model.number="bgTop" />
<span>{{ l.bottom }}</span><input type="number" v-model.number="bgBot" />
</div>
</div>
</div>
</template>
@ -41,6 +58,7 @@ export default {
},
...Object.fromEntries([
'x', 'y', 'fontSize', 'fgRGB', 'fgA', 'fgRGB2', 'fgA2',
'bgRGB', 'bgA', 'bgLeft', 'bgRight', 'bgTop', 'bgBot',
].map(k => [k, makeField(k)])),
},
methods: {
@ -72,7 +90,7 @@ input[type=number], td.right {
text-align: right;
}
input[type="number"] {
width: 3em;
width: 2em;
margin: 0 2px;
}
.appearance > div {

View file

@ -0,0 +1,68 @@
export const addrs = {
drawText: 0x817f0238,
drawWater: {
GMSJ01: 0x80206a00,
GMSJ0A: 0x8012556c,
GMSE01: 0x801441e0,
GMSP01: 0x80138e1c,
},
fillRect: {
GMSJ01: 0x80201ea8,
GMSJ0A: 0x80121660,
GMSE01: 0x80140390,
GMSP01: 0x80134f0c,
},
setup2D: {
GMSJ01: 0x80035228,
GMSJ0A: 0x802caecc,
GMSE01: 0x802eb6bc,
GMSP01: 0x802e3864,
},
getPollutionDegree: {
GMSJ01: 0x801ef6b8,
GMSE01: 0x8019db20,
GMSP01: 0x801963a8,
GMSJ0A: 0x8017e26c,
},
checkStickRotate: {
GMSJ01: 0x80130758,
GMSE01: 0x80251304,
GMSP01: 0x80249090,
GMSJ0A: 0x80231054,
},
};
export const r13offs = {
gpMarioOriginal: {
GMSJ01: -0x6748,
GMSE01: -0x60d8,
GMSP01: -0x61b0,
GMSJ0A: -0x6218,
},
gpMarDirector: {
GMSJ01: -0x6818,
GMSE01: -0x6048,
GMSP01: -0x6120,
GMSJ0A: -0x6188,
},
gpCamera: {
GMSJ01: -0x5750,
GMSE01: -0x7118,
GMSP01: -0x7158,
GMSJ0A: -0x5768,
},
gpPollution: {
GMSJ01: -0x6518,
GMSE01: -0x62f0,
GMSP01: -0x63c8,
GMSJ0A: -0x6430,
},
};
// r1 offset of J2DGrafContext in TGCConsole2::perform()
export const ctxSpOff = {
GMSJ01: 0xe90,
GMSJ0A: 0xbec,
GMSE01: 0xbd0,
GMSP01: 0xbe4,
};

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,
);
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,
);
Rc;
/**
* @param {number} op
* @param {number} LL
@ -101,14 +91,17 @@ const InstM = (op, RA, RS, SH, MB, ME, Rc) =>
* @param {number} LK
*/
const InstI = (op, LL, AA, LK) =>
makeInst(((op & 0x3f) << 26) | ((LL & 0xffffff) << 2) | ((AA & 1) << 1) | (LK & 1));
((op & 0x3f) << 26) | ((LL & 0xffffff) << 2) | ((AA & 1) << 1) | (LK & 1);
/** @type {(op: number) => InstD} */
const makeInstD = (op) => (rT, rA, D) => InstD(op, rT, rA, D);
/** @type {(op: number) => InstDS} */
const makeInstDS = (op) => (rA, rS, D) => InstD(op, rA, rS, D);
/** @type {(op: number, op2: number) => InstX} */
const makeInstX = (op, op2) => (rT, rA, rB, Rc) => InstX(op, rT, rA, rB, op2, +Rc);
const makeInstX =
(op, op2) =>
(rT, rA, rB, Rc = 0) =>
InstX(op, rT, rA, rB, op2, +Rc);
/** @type {(op: number, op2: number) => InstXS} */
const makeInstXS = (op, op2) => (rA, rS, rB, Rc) => InstX(op, rA, rS, rB, op2, +Rc);
/** @type {(op: number) => InstM} */
@ -132,6 +125,9 @@ export const ASM = {
lhz: makeInstD(40),
lwz: makeInstD(32),
lfs: makeInstD(48),
lfd: makeInstD(50),
// add
add: makeInstX(31, 266),
// li rT, D
addi: makeInstD(14),
li: (/**@type{number}*/ rT, /**@type{number}*/ D) => InstD(14, rT, 0, D),
@ -144,8 +140,10 @@ export const ASM = {
mr: (/**@type{number}*/ rT, /**@type{number}*/ rA, flag = 0) => InstX(31, rA, rT, rA, 444, flag),
// mask
rlwinm: makeInstM(21),
rlwimi: makeInstM(20),
// b
b: makeInstI(18),
bctr: (/**@type{number|boolean}*/ LK = 0) => InstX(19, 0b10100, 0, 0, 528, LK ? 1 : 0),
// mflr
mflr: (/**@type{number}*/ rT) => InstX(31, rT, 8, 0, 339, 0),
mfctr: (/**@type{number}*/ rT) => InstX(31, rT, 9, 0, 339, 0),
@ -155,6 +153,22 @@ export const ASM = {
// cr
crset: (/**@type{number}*/ B) => InstX(19, B, B, B, 289, 0),
crclr: (/**@type{number}*/ B) => InstX(19, B, B, B, 193, 0),
// float
fmr: (/**@type{number}*/ fT, /**@type{number}*/ fB, Rc = 0) => InstX(63, fT, 0, fB, 72, Rc),
};
export const $load = {
8: ASM.lbz,
16: ASM.lhz,
32: ASM.lwz,
float: ASM.lfs,
};
export const $store = {
8: ASM.stb,
16: ASM.sth,
32: ASM.stw,
float: ASM.stfs,
double: ASM.stfd,
};
/**
@ -163,26 +177,46 @@ export const ASM = {
*/
export function liDX(rT, D) {
if (-0x8000 <= D && D < 0x8000) {
return ASM.li(rT, D);
return [ASM.li(rT, D)];
} else if ((D & 0xffff) === 0) {
return ASM.lis(rT, D >>> 16);
return [ASM.lis(rT, D >>> 16)];
} else {
const h = D >>> 16;
const l = D & 0xffff;
return [...ASM.lis(rT, h), ...ASM.ori(rT, rT, l)];
return [ASM.lis(rT, h), ASM.ori(rT, rT, l)];
}
}
/** @param {string} s */
export function strlen(s) {
const fmtbuf = Encoding.convert(Encoding.stringToCode(s), 'SJIS');
return fmtbuf.length; // not NUL terminated
/**
* @param {string} s
* @param {string} version
*/
export function str2bytes(s, version) {
/** @type {Record<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;
}
/** @param {string} s */
export function str2inst(s) {
const fmtbuf = Encoding.convert(Encoding.stringToCode(s), 'SJIS');
fmtbuf.push(0); // NUL terminated
/**
* @param {string} s
* @param {string} version
*/
export const str2hex = (s, version) =>
str2bytes(s, version)
.map((x) => x.toString(16).toUpperCase().padStart(2, '0'))
.join('');
/**
* @param {string} s
* @param {string} version
*/
export function str2inst(s, version) {
const fmtbuf = str2bytes(s, version);
const fmtlen = fmtbuf.length;
const fmtlen3 = fmtlen & 3;
const pad = fmtlen3 ? 4 - fmtlen3 : 0;
@ -194,12 +228,13 @@ export function str2inst(s) {
return insts;
}
/** @param {number} pc */
export function makeProgram(pc) {
/** @type {Inst[]} */
const bufs = [];
return {
/**
* @param {number} pc
* @param {string} [hex]
*/
export const makeProgram = (pc, hex = '') => ({
pc,
hex,
/**
* @param {number} dst
* @param {boolean} LL
@ -214,12 +249,69 @@ export function makeProgram(pc) {
},
/** @param {Inst[]} codes */
push(...codes) {
bufs.push(...codes);
this.pc += codes.reduce((a, e) => a + e.length, 0) << 2;
this.hex += codes.map(inst2gecko).join('');
this.pc += codes.length << 2;
},
dump: () => bufs.flatMap((e) => e),
};
}
/** @param {string} data */
pushHex(data) {
this.hex += data;
this.pc += data.length >> 1;
},
align() {
const l4 = this.pc % 4;
if (l4) {
const diff = 4 - l4;
this.hex += ''.padEnd(diff << 1, '0');
this.pc += diff;
}
},
});
/** @param {number} x */
export const inst2gecko = (x) => (x >>> 0).toString(16).toUpperCase().padStart(8, '0');
/** @param {Inst[]} insts */
export const insts2hex = (insts) => insts.map(inst2gecko).join('');
/**
* @param {{
* x: number,
* y: number,
* fontSize: number,
* bgRGB: number,
* bgA: number,
* bgLeft: number,
* bgRight: number,
* bgTop: number,
* bgBot: number
* }} opt
* @param {{width: number, height: number}} size
**/
export const getFillRectParams = (
{ x, y, fontSize, bgRGB, bgA, bgLeft, bgRight, bgTop, bgBot },
{ width, height },
) => [
// rect
x - bgLeft, // x0
y - fontSize - bgTop, // y0
x + Math.ceil((width * fontSize) / 20) + bgRight, // x1
y - fontSize + Math.ceil((height * fontSize) / 20) + bgBot, // y1
// color
(bgRGB << 8) | bgA,
];
/**
* @param {{
* x: number,
* y: number,
* fontSize: number,
* fgRGB: number
* fgA: number
* fgRGB2: number | null
* fgA2: number | null
* }} config
*/
export function getDrawTextOpt({ x, y, fontSize, fgRGB, fgA, fgRGB2, fgA2 }) {
const colorTop = (fgRGB << 8) | fgA;
const colorBot = fgRGB2 == null || fgA2 == null ? colorTop : (fgRGB2 << 8) | fgA;
return [((x & 0xffff) << 16) | (y & 0xffff), fontSize, colorTop, colorBot];
}

View file

@ -6,7 +6,13 @@
"fgColorGrad": "グラデーション",
"fgColor1": "文字色(上)",
"fgColor2": "文字色(下)",
"alpha": "不透明度="
"alpha": "不透明度=",
"bgColor": "背景色:",
"bgOffset": "背景位置:",
"left": "左",
"right": "右",
"top": "上",
"bottom": "下"
},
"en-US": {
"location": "Location: ",
@ -15,6 +21,22 @@
"fgColorGrad": "Gradient",
"fgColor1": "Font color(Top): ",
"fgColor2": "Font color(Bottom): ",
"alpha": "Alpha="
"alpha": "Alpha=",
"bgColor": "Background color: ",
"bgOffset": "Background offset: ",
"left": "Left",
"right": "Right",
"top": "Top",
"bottom": "Bottom"
},
"fr-FR": {
"location": "Position : ",
"fontSize": "Taille de police : ",
"fgColor": "Couleur du texte : ",
"fgColorGrad": "Dégradé",
"fgColor1": "Couleur du texte (haut) : ",
"fgColor2": "Couleur du texte (bas) : ",
"bgColor": "Couleur de fond : ",
"alpha": "Alpha = "
}
}

View file

@ -1,10 +1,13 @@
import { parseJSON } from '../codegen.js';
import { getFillRectParams } from '../asm';
import { int2hex } from '../utils';
export const lskey = 'config/qft';
export const getPreviewText = () => '0:00.000';
export const defaultConfig = {
x: 16,
y: 456,
width: 112,
fontSize: 20,
fgRGB: 0xffffff,
fgA: 0xff,
@ -12,6 +15,10 @@ export const defaultConfig = {
fgA2: null,
bgRGB: 0x000000,
bgA: 0x80,
bgLeft: 0,
bgRight: 2,
bgTop: 2,
bgBot: 0,
freezeDuration: 30,
freeze: {
yellowCoin: false,
@ -35,6 +42,7 @@ export const defaultConfig = {
},
};
/** @returns {typeof defaultConfig} */
export function getConfig() {
const config =
(typeof localStorage !== 'undefined' && parseJSON(localStorage.getItem(lskey))) || {};
@ -45,6 +53,7 @@ export function getConfig() {
...defaultConfig.freeze,
...config.freeze,
},
text: getPreviewText(),
};
}
@ -57,8 +66,10 @@ import * as GMSP01 from './code/GMSP01.js';
import * as GMSJ0A from './code/GMSJ0A.js';
export const codes = { GMSJ01, GMSE01, GMSP01, GMSJ0A };
import statusDB from './code/status.js';
export const statusKeys = Object.keys(statusDB);
import { measureText } from '../text.js';
import statusDB0 from './code/status.js';
const statusDB = /**@type{Record<string,number[]>}*/ (statusDB0);
export const statusKeys = Object.keys(statusDB0);
/****
## save freeze frame, load and save QF
@ -100,7 +111,7 @@ export default function codegen(version, baseCode) {
const hook = freezeCodeHooks[key];
if (hook) {
if (key === 'blueCoin') {
const addr = hook;
const addr = /**@type{number}*/ (hook);
// special: needs to adjust QF -> use separate C2 instead
code += [
0xc2000000 + (addr & 0x1ffffff),
@ -234,25 +245,24 @@ export default function codegen(version, baseCode) {
// ui
/* bounds */
const { x, y, fontSize, width } = config;
const { x, y, fontSize, bgLeft, bgRight, bgTop, bgBot } = config;
const rect = getFillRectParams(config, measureText(getPreviewText(), version));
const [bgColor] = rect.splice(-1);
const scale = fontSize / 20;
code += '077F0094 0000001D';
code += [
x, // x1
y - fontSize - 2, // y1
x + width * scale, // x2
y, // y2
]
.map(int2gecko)
.join('');
code += rect.map(int2gecko).join('');
code += '25753a253032752e2530337500000000'; // fmt
/* fontSize, fgColor, bgColor */
code += '077F0110 00000010';
const bgColor = (config.bgRGB & 0xffffff) * 256 + (config.bgA & 0xff);
/**
* 817F0110 drawTextOpt: {x, y, fontSize, colorTop, colorBot}
* 817F0120 bgColor
*/
code += '077F0110 00000014';
const fgColor = (config.fgRGB & 0xffffff) * 256 + (config.fgA & 0xff);
const fgColor2 =
((config.fgRGB2 ?? config.fgRGB) & 0xffffff) * 256 + ((config.fgA2 ?? config.fgA) & 0xff);
code += [x, y].map((x) => int2hex(x, 2)).join('');
code += [fontSize, fgColor, fgColor2, bgColor].map(int2gecko).join('');
code += '00000000';
return code.replace(/\s/g, '');
}

View file

@ -2,17 +2,7 @@
<div id="config">
<section class="appearance">
<h3>{{ l.h3 }}</h3>
<div>
<TextConfig v-model="textConfig" />
</div>
<div>
<span>{{ l.bgColor }}</span
><input type="color" :value="rgbI2S(bgRGB)" @change="bgRGB = rgbS2I($event.target.value)" />
<span>{{ l.alpha }}</span
><input type="number" min="0" max="255" v-model.number="bgA" /><span
>/255={{ (bgA / 2.55).toFixed(1) }}%</span
>
</div>
</section>
<section class="freeze">
<h3>{{ l.freeze.h3 }}</h3>
@ -35,31 +25,12 @@
</template>
<script>
import { getConfig, lskey, codes, statusKeys } from './codegen.js';
import { rgbI2S, rgbS2I, rgbaI2S } from '../utils';
import { defaultConfig, lskey, getConfig, getPreviewText, codes, statusKeys } from './codegen.js';
import { makeUpdateConfig, rgbI2S, rgbS2I, rgbaI2S } from '../utils';
import labels from './labels.json';
import TextConfig from '../TextConfig.vue';
function updateConfig() {
const { x, y, fontSize, width, fgRGB, fgA, fgRGB2, fgA2, bgRGB, bgA, freeze, freezeDuration } = this;
const config = {
x,
y,
fontSize,
width,
fgRGB,
fgA,
fgRGB2,
fgA2,
bgRGB,
bgA,
freeze,
freezeDuration,
};
localStorage.setItem(lskey, JSON.stringify(config));
this.$emit('config', config);
}
const updateConfig = makeUpdateConfig(lskey, defaultConfig, getPreviewText);
export default {
components: {
TextConfig,
@ -87,21 +58,9 @@ export default {
rgbaI2S,
},
data() {
const { x, y, fontSize, width, fgRGB, fgA, fgRGB2, fgA2, bgRGB, bgA, freeze, freezeDuration } =
getConfig();
const config = getConfig();
return {
x,
y,
fontSize,
fgRGB,
fgA,
fgRGB2,
fgA2,
width,
bgRGB,
bgA,
freeze,
freezeDuration,
...config,
// const
freezeKeys: [
...Object.keys(codes[this.version]?.freezeCodeHooks ?? {}),
@ -115,8 +74,7 @@ export default {
},
textConfig: {
get() {
const { x, y, fontSize, fgRGB, fgA, fgRGB2, fgA2 } = this;
return { x, y, fontSize, fgRGB, fgA, fgRGB2, fgA2 };
return this;
},
set(value) {
Object.assign(this, value);
@ -126,8 +84,6 @@ export default {
},
watch: {
width: updateConfig,
bgRGB: updateConfig,
bgA: updateConfig,
freezeDuration: updateConfig,
},
};

View file

@ -1,14 +1,6 @@
{
"ja-JP": {
"h3": "見た目",
"location": "位置:",
"fontSize": "文字サイズ:",
"fgColor": "文字色:",
"fgColorGrad": "グラデーション",
"fgColor1": "文字色(上)",
"fgColor2": "文字色(下)",
"bgColor": "背景色:",
"alpha": "不透明度=",
"preview": "プレビュー",
"previewNote": "※ x座標の範囲は0~600、y座標の範囲は16~464です",
"freeze": {
@ -40,14 +32,6 @@
},
"en-US": {
"h3": "Appearance",
"location": "Location: ",
"fontSize": "Font size: ",
"fgColor": "Font color: ",
"fgColorGrad": "Gradient",
"fgColor1": "Font color (top): ",
"fgColor2": "Font color (bottom): ",
"bgColor": "Background color: ",
"alpha": "Alpha=",
"preview": "Preview",
"previewNote": "※ x ranges from 0 to 600, and y ranges from 16 to 464.",
"freeze": {
@ -79,14 +63,6 @@
},
"fr-FR": {
"h3": "Apparence",
"location": "Position : ",
"fontSize": "Taille de police : ",
"fgColor": "Couleur du texte : ",
"fgColorGrad": "Dégradé",
"fgColor1": "Couleur du texte (haut) : ",
"fgColor2": "Couleur du texte (bas) : ",
"bgColor": "Couleur de fond : ",
"alpha": "Alpha = ",
"preview": "Aperçu",
"previewNote": "※ x doit être entre 0 et 600, et y entre 16 et 464.",
"freeze": {

View file

@ -0,0 +1,54 @@
/** @typedef {{index: number, kerning: number, width: number, code: number}} CharInfo */
import charInfoJP from '../../data/charInfo-JP.json';
import charInfoEU from '../../data/charInfo-EU.json';
/**
* @param {string} version
*/
const getFontInfo = (version) =>
['GMSJ01', 'GMSJ0A'].includes(version)
? {
// JP
charInfo: /**@type{Record<string, CharInfo>}*/ (charInfoJP),
rowSize: 24, // how many char in a row of the img
}
: {
// EU (TODO US)
charInfo: /**@type{Record<string, CharInfo>}*/ (charInfoEU),
rowSize: 16, // how many char in a row of the img
};
/**
* @param {string} text
* @param {string} version
*/
export function measureText(text, version) {
const { charInfo, rowSize } = getFontInfo(version);
/** @type {{x: number, y: number, u: number, v: number}[]} */
const chars = [];
let x = 0;
let y = 0;
let w = 0;
let useKerning = false;
text.split('').forEach((c) => {
const { index, kerning, width } = charInfo[c] ?? charInfo['?'];
if (c === '\n') {
useKerning = false;
x = 0;
y += 20;
return;
}
if (useKerning) x -= kerning;
useKerning = true;
// uv
const [u, v] = [(index % rowSize) * 20, ((index / rowSize) | 0) * 20];
chars.push({ x, y, u, v });
// next
x += width + kerning;
// update width
if (x > w) w = x;
});
return { chars, width: w, height: y + 20 };
}

View file

@ -1,3 +1,38 @@
/**
* @template T extends {Record<string, any>|Record<string, any>[]}
* @param {string} lskey
* @param {T} defaultConfig
* @param {(config: T)=>string} [makeText]
*/
export function makeUpdateConfig(lskey, defaultConfig, makeText) {
const configKeys = Object.keys(defaultConfig);
/** @type {(o: any)=>T} */
const makeConfig =
defaultConfig instanceof Array
? (o) => o.config
: (o) => Object.fromEntries(configKeys.map((k) => [k, o[k]]));
/** @this {any} */
return function updateConfig() {
// save config to localStorage
const config = makeConfig(this);
localStorage.setItem(lskey, JSON.stringify(config));
// emit `config` event to parent
this.$emit('config', makeText ? { ...config, text: makeText(config) } : config);
};
}
/**
* @param {number} x -- number to convert
* @param {number} size -- byte count
*/
export const int2hex = (x, size) =>
(x >>> 0)
.toString(16)
.toUpperCase()
.padStart(size << 1, '0')
.slice(-(size << 1));
/** @param {number} rgb */
export const rgbI2S = (rgb) => '#' + rgb.toString(16).padStart(6, '0');
/** @param {string} s */
@ -9,7 +44,35 @@ export const rgbS2I = (s) => parseInt(s.slice(1), 16);
export const rgbaI2S = (rgb, a) =>
'#' + rgb.toString(16).padStart(6, '0') + a.toString(16).padStart(2, '0');
export const fg2Style = (rgb, a, rgb2, a2) => {
const c = rgbaI2S(rgb, a);
return rgb2 == null || a2 == null ? c : `linear-gradient(180deg, ${c}, ${rgbaI2S(rgb2, a2)})`;
};
/** @type {(labels: Record<string, any>, locale: string, fallbackLocale?: string) => (key: string) => string} */
export const makeGetLabel =
(labels, locale, fallbackLocale = 'en-US') =>
(key) => {
const segs = key.split('.');
for (const localeTry of [locale, fallbackLocale]) {
let o = labels[localeTry];
for (const seg of segs) {
if (o == null) break;
o = o[seg];
}
if (o != null) return o;
}
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];
}

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB