Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 860bc015e8 | |||
| 5cea0f9851 | |||
| 5bc2c18f06 | |||
| 4becc26e45 | |||
| e4fe499bef | |||
| 759d35bc93 | |||
| f715cb85e6 | |||
| 1ebea5eb1a | |||
| 36755cc78a | |||
| 5630d0e332 | |||
| 29d51ad59e | |||
| 5bb4a82ae0 | |||
| 2ca8dc53f4 | |||
| 9cb8c12c89 | |||
| fb36eba07d |
@@ -86,7 +86,10 @@ python app.py
|
||||
|
||||
- [仪表盘开发指南](docs/DASHBOARD_DEV.md) — 创建自定义仪表盘主题 (.tsd)
|
||||
- [游戏插件兼容开发指南](docs/PLUGIN_DEV.md) — 为任意赛车游戏编写遥测解析器 (.tsp)
|
||||
- [Forza 数据包格式参考](docs/FH_DATA_FORMAT.md) — 官方 UDP 包 324 字节完整字段定义
|
||||
- [Forza 数据包格式参考](docs/FH_DATA_FORMAT.md) — Forza Horizon UDP 324 字节完整定义
|
||||
- [F1 24 数据包格式参考](docs/F1_DATA_FORMAT.md) — F1 24 UDP 遥测规范
|
||||
- [ACC 数据包格式参考](docs/ACC_DATA_FORMAT.md) — Assetto Corsa Competizione 广播 API
|
||||
- [iRacing 遥测格式参考](docs/IRACING_DATA_FORMAT.md) — iRacing IRSDK 遥测变量
|
||||
|
||||
## 项目结构
|
||||
|
||||
|
||||
@@ -96,6 +96,16 @@ async def dashboard_preview(theme_id: str, ext: str):
|
||||
raise HTTPException(404)
|
||||
|
||||
|
||||
@app.get("/dashboard/{theme_id}/{path:path}")
|
||||
async def dashboard_static(theme_id: str, path: str):
|
||||
from models.dashboard import BUILTIN_DASHBOARDS_DIR, USER_DASHBOARDS_DIR
|
||||
for base in [BUILTIN_DASHBOARDS_DIR, USER_DASHBOARDS_DIR]:
|
||||
filepath = base / theme_id / path
|
||||
if filepath.exists() and filepath.is_file():
|
||||
return FileResponse(filepath)
|
||||
raise HTTPException(404)
|
||||
|
||||
|
||||
@app.get("/scene/{scene_id}/preview.{ext}")
|
||||
async def scene_preview(scene_id: str, ext: str):
|
||||
from models.scene import SCENES_DIR
|
||||
|
||||
@@ -17,6 +17,7 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
||||
"theme": "dark",
|
||||
"sidebar_collapsed": False,
|
||||
"telemetry_port": 20777,
|
||||
"tcp_port": 20778,
|
||||
"use_unified_port": True,
|
||||
"game_ports": {},
|
||||
"ui_test_mode": False,
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
|
||||
# 📡 ACC Broadcasting API 完整规范
|
||||
|
||||
## 一、概述
|
||||
|
||||
ACC 有两种遥测数据通道:
|
||||
|
||||
| 通道 | 协议 | 用途 |
|
||||
|---|---|---|
|
||||
| **Broadcasting API** | UDP(双向) | 广播/观战、实时计时、多车数据 |
|
||||
| **Shared Memory** | 共享内存(Windows) | 仪表盘、运动平台、FFB(仅玩家车辆) |
|
||||
|
||||
SDK 路径:`steamapps/common/Assetto Corsa Competizione/sdk/broadcasting/`
|
||||
|
||||
---
|
||||
|
||||
## 二、启用 Broadcasting
|
||||
|
||||
### 配置文件
|
||||
|
||||
编辑 `文档/Assetto Corsa Competizione/Config/broadcasting.json`(**必须 UTF-16LE BOM 编码**):
|
||||
|
||||
```json
|
||||
{
|
||||
"updListenerPort": 9000,
|
||||
"connectionPassword": "asd",
|
||||
"commandPassword": ""
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
| `updListenerPort` | ACC 监听的 UDP 端口(默认 9000) |
|
||||
| `connectionPassword` | 客户端连接密码 |
|
||||
| `commandPassword` | 控制命令密码(留空则所有已连接客户端都可发送命令) |
|
||||
|
||||
### 测试客户端
|
||||
|
||||
运行 `sdk/broadcasting/Testclient/ksBroadcastingTestClient.exe`,输入端口和密码后点击 Connect 即可验证。
|
||||
|
||||
---
|
||||
|
||||
## 三、协议规范
|
||||
|
||||
### 基本规则
|
||||
|
||||
- **字节序**:Little Endian
|
||||
- **协议版本**:`BROADCASTING_PROTOCOL_VERSION = 2`
|
||||
- **字符串编码**:UTF-8,前缀 `uint16` 长度字段
|
||||
- **通信流程**:客户端 → ACC 发起注册 → ACC 回传数据流
|
||||
|
||||
### 数据序列化规则
|
||||
|
||||
| 类型 | 字节数 |
|
||||
|---|---|
|
||||
| `byte` / `int8` | 1 |
|
||||
| `uint16` / `int16` | 2 |
|
||||
| `int32` / `uint32` | 4 |
|
||||
| `float32` | 4 |
|
||||
| `string` | 2(长度)+ N(UTF-8 内容) |
|
||||
|
||||
### Lap 结构体(通用圈速格式)
|
||||
|
||||
```
|
||||
int32 LaptimeMS // 圈速(ms),Int32.MaxValue = 无圈速
|
||||
uint16 CarIndex // 车辆索引
|
||||
uint16 DriverIndex // 车手索引
|
||||
uint8 SplitCount // 分段数(通常 3)
|
||||
int32[] Splits // 各分段成绩(ms),Int32.MaxValue = 无效
|
||||
uint8 IsInvalid // 0=有效, 1=无效
|
||||
uint8 IsValidForBest // 0=不可作最佳圈, 1=可作最佳圈
|
||||
uint8 IsOutLap // 0=非出场圈, 1=出场圈
|
||||
uint8 IsInLap // 0=非回场圈, 1=回场圈
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、出站消息(客户端 → ACC)
|
||||
|
||||
所有消息首字节为消息类型。
|
||||
|
||||
| 类型 ID | 消息名 | 说明 |
|
||||
|---|---|---|
|
||||
| `1` | **REGISTER_COMMAND_APPLICATION** | 注册连接(必须最先发送) |
|
||||
| `9` | UNREGISTER_COMMAND_APPLICATION | 取消注册 |
|
||||
| `10` | **REQUEST_ENTRY_LIST** | 请求参赛者列表 |
|
||||
| `11` | **REQUEST_TRACK_DATA** | 请求赛道数据 |
|
||||
| `49` | CHANGE_HUD_PAGE | 切换 HUD 页面 |
|
||||
| `50` | **CHANGE_FOCUS** | 切换焦点车辆/摄像机 |
|
||||
| `51` | **INSTANT_REPLAY_REQUEST** | 请求即时回放 |
|
||||
| `52` | PLAY_MANUAL_REPLAY_HIGHLIGHT | (预留) |
|
||||
| `60` | SAVE_MANUAL_REPLAY_HIGHLIGHT | (预留) |
|
||||
|
||||
### 1. REGISTER_COMMAND_APPLICATION(ID=1)
|
||||
|
||||
```
|
||||
byte 1 // 消息类型
|
||||
byte BROADCASTING_PROTOCOL_VERSION // = 2
|
||||
string DisplayName // 显示名称
|
||||
string ConnectionPassword // 连接密码(匹配 broadcasting.json)
|
||||
int32 RealtimeUpdateInterval // 实时更新间隔(ms),建议 250
|
||||
string CommandPassword // 命令密码
|
||||
```
|
||||
|
||||
### 2. CHANGE_FOCUS(ID=50)
|
||||
|
||||
```
|
||||
byte 50 // 消息类型
|
||||
int32 ConnectionId // 连接 ID
|
||||
uint8 HasCarChange // 0=不变, 1=切换车辆
|
||||
uint16 CarIndex // 目标车辆索引(仅 HasCarChange=1)
|
||||
uint8 HasCameraChange // 0=不变, 1=切换摄像机
|
||||
string CameraSet // 摄像机集合名
|
||||
string Camera // 摄像机名
|
||||
```
|
||||
|
||||
### 3. INSTANT_REPLAY_REQUEST(ID=51)
|
||||
|
||||
```
|
||||
byte 51 // 消息类型
|
||||
int32 ConnectionId
|
||||
float32 StartSessionTime // 回放起点(会话时间,ms)
|
||||
float32 DurationMS // 回放时长(ms)
|
||||
int32 InitialFocusedCar // 初始焦点车辆(-1 = 保持当前)
|
||||
string InitialCameraSet
|
||||
string InitialCamera
|
||||
```
|
||||
|
||||
### 4. REQUEST_ENTRY_LIST(ID=10)/ REQUEST_TRACK_DATA(ID=11)
|
||||
|
||||
```
|
||||
byte 10 或 11
|
||||
int32 ConnectionId
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、入站消息(ACC → 客户端)
|
||||
|
||||
| 类型 ID | 消息名 | 说明 |
|
||||
|---|---|---|
|
||||
| `1` | **REGISTRATION_RESULT** | 注册结果 |
|
||||
| `2` | **REALTIME_UPDATE** | 会话实时信息(全局) |
|
||||
| `3` | **REALTIME_CAR_UPDATE** | 车辆实时信息(每车) |
|
||||
| `4` | **ENTRY_LIST** | 参赛者索引列表 |
|
||||
| `5` | **TRACK_DATA** | 赛道和摄像机数据 |
|
||||
| `6` | **ENTRY_LIST_CAR** | 单辆车详细信息 |
|
||||
| `7` | **BROADCASTING_EVENT** | 广播事件 |
|
||||
|
||||
### 1. REGISTRATION_RESULT(ID=1)
|
||||
|
||||
```
|
||||
byte 1
|
||||
int32 ConnectionId // 分配的连接 ID(后续请求必须携带)
|
||||
uint8 ConnectionSuccess // 0=失败, 1=成功
|
||||
uint8 IsReadOnly // 0=可读写(即拥有 commandPassword)
|
||||
string ErrorMessage // 错误信息
|
||||
```
|
||||
|
||||
成功后应立即发送 `REQUEST_ENTRY_LIST` 和 `REQUEST_TRACK_DATA`。
|
||||
|
||||
---
|
||||
|
||||
### 2. REALTIME_UPDATE(ID=2)
|
||||
|
||||
```
|
||||
byte 2
|
||||
uint16 EventIndex // 赛事索引
|
||||
uint16 SessionIndex // 会话索引
|
||||
uint8 SessionType // 见下表
|
||||
uint8 Phase // 会话阶段(见下表)
|
||||
float32 SessionTime // 会话已过时间 (ms)
|
||||
float32 SessionEndTime // 会话剩余时间 (ms)
|
||||
int32 FocusedCarIndex // 当前焦点车辆
|
||||
string ActiveCameraSet // 当前摄像机集合
|
||||
string ActiveCamera // 当前摄像机
|
||||
string CurrentHUDPage // 当前 HUD 页面
|
||||
uint8 IsReplayPlaying // 是否回放中
|
||||
// 以下仅 IsReplayPlaying=1 时存在:
|
||||
float32 ReplaySessionTime // 回放中的会话时间
|
||||
float32 ReplayRemainingTime // 回放剩余时间
|
||||
int32 ReplayFocusedCar
|
||||
// 始终存在:
|
||||
uint16 TimeOfDay // 时间 hh*100+mm
|
||||
uint8 AmbientTemp // 环境温度 (°C)
|
||||
uint8 TrackTemp // 赛道温度 (°C)
|
||||
uint8 Clouds // 云量(÷10,即 0.0~1.0)
|
||||
uint8 RainLevel // 降雨量(÷10)
|
||||
uint8 Wetness // 路面湿度(÷10)
|
||||
Lap BestSessionLap // 当前会话最佳圈速
|
||||
```
|
||||
|
||||
#### RaceSessionType 枚举
|
||||
|
||||
| 值 | 类型 |
|
||||
|---|---|
|
||||
| 0 | Practice(练习) |
|
||||
| 4 | Qualifying(排位赛) |
|
||||
| 9 | Superpole(超级杆位) |
|
||||
| 10 | Race(正赛) |
|
||||
| 11 | Hotlap(单圈冲刺) |
|
||||
| 12 | Hotstint(阶段冲刺) |
|
||||
| 13 | HotlapSuperpole |
|
||||
| 14 | Replay(回放) |
|
||||
|
||||
#### SessionPhase 枚举
|
||||
|
||||
| 值 | 阶段 |
|
||||
|---|---|
|
||||
| 0 | NONE |
|
||||
| 1 | Starting |
|
||||
| 2 | PreFormation |
|
||||
| 3 | FormationLap(编队圈) |
|
||||
| 4 | PreSession |
|
||||
| 5 | **Session**(绿灯起跑后) |
|
||||
| 6 | SessionOver |
|
||||
| 7 | PostSession |
|
||||
| 8 | ResultUI |
|
||||
|
||||
---
|
||||
|
||||
### 3. REALTIME_CAR_UPDATE(ID=3)
|
||||
|
||||
**最重要的数据包**—每辆车独立发送。
|
||||
|
||||
```
|
||||
byte 3
|
||||
uint16 CarIndex // 车辆索引(对应 EntryList)
|
||||
uint16 DriverIndex // 当前车手索引
|
||||
uint8 DriverCount // 该车车手总数
|
||||
int8 Gear // -1=R, 0=N, 1~6...
|
||||
float32 WorldPosX // 世界坐标 X(始终为 0)
|
||||
float32 WorldPosY // 世界坐标 Y(始终为 0)
|
||||
float32 Yaw // 偏航角(始终为 0)
|
||||
uint8 CarLocation // 位置枚举(见下)
|
||||
uint16 Kmh // 速度 (km/h)
|
||||
uint16 Position // 排名(1-based)
|
||||
uint16 CupPosition // 组别排名(1-based)
|
||||
uint16 TrackPosition // 赛道位置序号(始终为 0)
|
||||
float32 SplinePosition // 赛道位置 0.0~1.0
|
||||
uint16 Laps // 已完成圈数
|
||||
int32 Delta // 与最佳圈速的实时差距 (ms)
|
||||
Lap BestSessionLap // 该车最佳圈速
|
||||
Lap LastLap // 上一圈
|
||||
Lap CurrentLap // 当前圈(时间持续更新,分段不填充)
|
||||
```
|
||||
|
||||
#### CarLocation 枚举
|
||||
|
||||
| 值 | 位置 |
|
||||
|---|---|
|
||||
| 0 | NONE |
|
||||
| 1 | **Track**(赛道上) |
|
||||
| 2 | Pitlane(维修区通道) |
|
||||
| 3 | PitEntry(进站入口) |
|
||||
| 4 | PitExit(出站出口) |
|
||||
|
||||
---
|
||||
|
||||
### 4. ENTRY_LIST(ID=4)+ ENTRY_LIST_CAR(ID=6)
|
||||
|
||||
**两步接收**:先收索引列表,再逐车收详情。
|
||||
|
||||
#### ENTRY_LIST 结构:
|
||||
|
||||
```
|
||||
byte 4
|
||||
int32 ConnectionId
|
||||
uint16 CarEntryCount
|
||||
uint16[] CarIndexes // 车辆索引数组
|
||||
uint16 DriverEntryCount
|
||||
uint16[] DriverIndexes // 保留字段
|
||||
```
|
||||
|
||||
#### ENTRY_LIST_CAR 结构:
|
||||
|
||||
```
|
||||
byte 6
|
||||
uint16 CarIndex // 对应 ENTRY_LIST 中的索引
|
||||
uint8 CarModelType // 车型(见下)
|
||||
string TeamName // 车队名
|
||||
int32 RaceNumber // 车号
|
||||
string TeamCarName // 车型名称
|
||||
string DisplayName // 显示名
|
||||
uint8 CupCategory // 组别:0=Overall/Pro, 1=ProAm, 2=Am, 3=Silver, 4=National
|
||||
uint8 CurrentDriverId // 当前车手 ID
|
||||
uint16 Nationality // 国籍(见附录)
|
||||
uint8 DriversOnCarCount // 该车手数量
|
||||
// 每个车手:
|
||||
uint16 DriverIndex
|
||||
uint8 HasDriverInfo // 0/1
|
||||
string FirstName
|
||||
string LastName
|
||||
string Nickname
|
||||
string ShortName // 缩写(如 "VER")
|
||||
uint8 Category // 3=Platinum, 2=Gold, 1=Silver, 0=Bronze
|
||||
uint16 Nationality
|
||||
```
|
||||
|
||||
#### CarModelType(部分)
|
||||
|
||||
| 值 | 车型 |
|
||||
|---|---|
|
||||
| 1 | Mercedes-AMG GT3 |
|
||||
| 2 | Ferrari 488 GT3 |
|
||||
| 15 | Lexus RC F GT3 |
|
||||
| 16 | Lamborghini Huracán GT3 |
|
||||
| 19 | Audi R8 LMS GT3 |
|
||||
| 20 | Aston Martin Vantage GT3 |
|
||||
| 23 | Porsche 911 GT3 R |
|
||||
|
||||
---
|
||||
|
||||
### 5. TRACK_DATA(ID=5)
|
||||
|
||||
```
|
||||
byte 5
|
||||
int32 ConnectionId
|
||||
string TrackName // 赛道名
|
||||
int32 TrackId // 赛道 ID(见下)
|
||||
int32 TrackMeters // 赛道长度 (m)
|
||||
uint8 CameraSetCount
|
||||
// 每个摄像机集合:
|
||||
string CameraSetName
|
||||
uint8 CameraCount
|
||||
string[] CameraNames // 逐个摄像机名
|
||||
uint8 HUDPageCount
|
||||
string[] HUDPageNames
|
||||
```
|
||||
|
||||
#### TrackId(部分)
|
||||
|
||||
| ID | 赛道 |
|
||||
|---|---|
|
||||
| 1 | Brands Hatch |
|
||||
| 2 | Spa-Francorchamps |
|
||||
| 3 | Monza |
|
||||
| 4 | Misano |
|
||||
| 5 | Paul Ricard |
|
||||
| 6 | Silverstone |
|
||||
| 7 | Hungaroring |
|
||||
| 8 | Nürburgring |
|
||||
| 9 | Barcelona |
|
||||
| 10 | Zolder |
|
||||
| 11 | Zandvoort |
|
||||
| 13 | Mount Panorama (Bathurst) |
|
||||
| 14 | Laguna Seca |
|
||||
| 15 | Suzuka |
|
||||
|
||||
---
|
||||
|
||||
### 6. BROADCASTING_EVENT(ID=7)
|
||||
|
||||
```
|
||||
byte 7
|
||||
uint8 Type // 事件类型
|
||||
string Msg // 事件消息(常为圈速字符串)
|
||||
int32 TimeMs // 自连接建立以来的时间 (ms)
|
||||
int32 CarId // 相关车辆 ID
|
||||
```
|
||||
|
||||
| 事件 Type | 说明 |
|
||||
|---|---|
|
||||
| 5 | LapCompleted(完成一圈) |
|
||||
| 6 | BestSessionLap(全场最佳圈) |
|
||||
| 7 | BestPersonalLap(个人最佳圈) |
|
||||
|
||||
---
|
||||
|
||||
## 六、Shared Memory API(补充)
|
||||
|
||||
用于本地仪表盘/运动平台,仅含玩家车辆数据。
|
||||
|
||||
### 三个内存页
|
||||
|
||||
| 页 | 大小 | 更新频率 | 内容 |
|
||||
|---|---|---|---|
|
||||
| **SPageFilePhysics** | ~800 B | 物理步进 | 速度、油门、刹车、G力、胎温等 |
|
||||
| **SPageFileGraphic** | ~852 B | 图形帧率 | 圈速、排名、旗语、天气、油耗等 |
|
||||
| **SPageFileStatic** | ~688 B | 固定 | 车辆名、赛道、最大RPM等 |
|
||||
|
||||
### Physics 关键字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `gas` | float | 油门 0.0~1.0 |
|
||||
| `brake` | float | 刹车 0.0~1.0 |
|
||||
| `gear` | int | 档位 |
|
||||
| `rpm` | int | 引擎转速 |
|
||||
| `speedKmh` | float | 速度 |
|
||||
| `steerAngle` | float | 转向角 |
|
||||
| `wheelPressure[4]` | float | 胎压 |
|
||||
| `tyreCoreTemp[4]` | float | 轮胎内核温度 |
|
||||
| `brakeTemp[4]` | float | 刹车温度 |
|
||||
| `carDamage` | struct | 车辆损伤 |
|
||||
| `fuel` | float | 油量 (L) |
|
||||
| `abs` | float | ABS 作用量 |
|
||||
| `tc` | float | TC 作用量 |
|
||||
| `localVelocity` | Vector3f | 局部速度 |
|
||||
| `localAngularVel` | Vector3f | 局部角速度 |
|
||||
| `gForce` | Vector3f | G力 |
|
||||
|
||||
### Graphics 关键字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `status` | enum | AC_OFF=0, AC_LIVE=1 等 |
|
||||
| `sessionType` | enum | 同 Broadcasting |
|
||||
| `currentTime` | int | 当前圈速 (ms) |
|
||||
| `lastTime` | int | 上一圈 (ms) |
|
||||
| `bestTime` | int | 最佳圈 (ms) |
|
||||
| `completedLaps` | int | 已完成圈数 |
|
||||
| `position` | int | 排名 |
|
||||
| `normalizedCarPosition` | float | 赛道位置 0.0~1.0 |
|
||||
| `isInPit` | bool | 是否在维修区 |
|
||||
| `flag` | enum | 旗语 |
|
||||
| `fuelPerLap` | float | 每圈油耗 |
|
||||
| `gapAhead` | int | 与前车差距 (ms) |
|
||||
| `gapBehind` | int | 与后车差距 (ms) |
|
||||
| `trackStatus` | string | Green/Fast/Optimum/Greasy/Damp/Wet/Flooded |
|
||||
| `rainIntensity` | enum | 降雨强度 |
|
||||
| `globalYellow` | bool | 黄旗 |
|
||||
|
||||
---
|
||||
|
||||
## 七、关键注意事项
|
||||
|
||||
1. **WorldPosX/Y 和 Yaw**:Broadcasting API 中始终为 0(出于反作弊考虑),需要全车坐标请用 Shared Memory
|
||||
2. **EntryList**:在首次连接时发送一次,新会话开始时不自动重发
|
||||
3. **TrackData**:同样只在连接时发送一次,新会话不重发
|
||||
4. **SplinePosition**:0.0~1.0 表示车辆在赛道上的归一化位置(维修区也有效)
|
||||
5. **Lap.Splits**:Int32.MaxValue 表示无效分段,超出分段数返回 0
|
||||
6. **CarUpdate 中 Gear**:传输值 - 2 = 实际档位(传输 1 = -1/R,传输 2 = 0/N,传输 3 = 1 档...)
|
||||
7. **broadcasting.json 编码**:必须是 UTF-16LE with BOM,否则 ACC 无法读取
|
||||
@@ -0,0 +1,318 @@
|
||||
|
||||
## 🏎️ F1 24 UDP Telemetry 规范总览
|
||||
|
||||
### 📌 官方文档
|
||||
- **官方规范 Word 文档**:[EA Forums — Data Output from F1 24 v27.2x.docx](https://forums.ea.com/t5/s/tghpe58374/attachments/tghpe58374/f1-24-general-discussion-en/4624/1/Data%20Output%20from%20F1%2024%20v27.2x.docx)
|
||||
- **GitHub 开源解析库参考**:https://github.com/MacManley/f1-24-udp
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 游戏设置
|
||||
|
||||
在游戏中启用 UDP 遥测:**Game Options → Settings → Telemetry Settings**
|
||||
|
||||
| 设置项 | 推荐值 |
|
||||
|---|---|
|
||||
| `UDP Telemetry` | On |
|
||||
| `UDP Broadcast Mode` | Off(多设备时可开启) |
|
||||
| `UDP IP Address` | 接收端 IP(本机用 `127.0.0.1`) |
|
||||
| `Port` | `20777`(默认) |
|
||||
| `UDP Send Rate` | `20Hz`(默认,最高 60Hz) |
|
||||
| `UDP Format` | `2024` |
|
||||
|
||||
> ⚠️ 注意:游戏仅在玩家进入 **计时圈(timed lap)** 后才开始发送有效遥测数据。
|
||||
|
||||
---
|
||||
|
||||
## 📦 数据编码
|
||||
|
||||
- **字节序**:Little Endian
|
||||
- **对齐**:packed(无填充)
|
||||
- **最大车辆数**:22
|
||||
|
||||
### 数据类型
|
||||
|
||||
| 类型 | 说明 |
|
||||
|---|---|
|
||||
| `uint8` / `int8` | 无符号/有符号 8-bit |
|
||||
| `uint16` / `int16` | 无符号/有符号 16-bit |
|
||||
| `uint32` | 无符号 32-bit |
|
||||
| `uint64` | 无符号 64-bit |
|
||||
| `float` | 32-bit 浮点 |
|
||||
|
||||
---
|
||||
|
||||
## 📋 包头结构(PacketHeader)
|
||||
|
||||
**大小:29 bytes**
|
||||
|
||||
```c
|
||||
struct PacketHeader {
|
||||
uint16 m_packetFormat; // 2024
|
||||
uint8 m_gameYear; // 年份后两位,如 24
|
||||
uint8 m_gameMajorVersion; // 主版本号 "X.00"
|
||||
uint8 m_gameMinorVersion; // 次版本号 "1.XX"
|
||||
uint8 m_packetVersion; // 包类型版本号,从 1 开始
|
||||
uint8 m_packetId; // 包类型 ID(见下表)
|
||||
uint64 m_sessionUID; // 会话唯一 ID
|
||||
float m_sessionTime; // 会话时间戳
|
||||
uint32 m_frameIdentifier; // 帧标识符
|
||||
uint32 m_overallFrameIdentifier; // 全局帧标识符(Flashback 不回退)
|
||||
uint8 m_playerCarIndex; // 玩家车辆索引
|
||||
uint8 m_secondaryPlayerCarIndex; // 分屏第二玩家索引(255 = 无)
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🆔 包类型 ID 汇总
|
||||
|
||||
| ID | 包名称 | 说明 | 频率 | 大小 |
|
||||
|---|---|---|---|---|
|
||||
| 0 | **Motion** | 所有车辆物理运动数据 | 菜单设定频率 | 1349 B |
|
||||
| 1 | **Session** | 会话信息(赛道、剩余时间) | 2/秒 | 753 B |
|
||||
| 2 | **Lap Data** | 所有车辆圈速数据 | 菜单设定频率 | 1285 B |
|
||||
| 3 | **Event** | 会话事件 | 事件触发时 | 45 B |
|
||||
| 4 | **Participants** | 参与者列表 | 每 5 秒 | 1350 B |
|
||||
| 5 | **Car Setups** | 车辆调校参数 | 2/秒 | 1133 B |
|
||||
| 6 | **Car Telemetry** | 遥测数据(速度/油门/DRS等) | 菜单设定频率 | 1352 B |
|
||||
| 7 | **Car Status** | 车辆状态(燃油/ERS等) | 菜单设定频率 | 1239 B |
|
||||
| 8 | **Final Classification** | 最终排名 | 结果界面每 5 秒 | 1020 B |
|
||||
| 9 | **Lobby Info** | 多人游戏大厅信息 | 大厅中 2/秒 | 1306 B |
|
||||
| 10 | **Car Damage** | 车辆损伤数据 | 10/秒 | 953 B |
|
||||
| 11 | **Session History** | 圈速和轮胎历史 | 20/秒(轮询车辆) | 1460 B |
|
||||
| 12 | **Tyre Sets** | 轮胎组详细数据 | 2/秒 | 231 B |
|
||||
| 13 | **Motion Ex** | 玩家车辆扩展运动数据 | 菜单设定频率 | 237 B |
|
||||
| 14 | **Time Trial** | 计时赛专用数据 | 1/秒(仅TT) | 101 B |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 各包数据结构详解
|
||||
|
||||
### 1. Motion Packet(ID=0)
|
||||
包含所有 22 辆车的物理数据。
|
||||
|
||||
```c
|
||||
struct CarMotionData {
|
||||
float m_worldPositionX, m_worldPositionY, m_worldPositionZ; // 世界位置 (m)
|
||||
float m_worldVelocityX, m_worldVelocityY, m_worldVelocityZ; // 世界速度 (m/s)
|
||||
int16 m_worldForwardDirX, m_worldForwardDirY, m_worldForwardDirZ; // 前向向量(÷32767 归一化)
|
||||
int16 m_worldRightDirX, m_worldRightDirY, m_worldRightDirZ; // 右向向量
|
||||
float m_gForceLateral, m_gForceLongitudinal, m_gForceVertical; // G 力分量
|
||||
float m_yaw, m_pitch, m_roll; // 角度 (rad)
|
||||
};
|
||||
// PacketMotionData: PacketHeader + CarMotionData[22]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Session Packet(ID=1)
|
||||
包含当前会话完整信息,如天气、温度、赛道、规则、辅助设置等。
|
||||
|
||||
关键字段:
|
||||
- `m_weather`: 0=晴, 1=少云, 2=阴, 3=小雨, 4=大雨, 5=暴雨
|
||||
- `m_trackId`: 赛道 ID(-1=未知,见附录)
|
||||
- `m_sessionType`: 会话类型
|
||||
- `m_formula`: 0=F1 Modern, 1=F1 Classic, 2=F2, 3=F1 Generic, 4=Beta, 6=Esports, 8=F1 World, 9=F1 Elimination
|
||||
- `m_safetyCarStatus`: 0=无, 1=实体安全车, 2=虚拟安全车, 3=编队圈
|
||||
- `m_weatherForecastSamples[64]`: 天气预测(最多 64 样本,适配冲刺赛周末)
|
||||
- `m_weekendStructure[12]`: 周末赛程结构
|
||||
- `m_sector2LapDistanceStart` / `m_sector3LapDistanceStart`: 赛段起点距离
|
||||
|
||||
---
|
||||
|
||||
### 3. Lap Data Packet(ID=2)
|
||||
```c
|
||||
struct LapData {
|
||||
uint32 m_lastLapTimeInMS; // 上一圈时间 (ms)
|
||||
uint32 m_currentLapTimeInMS; // 当前圈耗时 (ms)
|
||||
uint16 m_sector1TimeMSPart; // S1 毫秒部分
|
||||
uint8 m_sector1TimeMinutesPart; // S1 分钟部分
|
||||
uint16 m_sector2TimeMSPart; // S2 毫秒部分
|
||||
uint8 m_sector2TimeMinutesPart; // S2 分钟部分
|
||||
uint16 m_deltaToCarInFrontMSPart; // 与前车差距毫秒
|
||||
uint8 m_deltaToCarInFrontMinutesPart; // 与前车差距分钟
|
||||
uint16 m_deltaToRaceLeaderMSPart; // 与领跑者差距毫秒
|
||||
uint8 m_deltaToRaceLeaderMinutesPart; // 与领跑者差距分钟
|
||||
float m_lapDistance; // 当前圈已跑距离 (m)
|
||||
float m_totalDistance; // 总行驶距离 (m)
|
||||
float m_safetyCarDelta; // 安全车时间差 (s)
|
||||
uint8 m_carPosition; // 赛道排名
|
||||
uint8 m_currentLapNum; // 当前圈数
|
||||
uint8 m_pitStatus; // 0=无, 1=进站中, 2=维修区
|
||||
uint8 m_numPitStops; // 已进站次数
|
||||
uint8 m_sector; // 0=S1, 1=S2, 2=S3
|
||||
uint8 m_currentLapInvalid; // 0=有效, 1=无效
|
||||
uint8 m_penalties; // 累计罚时 (s)
|
||||
uint8 m_driverStatus; // 0=车库, 1=飞行圈, 2=回场圈, 3=出场圈, 4=赛道上
|
||||
uint8 m_resultStatus; // 0-7 (invalid/active/finished/DNF/DSQ等)
|
||||
float m_speedTrapFastestSpeed; // 速度陷阱最快速度 (km/h)
|
||||
// ... 等
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Event Packet(ID=3)
|
||||
事件代码:
|
||||
|
||||
| 代码 | 事件 | 代码 | 事件 |
|
||||
|---|---|---|---|
|
||||
| `SSTA` | 会话开始 | `SEND` | 会话结束 |
|
||||
| `FTLP` | 最快圈速 | `RTMT` | 退赛 |
|
||||
| `DRSE` | DRS 启用 | `DRSD` | DRS 禁用 |
|
||||
| `TMPT` | 队友进站 | `CHQF` | 方格旗 |
|
||||
| `RCWN` | 冠军产生 | `PENA` | 处罚发出 |
|
||||
| `SPTP` | 速度陷阱触发 | `STLG` | 发车灯 |
|
||||
| `LGOT` | 灯灭起跑 | `DTSV` | Drive-through 已执行 |
|
||||
| `SGSV` | Stop-Go 已执行 | `FLBK` | Flashback |
|
||||
| `BUTN` | 按钮状态 | `RDFL` | 红旗 |
|
||||
| `OVTK` | 超车 | `SCAR` | 安全车事件 |
|
||||
| `COLL` | 碰撞事件 | | |
|
||||
|
||||
---
|
||||
|
||||
### 5. Car Telemetry Packet(ID=6)
|
||||
```c
|
||||
struct CarTelemetryData {
|
||||
uint16 m_speed; // 速度 (km/h)
|
||||
float m_throttle; // 油门 (0.0~1.0)
|
||||
float m_steer; // 转向 (-1.0~1.0)
|
||||
float m_brake; // 刹车 (0.0~1.0)
|
||||
uint8 m_clutch; // 离合 (0~100)
|
||||
int8 m_gear; // 档位 (1-8, N=0, R=-1)
|
||||
uint16 m_engineRPM; // 引擎转速
|
||||
uint8 m_drs; // DRS: 0=关, 1=开
|
||||
uint8 m_revLightsPercent; // 转速灯百分比
|
||||
uint16 m_revLightsBitValue; // 转速灯位值
|
||||
uint16 m_brakesTemperature[4]; // 刹车温度 (°C)
|
||||
uint8 m_tyresSurfaceTemperature[4]; // 胎面温度 (°C)
|
||||
uint8 m_tyresInnerTemperature[4]; // 胎内温度 (°C)
|
||||
uint16 m_engineTemperature; // 引擎温度 (°C)
|
||||
float m_tyresPressure[4]; // 胎压 (PSI)
|
||||
uint8 m_surfaceType[4]; // 路面类型
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Car Status Packet(ID=7)
|
||||
```c
|
||||
struct CarStatusData {
|
||||
uint8 m_tractionControl; // 0=关, 1=中, 2=全
|
||||
uint8 m_antiLockBrakes; // 0=关, 1=开
|
||||
uint8 m_fuelMix; // 0=Lean, 1=Standard, 2=Rich, 3=Max
|
||||
uint8 m_frontBrakeBias; // 前刹车比 (%)
|
||||
float m_fuelInTank; // 燃油量 (kg)
|
||||
float m_fuelCapacity; // 油箱容量
|
||||
float m_fuelRemainingLaps; // 剩余油量可跑圈数
|
||||
uint8 m_drsAllowed; // DRS 是否允许
|
||||
uint16 m_drsActivationDistance; // DRS 激活距离 (m)
|
||||
uint8 m_actualTyreCompound; // 实际轮胎配方 (16=C5, 17=C4, 18=C3, 19=C2, 20=C1, 21=C0, 7=半雨, 8=全雨)
|
||||
uint8 m_visualTyreCompound; // 视觉轮胎配方 (16=软, 17=中, 18=硬)
|
||||
uint8 m_tyresAgeLaps; // 轮胎使用圈数
|
||||
int8 m_vehicleFiaFlags; // FIA 旗帜 (-1=未知, 0=无, 1=绿, 2=蓝, 3=黄)
|
||||
float m_enginePowerICE; // 内燃机功率 (W)
|
||||
float m_enginePowerMGUK; // MGU-K 功率 (W)
|
||||
float m_ersStoreEnergy; // ERS 储能 (J)
|
||||
uint8 m_ersDeployMode; // 0=无, 1=Medium, 2=Hotlap, 3=Overtake
|
||||
float m_ersHarvestedThisLapMGUK; // MGU-K 回收能量
|
||||
float m_ersHarvestedThisLapMGUH; // MGU-H 回收能量
|
||||
float m_ersDeployedThisLap; // 已使用 ERS 能量
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. Car Damage Packet(ID=10)
|
||||
```c
|
||||
struct CarDamageData {
|
||||
float m_tyresWear[4]; // 轮胎磨损 (%)
|
||||
uint8 m_tyresDamage[4]; // 轮胎损伤 (%)
|
||||
uint8 m_brakesDamage[4]; // 刹车损伤 (%)
|
||||
uint8 m_frontLeftWingDamage; // 前左翼损伤 (%)
|
||||
uint8 m_frontRightWingDamage; // 前右翼损伤 (%)
|
||||
uint8 m_rearWingDamage; // 尾翼损伤 (%)
|
||||
uint8 m_floorDamage; // 底板损伤 (%)
|
||||
uint8 m_diffuserDamage; // 扩散器损伤 (%)
|
||||
uint8 m_sidepodDamage; // 侧箱损伤 (%)
|
||||
uint8 m_drsFault; // DRS 故障 (0/1)
|
||||
uint8 m_ersFault; // ERS 故障 (0/1)
|
||||
uint8 m_gearBoxDamage; // 变速箱损伤 (%)
|
||||
uint8 m_engineDamage; // 引擎损伤 (%)
|
||||
uint8 m_engineMGUHWear; // MGU-H 磨损 (%)
|
||||
uint8 m_engineESWear; // ES 磨损 (%)
|
||||
uint8 m_engineCEWear; // CE 磨损 (%)
|
||||
uint8 m_engineICEWear; // ICE 磨损 (%)
|
||||
uint8 m_engineMGUKWear; // MGU-K 磨损 (%)
|
||||
uint8 m_engineTCWear; // TC 磨损 (%)
|
||||
uint8 m_engineBlown; // 引擎爆缸 (0/1)
|
||||
uint8 m_engineSeized; // 引擎卡死 (0/1)
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. Motion Ex Packet(ID=13)— 运动平台关键包
|
||||
|
||||
专为 **运动模拟平台(D-BOX 等)** 设计,包含更多细节:
|
||||
|
||||
```c
|
||||
float m_suspensionPosition[4]; // 悬挂位置 [RL, RR, FL, FR]
|
||||
float m_suspensionVelocity[4]; // 悬挂速度
|
||||
float m_suspensionAcceleration[4]; // 悬挂加速度
|
||||
float m_wheelSpeed[4]; // 各轮速度
|
||||
float m_wheelSlipRatio[4]; // 滑移率
|
||||
float m_wheelSlipAngle[4]; // 滑移角
|
||||
float m_wheelLatForce[4]; // 横向力
|
||||
float m_wheelLongForce[4]; // 纵向力
|
||||
float m_wheelVertForce[4]; // 垂向力
|
||||
float m_heightOfCOGAboveGround; // 重心高度
|
||||
float m_localVelocityX/Y/Z; // 局部速度
|
||||
float m_angularVelocityX/Y/Z; // 角速度 (rad/s)
|
||||
float m_angularAccelerationX/Y/Z; // 角加速度 (rad/s²)
|
||||
float m_frontWheelsAngle; // 前轮转角 (rad)
|
||||
float m_frontAeroHeight; // 前底盘高度
|
||||
float m_rearAeroHeight; // 后底盘高度
|
||||
float m_frontRollAngle; // 前悬挂侧倾角
|
||||
float m_rearRollAngle; // 后悬挂侧倾角
|
||||
float m_chassisYaw; // 底盘偏航角 (rad) — 可用于判断侧滑
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. 其余包简介
|
||||
|
||||
| 包 | 关键内容 |
|
||||
|---|---|
|
||||
| **Final Classification (8)** | 最终排名、圈数、积分、最佳圈速、轮胎使用记录 |
|
||||
| **Lobby Info (9)** | 多人游戏大厅的玩家信息 |
|
||||
| **Session History (11)** | 每辆车的全部圈速历史 + 轮胎使用历史(最多 100 圈) |
|
||||
| **Tyre Sets (12)** | 轮胎组详情:配方、磨损度、可用性、圈速差等 |
|
||||
| **Time Trial (14)** | 计时赛专属:玩家最佳、个人最佳、对手数据 |
|
||||
|
||||
---
|
||||
|
||||
## 🔒 隐私:Your Telemetry 设置
|
||||
|
||||
当玩家设置 `Your Telemetry = Restricted` 时,以下数据对其他玩家**归零**:
|
||||
- **Car Status**:燃油、ERS 相关数据
|
||||
- **Car Damage**:所有损伤数据
|
||||
- **Tyre Sets**:全部轮胎组数据
|
||||
|
||||
---
|
||||
|
||||
## 🆕 F1 24 相比 F1 23 的主要变化
|
||||
|
||||
1. 会话包中新增模拟/规则选项
|
||||
2. 新增安全车事件详情(`SCAR`)
|
||||
3. 大厅和参与者包中新增在线名称、Your Telemetry、F1 World Tech Level
|
||||
4. 圈速包中新增与前车/领先者的分钟差距字段
|
||||
5. 最终排名包改为结果界面每 5 秒重复发送
|
||||
6. 新增 **Time Trial 包**(ID=14)
|
||||
7. Motion Ex 包新增底盘高度(plank height)、悬挂侧倾角、底盘偏航角
|
||||
8. 新增碰撞事件(`COLL`)
|
||||
9. 新增周末赛程结构(`m_weekendStructure`)
|
||||
10. 天气预测数组从 56 扩展到 64
|
||||
11. 圈速包新增速度陷阱数据
|
||||
12. Car Setup 包新增发动机制动参数 + 下次进站前翼值
|
||||
+1
-11
@@ -238,12 +238,6 @@
|
||||
- **FH5**:约 74 个字段(比 FH6 少 CarGroup/SmashableVelDiff/SmashableMass),总计 **约 311 字节**
|
||||
- **FH4**:和 FH5 基本一致
|
||||
|
||||
好问题!您确实很难在游戏中见到 P 档和 N 档,因为正常驾驶时它们几乎不出现。让我查一下社区中关于这个细节的讨论。
|
||||
|
||||
让我搜索更详细的信息。让我再搜一下 GitHub 上关于这个问题的实际讨论。让我直接看看社区中资深开发者对 Gear 字段的完整说明。
|
||||
|
||||
根据搜索结果和实际代码分析,以下是关于 **N 档(Neutral)** 和 **P 档(Park)** 在 FH4/FH5/FH6 UDP 数据包中的完整定义:
|
||||
|
||||
---
|
||||
|
||||
## Gear 字段的完整取值范围
|
||||
@@ -259,11 +253,7 @@
|
||||
| **21** | **P 档(Park,驻车档)** | 赛车完全静止时极罕见触发 |
|
||||
| **22+** | 理论上可能更高(如 D 档标记) | 极少见 |
|
||||
|
||||
### 关键发现:P 档 = 21
|
||||
|
||||
社区中一位资深开发者(*0x20F*,Forza Telemetry 核心开发者)明确指出:
|
||||
|
||||
> **Gear 21 = P (Park)**
|
||||
### Gear 值说明
|
||||
|
||||
这个值在正常游戏中几乎不会出现,因为《极限竞速:地平线》本质上没有"驻车"机制——汽车要么在跑,要么完全静止时引擎仍在空档(Gear=0)。但极少数情况下(如某些特殊漂移状态下),游戏引擎会短暂报告 Gear=21。
|
||||
|
||||
|
||||
@@ -0,0 +1,752 @@
|
||||
|
||||
|
||||
# 📡 iRacing Telemetry API(IRSDK)完整规范
|
||||
|
||||
## 一、概述
|
||||
|
||||
iRacing 的遥测系统通过 **共享内存映射文件(Memory-Mapped File)** 工作,所有数据是**只读**的。API 版本当前为 **IRSDK_VER = 2**。
|
||||
|
||||
### 三种数据通道
|
||||
|
||||
| 通道 | 形式 | 频率 | 说明 |
|
||||
|---|---|---|---|
|
||||
| **Live Telemetry** | 共享内存 `Local\IRSDKMemMapFileName` | 60Hz(或 360Hz) | 实时变量数据,三缓冲机制 |
|
||||
| **Session String** | 共享内存中的 YAML 字符串 | 变更时更新 | 会话元数据(赛道、车手、天气等) |
|
||||
| **Disk Telemetry (.ibt)** | 二进制文件 | 60Hz | 按 `Alt+L` 录制,结构等同于 Live 数据 |
|
||||
|
||||
### 启用遥测
|
||||
|
||||
在 iRacing 的 `app.ini` 中确认:
|
||||
|
||||
```ini
|
||||
[External Telemetry]
|
||||
enableTelemetry=1 # 启用遥测
|
||||
enableIRSDKTelemetry=1 # 启用 IRSDK
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、核心架构
|
||||
|
||||
### 共享内存布局
|
||||
|
||||
```
|
||||
[irsdk_header]
|
||||
↓
|
||||
[irsdk_varHeader[0] ... irsdk_varHeader[numVars-1]]
|
||||
↓
|
||||
[Session Info YAML String]
|
||||
↓
|
||||
[irsdk_varBuf[0]] → [Telemetry Data Line 0]
|
||||
[irsdk_varBuf[1]] → [Telemetry Data Line 1]
|
||||
[irsdk_varBuf[2]] → [Telemetry Data Line 2]
|
||||
[irsdk_varBuf[3]] → [Telemetry Data Line 3] (IRSDK_MAX_BUFS = 4)
|
||||
```
|
||||
|
||||
- **三缓冲**:模拟器写满一个缓冲区后立即切到下一个,给客户端 16ms+ 窗口来读取
|
||||
- **同步事件**:`Local\IRSDKDataValidEvent` — 新数据就绪时触发
|
||||
|
||||
### 字节序与编码
|
||||
|
||||
- **字节序**:Little Endian
|
||||
- **YAML 编码**:UTF-8
|
||||
- **对齐**:16 字节对齐
|
||||
- **变量名最大长**:`IRSDK_MAX_STRING = 32`
|
||||
- **描述最大长**:`IRSDK_MAX_DESC = 64`
|
||||
|
||||
---
|
||||
|
||||
## 三、数据结构
|
||||
|
||||
### 1. irsdk_header(主头部)
|
||||
|
||||
```c
|
||||
struct irsdk_header {
|
||||
int ver; // API 版本 = 2
|
||||
int status; // 位字段 (irsdk_stConnected = 1)
|
||||
int tickRate; // tick 频率 (60 或 360)
|
||||
|
||||
// 会话信息(变更时更新)
|
||||
int sessionInfoUpdate; // 会话信息变更计数
|
||||
int sessionInfoLen; // YAML 字符串长度(字节)
|
||||
int sessionInfoOffset; // 距头部的偏移
|
||||
|
||||
// 遥测变量
|
||||
int numVars; // 变量数量
|
||||
int varHeaderOffset; // varHeader 数组偏移
|
||||
int numBuf; // 缓冲区数量(4)
|
||||
int bufLen; // 每行字节数
|
||||
int pad1[2]; // 16 字节对齐
|
||||
|
||||
irsdk_varBuf varBuf[IRSDK_MAX_BUFS]; // 4 个缓冲区
|
||||
};
|
||||
```
|
||||
|
||||
### 2. irsdk_varHeader(变量描述头)
|
||||
|
||||
```c
|
||||
struct irsdk_varHeader {
|
||||
int type; // irsdk_VarType 枚举
|
||||
int offset; // 在缓冲区行中的偏移
|
||||
int count; // 数组元素数(标量 = 1)
|
||||
bool countAsTime;
|
||||
char pad[3]; // 16 字节对齐
|
||||
|
||||
char name[IRSDK_MAX_STRING]; // 变量名,如 "Speed"
|
||||
char desc[IRSDK_MAX_DESC]; // 描述,如 "GPS vehicle speed"
|
||||
char unit[IRSDK_MAX_STRING]; // 单位,如 "m/s"
|
||||
};
|
||||
```
|
||||
|
||||
### 3. irsdk_varBuf(数据缓冲区)
|
||||
|
||||
```c
|
||||
struct irsdk_varBuf {
|
||||
int tickCount; // 检测数据更新
|
||||
int bufOffset; // 距头部的偏移
|
||||
int pad[2]; // 16 字节对齐
|
||||
};
|
||||
```
|
||||
|
||||
### 4. irsdk_VarType(变量类型)
|
||||
|
||||
| 枚举值 | 类型 | 字节数 |
|
||||
|---|---|---|
|
||||
| `irsdk_char = 0` | char | 1 |
|
||||
| `irsdk_bool` | bool | 1 |
|
||||
| `irsdk_int` | int32 | 4 |
|
||||
| `irsdk_bitField` | bitfield (int32) | 4 |
|
||||
| `irsdk_float` | float32 | 4 |
|
||||
| `irsdk_double` | float64 | 8 |
|
||||
|
||||
### 5. irsdk_diskSubHeader(仅 .ibt 文件)
|
||||
|
||||
```c
|
||||
struct irsdk_diskSubHeader {
|
||||
time_t sessionStartDate;
|
||||
double sessionStartTime;
|
||||
double sessionEndTime;
|
||||
int sessionLapCount;
|
||||
int sessionRecordCount;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、Session String(YAML 结构)
|
||||
|
||||
YAML 字符串通过 `irsdk_getSessionInfoStr()` 获取,包含以下顶级节点:
|
||||
|
||||
### WeekendInfo
|
||||
|
||||
```yaml
|
||||
WeekendInfo:
|
||||
TrackName: "suzuka"
|
||||
TrackID: 123
|
||||
TrackLength: "5807 m"
|
||||
TrackDisplayName: "Suzuka International Racing Course"
|
||||
TrackCity: "Suzuka"
|
||||
TrackCountry: "JPN"
|
||||
TrackNumTurns: 18
|
||||
TrackPitSpeedLimit: "80 kph"
|
||||
TrackType: "road"
|
||||
TrackWeatherType: "Dynamic"
|
||||
TrackSkies: "Partly Cloudy"
|
||||
TrackSurfaceTemp: "38 C"
|
||||
TrackAirTemp: "25 C"
|
||||
TrackWindVel: "3 m/s"
|
||||
TrackWindDir: "180 deg"
|
||||
TrackRelativeHumidity: "55 %"
|
||||
TrackFogLevel: "0 %"
|
||||
SeriesID: 100
|
||||
SeasonID: 2024
|
||||
SessionID: 456
|
||||
SubSessionID: 789
|
||||
LeagueID: 0
|
||||
Official: 1
|
||||
RaceWeek: 5
|
||||
EventType: "Race"
|
||||
Category: "Road"
|
||||
SimMode: "full"
|
||||
TeamRacing: 0
|
||||
MinDrivers: 6
|
||||
MaxDrivers: 26
|
||||
NumCarClasses: 1
|
||||
NumCarTypes: 3
|
||||
WeekendOptions:
|
||||
NumStarters: 20
|
||||
StartingGrid: "2x2"
|
||||
QualifyScoring: "single"
|
||||
CourseCautions: "full"
|
||||
StandingStart: 0
|
||||
Restarts: "singleFile"
|
||||
WeatherType: "Dynamic"
|
||||
Skies: "Partly Cloudy"
|
||||
WindDirection: "180 deg"
|
||||
WindSpeed: "3 m/s"
|
||||
WeatherTemp: "25 C"
|
||||
RelativeHumidity: "55 %"
|
||||
FogLevel: "0 %"
|
||||
Unofficial: 0
|
||||
IsFixedSetup: 0
|
||||
HardcoreLevel: 1
|
||||
```
|
||||
|
||||
### SessionInfo
|
||||
|
||||
```yaml
|
||||
SessionInfo:
|
||||
Sessions:
|
||||
- SessionNum: 0
|
||||
SessionLaps: "unlimited"
|
||||
SessionTime: "30 min"
|
||||
SessionType: "Practice"
|
||||
ResultsPositions: []
|
||||
ResultsFastestLap: []
|
||||
- SessionNum: 1
|
||||
SessionLaps: "2 laps"
|
||||
SessionTime: "10 min"
|
||||
SessionType: "Lone Qualify"
|
||||
- SessionNum: 2
|
||||
SessionLaps: "12 laps"
|
||||
SessionTime: "30 min"
|
||||
SessionType: "Race"
|
||||
```
|
||||
|
||||
### DriverInfo
|
||||
|
||||
```yaml
|
||||
DriverInfo:
|
||||
DriverCarIdx: 0
|
||||
DriverCarIdleRPM: 3500.0
|
||||
DriverCarRedLine: 9500.0
|
||||
DriverCarSLFirstRPM: 8500.0
|
||||
DriverCarSLShiftRPM: 9000.0
|
||||
DriverCarSLLastRPM: 9500.0
|
||||
DriverCarSLBlinkRPM: 9200.0
|
||||
Drivers:
|
||||
- CarIdx: 0
|
||||
UserName: "John Doe"
|
||||
AbbrevName: "J. Doe"
|
||||
Initials: "JD"
|
||||
CarNumber: "23"
|
||||
CarPath: "mercedesamggt3"
|
||||
CarClassID: 1
|
||||
IRating: 2500
|
||||
LicString: "A 4.99"
|
||||
IsSpectator: 0
|
||||
```
|
||||
|
||||
### CameraInfo
|
||||
|
||||
```yaml
|
||||
CameraInfo:
|
||||
Groups:
|
||||
- GroupNum: 1
|
||||
GroupName: "TV1"
|
||||
Cameras:
|
||||
- CameraNum: 1
|
||||
CameraName: "TV1_Turn1"
|
||||
```
|
||||
|
||||
### SplitTimeInfo
|
||||
|
||||
```yaml
|
||||
SplitTimeInfo:
|
||||
Sectors:
|
||||
- SectorNum: 0
|
||||
SectorStartPct: 0.0
|
||||
- SectorNum: 1
|
||||
SectorStartPct: 0.35
|
||||
- SectorNum: 2
|
||||
SectorStartPct: 0.70
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、核心遥测变量列表(完整)
|
||||
|
||||
### 🏎️ 玩家车辆控制
|
||||
|
||||
| 变量名 | 类型 | 单位 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `SteeringWheelAngle` | float | rad | 方向盘转角 |
|
||||
| `Throttle` | float | % (0-1) | 油门(处理后的) |
|
||||
| `Brake` | float | % (0-1) | 刹车(处理后的) |
|
||||
| `Clutch` | float | % (0-1) | 离合 |
|
||||
| `Gear` | int | — | -1=R,0=N,1~n=前进档 |
|
||||
| `RPM` | float | revs/min | 引擎转速 |
|
||||
| `ThrottleRaw` | float | % (0-1) | 原始油门输入 |
|
||||
| `BrakeRaw` | float | % (0-1) | 原始刹车输入 |
|
||||
| `HandbrakeRaw` | float | % (0-1) | 原始手刹输入 |
|
||||
| `Speed` | float | m/s | GPS 车速 |
|
||||
| `OnPitRoad` | bool | — | 是否在维修区通道 |
|
||||
|
||||
### 📐 位置与运动
|
||||
|
||||
| 变量名 | 类型 | 单位 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `Yaw` | float | rad | 偏航角 |
|
||||
| `YawNorth` | float | rad | 相对北方的偏航角 |
|
||||
| `Pitch` | float | rad | 俯仰角 |
|
||||
| `Roll` | float | rad | 侧倾角 |
|
||||
| `YawRate` / `YawRate_ST` | float | rad/s | 偏航率(ST=子tick精度) |
|
||||
| `PitchRate` / `PitchRate_ST` | float | rad/s | 俯仰率 |
|
||||
| `RollRate` / `RollRate_ST` | float | rad/s | 侧倾率 |
|
||||
| `VelocityX` / `VelocityX_ST` | float | m/s | X 方向速度 |
|
||||
| `VelocityY` / `VelocityY_ST` | float | m/s | Y 方向速度 |
|
||||
| `VelocityZ` / `VelocityZ_ST` | float | m/s | Z 方向速度 |
|
||||
| `LatAccel` / `LatAccel_ST` | float | m/s² | 横向加速度 |
|
||||
| `LongAccel` / `LongAccel_ST` | float | m/s² | 纵向加速度 |
|
||||
| `VertAccel` / `VertAccel_ST` | float | m/s² | 垂向加速度 |
|
||||
|
||||
> `_ST` 后缀变量提供 **子 tick 精度**,适合运动平台等精密场景。
|
||||
|
||||
### ⏱️ 圈速与计时
|
||||
|
||||
| 变量名 | 类型 | 单位 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `Lap` | int | — | 当前圈号 |
|
||||
| `LapCompleted` | int | — | 已完成圈数 |
|
||||
| `LapDist` | float | m | 当前圈已跑距离 |
|
||||
| `LapDistPct` | float | % | 当前圈完成百分比 |
|
||||
| `RaceLaps` | int | — | 比赛中已完成圈数 |
|
||||
| `LapBestLap` | int | — | 最佳圈号 |
|
||||
| `LapBestLapTime` | float | s | 最佳圈速 |
|
||||
| `LapLastLapTime` | float | s | 上一圈用时 |
|
||||
| `LapCurrentLapTime` | float | s | 当前圈用时(F3 黑盒) |
|
||||
| `LapBestNLapLap` | int | — | 最佳 N 圈均值的最后一圈 |
|
||||
| `LapBestNLapTime` | float | s | 最佳 N 圈平均时间 |
|
||||
| `LapLastNLapTime` | float | s | 最近 N 圈平均时间 |
|
||||
| `LapLasNLapSeq` | int | — | 连续干净圈数 |
|
||||
|
||||
### 🎯 Delta 时间
|
||||
|
||||
| 变量名 | 类型 | 单位 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `LapDeltaToBestLap` | float | s | 与个人最佳圈的差距 |
|
||||
| `LapDeltaToBestLap_DD` | float | s/s | 差距变化率 |
|
||||
| `LapDeltaToBestLap_OK` | bool | — | delta 数据是否有效 |
|
||||
| `LapDeltaToOptimalLap` | float | s | 与理论最佳圈的差距 |
|
||||
| `LapDeltaToOptimalLap_DD` | float | s/s | 差距变化率 |
|
||||
| `LapDeltaToOptimalLap_OK` | bool | — | 是否有效 |
|
||||
| `LapDeltaToSessionBestLap` | float | s | 与全场最佳圈差距 |
|
||||
| `LapDeltaToSessionBestLap_DD` | float | s/s | 差距变化率 |
|
||||
| `LapDeltaToSessionBestLap_OK` | bool | — | 是否有效 |
|
||||
| `LapDeltaToSessionOptimalLap` | float | s | 与全场理论最佳差距 |
|
||||
| `LapDeltaToSessionLastlLap` | float | s | 与全场上一圈差距 |
|
||||
|
||||
### ⛽ 引擎 & 燃油
|
||||
|
||||
| 变量名 | 类型 | 单位 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `FuelLevel` | float | L | 剩余油量(升) |
|
||||
| `FuelLevelPct` | float | % | 剩余油量百分比 |
|
||||
| `FuelPress` | float | bar | 燃油压力 |
|
||||
| `FuelUsePerHour` | float | kg/h | 瞬时油耗率 |
|
||||
| `ManifoldPress` | float | bar | 进气歧管压力 |
|
||||
| `OilLevel` | float | L | 机油液位 |
|
||||
| `OilPress` | float | bar | 机油压力 |
|
||||
| `OilTemp` | float | °C | 机油温度 |
|
||||
| `WaterLevel` | float | L | 冷却液液位 |
|
||||
| `WaterTemp` | float | °C | 冷却液温度 |
|
||||
| `Voltage` | float | V | 电压 |
|
||||
| `EngineWarnings` | bitfield | — | 引擎警告灯(见下文) |
|
||||
| `BrakeABSactive` | bool | — | ABS 是否激活 |
|
||||
|
||||
### 🛞 轮胎数据(每轮 LF/RF/LR/RR)
|
||||
|
||||
设 `X` ∈ {LF, RF, LR, RR}:
|
||||
|
||||
| 变量名 | 类型 | 单位 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `XcoldPressure` | float | kPa | 冷胎压 |
|
||||
| `Xpressure` | float | kPa | 当前胎压 |
|
||||
| `XtempCL/M/R` | float | °C | 胎面左/中/右温度 |
|
||||
| `XwearL/M/R` | float | % | 胎面左/中/右磨损 |
|
||||
| `XbrakeLinePress` | float | bar | 刹车管压力 |
|
||||
| `XshockDefl` | float | mm | 悬挂压缩量 |
|
||||
| `XshockVel` | float | mm/s | 悬挂速度 |
|
||||
| `XrideHeight` | float | mm | 行驶高度 |
|
||||
| `Xspeed` | float | rad/s | 轮速 |
|
||||
|
||||
### 🎛️ 力反馈
|
||||
|
||||
| 变量名 | 类型 | 单位 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `SteeringWheelTorque` | float | N·m | 转向轴输出扭矩 |
|
||||
| `SteeringWheelTorque_ST` | float | N·m | 转向轴扭矩(子tick) |
|
||||
| `SteeringWheelPctTorque` | float | % | FFB 扭矩百分比(无符号) |
|
||||
| `SteeringWheelPctTorqueSign` | float | % | FFB 扭矩百分比(有符号) |
|
||||
| `SteeringWheelPctTorqueSignStops` | float | % | FFB 扭矩百分比(含止挡) |
|
||||
| `SteeringWheelPctDamper` | float | % | FFB 阻尼百分比 |
|
||||
| `SteeringWheelAngleMax` | float | rad | 方向盘最大转角 |
|
||||
| `SteeringWheelPeakForceNm` | float | N·m | FFB 峰值力映射 |
|
||||
| `SteeringWheelMaxForceNm` | float | N·m | 最大力 |
|
||||
| `SteeringWheelUseLinear` | bool | — | 是否线性模式 |
|
||||
|
||||
### 🏁 会话状态
|
||||
|
||||
| 变量名 | 类型 | 单位 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `SessionTime` | double | s | 当前会话已过时间 |
|
||||
| `SessionTimeRemain` | double | s | 会话剩余时间 |
|
||||
| `SessionTimeTotal` | double | s | 会话总时间 |
|
||||
| `SessionLapsRemain` | int | — | 剩余圈数 |
|
||||
| `SessionLapsRemainEx` | int | — | 扩展剩余圈数 |
|
||||
| `SessionLapsTotal` | int | — | 总圈数 |
|
||||
| `SessionNum` | int | — | 会话序号 |
|
||||
| `SessionState` | int | — | 会话状态(见枚举) |
|
||||
| `SessionUniqueID` | int | — | 会话唯一 ID |
|
||||
| `SessionFlags` | bitfield | — | 旗语(见下文) |
|
||||
| `SessionTick` | int | — | Tick 计数 |
|
||||
| `SessionTimeOfDay` | float | s | 赛道时间 |
|
||||
|
||||
### 🚩 旗语 & 状态
|
||||
|
||||
**SessionFlags** 位字段:
|
||||
|
||||
| 位标记 | 含义 |
|
||||
|---|---|
|
||||
| `0x00000001` | 🏁 方格旗 |
|
||||
| `0x00000002` | ⬜ 白旗 |
|
||||
| `0x00000004` | 🟩 绿旗 |
|
||||
| `0x00000008` | 🟨 黄旗 |
|
||||
| `0x00000010` | 🟥 红旗 |
|
||||
| `0x00000020` | 🟦 蓝旗 |
|
||||
| `0x00000040` | 💥 碎片 |
|
||||
| `0x00000100` | 🟨 黄旗挥动 |
|
||||
| `0x00000200` | 绿旗前最后一圈 |
|
||||
| `0x00004000` | ⚠️ Full Course Caution |
|
||||
| `0x00010000` | ⬛ 黑旗 |
|
||||
| `0x00020000` | ❌ DSQ |
|
||||
| `0x10000000` | 🔴 发车灯隐藏 |
|
||||
| `0x20000000` | 🔴 发车灯就绪 |
|
||||
| `0x40000000` | 🔴 发车灯 Set |
|
||||
| `0x80000000` | 🟢 发车灯 Go |
|
||||
|
||||
**EngineWarnings** 位字段:
|
||||
|
||||
| 位标记 | 含义 |
|
||||
|---|---|
|
||||
| `0x0001` | 水温警告 |
|
||||
| `0x0002` | 燃油压力警告 |
|
||||
| `0x0004` | 机油压力警告 |
|
||||
| `0x0008` | 引擎熄火 |
|
||||
| `0x0010` | Pit Speed Limiter |
|
||||
| `0x0020` | 转速限制器激活 |
|
||||
| `0x0040` | 油温警告 |
|
||||
|
||||
### 🚗 多车数据(CarIdx 数组,最多 64 辆车)
|
||||
|
||||
| 变量名 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `CarIdxLap` | int[] | 每辆车的圈数 |
|
||||
| `CarIdxLapCompleted` | int[] | 每辆车已完成圈数 |
|
||||
| `CarIdxLapDistPct` | float[] | 每辆车的位置百分比 |
|
||||
| `CarIdxPosition` | int[] | 每辆车的排名 |
|
||||
| `CarIdxClassPosition` | int[] | 组别排名 |
|
||||
| `CarIdxClass` | int[] | 组别 ID |
|
||||
| `CarIdxF2Time` | float[] | 与领先者的差距(或最快圈速) |
|
||||
| `CarIdxEstTime` | float[] | 预计到达当前位置的时间 |
|
||||
| `CarIdxLastLapTime` | float[] | 上一圈时间 |
|
||||
| `CarIdxBestLapTime` | float[] | 最佳圈时间 |
|
||||
| `CarIdxBestLapNum` | int[] | 最佳圈号 |
|
||||
| `CarIdxSteer` | float[] | 方向盘转角 |
|
||||
| `CarIdxRPM` | float[] | 引擎转速 |
|
||||
| `CarIdxGear` | int[] | 档位 |
|
||||
| `CarIdxOnPitRoad` | bool[] | 是否在维修区 |
|
||||
| `CarIdxTrackSurface` | int[] | 赛道位置类型 |
|
||||
| `CarIdxTrackSurfaceMaterial` | int[] | 路面材质 |
|
||||
| `CarIdxTireCompound` | int[] | 轮胎配方 |
|
||||
| `CarIdxP2P_Status` | bool[] | Push-to-Pass 状态 |
|
||||
| `CarIdxP2P_Count` | int[] | Push-to-Pass 剩余次数 |
|
||||
|
||||
### 🛑 进站 & 维修
|
||||
|
||||
| 变量名 | 类型 | 单位 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `PitsOpen` | bool | — | 维修区是否开放 |
|
||||
| `PitstopActive` | bool | — | 正在进站 |
|
||||
| `PitRepairLeft` | float | s | 强制维修剩余时间 |
|
||||
| `PitOptRepairLeft` | float | s | 可选维修剩余时间 |
|
||||
| `PitSvFlags` | bitfield | — | 进站服务勾选 |
|
||||
| `PitSvLFP/RFP/LRP/RRP` | float | kPa | 指定胎压 |
|
||||
| `PitSvFuel` | float | L | 指定加油量 |
|
||||
| `PitSvTireCompound` | int | — | 指定轮胎配方 |
|
||||
| `FastRepairUsed` | int | — | 已用快速维修次数 |
|
||||
| `FastRepairAvailable` | int | — | 可用快速维修次数 |
|
||||
| `PlayerCarInPitStall` | bool | — | 玩家在维修位 |
|
||||
| `PlayerCarPitSvStatus` | int | — | 进站服务状态 |
|
||||
|
||||
**PitSvFlags** 位字段:
|
||||
|
||||
| 位标记 | 含义 |
|
||||
|---|---|
|
||||
| `0x0001` | 左前换胎 |
|
||||
| `0x0002` | 右前换胎 |
|
||||
| `0x0004` | 左后换胎 |
|
||||
| `0x0008` | 右后换胎 |
|
||||
| `0x0010` | 加油 |
|
||||
| `0x0020` | 撕膜 |
|
||||
| `0x0040` | 快速维修 |
|
||||
|
||||
### 🌦️ 天气 & 环境
|
||||
|
||||
| 变量名 | 类型 | 单位 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `TrackTemp` | float | °C | 赛道温度(起终点) |
|
||||
| `TrackTempCrew` | float | °C | 赛道温度(Pit 区) |
|
||||
| `TrackWetness` | float | — | 赛道湿度级别 |
|
||||
| `AirTemp` | float | °C | 空气温度 |
|
||||
| `AirDensity` | float | kg/m³ | 空气密度 |
|
||||
| `AirPressure` | float | Hg | 气压 |
|
||||
| `WindVel` | float | m/s | 风速 |
|
||||
| `WindDir` | float | rad | 风向 |
|
||||
| `RelativeHumidity` | float | % | 相对湿度 |
|
||||
| `FogLevel` | float | % | 雾级别 |
|
||||
| `WeatherType` | int | — | 0=恒定,1=动态 |
|
||||
| `Skies` | int | — | 0=晴,1=少云,2=多云,3=阴 |
|
||||
|
||||
### 🎥 回放 & 系统
|
||||
|
||||
| 变量名 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `IsReplayPlaying` | bool | 是否回放中 |
|
||||
| `ReplayFrameNum` | int | 回放当前帧号 |
|
||||
| `ReplayFrameNumEnd` | int | 回放末尾帧号 |
|
||||
| `ReplayPlaySpeed` | int | 回放速度 |
|
||||
| `ReplayPlaySlowMotion` | bool | 是否慢动作 |
|
||||
| `ReplaySessionTime` | double | 回放中的会话时间 |
|
||||
| `ReplaySessionNum` | int | 回放会话序号 |
|
||||
| `IsOnTrack` | bool | 玩家是否在赛道上 |
|
||||
| `IsInGarage` | bool | 是否在车库 |
|
||||
| `IsDiskLoggingEnabled` | bool | 磁盘遥测是否开启 |
|
||||
| `IsDiskLoggingActive` | bool | 是否正在录制 .ibt |
|
||||
|
||||
### 👥 车手变更(Team Racing)
|
||||
|
||||
| 变量名 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `DCLapStatus` | int | Driver Change 圈数状态 |
|
||||
| `DCDriversSoFar` | int | 已驾驶过的车手数 |
|
||||
|
||||
### 📷 摄像机
|
||||
|
||||
| 变量名 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `CamCarIdx` | int | 当前焦点车辆索引 |
|
||||
| `CamCameraNumber` | int | 当前摄像机编号 |
|
||||
| `CamGroupNumber` | int | 当前摄像机组编号 |
|
||||
| `CamCameraState` | bitfield | 摄像机状态位字段 |
|
||||
|
||||
---
|
||||
|
||||
## 六、重要枚举汇总
|
||||
|
||||
### SessionState
|
||||
|
||||
| 值 | 状态 |
|
||||
|---|---|
|
||||
| 0 | StateInvalid |
|
||||
| 1 | StateGetInCar(上车中) |
|
||||
| 2 | StateWarmup(暖胎圈) |
|
||||
| 3 | StateParadeLaps(编队圈) |
|
||||
| 4 | **StateRacing**(正赛中) |
|
||||
| 5 | StateCheckered(方格旗) |
|
||||
| 6 | StateCoolDown(冷却圈) |
|
||||
|
||||
### TrkLoc(赛道位置)
|
||||
|
||||
| 值 | 含义 |
|
||||
|---|---|
|
||||
| -1 | NotInWorld |
|
||||
| 0 | OffTrack |
|
||||
| 1 | InPitStall(维修区) |
|
||||
| 2 | AproachingPits |
|
||||
| 3 | OnTrack |
|
||||
|
||||
### TrkSurf(路面材质)
|
||||
|
||||
| 值 | 材质 |
|
||||
|---|---|
|
||||
| 0 | Undefined |
|
||||
| 1-4 | Asphalt |
|
||||
| 5-6 | Concrete |
|
||||
| 7-8 | Racing Dirt |
|
||||
| 9-10 | Paint |
|
||||
| 11-14 | Rumble Strip(路肩) |
|
||||
| 15-18 | Grass |
|
||||
| 19-22 | Dirt |
|
||||
| 23 | Sand |
|
||||
| 24-25 | Gravel |
|
||||
| 26 | Grasscrete |
|
||||
| 27 | Astroturf |
|
||||
|
||||
### TrackWetness(赛道湿度)
|
||||
|
||||
| 值 | 级别 |
|
||||
|---|---|
|
||||
| 0 | Unknown |
|
||||
| 1 | Dry |
|
||||
| 2 | Mostly Dry |
|
||||
| 3 | Very Lightly Wet |
|
||||
| 4 | Lightly Wet |
|
||||
| 5 | Moderately Wet |
|
||||
| 6 | Very Wet |
|
||||
| 7 | Extremely Wet |
|
||||
|
||||
### PaceMode
|
||||
|
||||
| 值 | 模式 |
|
||||
|---|---|
|
||||
| 0 | Single File Start |
|
||||
| 1 | Double File Start |
|
||||
| 2 | Single File Restart |
|
||||
| 3 | Double File Restart |
|
||||
| 4 | Not Pacing |
|
||||
|
||||
### CarLeftRight
|
||||
|
||||
| 值 | 含义 |
|
||||
|---|---|
|
||||
| 0 | Off |
|
||||
| 1 | Clear(两侧无车) |
|
||||
| 2 | Car Left |
|
||||
| 3 | Car Right |
|
||||
| 4 | Cars on Both Sides |
|
||||
| 5 | 2 Cars Left |
|
||||
| 6 | 2 Cars Right |
|
||||
|
||||
---
|
||||
|
||||
## 七、SDK 客户端 API(C 语言接口)
|
||||
|
||||
```c
|
||||
// 初始化 / 关闭
|
||||
bool irsdk_startup();
|
||||
void irsdk_shutdown();
|
||||
|
||||
// 连接检测
|
||||
bool irsdk_isConnected();
|
||||
|
||||
// 获取头部
|
||||
const irsdk_header* irsdk_getHeader();
|
||||
|
||||
// 等待新数据并复制到本地缓冲区(推荐方式)
|
||||
bool irsdk_waitForDataReady(int timeOut, char *data);
|
||||
|
||||
// 获取会话 YAML 字符串
|
||||
const char* irsdk_getSessionInfoStr();
|
||||
int irsdk_getSessionInfoStrUpdate(); // 检测会话信息更新
|
||||
|
||||
// 获取变量头
|
||||
const irsdk_varHeader* irsdk_getVarHeaderPtr();
|
||||
const irsdk_varHeader* irsdk_getVarHeaderEntry(int index);
|
||||
|
||||
// 按名称查找变量
|
||||
int irsdk_varNameToIndex(const char *name);
|
||||
int irsdk_varNameToOffset(const char *name);
|
||||
|
||||
// 获取指定缓冲区的数据
|
||||
const char* irsdk_getData(int index);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、Broadcast Messages(远程控制)
|
||||
|
||||
通过 Windows 消息 `IRSDK_BROADCASTMSG` 发送控制命令:
|
||||
|
||||
### 摄像机控制
|
||||
|
||||
```c
|
||||
// 切换摄像机:按位置
|
||||
irsdk_broadcastMsg(irsdk_BroadcastCamSwitchPos, carPosition, group, camera);
|
||||
|
||||
// 切换摄像机:按车号
|
||||
irsdk_broadcastMsg(irsdk_BroadcastCamSwitchNum, driverNum, group, camera);
|
||||
|
||||
// 设置摄像机状态
|
||||
irsdk_broadcastMsg(irsdk_BroadcastCamSetState, irsdk_CameraState, 0, 0);
|
||||
```
|
||||
|
||||
### 回放控制
|
||||
|
||||
```c
|
||||
// 设置播放速度
|
||||
irsdk_broadcastMsg(irsdk_BroadcastReplaySetPlaySpeed, speed, slowMotion, 0);
|
||||
|
||||
// 跳转位置
|
||||
irsdk_broadcastMsg(irskd_BroadcastReplaySetPlayPosition,
|
||||
irsdk_RpyPos_Begin, frameHigh, frameLow);
|
||||
|
||||
// 搜索事件
|
||||
irsdk_broadcastMsg(irsdk_BroadcastReplaySearch,
|
||||
irsdk_RpySrch_NextLap, 0, 0);
|
||||
|
||||
// 搜索会话时间
|
||||
irsdk_broadcastMsg(irsdk_BroadcastReplaySearchSessionTime,
|
||||
sessionNum, sessionTimeMS_high, sessionTimeMS_low);
|
||||
```
|
||||
|
||||
### 进站控制
|
||||
|
||||
```c
|
||||
// 清空所有勾选
|
||||
irsdk_broadcastMsg(irsdk_BroadcastPitCommand, irsdk_PitCommand_Clear, 0);
|
||||
|
||||
// 加油(0 = 使用默认值)
|
||||
irsdk_broadcastMsg(irsdk_BroadcastPitCommand, irsdk_PitCommand_Fuel, addLiters);
|
||||
|
||||
// 换左前胎(指定胎压 kPa,0 = 默认)
|
||||
irsdk_broadcastMsg(irsdk_BroadcastPitCommand, irsdk_PitCommand_LF, pressureKpa);
|
||||
|
||||
// 换胎配方
|
||||
irsdk_broadcastMsg(irsdk_BroadcastPitCommand, irsdk_PitCommand_TC, compound);
|
||||
|
||||
// 快速维修
|
||||
irsdk_broadcastMsg(irsdk_BroadcastPitCommand, irsdk_PitCommand_FR, 0);
|
||||
```
|
||||
|
||||
### 遥测录制控制
|
||||
|
||||
```c
|
||||
irsdk_broadcastMsg(irsdk_BroadcastTelemCommand, irsdk_TelemCommand_Start, 0, 0);
|
||||
irsdk_broadcastMsg(irsdk_BroadcastTelemCommand, irsdk_TelemCommand_Stop, 0, 0);
|
||||
irsdk_broadcastMsg(irsdk_BroadcastTelemCommand, irsdk_TelemCommand_Restart, 0, 0);
|
||||
```
|
||||
|
||||
### 聊天宏
|
||||
|
||||
```c
|
||||
irsdk_broadcastMsg(irsdk_BroadcastChatComand, irsdk_ChatCommand_Macro, macroNum, 0);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、与 F1 24 / ACC 的对比
|
||||
|
||||
| 特性 | iRacing | F1 24 | ACC |
|
||||
|---|---|---|---|
|
||||
| **协议** | 共享内存 | UDP | UDP + 共享内存 |
|
||||
| **更新频率** | 60Hz(可 360Hz) | 最高 60Hz | 物理步进 |
|
||||
| **多车数据** | ✅ CarIdx 数组(≤64 辆车) | ✅ 22 辆车 | ✅ Broadcasting |
|
||||
| **回放数据** | ✅ .ibt 文件 | ❌ | ❌ |
|
||||
| **远程控制** | ✅ Broadcast Msg | ❌ | ✅ Camera/Focus |
|
||||
| **写入支持** | ✅ 进站/摄像机/遥测 | ❌ | ✅ Camera/HUD |
|
||||
| **变量动态性** | 每车/每会话不同 | 固定结构 | 固定结构 |
|
||||
| **子 Tick 精度** | ✅ `_ST` 后缀变量 | ❌ | ❌ |
|
||||
| **运动平台** | ✅ 完整扭矩/加速度 | ✅ Motion Ex | ✅ Shared Memory |
|
||||
|
||||
---
|
||||
|
||||
## 十、关键注意事项
|
||||
|
||||
1. **变量动态性**:iRacing 的变量列表不是固定的—不同车辆/会话中可能不同,必须先读取 `varHeader` 来确定可用的变量和偏移量
|
||||
2. **只读**:所有遥测数据只读,不能通过 SDK 修改车辆参数(只能通过 Broadcast Message 控制进站选项)
|
||||
3. **三缓冲**:务必在 `irsdk_waitForDataReady()` 返回后立即拷贝数据,不要直接引用共享内存
|
||||
4. **Session Info 更新**:通过 `sessionInfoUpdate` 计数器检测 YAML 字符串是否变化
|
||||
5. **.ibt 兼容**:磁盘遥测文件结构与 Live 相同,可用相同代码解析(多了 `irsdk_diskSubHeader`)
|
||||
6. **启动时序**:先调用 `irsdk_startup()` → 等待 `irsdk_isConnected()` → 等待 `SessionState >= StateRacing`
|
||||
7. **360Hz 模式**:部分车辆支持 360Hz 遥测,检查 `tickRate` 字段
|
||||
+5
-1
@@ -17,6 +17,8 @@ games/user/my_game/
|
||||
"id": "my_game",
|
||||
"name": "我的游戏",
|
||||
"parser_type": "forza",
|
||||
"protocol": "udp",
|
||||
"mode": "server",
|
||||
"description": "自定义游戏遥测",
|
||||
"author": "你的名字",
|
||||
"version": "1.0.0",
|
||||
@@ -29,10 +31,12 @@ games/user/my_game/
|
||||
| `id` | string | 是 | 唯一标识,= 文件夹名 |
|
||||
| `name` | string | 是 | 显示名称 |
|
||||
| `parser_type` | string | 是 | 解析器类型标签 |
|
||||
| `protocol` | string | 否 | 传输协议: `udp`(默认) / `tcp` |
|
||||
| `mode` | string | 否 | TCP模式: `server`(框架监听) / `client`(插件自连) |
|
||||
| `description` | string | 否 | 描述文字 |
|
||||
| `author` | string | 否 | 作者 |
|
||||
| `version` | string | 否 | 版本号 |
|
||||
| `default_port` | int | 否 | 默认 UDP 端口 (默认 20777) |
|
||||
| `default_port` | int | 否 | 默认端口 (UDP默认20777, TCP默认20778) |
|
||||
|
||||
## parser.py 规范
|
||||
|
||||
|
||||
@@ -8,6 +8,12 @@ def get_parser():
|
||||
|
||||
|
||||
class F124Parser:
|
||||
"""F1 24 UDP telemetry parser based on official EA SPORTS specification."""
|
||||
|
||||
# Packet header (29 bytes)
|
||||
HEADER_SIZE = 29
|
||||
CAR_TELEMETRY_SIZE = 43 # sizeof(CarTelemetryData) with padding
|
||||
|
||||
def game_id(self) -> str:
|
||||
return "f1_24"
|
||||
|
||||
@@ -15,22 +21,64 @@ class F124Parser:
|
||||
td = TelemetryData(game_id="f1_24", timestamp=time.time())
|
||||
td.raw = {"raw_hex": data.hex(), "length": len(data), "addr": f"{addr[0]}:{addr[1]}"}
|
||||
|
||||
if len(data) < 1289:
|
||||
if len(data) < 50:
|
||||
return td
|
||||
|
||||
try:
|
||||
td.speed_kmh = struct.unpack_from("<f", data, 37)[0]
|
||||
# Parse header
|
||||
packet_id = struct.unpack_from("<B", data, 6)[0]
|
||||
player_idx = struct.unpack_from("<B", data, 27)[0]
|
||||
td.raw["packet_id"] = packet_id
|
||||
td.raw["player_index"] = player_idx
|
||||
|
||||
if packet_id == 6: # Car Telemetry Packet (ID=6)
|
||||
offset = self.HEADER_SIZE + player_idx * self.CAR_TELEMETRY_SIZE
|
||||
|
||||
td.speed_kmh = struct.unpack_from("<H", data, offset)[0]
|
||||
td.speed_mph = td.speed_kmh * 0.621371
|
||||
td.rpm = struct.unpack_from("<H", data, 41)[0]
|
||||
td.max_rpm = struct.unpack_from("<H", data, 43)[0]
|
||||
td.gear = struct.unpack_from("<B", data, 46)[0] & 0x0F
|
||||
td.throttle = struct.unpack_from("<f", data, 47)[0]
|
||||
td.brake = struct.unpack_from("<f", data, 55)[0]
|
||||
td.steering = struct.unpack_from("<B", data, 45)[0] / 127.0
|
||||
td.lap_number = struct.unpack_from("<B", data, 262)[0]
|
||||
td.lap_time = struct.unpack_from("<f", data, 63)[0]
|
||||
td.best_lap = struct.unpack_from("<f", data, 268)[0]
|
||||
td.fuel = struct.unpack_from("<f", data, 51)[0]
|
||||
|
||||
td.throttle = struct.unpack_from("<f", data, offset + 4)[0]
|
||||
td.steering = struct.unpack_from("<f", data, offset + 8)[0]
|
||||
td.brake = struct.unpack_from("<f", data, offset + 12)[0]
|
||||
td.clutch = struct.unpack_from("<B", data, offset + 16)[0] / 100.0
|
||||
td.gear = struct.unpack_from("<b", data, offset + 17)[0]
|
||||
td.rpm = struct.unpack_from("<H", data, offset + 18)[0]
|
||||
td.raw["drs"] = struct.unpack_from("<B", data, offset + 20)[0]
|
||||
td.raw["rev_lights_percent"] = struct.unpack_from("<B", data, offset + 21)[0]
|
||||
td.raw["rev_lights_bit"] = struct.unpack_from("<H", data, offset + 22)[0]
|
||||
|
||||
for i in range(4):
|
||||
td.raw[f"tyre_surface_temp_{i}"] = struct.unpack_from("<B", data, offset + 30 + i)[0]
|
||||
td.raw[f"tyre_inner_temp_{i}"] = struct.unpack_from("<B", data, offset + 34 + i)[0]
|
||||
|
||||
td.raw["engine_temp"] = struct.unpack_from("<H", data, offset + 38)[0]
|
||||
|
||||
elif packet_id == 0: # Motion Packet (ID=0)
|
||||
offset = self.HEADER_SIZE + player_idx * 64 # sizeof(CarMotionData)
|
||||
|
||||
td.acceleration_x = struct.unpack_from("<f", data, offset + 28)[0]
|
||||
td.acceleration_y = struct.unpack_from("<f", data, offset + 32)[0]
|
||||
td.acceleration_z = struct.unpack_from("<f", data, offset + 36)[0]
|
||||
|
||||
elif packet_id == 2: # Lap Data Packet (ID=2)
|
||||
offset = self.HEADER_SIZE + player_idx * 24 # sizeof(LapData)
|
||||
|
||||
td.last_lap = struct.unpack_from("<f", data, offset)[0]
|
||||
td.lap_time = struct.unpack_from("<f", data, offset + 4)[0]
|
||||
td.best_lap = struct.unpack_from("<f", data, offset + 8)[0]
|
||||
|
||||
elif packet_id == 7: # Car Status Packet (ID=7)
|
||||
offset = self.HEADER_SIZE + player_idx * 41 # sizeof(CarStatusData)
|
||||
|
||||
td.raw["traction_control"] = struct.unpack_from("<B", data, offset)[0]
|
||||
td.raw["anti_lock_brakes"] = struct.unpack_from("<B", data, offset + 1)[0]
|
||||
td.fuel = struct.unpack_from("<f", data, offset + 2)[0] / 110.0 # normalized to 0-1
|
||||
if td.fuel > 1.0:
|
||||
td.fuel = min(1.0, td.fuel / 100.0)
|
||||
td.max_rpm = struct.unpack_from("<H", data, offset + 26)[0]
|
||||
td.raw["ers_deploy_mode"] = struct.unpack_from("<B", data, offset + 34)[0]
|
||||
td.raw["ers_store_energy"] = struct.unpack_from("<f", data, offset + 36)[0]
|
||||
|
||||
except struct.error:
|
||||
pass
|
||||
|
||||
|
||||
@@ -8,25 +8,13 @@ def get_parser():
|
||||
|
||||
|
||||
class IRacingParser:
|
||||
"""
|
||||
iRacing uses shared memory (IRSDK Memory-Mapped File), NOT UDP.
|
||||
This parser is a stub - it does NOT work with UDP input.
|
||||
Requires IRSDK mmap reader (not yet implemented).
|
||||
"""
|
||||
def game_id(self) -> str:
|
||||
return "iracing"
|
||||
|
||||
def parse(self, data: bytes, addr: tuple) -> TelemetryData:
|
||||
td = TelemetryData(game_id="iracing", timestamp=time.time())
|
||||
td.raw = {"raw_hex": data.hex(), "length": len(data), "addr": f"{addr[0]}:{addr[1]}"}
|
||||
|
||||
if len(data) < 100:
|
||||
return td
|
||||
|
||||
try:
|
||||
td.speed_mph = struct.unpack_from("<f", data, 36)[0]
|
||||
td.speed_kmh = td.speed_mph * 1.60934
|
||||
td.rpm = struct.unpack_from("<f", data, 48)[0]
|
||||
td.gear = struct.unpack_from("<i", data, 56)[0]
|
||||
td.throttle = struct.unpack_from("<f", data, 4)[0]
|
||||
td.brake = struct.unpack_from("<f", data, 8)[0]
|
||||
td.steering = struct.unpack_from("<f", data, 0)[0]
|
||||
except struct.error:
|
||||
pass
|
||||
|
||||
return td
|
||||
return TelemetryData(game_id="iracing", timestamp=time.time(), raw={"note": "iRacing uses shared memory, not UDP"})
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import Any, Callable
|
||||
from config.settings import get_config
|
||||
from server.telemetry.parsers import PARSER_MAP, BaseParser
|
||||
from server.telemetry.data import TelemetryData
|
||||
from server.telemetry.tcp_listener import TCPListener
|
||||
from server.game_manager import game_plugin_manager
|
||||
from utils.logger import get_logger
|
||||
|
||||
@@ -50,7 +51,9 @@ class DataForwarder:
|
||||
class TelemetryListener:
|
||||
def __init__(self):
|
||||
self._transport: asyncio.DatagramTransport | None = None
|
||||
self._tcp: TCPListener | None = None
|
||||
self._running = False
|
||||
self._current_protocol = "udp"
|
||||
self._callbacks: list[Callable[[TelemetryData], None]] = []
|
||||
self._parser: BaseParser | None = None
|
||||
self._parser_cache: dict[str, BaseParser] = {}
|
||||
@@ -90,6 +93,48 @@ class TelemetryListener:
|
||||
host = cfg.get("telemetry_host", "0.0.0.0")
|
||||
port = cfg.get("telemetry_port", 20777)
|
||||
|
||||
selected_game = cfg.get("selected_game_id")
|
||||
if selected_game:
|
||||
self.set_parser_for_game(selected_game)
|
||||
gp = game_plugin_manager.get(selected_game)
|
||||
if gp and gp.manifest_path:
|
||||
import json as _json
|
||||
try:
|
||||
with open(f"{gp.manifest_path}/manifest.json", "r") as f:
|
||||
manifest = _json.load(f)
|
||||
self._current_protocol = manifest.get("protocol", "udp")
|
||||
except Exception:
|
||||
self._current_protocol = "udp"
|
||||
|
||||
if self._current_protocol == "tcp":
|
||||
import json as _json
|
||||
mode = "server"
|
||||
tcp_path = "/"
|
||||
if selected_game:
|
||||
gp = game_plugin_manager.get(selected_game)
|
||||
if gp and gp.manifest_path:
|
||||
try:
|
||||
with open(f"{gp.manifest_path}/manifest.json", "r") as f:
|
||||
m = _json.load(f)
|
||||
mode = m.get("mode", "server")
|
||||
tcp_path = m.get("path", "/")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if mode == "server":
|
||||
self._tcp = TCPListener(self._handle_packet)
|
||||
ok = await self._tcp.start(host, port)
|
||||
if ok:
|
||||
self._running = True
|
||||
self.forwarder.reload_targets()
|
||||
logger.info("TCP server started on %s:%d [game=%s]", host, port, selected_game or "none")
|
||||
return ok
|
||||
else:
|
||||
# TCP client mode: plugin handles its own connection
|
||||
logger.info("TCP client mode - plugin handles connection [game=%s]", selected_game or "none")
|
||||
self._running = True
|
||||
return True
|
||||
else:
|
||||
loop = asyncio.get_event_loop()
|
||||
try:
|
||||
self._transport, _ = await loop.create_datagram_endpoint(
|
||||
@@ -98,16 +143,10 @@ class TelemetryListener:
|
||||
)
|
||||
self._running = True
|
||||
self.forwarder.reload_targets()
|
||||
|
||||
selected_game = cfg.get("selected_game_id")
|
||||
if selected_game:
|
||||
self.set_parser_for_game(selected_game)
|
||||
|
||||
logger.info("Telemetry listener started on %s:%d [game=%s]",
|
||||
host, port, selected_game or "none")
|
||||
logger.info("UDP listener started on %s:%d [game=%s]", host, port, selected_game or "none")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("Failed to start telemetry listener: %s", e)
|
||||
logger.error("Failed to start UDP listener: %s", e)
|
||||
return False
|
||||
|
||||
def stop(self):
|
||||
@@ -116,6 +155,9 @@ class TelemetryListener:
|
||||
if self._transport:
|
||||
self._transport.close()
|
||||
self._transport = None
|
||||
if self._tcp:
|
||||
asyncio.ensure_future(self._tcp.stop())
|
||||
self._tcp = None
|
||||
if self.forwarder._sock:
|
||||
self.forwarder._sock.close()
|
||||
self.forwarder._sock = None
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import socket
|
||||
import struct
|
||||
import time
|
||||
from typing import Any, Callable
|
||||
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class TCPListener:
|
||||
def __init__(self, handle_packet):
|
||||
self._server: asyncio.Server | None = None
|
||||
self._handle_packet = handle_packet
|
||||
self._running = False
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
return self._running
|
||||
|
||||
async def start(self, host: str, port: int) -> bool:
|
||||
if self._running:
|
||||
await self.stop()
|
||||
try:
|
||||
self._server = await asyncio.start_server(
|
||||
self._on_client_connected, host, port
|
||||
)
|
||||
self._running = True
|
||||
logger.info("TCP listener started on %s:%d", host, port)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("Failed to start TCP listener: %s", e)
|
||||
return False
|
||||
|
||||
async def stop(self):
|
||||
self._running = False
|
||||
if self._server:
|
||||
self._server.close()
|
||||
await self._server.wait_closed()
|
||||
self._server = None
|
||||
logger.info("TCP listener stopped")
|
||||
|
||||
async def _on_client_connected(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
|
||||
addr = writer.get_extra_info('peername')
|
||||
logger.debug("TCP client connected: %s", addr)
|
||||
try:
|
||||
while self._running:
|
||||
data = await reader.read(65536)
|
||||
if not data:
|
||||
break
|
||||
self._handle_packet(data, addr)
|
||||
except Exception as e:
|
||||
logger.debug("TCP client disconnected: %s - %s", addr, e)
|
||||
finally:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
@@ -3,6 +3,14 @@ const Topbar = {
|
||||
|
||||
init() {
|
||||
this._statusEl = document.getElementById('connection-status');
|
||||
this._statusEl.style.cursor = 'pointer';
|
||||
this._statusEl.addEventListener('click', () => {
|
||||
if (!WS.connected) {
|
||||
WS._reconnectDelay = 500;
|
||||
WS._connect();
|
||||
Toast.show('正在重连...', 'info', 1500);
|
||||
}
|
||||
});
|
||||
|
||||
WS.on('status', (status) => {
|
||||
this._statusEl.className = `status-indicator ${status}`;
|
||||
|
||||
@@ -221,7 +221,12 @@ const PageSettings = {
|
||||
|
||||
_gamePortListHtml(games, cfg) {
|
||||
const gamePorts = cfg.game_ports || {};
|
||||
return games.map(g => '<div class="game-port-row" style="display:flex;gap:8px;align-items:center;margin-bottom:6px"><span class="gp-id" style="width:140px;font-size:13px;color:var(--text-secondary)">' + g.name + '</span><input class="gp-port" type="number" value="' + (gamePorts[g.id] || g.default_port || 20777) + '" min="1024" max="65535" style="width:100px"><span style="font-size:11px;color:var(--text-tertiary)">默认: ' + (g.default_port || 20777) + '</span></div>').join('');
|
||||
return games.map(g => {
|
||||
const proto = g.protocol || 'udp';
|
||||
const mode = g.mode || (proto === 'tcp' ? 'server' : '');
|
||||
const protoLabel = proto.toUpperCase() + (mode ? '/' + mode : '');
|
||||
return '<div class="game-port-row" style="display:flex;gap:8px;align-items:center;margin-bottom:6px"><span class="gp-id" style="width:120px;font-size:13px;color:var(--text-secondary)">' + g.name + '</span><span style="width:70px;font-size:11px;color:var(--text-tertiary)">' + protoLabel + '</span><input class="gp-port" type="number" value="' + (gamePorts[g.id] || g.default_port || (proto==='tcp'?cfg.tcp_port||20778:20777)) + '" min="1024" max="65535" style="width:100px"><span style="font-size:11px;color:var(--text-tertiary)">默认: ' + (g.default_port || (proto==='tcp'?cfg.tcp_port||20778:20777)) + '</span></div>';
|
||||
}).join('');
|
||||
},
|
||||
|
||||
_forwardListHtml(forwards) {
|
||||
|
||||
@@ -51,6 +51,8 @@ const Lang = {
|
||||
|
||||
init() {
|
||||
this._current = localStorage.getItem('turbosu-lang') || 'zh';
|
||||
const btn = document.getElementById('lang-toggle');
|
||||
if (btn) btn.textContent = this._current === 'zh' ? 'EN' : '中';
|
||||
},
|
||||
|
||||
t(key) {
|
||||
|
||||
+16
-2
@@ -5,15 +5,26 @@ const WS = {
|
||||
_reconnectDelay: 2000,
|
||||
_handlers: {},
|
||||
_connected: false,
|
||||
_lastPong: 0,
|
||||
|
||||
init() {
|
||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
this._url = `${protocol}//${location.host}/ws`;
|
||||
this._connect();
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (!document.hidden && !this._connected) {
|
||||
this._reconnectDelay = 500;
|
||||
this._connect();
|
||||
}
|
||||
if (!document.hidden && this._connected) {
|
||||
this.send({type: 'ping'});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_connect() {
|
||||
if (this._ws && this._ws.readyState === WebSocket.OPEN) return;
|
||||
if (this._ws && (this._ws.readyState === WebSocket.OPEN || this._ws.readyState === WebSocket.CONNECTING)) return;
|
||||
|
||||
try {
|
||||
this._ws = new WebSocket(this._url);
|
||||
@@ -24,6 +35,7 @@ const WS = {
|
||||
|
||||
this._ws.onopen = () => {
|
||||
this._connected = true;
|
||||
this._lastPong = Date.now();
|
||||
this._reconnectDelay = 2000;
|
||||
this._emit('connection', true);
|
||||
this._emit('status', 'connected');
|
||||
@@ -32,6 +44,7 @@ const WS = {
|
||||
this._ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (msg.type === 'pong') { this._lastPong = Date.now(); return; }
|
||||
this._emit(msg.type || 'message', msg.data || msg);
|
||||
if (msg.type === 'telemetry') {
|
||||
this._emit('telemetry', msg.data);
|
||||
@@ -47,7 +60,8 @@ const WS = {
|
||||
};
|
||||
|
||||
this._ws.onerror = () => {
|
||||
this._emit('status', 'error');
|
||||
this._emit('status', 'disconnected');
|
||||
this._connected = false;
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user