docs: add dedicated DASHBOARD_DEV.md and PLUGIN_DEV.md, simplify README & CONTRIBUTING
This commit is contained in:
+2
-67
@@ -2,73 +2,8 @@
|
||||
|
||||
## 贡献方式
|
||||
|
||||
### 提交游戏插件 (`.tsp`)
|
||||
|
||||
创建一个包含 `manifest.json` 和 `parser.py` 的文件夹,压缩为 zip 改后缀:
|
||||
|
||||
```
|
||||
my_game/
|
||||
├── manifest.json
|
||||
└── parser.py
|
||||
```
|
||||
|
||||
#### manifest.json
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "unique_game_id",
|
||||
"name": "游戏名称",
|
||||
"parser_type": "forza",
|
||||
"description": "简短描述",
|
||||
"author": "你的名字",
|
||||
"version": "1.0.0",
|
||||
"default_port": 20777
|
||||
}
|
||||
```
|
||||
|
||||
#### parser.py 规范
|
||||
|
||||
必须实现 `get_parser()` 函数,返回对象包含:
|
||||
|
||||
```python
|
||||
import time
|
||||
from server.telemetry.data import TelemetryData
|
||||
|
||||
|
||||
def get_parser():
|
||||
return MyParser()
|
||||
|
||||
|
||||
class MyParser:
|
||||
def game_id(self) -> str:
|
||||
return "unique_game_id"
|
||||
|
||||
def parse(self, data: bytes, addr: tuple) -> TelemetryData:
|
||||
td = TelemetryData(game_id="unique_game_id", timestamp=time.time())
|
||||
td.raw = {"raw_hex": data.hex(), "length": len(data)}
|
||||
# 填充遥测字段...
|
||||
return td
|
||||
```
|
||||
|
||||
安装方式:在 TurboSu 设置页面 → 游戏插件管理 → 导入 `.tsp`
|
||||
|
||||
### 提交仪表盘主题 (`.tsd`)
|
||||
|
||||
创建独立文件夹:
|
||||
|
||||
```
|
||||
my_dashboard/
|
||||
├── manifest.json # 元信息 (参考下方格式)
|
||||
└── index.html # 完整独立页面
|
||||
```
|
||||
|
||||
`index.html` 需包含:
|
||||
- WebSocket 连接:`ws://<host>/ws`
|
||||
- 接收 `telemetry` 消息类型的数据
|
||||
- 用 `data-bind="字段名"` 属性绑定数据
|
||||
- 比例约束渲染逻辑
|
||||
|
||||
安装方式:在 TurboSu 仪表盘页面 → 导入 `.tsd`,或右键卡片导出。
|
||||
- **提交游戏插件** (.tsp) — 见 [游戏插件兼容开发指南](docs/PLUGIN_DEV.md)
|
||||
- **提交仪表盘主题** (.tsd) — 见 [仪表盘开发指南](docs/DASHBOARD_DEV.md)
|
||||
|
||||
### 报告 Bug
|
||||
|
||||
|
||||
@@ -80,120 +80,8 @@ python app.py
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 自定义仪表盘格式
|
||||
|
||||
每个仪表盘是一个独立文件夹,放在 `dashboards/` 或 `data/dashboards/`:
|
||||
|
||||
```
|
||||
dashboards/my_dashboard/
|
||||
├── manifest.json # 元信息
|
||||
└── index.html # 完整独立页面 (含 WS 连接 + 数据绑定)
|
||||
```
|
||||
|
||||
#### manifest.json
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "my_dashboard",
|
||||
"name": "我的仪表盘",
|
||||
"category": "custom",
|
||||
"description": "简短描述",
|
||||
"author": "你的名字",
|
||||
"version": "1.0.0",
|
||||
"config": {
|
||||
"icon": "🏎️",
|
||||
"aspect_ratio": "16:9",
|
||||
"render_mode": "contain"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### index.html
|
||||
|
||||
完全独立的 HTML 页面。需包含:
|
||||
- WebSocket 连接到 `ws://<host>/ws` 接收遥测数据
|
||||
- `data-bind` 属性绑定数据字段(如 `data-bind="speed_kmh"`)
|
||||
- 比例约束渲染逻辑(参考内置仪表盘)
|
||||
|
||||
打包分发:将整个文件夹压缩为 zip,改后缀为 `.tsd`。
|
||||
|
||||
### 自定义游戏插件
|
||||
|
||||
插件文件夹放在 `games/user/`:
|
||||
|
||||
```
|
||||
games/user/my_game/
|
||||
├── manifest.json # 元信息
|
||||
└── parser.py # 解析逻辑 (get_parser() + parse())
|
||||
```
|
||||
|
||||
#### manifest.json
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "my_game",
|
||||
"name": "我的游戏",
|
||||
"parser_type": "forza",
|
||||
"description": "自定义游戏遥测",
|
||||
"author": "你的名字",
|
||||
"version": "1.0.0",
|
||||
"default_port": 20777
|
||||
}
|
||||
```
|
||||
|
||||
#### parser.py
|
||||
|
||||
```python
|
||||
import time
|
||||
from server.telemetry.data import TelemetryData
|
||||
|
||||
|
||||
def get_parser():
|
||||
return MyGameParser()
|
||||
|
||||
|
||||
class MyGameParser:
|
||||
def game_id(self) -> str:
|
||||
return "my_game"
|
||||
|
||||
def parse(self, data: bytes, addr: tuple) -> TelemetryData:
|
||||
td = TelemetryData(game_id="my_game", timestamp=time.time())
|
||||
td.raw = {"raw_hex": data.hex(), "length": len(data)}
|
||||
# 解析你的游戏数据...
|
||||
# td.speed_kmh = ...
|
||||
# td.rpm = ...
|
||||
# td.gear = ...
|
||||
return td
|
||||
```
|
||||
|
||||
打包分发:将文件夹压缩为 zip,改后缀为 `.tsp`,在设置页面导入。
|
||||
|
||||
### TelemetryData 可用字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `speed_kmh` | float | 速度 (km/h) |
|
||||
| `speed_mph` | float | 速度 (mph) |
|
||||
| `rpm` | float | 发动机转速 |
|
||||
| `max_rpm` | float | 最大转速 |
|
||||
| `gear` | int | 档位 (0=N, -1=R) |
|
||||
| `throttle` | float | 油门 (0~1) |
|
||||
| `brake` | float | 刹车 (0~1) |
|
||||
| `clutch` | float | 离合 (0~1) |
|
||||
| `handbrake` | float | 手刹 (0~1) |
|
||||
| `steering` | float | 转向 (-1~1) |
|
||||
| `lap_time` | float | 当前圈速 (秒) |
|
||||
| `best_lap` | float | 最佳圈速 (秒) |
|
||||
| `last_lap` | float | 上圈时间 (秒) |
|
||||
| `lap_number` | int | 圈数 |
|
||||
| `fuel` | float | 燃油 |
|
||||
| `boost` | float | 涡轮增压 |
|
||||
| `horsepower` | float | 马力 |
|
||||
| `torque` | float | 扭矩 |
|
||||
| `position_x/y/z` | float | 坐标 |
|
||||
| `engine_temp` | float | 发动机温度 |
|
||||
| `oil_temp` | float | 油温 |
|
||||
| `raw` | dict | 原始数据 |
|
||||
- [仪表盘开发指南](docs/DASHBOARD_DEV.md) — 创建自定义仪表盘主题
|
||||
- [游戏插件兼容开发指南](docs/PLUGIN_DEV.md) — 为任意赛车游戏编写遥测解析器
|
||||
|
||||
## 项目结构
|
||||
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
# 仪表盘开发指南
|
||||
|
||||
每个仪表盘是 `dashboards/` (内置) 或 `data/dashboards/` (用户) 下的独立文件夹,启动时自动扫描。
|
||||
|
||||
## 文件夹结构
|
||||
|
||||
```
|
||||
dashboards/my_dashboard/
|
||||
├── manifest.json # 元信息
|
||||
├── index.html # 完整独立页面
|
||||
└── preview.png # 预览图 (可选)
|
||||
```
|
||||
|
||||
## manifest.json 格式
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "my_dashboard",
|
||||
"name": "我的仪表盘",
|
||||
"category": "custom",
|
||||
"description": "简短描述",
|
||||
"author": "你的名字",
|
||||
"version": "1.0.0",
|
||||
"config": {
|
||||
"icon": "🏎️",
|
||||
"aspect_ratio": "16:9",
|
||||
"render_mode": "contain"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `id` | string | 是 | 唯一标识,= 文件夹名 |
|
||||
| `name` | string | 是 | 显示名称 |
|
||||
| `category` | string | 否 | 分类 (speed/rpm/basic/timing/custom) |
|
||||
| `description` | string | 否 | 描述文字 |
|
||||
| `author` | string | 否 | 作者 |
|
||||
| `version` | string | 否 | 版本号 |
|
||||
| `config.icon` | string | 否 | 预览图标 emoji |
|
||||
| `config.aspect_ratio` | string | 否 | 渲染比例: `auto` / `16:9` / `4:3` / `1:1` / `21:9` |
|
||||
| `config.render_mode` | string | 否 | 渲染模式: `contain` / `cover` / `fill` / `center` |
|
||||
|
||||
### 渲染模式说明
|
||||
|
||||
| 模式 | 行为 |
|
||||
|------|------|
|
||||
| `contain` | 保持比例完整显示,不足处留黑边 (推荐) |
|
||||
| `cover` | 保持比例铺满屏幕,超出部分裁切 |
|
||||
| `fill` | 拉伸填满视口,忽略比例 |
|
||||
| `center` | 居中显示,不超过 `max_width` × `max_height` |
|
||||
|
||||
## index.html 规范
|
||||
|
||||
`index.html` 是完整的独立 HTML 页面,直接通过 `/dashboard/<id>` 访问。
|
||||
|
||||
### 必须包含
|
||||
|
||||
1. **WebSocket 连接** — 连接到 `ws://<host>/ws` 接收遥测数据
|
||||
|
||||
```javascript
|
||||
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${proto}//${location.host}/ws`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (msg.type === 'telemetry') {
|
||||
// msg.data 包含所有遥测字段
|
||||
updateUI(msg.data);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
// 断线重连
|
||||
setTimeout(connect, 2000);
|
||||
};
|
||||
```
|
||||
|
||||
2. **数据绑定** — 使用 `data-bind` 属性自动绑定
|
||||
|
||||
```html
|
||||
<div data-bind="speed_kmh">0</div>
|
||||
<span data-bind="rpm">0</span>
|
||||
```
|
||||
|
||||
3. **比例约束渲染** — 处理不同设备屏幕比例
|
||||
|
||||
```javascript
|
||||
const CONFIG = { aspect_ratio: "16:9", render_mode: "contain" };
|
||||
|
||||
function applyAspectRatio() {
|
||||
const ratio = parseRatio(CONFIG.aspect_ratio);
|
||||
const vw = window.innerWidth, vh = window.innerHeight;
|
||||
|
||||
if (CONFIG.render_mode === 'contain') {
|
||||
// 缩放以完整显示,不足处留黑边
|
||||
const scale = Math.min(vw / refW, vh / refH);
|
||||
canvas.style.width = (refW * scale) + 'px';
|
||||
canvas.style.height = (refH * scale) + 'px';
|
||||
}
|
||||
// ... 其他模式
|
||||
}
|
||||
```
|
||||
|
||||
## 可绑定的遥测字段
|
||||
|
||||
| data-bind | 字段 | 类型 | 说明 |
|
||||
|-----------|------|------|------|
|
||||
| `speed_kmh` | speed_kmh | float | 速度 (km/h) |
|
||||
| `speed_mph` | speed_mph | float | 速度 (mph) |
|
||||
| `rpm` | rpm | float | 发动机转速 |
|
||||
| `max_rpm` | max_rpm | float | 最大转速 |
|
||||
| `gear` | gear | int | 档位 (0=N, -1=R, 1~8) |
|
||||
| `throttle` | throttle | float | 油门 (0~1) |
|
||||
| `brake` | brake | float | 刹车 (0~1) |
|
||||
| `clutch` | clutch | float | 离合 (0~1) |
|
||||
| `steering` | steering | float | 转向 (-1~1) |
|
||||
| `lap_time` | lap_time | float | 当前圈速 (秒) |
|
||||
| `best_lap` | best_lap | float | 最佳圈速 (秒) |
|
||||
| `last_lap` | last_lap | float | 上圈时间 (秒) |
|
||||
| `lap_number` | lap_number | int | 圈数 |
|
||||
| `fuel` | fuel | float | 燃油量 |
|
||||
| `boost` | boost | float | 涡轮增压 |
|
||||
| `horsepower` | horsepower | float | 马力 |
|
||||
| `torque` | torque | float | 扭矩 |
|
||||
| `engine_temp` | engine_temp | float | 发动机温度 |
|
||||
| `oil_temp` | oil_temp | float | 油温 |
|
||||
|
||||
特殊自定义属性:
|
||||
|
||||
| 属性 | 说明 |
|
||||
|------|------|
|
||||
| `data-bind-rpm` | CSS 变量 `--rpm` / `--rpm-pct` / `--rpm-max` |
|
||||
| `data-bind-speed` | CSS 变量 `--speed` |
|
||||
| `data-bind-gear` | CSS 变量 `--gear` |
|
||||
|
||||
## 快速模板
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,viewport-fit=cover">
|
||||
<title>TurboSu - My Dashboard</title>
|
||||
<style>
|
||||
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
|
||||
html,body{width:100%;height:100%;overflow:hidden;background:#000;color:#fff;
|
||||
font-family:'SF Pro Text',-apple-system,'PingFang SC',sans-serif}
|
||||
#root{width:100%;height:100%;display:flex;align-items:center;justify-content:center}
|
||||
#box{position:relative;overflow:hidden}
|
||||
#box.contain{max-width:100vw;max-height:100vh}
|
||||
#box.cover{min-width:100vw;min-height:100vh}
|
||||
#box.fill{width:100vw;height:100vh}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"><div id="box" class="contain"><div id="content">
|
||||
<div data-bind="speed_kmh" style="font-size:80px;font-weight:900;">0</div>
|
||||
<div style="font-size:24px;color:rgba(255,255,255,.5);">KM/H</div>
|
||||
</div></div></div>
|
||||
<script>
|
||||
const M={aspect_ratio:"auto",render_mode:"contain"};
|
||||
(function(){
|
||||
const box=document.getElementById('box');
|
||||
const proto=location.protocol==='https:'?'wss:':'ws:';
|
||||
let ws,rt;
|
||||
|
||||
function parseRatio(r){
|
||||
if(!r||r==='auto')return null;
|
||||
const p=r.split(':');if(p.length===2){const w=+p[0],h=+p[1];if(w>0&&h>0)return w/h}
|
||||
return null
|
||||
}
|
||||
|
||||
function apply(){
|
||||
const ratio=parseRatio(M.aspect_ratio),mode=M.render_mode;
|
||||
if(!ratio){box.style.width='100vw';box.style.height='100vh';box.className='fill';return}
|
||||
const vw=window.innerWidth,vh=window.innerHeight,vr=vw/vh;let cw,ch;
|
||||
if(mode==='cover'){if(vr>ratio){cw=vw;ch=vw/ratio}else{ch=vh;cw=vh*ratio};box.className='cover'}
|
||||
else{box.className='contain';if(vr>ratio){ch=vh;cw=vh*ratio}else{cw=vw;ch=vw/ratio}}
|
||||
box.style.width=cw+'px';box.style.height=ch+'px'
|
||||
}
|
||||
|
||||
function connect(){
|
||||
ws=new WebSocket(proto+'//'+location.host+'/ws');
|
||||
ws.onopen=()=>{if(rt){clearTimeout(rt);rt=null}};
|
||||
ws.onmessage=(e)=>{
|
||||
try{const m=JSON.parse(e.data);if(m.type==='telemetry')
|
||||
document.querySelectorAll('[data-bind]').forEach(el=>{
|
||||
const v=m.data[el.getAttribute('data-bind')];
|
||||
if(v!==undefined)el.textContent=typeof v==='number'?v.toFixed(1):v
|
||||
})
|
||||
}catch(e){}
|
||||
};
|
||||
ws.onclose=()=>{rt=setTimeout(connect,2000)}
|
||||
}
|
||||
|
||||
apply();window.addEventListener('resize',apply);connect()
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## 打包分发
|
||||
|
||||
将整个文件夹压缩为 zip,改后缀为 `.tsd`,即可在 TurboSu 仪表盘页面导入或在社区分享。
|
||||
|
||||
```bash
|
||||
zip -r my_dashboard.tsd my_dashboard/
|
||||
```
|
||||
@@ -0,0 +1,222 @@
|
||||
# 游戏插件兼容开发指南
|
||||
|
||||
TurboSu 通过插件系统支持任意赛车游戏的遥测数据接入。每个插件是 `games/builtin/` 或 `games/user/` 下的独立文件夹。
|
||||
|
||||
## 文件夹结构
|
||||
|
||||
```
|
||||
games/user/my_game/
|
||||
├── manifest.json # 元信息
|
||||
└── parser.py # 解析逻辑
|
||||
```
|
||||
|
||||
## manifest.json 格式
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "my_game",
|
||||
"name": "我的游戏",
|
||||
"parser_type": "forza",
|
||||
"description": "自定义游戏遥测",
|
||||
"author": "你的名字",
|
||||
"version": "1.0.0",
|
||||
"default_port": 20777
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `id` | string | 是 | 唯一标识,= 文件夹名 |
|
||||
| `name` | string | 是 | 显示名称 |
|
||||
| `parser_type` | string | 是 | 解析器类型标签 |
|
||||
| `description` | string | 否 | 描述文字 |
|
||||
| `author` | string | 否 | 作者 |
|
||||
| `version` | string | 否 | 版本号 |
|
||||
| `default_port` | int | 否 | 默认 UDP 端口 (默认 20777) |
|
||||
|
||||
## parser.py 规范
|
||||
|
||||
必须导出 `get_parser()` 函数,返回的对象必须实现以下接口:
|
||||
|
||||
```python
|
||||
class MyParser:
|
||||
def game_id(self) -> str:
|
||||
"""返回与 manifest.json 一致的 game id"""
|
||||
return "my_game"
|
||||
|
||||
def parse(self, data: bytes, addr: tuple) -> TelemetryData:
|
||||
"""
|
||||
解析 UDP 数据包
|
||||
data: 原始字节数据
|
||||
addr: (host, port) 发送方地址
|
||||
返回: server.telemetry.data.TelemetryData
|
||||
"""
|
||||
```
|
||||
|
||||
### 完整示例
|
||||
|
||||
```python
|
||||
import struct
|
||||
import time
|
||||
from server.telemetry.data import TelemetryData
|
||||
|
||||
|
||||
def get_parser():
|
||||
return MyGameParser()
|
||||
|
||||
|
||||
class MyGameParser:
|
||||
def game_id(self) -> str:
|
||||
return "my_game"
|
||||
|
||||
def parse(self, data: bytes, addr: tuple) -> TelemetryData:
|
||||
td = TelemetryData(
|
||||
game_id="my_game",
|
||||
timestamp=time.time(),
|
||||
)
|
||||
td.raw = {
|
||||
"raw_hex": data.hex(),
|
||||
"length": len(data),
|
||||
"addr": f"{addr[0]}:{addr[1]}",
|
||||
}
|
||||
|
||||
# 解析你的游戏数据协议
|
||||
# 示例: 简单的二进制格式
|
||||
try:
|
||||
td.speed_kmh = struct.unpack_from("<f", data, 0)[0]
|
||||
td.rpm = struct.unpack_from("<f", data, 4)[0]
|
||||
td.gear = struct.unpack_from("<B", data, 8)[0]
|
||||
td.throttle = struct.unpack_from("<f", data, 12)[0]
|
||||
td.brake = struct.unpack_from("<f", data, 16)[0]
|
||||
td.steering = struct.unpack_from("<f", data, 20)[0]
|
||||
except struct.error:
|
||||
pass
|
||||
|
||||
return td
|
||||
```
|
||||
|
||||
## TelemetryData 完整字段
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class TelemetryData:
|
||||
game_id: str = "" # 游戏标识
|
||||
timestamp: float = 0.0 # 时间戳
|
||||
|
||||
# 速度和转速
|
||||
speed_kmh: float = 0.0 # 速度 (km/h)
|
||||
speed_mph: float = 0.0 # 速度 (mph)
|
||||
rpm: float = 0.0 # 发动机转速
|
||||
max_rpm: float = 8000.0 # 最大转速
|
||||
|
||||
# 传动
|
||||
gear: int = 0 # 档位 (0=N, -1=R, 1~8+)
|
||||
|
||||
# 踏板 (0~1)
|
||||
throttle: float = 0.0 # 油门
|
||||
brake: float = 0.0 # 刹车
|
||||
clutch: float = 0.0 # 离合
|
||||
handbrake: float = 0.0 # 手刹
|
||||
|
||||
# 转向 (-1 ~ 1)
|
||||
steering: float = 0.0
|
||||
|
||||
# 计时
|
||||
lap_time: float = 0.0 # 当前圈速 (秒)
|
||||
best_lap: float = 0.0 # 最佳圈速 (秒)
|
||||
last_lap: float = 0.0 # 上圈时间 (秒)
|
||||
lap_number: int = 0 # 圈数
|
||||
|
||||
# 位置/加速度
|
||||
position_x: float = 0.0
|
||||
position_y: float = 0.0
|
||||
position_z: float = 0.0
|
||||
acceleration_x: float = 0.0
|
||||
acceleration_y: float = 0.0
|
||||
acceleration_z: float = 0.0
|
||||
|
||||
# 车辆状态
|
||||
engine_temp: float = 0.0 # 发动机温度
|
||||
oil_temp: float = 0.0 # 油温
|
||||
fuel: float = 0.0 # 燃油量
|
||||
|
||||
# 性能
|
||||
boost: float = 0.0 # 涡轮增压
|
||||
horsepower: float = 0.0 # 马力
|
||||
torque: float = 0.0 # 扭矩
|
||||
|
||||
# 车辆信息
|
||||
car_name: str = "" # 车辆名称
|
||||
car_class: str = "" # 车辆等级
|
||||
|
||||
# 原始数据 (自动转发给前端调试)
|
||||
raw: dict = {}
|
||||
```
|
||||
|
||||
## 数据协议解析技巧
|
||||
|
||||
### 二进制格式 (Forza / ACC / F1 / iRacing)
|
||||
|
||||
使用 `struct` 模块解析固定长度的二进制包:
|
||||
|
||||
```python
|
||||
import struct
|
||||
|
||||
# 小端浮点
|
||||
speed = struct.unpack_from("<f", data, offset)[0]
|
||||
|
||||
# 小端无符号字节
|
||||
gear = struct.unpack_from("<B", data, offset)[0]
|
||||
|
||||
# 小端无符号短整型
|
||||
rpm = struct.unpack_from("<H", data, offset)[0]
|
||||
```
|
||||
|
||||
### 文本格式 (Assetto Corsa)
|
||||
|
||||
使用字符串分割解析:
|
||||
|
||||
```python
|
||||
text = data.decode("utf-8", errors="replace").rstrip("\r\n")
|
||||
parts = text.split("\t")
|
||||
speed = float(parts[0])
|
||||
rpm = float(parts[1])
|
||||
gear = int(float(parts[2]))
|
||||
```
|
||||
|
||||
### 未知协议
|
||||
|
||||
如果游戏使用未知/自定义协议:
|
||||
1. 启用 TurboSu 的数据测试页面查看原始 hex 数据
|
||||
2. 分析数据模式 (浮点数/整数/字符串)
|
||||
3. 对照游戏文档或社区逆向结果
|
||||
|
||||
## 测试方法
|
||||
|
||||
1. 将插件文件夹放入 `games/user/`
|
||||
2. 启动 TurboSu → 侧边栏选择你的游戏
|
||||
3. 打开数据测试页面查看实时数据
|
||||
4. 调整解析逻辑直到字段正确
|
||||
|
||||
## 打包分发
|
||||
|
||||
将整个文件夹压缩为 zip,改后缀为 `.tsp`,即可在 TurboSu 设置页面导入。
|
||||
|
||||
```bash
|
||||
# 进入插件目录
|
||||
cd games/user/my_game
|
||||
|
||||
# 打包 (不含父路径)
|
||||
zip -r my_game.tsp manifest.json parser.py
|
||||
```
|
||||
|
||||
## 常见游戏数据格式参考
|
||||
|
||||
| 游戏 | 格式 | 数据包大小 | 参考来源 |
|
||||
|------|------|-----------|----------|
|
||||
| Forza Horizon 4/5 | 二进制 LE | ~324 bytes | Forza Data Out 文档 |
|
||||
| Forza Motorsport | 二进制 LE | ~324 bytes | Forza Data Out 文档 |
|
||||
| Assetto Corsa | 文本 (TSV) | 变长 | AC UDP 插件协议 |
|
||||
| ACC | 二进制 LE | ~200 bytes | ACC Broadcasting SDK |
|
||||
| F1 系列 | 二进制 LE | ~1289+ bytes | Codemasters F1 UDP Spec |
|
||||
| iRacing | 二进制 LE | 变长 | iRacing IRSDK |
|
||||
Reference in New Issue
Block a user