[v0.1.0-beta.2] Added static variables; improved UI

- Added api.getVersion()
- Added static variables
- Improved UI
  - Added buttons to show/hide UI elements
  - Added button to reload managers
This commit is contained in:
sup39 2023-07-24 22:58:57 +09:00
parent 5449b8f03d
commit 393a9fcd2f
No known key found for this signature in database
GPG key ID: 14D2E0D21140D260
21 changed files with 846 additions and 101 deletions

View file

@ -1,4 +1,10 @@
# Changelog
## v0.1.0-beta.2 (2023/07/24)
- Added api.getVersion()
- Added static variables
- Improved UI
- Added buttons to show/hide UI elements
- Added button to reload managers
## v0.1.0-beta.1 (2023/07/23)
- Implemented ObjectViewer
- load/reload `ObjectParameters/*.json`

2
Cargo.lock generated
View file

@ -854,7 +854,7 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "sup-smsac"
version = "0.1.0-beta.1"
version = "0.1.0-beta.2"
dependencies = [
"clap",
"encoding_rs",

View file

@ -1,6 +1,6 @@
[package]
name = "sup-smsac"
version = "0.1.0-beta.1"
version = "0.1.0-beta.2"
edition = "2021"
license = "MIT OR Apache-2.0"
authors = ["sup39 <sms@sup39.dev>"]

View file

@ -0,0 +1,47 @@
{
"J3DFrameCtrl": {
"size": 20,
"offsets": [
{
"offset": "0",
"type": "void*",
"name": "vtable",
"notes": "",
"hidden": true
},
{
"offset": "4",
"type": "u8",
"name": "field_0x4",
"notes": "",
"hidden": true
},
{
"offset": "5",
"type": "u8",
"format": "hex",
"name": "Flag",
"notes": "",
"hidden": false
},
{
"offset": "8",
"type": "s16",
"name": "Length",
"notes": ""
},
{
"offset": "c",
"type": "float",
"name": "Step",
"notes": ""
},
{
"offset": "10",
"type": "float",
"name": "Counter",
"notes": ""
}
]
}
}

View file

@ -12,7 +12,8 @@
"offset": "20",
"type": "JStage::TActor",
"name": "Inherited fields",
"notes": ""
"notes": "",
"hidden": true
},
{
"offset": "24",

View file

@ -0,0 +1,31 @@
{
"MActor": {
"size": 72,
"offsets": [
{
"offset": "0",
"type": "MActorAnmData*",
"name": "",
"notes": ""
},
{
"offset": "4",
"type": "J3DModel*",
"name": "",
"notes": ""
},
{
"offset": "c",
"type": "MActorAnmBck*",
"name": "Current Animation",
"notes": ""
},
{
"offset": "28",
"type": "MActorAnmBck**",
"name": "Animations?",
"notes": ""
}
]
}
}

View file

@ -0,0 +1,19 @@
{
"MActorAnmBck": {
"size": 60,
"offsets": [
{
"offset": "0",
"type": "s32",
"name": "id",
"notes": ""
},
{
"offset": "4",
"type": "J3DFrameCtrl",
"name": "Frame *",
"notes": ""
}
]
}
}

View file

@ -0,0 +1,59 @@
{
"TApplication": {
"size": 80,
"offsets": [
{
"offset": "0",
"type": "TApplication*",
"name": "Pointer to itself",
"notes": "",
"hidden": true
},
{
"offset": "4",
"type": "TDirector*",
"name": "Current director",
"notes": ""
},
{
"offset": "8",
"type": "u8",
"name": "Director type",
"notes": "Director type\n2: [null] setup for 3? (after boot)\n3: [TGCLogoDir] Show Nintendo logo\n4: [TMovieDirector] Title screen FMV\n5: [TMarDirector/TMovieDirector] Regular game play (including most FMV)\n6: [TMovieDirector] other independent FMV (e.g. FLUDD tutorial, BH2 warp zone)\n7: Error? \n8: [TSelectDir] shine select\n9: [TMenuDirector] level select (for debug purpose?)"
},
{
"offset": "a",
"type": "u16",
"format": "hex",
"name": "Previous area",
"notes": ""
},
{
"offset": "e",
"type": "u16",
"format": "hex",
"name": "Current area",
"notes": ""
},
{
"offset": "12",
"type": "u16",
"format": "hex",
"name": "Next area",
"notes": ""
},
{
"offset": "18",
"type": "u16",
"name": "Current FMV",
"notes": ""
},
{
"offset": "38",
"type": "s32",
"name": "Current save file",
"notes": ""
}
]
}
}

View file

@ -0,0 +1,55 @@
{
"TBiancoGateKeeper": {
"size": 672,
"offsets": [
{
"offset": "0",
"type": "TSpineEnemy",
"name": "Inherited fields",
"notes": ""
},
{
"offset": "13c",
"type": "u8",
"name": "HP",
"notes": ""
},
{
"offset": "154",
"type": "u32",
"name": "Received water count",
"notes": "Increased in TGateKeeperBase::receiveMessage()\nReset in TGateKeeperBase::perform() every frame"
},
{
"offset": "17c",
"type": "s16",
"name": "Total water damage (~255)",
"notes": ""
},
{
"offset": "17e",
"type": "s16",
"name": "Water damage timer (QF)",
"notes": ""
},
{
"offset": "292",
"type": "u8",
"name": "GateKeeper type",
"notes": "[0] in AP\n[1] in BH [2] BH unlock\n[3] RH unlock\n[4] GB unlock"
},
{
"offset": "296",
"type": "s16",
"name": "Death count",
"notes": ""
},
{
"offset": "298",
"type": "s16",
"name": "Timer to launch Goro (BH only)",
"notes": ""
}
]
}
}

View file

@ -61,14 +61,15 @@
"offset": "60",
"type": "float",
"name": "Entry radius",
"notes": ""
"notes": "",
"hidden": true
},
{
"offset": "64",
"type": "u32",
"format": "hex",
"name": "Collision flag\n (0) active / (1) no collision",
"notes": ""
"name": "Collision flag",
"notes": "(0) active / (1) no collision"
}
]
}

View file

@ -18,32 +18,22 @@
{
"offset": "74",
"type": "MActor*",
"name": "",
"notes": ""
"name": "MActor",
"notes": "",
"hidden": true
},
{
"offset": ["74", "c", "0"],
"type": "s32",
"name": "Animation id",
"type": "MActorAnmBck",
"name": "Current Animation *",
"notes": ""
},
{
"offset": ["74", "28", "0", "14"],
"type": "float",
"name": "Animation frame counter",
"notes": ""
},
{
"offset": ["74", "28", "0", "c"],
"type": "s16",
"name": "Animation frame length",
"notes": ""
},
{
"offset": ["74", "28", "0", "10"],
"type": "float",
"name": "Animation frame rate",
"notes": ""
"offset": ["74", "28", "0", "0"],
"type": "MActorAnmBck",
"name": "Animations[0] *",
"notes": "",
"hidden": true
},
{
"offset": "7c",
@ -70,7 +60,8 @@
"offset": "8c",
"type": "TSpineBase<TLiveActor>*",
"name": "AI",
"notes": ""
"notes": "",
"hidden": true
},
{
"offset": ["8c", "1c", "0"],
@ -94,37 +85,43 @@
"offset": "94",
"type": "JGeometry::TVec3<float>",
"name": "* Movement (unit/step)",
"notes": ""
"notes": "",
"hidden": true
},
{
"offset": "ac",
"type": "JGeometry::TVec3<float>",
"name": "* Speed (unit/step)",
"notes": ""
"notes": "",
"hidden": true
},
{
"offset": "bc",
"type": "float",
"name": "Wall hitbox width",
"notes": ""
"notes": "",
"hidden": true
},
{
"offset": "c4",
"type": "TBGCheckData*",
"name": "",
"notes": ""
"name": "Ground",
"notes": "",
"hidden": true
},
{
"offset": "c8",
"type": "float",
"name": "Ground Height",
"notes": ""
"notes": "",
"hidden": true
},
{
"offset": "cc",
"type": "float",
"name": "Gravity (unit/step²)",
"notes": ""
"notes": "",
"hidden": true
},
{
"offset": "f0",

View file

@ -0,0 +1,62 @@
{
"TMarDirector::TDemoInfo": {
"size": 34,
"offsets": [
{
"offset": "0",
"type": "string",
"name": "BCK name",
"notes": ""
},
{
"offset": ["4", "0"],
"type": "JGeometry::TVec3<float>",
"name": "location.*",
"notes": ""
},
{
"offset": "8",
"type": "s32",
"name": "",
"notes": ""
},
{
"offset": "c",
"type": "float",
"name": "",
"notes": ""
},
{
"offset": "10",
"type": "u8",
"name": "",
"notes": "bool"
},
{
"offset": "14",
"type": "void*",
"name": "Callback",
"notes": ""
},
{
"offset": "18",
"type": "u32",
"format": "hex",
"name": "Callback parameter",
"notes": ""
},
{
"offset": "1c",
"type": "TActor*",
"name": "TActor*",
"notes": ""
},
{
"offset": "20",
"type": "s16",
"name": "flag",
"notes": ""
}
]
}
}

View file

@ -0,0 +1,166 @@
{
"TMarDirector": {
"size": 616,
"offsets": [
{
"offset": "4c",
"type": "u16",
"format": "hex",
"name": "Game mode flags",
"notes": ""
},
{
"offset": "4e",
"type": "u16",
"format": "hex",
"name": "",
"notes": ""
},
{
"offset": "50",
"type": "u16",
"format": "hex",
"name": "",
"notes": ""
},
{
"offset": "54",
"type": "u32",
"name": "QF synchronizer",
"notes": ""
},
{
"offset": "58",
"type": "u32",
"name": "Game QF",
"notes": ""
},
{
"offset": "5c",
"type": "u32",
"name": "Global QF",
"notes": ""
},
{
"offset": "60",
"type": "u32",
"name": "Started QF of cutscene",
"notes": ""
},
{
"offset": "64",
"type": "u8",
"name": "Game state",
"notes": ""
},
{
"offset": "7c",
"type": "s8",
"name": "Current stage",
"notes": ""
},
{
"offset": "7d",
"type": "s8",
"name": "Current episode",
"notes": ""
},
{
"offset": "b4",
"type": "u8",
"name": "Next director type",
"notes": ""
},
{
"offset": "124",
"type": "u8",
"name": "Current game mode state",
"notes": ""
},
{
"offset": "125",
"type": "u8",
"name": "Previous game mode state",
"notes": ""
},
{
"offset": "126",
"type": "u8",
"name": "Next game mode state",
"notes": ""
},
{
"offset": "12c",
"type": "TMarDirector::TDemoInfo",
"name": "slot0 *",
"notes": ""
},
{
"offset": "150",
"type": "TMarDirector::TDemoInfo",
"name": "slot1 *",
"notes": ""
},
{
"offset": "174",
"type": "TMarDirector::TDemoInfo",
"name": "slot2 *",
"notes": ""
},
{
"offset": "198",
"type": "TMarDirector::TDemoInfo",
"name": "slot3 *",
"notes": ""
},
{
"offset": "1bc",
"type": "TMarDirector::TDemoInfo",
"name": "slot4 *",
"notes": ""
},
{
"offset": "1e0",
"type": "TMarDirector::TDemoInfo",
"name": "slot5 *",
"notes": ""
},
{
"offset": "204",
"type": "TMarDirector::TDemoInfo",
"name": "slot6 *",
"notes": ""
},
{
"offset": "228",
"type": "TMarDirector::TDemoInfo",
"name": "slot7 *",
"notes": ""
},
{
"offset": "24c",
"type": "u8",
"name": "Next cutscene index to assign",
"notes": ""
},
{
"offset": "24d",
"type": "u8",
"name": "Next cutscene index to play",
"notes": ""
},
{
"offset": "260",
"type": "u8",
"name": "Stage load done",
"notes": ""
},
{
"offset": "261",
"type": "s8",
"name": "Prompt id",
"notes": ""
}
]
}
}

View file

@ -0,0 +1,19 @@
{
"TMarDirector@QF": {
"size": 616,
"offsets": [
{
"offset": "58",
"type": "u32",
"name": "Game QF",
"notes": ""
},
{
"offset": "5c",
"type": "u32",
"name": "Global QF",
"notes": ""
}
]
}
}

View file

@ -0,0 +1,131 @@
{
"TMario": {
"size": 17040,
"offsets": [
{
"offset": "0",
"type": "TTakeActor",
"name": "Inherited fields",
"notes": ""
},
{
"offset": "7c",
"type": "u32",
"format": "hex",
"name": "Current state",
"notes": ""
},
{
"offset": "80",
"type": "u32",
"format": "hex",
"name": "Previous state",
"notes": ""
},
{
"offset": "84",
"type": "u16",
"name": "Substate",
"notes": ""
},
{
"offset": "86",
"type": "u16",
"name": "Substate timer",
"notes": ""
},
{
"offset": "8c",
"type": "float",
"name": "Base acceleration (0-32)",
"notes": ""
},
{
"offset": "90",
"type": "s16",
"name": "Acceleration direction",
"notes": ""
},
{
"offset": "94",
"type": "s16",
"name": "X angle",
"notes": ""
},
{
"offset": "96",
"type": "s16",
"name": "Y angle (yaw)",
"notes": ""
},
{
"offset": "98",
"type": "s16",
"name": "Z angle",
"notes": ""
},
{
"offset": "a4",
"type": "JGeometry::TVec3<float>",
"name": "* Speed",
"notes": ""
},
{
"offset": "b0",
"type": "float",
"name": "Forward speed",
"notes": ""
},
{
"offset": "e0",
"type": "TBGCheckData*",
"name": "Floor triangle under Mario",
"notes": ""
},
{
"offset": "e8",
"type": "float",
"name": "Height of the ceiling above Mario",
"notes": "9,999,999 if none"
},
{
"offset": "ec",
"type": "float",
"name": "Height of the floor below Mario",
"notes": "-32,767 if none"
},
{
"offset": "f0",
"type": "float",
"name": "Height of the water surface at Marios position",
"notes": "Y position if none"
},
{
"offset": "118",
"type": "u32",
"name": "Mario flags",
"format": "hex",
"notes": "& 0x00000001: \n& 0x00000002: Above a sewer floor?\n& 0x00000004: Is visible?\n& 0x00000008: Talking to NPC?\n& 0x00000010: Left water recently?\n& 0x00000020: Is in shadow\n& 0x00000040: Is in goop\n& 0x00000080: Filling FLUDD\n& 0x00000100: Is NOT on wire (starts off?)\n& 0x00000200: \n& 0x00000400: Is \"TOO BAD!\"?\n& 0x00000800: Is Ground Pound sit up?\n& 0x00001000: Has helmet?\n& 0x00002000: \n& 0x00004000: Is turbo boosting\n& 0x00008000: Has FLUDD?\n& 0x00010000: Is standing in water?\n& 0x00020000: Is in water?\n& 0x00040000: Is above sand?\n& 0x00080000: \n& 0x00100000: \n& 0x00200000: \n& 0x00400000: \n& 0x00800000: \n& 0x01000000: \n& 0x02000000: \n& 0x04000000: \n& 0x08000000: \n& 0x10000000: \n& 0x20000000: \n& 0x40000000: \n& 0x80000000: \n"
},
{
"offset": "11C",
"type": "u32",
"format": "hex",
"name": "Previous Mario flags",
"notes": ""
},
{
"offset": "120",
"type": "s16",
"name": "HP",
"notes": ""
},
{
"offset": "12c",
"type": "float",
"name": "Underwater HP",
"notes": ""
}
]
}
}

View file

@ -263,6 +263,11 @@ pub async fn handle_command(
})
},
"getVersion" => {
let_dolphin!(d);
Ok(json!(d.ver().to_string()))
},
"reload" => {
let mut lock_obj_params = env.obj_params_result.lock().await;
load_obj_params(&env.obj_params_dir)

View file

@ -5,7 +5,11 @@
pub enum SMSVersion {
GMSJ01, GMSE01, GMSP01, GMSJ0A,
}
pub mod vt;
impl std::fmt::Display for SMSVersion {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
std::fmt::Debug::fmt(self, fmt)
}
}
use crate::addr::Addr;
use crate::dolphin::{DolphinMemory, Dolphin};
@ -20,6 +24,7 @@ impl Dolphin for SMSDolphin {
}
}
pub mod vt;
impl SMSDolphin {
#[inline]
pub fn pid(&self) -> usize {

View file

@ -4,6 +4,7 @@
// @ts-check
/**
* @typedef {number|number[]} ReqAddr
* @typedef {'GMSJ01'|'GMSE01'|'GMSP01'|'GMSJ0A'} SMSVersion
*/
/** @param {string} s */
@ -27,10 +28,10 @@ function Client({onClose = null}={}) {
/**
* @template T
* @param {string} action
* @param {any} payload
* @param {any} [payload]
* @returns {Promise<T>}
*/
const request = (action, payload) => new Promise((rsv, rjt) => {
const request = (action, payload=null) => new Promise((rsv, rjt) => {
if (ws == null) throw Error('Client is not connected to server. Use `client.connect()` first.');
const id = nextId++;
reqs.set(id, {rsv, rjt});
@ -62,7 +63,7 @@ function Client({onClose = null}={}) {
/**
* @returns {Promise<number|null>}
*/
init: () => request('init', null),
init: () => request('init'),
/**
* @param {ReqAddr} addr
@ -123,16 +124,19 @@ function Client({onClose = null}={}) {
*/
getFields: type => request('getFields', type),
getManagers: () => request('getManagers', 0)
.then((/**@type{[addr: number, cls: string, name: string, count: number][]}*/rows) =>
rows.map(row => ({addr: row[0], cls: row[1], name: row[2], count: row[3]}))),
getManagers: () => request('getManagers')
.then((/**@type{[addr: number, type: string, name: string, count: number][]|null}*/rows) =>
rows?.map(row => ({addr: row[0], type: row[1], name: row[2], count: row[3]})) ?? []),
/**
* @param {ReqAddr} addr
*/
getManagees: addr => request('getManagees', addr)
.then((/**@type{[addr: number, cls: string, name: string][]}*/rows) =>
rows.map(row => ({addr: row[0], cls: row[1], name: row[2]}))),
.then((/**@type{[addr: number, type: string, name: string][]|null}*/rows) =>
rows?.map(row => ({addr: row[0], type: row[1], name: row[2]})) ?? []),
/** @returns {Promise<SMSVersion>} */
getVersion: () => request('getVersion'),
reload: () => request('reload', null),
},

View file

@ -36,18 +36,28 @@ table.list td {
padding: 1px 0.3em;
white-space: pre-line;
}
#managers td:nth-child(1),
#managers td:nth-child(5) {
#managerList td:nth-child(1),
#managerList td:nth-child(5) {
padding: 0 2px
}
#managers tr.managee > td:nth-child(2) {
#managerList tr.managee > td:nth-child(2) {
padding-left: 1em
}
#fields-viewer td:nth-child(1),
#fields-viewer td:nth-child(3) {
#fieldsViewer h3 {
margin-block-start: 0;
margin-block-end: 0.5em;
}
#fieldsViewer td:nth-child(1),
#fieldsViewer td:nth-child(3) {
text-align: right;
}
#fieldsViewer td:nth-child(4) {
display: none;
}
#fieldsViewer.showNotes td:nth-child(4) {
display: unset;
}
.flex {
display: flex;
@ -55,11 +65,23 @@ table.list td {
}
.flex > * {
margin-right: 4px;
margin-block-end: 1em;
}
body.wrap .flex {
flex-wrap: wrap;
}
.hidden {
display: none;
}
main {
visibility: hidden;
}
body.ready main {
visibility: visible;
}
body {
background: var(--bg);
color: var(--fg);
@ -68,7 +90,7 @@ body {
font-size: 14px;
padding: 4px;
}
body.disconnected #msg {
body.error #msg {
background: var(--bg-red);
}
h1 {
@ -83,6 +105,10 @@ header {
margin-block-end: 0.75em;
padding-left: 4px;
}
section {
margin-block-end: 0.5em;
}
details {
border: solid 1px var(--fg);
padding: 0.5em 1em;

View file

@ -1,14 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<title>SMS Web Object Viewer (v0.1.0-beta.1)</title>
<title>SMS Web Object Viewer (v0.1.0-beta.2)</title>
<link rel="stylesheet" type="text/css" href="index.css">
<link rel="icon" type="image/svg+xml" href="icon.svg">
<script src="api.js"></script>
<script src="index.js"></script>
</head>
<body>
<h1>SMS Web Object Viewer</h1>
<h1>SMS Web Object Viewer (v0.1.0-beta.2)</h1>
<header>
<div id="msg"></div>
<details>
@ -20,13 +20,27 @@
<iframe id="license" src="/LICENSE.html" title="license"></iframe>
</details>
</header>
<section>
<button id="btn-reload" class="hidden">Reload Object Parameters</button>
</section>
<section class="flex-wrapper">
<div class="flex">
<table id="managers" class="list"></table>
<table id="fields-viewer" class="list"></table>
</div>
</section>
<main>
<section>
<button id="btnReloadManagers">Reload Managers</button>
<input type="checkbox" id="cbShowManagers" checked>
<label for="cbShowManagers">Show Manager List</label>
<input type="checkbox" id="cbShowObjParamsNotes">
<label for="cbShowObjParamsNotes">Show Notes of Object Parameters</label>
<input type="checkbox" id="cbWrapFlex">
<label for="cbWrapFlex">Wrap UI</label>
<button id="btnReloadObjParams">Reload ObjectParameters</button>
</section>
<section class="flex-wrapper">
<div class="flex">
<div id="managerList">
<table class="list"></table>
</div>
<div id="fieldsViewer" class="hidden">
<h3></h3>
<table class="list"></table>
</div>
</div>
</section>
</main>
</body>

View file

@ -3,8 +3,8 @@
// @ts-check
/**
* @typedef {{addr: number, cls: string, name: string, count: number}} Manager
* @typedef {{addr: number, cls: string, name: string}} Managee
* @typedef {{addr: number, type: string, name: string, count: number}} Manager
* @typedef {{addr: number, type: string, name: string}} Managee
* @typedef {(td: HTMLTableCellElement) => void} CellFactory
* @typedef {CellFactory[]} RowFactory
* @typedef {{name: string, notes: string, offset: string|string[], type: string}} Field
@ -16,12 +16,6 @@
const fmt = {
/** @param {number} x */
hex: x => x.toString(16).toUpperCase(),
/** @type {(x: number) => string} */
float: (LOG_10_2 => x => {
const u = Math.floor(LOG_10_2*(Math.log2(Math.abs(x))-23));
return x === 0 ? '0.0' : u > 0 || u < -8 ? x.toExponential(7) :
x.toFixed(-u);
})(Math.log10(2)),
};
/**
@ -29,38 +23,113 @@ const fmt = {
* @param {((td: HTMLTableCellElement)=>void)[][]} gTable
*/
function initTable(table, gTable) {
const nRow = gTable.length;
for (let r=table.rows.length; r<nRow; r++) table.insertRow();
for (let r=table.rows.length; r>nRow; r--) table.deleteRow(-1);
gTable.forEach((gRow, r) => {
const row = table.rows[r];
const nCol = gRow.length;
for (let c=row.cells.length; c<nCol; c++) row.insertCell();
for (let c=row.cells.length; c>nCol; c--) row.deleteCell(-1);
gRow.forEach((g, c) => g(row.cells[c]));
const nRow0 = table.rows.length;
for (let r=0; r<nRow0; r++) table.deleteRow(-1);
gTable.forEach(gRow => {
const row = table.insertRow();
gRow.forEach(g => g(row.insertCell()));
});
}
document.addEventListener('DOMContentLoaded', async () => {
const elmMsg = /**@type {HTMLDivElement}*/(document.getElementById('msg'));
const btnReload = /**@type {HTMLButtonElement}*/(document.getElementById('btn-reload'));
const elmManagers = /**@type {HTMLTableElement}*/(document.getElementById('managers'));
const elmFieldsViewer = /**@type {HTMLTableElement}*/(document.getElementById('fields-viewer'));
const btnReloadObjParams = /**@type {HTMLButtonElement}*/(document.getElementById('btnReloadObjParams'));
const btnReloadManagers = /**@type {HTMLButtonElement}*/(document.getElementById('btnReloadManagers'));
const cbShowManagers = /**@type {HTMLInputElement}*/(document.getElementById('cbShowManagers'));
const cbShowObjParamsNotes = /**@type {HTMLInputElement}*/(document.getElementById('cbShowObjParamsNotes'));
const cbWrapFlex = /**@type {HTMLInputElement}*/(document.getElementById('cbWrapFlex'));
/** @param {string} msg */
function showError(msg) {
elmMsg.textContent = msg;
document.body.classList.add('error');
}
const client = Client({
onClose() {
elmMsg.textContent = `Disconnected from server. Please reload the page.`;
document.body.classList.add('disconnected');
},
onClose: () => showError(`Disconnected from server. Please reload the page.`),
});
const {api} = client;
Object.assign(window, {client, api}); // TODO
/**************** UI definition ****************/
/**
* @param {HTMLTableElement} elm
* @param {HTMLElement|null} elm
*/
function ManagerList(elm) {
if (elm == null) throw new Error('ManagerList not found');
const elmTable = (() => {
const e = elm.querySelector('table');
if (e == null) throw new Error('table should present in ManagerList');
return e;
})();
// TODO put in a json file
const staticVariables = [
{
name: 'gpApplication',
addrs: [0x803E6000, 0x803E9700, 0x803E10C0, 0x803DA8E0],
type: 'TApplication',
},
{
name: 'gpMarDirector',
addrs: [0x8040A2A8, 0x8040E178, 0x80405840, 0x803FF018],
type: 'TMarDirector*',
},
{
name: 'QF',
addrs: [0x8040A2A8, 0x8040E178, 0x80405840, 0x803FF018],
type: 'TMarDirector@QF*',
},
{
name: 'マリオ',
addrs: [0x8040A378, 0x8040E0E8, 0x804057B0, 0x803FEF88],
type: 'TMario*',
},
];
return {
get classList() {
return elm.classList;
},
async reload() {
const [vars, managers] = await Promise.all([
api.getVersion().then(async ver => {
const iver = ['GMSJ01', 'GMSE01', 'GMSP01', 'GMSJ0A'].indexOf(ver);
return await Promise.all(staticVariables.map(async o => {
const ptrlv = o.type.match(/\*+$/)?.[0].length ?? 0;
const type = o.type.substring(0, o.type.length-ptrlv);
const addr0 = o.addrs[iver];
const addr = ptrlv === 0 ? addr0 : await api.readBytes(
[addr0].concat(...Array(ptrlv-1).fill(0)), 4,
).then(dv => dv?.getUint32(0) ?? 0);
return {name: o.name, addr, type};
}));
}),
api.getManagers(),
]);
fieldsViewer.reset();
initTable(elmTable, [
...vars.map(o => makeManageesRowFactory(o)),
...managers.map(makeManagersRowFactory),
]);
},
};
}
const managerList = ManagerList(document.getElementById('managerList'));
/**
* @param {HTMLElement|null} elm
*/
function FieldsViewer(elm) {
if (elm == null) throw new Error('FieldsViewer not found');
const elmTitle = (() => {
const e = elm.querySelector('h3');
if (e == null) throw new Error('h3 should present in FieldsViewer');
return e;
})();
const elmTable = (() => {
const e = elm.querySelector('table');
if (e == null) throw new Error('table should present in FieldsViewer');
return e;
})();
// states
const tdidxVal = 2;
let hAnm = NaN;
let t0 = 0;
@ -68,19 +137,22 @@ document.addEventListener('DOMContentLoaded', async () => {
let target = null;
async function readValues() {
if (target == null) return [];
const values = await api.read([target.addr], target.cls);
const values = await api.read([target.addr], target.type);
return values instanceof Array ? values : [values];
}
/** @param {DOMHighResTimeStamp} t */
async function render(t) {
if (t-t0 >= 33) { // TODO configurable fps
(await readValues())
.forEach((s, i) => elm.rows[i].cells[tdidxVal].textContent = s);
.forEach((s, i) => elmTable.rows[i].cells[tdidxVal].textContent = s);
t0 = t;
}
hAnm = requestAnimationFrame(render);
}
const methods = {
get classList() {
return elm.classList;
},
reload() {
api.reload().then(() => {
target != null && methods.view(target);
@ -88,34 +160,55 @@ document.addEventListener('DOMContentLoaded', async () => {
elmMsg.textContent = err;
});
},
reset() {
elm.classList.add('hidden');
},
/** @param {Manager|Managee} o */
async view(o) {
btnReload.classList.remove('hidden');
cancelAnimationFrame(hAnm);
target = o;
const fields = await api.getFields(o.cls);
elmTitle.textContent = `${o.name} (${o.type}) [${fmt.hex(o.addr)}]`;
const fields = await api.getFields(o.type);
const values = readValues();
initTable(elm, fields.map((r, i) => [
initTable(elmTable, fields.map((r, i) => [
td => td.textContent = r[0],
td => td.textContent = r[1],
td => td.textContent = values[i],
// td => td.textContent = r[2], // TODO
td => td.textContent = r[2],
td => td.textContent = r[3],
td => td.textContent = r[4],
]));
elm.classList.remove('hidden');
if (fields.length) hAnm = requestAnimationFrame(render);
},
}
return methods;
}
const fieldsViewer = FieldsViewer(elmFieldsViewer);
btnReload.addEventListener('click', () => {
const fieldsViewer = FieldsViewer(document.getElementById('fieldsViewer'));
btnReloadObjParams.addEventListener('click', () => {
fieldsViewer.reload();
});
btnReloadManagers.addEventListener('click', async () => {
managerList.reload();
});
cbShowManagers.addEventListener('change', function () {
managerList.classList[this.checked ? 'remove' : 'add']('hidden');
});
cbShowObjParamsNotes.addEventListener('change', function () {
fieldsViewer.classList[this.checked ? 'add' : 'remove']('showNotes');
});
cbWrapFlex.addEventListener('change', function () {
// TODO
document.body.classList[this.checked ? 'add' : 'remove']('wrap');
});
/** @type {(o: Manager) => RowFactory} */
const makeManagersRowFactory = o => [
td => {
// remove old button
const btn0 = td.querySelector('button');
if (btn0) td.removeChild(btn0);
// create new button
const btn = document.createElement('button');
let open = false;
/** @type {HTMLTableRowElement[] | null} */
@ -146,22 +239,26 @@ document.addEventListener('DOMContentLoaded', async () => {
td.appendChild(btn);
},
td => td.textContent = `${o.name} (${o.count})`,
td => td.textContent = `${o.cls}`,
td => td.textContent = `${o.type}`,
td => td.textContent = `${fmt.hex(o.addr)}`,
makeViewerNavigatorFactory(o),
];
/** @type {(o: Managee, i: number) => ((td: HTMLTableCellElement) => void)[]} */
/** @type {(o: Managee, i?: number) => ((td: HTMLTableCellElement) => void)[]} */
const makeManageesRowFactory = (o, i) => [
_ => {},
td => td.textContent = `${i}: (${o.name})`,
td => td.textContent = `${o.cls}`,
td => td.textContent = i == null ? o.name : `${i}: ${o.name}`,
td => td.textContent = `${o.type}`,
td => td.textContent = `${fmt.hex(o.addr)}`,
makeViewerNavigatorFactory(o),
];
/** @type {(o: Manager|Managee) => CellFactory} */
const makeViewerNavigatorFactory = o => td => {
// remove old button
const btn0 = td.querySelector('button');
if (btn0) td.removeChild(btn0);
// create new button
const btn = document.createElement('button');
btn.textContent = '>';
btn.addEventListener('click', () => {
@ -175,11 +272,11 @@ document.addEventListener('DOMContentLoaded', async () => {
await client.connect(); // TODO url
const pid = await api.init();
console.log('pid:', pid);
document.body.classList.add('ready');
const managers = await api.getManagers();
initTable(elmManagers, managers.map(makeManagersRowFactory));
await managerList.reload();
} catch(e) {
elmMsg.textContent = e;
return; // TODO
console.log(e);
showError(e);
}
});