[v0.1.0-beta.1] Implemented ObjectViewer
- load/reload `ObjectParameters/*.json` - get managers, managees - read bytes, struct, string, class name - write bytes
This commit is contained in:
commit
89e808d3fe
48 changed files with 15582 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
target/
|
||||
out/
|
7
CHANGELOG.md
Normal file
7
CHANGELOG.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Changelog
|
||||
## v0.1.0-beta.1 (2023/07/23)
|
||||
- Implemented ObjectViewer
|
||||
- load/reload `ObjectParameters/*.json`
|
||||
- get managers, managees
|
||||
- read bytes, struct, string, class name
|
||||
- write bytes
|
1230
Cargo.lock
generated
Normal file
1230
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
26
Cargo.toml
Normal file
26
Cargo.toml
Normal file
|
@ -0,0 +1,26 @@
|
|||
[package]
|
||||
name = "sup-smsac"
|
||||
version = "0.1.0-beta.1"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/sup39/sup-smsac"
|
||||
|
||||
[dependencies]
|
||||
sup-smsac-derive = { path = "./sup-smsac-derive" }
|
||||
encoding_rs = "0.8.32"
|
||||
windows = {version = "0.48.0", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_System_Diagnostics_ToolHelp",
|
||||
"Win32_System_Memory",
|
||||
]}
|
||||
futures-util = "0.3.28"
|
||||
hyper = { version = "0.14.27", features = ["full"] }
|
||||
hyper-tungstenite = "0.10.0"
|
||||
serde_json = "1.0.103"
|
||||
tokio = { version = "1.29.1", features = ["full"] }
|
||||
tokio-util = "0.7.8"
|
||||
open = "5.0.0"
|
||||
clap = { version = "4.3.16", features = ["derive"] }
|
||||
mime_guess = "2.0.4"
|
||||
urlencoding = "2.1.2"
|
||||
serde = { version = "1.0.174", features = ["rc", "derive"] }
|
4
LICENSE.txt
Normal file
4
LICENSE.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
# SPDX-FileCopyrightText: 2023 sup39 <sms@sup39.dev>
|
||||
# SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
See "www/LICENSE.html" for the full license text.
|
36
README.md
Normal file
36
README.md
Normal file
|
@ -0,0 +1,36 @@
|
|||
# sup-smsac
|
||||
A tool to support Super Mario Sunshine academic research and TAS.
|
||||
It is written in Rust (backend) and JavaScript (frontend), and uses HTTP + WebSocket to communicate between backend and frontend.
|
||||
It only contains a simple Object Viewer at the moment.
|
||||
|
||||
## Usage
|
||||
Download the binary from the [releases page](https://github.com/sup39/sup-smsac/releases). Unzip and double click `sup-smsac.exe`. It should open browser automatically for you. If it doesn't, open browser and navigate to the url shown in the terminal manually.
|
||||
|
||||
## Building from Source (Windows only)
|
||||
Requirements:
|
||||
- [cargo](https://www.rust-lang.org/tools/install)
|
||||
- [Git Bash](https://git-scm.com/download/win)
|
||||
|
||||
```sh
|
||||
# Clone the repository
|
||||
git clone https://github.com/sup39/sup-smsac
|
||||
|
||||
# cd to the directory of the repository
|
||||
cd sup-smsac
|
||||
|
||||
# run the build script
|
||||
sh build.sh
|
||||
|
||||
# the out files will be in "out/sup-smsac-$version"
|
||||
```
|
||||
|
||||
Note that if you are using `cargo run`, you have to pass `-d path/to/repository/directory` as argument to specify the path to the directory of the repository:
|
||||
```
|
||||
# assuming you are in the directory of the repository
|
||||
cargo run -- -d .
|
||||
```
|
||||
|
||||
## TODO
|
||||
- [ ] documentation of the WebSocket API
|
||||
- [ ] add more ObjectParameters files
|
||||
- [ ] UI improvement
|
70
about.hbs
Normal file
70
about.hbs
Normal file
|
@ -0,0 +1,70 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: #333;
|
||||
color: white;
|
||||
}
|
||||
a {
|
||||
color: skyblue;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
font-family: sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.intro {
|
||||
text-align: center;
|
||||
}
|
||||
.licenses-list {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.license-used-by {
|
||||
margin-top: -10px;
|
||||
}
|
||||
.license-text {
|
||||
max-height: 200px;
|
||||
overflow-y: scroll;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="container">
|
||||
<div class="intro">
|
||||
<h1>Third Party Licenses</h1>
|
||||
<p>This page lists the licenses of the projects used in cargo-about.</p>
|
||||
</div>
|
||||
|
||||
<h2>Overview of licenses:</h2>
|
||||
<ul class="licenses-overview">
|
||||
{{#each overview}}
|
||||
<li><a href="#{{id}}">{{name}}</a> ({{count}})</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
|
||||
<h2>All license text:</h2>
|
||||
<ul class="licenses-list">
|
||||
{{#each licenses}}
|
||||
<li class="license">
|
||||
<h3 id="{{id}}">{{name}}</h3>
|
||||
<h4>Used by:</h4>
|
||||
<ul class="license-used-by">
|
||||
{{#each used_by}}
|
||||
<li><a href="{{#if crate.repository}} {{crate.repository}} {{else}} https://crates.io/crates/{{crate.name}} {{/if}}">{{crate.name}} {{crate.version}}</a></li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
<pre class="license-text">{{text}}</pre>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
10
about.toml
Normal file
10
about.toml
Normal file
|
@ -0,0 +1,10 @@
|
|||
accepted = [
|
||||
"Apache-2.0",
|
||||
"MIT",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"Unicode-DFS-2016",
|
||||
]
|
||||
|
||||
ignore-build-dependencies = true
|
||||
ignore-dev-dependencies = true
|
10
build.sh
Normal file
10
build.sh
Normal file
|
@ -0,0 +1,10 @@
|
|||
set -e
|
||||
|
||||
version=$(grep -Po '(?<=version = ").*(?=")' Cargo.toml | head -n1)
|
||||
outDir=out/sup-smsac-$version
|
||||
#rm -rf "$outDir"
|
||||
mkdir -p "$outDir"
|
||||
|
||||
cargo build --release
|
||||
cp ./target/release/sup-smsac.exe "$outDir/"
|
||||
cp -r www res README.md LICENSE.txt CHANGELOG.md "$outDir/"
|
38
res/ObjectParameters/JDrama TActor.json
Normal file
38
res/ObjectParameters/JDrama TActor.json
Normal file
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"JDrama::TActor": {
|
||||
"size": 68,
|
||||
"offsets": [
|
||||
{
|
||||
"offset": "0",
|
||||
"type": "JDrama::TPlacement",
|
||||
"name": "Inherited fields",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "20",
|
||||
"type": "JStage::TActor",
|
||||
"name": "Inherited fields",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "24",
|
||||
"type": "JGeometry::TVec3<float>",
|
||||
"name": "* Scale",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "30",
|
||||
"type": "JGeometry::TVec3<float>",
|
||||
"name": "* Rotation",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "40",
|
||||
"type": "JDrama::TLightMap*",
|
||||
"name": "",
|
||||
"notes": "",
|
||||
"hidden": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
26
res/ObjectParameters/JDrama TNameRef.json
Normal file
26
res/ObjectParameters/JDrama TNameRef.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"JDrama::TNameRef": {
|
||||
"size": 10,
|
||||
"offsets": [
|
||||
{
|
||||
"offset": "0",
|
||||
"type": "void*",
|
||||
"name": "vtable",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "4",
|
||||
"type": "string",
|
||||
"name": "Name",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "8",
|
||||
"type": "s16",
|
||||
"name": "Key code",
|
||||
"notes": "",
|
||||
"hidden": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
26
res/ObjectParameters/JDrama TPlacement.json
Normal file
26
res/ObjectParameters/JDrama TPlacement.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"JDrama::TPlacement": {
|
||||
"size": 32,
|
||||
"offsets": [
|
||||
{
|
||||
"offset": "0",
|
||||
"type": "JDrama::TViewObj",
|
||||
"name": "Inherited fields",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "10",
|
||||
"type": "JGeometry::TVec3<float>",
|
||||
"name": "* Position",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "1c",
|
||||
"type": "s16",
|
||||
"name": "",
|
||||
"notes": "",
|
||||
"hidden": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
20
res/ObjectParameters/JDrama TViewObj.json
Normal file
20
res/ObjectParameters/JDrama TViewObj.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"JDrama::TViewObj": {
|
||||
"size": 16,
|
||||
"offsets": [
|
||||
{
|
||||
"offset": "0",
|
||||
"type": "JDrama::TNameRef",
|
||||
"name": "Inherited fields",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "c",
|
||||
"type": "u16",
|
||||
"name": "Flags",
|
||||
"notes": "",
|
||||
"hidden": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
25
res/ObjectParameters/JGeometry TVec3(float).json
Normal file
25
res/ObjectParameters/JGeometry TVec3(float).json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"JGeometry::TVec3<float>": {
|
||||
"size": 12,
|
||||
"offsets": [
|
||||
{
|
||||
"offset": "0",
|
||||
"type": "float",
|
||||
"name": "X",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "4",
|
||||
"type": "float",
|
||||
"name": "Y",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "8",
|
||||
"type": "float",
|
||||
"name": "Z",
|
||||
"notes": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
73
res/ObjectParameters/TBossEel.json
Normal file
73
res/ObjectParameters/TBossEel.json
Normal file
|
@ -0,0 +1,73 @@
|
|||
{
|
||||
"TBossEel": {
|
||||
"size": 544,
|
||||
"offsets": [
|
||||
{
|
||||
"offset": "0",
|
||||
"type": "TSpineEnemy",
|
||||
"name": "Inherited fields",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": ["16c", "70"],
|
||||
"type": "s32",
|
||||
"name": "tooth[0].hp",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": ["170", "70"],
|
||||
"type": "s32",
|
||||
"name": "tooth[1].hp",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": ["174", "70"],
|
||||
"type": "s32",
|
||||
"name": "tooth[2].hp",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": ["178", "70"],
|
||||
"type": "s32",
|
||||
"name": "tooth[3].hp",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": ["17c", "70"],
|
||||
"type": "s32",
|
||||
"name": "tooth[4].hp",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": ["180", "70"],
|
||||
"type": "s32",
|
||||
"name": "tooth[5].hp",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": ["184", "70"],
|
||||
"type": "s32",
|
||||
"name": "tooth[6].hp",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": ["188", "70"],
|
||||
"type": "s32",
|
||||
"name": "tooth[7].hp",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "1fc",
|
||||
"type": "u8",
|
||||
"name": "Water hit tooth flag",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "1fd",
|
||||
"type": "u8",
|
||||
"name": "Molar washed flag",
|
||||
"notes": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
75
res/ObjectParameters/THitActor.json
Normal file
75
res/ObjectParameters/THitActor.json
Normal file
|
@ -0,0 +1,75 @@
|
|||
{
|
||||
"THitActor": {
|
||||
"size": 104,
|
||||
"offsets": [
|
||||
{
|
||||
"offset": "0",
|
||||
"type": "JDrama::TActor",
|
||||
"name": "Inherited fields",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "44",
|
||||
"type": "THitActor**",
|
||||
"name": "Collided object array",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "48",
|
||||
"type": "s16",
|
||||
"name": "Collided object count",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "4a",
|
||||
"type": "s16",
|
||||
"name": "Collided object capacity",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "4c",
|
||||
"type": "u32",
|
||||
"format": "hex",
|
||||
"name": "Actor type",
|
||||
"notes": "& 0x8000_0000: player\n& 0x1000_0000: enemy\n& 0x0800_0000: boss"
|
||||
},
|
||||
{
|
||||
"offset": "50",
|
||||
"type": "float",
|
||||
"name": "Attacking hitbox radius",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "54",
|
||||
"type": "float",
|
||||
"name": "Attacking hitbox height",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "58",
|
||||
"type": "float",
|
||||
"name": "Receiving hitbox radius",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "5c",
|
||||
"type": "float",
|
||||
"name": "Receiving hitbox height",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "60",
|
||||
"type": "float",
|
||||
"name": "Entry radius",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "64",
|
||||
"type": "u32",
|
||||
"format": "hex",
|
||||
"name": "Collision flag\n (0) active / (1) no collision",
|
||||
"notes": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
138
res/ObjectParameters/TLiveActor.json
Normal file
138
res/ObjectParameters/TLiveActor.json
Normal file
|
@ -0,0 +1,138 @@
|
|||
{
|
||||
"TLiveActor": {
|
||||
"size": 244,
|
||||
"offsets": [
|
||||
{
|
||||
"offset": "0",
|
||||
"type": "TTakeActor",
|
||||
"name": "Inherited fields",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "70",
|
||||
"type": "TLiveManager*",
|
||||
"name": "Manager",
|
||||
"notes": "",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"offset": "74",
|
||||
"type": "MActor*",
|
||||
"name": "",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": ["74", "c", "0"],
|
||||
"type": "s32",
|
||||
"name": "Animation id",
|
||||
"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": "7c",
|
||||
"type": "s16",
|
||||
"name": "Index in manager?",
|
||||
"notes": "",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"offset": "80",
|
||||
"type": "MAnimSound*",
|
||||
"name": "",
|
||||
"notes": "",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"offset": "84",
|
||||
"type": "JKRFileLoader*",
|
||||
"name": "",
|
||||
"notes": "",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"offset": "8c",
|
||||
"type": "TSpineBase<TLiveActor>*",
|
||||
"name": "AI",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": ["8c", "1c", "0"],
|
||||
"type": "void*",
|
||||
"name": "Previous nerve",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": ["8c", "14", "0"],
|
||||
"type": "void*",
|
||||
"name": "Current nerve",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": ["8c", "20"],
|
||||
"type": "s32",
|
||||
"name": "Nerve timer",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "94",
|
||||
"type": "JGeometry::TVec3<float>",
|
||||
"name": "* Movement (unit/step)",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "ac",
|
||||
"type": "JGeometry::TVec3<float>",
|
||||
"name": "* Speed (unit/step)",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "bc",
|
||||
"type": "float",
|
||||
"name": "Wall hitbox width",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "c4",
|
||||
"type": "TBGCheckData*",
|
||||
"name": "",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "c8",
|
||||
"type": "float",
|
||||
"name": "Ground Height",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "cc",
|
||||
"type": "float",
|
||||
"name": "Gravity (unit/step²)",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "f0",
|
||||
"type": "u32",
|
||||
"format": "hex",
|
||||
"name": "Flag",
|
||||
"notes": "& 0x0001: active(0) / inactive(1)\n& 0x0010: this.bind() enabled(0) / disabled(1)\n& 0x0080: is airborne(1): next position > height of ground below + 0.05\n& 0x1000: checkGround(0) / checkGroundIgnoreWaterSurface(1)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
20
res/ObjectParameters/TSpineEnemy.json
Normal file
20
res/ObjectParameters/TSpineEnemy.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"TSpineEnemy": {
|
||||
"size": 336,
|
||||
"offsets": [
|
||||
{
|
||||
"offset": "0",
|
||||
"type": "TLiveActor",
|
||||
"name": "Inherited fields",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "124",
|
||||
"type": "TGraphTracer*",
|
||||
"name": "Movement graph",
|
||||
"notes": "",
|
||||
"hidden": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
25
res/ObjectParameters/TTakeActor.json
Normal file
25
res/ObjectParameters/TTakeActor.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"TTakeActor": {
|
||||
"size": 112,
|
||||
"offsets": [
|
||||
{
|
||||
"offset": "0",
|
||||
"type": "THitActor",
|
||||
"name": "Inherited fields",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "68",
|
||||
"type": "TTakeActor*",
|
||||
"name": "Holder",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"offset": "6c",
|
||||
"type": "TTakeActor*",
|
||||
"name": "Held object",
|
||||
"notes": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
13
res/ObjectParameters/_default.json
Normal file
13
res/ObjectParameters/_default.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"_default": {
|
||||
"size": -1,
|
||||
"offsets": [
|
||||
{
|
||||
"offset": "0",
|
||||
"type": "TSpineEnemy",
|
||||
"name": "",
|
||||
"notes": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
81
src/addr.rs
Normal file
81
src/addr.rs
Normal file
|
@ -0,0 +1,81 @@
|
|||
/// SPDX-FileCopyrightText: 2023 sup39 <sms@sup39.dev>
|
||||
/// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use crate::big_endian::DecodeBE;
|
||||
use sup_smsac_derive::DecodeBE;
|
||||
|
||||
#[derive(DecodeBE, Clone, Copy, PartialEq, PartialOrd)]
|
||||
pub struct Addr(pub u32);
|
||||
|
||||
impl From<Addr> for u32 {
|
||||
fn from(x: Addr) -> u32 {
|
||||
x.0
|
||||
}
|
||||
}
|
||||
impl From<u32> for Addr {
|
||||
fn from(x: u32) -> Self {
|
||||
Self(x)
|
||||
}
|
||||
}
|
||||
|
||||
impl Addr {
|
||||
pub fn add(&self, rhs: u32) -> Addr {
|
||||
Addr(self.0 + rhs)
|
||||
}
|
||||
pub fn offset(&self, by: i32) -> Addr {
|
||||
Addr(self.0 + by as u32)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add<u32> for Addr {
|
||||
type Output = Addr;
|
||||
fn add(self, other: u32) -> Addr {
|
||||
Addr(self.0+other)
|
||||
}
|
||||
}
|
||||
impl std::ops::Sub<u32> for Addr {
|
||||
type Output = Addr;
|
||||
fn sub(self, other: u32) -> Addr {
|
||||
Addr(self.0-other)
|
||||
}
|
||||
}
|
||||
impl std::ops::Sub<Addr> for Addr {
|
||||
type Output = isize;
|
||||
fn sub(self, other: Addr) -> isize {
|
||||
(self.0 - other.0) as i32 as isize
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for Addr {
|
||||
fn fmt(&self, f:&mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:08X}", self.0)
|
||||
}
|
||||
}
|
||||
impl std::fmt::Debug for Addr {
|
||||
fn fmt(&self, f:&mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:08X}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AddrOffsets<T=u32>(pub T, pub Box<[T]>);
|
||||
impl std::ops::Add<&AddrOffsets> for &AddrOffsets {
|
||||
type Output = AddrOffsets;
|
||||
fn add(self, other: &AddrOffsets) -> AddrOffsets {
|
||||
match self.1.split_last() {
|
||||
Some((last, init)) => AddrOffsets(
|
||||
self.0,
|
||||
[init, &[last+other.0], &other.1].concat().into(),
|
||||
),
|
||||
None => AddrOffsets(self.0+other.0, other.1.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for AddrOffsets {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
write!(fmt, "{:X}", self.0)?;
|
||||
for off in self.1.iter() {
|
||||
write!(fmt, ",{:X}", off)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
53
src/big_endian.rs
Normal file
53
src/big_endian.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
/// SPDX-FileCopyrightText: 2023 sup39 <sms@sup39.dev>
|
||||
/// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
pub trait DecodeBE {
|
||||
const PACKED_SIZE: usize;
|
||||
/// # Safety
|
||||
///
|
||||
/// [`ptr`, `ptr+size_of(Self)`) must be valid
|
||||
unsafe fn decode_be(ptr: *const u8) -> Self;
|
||||
}
|
||||
|
||||
macro_rules! impl_decode_be_for_int {
|
||||
($type:ident, $size:literal) => {
|
||||
impl DecodeBE for $type {
|
||||
const PACKED_SIZE: usize = $size;
|
||||
#[inline]
|
||||
unsafe fn decode_be(ptr: *const u8) -> Self {
|
||||
$type::to_be(*(ptr as *const $type))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
macro_rules! impl_decode_be_for_float {
|
||||
($ftype:ident, $itype:ident, $size:literal) => {
|
||||
impl DecodeBE for $ftype {
|
||||
const PACKED_SIZE: usize = $size;
|
||||
#[inline]
|
||||
unsafe fn decode_be(ptr: *const u8) -> Self {
|
||||
$ftype::from_bits($itype::to_be(*(ptr as *const $itype)))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_decode_be_for_int!(u8, 1);
|
||||
impl_decode_be_for_int!(i8, 1);
|
||||
impl_decode_be_for_int!(u16, 2);
|
||||
impl_decode_be_for_int!(i16, 2);
|
||||
impl_decode_be_for_int!(u32, 4);
|
||||
impl_decode_be_for_int!(i32, 4);
|
||||
impl_decode_be_for_int!(u64, 8);
|
||||
impl_decode_be_for_int!(i64, 8);
|
||||
impl_decode_be_for_int!(u128, 16);
|
||||
impl_decode_be_for_int!(i128, 16);
|
||||
impl_decode_be_for_float!(f32, u32, 4);
|
||||
impl_decode_be_for_float!(f64, u64, 8);
|
||||
|
||||
impl<const N: usize> DecodeBE for &[u8; N] {
|
||||
const PACKED_SIZE: usize = N;
|
||||
unsafe fn decode_be(ptr: *const u8) -> Self {
|
||||
&*(ptr as *const [u8; N])
|
||||
}
|
||||
}
|
94
src/dolphin.rs
Normal file
94
src/dolphin.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
/// SPDX-FileCopyrightText: 2023 sup39 <sms@sup39.dev>
|
||||
/// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use crate::sys::process::PidType;
|
||||
use crate::sys::shared_memory::{SharedMemory, SharedMemoryOpenError};
|
||||
use crate::addr::Addr;
|
||||
use crate::big_endian::DecodeBE;
|
||||
use crate::sys::process::{Process32Iterator, ProcessInfo};
|
||||
use encoding_rs::SHIFT_JIS;
|
||||
|
||||
pub const MEM1_START_ADDR: Addr = Addr(0x8000_0000);
|
||||
pub const MEM1_END_ADDR: Addr = Addr(0x8180_0000);
|
||||
pub trait Dolphin {
|
||||
/// # Safety
|
||||
///
|
||||
/// The offset must be smaller than the size of the memory region
|
||||
unsafe fn mem<T: Into<isize>>(&self, offset: T) -> *mut u8;
|
||||
|
||||
fn get_ptr_mut(&self, addr: Addr, size: usize) -> Option<*mut u8> {
|
||||
if MEM1_START_ADDR <= addr && addr < MEM1_END_ADDR - size as u32 {
|
||||
Some(unsafe {self.mem(addr - MEM1_START_ADDR)})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_bytes(&self, addr: Addr, size: usize) -> Option<&[u8]> {
|
||||
self.get_ptr_mut(addr, size)
|
||||
.map(|ptr| unsafe {std::slice::from_raw_parts(ptr, size)})
|
||||
}
|
||||
#[inline]
|
||||
fn read<T: DecodeBE>(&self, addr: Addr) -> Option<T> {
|
||||
let size = std::mem::size_of::<T>();
|
||||
self.get_ptr_mut(addr, size)
|
||||
.map(|ptr| unsafe {T::decode_be(ptr)})
|
||||
}
|
||||
fn read_str(&self, addr: Addr) -> Option<String> {
|
||||
if MEM1_START_ADDR <= addr && addr < MEM1_END_ADDR {
|
||||
let ptr = unsafe {self.mem(addr - MEM1_START_ADDR)};
|
||||
const MAX_LENGTH: u32 = 256; // TODO
|
||||
let max_length = MAX_LENGTH; // TODO
|
||||
let maxlen = std::cmp::min(max_length as usize, (MEM1_END_ADDR - addr) as usize);
|
||||
// let maxlen = (MEM1_END_ADDR - addr) as usize;
|
||||
let mut len = 0usize;
|
||||
while len < maxlen {
|
||||
if unsafe{*ptr.add(len) == 0} {break;}
|
||||
len += 1;
|
||||
}
|
||||
SHIFT_JIS.decode_without_bom_handling_and_without_replacement(unsafe{std::slice::from_raw_parts(ptr, len)}).map(|x| x.into_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn write_bytes(&self, addr: Addr, payload: &[u8]) -> Option<()> {
|
||||
let size = payload.len();
|
||||
self.get_ptr_mut(addr, size)
|
||||
.map(|ptr| unsafe {std::ptr::copy(payload.as_ptr(), ptr, size)})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DolphinMemory {
|
||||
shared_memory: SharedMemory,
|
||||
pid: PidType,
|
||||
}
|
||||
|
||||
impl Dolphin for DolphinMemory {
|
||||
#[inline]
|
||||
unsafe fn mem<T: Into<isize>>(&self, offset: T) -> *mut u8 {
|
||||
self.shared_memory.get_ptr().offset(offset.into())
|
||||
}
|
||||
}
|
||||
impl DolphinMemory {
|
||||
#[inline]
|
||||
pub fn pid(&self) -> PidType {
|
||||
self.pid
|
||||
}
|
||||
|
||||
pub fn open_pid<T: Into<PidType>>(pid: T) -> Result<DolphinMemory, SharedMemoryOpenError> {
|
||||
let pid: usize = pid.into();
|
||||
let shared_memory = SharedMemory::open(&format!("dolphin-emu.{}", pid))?;
|
||||
Ok(DolphinMemory {shared_memory, pid})
|
||||
}
|
||||
pub fn list_dolphin() -> impl Iterator<Item = (usize, Result<DolphinMemory, SharedMemoryOpenError>)> {
|
||||
Process32Iterator::new().filter_map(|p| p.get_name().to_str().and_then(|name|
|
||||
match name {
|
||||
"Dolphin.exe" => Some((p.pid(), DolphinMemory::open_pid(p.pid()))),
|
||||
_ => None,
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
63
src/main.rs
Normal file
63
src/main.rs
Normal file
|
@ -0,0 +1,63 @@
|
|||
/// SPDX-FileCopyrightText: 2023 sup39 <sms@sup39.dev>
|
||||
/// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
pub mod dolphin;
|
||||
pub mod addr;
|
||||
pub mod big_endian;
|
||||
pub mod sys;
|
||||
pub mod sms;
|
||||
pub mod server;
|
||||
pub mod obj_params;
|
||||
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::path::PathBuf;
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Args {
|
||||
#[arg(long, default_value_t = IpAddr::V4(std::net::Ipv4Addr::LOCALHOST))]
|
||||
host: IpAddr,
|
||||
|
||||
#[arg(short='p', long, default_value_t = 35353)]
|
||||
port: u16,
|
||||
|
||||
#[arg(long)]
|
||||
no_browser: bool,
|
||||
|
||||
#[arg(short='d', long)]
|
||||
root_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let args = Args::parse();
|
||||
|
||||
let listener = {
|
||||
let mut sock_addr = SocketAddr::new(args.host, args.port);
|
||||
match tokio::net::TcpListener::bind(&sock_addr).await {
|
||||
Ok(listener) => {
|
||||
listener
|
||||
},
|
||||
Err(err) => {
|
||||
println!("Failed to listen on {sock_addr}: {err}\nTrying other port...\n");
|
||||
sock_addr.set_port(0);
|
||||
tokio::net::TcpListener::bind(&sock_addr).await.unwrap()
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
let url = format!("http://{}", listener.local_addr().unwrap());
|
||||
println!("Listening on {url}");
|
||||
if !args.no_browser {
|
||||
let _ = open::that(url);
|
||||
}
|
||||
|
||||
let root_dir = args.root_dir
|
||||
.unwrap_or_else(|| {
|
||||
let mut path = std::env::current_exe().unwrap();
|
||||
path.pop();
|
||||
path
|
||||
}).canonicalize().unwrap().into_boxed_path();
|
||||
|
||||
server::http::serve(listener, root_dir).await.unwrap();
|
||||
}
|
76
src/obj_params/field_reader.rs
Normal file
76
src/obj_params/field_reader.rs
Normal file
|
@ -0,0 +1,76 @@
|
|||
use crate::{
|
||||
addr::Addr,
|
||||
big_endian::DecodeBE,
|
||||
dolphin::Dolphin,
|
||||
sms::SMSDolphin,
|
||||
};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
pub trait FieldReader<D: Dolphin, T> {
|
||||
fn read(&self, d: &D, addr: Addr) -> Option<T>;
|
||||
}
|
||||
|
||||
pub struct PrimitiveFieldReader<T> {
|
||||
phantom: PhantomData<T>,
|
||||
}
|
||||
impl<T> PrimitiveFieldReader<T> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<D: Dolphin, T: DecodeBE> FieldReader<D, T> for PrimitiveFieldReader<T> {
|
||||
fn read(&self, d: &D, addr: Addr) -> Option<T> {
|
||||
d.read::<T>(addr)
|
||||
}
|
||||
}
|
||||
impl<D: Dolphin, T: DecodeBE + ToString> FieldReader<D, String> for PrimitiveFieldReader<T> {
|
||||
fn read(&self, d: &D, addr: Addr) -> Option<String> {
|
||||
d.read::<T>(addr).map(|x| x.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct F32FieldReader;
|
||||
impl<D: Dolphin> FieldReader<D, f32> for F32FieldReader {
|
||||
fn read(&self, d: &D, addr: Addr) -> Option<f32> {
|
||||
d.read::<f32>(addr)
|
||||
}
|
||||
}
|
||||
impl<D: Dolphin> FieldReader<D, String> for F32FieldReader {
|
||||
fn read(&self, d: &D, addr: Addr) -> Option<String> {
|
||||
d.read::<f32>(addr).map(|x| match x.abs() {
|
||||
m if (1e-4..1e8).contains(&m) || m == 0f32 => {
|
||||
let s = format!("{x}");
|
||||
match m < 8388608f32 && s.contains('.') {
|
||||
true => s,
|
||||
false => s+".0",
|
||||
}
|
||||
},
|
||||
_ => format!("{x:e}"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StringFieldReader;
|
||||
impl<D: Dolphin> FieldReader<D, String> for StringFieldReader {
|
||||
fn read(&self, d: &D, addr: Addr) -> Option<String> {
|
||||
d.read::<Addr>(addr).and_then(|a| d.read_str(a))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClassNameReader;
|
||||
impl FieldReader<SMSDolphin, String> for ClassNameReader {
|
||||
fn read(&self, d: &SMSDolphin, addr: Addr) -> Option<String> {
|
||||
d.read::<Addr>(addr)
|
||||
.map(|addr| d.get_class_string(addr))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HexFieldReader(pub usize);
|
||||
impl<D: Dolphin> FieldReader<D, String> for HexFieldReader {
|
||||
fn read(&self, d: &D, addr: Addr) -> Option<String> {
|
||||
d.read_bytes(addr, self.0)
|
||||
.map(|bytes| bytes.iter().map(|x| format!("{x:02X}")).collect())
|
||||
}
|
||||
}
|
286
src/obj_params/mod.rs
Normal file
286
src/obj_params/mod.rs
Normal file
|
@ -0,0 +1,286 @@
|
|||
/// SPDX-FileCopyrightText: 2023 sup39 <sms@sup39.dev>
|
||||
/// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use std::fs::{read_dir, File};
|
||||
use std::io::BufReader;
|
||||
use std::sync::Arc;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use serde_json;
|
||||
use serde::{Deserialize, Deserializer, de::{self, Visitor}};
|
||||
use crate::{
|
||||
addr::{Addr, AddrOffsets},
|
||||
big_endian::DecodeBE,
|
||||
dolphin::Dolphin,
|
||||
sms::SMSDolphin,
|
||||
};
|
||||
|
||||
mod field_reader;
|
||||
use field_reader::*;
|
||||
|
||||
/**** original json ****/
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ObjParamsJson {
|
||||
offsets: Box<[ObjParamsOffsetEntry]>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ObjParamsOffsetEntry {
|
||||
#[serde(deserialize_with = "deserialize_obj_params_offset_entry")]
|
||||
offset: AddrOffsets,
|
||||
#[serde(rename = "type")]
|
||||
type_: Arc<str>,
|
||||
name: Arc<str>,
|
||||
notes: Arc<str>,
|
||||
#[serde(
|
||||
default = "ObjParamsOffsetEntryFormat::none",
|
||||
deserialize_with = "deserialize_obj_params_offset_entry_format",
|
||||
)]
|
||||
format: Option<ObjParamsOffsetEntryFormat>,
|
||||
hidden: Option<bool>,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
enum ObjParamsOffsetEntryFormat {
|
||||
Hex,
|
||||
}
|
||||
impl ObjParamsOffsetEntryFormat {
|
||||
#[inline]
|
||||
fn none() -> Option<ObjParamsOffsetEntryFormat> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ObjParamsOffsetEntryFormat {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
match self {
|
||||
Self::Hex => write!(fmt, "hex"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_obj_params_offset_entry<'de, D>(deserializer: D) -> Result<AddrOffsets, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct ValueVisitor;
|
||||
impl<'de> Visitor<'de> for ValueVisitor {
|
||||
type Value = AddrOffsets;
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a hex string or a non-empty array of hex string")
|
||||
}
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
u32::from_str_radix(value, 16)
|
||||
.map_err(|e| E::custom(e))
|
||||
.map(|x| AddrOffsets(x, Box::from([])))
|
||||
}
|
||||
fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error>
|
||||
where
|
||||
S: de::SeqAccess<'de>,
|
||||
{
|
||||
let mut arr = match seq.size_hint() {
|
||||
Some(size) => Vec::<u32>::with_capacity(size),
|
||||
None => Vec::<u32>::new(),
|
||||
};
|
||||
while let Some(value) = seq.next_element::<Cow<'de, str>>()? {
|
||||
arr.push(u32::from_str_radix(&value, 16).map_err(de::Error::custom)?);
|
||||
}
|
||||
arr.split_first()
|
||||
.map(|p| AddrOffsets(*p.0, p.1.into()))
|
||||
.ok_or_else(|| de::Error::custom("Offset array must not be empty"))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(ValueVisitor)
|
||||
}
|
||||
|
||||
fn deserialize_obj_params_offset_entry_format<'de, D>(deserializer: D)
|
||||
-> Result<Option<ObjParamsOffsetEntryFormat>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct ValueVisitor;
|
||||
impl<'de> Visitor<'de> for ValueVisitor {
|
||||
type Value = Option<ObjParamsOffsetEntryFormat>;
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("\"hex\" or omitted")
|
||||
}
|
||||
fn visit_none<E>(self) -> Result<Self::Value, E> {
|
||||
Ok(None)
|
||||
}
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
match value {
|
||||
"hex" => Ok(Some(ObjParamsOffsetEntryFormat::Hex)),
|
||||
_ => Err(E::unknown_variant(value, &["hex"])),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(ValueVisitor)
|
||||
}
|
||||
|
||||
/**** parsed ****/
|
||||
pub enum ObjectType<D: Dolphin> {
|
||||
Primitive(ClassFieldReader<D>),
|
||||
Class(Box<[ClassField<D>]>),
|
||||
}
|
||||
impl<D: Dolphin> ObjectType<D> {
|
||||
fn new_primitive<T: DecodeBE + ToString + Send + Sync + 'static>() -> Self {
|
||||
ObjectType::<D>::Primitive(Arc::new(PrimitiveFieldReader::<T>::new()))
|
||||
}
|
||||
}
|
||||
|
||||
type ClassFieldReader<D> = Arc<dyn FieldReader<D, String> + Send + Sync>;
|
||||
pub struct ClassField<D: Dolphin> {
|
||||
pub offset: AddrOffsets,
|
||||
pub type_: Arc<str>,
|
||||
pub name: Arc<str>,
|
||||
pub notes: Arc<str>,
|
||||
pub class: Arc<str>,
|
||||
pub reader: ClassFieldReader<D>,
|
||||
}
|
||||
impl<D: Dolphin> FieldReader<D, String> for ClassField<D> {
|
||||
#[inline]
|
||||
fn read(&self, d: &D, addr: Addr) -> Option<String> {
|
||||
self.reader.read(d, addr)
|
||||
}
|
||||
}
|
||||
|
||||
pub type ObjParams<D> = HashMap<Arc<str>, ObjectType<D>>;
|
||||
pub type ObjParamsLoadResult<D> = Result<ObjParams<D>, std::io::Error>;
|
||||
pub fn load_obj_params(dir: &Path) -> ObjParamsLoadResult<SMSDolphin> {
|
||||
type D = SMSDolphin; // TODO
|
||||
let entry_reader = read_dir(dir)?;
|
||||
let mut db_raw = HashMap::<Arc<str>, Arc<ObjParamsJson>>::new();
|
||||
entry_reader.for_each(|entry| {
|
||||
let Ok(entry) = entry.map_err(|e| eprintln!("Fail to get entry: {e}")) else {return};
|
||||
let path = entry.path();
|
||||
if Some(true) != path.extension().map(|e| e == "json") {return}
|
||||
let Ok(file) = File::open(&path)
|
||||
.map_err(|e| eprintln!("Fail to open file \"{}\": {e}", path.to_string_lossy())) else {return};
|
||||
let reader = BufReader::new(file);
|
||||
let Ok(o) = serde_json::from_reader::<_, HashMap<Arc<str>, Arc<ObjParamsJson>>>(reader)
|
||||
.map_err(|e| eprintln!("Fail to parse {}: {e}", path.to_string_lossy())) else {return};
|
||||
for e in o {
|
||||
db_raw.insert(e.0.clone(), e.1.clone());
|
||||
}
|
||||
});
|
||||
|
||||
struct Env<'a, D: Dolphin> {
|
||||
db_raw: &'a HashMap::<Arc<str>, Arc<ObjParamsJson>>,
|
||||
db_types: HashMap::<Arc<str>, ObjectType<D>>,
|
||||
db_formatted: HashMap::<(&'a str, ObjParamsOffsetEntryFormat), ClassFieldReader<D>>,
|
||||
reader_unk: ClassFieldReader<D>,
|
||||
type_addr: ObjectType<D>,
|
||||
}
|
||||
fn resolve_type<'a, D: Dolphin>(env: &'a mut Env<D>, type_: Arc<str>) -> &'a ObjectType<D> {
|
||||
if !env.db_types.contains_key(&type_) {
|
||||
if type_.ends_with('*') {
|
||||
return &env.type_addr;
|
||||
}
|
||||
let new_type = match env.db_raw.get(&type_) {
|
||||
Some(o) => {
|
||||
let mut class_fields = Vec::<ClassField<D>>::new();
|
||||
for field in o.offsets.iter() {
|
||||
// skip hidden fields
|
||||
if let Some(true) = field.hidden {continue}
|
||||
// format
|
||||
if let Some(format) = field.format {
|
||||
if let Some(reader) = env.db_formatted.get(&(&field.type_, format)) {
|
||||
class_fields.push(
|
||||
ClassField {
|
||||
reader: reader.clone(),
|
||||
offset: field.offset.clone(),
|
||||
name: field.name.clone(),
|
||||
notes: field.notes.clone(),
|
||||
type_: field.type_.clone(),
|
||||
class: type_.clone(),
|
||||
},
|
||||
);
|
||||
continue;
|
||||
} else {
|
||||
eprintln!("format \"{format}\" cannot be used for type \"{}\" (in class \"{type_}\")", field.type_);
|
||||
}
|
||||
}
|
||||
// resolve
|
||||
match resolve_type(env, field.type_.clone()) {
|
||||
ObjectType::<D>::Primitive(reader) => class_fields.push(
|
||||
ClassField {
|
||||
reader: reader.clone(),
|
||||
offset: field.offset.clone(),
|
||||
name: field.name.clone(),
|
||||
notes: field.notes.clone(),
|
||||
type_: field.type_.clone(),
|
||||
class: type_.clone(),
|
||||
},
|
||||
),
|
||||
ObjectType::<D>::Class(subfields) => {
|
||||
let is_name_template = field.name.contains('*');
|
||||
for subfield in subfields.iter() {
|
||||
class_fields.push(ClassField {
|
||||
reader: subfield.reader.clone(),
|
||||
offset: &field.offset + &subfield.offset,
|
||||
name: match is_name_template {
|
||||
true => Arc::from(field.name.replace('*', &subfield.name)),
|
||||
false => subfield.name.clone(),
|
||||
},
|
||||
notes: subfield.notes.clone(),
|
||||
type_: subfield.type_.clone(),
|
||||
class: subfield.class.clone(),
|
||||
})
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
ObjectType::<D>::Class(class_fields.into())
|
||||
},
|
||||
None => {
|
||||
ObjectType::<D>::Primitive(env.reader_unk.clone())
|
||||
},
|
||||
};
|
||||
env.db_types.insert(type_.clone(), new_type);
|
||||
}
|
||||
env.db_types.get(&type_).unwrap()
|
||||
}
|
||||
|
||||
let mut env = Env {
|
||||
db_raw: &db_raw,
|
||||
db_types: HashMap::<Arc<str>, ObjectType<D>>::from([
|
||||
(Arc::from("u8"), ObjectType::<D>::new_primitive::<u8>()),
|
||||
(Arc::from("u16"), ObjectType::<D>::new_primitive::<u16>()),
|
||||
(Arc::from("u32"), ObjectType::<D>::new_primitive::<u32>()),
|
||||
(Arc::from("s8"), ObjectType::<D>::new_primitive::<i8>()),
|
||||
(Arc::from("s16"), ObjectType::<D>::new_primitive::<i16>()),
|
||||
(Arc::from("s32"), ObjectType::<D>::new_primitive::<i32>()),
|
||||
(Arc::from("float"), ObjectType::<D>::Primitive(Arc::new(F32FieldReader))),
|
||||
(Arc::from("string"), ObjectType::<D>::Primitive(Arc::new(StringFieldReader))),
|
||||
(Arc::from("void*"), ObjectType::<D>::Primitive(Arc::new(ClassNameReader))),
|
||||
]),
|
||||
db_formatted: HashMap::from([
|
||||
(
|
||||
("u8", ObjParamsOffsetEntryFormat::Hex),
|
||||
Arc::new(HexFieldReader(1)) as ClassFieldReader<D>,
|
||||
),
|
||||
(
|
||||
("u16", ObjParamsOffsetEntryFormat::Hex),
|
||||
Arc::new(HexFieldReader(2)) as ClassFieldReader<D>,
|
||||
),
|
||||
(
|
||||
("u32", ObjParamsOffsetEntryFormat::Hex),
|
||||
Arc::new(HexFieldReader(4)) as ClassFieldReader<D>,
|
||||
),
|
||||
]),
|
||||
reader_unk: Arc::new(PrimitiveFieldReader::<Addr>::new()), // TODO
|
||||
type_addr: ObjectType::<D>::new_primitive::<Addr>(),
|
||||
};
|
||||
for type_ in db_raw.keys() {
|
||||
resolve_type(&mut env, type_.clone());
|
||||
}
|
||||
Ok(env.db_types)
|
||||
}
|
280
src/server/api.rs
Normal file
280
src/server/api.rs
Normal file
|
@ -0,0 +1,280 @@
|
|||
/// SPDX-FileCopyrightText: 2023 sup39 <sms@sup39.dev>
|
||||
/// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use crate::{
|
||||
addr::{Addr, AddrOffsets},
|
||||
dolphin::Dolphin,
|
||||
sms::{SMSDolphin, SMSVersion},
|
||||
big_endian::DecodeBE,
|
||||
server::http::HttpEnv,
|
||||
obj_params::{load_obj_params, ObjectType},
|
||||
};
|
||||
use sup_smsac_derive::DecodeBE;
|
||||
use serde_json::{self, json, Value as JsonValue};
|
||||
|
||||
#[derive(Debug, DecodeBE)]
|
||||
struct ConductorNode {
|
||||
next: Addr,
|
||||
_prev: Addr,
|
||||
obj: Addr,
|
||||
}
|
||||
#[derive(Debug, DecodeBE)]
|
||||
struct ChildInfo {
|
||||
count: u32,
|
||||
addr: Addr,
|
||||
}
|
||||
|
||||
trait DolphinMemoryJsExt {
|
||||
fn resolve_addr(&self, addr: &JsonValue) -> Result<Option<Addr>, ()>;
|
||||
fn resolve_addr_offsets(&self, base: Addr, offsets: &AddrOffsets) -> Option<Addr>;
|
||||
}
|
||||
impl<T: Dolphin> DolphinMemoryJsExt for T {
|
||||
fn resolve_addr(&self, addr: &JsonValue) -> Result<Option<Addr>, ()> {
|
||||
// single addr
|
||||
if let Some(addr) = addr.as_u64() {
|
||||
return Ok(Some(Addr(addr as u32)));
|
||||
}
|
||||
// addr + offsets
|
||||
let Some((Some(mut addr), offs)) = addr.as_array()
|
||||
.and_then(|x| x.split_first())
|
||||
.map(|e| (e.0.as_u64().map(|x| Addr(x as u32)), e.1))
|
||||
else {return Err(())};
|
||||
// resolve
|
||||
for off in offs {
|
||||
let Some(off) = off.as_i64().map(|x| x as u32) else {
|
||||
return Err(());
|
||||
};
|
||||
match self.read::<Addr>(addr) {
|
||||
None => return Ok(None),
|
||||
Some(_addr) => addr = _addr+off,
|
||||
}
|
||||
}
|
||||
Ok(Some(addr))
|
||||
}
|
||||
fn resolve_addr_offsets(&self, base: Addr, offsets: &AddrOffsets) -> Option<Addr> {
|
||||
let mut addr = base + offsets.0;
|
||||
for off in offsets.1.iter() {
|
||||
match self.read::<Addr>(addr) {
|
||||
None => return None,
|
||||
Some(_addr) => addr = _addr + *off,
|
||||
}
|
||||
}
|
||||
Some(addr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn handle_command(
|
||||
env: &HttpEnv,
|
||||
dolphin: &mut Option<SMSDolphin>,
|
||||
command: &str,
|
||||
body: &JsonValue,
|
||||
) -> Result<JsonValue, JsonValue> {
|
||||
macro_rules! return_err {
|
||||
($($msg:expr),+) => {
|
||||
return Err(json!(format!($($msg),+)))
|
||||
}
|
||||
}
|
||||
macro_rules! let_dolphin {
|
||||
($d: ident) => {
|
||||
let $d = match &dolphin {
|
||||
Some(d) => d,
|
||||
None => match SMSDolphin::find_one() {
|
||||
Ok(d) => {
|
||||
*dolphin = Some(d);
|
||||
match &dolphin {
|
||||
Some(d) => d,
|
||||
None => unreachable!(),
|
||||
}
|
||||
},
|
||||
Err(e) => return_err!("{}", e),
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! let_obj_params_fields {
|
||||
($fields:ident, $type: ident) => {
|
||||
let lock_obj_params = env.obj_params_result.lock().await;
|
||||
let obj_params = match &*lock_obj_params {
|
||||
Ok(v) => v,
|
||||
Err(e) => return_err!("Fail to get ObjectParameters: {e}"),
|
||||
};
|
||||
let Some($fields) = obj_params.get($type).or_else(|| obj_params.get("_default")) else {
|
||||
return_err!("unknown type: \"{}\". Please defined \"_default\" type in ObjectParameters/*.json", $type);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
match command {
|
||||
"init" => {
|
||||
let_dolphin!(d);
|
||||
Ok(json!(d.pid()))
|
||||
},
|
||||
|
||||
"getManagers" => {
|
||||
let_dolphin!(d);
|
||||
Ok(d.read::<Addr>(Addr(match d.ver() {
|
||||
// TODO put addr in external file
|
||||
SMSVersion::GMSJ01 => 0x8040A6E8,
|
||||
SMSVersion::GMSE01 => 0x8040D110,
|
||||
SMSVersion::GMSP01 => 0x80404870,
|
||||
SMSVersion::GMSJ0A => 0x803FE048,
|
||||
}))
|
||||
.and_then(|a| d.read::<ChildInfo>(a+0x14))
|
||||
.and_then(|o| {
|
||||
let mut next = o.addr;
|
||||
let mut arr: Vec<JsonValue> = Vec::with_capacity(o.count as usize);
|
||||
for _i in 0..o.count {
|
||||
let Some(node) = d.read::<ConductorNode>(next) else {return None};
|
||||
arr.push(json!([
|
||||
node.obj.0,
|
||||
d.read::<Addr>(node.obj)
|
||||
.map(|a| d.get_class_string(a))
|
||||
.unwrap_or_else(|| "({a})".to_string()),
|
||||
d.read::<Addr>(node.obj+4).and_then(|a| d.read_str(a)).unwrap_or_else(|| "<EFBFBD>".to_string()),
|
||||
d.read::<i32>(node.obj+0x14).unwrap_or(-1),
|
||||
]));
|
||||
next = node.next;
|
||||
}
|
||||
Some(JsonValue::Array(arr))
|
||||
}).unwrap_or_else(|| json!(null))
|
||||
)
|
||||
},
|
||||
|
||||
"getManagees" => {
|
||||
let_dolphin!(d);
|
||||
let Some(addr) = body.as_u64().map(|x| Addr(x as u32)) else {
|
||||
return_err!("\"body\" must be a string");
|
||||
};
|
||||
Ok(d.read::<ChildInfo>(addr+0x14).and_then(|o| {
|
||||
let mut arr: Vec<JsonValue> = Vec::with_capacity(o.count as usize);
|
||||
for a in (0..o.count).map(|i| d.read::<Addr>(o.addr+4*i)) {
|
||||
let Some(a) = a else {return None};
|
||||
arr.push(json!([
|
||||
a.0,
|
||||
d.read::<Addr>(a).map(|a| d.get_class_string(a)),
|
||||
d.read::<Addr>(a+4).and_then(|a| d.read_str(a)).unwrap_or_else(|| "<EFBFBD>".to_string()),
|
||||
]));
|
||||
}
|
||||
Some(JsonValue::Array(arr))
|
||||
}).unwrap_or_else(|| json!(null)))
|
||||
},
|
||||
|
||||
"read" => {
|
||||
let_dolphin!(d);
|
||||
let Some(addr) = body.get("addr") else {
|
||||
return_err!("addr must be specified");
|
||||
};
|
||||
let Ok(addr) = d.resolve_addr(addr) else {
|
||||
return_err!("invalid addr: {addr:?}");
|
||||
};
|
||||
let Some(addr) = addr else {
|
||||
return Ok(json!(null));
|
||||
};
|
||||
|
||||
Ok(match body.get("size") {
|
||||
Some(size) => {
|
||||
if body.get("type").is_some() {
|
||||
return_err!("\"size\" and \"type\" cannot be specified at the same time");
|
||||
}
|
||||
let Some(size) = size.as_u64().map(|x| x as usize) else {
|
||||
return_err!("\"size\" must be a positive integer");
|
||||
};
|
||||
d.read_bytes(addr, size)
|
||||
.map(|bytes| json!(bytes.iter().map(|x| format!("{:02X}", x)).collect::<String>()))
|
||||
.unwrap_or_else(|| json!(null))
|
||||
},
|
||||
None => {
|
||||
let Some(type_) = body.get("type") else {
|
||||
return_err!("either \"size\" and \"type\" must be specified");
|
||||
};
|
||||
let Some(type_) = type_.as_str() else {
|
||||
return_err!("\"type\" must be a string");
|
||||
};
|
||||
let_obj_params_fields!(fields, type_);
|
||||
match fields {
|
||||
ObjectType::Primitive(p) => p.read(d, addr)
|
||||
.map(|x| json!(x))
|
||||
.unwrap_or_else(|| json!(null)),
|
||||
ObjectType::Class(fields) => JsonValue::Array(fields.iter().map(|field| {
|
||||
d.resolve_addr_offsets(addr, &field.offset)
|
||||
.map(|addr| field.reader.read(d, addr))
|
||||
.map(|x| json!(x))
|
||||
.unwrap_or_else(|| json!(null))
|
||||
}).collect()),
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
"readString" => {
|
||||
let_dolphin!(d);
|
||||
let Ok(addr) = body.get("addr").ok_or(()).and_then(|o| d.resolve_addr(o)) else {
|
||||
return_err!("Invalid body: {body:?}");
|
||||
};
|
||||
Ok(addr
|
||||
.and_then(|addr| d.read_str(addr))
|
||||
.map(|addr| json!(addr))
|
||||
.unwrap_or_else(|| json!(null)))
|
||||
},
|
||||
|
||||
"write" => {
|
||||
let_dolphin!(d);
|
||||
let (Ok(addr), Ok(payload)) = (
|
||||
body.get("addr").ok_or(()).and_then(|o| d.resolve_addr(o)),
|
||||
body.get("payload")
|
||||
.and_then(|x| x.as_str()).ok_or(())
|
||||
.and_then(|s| (0..s.len()).step_by(2)
|
||||
.map(|i| u8::from_str_radix(&s[i..i+2], 16).map_err(|_| ()))
|
||||
.collect::<Result<Vec<u8>, ()>>()
|
||||
),
|
||||
) else {
|
||||
return_err!("Invalid body: {body:?}");
|
||||
};
|
||||
Ok(json!(
|
||||
addr.and_then(|addr| d.write_bytes(addr, &payload)).is_some()
|
||||
))
|
||||
},
|
||||
|
||||
"getClass" => {
|
||||
let_dolphin!(d);
|
||||
let Ok(addr) = body.get("addr").ok_or(()).and_then(|o| d.resolve_addr(o)) else {
|
||||
return_err!("Invalid body: {body:?}");
|
||||
};
|
||||
Ok(addr
|
||||
.and_then(|addr| d.read::<Addr>(addr))
|
||||
.map(|a| json!(d.get_class_string(a)))
|
||||
.unwrap_or_else(|| json!(null)))
|
||||
},
|
||||
|
||||
"getFields" => {
|
||||
let Some(type_) = body.as_str() else {
|
||||
return_err!("body must be a string");
|
||||
};
|
||||
let_obj_params_fields!(fields, type_);
|
||||
Ok(match fields {
|
||||
// [offsets, name, value, notes, type, class]
|
||||
ObjectType::Primitive(_) =>
|
||||
json!([["0", "value", "", type_, type_]]),
|
||||
ObjectType::Class(fields) => JsonValue::Array(fields.iter().map(|r| json!([
|
||||
r.offset.to_string(), r.name, r.notes, r.type_, r.class,
|
||||
])).collect()),
|
||||
})
|
||||
},
|
||||
|
||||
"reload" => {
|
||||
let mut lock_obj_params = env.obj_params_result.lock().await;
|
||||
load_obj_params(&env.obj_params_dir)
|
||||
.map(|db| {
|
||||
*lock_obj_params = Ok(db);
|
||||
json!(null)
|
||||
})
|
||||
.map_err(|e| json!(e.to_string()))
|
||||
},
|
||||
|
||||
_ => {
|
||||
return_err!("Unknown command: {command}")
|
||||
},
|
||||
}
|
||||
}
|
138
src/server/http.rs
Normal file
138
src/server/http.rs
Normal file
|
@ -0,0 +1,138 @@
|
|||
/// SPDX-FileCopyrightText: 2023 sup39 <sms@sup39.dev>
|
||||
/// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::fs::File;
|
||||
use tokio_util::codec::{BytesCodec, FramedRead};
|
||||
use hyper::{Body, Request, Response, StatusCode};
|
||||
use hyper_tungstenite::tungstenite;
|
||||
use urlencoding;
|
||||
use mime_guess;
|
||||
use crate::{
|
||||
sms::SMSDolphin,
|
||||
obj_params::{load_obj_params, ObjParamsLoadResult},
|
||||
server::ws::serve_websocket,
|
||||
};
|
||||
|
||||
pub struct HttpEnv {
|
||||
static_dir: Box<Path>,
|
||||
pub obj_params_dir: Box<Path>,
|
||||
pub obj_params_result: Mutex<ObjParamsLoadResult<SMSDolphin>>,
|
||||
}
|
||||
|
||||
pub async fn serve(listener: TcpListener, root_dir: Box<Path>) -> Result<(), tungstenite::Error> {
|
||||
let obj_params_dir = {
|
||||
let mut dir = root_dir.to_path_buf();
|
||||
dir.push("res/ObjectParameters");
|
||||
dir.into_boxed_path()
|
||||
};
|
||||
let obj_params_result = Mutex::new(load_obj_params(&obj_params_dir));
|
||||
|
||||
let env = Arc::new(HttpEnv {
|
||||
static_dir: {
|
||||
let mut static_dir = root_dir.to_path_buf();
|
||||
static_dir.push("www");
|
||||
static_dir.into_boxed_path()
|
||||
},
|
||||
obj_params_dir,
|
||||
obj_params_result,
|
||||
});
|
||||
|
||||
let http = hyper::server::conn::Http::new();
|
||||
loop {
|
||||
let env = env.clone();
|
||||
let (stream, _) = listener.accept().await?;
|
||||
let connection = http
|
||||
.serve_connection(stream, hyper::service::service_fn(move |req| handle_request(req, env.clone())))
|
||||
.with_upgrades();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = connection.await {
|
||||
println!("Error serving HTTP connection: {:?}", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn response_text<T>(status: T, e: &dyn std::fmt::Display) -> Response<Body>
|
||||
where
|
||||
StatusCode: TryFrom<T>,
|
||||
<StatusCode as TryFrom<T>>::Error: Into<hyper::http::Error>,
|
||||
{
|
||||
Response::builder()
|
||||
.status(status)
|
||||
.header("Content-Type", "text/plain; charset=utf-8")
|
||||
.body(Body::from(format!("{}", e)))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn handle_request(
|
||||
mut req: Request<Body>,
|
||||
env: Arc<HttpEnv>,
|
||||
) -> Result<Response<Body>, tungstenite::Error> {
|
||||
let is_upgrade = hyper_tungstenite::is_upgrade_request(&req);
|
||||
|
||||
let mut lock_obj_params = env.obj_params_result.lock().await;
|
||||
if lock_obj_params.is_err() {
|
||||
*lock_obj_params = load_obj_params(&env.obj_params_dir);
|
||||
if let Err(e) = &*lock_obj_params {
|
||||
return Ok(response_text(500, &format!(
|
||||
"Fail to load ObjectParameters at {}: {e}",
|
||||
env.obj_params_dir.to_string_lossy(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
if is_upgrade {
|
||||
let (res, ws) = hyper_tungstenite::upgrade(&mut req, None)?;
|
||||
let env = env.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = serve_websocket(ws, env).await {
|
||||
eprintln!("Error in websocket connection: {}", e);
|
||||
}
|
||||
});
|
||||
Ok(res)
|
||||
} else {
|
||||
let url_path = match urlencoding::decode(req.uri().path()) {
|
||||
Ok(p) if p.starts_with('/') => p,
|
||||
_ => return Ok(Response::builder().status(400).body(Body::empty()).unwrap()),
|
||||
};
|
||||
|
||||
let mut file_path = PathBuf::new();
|
||||
file_path.push(&*env.static_dir);
|
||||
file_path.push(if url_path.ends_with('/') {
|
||||
"index.html"
|
||||
} else {
|
||||
&url_path[1..]
|
||||
});
|
||||
Ok(serve_file(&file_path).await)
|
||||
}
|
||||
}
|
||||
|
||||
async fn serve_file(path: &Path) -> Response<Body> {
|
||||
match File::open(path).await {
|
||||
Err(err) => response_text(404, &match path.canonicalize() {
|
||||
Ok(e) => format!("\"{}\" not found: {err}", e.to_string_lossy()),
|
||||
Err(_) => {
|
||||
match std::env::current_dir() {
|
||||
Ok(mut cwd) => {
|
||||
cwd.push(path);
|
||||
format!("\"{}\" not found: {err}", cwd.to_string_lossy())
|
||||
},
|
||||
Err(_) => format!("\"{}\" not found: {err}", path.to_string_lossy()),
|
||||
}
|
||||
},
|
||||
}),
|
||||
Ok(file) => {
|
||||
let mut res = Response::builder();
|
||||
if let Some(mime) = mime_guess::from_path(path).first() {
|
||||
res = res.header("Content-Type", format!("{mime}; charset=utf-8"));
|
||||
}
|
||||
let stream = FramedRead::new(file, BytesCodec::new());
|
||||
res.body(Body::wrap_stream(stream)).unwrap()
|
||||
},
|
||||
}
|
||||
}
|
6
src/server/mod.rs
Normal file
6
src/server/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
/// SPDX-FileCopyrightText: 2023 sup39 <sms@sup39.dev>
|
||||
/// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
pub mod http;
|
||||
pub mod ws;
|
||||
pub mod api;
|
63
src/server/ws.rs
Normal file
63
src/server/ws.rs
Normal file
|
@ -0,0 +1,63 @@
|
|||
/// SPDX-FileCopyrightText: 2023 sup39 <sms@sup39.dev>
|
||||
/// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use crate::{
|
||||
sms::SMSDolphin,
|
||||
server::{http::HttpEnv, api::handle_command},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use hyper_tungstenite::{tungstenite::{self, Message}, HyperWebsocket};
|
||||
use serde_json::{self, json, Value as JsonValue};
|
||||
|
||||
pub async fn serve_websocket(
|
||||
ws: HyperWebsocket,
|
||||
env: Arc<HttpEnv>,
|
||||
) -> Result<(), tungstenite::Error> {
|
||||
let mut ws = ws.await?;
|
||||
let mut dolphin: Option<SMSDolphin> = None;
|
||||
macro_rules! return_err {
|
||||
($($msg:expr),+) => {
|
||||
eprintln!($($msg),+);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(msg) = ws.next().await {
|
||||
let Ok(msg) = msg else {continue};
|
||||
if let Some(res) = (|| async {match msg {
|
||||
Message::Text(payload) => {
|
||||
let Ok(payload) = serde_json::from_str::<JsonValue>(&payload) else {
|
||||
eprintln!("Invalid payload (failed to deserialize): {payload}");
|
||||
return None;
|
||||
};
|
||||
let Some((Some(id), Some(command), body)) = payload.as_array()
|
||||
.and_then(|v| if v.len() == 3 {Some(v)} else {None})
|
||||
.map(|args| (
|
||||
// id must be positive
|
||||
args[0].as_i64().and_then(|x| if x<=0 {None} else {Some(x)}),
|
||||
args[1].as_str(),
|
||||
&args[2],
|
||||
))
|
||||
else {
|
||||
return_err!("Invalid payload (invalid format): {payload}");
|
||||
};
|
||||
|
||||
match handle_command(&env, &mut dolphin, command, body).await {
|
||||
Ok(body) => Some(json!([id, body])),
|
||||
Err(msg) => Some(json!([-id, msg])),
|
||||
}
|
||||
},
|
||||
Message::Binary(payload) => {
|
||||
Some(json!(format!("{}", payload.len())))
|
||||
},
|
||||
_ => None,
|
||||
}})().await {
|
||||
if let Err(e) = ws.send(Message::Text(res.to_string())).await {
|
||||
eprintln!("Fail to send message: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
92
src/sms/mod.rs
Normal file
92
src/sms/mod.rs
Normal file
|
@ -0,0 +1,92 @@
|
|||
/// SPDX-FileCopyrightText: 2023 sup39 <sms@sup39.dev>
|
||||
/// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum SMSVersion {
|
||||
GMSJ01, GMSE01, GMSP01, GMSJ0A,
|
||||
}
|
||||
pub mod vt;
|
||||
|
||||
use crate::addr::Addr;
|
||||
use crate::dolphin::{DolphinMemory, Dolphin};
|
||||
pub struct SMSDolphin {
|
||||
d: DolphinMemory,
|
||||
ver: SMSVersion,
|
||||
}
|
||||
impl Dolphin for SMSDolphin {
|
||||
#[inline]
|
||||
unsafe fn mem<T: Into<isize>>(&self, offset: T) -> *mut u8 {
|
||||
self.d.mem(offset)
|
||||
}
|
||||
}
|
||||
|
||||
impl SMSDolphin {
|
||||
#[inline]
|
||||
pub fn pid(&self) -> usize {
|
||||
self.d.pid()
|
||||
}
|
||||
#[inline]
|
||||
pub fn ver(&self) -> SMSVersion {
|
||||
self.ver
|
||||
}
|
||||
|
||||
pub fn from_dolphin_memory(d: DolphinMemory) -> Result<SMSDolphin, Option<[u8; 8]>> {
|
||||
match d.read::<&[u8; 8]>(Addr(0x80000000)) {
|
||||
None => Err(None),
|
||||
Some(rver) => match rver {
|
||||
b"GMSJ01\x00\x00" => Ok(SMSVersion::GMSJ01),
|
||||
b"GMSE01\x00\x30" => Ok(SMSVersion::GMSE01),
|
||||
b"GMSP01\x00\x00" => Ok(SMSVersion::GMSP01),
|
||||
b"GMSJ01\x00\x01" => Ok(SMSVersion::GMSJ0A),
|
||||
_ => Err(Some(rver.to_owned())),
|
||||
}.map(|ver| SMSDolphin {d, ver}),
|
||||
}
|
||||
}
|
||||
pub fn get_class(&self, addr: Addr) -> Option<&'static str> {
|
||||
vt::get_class(self.ver, addr)
|
||||
}
|
||||
pub fn get_class_string(&self, addr: Addr) -> String {
|
||||
vt::get_class_string(self.ver, addr)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SMSDolphinFindOneError {
|
||||
DolphinNotRunning,
|
||||
NoGameRunning,
|
||||
SMSNotRunning,
|
||||
}
|
||||
impl std::fmt::Display for SMSDolphinFindOneError {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
match self {
|
||||
SMSDolphinFindOneError::DolphinNotRunning => write!(fmt, "Dolphin is not running"),
|
||||
SMSDolphinFindOneError::NoGameRunning => write!(fmt, "Dolphin is found, but no game is running"),
|
||||
SMSDolphinFindOneError::SMSNotRunning => write!(fmt, "SMS is not running"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SMSDolphin {
|
||||
pub fn find_one() -> Result<SMSDolphin, SMSDolphinFindOneError> {
|
||||
let mut dolphin_running = false;
|
||||
let mut game_running = false;
|
||||
for (_pid, d) in DolphinMemory::list_dolphin() {
|
||||
match d {
|
||||
Ok(d) => {
|
||||
if let Ok(o) = SMSDolphin::from_dolphin_memory(d) {
|
||||
return Ok(o)
|
||||
}
|
||||
game_running = true;
|
||||
},
|
||||
Err(_) => dolphin_running = true,
|
||||
}
|
||||
}
|
||||
Err(if game_running {
|
||||
SMSDolphinFindOneError::SMSNotRunning
|
||||
} else if dolphin_running {
|
||||
SMSDolphinFindOneError::NoGameRunning
|
||||
} else {
|
||||
SMSDolphinFindOneError::DolphinNotRunning
|
||||
})
|
||||
}
|
||||
}
|
1514
src/sms/vt/GMSE01.json
Normal file
1514
src/sms/vt/GMSE01.json
Normal file
File diff suppressed because it is too large
Load diff
1510
src/sms/vt/GMSJ01.json
Normal file
1510
src/sms/vt/GMSJ01.json
Normal file
File diff suppressed because it is too large
Load diff
1471
src/sms/vt/GMSJ0A.json
Normal file
1471
src/sms/vt/GMSJ0A.json
Normal file
File diff suppressed because it is too large
Load diff
1472
src/sms/vt/GMSP01.json
Normal file
1472
src/sms/vt/GMSP01.json
Normal file
File diff suppressed because it is too large
Load diff
21
src/sms/vt/mod.rs
Normal file
21
src/sms/vt/mod.rs
Normal file
|
@ -0,0 +1,21 @@
|
|||
/// SPDX-FileCopyrightText: 2023 sup39 <sms@sup39.dev>
|
||||
/// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use crate::addr::Addr;
|
||||
use crate::sms::SMSVersion;
|
||||
use sup_smsac_derive::match_class_from_json;
|
||||
|
||||
pub fn get_class(ver: SMSVersion, addr: Addr) -> Option<&'static str> {
|
||||
match ver {
|
||||
SMSVersion::GMSJ01 => match_class_from_json!("src/sms/vt/GMSJ01.json")(addr.0),
|
||||
SMSVersion::GMSE01 => match_class_from_json!("src/sms/vt/GMSE01.json")(addr.0),
|
||||
SMSVersion::GMSP01 => match_class_from_json!("src/sms/vt/GMSP01.json")(addr.0),
|
||||
SMSVersion::GMSJ0A => match_class_from_json!("src/sms/vt/GMSJ0A.json")(addr.0),
|
||||
}
|
||||
}
|
||||
pub fn get_class_string(ver: SMSVersion, addr: Addr) -> String {
|
||||
match get_class(ver, addr) {
|
||||
Some(s) => s.to_string(),
|
||||
None => format!("({addr})"),
|
||||
}
|
||||
}
|
5
src/sys/mod.rs
Normal file
5
src/sys/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
/// SPDX-FileCopyrightText: 2023 sup39 <sms@sup39.dev>
|
||||
/// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
pub mod process;
|
||||
pub mod shared_memory;
|
65
src/sys/process.rs
Normal file
65
src/sys/process.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
/// SPDX-FileCopyrightText: 2023 sup39 <sms@sup39.dev>
|
||||
/// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::os::windows::ffi::OsStringExt;
|
||||
use windows::Win32::Foundation::{HANDLE, BOOL};
|
||||
use windows::Win32::System::Diagnostics::ToolHelp::{
|
||||
CreateToolhelp32Snapshot,
|
||||
TH32CS_SNAPPROCESS,
|
||||
PROCESSENTRY32W,
|
||||
Process32FirstW,
|
||||
Process32NextW,
|
||||
};
|
||||
|
||||
pub struct Process32Iterator {
|
||||
hsnapshot: HANDLE,
|
||||
fn_next: unsafe fn(HANDLE, *mut PROCESSENTRY32W) -> BOOL,
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-createtoolhelp32snapshot
|
||||
impl Process32Iterator {
|
||||
pub fn new() -> Self {
|
||||
Process32Iterator {
|
||||
hsnapshot: unsafe {CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0).unwrap()},
|
||||
fn_next: Process32FirstW,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Default for Process32Iterator {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Process32Iterator {
|
||||
type Item = PROCESSENTRY32W;
|
||||
fn next(&mut self) -> Option<PROCESSENTRY32W> {
|
||||
let mut lppe = PROCESSENTRY32W {
|
||||
dwSize: std::mem::size_of::<PROCESSENTRY32W>() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
match unsafe {(self.fn_next)(self.hsnapshot, &mut lppe)}.as_bool() {
|
||||
false => None,
|
||||
true => {
|
||||
self.fn_next = Process32NextW;
|
||||
Some(lppe)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type PidType = usize;
|
||||
pub trait ProcessInfo {
|
||||
fn pid(&self) -> PidType;
|
||||
fn get_name(&self) -> OsString;
|
||||
}
|
||||
impl ProcessInfo for PROCESSENTRY32W {
|
||||
fn pid(&self) -> PidType {
|
||||
self.th32ProcessID as PidType
|
||||
}
|
||||
fn get_name(&self) -> OsString {
|
||||
let len = self.szExeFile.iter().position(|p| *p==0).unwrap_or(self.szExeFile.len());
|
||||
OsString::from_wide(&self.szExeFile[..len])
|
||||
}
|
||||
}
|
66
src/sys/shared_memory.rs
Normal file
66
src/sys/shared_memory.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
/// SPDX-FileCopyrightText: 2023 sup39 <sms@sup39.dev>
|
||||
/// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use windows::core::PCSTR;
|
||||
use windows::Win32::Foundation::{HANDLE, CloseHandle};
|
||||
use windows::Win32::System::Memory::{
|
||||
OpenFileMappingA,
|
||||
FILE_MAP_ALL_ACCESS,
|
||||
MapViewOfFile,
|
||||
UnmapViewOfFile,
|
||||
MEMORYMAPPEDVIEW_HANDLE,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SharedMemoryOpenError {
|
||||
OpenFileFailure(String),
|
||||
MapViewFailure(String),
|
||||
MemoryUninitialized,
|
||||
}
|
||||
|
||||
pub struct SharedMemory {
|
||||
h_file_mapping: HANDLE,
|
||||
h_map_view: MEMORYMAPPEDVIEW_HANDLE,
|
||||
}
|
||||
impl SharedMemory {
|
||||
#[inline]
|
||||
pub fn get_ptr(&self) -> *mut u8 {
|
||||
self.h_map_view.0 as *mut u8
|
||||
}
|
||||
}
|
||||
|
||||
impl SharedMemory {
|
||||
pub fn open(name: &str) -> Result<Self, SharedMemoryOpenError> {
|
||||
let name = name.to_owned() + "\0";
|
||||
|
||||
// open file mapping
|
||||
let h_file_mapping = unsafe {
|
||||
OpenFileMappingA(FILE_MAP_ALL_ACCESS.0, false, PCSTR::from_raw(name.as_ptr()))
|
||||
.map_err(|e| SharedMemoryOpenError::OpenFileFailure(e.message().to_string()))?
|
||||
};
|
||||
|
||||
// create map view
|
||||
let h_map_view = unsafe {
|
||||
MapViewOfFile(h_file_mapping, FILE_MAP_ALL_ACCESS, 0, 0, 0).map_err(|e| {
|
||||
CloseHandle(h_file_mapping);
|
||||
SharedMemoryOpenError::MapViewFailure(e.message().to_string())
|
||||
})?
|
||||
};
|
||||
if h_map_view.is_invalid() {
|
||||
unsafe {CloseHandle(h_file_mapping)};
|
||||
return Err(SharedMemoryOpenError::MemoryUninitialized);
|
||||
}
|
||||
|
||||
// create SharedMemory successfully
|
||||
Ok(Self {h_file_mapping, h_map_view})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SharedMemory {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
UnmapViewOfFile(self.h_map_view);
|
||||
CloseHandle(self.h_file_mapping);
|
||||
}
|
||||
}
|
||||
}
|
76
sup-smsac-derive/Cargo.lock
generated
Normal file
76
sup-smsac-derive/Cargo.lock
generated
Normal file
|
@ -0,0 +1,76 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.171"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9"
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sup-smsac-derive"
|
||||
version = "0.1.0-beta.1"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"serde_json",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73"
|
14
sup-smsac-derive/Cargo.toml
Normal file
14
sup-smsac-derive/Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "sup-smsac-derive"
|
||||
version = "0.1.0-beta.1"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/sup39/sup-smsac"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = "1.0"
|
||||
quote = "1.0"
|
||||
serde_json = "1.0.103"
|
109
sup-smsac-derive/src/lib.rs
Normal file
109
sup-smsac-derive/src/lib.rs
Normal file
|
@ -0,0 +1,109 @@
|
|||
/// SPDX-FileCopyrightText: 2023 sup39 <sms@sup39.dev>
|
||||
/// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn;
|
||||
|
||||
#[proc_macro_derive(DecodeBE)]
|
||||
pub fn decode_be_derive(input: TokenStream) -> TokenStream {
|
||||
let ast: syn::DeriveInput = syn::parse(input).expect("Fail to parse input token stream");
|
||||
let syn::Data::Struct(data) = &ast.data else {
|
||||
panic!("Only struct can derives DecodeBE");
|
||||
};
|
||||
|
||||
let type_name = &ast.ident;
|
||||
let type_generics = &ast.generics;
|
||||
let type_generics_params = ast.generics.params.iter().map(|e| match e {
|
||||
syn::GenericParam::Type(ty) => {let ident = &ty.ident; quote! {#ident}},
|
||||
syn::GenericParam::Lifetime(ty) => {let lifetime = &ty.lifetime; quote! {#lifetime}},
|
||||
syn::GenericParam::Const(ty) => {let ident = &ty.ident; quote! {#ident}},
|
||||
});
|
||||
let q_impl = quote! {impl #type_generics DecodeBE for #type_name<#(#type_generics_params),*>};
|
||||
let fields = match &data.fields {
|
||||
syn::Fields::Named(fields) => &fields.named,
|
||||
syn::Fields::Unnamed(fields) => &fields.unnamed,
|
||||
syn::Fields::Unit => panic!("Unit type cannot derive DecodeBE"),
|
||||
};
|
||||
let q_size = fields.iter().map(|e| {
|
||||
let ty = &e.ty;
|
||||
quote! {<#ty>::PACKED_SIZE}
|
||||
});
|
||||
|
||||
// TODO empty struct
|
||||
let mut ty0: Option<&syn::Type> = None;
|
||||
let q_new_self = match &data.fields {
|
||||
syn::Fields::Named(_) => {
|
||||
let q_decode_fields = fields.iter().map(|e| {
|
||||
let name = &e.ident;
|
||||
let ty = &e.ty;
|
||||
let ty_last = ty0;
|
||||
ty0 = Some(ty);
|
||||
if let Some(ty0) = ty_last {
|
||||
quote! {#name: {ptr = ptr.add(#ty0::PACKED_SIZE); <#ty>::decode_be(ptr)}}
|
||||
} else {
|
||||
quote! {#name: <#ty>::decode_be(ptr)}
|
||||
}
|
||||
});
|
||||
quote! {
|
||||
Self{#(#q_decode_fields),*}
|
||||
}
|
||||
},
|
||||
syn::Fields::Unnamed(_) => {
|
||||
let q_decode_fields = fields.iter().map(|e| {
|
||||
let ty = &e.ty;
|
||||
let ty_last = ty0;
|
||||
ty0 = Some(ty);
|
||||
if let Some(ty0) = ty_last {
|
||||
quote! {{ptr = ptr.add(#ty0::PACKED_SIZE); <#ty>::decode_be(ptr)}}
|
||||
} else {
|
||||
quote! {<#ty>::decode_be(ptr)}
|
||||
}
|
||||
});
|
||||
quote! {
|
||||
Self(#(#q_decode_fields),*)
|
||||
}
|
||||
},
|
||||
syn::Fields::Unit => panic!("Unit type cannot derive DecodeBE"),
|
||||
};
|
||||
quote! {
|
||||
#q_impl {
|
||||
const PACKED_SIZE: usize = #(#q_size)+*;
|
||||
unsafe fn decode_be(ptr: *const u8) -> Self {
|
||||
let mut ptr: *const u8 = ptr;
|
||||
#q_new_self
|
||||
}
|
||||
}
|
||||
}.into()
|
||||
}
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
|
||||
#[proc_macro]
|
||||
pub fn match_class_from_json(input: TokenStream) -> TokenStream {
|
||||
let file_name = input.to_string();
|
||||
let file_name = if file_name.starts_with('\"') && file_name.ends_with('\"') {
|
||||
&file_name[1..file_name.len()-1]
|
||||
} else {
|
||||
&file_name
|
||||
};
|
||||
// let file = File::open(&file_name).unwrap();
|
||||
// let cwd = std::env::current_dir().unwrap();
|
||||
let file = File::open(&file_name).expect(&file_name);
|
||||
let reader = BufReader::new(file);
|
||||
let vt: HashMap<String, String> = serde_json::from_reader(reader)
|
||||
.expect("The JSON file is not in the form of HashMap<String, String>");
|
||||
let entries = vt.iter().map(|e| {
|
||||
let (addr, name) = e;
|
||||
let addr = u32::from_str_radix(addr, 16).unwrap();
|
||||
quote! {#addr => Some(#name)}
|
||||
});
|
||||
quote! {
|
||||
|x| match x {
|
||||
#(#entries),*,
|
||||
_ => None,
|
||||
}
|
||||
}.into()
|
||||
}
|
5660
www/LICENSE.html
Normal file
5660
www/LICENSE.html
Normal file
File diff suppressed because it is too large
Load diff
137
www/api.js
Normal file
137
www/api.js
Normal file
|
@ -0,0 +1,137 @@
|
|||
// @ts-check
|
||||
/**
|
||||
* @typedef {number|number[]} ReqAddr
|
||||
*/
|
||||
|
||||
/** @param {string} s */
|
||||
const hex2dv = s => new DataView(Uint8Array.from(
|
||||
(s.match(/../g) ?? /**@type{string[]}*/([]))
|
||||
.map(s => parseInt(s, 16))).buffer
|
||||
);
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* onClose?: null | ((this: WebSocket, ev: CloseEvent)=>void)
|
||||
* }} options
|
||||
*/
|
||||
function Client({onClose = null}={}) {
|
||||
/** @type {Map<number, {rsv: (res: any)=>void, rjt: (res: any)=>void}>} */
|
||||
const reqs = new Map();
|
||||
/** @type {WebSocket|null} */
|
||||
let ws = null;
|
||||
let nextId = 1;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {string} action
|
||||
* @param {any} payload
|
||||
* @returns {Promise<T>}
|
||||
*/
|
||||
const request = (action, payload) => 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});
|
||||
ws.send(JSON.stringify([id, action, payload]));
|
||||
});
|
||||
|
||||
return {
|
||||
connect: (url=`ws://${window.location.host}/`, protocol=undefined) => new Promise((rsv, rjt) => {
|
||||
const ws1 = new WebSocket(url, protocol);
|
||||
ws1.onmessage = ({data}) => {
|
||||
if (typeof data !== 'string') return; // TODO
|
||||
const [id, body] = JSON.parse(data);
|
||||
if (id > 0) {
|
||||
reqs.get(id)?.rsv(body);
|
||||
reqs.delete(id);
|
||||
} else {
|
||||
reqs.get(-id)?.rjt(body);
|
||||
reqs.delete(-id);
|
||||
}
|
||||
};
|
||||
ws1.onopen = rsv;
|
||||
ws1.onerror = rjt; // TODO auto reconnect
|
||||
ws1.onclose = onClose;
|
||||
ws = ws1;
|
||||
}),
|
||||
get ws() {return ws},
|
||||
request,
|
||||
api: {
|
||||
/**
|
||||
* @returns {Promise<number|null>}
|
||||
*/
|
||||
init: () => request('init', null),
|
||||
|
||||
/**
|
||||
* @param {ReqAddr} addr
|
||||
* @param {string} type
|
||||
*/
|
||||
read: (addr, type) => request('read', {
|
||||
addr: addr instanceof Array ? addr : [addr],
|
||||
type,
|
||||
}).then((/**@type{string[]|string|null}*/s) => s),
|
||||
|
||||
/**
|
||||
* @param {ReqAddr} addr
|
||||
* @param {number} size
|
||||
*/
|
||||
readBytes: (addr, size) => request('read', {
|
||||
addr: addr instanceof Array ? addr : [addr],
|
||||
size,
|
||||
}).then((/**@type{string|null}*/s) => s == null ? null : hex2dv(s)),
|
||||
|
||||
/**
|
||||
* @param {ReqAddr} addr
|
||||
* @returns {Promise<string|null>}
|
||||
*/
|
||||
readString: addr => request('readString', {
|
||||
addr: typeof addr === 'number' ? [addr] : addr,
|
||||
}),
|
||||
|
||||
/**
|
||||
* @param {ReqAddr} addr
|
||||
* @param {string|ArrayBuffer|ArrayBufferView} payload
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
write: (addr, payload) => request('write', {
|
||||
addr: typeof addr === 'number' ? [addr] : addr,
|
||||
payload: typeof payload === 'string' ? payload : Array.from(
|
||||
new Uint8Array(payload instanceof ArrayBuffer ? payload : payload.buffer),
|
||||
x => x.toString(16).padStart(2, '0'),
|
||||
).join(''),
|
||||
}),
|
||||
|
||||
/**
|
||||
* @param {ReqAddr} addr
|
||||
* @returns {Promise<string|null>}
|
||||
*/
|
||||
getClass: addr => request('getClass', {
|
||||
addr: typeof addr === 'number' ? [addr] : addr,
|
||||
}),
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
* @returns {Promise<[
|
||||
* offsets: string,
|
||||
* name: string,
|
||||
* notes: string,
|
||||
* type: string,
|
||||
* class_: string,
|
||||
* ][]>}
|
||||
*/
|
||||
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]}))),
|
||||
|
||||
/**
|
||||
* @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]}))),
|
||||
|
||||
reload: () => request('reload', null),
|
||||
},
|
||||
};
|
||||
}
|
36
www/icon.svg
Normal file
36
www/icon.svg
Normal file
|
@ -0,0 +1,36 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-60,-66 120,120">
|
||||
<defs>
|
||||
<path id="limb" d="M50,0 l-20,10 v-20 z"/>
|
||||
<g id="palp">
|
||||
<path d="M55,0 l-25,10 v-20 z"/>
|
||||
<circle cx="55" cy="0" r="7.5"/>
|
||||
</g>
|
||||
<ellipse id="eye" cx="8.5" cy="-4" rx="4" ry="8"/>
|
||||
<style>
|
||||
g {
|
||||
stroke: #111;
|
||||
stroke-width: 2;
|
||||
fill: #ff9;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
#eye {
|
||||
fill: #111;
|
||||
stroke: none;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g>
|
||||
<use xlink:href="#limb"/>
|
||||
<use xlink:href="#limb" transform="rotate(45)"/>
|
||||
<use xlink:href="#limb" transform="rotate(90)"/>
|
||||
<use xlink:href="#limb" transform="rotate(135)"/>
|
||||
<use xlink:href="#limb" transform="rotate(180)"/>
|
||||
<use xlink:href="#palp" transform="rotate(-45)"/>
|
||||
<use xlink:href="#palp" transform="rotate(-90)"/>
|
||||
<use xlink:href="#palp" transform="rotate(-135)"/>
|
||||
<circle cx="0" cy="0" r="35"/>
|
||||
<circle cx="0" cy="0" r="28.5"/>
|
||||
<use xlink:href="#eye"/>
|
||||
<use xlink:href="#eye" transform="scale(-1,1)"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 986 B |
106
www/index.css
Normal file
106
www/index.css
Normal file
|
@ -0,0 +1,106 @@
|
|||
:root {
|
||||
--bg: #fff;
|
||||
--fg: #000;
|
||||
--bg-stripe: #0001;
|
||||
--bg-red: #b00;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #282830;
|
||||
--fg: #f7f7f7;
|
||||
--bg-stripe: #fff1;
|
||||
--fg-link: #72e5db;
|
||||
}
|
||||
}
|
||||
|
||||
table.list tr:nth-child(2n + 1) {
|
||||
background: var(--bg-stripe);
|
||||
}
|
||||
|
||||
button {
|
||||
background: none;
|
||||
color: var(--fg);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
td > button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table.list td {
|
||||
padding: 1px 0.3em;
|
||||
white-space: pre-line;
|
||||
}
|
||||
#managers td:nth-child(1),
|
||||
#managers td:nth-child(5) {
|
||||
padding: 0 2px
|
||||
}
|
||||
#managers tr.managee > td:nth-child(2) {
|
||||
padding-left: 1em
|
||||
}
|
||||
|
||||
#fields-viewer td:nth-child(1),
|
||||
#fields-viewer td:nth-child(3) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.flex > * {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-family: "Consolas";
|
||||
font-size: 14px;
|
||||
padding: 4px;
|
||||
}
|
||||
body.disconnected #msg {
|
||||
background: var(--bg-red);
|
||||
}
|
||||
h1 {
|
||||
margin-block-start: 0;
|
||||
margin-block-end: 0.5em;
|
||||
}
|
||||
a {
|
||||
color: var(--fg-link);
|
||||
}
|
||||
|
||||
header {
|
||||
margin-block-end: 0.75em;
|
||||
padding-left: 4px;
|
||||
}
|
||||
details {
|
||||
border: solid 1px var(--fg);
|
||||
padding: 0.5em 1em;
|
||||
margin-block-start: 0.5em;
|
||||
margin-block-end: 0.5em;
|
||||
}
|
||||
details > summary {
|
||||
padding: 4px 0.5em;
|
||||
margin: -0.5em -1em -0.5em;
|
||||
}
|
||||
details[open] {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
details[open] > summary {
|
||||
border-bottom: 1px solid;
|
||||
margin: -0.5em -1em 1em;
|
||||
}
|
||||
details p {
|
||||
margin-block-start: 0.5em;
|
||||
margin-block-end: 0.5em;
|
||||
}
|
||||
#license {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
height: 100px;
|
||||
}
|
32
www/index.html
Normal file
32
www/index.html
Normal file
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>SMS Web Object Viewer (v0.1.0-beta.1)</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>
|
||||
<header>
|
||||
<div id="msg"></div>
|
||||
<details>
|
||||
<summary>LICENSE</summary>
|
||||
<p>Repository: <a href="https://github.com/sup39/sup-smsac" target="_blank" rel="noopener noreferrer">https://github.com/sup39/sup-smsac</a></p>
|
||||
<p>Author: <a href="https://misskey.sup39.dev/@sms" target="_blank" rel="noopener noreferrer">sup39</a></p>
|
||||
<p>License: MIT OR Apache-2.0</p>
|
||||
<p><a href="/LICENSE.html" target="_blank" rel="noopener noreferrer">Full license text:</a></p>
|
||||
<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>
|
||||
</body>
|
182
www/index.js
Normal file
182
www/index.js
Normal file
|
@ -0,0 +1,182 @@
|
|||
// @ts-check
|
||||
/**
|
||||
* @typedef {{addr: number, cls: string, name: string, count: number}} Manager
|
||||
* @typedef {{addr: number, cls: string, name: string}} Managee
|
||||
* @typedef {(td: HTMLTableCellElement) => void} CellFactory
|
||||
* @typedef {CellFactory[]} RowFactory
|
||||
* @typedef {{name: string, notes: string, offset: string|string[], type: string}} Field
|
||||
* @typedef {(Omit<Field, 'offset'> & {offset: number[], srcType: string})} FieldView
|
||||
* @typedef {{offsets: Field[]}} ObjParamsDBEntry
|
||||
* @typedef {Record<string, null|ObjParamsDBEntry>} ObjParamsDB
|
||||
*/
|
||||
|
||||
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)),
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {HTMLTableElement} table
|
||||
* @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]));
|
||||
});
|
||||
}
|
||||
|
||||
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 client = Client({
|
||||
onClose() {
|
||||
elmMsg.textContent = `Disconnected from server. Please reload the page.`;
|
||||
document.body.classList.add('disconnected');
|
||||
},
|
||||
});
|
||||
const {api} = client;
|
||||
Object.assign(window, {client, api}); // TODO
|
||||
|
||||
/**************** UI definition ****************/
|
||||
/**
|
||||
* @param {HTMLTableElement} elm
|
||||
*/
|
||||
function FieldsViewer(elm) {
|
||||
const tdidxVal = 2;
|
||||
let hAnm = NaN;
|
||||
let t0 = 0;
|
||||
/** @type {Managee|null} */
|
||||
let target = null;
|
||||
async function readValues() {
|
||||
if (target == null) return [];
|
||||
const values = await api.read([target.addr], target.cls);
|
||||
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);
|
||||
t0 = t;
|
||||
}
|
||||
hAnm = requestAnimationFrame(render);
|
||||
}
|
||||
const methods = {
|
||||
reload() {
|
||||
api.reload().then(() => {
|
||||
target != null && methods.view(target);
|
||||
}, err => {
|
||||
elmMsg.textContent = err;
|
||||
});
|
||||
},
|
||||
/** @param {Manager|Managee} o */
|
||||
async view(o) {
|
||||
btnReload.classList.remove('hidden');
|
||||
cancelAnimationFrame(hAnm);
|
||||
target = o;
|
||||
const fields = await api.getFields(o.cls);
|
||||
const values = readValues();
|
||||
initTable(elm, 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[3],
|
||||
td => td.textContent = r[4],
|
||||
]));
|
||||
if (fields.length) hAnm = requestAnimationFrame(render);
|
||||
},
|
||||
}
|
||||
return methods;
|
||||
}
|
||||
const fieldsViewer = FieldsViewer(elmFieldsViewer);
|
||||
btnReload.addEventListener('click', () => {
|
||||
fieldsViewer.reload();
|
||||
});
|
||||
|
||||
/** @type {(o: Manager) => RowFactory} */
|
||||
const makeManagersRowFactory = o => [
|
||||
td => {
|
||||
const btn = document.createElement('button');
|
||||
let open = false;
|
||||
/** @type {HTMLTableRowElement[] | null} */
|
||||
let trManagees = null;
|
||||
btn.textContent = '>';
|
||||
btn.addEventListener('click', async () => {
|
||||
open = !open;
|
||||
btn.textContent = open ? 'v' : '>';
|
||||
if (open) {
|
||||
if (trManagees == null) {
|
||||
const managees = await api.getManagees(o.addr);
|
||||
const trP = td.parentElement?.parentElement;
|
||||
const tr1 = td.parentElement?.nextSibling ?? null;
|
||||
trManagees = managees.map((o, i) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.classList.add('managee');
|
||||
trP?.insertBefore(tr, tr1);
|
||||
makeManageesRowFactory(o, i).forEach(f => f(tr.insertCell()));
|
||||
return tr;
|
||||
});
|
||||
} else {
|
||||
trManagees.forEach(e => e.classList.remove('hidden'));
|
||||
}
|
||||
} else {
|
||||
trManagees?.forEach(e => e.classList.add('hidden'));
|
||||
}
|
||||
});
|
||||
td.appendChild(btn);
|
||||
},
|
||||
td => td.textContent = `${o.name} (${o.count})`,
|
||||
td => td.textContent = `${o.cls}`,
|
||||
td => td.textContent = `${fmt.hex(o.addr)}`,
|
||||
makeViewerNavigatorFactory(o),
|
||||
];
|
||||
|
||||
/** @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 = `${fmt.hex(o.addr)}`,
|
||||
makeViewerNavigatorFactory(o),
|
||||
];
|
||||
|
||||
/** @type {(o: Manager|Managee) => CellFactory} */
|
||||
const makeViewerNavigatorFactory = o => td => {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = '>';
|
||||
btn.addEventListener('click', () => {
|
||||
fieldsViewer.view(o).catch(e => elmMsg.textContent = e);
|
||||
});
|
||||
td.appendChild(btn);
|
||||
};
|
||||
|
||||
/**************** UI initialization ****************/
|
||||
try {
|
||||
await client.connect(); // TODO url
|
||||
const pid = await api.init();
|
||||
console.log('pid:', pid);
|
||||
|
||||
const managers = await api.getManagers();
|
||||
initTable(elmManagers, managers.map(makeManagersRowFactory));
|
||||
} catch(e) {
|
||||
elmMsg.textContent = e;
|
||||
return; // TODO
|
||||
}
|
||||
});
|
Loading…
Reference in a new issue