223 lines
5.8 KiB
Markdown
223 lines
5.8 KiB
Markdown
# 游戏插件兼容开发指南
|
|
|
|
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 |
|