[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