Add CustomizedDisplay and preview for NTSC-J

This commit is contained in:
sup39 2022-10-15 19:31:18 +09:00
parent b80d408287
commit d31cf7d266
23 changed files with 1312 additions and 86 deletions

238
Codes.xml
View file

@ -2861,6 +2861,7 @@
</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>
@ -3089,6 +3090,7 @@
</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>
@ -3277,6 +3279,163 @@
60000000 00000000
</source>
</code>
<code>
<category>metadata</category>
<id>CustomizedDisplay</id>
<title lang="en-US">Customized display</title>
<title lang="ja-JP">カスタマイズ表示</title>
<author>sup39(サポミク)</author>
<version>0.1</version>
<date>Oct 15, 2022</date>
<dependencies>drawText</dependencies>
<description lang="en-US">
Shows metadata at any given time.
::: warning
This code is not compatible with other Display codes.
:::
::: warning
The preview is based on NTSC-J's font data, and may be inaccurate for NTSC-U and PAL.
In addition, some characters may not shown properly.
:::
#### Format
Use `&lt; ID-of-the-data | format | value-shown-in-preview &gt;` to display a metadata.
Supported data:
| ID |data|type|
|----|----|----|
|`x`|X coordinate of Mario|float|
|`y`|Y coordinate of Mario|float|
|`z`|Z coordinate of Mario|float|
|`angle`|Angle of Mario|uint16|
|`HSpd`|Horizontal speed of Mario|float|
|`VSpd`|Vertical speed of Mario|float|
|`QF`|QF offset|\{0,1,2,3}|
For float data, you can set the *format* to `.{digit}` to specify how many digits to show.
%details[
%summary[All printable characters (for NTSC-J)]
![Printable charaters](/img/preview/font-jp.png){style="background:black"}
]
#### Preview
</description>
<description lang="ja-JP">
指定した情報を表示します。
#### フォーマット
ゲーム内の情報を表示するために、`&lt; 情報のID | 表示のフォーマット | プレビューで表示する値 &gt;`を使います。
表示可能の情報一覧:
|情報のID|情報|型|
|----|----|----|
|`x`|マリオのX座標|float|
|`y`|マリオのY座標|float|
|`z`|マリオのZ座標|float|
|`angle`|マリオの角度|uint16|
|`HSpd`|マリオの水平速度|float|
|`VSpd`|マリオのY速度|float|
|`QF`|ずれたQFの数|\{0,1,2,3}|
float(小数)型に対して、「表示のフォーマット」を`.{桁数}`に設定して何桁まで表示するか指定できます。
また、全てのひらがなとカタカナ及び一部の漢字の表示も可能です。
%details[
%summary[表示可能な文字一覧:]
![表示可能な文字一覧](/img/preview/font-jp.png){style="background:black"}
]
#### プレビュー
</description>
<source version="GMSJ01">
C6206734 817FA000
077FA000 00000084
4A83B229 806D98B8
C0230010 C0430014
C0630018 A1230096
C08300B0 C0A300A8
806D97E8 81430058
554A07BE 48000035
5820252E 30660A59
20252E30 660A5A20
252E3066 0A412025
68750A48 20252E32
660A5620 252E3266
0A514620 25750000
7D0802A6 38600010
388000C8 38A00012
38C0FFFF 7CC73378
4CC63242 4BFF61BD
4AA0C6B8 00000000
</source>
<source version="GMSJ0A">
C61252A0 817FA000
077FA000 00000084
4AAD0ECD 806D9DE8
C0230010 C0430014
C0630018 A1230096
C08300B0 C0A300A8
806D9E78 81430058
554A07BE 48000035
5820252E 30660A59
20252E30 660A5A20
252E3066 0A412025
68750A48 20252E32
660A5620 252E3266
0A514620 25750000
7D0802A6 38600010
388000C8 38A00012
38C0FFFF 7CC73378
4CC63242 4BFF61BD
4A92B224 00000000
</source>
<source version="GMSE01">
C6143F14 817FA000
077FA000 00000084
4AAF16BD 806D9F28
C0230010 C0430014
C0630018 A1230096
C08300B0 C0A300A8
806D9FB8 81430058
554A07BE 48000035
5820252E 30660A59
20252E30 660A5A20
252E3066 0A412025
68750A48 20252E32
660A5620 252E3266
0A514620 25750000
7D0802A6 38600010
388000C8 38A00012
38C0FFFF 7CC73378
4CC63242 4BFF61BD
4A949E98 00000000
</source>
<source version="GMSP01">
C6138B50 817FA000
077FA000 00000084
4AAE9865 806D9E50
C0230010 C0430014
C0630018 A1230096
C08300B0 C0A300A8
806D9EE0 81430058
554A07BE 48000035
5820252E 30660A59
20252E30 660A5A20
252E3066 0A412025
68750A48 20252E32
660A5620 252E3266
0A514620 25750000
7D0802A6 38600010
388000C8 38A00012
38C0FFFF 7CC73378
4CC63242 4BFF61BD
4A93EAD4 00000000
</source>
</code>
<code>
<category>qol</category>
<presets>standard,recommended,il</presets>
@ -3671,6 +3830,7 @@
</code>
<code>
<category>qol</category>
<id>PatternSelector</id>
<title lang="en-US">Pattern Selector</title>
<title lang="ja-JP">パターン選択</title>
<author>sup39(サポミク)</author>
@ -3678,8 +3838,6 @@
<date>Apr 25, 2022</date>
<dependencies>drawText</dependencies>
<description lang="en-US">
![Preview](/img/PatternSelector/preview.jpg)
Ⓑ is the cursor to select the pattern,
and the three digits are the numbers representing the pattern.
See below for more information.
@ -3718,8 +3876,6 @@
%object[]{data="/img/PatternSelector/PV1-3.svg"}
</description>
<description lang="ja-JP">
![プレビュー](/img/PatternSelector/preview.jpg)
Ⓑはパターンを選択するためのカーソルであり、三つの数字はパターンの番号を表します。
各パターンの番号は下記を参照してください。
@ -4054,6 +4210,80 @@
7C0803A6 4E800020
00000000 59800004
</source>
<source version="GMSE01">
077F0238 00000110
9421FED8 7C0802A6
BF810118 7C7F1B78
9001012C 7C9E2378
90C100B8 7CBD2B78
90E100BC 7D1C4378
912100C0 914100C4
40860024 D82100C8
D84100D0 D86100D8
D88100E0 D8A100E8
D8C100F0 D8E100F8
D9010100 39200600
390100BC B12100A0
39210130 912100A4
392100A8 912100A8
808D9FC8 38E100B8
7FA6EB78 38A00000
38610008 6FDE8000
4AADDA39 3D204330
91210108 3D40817F
93C1010C 6FFF8000
C00A0344 38610070
C9810108 91210110
3D20817F 93E10114
FC4C0028 C0690340
C9810110 93A10064
FC2C0028 93A10060
4AB59AC9 38800000
38610070 4AB72AF9
38C100A0 7F85E378
388000FF 38610008
4AADD895 BB810118
38210128 80010004
7C0803A6 4E800020
00000000 59800004
</source>
<source version="GMSP01">
077F0238 00000110
9421FED8 7C0802A6
BF810118 7C7F1B78
9001012C 7C9E2378
90C100B8 7CBD2B78
90E100BC 7D1C4378
912100C0 914100C4
40860024 D82100C8
D84100D0 D86100D8
D88100E0 D8A100E8
D8C100F0 D8E100F8
D9010100 39200600
390100BC B12100A0
39210130 912100A4
392100A8 912100A8
808D9EF0 38E100B8
7FA6EB78 38A00000
38610008 6FDE8000
4AAD5ACD 3D204330
91210108 3D40817F
93C1010C 6FFF8000
C00A0344 38610070
C9810108 91210110
3D20817F 93E10114
FC4C0028 C0690340
C9810110 93A10064
FC2C0028 93A10060
4AB51D25 38800000
38610070 4AB6AD19
38C100A0 7F85E378
388000FF 38610008
4AAD5929 BB810118
38210128 80010004
7C0803A6 4E800020
00000000 59800004
</source>
</code>
<code>
<id>InstantRestart</id>

View file

@ -23,6 +23,19 @@ The codes are stored in the `Codes.xml` file. If you want to add or change codes
When adding new codes keep in mind that the English title/description are mandatory.
#### Codes with configuration
Codes with configuration are usually defined as vue components,
which is defined in [site/.vuepress/components/codes/](site/.vuepress/components/codes/).
When creating/updating those codes,
in addition to editing the `Codes.xml` file,
you may also need to check the following files:
- [site/.vuepress/components/codes/codegen.js](site/.vuepress/components/codes/codegen.js):
Specify the Gecko code generator function of the code.
The version string will be passed as the first argument.
- [site/.vuepress/components/codes/ui.js](site/.vuepress/components/codes/ui.js):
Specify the vue component for the configuration of the code.
The version string will be passed as a property.
#### Reserved Memory
Some codes store some states in the games memory starting from address 0x817F0000. To avoid collisions use a memory range in the unallocated ranges:

View file

@ -2,6 +2,11 @@
## Oct 16, 2022
\[QFT v1.2] Fix QFT background when 112\*fontSize is not an integer
## Oct 15, 2022
- Add preview for selected codes (for NTSC-J)
- Add CustomizedDisplay
- Add instructions to create/update codes with configuration in Readme.md
## Jun 8, 2022
### Fixed 'Shine Get Timer' stopping on any cutscene started after touching a Shine
Shoutouts to plankton for touching the Pinna 1 Shine before its spawn cutscene started, in which case the timer would stop around 8 seconds early.

106
package-lock.json generated
View file

@ -9,6 +9,8 @@
"version": "3.0.1",
"license": "Apache-2.0",
"dependencies": {
"@types/encoding-japanese": "^2.0.1",
"encoding-japanese": "^2.0.0",
"vuedraggable": "2.24.3"
},
"devDependencies": {
@ -1869,6 +1871,11 @@
"@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=="
},
"node_modules/@types/express": {
"version": "4.17.13",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz",
@ -3748,6 +3755,18 @@
"ms": "2.0.0"
}
},
"node_modules/body-parser/node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/body-parser/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@ -6572,6 +6591,14 @@
"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==",
"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",
@ -8899,12 +8926,12 @@
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
@ -12582,6 +12609,18 @@
"node": ">= 0.8"
}
},
"node_modules/raw-body/node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
@ -17198,18 +17237,6 @@
"node": ">=12"
}
},
"node_modules/whatwg-encoding/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/whatwg-mimetype": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
@ -18850,6 +18877,11 @@
"@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=="
},
"@types/express": {
"version": "4.17.13",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz",
@ -20451,6 +20483,15 @@
"ms": "2.0.0"
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@ -22759,6 +22800,11 @@
"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=="
},
"end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@ -24528,12 +24574,12 @@
"dev": true
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
"safer-buffer": ">= 2.1.2 < 3.0.0"
}
},
"icss-replace-symbols": {
@ -27491,6 +27537,15 @@
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"dev": true
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
}
}
},
@ -31223,17 +31278,6 @@
"dev": true,
"requires": {
"iconv-lite": "0.6.3"
},
"dependencies": {
"iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
}
}
}
},
"whatwg-mimetype": {

View file

@ -22,8 +22,10 @@
"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.7",
"@vuepress/plugin-medium-zoom": "1.9.7",
"encoding-japanese": "^2.0.0",
"jsdom": "20.0.0",
"pre-commit": "1.2.2",
"prettier": "2.7.1",

View file

@ -10,9 +10,10 @@
>
<span v-else>{{ getLabel('codeinfo.author') }} {{ translatedCode.author }}</span>
</div>
<Preview v-if="showPreview" :config="previewConfig" />
<p class="description" v-html="translatedCode.description"></p>
<component v-if="configUI" :is="configUI" :version="version"
:codeConfigs="codeConfigs" @config="$emit('config', {[code.id]: $event})" />
:previewConfig="previewConfig" @config="$emit('config', {[code.id]: $event})" />
</div>
</template>
@ -25,7 +26,7 @@ export default {
anchor: { type: Boolean },
code: { type: Object },
version: { type: String },
codeConfigs: { type: Object },
previewConfig: { type: Object },
},
computed: {
translatedCode: function () {
@ -34,6 +35,15 @@ export default {
configUI: function () {
return configUIs[this.code.id];
},
showPreview() {
return [
'PatternSelector',
'PASDisplay',
'SpeedDisplay',
'CustomizedDisplay',
'qft',
].includes(this.code.id); // TODO
},
},
data() {
return {};

View file

@ -72,7 +72,7 @@ export default {
// generate file
const codeSize = c.reduce((a, e) => a+e.source.length, 0)/2 + 16; // 8(00D0)+8(F000)
console.log(codeSize, c);
// console.log(codeSize, c);
switch (this.format) {
case 'gct':
this.alertGCTCodeSize(codeSize);

View file

@ -43,7 +43,7 @@
<div v-if="codes && codes.length > 0" class="help">
<h3>{{ getLabel('headers.help') }}</h3>
<CodeInfo v-if="!!inspectingCode" :code="inspectingCode" :version="selectedVersion"
:codeConfigs="codeConfigs" @config="onCodeConfigChanged" />
:previewConfig="previewConfig" @config="onCodeConfigChanged" />
<div v-else-if="showStageLoaderHelp">
<h3>{{ getLabel('headers.stageloader') }}</h3>
<div>
@ -106,12 +106,14 @@
<script>
// Data
import gameVersions from '../data/gameVersions.json';
import codeCategories from '../data/codeCategories.json';
// Util
import { translate } from '../i18n/localeHelper';
// Code Configs
import {getConfig as qftGetConfig} from './codes/qft/codegen';
import {getConfig as getConfigQFT} from './codes/qft/codegen';
import {getConfig as getConfigCD} from './codes/CustomizedDisplay/codegen';
export default {
data() {
@ -131,7 +133,11 @@ export default {
},
created() {
this.codeConfigs = {
qft: qftGetConfig(),
qft: getConfigQFT(),
PatternSelector: {},
SpeedDisplay: {},
PASDisplay: {},
CustomizedDisplay: getConfigCD(this.version),
};
},
methods: {
@ -207,6 +213,17 @@ export default {
this.codeConfigs = {...this.codeConfigs, ...e};
},
},
computed: {
previewConfig() {
const {id, category} = this.inspectingCode ?? {};
const {exclusive} = codeCategories.find((c) => c.identifier === category) ?? {};
const ids = new Set(this.selectedCheats
.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)));
},
},
};
</script>
<style scoped>

View file

@ -1,39 +1,57 @@
<template>
<div class="preview-root">
<div class="preview-ctn">
<div :style="qft.bgStyle" />
<PreviewString v-if="qft" :x="qft.x" :y="qft.y" :size="qft.fontSize" :color="qft.color" text="0:00:00" />
<PreviewString v-if="mdp" :x="mdp.x" :y="mdp.y" :size="mdp.fontSize" :color="mdp.color" :text="mdp.text" />
<PreviewString :x="16" :y="320" :size="20" :color="'#fff'" text="Pattern #0 0 0" />
<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="config.PatternSelector" :x="16" :y="320" :size="20" :color="'#fff'" text="Pattern #0 0 0" />
</div>
</div>
</template>
<script>
import { rgbaI2S, fg2Style } from './codes/utils.js';
export default {
props: {
config: {type: Object},
},
computed: {
mdp() {
return {
mdps() {
const {config} = this;
if (config.PASDisplay) return [{
x: 16,
y: 200,
fontSize: 20,
color: '#fff',
text: 'X Pos -4\nY Pos 27\nZ Pos -1515\nAngle 3117\nH Spd 4.26\nV Spd 4.28',
};
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 i2s = (rgb, a) => '#'+rgb.toString(16).padStart(6, '0')+a.toString(16).padStart(2, '0');
const bg = i2s(bgRGB, bgA);
const fg1 = i2s(fgRGB, fgA);
const bg = rgbaI2S(bgRGB, bgA);
return {
x, y, fontSize,
color: fgRGB2==null || fgA2==null ? fg1 : `linear-gradient(180deg, ${fg1}, ${i2s(fgRGB2, fgA2)})`,
color: fg2Style(fgRGB, fgA, fgRGB2, fgA2),
bgStyle: {
left: x+'px',
top: (y-fontSize)+'px',

View file

@ -1,5 +1,5 @@
<template>
<div class="preview-str" :style="styles.root" >
<div v-if="text" class="preview-str" :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" />

View file

@ -0,0 +1,51 @@
<template>
<div>
<TextConfig v-model="textConfig" />
<div>
<div>{{l.format}}</div>
<textarea v-model="fmt" :rows="rows" :cols="cols" />
</div>
</div>
</template>
<script>
import labels from './labels.json';
import TextConfig from '../TextConfig.vue';
import {format2previewText} from './codegen.js';
export default {
components: {
TextConfig,
},
props: {
value: {type: Object},
version: {type: String},
rows: {type: Number, default: 7},
cols: {type: Number, default: 40},
},
data() {
const {fmt, text, ...textConfig} = this.value;
return {
textConfig,
fmt,
};
},
computed: {
l() {
return labels[this.$lang] ?? labels['en-US'];
},
preview() {
return format2previewText(this.fmt, this.version);
},
},
methods: {
update() {
this.$emit('input', {fmt: this.fmt, text: this.preview, ...this.textConfig});
},
},
watch: {
textConfig() {this.update()},
fmt() {this.update()},
},
}
</script>

View file

@ -0,0 +1,219 @@
import * as Encoding from 'encoding-japanese';
/**
* @typedef {number[]} Inst
*
* @typedef {(
* rT: number,
* rA: number,
* D: number,
* ) => Inst} InstD
* @typedef {(
* rS: number,
* rA: number,
* D: number,
* ) => Inst} InstDS
*
* @typedef {(
* rA: number,
* rS: number,
* SH: number,
* MB: number,
* ME: number,
* Rc: number|boolean,
* ) => Inst} InstM
*
* @typedef {(
* rT: number,
* rA: number,
* rB: number,
* Rc: number|boolean,
* ) => Inst} InstX
* @typedef {(
* rS: number,
* rA: number,
* rB: number,
* Rc: number|boolean,
* ) => Inst} InstXS
*
* @typedef {(
* LL: number,
* LK: number|boolean,
* AA?: number|boolean,
* ) => 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
* @param {number} rA
* @param {number} D
*/
const InstD = (op, rT, rA, D) =>
makeInst(((op & 0x3f) << 26) | ((rT & 0x1f) << 21) | ((rA & 0x1f) << 16) | (D & 0xffff));
/**
* @param {number} op
* @param {number} rT
* @param {number} rA
* @param {number} rB
* @param {number} op2
* @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,
);
/**
* @param {number} op
* @param {number} RS
* @param {number} RA
* @param {number} SH
* @param {number} MB
* @param {number} ME
* @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,
);
/**
* @param {number} op
* @param {number} LL
* @param {number} AA
* @param {number} LK
*/
const InstI = (op, LL, AA, LK) =>
makeInst(((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);
/** @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} */
const makeInstM = (op) => (rA, rS, SH, MB, ME, Rc) => InstM(op, rA, rS, SH, MB, ME, +Rc);
/** @type {(op: number) => InstI} */
const makeInstI =
(op) =>
(LL, LK, AA = 0) =>
InstI(op, LL >> 2, +AA, +LK);
export const ASM = {
// store rT, rA, D
stb: makeInstD(38),
sth: makeInstD(44),
stw: makeInstD(36),
stfs: makeInstD(52),
stfd: makeInstD(54),
stwu: makeInstD(37),
// load rS, rA, D
lbz: makeInstD(34),
lhz: makeInstD(40),
lwz: makeInstD(32),
lfs: makeInstD(48),
// li rT, D
addi: makeInstD(14),
li: (/**@type{number}*/ rT, /**@type{number}*/ D) => InstD(14, rT, 0, D),
addis: makeInstD(15),
lis: (/**@type{number}*/ rT, /**@type{number}*/ D) => InstD(15, rT, 0, D),
// ori rA, rS, D
ori: makeInstDS(24),
// or rA, rS, rB, flag
or: makeInstXS(31, 444),
mr: (/**@type{number}*/ rT, /**@type{number}*/ rA, flag = 0) => InstX(31, rA, rT, rA, 444, flag),
// mask
rlwinm: makeInstM(21),
// b
b: makeInstI(18),
// mflr
mflr: (/**@type{number}*/ rT) => InstX(31, rT, 8, 0, 339, 0),
mfctr: (/**@type{number}*/ rT) => InstX(31, rT, 9, 0, 339, 0),
// mtlr
mtlr: (/**@type{number}*/ rS) => InstX(31, rS, 8, 0, 467, 0),
mtctr: (/**@type{number}*/ rS) => InstX(31, rS, 9, 0, 467, 0),
// cr
crset: (/**@type{number}*/ B) => InstX(19, B, B, B, 289, 0),
crclr: (/**@type{number}*/ B) => InstX(19, B, B, B, 193, 0),
};
/**
* @param {number} rT
* @param {number} D
*/
export function liDX(rT, D) {
if (-0x8000 <= D && D < 0x8000) {
return ASM.li(rT, D);
} else if ((D & 0xffff) === 0) {
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)];
}
}
/** @param {string} s */
export function str2inst(s) {
const fmtbuf = Encoding.convert(Encoding.stringToCode(s), 'SJIS');
fmtbuf.push(0); // NUL terminated
const fmtlen = fmtbuf.length;
const fmtlen3 = fmtlen & 3;
const pad = fmtlen3 ? 4 - fmtlen3 : 0;
fmtbuf.push(...Array(pad).fill(0));
const dv = new DataView(Uint8Array.from(fmtbuf).buffer);
const insts = Array((fmtlen + pad) >> 2)
.fill(0)
.map((_, i) => dv.getUint32(i << 2, false));
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} x */
export const inst2gecko = (x) => (x >>> 0).toString(16).toUpperCase().padStart(8, '0');

View file

@ -0,0 +1,427 @@
import { parseJSON } from '../codegen.js';
import { ASM, makeInst, liDX, str2inst, makeProgram, inst2gecko } from './asm.js';
export const lskey = 'config/CustomizedDisplay';
export const defaultConfig = [
{
x: 16,
y: 200,
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>`,
},
];
/** @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 }) => ({
...o,
fmt,
text: format2previewText(fmt, 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),
}),
};
/** @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),
},
];
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: 0x80206734,
GMSJ0A: 0x801252a0,
GMSE01: 0x80143f14,
GMSP01: 0x80138b50,
};
const addrsSetup2D = {
GMSJ01: 0x80035228,
GMSJ0A: 0x802caecc,
GMSE01: 0x802eb6bc,
GMSP01: 0x802e3864,
};
const addrDrawText = 0x817f0238;
const addrDst = 0x817fa000;
/**
* @param {GameVersion} version
*/
export default function codegen(version) {
const config = getConfig(version);
let spOff = 0;
const fcodes = /** @type {Inst[]} */ ([]);
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);
}
const addrOrig = addrsOrig[version];
const addrSetup2D = addrsSetup2D[version];
// program
const program = makeProgram(addrDst);
// addi r1, r1, -spOff
if (spOff) program.push(ASM.addi(1, 1, -spOff));
// bl setup
program.bl(addrSetup2D);
// (drawText)
for (const code of fcodes) {
program.push(code);
program.bl(addrDrawText);
}
// addi r1, r1, spOff
if (spOff) program.push(ASM.addi(1, 1, spOff));
// b orig+4
program.b(addrOrig + 4);
// dump code
const pcode = program.dump();
const psize = pcode.length;
return [
makeInst((0xc6 << 24) | (addrOrig & 0x1ffffff)),
makeInst(addrDst),
makeInst((0x06 << 24) | (addrDst & 0x1fffffff)),
makeInst(psize << 2),
pcode,
psize & 1 ? [0] : [],
]
.flatMap((e) => e)
.map(inst2gecko)
.join('');
}

View file

@ -0,0 +1,60 @@
<template>
<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>
<Cell :value="c" @input="$event => config.splice(i, 1, $event)" :version="version" />
</div>
<div>
<button @click="config.push(defaultConfigCell)" class="textcell-add">+</button>
</div>
</div>
</template>
<script>
import { defaultConfig, getConfig, lskey, format2previewText } from './codegen.js';
import Cell from './Cell.vue';
export default {
components: {
Cell,
},
props: {
version: {type: String},
previewConfig: {type: Object},
},
data() {
const config = getConfig();
const defaultConfigCell = {
text: format2previewText(defaultConfig[0].fmt, this.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);
},
},
}
</script>
<style scoped>
.textcell {
border: 1px solid black;
padding: 4px 8px;
margin: 4px 0;
}
.textcell-remove {
padding: 0;
background: transparent;
border: none;
font-size: 1.2rem;
color: red;
cursor: pointer;
}
</style>

View file

@ -0,0 +1,8 @@
{
"ja-JP": {
"format": "フォーマット:"
},
"en-US": {
"format": "Format:"
}
}

View file

@ -0,0 +1,90 @@
<template>
<div>
<div>
<span>{{l.location}}(</span><input type="number" min="0" max="600" v-model.number="x"><span>, </span><input type="number" min="16" max="464" v-model.number="y"><span>)</span>
</div>
<div>
<span>{{l.fontSize}}</span><input type="number" min="0" v-model.number="fontSize">
</div>
<div>
<span>{{fgRGB2==null ? l.fgColor : l.fgColor1}}</span><input type="color" :value="rgbI2S(fgRGB)" @change="fgRGB = rgbS2I($event.target.value)">
<span>{{l.alpha}}</span><input type="number" min="0" max="255" v-model.number="fgA"><span>/255={{(fgA/2.55).toFixed(1)}}%</span>
<input type="checkbox" :checked="fgRGB2!=null" @change="toggleGradient"><span>{{l.fgColorGrad}}</span>
</div>
<div v-if="fgRGB2 != null">
<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>
</template>
<script>
import labels from './labels.json';
import {rgbI2S, rgbS2I, rgbaI2S} from './utils';
const makeField = key => ({
get() {
return this.$props.value[key];
},
set(value) {
this.update({[key]: value});
},
});
export default {
props: {
value: {type: Object},
},
computed: {
l() {
return labels[this.$lang] ?? labels['en-US'];
},
...Object.fromEntries([
'x', 'y', 'fontSize', 'fgRGB', 'fgA', 'fgRGB2', 'fgA2',
].map(k => [k, makeField(k)])),
},
methods: {
update(o) {
this.$emit('input', {...this.value, ...o});
},
toggleGradient($event) {
if ($event.target.checked) {
this.update({
fgRGB2: this.fgRGB,
fgA2: this.fgA,
});
} else {
this.update({
fgRGB2: null,
fgA2: null,
});
}
},
rgbI2S,
rgbS2I,
rgbaI2S,
},
}
</script>
<style scoped>
input[type=number], td.right {
text-align: right;
}
input[type="number"] {
width: 3em;
margin: 0 2px;
}
.appearance > div {
padding: 0 0 4px;
}
input[type=number] {
-moz-appearance: textfield;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
</style>

View file

@ -1,9 +1,11 @@
import InstantRestart from './InstantRestart/codegen.js';
import qft from './qft/codegen.js';
import CustomizedDisplay from './CustomizedDisplay/codegen.js';
export default {
InstantRestart,
qft,
CustomizedDisplay,
};
/**

View file

@ -0,0 +1,20 @@
{
"ja-JP": {
"location": "位置:",
"fontSize": "文字サイズ:",
"fgColor": "文字色:",
"fgColorGrad": "グラデーション",
"fgColor1": "文字色(上)",
"fgColor2": "文字色(下)",
"alpha": "不透明度="
},
"en-US": {
"location": "Location: ",
"fontSize": "Font size: ",
"fgColor": "Font color: ",
"fgColorGrad": "Gradient",
"fgColor1": "Font color(Top): ",
"fgColor2": "Font color(Bottom): ",
"alpha": "Alpha="
}
}

View file

@ -3,27 +3,12 @@
<section v-if="['GMSJ01', 'GMSJ0A'].includes(version)" class="appearance">
<h3>{{l.h3}}</h3>
<div>
<span>{{l.location}}(</span><input type="number" min="0" max="600" v-model.number="x"><span>, </span><input type="number" min="16" max="464" v-model.number="y"><span>)</span>
</div>
<div>
<span>{{l.fontSize}}</span><input type="number" min="0" v-model.number="fontSize">
</div>
<div>
<span>{{fgRGB2==null ? l.fgColor : l.fgColor1}}</span><input type="color" :value="rgbI2S(fgRGB)" @change="fgRGB = rgbS2I($event.target.value)">
<span>{{l.alpha}}</span><input type="number" min="0" max="255" v-model.number="fgA"><span>/255={{(fgA/2.55).toFixed(1)}}%</span>
<input type="checkbox" :checked="fgRGB2!=null" @change="toggleGradient"><span>{{l.fgColorGrad}}</span>
</div>
<div v-if="fgRGB2 != null">
<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>
<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>
<h4>{{l.preview}}</h4>
<Preview :config="codeConfigs" />
<div style="white-space: pre">{{l.previewNote}}</div>
</section>
<section class="freeze">
<h3>{{l.freeze.h3}}</h3>
@ -44,9 +29,10 @@
</template>
<script>
// import Preview from '../../PreviewString.vue';
import {getConfig, lskey, codes} from './codegen.js';
import {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;
@ -58,9 +44,11 @@ function updateConfig() {
}
export default {
components: {
TextConfig,
},
props: {
version: {type: String},
codeConfigs: {type: Object},
},
methods: {
onChangeFreeze($event, key) {
@ -76,15 +64,17 @@ export default {
this.fgA2 = null;
}
},
rgbI2S: (x) => '#'+x.toString(16).padStart(6, '0'),
rgbS2I: (s) => parseInt(s.slice(1), 16),
rgbaI2S: (rgb, a) => '#'+rgb.toString(16).padStart(6, '0')+a.toString(16).padStart(2, '0'),
updateConfig,
rgbI2S,
rgbS2I,
rgbaI2S,
},
data() {
const {x, y, fontSize, width, fgRGB, fgA, fgRGB2, fgA2, bgRGB, bgA, freeze, freezeDuration} = getConfig();
return {
x, y, fontSize, width,
fgRGB, fgA, fgRGB2, fgA2, bgRGB, bgA,
x, y, fontSize,
fgRGB, fgA, fgRGB2, fgA2,
width, bgRGB, bgA,
freeze, freezeDuration,
// const
freezeKeys: Object.keys(codes[this.version]?.freezeCodeInfo ?? {}),
@ -94,16 +84,19 @@ export default {
l() {
return labels[this.$lang] ?? labels['en-US'];
},
textConfig: {
get() {
const {x, y, fontSize, fgRGB, fgA, fgRGB2, fgA2} = this;
return {x, y, fontSize, fgRGB, fgA, fgRGB2, fgA2};
},
set(value) {
Object.assign(this, value);
this.updateConfig();
},
},
},
watch: {
x: updateConfig,
y: updateConfig,
fontSize: updateConfig,
width: updateConfig,
fgRGB: updateConfig,
fgA: updateConfig,
fgRGB2: updateConfig,
fgA2: updateConfig,
bgRGB: updateConfig,
bgA: updateConfig,
freezeDuration: updateConfig,

View file

@ -10,7 +10,7 @@
"bgColor": "背景色:",
"alpha": "不透明度=",
"preview": "プレビュー",
"previewNote": "※ x座標の範囲は0~600、y座標の範囲は16~464です\n※ ゲーム内のフォントはプレビューのより幅が広いです",
"previewNote": "※ x座標の範囲は0~600、y座標の範囲は16~464です",
"freeze": {
"h3": "一時停止",
"duration": "長さ:",
@ -39,7 +39,7 @@
"bgColor": "Background color: ",
"alpha": "Alpha=",
"preview": "Preview",
"previewNote": "※ x ranges from 0 to 600, and y ranges from 16 to 464.\n※ The font is wider in the game compared to the preview.",
"previewNote": "※ x ranges from 0 to 600, and y ranges from 16 to 464.",
"freeze": {
"h3": "Freezing the timer",
"duration": "Duration: ",

View file

@ -1,7 +1,9 @@
import InstantRestart from './InstantRestart/config.vue';
import CustomizedDisplay from './CustomizedDisplay/config.vue';
import qft from './qft/config.vue';
export default {
InstantRestart,
CustomizedDisplay,
qft,
};

View file

@ -0,0 +1,15 @@
/** @param {number} rgb */
export const rgbI2S = (rgb) => '#' + rgb.toString(16).padStart(6, '0');
/** @param {string} s */
export const rgbS2I = (s) => parseInt(s.slice(1), 16);
/**
* @param {number} rgb
* @param {number} a
*/
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)})`;
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB