PearlCalculatorRS:Minecraft FTL 计算器开发笔记
PearlCalculatorRS 项目的架构设计、物理模拟原理和算法优化历程。
1. 项目架构
PearlCalculatorRS 是一个 Minecraft FTL 珍珠炮轨迹计算工具。整个项目用 Rust 写核心计算逻辑,一套代码同时跑在桌面端(Tauri)和网页端(WASM)。下面按从外到内的顺序,把架构、Workspace 组织和各模块职责过一遍。
1.1 整体结构
下面这张图展示了从用户操作到计算结果返回的完整流程。无论是桌面版还是网页版,最终都会调用同一套 Rust 核心代码:
sequenceDiagram
box 前端层 pearl_calculator_ui
participant UI as 用户界面
participant Pages as 页面组件
participant Services as ICalculatorService
end
box 运行时适配层
participant Tauri as TauriService
participant WASM as WebService
end
box 桥接层 pearl_calculator_bridge
participant Bridge as Bridge
end
box 核心层 pearl_calculator_core
participant Core as Core Engine
end
UI->>Pages: 用户操作
Pages->>Services: 调用服务
alt Tauri 桌面模式
Services->>Tauri: invoke()
Tauri->>Bridge: IPC 调用
else WASM 网页模式
Services->>WASM: wasm 调用
WASM->>Bridge: wasm-bindgen
end
Bridge->>Core: 执行计算
Core-->>Bridge: 返回结果
Bridge-->>Services: JSON 序列化
Services-->>UI: 更新界面四层分别干的事情:
- 前端层:React + TypeScript,画界面
- 运行时适配层:根据环境切 Tauri IPC 或 WASM 调用
- 桥接层:JSON ↔ Rust 结构体互转
- 核心层:纯 Rust,跑物理计算
1.2 Cargo Workspace
项目使用 Cargo Workspace 管理多个 crate:
[workspace]
members = [
"pearl_calculator_core", # 核心计算库
"pearl_calculator_ui/src-tauri", # Tauri 桌面后端
"pearl_calculator_bridge", # I/O 桥接层
"pearl_calculator_wasm", # WASM 绑定
"pearl_calculator_utils", # 共享类型与配置工具(新增)
]
resolver = "3"构建时有几个优化配置值得一提:
- Release 模式开 LTO(链接时优化),减小二进制体积
- 开发模式下依赖包也设
opt-level = 3,调试时性能不会太烂
1.3 各模块介绍
接下来将逐一介绍每个模块。如果你想快速定位某个功能的代码,这部分会对你有帮助。
1.3.1 pearl_calculator_core
整个项目的核心,所有物理计算都在这里。不依赖任何运行时,可以独立编成原生库或 WASM 模块。
pearl_calculator_core/src/
├── lib.rs # 模块入口
├── calculation/ # 计算逻辑
│ ├── calculation.rs # TNT 搜索算法
│ ├── simulation.rs # 轨迹模拟
│ ├── inputs.rs # 内部输入结构
│ └── results.rs # 计算结果结构
├── physics/ # 物理引擎
│ ├── entities/ # 实体模型
│ │ ├── entities.rs # EntityData 基类
│ │ ├── pearl_entities.rs # 末影珍珠实体
│ │ ├── tnt_entities.rs # TNT 实体
│ │ └── movement.rs # 运动系统(版本分化)
│ ├── aabb/ # 碰撞检测
│ │ └── aabb_box.rs # AABB 包围盒
│ ├── constants/ # 物理常量
│ │ └── constants.rs # Minecraft 物理参数
│ └── world/ # 世界系统
│ ├── space.rs # Space3D 向量
│ ├── direction.rs # 方向枚举
│ └── layout_direction.rs # 布局方向
├── settings/ # 配置管理
│ ├── types.rs # 配置类型定义
│ ├── defaults.rs # 默认值
│ └── io.rs # 配置读写
└── utils/ # 工具函数
└── parallel.rs # 并行计算封装依赖关系:
| 依赖 | 版本 | 用途 |
|---|---|---|
serde | 1.0 | 序列化/反序列化 |
serde_json | 1.0 | JSON 处理 |
rayon | 1.11 | 并行计算(可选 feature) |
Feature Flags:
[features]
default = ["enable-rayon"] # 默认启用多线程
enable-rayon = ["dep:rayon"] # WASM 构建时禁用1.3.2 pearl_calculator_bridge
桥接层的活很单一:前端 JSON ↔ Rust 结构体互转,Tauri 和 WASM 两边共用一套数据格式。
pearl_calculator_bridge/src/
├── lib.rs # 模块入口
├── inputs.rs # 输入数据转换
└── outputs.rs # 输出数据转换核心输入结构:
| 结构体 | 用途 |
|---|---|
CalculationInput | TNT 搜索请求参数 |
PearlTraceInput | 珍珠轨迹追踪参数 |
RawTraceInput | 自由模拟器参数 |
Space3DInput | 三维坐标输入 |
TntGroupInput | TNT 组配置 |
核心输出结构:
| 结构体 | 用途 |
|---|---|
TNTResultOutput | 单条 TNT 配置结果 |
PearlTraceOutput | 完整轨迹数据 |
ClosestApproachOutput | 最近接近点信息 |
Space3DOutput | 三维坐标输出 |
1.3.3 pearl_calculator_wasm
通过 wasm-bindgen 把核心计算能力暴露给 JavaScript。网页版调用 Rust 代码的入口就在这里:
// 暴露的 WASM 函数
#[wasm_bindgen]
pub fn calculate_tnt_amount(val: JsValue) -> Result<JsValue, JsError>;
#[wasm_bindgen]
pub fn calculate_pearl_trace(val: JsValue) -> Result<JsValue, JsError>;
#[wasm_bindgen]
pub fn calculate_raw_trace(val: JsValue) -> Result<JsValue, JsError>;特性:
- 使用
serde_wasm_bindgen进行 JS ↔ Rust 数据转换 - 禁用
enable-rayonfeature(WASM 环境单线程) - 编译目标:
wasm32-unknown-unknown
1.3.4 pearl_calculator_ui/src-tauri
Tauri 2.0 桌面后端,负责 IPC 通信和系统能力调用。
src-tauri/
├── Cargo.toml # 依赖配置
├── tauri.conf.json # Tauri 配置
├── src/
│ ├── main.rs # 入口
│ ├── lib.rs # 命令注册
│ └── commands/ # Tauri 命令
│ ├── mod.rs
│ ├── calculation.rs # 计算命令
│ └── config.rs # 配置命令
└── capabilities/ # 权限配置注册的 Tauri 命令:
| 命令 | 功能 |
|---|---|
calculate_tnt_amount_command | 执行 TNT 搜索 |
calculate_pearl_trace_command | 计算珍珠轨迹 |
calculate_raw_trace_command | 自由模拟计算 |
verify_config | 验证配置文件 |
load_config | 加载配置文件 |
load_config_from_content | 从内容加载配置 |
启用的插件:
tauri-plugin-fs- 文件系统访问tauri-plugin-dialog- 系统对话框tauri-plugin-opener- 打开外部链接
1.3.5 pearl_calculator_ui
前端界面,使用 React + TypeScript 构建。
pearl_calculator_ui/src/
├── main.tsx # 应用入口
├── App.tsx # 根组件
├── pages/ # 页面组件
│ ├── Calculator.tsx # 计算器页面
│ ├── Configuration.tsx # 配置页面
│ └── Simulator.tsx # 模拟器页面
├── components/ # UI 组件库
├── services/ # 服务抽象
│ ├── index.ts # 服务工厂
│ ├── interface.ts # 接口定义
│ ├── tauri.ts # Tauri 实现
│ └── wasm.ts # WASM 实现
├── context/ # React Context
├── hooks/ # 自定义 Hooks
├── types/ # TypeScript 类型
├── locales/ # i18n 翻译文件
└── lib/ # 工具库1.3.6 pearl_calculator_utils(新增)
v2.0 之后从 bridge/core/UI 三处抽出来的共享工具库,把重复的类型和逻辑集中到一处。
pearl_calculator_utils/src/
├── lib.rs # 模块入口
├── types.rs # 共享类型定义
├── config.rs # 配置管理引擎(~1000 行)
└── bit.rs # Bit 模板计算引擎依赖:serde、serde_json、base64
核心职责:
types.rs:定义GeneralConfig、CannonMode(Standard / Accumulation / Vector3D)、BitTemplateConfig、MultiplierConfig、DraftConfig(配置向导的中间表示)、CalculatorInputs(前端输入)等,所有 crate 共用config.rs:Draft ↔ Config 双向转换、配置导出、JSON 解析与 normalization、二进制编码/解码——详见第 7 章bit.rs:Bit 模板的验证、解码(贪心子集和)、Multiplier 解码、方向掩码查找、一次完整的calculate_bits流程
以前这些逻辑分布在 bridge 的 inputs/outputs、core 的 types、UI 的 services 里。拆出来之后,Tauri 和 WASM 两端的配置处理完全相同,改一处就行。
1.4 运行时适配
前端不感知自己跑在桌面端还是网页端,通过策略模式屏蔽差异:
// 接口定义
export interface ICalculatorService {
calculateTNTAmount(input: CalculationInput): Promise<TNTResult[]>;
calculatePearlTrace(input: PearlTraceInput): Promise<PearlTraceResult>;
calculateRawTrace(input: RawTraceInput): Promise<PearlTraceResult>;
}
// 运行时自动检测
const isTauri = '__TAURI_INTERNALS__' in window;
export const calculatorService: ICalculatorService = isTauri
? new TauriCalculatorService() // 桌面应用:IPC 调用
: new WebCalculatorService(); // 网页应用:WASM 调用Tauri 实现 通过 invoke() 调用后端命令:
class TauriCalculatorService implements ICalculatorService {
async calculateTNTAmount(input: CalculationInput): Promise<TNTResult[]> {
return invoke<TNTResult[]>("calculate_tnt_amount_command", { input });
}
}WASM 实现 直接调用编译好的 WASM 模块:
class WebCalculatorService implements ICalculatorService {
async calculateTNTAmount(input: CalculationInput): Promise<TNTResult[]> {
const wasm = await import("pearl_calculator_wasm");
return wasm.calculate_tnt_amount(input);
}
}1.5 请求处理流程
sequenceDiagram
participant User as 用户
participant UI as React UI
participant Service as Service Layer
participant Backend as Tauri/WASM
participant Bridge as Bridge Layer
participant Core as Core Engine
User->>UI: 输入参数
UI->>Service: calculatorService.calculateTNTAmount(input)
alt Tauri 桌面模式
Service->>Backend: invoke("calculate_tnt_amount_command")
Backend->>Bridge: CalculationInput 反序列化
else WASM 网页模式
Service->>Backend: wasm.calculate_tnt_amount(input)
Backend->>Bridge: serde_wasm_bindgen::from_value
end
Bridge->>Core: calculate_tnt_amount(&cannon, ...)
Core->>Core: 并行 TNT 搜索
Core-->>Bridge: Vec<TNTResult>
Bridge-->>Backend: TNTResultOutput 序列化
Backend-->>Service: JSON/JsValue
Service-->>UI: TNTResult[]
UI-->>User: 显示结果2. 物理模拟
珍珠轨迹准不准,取决于能不能精确复现 Minecraft 的物理规则。这一章覆盖所有涉及的物理系统:重力、空气阻力、TNT 爆炸推进、碰撞检测。
2.1 物理常量
下表是模拟中用到的所有常量,来自 Minecraft 反编译代码和 Wiki:
| 常量名 | 符号 | 值 | 单位 | 说明 |
|---|---|---|---|---|
| 重力加速度 | 0.03 | blocks/tick² | 末影珍珠重力 | |
| 空气阻力系数 | 0.99 | - | 速度衰减乘数 | |
| 珍珠半径 | 0.125 | blocks | 碰撞箱宽度 | |
| 珍珠高度 | 0.25 | blocks | 碰撞箱高度 | |
| TNT 半径 | 0.49 | blocks | TNT 碰撞箱宽度 | |
| TNT 高度 | 0.98 | blocks | TNT 碰撞箱高度 | |
| 爆炸半径 | 8.0 | blocks | 爆炸影响范围 | |
| TNT Y 偏移 | 0.06125 | blocks | TNT 实体 Y 轴偏移 | |
| 珍珠爆炸 Y 系数 | 0.85 | - | 爆炸力作用点系数 | |
| 浮点精度阈值 | 1e-10 | - | 零值判断阈值 |
2.2 珍珠运动
2.2.1 状态向量
珍珠在任意时刻的状态由位置 和速度 两个向量描述:
2.2.2 每 Tick 的更新
每个游戏 Tick(1/20 秒),珍珠的位置和速度会按特定顺序更新,但不同 Minecraft 版本的更新顺序不一样:
flowchart LR
subgraph Legacy["Legacy (≤1.20.4)"]
direction TB
L1[位置更新] --> L2[阻力衰减] --> L3[重力应用]
end
subgraph Post1205["Post1205 (1.20.5-1.21.1)"]
direction TB
P1[位置更新] --> P2[阻力衰减] --> P3[重力应用]
end
subgraph Post1212["Post1212 (≥1.21.2)"]
direction TB
N1[重力应用] --> N2[阻力衰减] --> N3[位置更新]
end
Legacy ~~~ Post1205 ~~~ Post12122.3 版本差异
下面是每个版本的具体实现。只关心结论的话直接跳 2.4 的对比表格。
2.3.1 Legacy 版本(≤ 1.20.4)
这个版本使用 32 位 float 做速度计算,然后再转回 64 位。更新顺序是:移动 → 阻力 → 重力。
// 1. 位置更新
position += velocity;
// 2. 阻力衰减 (float 精度)
velocity.x = (velocity.x as f32 * 0.99) as f64;
velocity.y = (velocity.y as f32 * 0.99) as f64;
velocity.z = (velocity.z as f32 * 0.99) as f64;
// 3. 重力应用 (float 精度)
velocity.y = (velocity.y as f32 - 0.03) as f64;数学表达:
WARNINGLegacy 版本因 float 精度损失,长距离飞行会出现明显误差累积。
2.3.2 Post1205 版本(1.20.5 - 1.21.1)
这个版本完全使用 64 位 double,更新顺序和 Legacy 一样,但精度更高:
// 1. 位置更新
position += velocity;
// 2. 阻力衰减 (double 精度)
velocity *= 0.99;
// 3. 重力应用
velocity.y -= 0.03;数学表达:
2.3.3 Post1212 版本(≥ 1.21.2)
Mojang 在这个版本重构了实体物理系统,调整了操作顺序:重力 → 阻力 → 移动。
// 1. 重力应用
velocity.y -= 0.03;
// 2. 阻力衰减
velocity *= 0.99;
// 3. 位置更新
position += velocity;数学表达:
Post1212 的顺序变化会导致相同初始条件下轨迹与旧版本产生偏差,远距离飞行尤为明显。
2.4 版本对比总结
| 特性 | Legacy (≤1.20.4) | Post1205 (1.20.5-1.21.1) | Post1212 (≥1.21.2) |
|---|---|---|---|
| 浮点精度 | float (32-bit) | double (64-bit) | double (64-bit) |
| Tick 顺序 | Move→Drag→Gravity | Move→Drag→Gravity | Gravity→Drag→Move |
| 精度累积误差 | 较大 | 较小 | 较小 |
| 轨迹特性 | 相对抛物线偏短 | 标准抛物线 | 阻力先于移动,飞行略短 |
2.5 TNT 爆炸推进
TNT 爆炸会对范围内的珍珠施加一个推进力。力的计算涉及距离衰减和方向向量。
2.5.1 爆炸力计算
设 TNT 爆炸中心为 ,珍珠位置为 :
1. 计算 TNT 实际爆炸中心(Y 轴偏移):
其中 (TNT 高度 × 0.0625)
2. 计算距离向量:
3. 判断是否在爆炸范围内:
4. 计算爆炸方向向量(考虑珍珠高度):
其中 (爆炸作用点在珍珠高度的 85% 处)
5. 归一化方向向量:
6. 计算爆炸强度(距离衰减):
7. 最终推进向量:
2.5.2 爆炸力可视化
graph TD
subgraph "爆炸力衰减曲线"
A["距离 d = 0"] -->|"强度 s = 1.0"| B["最大推力"]
C["距离 d = 4"] -->|"强度 s = 0.5"| D["半衰推力"]
E["距离 d = 8"] -->|"强度 s = 0"| F["无推力"]
end推力强度随距离的衰减公式:
2.6 碰撞检测
项目使用 AABB(轴对齐包围盒)算法处理珍珠与世界方块的碰撞。
2.6.1 AABB 包围盒定义
AABB 由最小点和最大点定义:
2.6.2 珍珠包围盒计算
珍珠在位置 的包围盒:
2.6.3 扫掠碰撞检测(Sweep Collision)
实体移动时按 Y → X → Z 顺序依次检测碰撞:
原始移动: (xa, ya, za)
↓
[Y轴碰撞检测] → 修正 ya
↓
[更新包围盒 Y]
↓
[X轴碰撞检测] → 修正 xa
↓
[更新包围盒 X]
↓
[Z轴碰撞检测] → 修正 za
↓
[最终位置更新]2.6.4 单轴碰撞检测算法
以 Y 轴为例(X、Z 轴类似):
fn y_offset(&self, other: &AABBBox, mut offset_y: f64) -> f64 {
// 1. 检查 X-Z 平面是否重叠
if other.max_x <= self.min_x || other.min_x >= self.max_x { return offset_y; }
if other.max_z <= self.min_z || other.min_z >= self.max_z { return offset_y; }
// 2. 向上移动时的碰撞检测
if offset_y > 0.0 && other.max_y <= self.min_y {
let d = self.min_y - other.max_y;
if d < offset_y { offset_y = d; }
}
// 3. 向下移动时的碰撞检测
if offset_y < 0.0 && other.min_y >= self.max_y {
let d = self.max_y - other.min_y;
if d > offset_y { offset_y = d; }
}
offset_y
}2.6.5 碰撞后状态更新
// 发生碰撞时清零对应轴的速度
if original_xa != xa { motion.x = 0.0; }
if original_ya != ya { motion.y = 0.0; }
if original_za != za { motion.z = 0.0; }
// 更新碰撞标志
is_collided_horizontally = (original_xa != xa) || (original_za != za);
is_collided_vertically = original_ya != ya;
on_ground = (original_ya != ya) && (original_ya < 0.0);2.7 Space3D 向量数学
项目使用 Space3D 结构表示三维向量,支持以下运算:
| 运算 | 数学表达 | Rust 实现 |
|---|---|---|
| 向量加法 | a + b | |
| 向量减法 | a - b | |
| 标量乘法 | a * k | |
| 向量长度 | a.length() | |
| 长度平方 | a.length_sq() | |
| 3D 距离 | a.distance(&b) | |
| 2D 距离 | a.distance_2d(&b) |
2.8 模拟精度说明
TIP本项目的物理模拟与 Minecraft 游戏行为完全一致,采用相同的常量值和计算顺序。唯一差异在于 Legacy 模式的 float 精度模拟使用显式类型转换实现。
精度上有几个保障:
- 浮点计算全用 IEEE 754 双精度
- 坐标精度到小数点后 10 位以上
- 速度归零判定阈值
3. 核心算法
这一章讲 TNT 数量搜索算法的原理,以及怎么从每秒几十个组合一路优化到每秒几百万个。3.9 节记录了完整的优化过程。
3.1 整体流程
先从全局视角看执行流程:
graph TD
subgraph Input["输入参数"]
direction LR
Cannon[炮台配置] ~~~ Dest[目标坐标] ~~~ MaxTNT[最大 TNT] ~~~ MaxTicks[最大 Tick]
end
subgraph Core["核心算法流程"]
A[计算飞行方向] --> B[求解 TNT 向量]
B --> C[几何方程求解理想 TNT]
C --> D[Tick 分组优化]
D --> E[并行轨迹扫描]
E --> F[结果去重与排序]
end
subgraph Output["输出结果"]
Results[TNTResult 列表]
end
Input --> Core
Core --> Output3.2 TNT 搜索算法
整个工具要解决的核心问题:给定珍珠初始位置、速度和目标位置,找出能让珍珠飞到目标附近的 TNT 组合。
3.2.1 问题定义
已知:
- 珍珠初始位置 和初始速度
- 目标位置
- 红色/蓝色 TNT 的爆炸向量 、
求:红色 TNT 数量 和蓝色 TNT 数量 ,使珍珠落点尽可能接近目标。
3.2.2 飞行方向判定
首先根据目标位置计算飞行方向:
方向判定规则:
| 角度范围 | 方向 |
|---|---|
| East (东) | |
| South (南) | |
| West (西) | |
| 其他 | North (北) |
3.2.3 TNT 向量计算
根据飞行方向确定红色和蓝色 TNT 的位置,然后计算爆炸向量:
3.2.4 线性方程组求解(Cramer 法则)
珍珠最终位移可表示为 TNT 推力乘以累积因子:
其中累积因子 随 Tick 变化(见下文)。
将 X-Z 平面投影为二元一次方程组:
使用 Cramer 法则 求解:
行列式计算:
解出 TNT 比例:
考虑累积因子后的理想整数解:
3.2.5 累积因子公式
珍珠每 tick 都在减速(因为空气阻力),所以同样 1 m/s 的初速度,对最终位移的贡献是递减的。累积因子 就是用来描述 个 tick 后速度对位移的总贡献。
因为阻力系数 ,这实际上是个等比数列求和问题:
Legacy / Post1205 版本:
Post1212 版本(由于阻力先于移动,公式略有不同):
3.3 Tick 分组优化
3.3.1 优化原理
思路来自一个很直观的观察:空气阻力让速度指数衰减,相邻 tick 算出来的理想 TNT 值非常接近,四舍五入后经常一模一样。
做法是把所有 tick 按理想 TNT 组合 分组,相同组合的 tick 共享一次模拟:
// 按理想 TNT 组合分组
let mut groups: HashMap<(i32, i32), Vec<u32>> = HashMap::new();
for tick in 1..=max_ticks {
let (ideal_red, ideal_blue) = solve_ideal_tnt(tick);
groups.entry((ideal_red, ideal_blue))
.or_insert_with(Vec::new)
.push(tick);
}3.3.2 搜索邻域扩展
数学解是浮点数,取整会有误差,所以实际搜索时在理想值附近各扩 5 个单位:
形成 个候选组合。
3.4 并行计算优化
3.4.1 并行策略
分组之间互不依赖,天然适合并行。用 Rayon 库,迭代器包装成并行版本,几乎不用动原有逻辑:
let raw_results: Vec<TNTResult> = group_list
.into_par_iter() // 并行迭代
.flat_map(|((ideal_red, ideal_blue), valid_ticks)| {
// 每个分组独立计算
let mut local_results = Vec::new();
for r_adjust in -5..=5 {
for b_adjust in -5..=5 {
// 模拟并检查命中
}
}
local_results
})
.collect();3.4.2 运行时适配
WASM 环境跑不了多线程,通过 feature flag 处理兼容:WASM 构建时禁用 Rayon,into_par_iter() 自动降级为普通顺序迭代:
| 运行时 | 并行实现 | 线程数 |
|---|---|---|
| Tauri 桌面 | Rayon | CPU 核心数 |
| WASM 网页 | 顺序迭代 | 1(单线程) |
#[cfg(not(feature = "enable-rayon"))]
pub mod prelude {
pub trait IntoParallelIterator {
type Item;
type IntoIter: Iterator<Item = Self::Item>;
fn into_par_iter(self) -> Self::IntoIter;
}
impl<I: IntoIterator> IntoParallelIterator for I {
type Item = I::Item;
type IntoIter = I::IntoIter;
fn into_par_iter(self) -> Self::IntoIter {
self.into_iter() // 降级为顺序迭代
}
}
}3.5 轨迹模拟算法
3.5.1 scan_trajectory 函数
搜索算法最终要调用的是轨迹模拟。这个函数从初始状态开始,逐 tick 推进珍珠位置,在每个目标 tick 检查是否命中:
pub fn scan_trajectory(
data: &GeneralData, // 珍珠初始状态
destination: Space3D, // 目标位置
max_tick: u32, // 最大模拟 Tick
valid_ticks: &[bool], // 有效 Tick 掩码
world_collisions: &[AABBBox], // 碰撞体
offset: Space3D, // 坐标偏移
version: PearlVersion, // MC 版本
max_distance_sq: f64, // 最大距离平方
) -> Vec<SimResult>3.5.2 模拟循环
整个模拟过程看下面这张流程图。注意有个提前退出条件——珍珠速度趋近于零:
graph TD
A[初始化珍珠实体] --> B[Tick 0]
B --> C{tick <= max_tick?}
C -->|是| D[应用 TNT 爆炸力]
D --> E[执行物理更新]
E --> F{是有效 Tick?}
F -->|是| G[计算距离]
G --> H{距离 <= 阈值?}
H -->|是| I[记录命中结果]
H -->|否| J[继续]
F -->|否| J
I --> J
J --> K{速度趋零?}
K -->|是| L[提前终止]
K -->|否| M[tick++]
M --> C
C -->|否| N[返回结果]
L --> N3.5.3 距离计算
判断珍珠是否”命中”目标时,只看 X-Z 平面的距离:
只要这个距离在用户设定的阈值内,就算命中:
3.5.4 提前终止条件
当珍珠速度趋近于零时提前终止模拟:
其中
3.6 结果处理算法
3.6.1 去重逻辑
一个 TNT 组合可能在多个 tick 都命中目标范围,需要去重:相同 的组合只留最优解:
let mut best_results_map: HashMap<(u32, u32), TNTResult> = HashMap::new();
for res in raw_results {
let key = (res.red, res.blue);
match best_results_map.entry(key) {
Vacant(e) => { e.insert(res); }
Occupied(mut e) => {
let is_better = if (res.distance - current.distance).abs() < ε {
res.tick < current.tick // 距离相同时优先选择更早的 Tick
} else {
res.distance < current.distance // 否则选择距离更近的
};
if is_better { e.insert(res); }
}
}
}3.6.2 排序规则
最终结果按距离升序排序:
final_results.sort_by(|a, b| {
a.distance.partial_cmp(&b.distance)
.unwrap_or(std::cmp::Ordering::Equal)
});3.7 轨迹计算
3.7.1 calculate_pearl_trace 函数
给定特定 TNT 配置,计算完整轨迹:
pub fn calculate_pearl_trace(
cannon: &Cannon,
red_tnt: u32,
blue_tnt: u32,
direction: Direction,
max_ticks: u32,
world_collisions: &[AABBBox],
version: PearlVersion,
) -> Option<CalculationResult>3.7.2 轨迹数据结构
返回值包含了前端需要的所有信息——落点、轨迹、飞行时间等:
pub struct CalculationResult {
pub landing_position: Space3D, // 最终落点
pub pearl_trace: Vec<Space3D>, // 位置轨迹
pub pearl_motion_trace: Vec<Space3D>, // 速度轨迹
pub is_successful: bool, // 是否命中目标
pub tick: u32, // 飞行 Tick 数
pub final_motion: Space3D, // 最终速度
pub distance: f64, // 与目标距离
}3.7.3 轨迹采样
模拟过程中,每个 tick 都会记录一次位置和速度:
去重处理(移除连续重复点):
final_traces.dedup();
final_motion_traces.dedup();3.8 复杂度分析
最后总结一下算法各阶段的时间和空间复杂度:
| 算法阶段 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 几何求解与分组 | ||
| 并行搜索 | ||
| 去重排序 |
其中:
- = 最大 Tick 数
- = 分组数量(通常 )
- = 各分组最大 tick 的平均值
- = 并行线程数
- = 原始结果数量
NOTE几何求解与分组在代码中是同一循环完成的。空间复杂度为 是因为所有 tick 都需要存入对应分组。
总复杂度:
3.9 优化过程
初始版本跑一趟超过 5 分钟,最终版本 0.13 秒,加速约 2300 倍。
3.9.1 初始状态
搜索参数:
- 搜索范围:10000 tick(约 8.3 分钟飞行时间)
- 每 tick 搜索 121 种 TNT 组合(理想值 ±5)
- 总搜索空间: 万次模拟
初始实现性能:
- 耗时:> 300 秒
- CPU 占用:6%(8 核 16 线程仅 1 核工作)
初始实现有几个问题:
问题一:O(N²) 复杂度
检查第 t tick 是否命中,需要从头模拟 t 次物理迭代。总计算量:
问题二:频繁堆分配
simulation::run 返回 LinkedList<Space3D> 记录完整轨迹,每次调用都触发堆内存分配。在热路径上频繁 malloc/free 严重影响性能。
问题三:累积因子重复计算
计算理想 TNT 需要累积因子 ,原实现用循环累加,复杂度 O(t)。对于 tick 10000,需要累加 10000 次。
3.9.2 优化一:减少内存分配
将模拟函数拆分为两个版本:
| 函数 | 用途 | 内存模式 |
|---|---|---|
run | 前端轨迹绘图 | 返回 Vec<Space3D> |
check_landing | 计算器搜索 | 纯栈内存,仅返回最终位置 |
check_landing 实现:
pub fn check_landing(...) -> Option<(Space3D, Space3D, u32)> {
let mut pearl = PearlEntity::new(...);
for tick in 0..max_ticks {
pearl.tick();
if pearl.motion.length_sq() < EPSILON { break; }
}
if pearl.position.distance_2d(&dest) <= max_distance {
Some((pearl.position, pearl.motion, tick))
} else {
None
}
}附加微优化:
- 距离判断使用平方比较,避免
sqrt运算 - 速度接近零时提前退出循环
效果:约 1.3 倍提升。但 O(N²) 复杂度没变,瓶颈在算法不在内存。
3.9.3 优化二:并行化
使用 Rayon 将外层 tick 循环并行化:
use rayon::prelude::*;
let results: Vec<_> = (1..=max_ticks)
.into_par_iter()
.flat_map(|tick| calculate_for_tick(tick))
.collect();效果:
- CPU 占用:6% → 100%
- 耗时:300 秒 → 42 秒
- 加速比:~7x(接近 8 核理论上限)
但 42 秒对实时工具还是太慢,算法复杂度 O(N²/P) 也没根本改变。
3.9.4 优化三:等比数列公式
回头看原实现的累积因子计算:
let mut divider = 0.0;
for t in 1..=tick {
divider += 0.99_f64.powi(t as i32); // O(N) 循环
}一眼就能看出来这是等比数列求和,直接套公式:
优化后:
let divider = 0.99 * (1.0 - 0.99_f64.powi(tick as i32)) / 0.01; // O(1)效果:累积因子计算从 O(N) 降为 O(1)。
3.9.5 优化四:Tick 归组
关键洞察:空气阻力让珍珠速度指数衰减,后期 tick 的理想 TNT 值变化极小:
- tick 100 和 tick 101 的理想 TNT 可能相差 0.5
- tick 5000 和 tick 5001 的理想 TNT 可能相差 0.0001,四舍五入后完全相同
将 10000 个 tick 按理想 TNT 值分组:
let mut groups: HashMap<(i32, i32), Vec<u32>> = HashMap::new();
for tick in 1..=10000 {
let ideal_red = (true_red / divider).round() as i32;
let ideal_blue = (true_blue / divider).round() as i32;
groups.entry((ideal_red, ideal_blue))
.or_default()
.push(tick);
}分组结果:10000 个 tick 仅产生约 50 个不同的组。
对每组采用单次遍历扫描:只运行一次模拟到该组最大 tick,在过程中用布尔掩码检查所有目标时间点。
效果:
- 模拟次数: →
- 计算量减少:99.5%
- 耗时:42 秒 → 0.13 秒
- 加速比:~320x
3.9.6 优化五:结果去重
优化后冒出新问题:结果大量重复。原因:
- 珍珠落地后静止,从着陆 tick 到 10000 tick 均在目标范围内
- 不同理想 TNT 微调后可能得到相同实际 TNT
处理方式:
- 组内去重:每次模拟扫描后,使用
min_by仅保留距离最近的结果 - 全局去重:用
HashMap<(u32, u32), TNTResult>确保每个 (red, blue) 组合只输出最优解
3.9.7 最终对比
| 阶段 | 耗时 | 加速比 | 主要改进 |
|---|---|---|---|
| 初始实现 | > 300 秒 | 1x | - |
| 内存优化 | ~230 秒 | 1.3x | 零分配模拟 |
| 并行化 | 42 秒 | 7x | Rayon 多核 |
| 算法优化 | 0.13 秒 | 2300x | 归组 + 公式 |
复杂度变化:
主要收益来自算法层面的优化,而非硬件并行。归组策略将有效搜索空间压缩了 200 倍,这是性能跃升的根本原因。
3.10 计算管线重构
最初的 calculation.rs 是一个大模块,所有逻辑揉在一起。随着 Vector3D 模式和 Y-plane 拦截的加入,复杂度撑不住了。v2.1 之后拆成了四个子模块,形成一条流水线:
solve_theoretical_tnt() ← solver.rs
│ HashMap<(r,b,v), Vec<tick>>
▼
generate_candidates() ← optimizer.rs
│ Vec<((r,b,v), Vec<tick>)>
▼
validate_candidates() ← trace.rs (并行)
│ Vec<TNTResult>
▼
去重、排序,输出最终结果流水线前后的 Direction 和向量计算由 vectors.rs 负责,calculation.rs 退化为一个薄薄的编排层。
3.10.1 Solver:理论求解
solver.rs 逐个 tick 推进重力和运动累积,计算出补偿后的位移,然后根据计算模式走三条路径之一:
- 标准 2D Cramer:X-Z 平面投影,和以前一样
- 3D 线性系统:垂直向量有效时,用三重积求行列式,解 (r, b, v) 三元组
- Y-plane 拦截:插值找到珍珠穿过目标 Y 平面的时刻再求解
核心函数签名:
pub fn solve_theoretical_tnt(input: &SolverInput) -> HashMap<(i32, i32, i32), Vec<u32>>SolverInput 包含了红/蓝/垂直 TNT 向量、珍珠起始状态、目标、最大 tick、版本和 Y-plane 开关。返回值是理论 TNT 组合到对应 tick 列表的映射——和原来 tick 分组优化的思路一致,但 key 从 (r, b) 变成了 (r, b, v)。
3.10.2 Optimizer:候选生成
solver 输出的是精确数学解,但现实中的 TNT 必须取整。optimizer.rs 在每个理论解附近做搜索邻域扩展:
pub struct SearchParams {
pub max_tnt: u32,
pub max_vertical_tnt: Option<u32>,
pub search_radius: i32, // 红/蓝 ±5
pub has_vertical: bool,
pub is_valid_3d: bool,
pub cannon_mode: CannonMode,
}3D 模式下垂直方向也扩展 ±1。Accumulation 模式跳过 max_tnt 的单边限制(Accumulation 的 max_tnt 含义不同)。
输出是所有去重后的 (r, b, v) 候选及其有效 tick,供 trace.rs 验证。
3.10.3 Trace:并行验证与去重
trace.rs 的 validate_candidates() 对 optimizer 产出的每组合并 TNT 推力和珍珠初速度,调 scan_trajectory() 跑实际模拟。用 rayon 并行,每组内取最近距离的最优 hit,跨组再用 HashMap 去重。
calculate_pearl_trace() 和 calculate_raw_trace() 也搬到了这里——给定明确的 TNT 数量,算出一条完整轨迹。
3.10.4 Vectors:方向到向量映射
vectors.rs 解决了一个老问题:珍珠飞行方向变了,应该用哪个 dispenser 当红色 TNT、哪个当蓝色 TNT?
路由逻辑:
- 把
LayoutDirection(NorthWest 等)转为 cardinal 位掩码 - 检查飞行方向是否与蓝色 duper 的位掩码重叠
- 不重叠 → 蓝色用默认位置,红色算一个备选
- 重叠 → 红色用默认位置,蓝色算一个备选
- 备选 = 反向位 | 其他位,再映射回
LayoutDirection
同时还负责计算垂直 TNT 向量(如果炮台配置了的话)。
3.11 Vector3D 模式
Vector3D 是第三个计算模式。Standard 和 Accumulation 用的 FTL 炮台只有水平四方向的 TNT,Vector3D 炮台多了一组垂直方向(Y 轴)的 TNT dispenser。
配置层面多了两个字段:
vertical_tnt: 垂直 TNT 的 3D 位置max_vertical_tnt: 垂直 TNT 上限
算法上用 3D Cramer 法则求解。三个向量的线性组合 → 一个三重积行列式:
// solver.rs
fn solve_tnt_system_3d(red, blue, vert, target) -> Option<(f64, f64, f64)> {
let det = red.dot(blue.cross(vert)); // 三重积
if det.abs() < EPSILON { return None; }
let dr = target.dot(blue.cross(vert));
let db = red.dot(target.cross(vert));
let dv = red.dot(blue.cross(target));
Some((dr / det, db / det, dv / det))
}TNTResult 也加了 vertical: u32 字段。
3.12 Y-plane 拦截
Standard 模式原来只看水平距离最近的 tick。但有些场景下,用户关心的是珍珠在特定高度穿过目标正上方的时刻——比如跨海飞行时,更关注 Y=64 平面上的对齐。
开了 Y-plane 后,求解逻辑变成:
- 每个 tick pair 之间,插值找到珍珠路径与目标 Y 平面的交点
- 计算插值因子
alpha ∈ [0, 1] - 在交点处求解 TNT 数量
// 核心逻辑:在两个 tick 之间找 Y-plane 交叉点
let base_delta = current_base - previous_base;
let numerator = true_distance.y
- previous_base.y
- y_from_x_coeff * (true_distance.x - previous_base.x)
- y_from_z_coeff * (true_distance.z - previous_base.z);
let denominator_s = base_delta.y
- y_from_x_coeff * base_delta.x
- y_from_z_coeff * base_delta.z;
let interpolation = numerator / denominator_s;interpolation 在 [0,1] 之间说明这个 tick pair 确实穿过了目标 Y 平面,求解才有意义。
前端通过 CalculatorInputField::PlaneInterceptY 和 DestY 开关/输入框控制。
3.13 多方向搜索
之前版本只对一个飞行方向求解。重构后 calculate_tnt_amount() 会调用 Direction::from_angle_with_fallbacks(yaw) 产生多个候选方向,对每个方向分别跑完整的 solver → optimizer → trace 流水线,汇总所有结果。
这很重要——有些目标恰好落在方向分界线上,单方向求解要么根本无解,要么结果很差。多方向尝试自然地覆盖了这些边界情况。
4. 数据结构
这一章覆盖项目中最关键的数据模型:向量、空间、实体、设置。改代码前可以翻翻。
4.1 基础类型
4.1.1 Space3D
整个物理系统的基础——三维向量,同时用于表示位置和速度:
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
pub struct Space3D {
pub x: f64,
pub y: f64,
pub z: f64,
}方法:
| 方法 | 签名 | 说明 |
|---|---|---|
new | fn new(x: f64, y: f64, z: f64) -> Self | 构造函数 |
distance | fn distance(&self, other: &Self) -> f64 | 3D 欧几里得距离 |
distance_sq | fn distance_sq(&self, other: &Self) -> f64 | 距离平方(避免 sqrt) |
length | fn length(&self) -> f64 | 向量长度 |
length_sq | fn length_sq(&self) -> f64 | 长度平方 |
distance_2d | fn distance_2d(&self, other: &Self) -> f64 | XZ 平面距离 |
angle_to_yaw | fn angle_to_yaw(&self, other: &Self) -> f64 | 计算朝向角度 |
运算符重载:
| 运算符 | 操作 |
|---|---|
+ / += | 向量加法 |
- | 向量减法 |
* / *= | 标量乘法 |
/ / /= | 标量除法 |
4.1.2 AABBBox
轴对齐包围盒(Axis-Aligned Bounding Box),用于碰撞检测。
定义:
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct AABBBox {
pub min_x: f64,
pub min_y: f64,
pub min_z: f64,
pub max_x: f64,
pub max_y: f64,
pub max_z: f64,
}方法:
| 方法 | 说明 |
|---|---|
new(min_x, min_y, min_z, max_x, max_y, max_z) | 构造函数 |
offset(x, y, z) | 返回平移后的新包围盒 |
x_offset(&self, other, offset_x) | X 轴碰撞检测与修正 |
y_offset(&self, other, offset_y) | Y 轴碰撞检测与修正 |
z_offset(&self, other, offset_z) | Z 轴碰撞检测与修正 |
4.2 物理实体
4.2.1 EntityData
通用实体数据结构,包含位置、速度、碰撞状态等。
定义:
#[derive(Debug, Clone, PartialEq)]
pub struct EntityData {
pub position: Space3D, // 实体中心位置
pub motion: Space3D, // 速度向量
pub bounding_box: AABBBox, // 碰撞包围盒
pub on_ground: bool, // 是否着地
pub is_collided_horizontally: bool, // 水平碰撞标志
pub is_collided_vertically: bool, // 垂直碰撞标志
pub is_gravity: bool, // 是否受重力影响
}方法:
| 方法 | 说明 |
|---|---|
new(position, motion, bounding_box) | 构造函数 |
move_entity(xa, ya, za, world_collisions) | 执行移动并处理碰撞 |
move_entity 碰撞处理流程:
- 按 Y → X → Z 顺序检测碰撞
- 修正移动量以避免穿透
- 更新位置和碰撞状态标志
- 若发生碰撞,清零对应轴速度
4.2.2 Pearl / PearlEntity
珍珠实体专用结构。
Pearl(轻量配置结构):
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Pearl {
pub position: Space3D, // 初始位置
pub motion: Space3D, // 初始速度
pub offset: Space3D, // 坐标偏移量
}PearlEntity(物理模拟实体):
pub struct PearlEntity<M: PearlMovement> {
pub data: EntityData, // 实体数据
_movement: PhantomData<M>, // 版本特定运动逻辑
}泛型参数 M 决定使用的运动模型:
MovementLegacy:≤ 1.20.4MovementPost1205:1.20.5 - 1.21.1MovementPost1212:≥ 1.21.2
4.3 炮台配置
4.3.1 Cannon
运行时炮台配置,由 CannonSettings 转换而来。
定义:
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Cannon {
pub pearl: Pearl, // 珍珠配置
pub north_west_tnt: Space3D, // 西北 TNT 位置
pub north_east_tnt: Space3D, // 东北 TNT 位置
pub south_west_tnt: Space3D, // 西南 TNT 位置
pub south_east_tnt: Space3D, // 东南 TNT 位置
pub vertical_tnt: Option<Space3D>, // 垂直 TNT 位置(Vector3D 模式)
pub default_red_duper: Option<LayoutDirection>, // 默认红色 Duper 方向
pub default_blue_duper: Option<LayoutDirection>, // 默认蓝色 Duper 方向
}关联方法:
impl Cannon {
pub fn from_settings(settings: &CannonSettings) -> Self;
}4.3.2 CannonSettings
持久化炮台配置,支持 JSON 序列化。
定义:
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct CannonSettings {
#[serde(rename = "MaxTNT")]
pub max_tnt: u32, // 最大 TNT 数量
pub default_red_direction: Option<LayoutDirection>,
pub default_blue_direction: Option<LayoutDirection>,
#[serde(rename = "NorthWestTNT")]
pub north_west_tnt: Space3D,
#[serde(rename = "NorthEastTNT")]
pub north_east_tnt: Space3D,
#[serde(rename = "SouthWestTNT")]
pub south_west_tnt: Space3D,
#[serde(rename = "SouthEastTNT")]
pub south_east_tnt: Space3D,
pub offset: Surface2D, // 坐标偏移
pub pearl: PearlInfo, // 珍珠信息
}辅助结构:
pub struct PearlInfo {
pub motion: Space3D, // 初始速度
pub position: Space3D, // 初始位置
}
pub struct Surface2D {
pub x: f64,
pub z: f64,
}4.4 计算输入
4.4.1 GeneralData
通用模拟输入数据。
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GeneralData {
pub pearl_position: Space3D, // 珍珠初始位置
pub pearl_motion: Space3D, // 珍珠初始速度(含 TNT 推力)
pub tnt_charges: Vec<TNT>, // TNT 列表(用于分时爆炸)
}4.4.2 TNT
TNT 实体配置。
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TNT {
pub position: Space3D, // 位置
pub fuse: u32, // 引信 Tick(0 = 立即爆炸)
}4.5 计算输出
4.5.1 TNTResult
TNT 搜索结果,单条配置方案。
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TNTResult {
pub distance: f64, // 与目标的 2D 距离
pub tick: u32, // 命中 Tick
pub blue: u32, // 蓝色 TNT 数量
pub red: u32, // 红色 TNT 数量
pub vertical: u32, // 垂直 TNT 数量(Vector3D 模式)
pub total: u32, // 总 TNT 数量
pub pearl_end_pos: Space3D, // 珍珠最终位置
pub pearl_end_motion: Space3D, // 珍珠最终速度
pub direction: Direction, // 飞行方向
pub yaw: f64, // 飞行偏航角(度)
pub pitch: f64, // 飞行俯仰角(度)
}4.5.2 CalculationResult
轨迹追踪结果,包含完整飞行路径。
#[derive(Debug, Clone, PartialEq)]
pub struct CalculationResult {
pub landing_position: Space3D, // 最终落点
pub pearl_trace: Vec<Space3D>, // 位置轨迹
pub pearl_motion_trace: Vec<Space3D>,// 速度轨迹
pub is_successful: bool, // 是否命中目标
pub tick: u32, // 模拟的最大 Tick 数(即传入的 max_ticks)
pub final_motion: Space3D, // 最终速度
pub distance: f64, // 与目标距离
}4.6 枚举类型
4.6.1 Direction
基本方向枚举,使用位标志表示。
#[repr(u8)]
pub enum Direction {
North = 1, // 0b0001
South = 2, // 0b0010
East = 4, // 0b0100
West = 8, // 0b1000
}方法:
| 方法 | 说明 |
|---|---|
invert() | 返回相反方向 |
from_angle(angle: f64) | 从角度判定方向 |
4.6.2 LayoutDirection
布局方向枚举,表示象限。
pub enum LayoutDirection {
NorthWest, // 西北
NorthEast, // 东北
SouthWest, // 西南
SouthEast, // 东南
North, // 北(v2.1 新增,用于位运算备选方向)
South, // 南(v2.1 新增)
West, // 西(v2.1 新增)
East, // 东(v2.1 新增)
}4.6.3 PearlVersion
Minecraft 版本枚举,决定物理计算方式。
pub enum PearlVersion {
Legacy, // ≤ 1.20.4(float 精度)
Post1205, // 1.20.5 - 1.21.1
Post1212, // ≥ 1.21.2(运算顺序变化)
}4.7 数据结构关系图
graph TD
subgraph Core["核心层"]
Space3D --> EntityData
AABBBox --> EntityData
EntityData --> PearlEntity
Space3D --> GeneralData
GeneralData --> TNT
Space3D --> TNTResult
Space3D --> CalculationResult
end
subgraph Config["配置层"]
Space3D --> CannonSettings
Surface2D --> CannonSettings
PearlInfo --> CannonSettings
LayoutDirection --> CannonSettings
CannonSettings --> Cannon
Pearl --> Cannon
end
subgraph Enum["枚举"]
Direction
LayoutDirection
PearlVersion
end5. 接口文档
调用计算引擎需要的全部信息。两种接口:Tauri IPC 命令(桌面版)和 WASM 导出函数(网页版)。
5.1 接口总览
| 运行时 | 接口类型 | 数量 |
|---|---|---|
| Tauri 桌面 | IPC 命令 | 6 |
| WASM 网页 | 导出函数 | 3 |
5.2 Tauri Commands
5.2.1 calculate_tnt_amount_command
功能:搜索能命中目标的 TNT 配置方案。
调用方式:
invoke<TNTResultOutput[]>("calculate_tnt_amount_command", { input: CalculationInput })输入参数 CalculationInput:
| 字段 | 类型 | 说明 |
|---|---|---|
pearlX, pearlY, pearlZ | f64 | 珍珠初始位置 |
pearlMotionX, pearlMotionY, pearlMotionZ | f64 | 珍珠初始速度 |
offsetX, offsetZ | f64 | 坐标偏移量 |
cannonY | f64 | 炮台 Y 坐标 |
northWestTnt, northEastTnt, southWestTnt, southEastTnt | Space3DInput | 四方向 TNT 位置 |
defaultRedDirection, defaultBlueDirection | string | Duper 方向 |
destinationX, destinationZ | f64 | 目标坐标 |
maxTnt | u32 | 最大 TNT 数量 |
maxTicks | u32 | 最大搜索 Tick |
maxDistance | f64 | 最大误差距离 |
version | string | MC 版本 ("Legacy" / "Post1205" / "Post1212") |
输出 TNTResultOutput[]:
| 字段 | 类型 | 说明 |
|---|---|---|
distance | f64 | 与目标的 2D 距离 |
tick | u32 | 命中 Tick |
blue, red, total | u32 | TNT 数量 |
pearlEndPos | Space3DOutput | 珍珠最终位置 |
pearlEndMotion | Space3DOutput | 珍珠最终速度 |
direction | string | 飞行方向 |
5.2.2 calculate_pearl_trace_command
功能:计算指定 TNT 配置下的完整珍珠轨迹。
调用方式:
invoke<PearlTraceOutput>("calculate_pearl_trace_command", { input: PearlTraceInput })输入参数 PearlTraceInput:
| 字段 | 类型 | 说明 |
|---|---|---|
redTnt, blueTnt | u32 | 红/蓝 TNT 数量 |
pearlX, pearlY, pearlZ | f64 | 珍珠初始位置 |
pearlMotionX, pearlMotionY, pearlMotionZ | f64 | 珍珠初始速度 |
offsetX, offsetZ | f64 | 坐标偏移量 |
cannonY | f64 | 炮台 Y 坐标 |
northWestTnt, northEastTnt, southWestTnt, southEastTnt | Space3DInput | 四方向 TNT 位置 |
defaultRedDirection, defaultBlueDirection | string | Duper 方向 |
destinationX, destinationZ | f64 | 目标坐标 |
direction | string? | 飞行方向(可选) |
version | string | MC 版本 |
输出 PearlTraceOutput:
| 字段 | 类型 | 说明 |
|---|---|---|
landingPosition | Space3DOutput | 最终落点 |
pearlTrace | Space3DOutput[] | 位置轨迹 |
pearlMotionTrace | Space3DOutput[] | 速度轨迹 |
isSuccessful | bool | 是否命中目标 |
tick | u32 | 飞行 Tick 数 |
finalMotion | Space3DOutput | 最终速度 |
distance | f64 | 与目标距离 |
closestApproach | ClosestApproachOutput? | 最近接近点 |
5.2.3 calculate_raw_trace_command
功能:自由模拟器,支持自定义 TNT 位置和数量。
调用方式:
invoke<PearlTraceOutput>("calculate_raw_trace_command", { input: RawTraceInput })输入参数 RawTraceInput:
| 字段 | 类型 | 说明 |
|---|---|---|
pearlX, pearlY, pearlZ | f64 | 珍珠初始位置 |
pearlMotionX, pearlMotionY, pearlMotionZ | f64 | 珍珠初始速度 |
tntGroups | TntGroupInput[] | TNT 组列表 |
version | string | MC 版本 |
TntGroupInput:
| 字段 | 类型 | 说明 |
|---|---|---|
x, y, z | f64 | TNT 位置 |
amount | u32 | TNT 数量 |
5.2.4 verify_config
功能:验证配置文件格式是否正确。
调用方式:
invoke<string>("verify_config", { path: string })输入:配置文件绝对路径
输出:验证结果消息或错误信息
5.2.5 load_config
功能:从文件加载炮台配置。
调用方式:
invoke<CannonSettings>("load_config", { path: string })输入:配置文件绝对路径
输出:第一个炮台配置的 JSON 对象
5.2.6 load_config_from_content
功能:从 JSON 字符串加载炮台配置。
调用方式:
invoke<CannonSettings>("load_config_from_content", { content: string })输入:配置文件 JSON 内容
输出:第一个炮台配置的 JSON 对象
5.3 WASM Exports
WASM 模块从最初的 3 个导出函数增长到 20 个,通过 wasm-bindgen 暴露给 JS。核心计算函数不变,新增的全是配置管理、Bit 模板操作、二进制编解码的辅助函数。
5.3.1 calculate_tnt_amount
功能:与 Tauri 版本相同,搜索 TNT 配置方案。
签名:
function calculate_tnt_amount(input: CalculationInput): TNTResultOutput[];使用示例:
import init, { calculate_tnt_amount } from "pearl_calculator_wasm";
await init();
const results = calculate_tnt_amount({
pearlX: 0.5,
pearlY: 64.0,
pearlZ: 0.5,
// ... 其他参数
});5.3.2 calculate_pearl_trace
功能:计算指定 TNT 配置下的完整轨迹。
签名:
function calculate_pearl_trace(input: PearlTraceInput): PearlTraceOutput;5.3.3 calculate_raw_trace
功能:自由模拟器。
签名:
function calculate_raw_trace(input: RawTraceInput): PearlTraceOutput;5.3.4 新增的配置管理函数
// JSON 配置解析(浏览器端替代 filesystem)
function parse_configuration_content(content: string, path: string): ImportedConfiguration;
// Draft(配置向导的字符串中间态)↔ GeneralConfig 互转
function convert_draft_to_config(draft, center, red_tnt_location, mode): GeneralConfig;
function convert_config_to_draft(config): { config: DraftConfig, center, momentum };
// 导出与编码
function build_export_config(draft, center, red_tnt_location, bit_state, mode, mult_state): object;
function build_encodable_config(config, bit_template): EncodableConfig;
function encode_config(config): string; // → Base64 字符串(带 # 前缀)
function decode_config(code): DecodedConfig;5.3.5 新增的 Bit 模板函数
function validate_bit_template(values: number[]): BitValidationResult;
function decode_bit_value(values: number[], target: number): BitDecodeResult;
function decode_with_multiplier(values: number[], multiplier: number, target: number): MultiplierDecodeResult;
function get_direction_bits(masks: MaskGroup[], direction: string): [boolean, boolean];
function parse_template_values(state: BitInputState): number[] | null;
function calculate_bits(state, blue_tnt: number, red_tnt: number, direction: string): BitCalculationResponse;
// BitTemplateConfig ↔ BitInputState(UI 编辑用)
function config_to_input_state(config): BitInputState;
function input_state_to_config(state): BitTemplateConfig;
// Multiplier 版本
function config_to_multiplier_input_state(config): MultiplierBitInputState;
function input_state_to_multiplier_config(state): MultiplierConfig;所有函数都走 serde_wasm_bindgen 做 JSON 序列化,不需要手动处理字符串。
5.4 Bridge API 层
pearl_calculator_bridge/src/api.rs 是 bridge 新增的薄封装层,三行代码能看出它的定位:
pub fn calculate_tnt_amount(input: CalculationInput) -> Result<Vec<TNTResultOutput>, String> {
let cannon = input.get_cannon()?;
let results = core_calculate_tnt_amount(&cannon, ...);
Ok(results.into_iter().map(Into::into).collect())
}以前 Tauri commands 和 WASM 函数各自写参数解析、类型转换、错误处理,很容易两边不一致。api.rs 统一了这一层——Tauri 和 WASM 都直接调它,参数校验和结果映射只写一次。
5.5 通用数据类型
5.5.1 Space3DInput(输入)
interface Space3DInput {
x: number;
y: number;
z: number;
}5.5.2 Space3DOutput(输出)
interface Space3DOutput {
X: number; // 注意大写
Y: number;
Z: number;
}5.5.3 ClosestApproachOutput
interface ClosestApproachOutput {
tick: number; // 最近接近 Tick
point: Space3DOutput; // 最近接近点坐标
distance: number; // 最近距离
}5.6 错误处理
所有 API 调用均返回 Result 类型:
| 错误类型 | 说明 |
|---|---|
"Invalid pearl version" | 版本字符串不合法 |
"Invalid red direction" | 红色 Duper 方向不合法 |
"Pearl trace calculation failed" | 轨迹计算失败 |
"Raw trace calculation failed" | 自由模拟失败 |
"No cannon configurations found in the file" | 配置文件无有效炮台 |
"Failed to parse configuration: ..." | 配置 JSON 解析失败 |
5.7 调用流程图
sequenceDiagram
participant JS as JavaScript
participant Bridge as Bridge Layer
participant Core as Core Engine
alt Tauri 桌面模式
JS->>+Bridge: invoke("calculate_tnt_amount_command", input)
Note over Bridge: 反序列化 CalculationInput
Bridge->>+Core: calculate_tnt_amount(&cannon, ...)
Core-->>-Bridge: Vec<TNTResult>
Note over Bridge: 序列化为 TNTResultOutput[]
Bridge-->>-JS: JSON 结果
else WASM 网页模式
JS->>+Bridge: calculate_tnt_amount(JsValue)
Note over Bridge: serde_wasm_bindgen::from_value
Bridge->>+Core: calculate_tnt_amount(&cannon, ...)
Core-->>-Bridge: Vec<TNTResult>
Note over Bridge: serde_wasm_bindgen::to_value
Bridge-->>-JS: JsValue 结果
end6. 附录
6.1 参考资料
- Minecraft Wiki: https://zh.minecraft.wiki/w/%E5%AE%9E%E4%BD%93#%E8%BF%90%E5%8A%A8
- LegendsOfSky/PearlCalculatorCore: https://github.com/LegendsOfSky/PearlCalculatorCore/tree/main/PearlCalculatorLib
6.2
这是モニカ・エヴァレット,没别的意思,她也没走丢,也没生病,也没有被我绑架。就是她实在太可爱了,忍不住想发出来让你们也看看。
7. 配置系统与工程实践
这一章全是 v2.0 之后新增的东西,跟核心计算无关,但占了这个项目一半以上的开发时间。
7.1 二进制配置编码
每次分享炮台配置要么贴一大段 JSON,要么截图手动填。v2.1 搞了一套二进制序列化方案:把配置压成 Base64 字符串,前面加个 # 前缀,能直接塞 URL 里。接收方拷进去瞬间还原。
编码格式(总长 ~50-140 字节不等):
| 偏移 | 字段 | 编码 |
|---|---|---|
| 0 | Header | side_mode (5 bits) + is_red_arrow_center (1 bit) |
| 1-2 | MaxTNT | u16 LE |
| 3-5 | Float mask | 3 字节位掩码(标记 20 个 f32 中哪些非零) |
| 可变 | Float values | 非零值按序写 f32 LE(每个 4 字节) |
| 可变 | Direction byte 1 | Red 方向 (4 bits) + Blue 方向 (4 bits) |
| 可变 | Direction byte 2 | 4 个方向掩码(各 2 bits) |
| 可变 | RedValues | Varint 编码,每个 side 一个 |
| 末字节 | CRC8 | x⁸ + x² + x + 1 |
// utils/config.rs
pub fn encode_config(data: &EncodableConfig) -> Result<String, String> {
// ... 逐字段写入 Vec<u8> → Base64 → 加 # 前缀
}
pub fn decode_config(input: &str) -> Result<DecodedConfig, String> {
// 去 # 前缀 → Base64 解码 → CRC8 校验 → 逐字段读出
}20 个浮点字段的排序是:四个 TNT 位置 (12 个 f32) → Offset X/Z (2) → Pearl 位置 (3) → Pearl 速度 (3)。大部分配置里超过一半是零,位掩码直接跳过去,省了不少字节。
WASM 导出:encode_config、decode_config、build_encodable_config,浏览器里也能用。
7.2 Bit 模板与 Multiplier 系统
FTL 炮台用红石电路控制 TNT 数量,但布线方式不是”放几组就点几组”——是通过红石刻(bit)的组合来表示。比如模板值是 [1, 2, 4, 8],要激活 5 个 TNT 就拨 1 + 4 两 bit。
验证:确保全覆盖
Bit 模板有个要求:所有 ≤ 总和的整数都能被某个子集表示。检查方法是贪心的:
// bit.rs
pub fn validate_bit_template(values: &[u32]) -> BitValidationResult {
let mut sorted = values.to_vec();
sorted.sort_unstable();
if sorted.first() != Some(&1) { return error("no_unit"); }
let mut max_reach = 0u32;
for v in sorted {
if v > max_reach + 1 { return error("gap", max_reach + 1); }
max_reach += v;
}
BitValidationResult { is_valid: true, .. }
}三个错误类型:
empty:没填任何值no_unit:没有 1(任何数都无法表示)gap:存在覆盖空洞(比如 [1, 3] → 2 无法表示)
解码:贪心子集和
给目标 TNT 数,算出要激活哪些 bit。标准贪心——从大到小遍历,能减就减:
pub fn decode_bit_value(values: &[u32], target: u32) -> BitDecodeResult {
// 降序排列 → 贪心选取 → 返回 activated_indices
}Multiplier 扩展
某些炮台用乘法器——每个 bit 代表的不是 1/2/4/8 而是 200/400/800/1600。decode_with_multiplier() 在贪心前先乘倍数,其余逻辑一致。默认 multiplier = 200。
一个完整的 calculate_bits() 流程:
- 解析 template 字符串值 → u32 数组
validate_bit_template()验证- 对蓝色和红色 TNT 分别
decode_bit_value() get_direction_bits()查飞行方向对应的 bit mask
前端配好模板,结果自动算出来。
7.3 全局状态管理
Tauri 前端状态分散在各个 React 组件里的时候,跨页面数据同步很痛苦。v2.1 在后端搞了个集中式的 Redux 模式。
// app_state.rs
pub struct AppStateSnapshot {
pub config: ConfigStoreState, // 当前加载的配置
pub configuration: ConfigurationStoreState, // 配置向导状态
pub calculator: CalculatorStoreState, // 计算器 + 模拟器状态
}
pub enum AppStateAction {
SetHasConfig(bool),
SetConfigData(GeneralConfig),
SetVersion(PearlVersion),
UpdateDefaultInput(CalculatorInputField), // 计算器输入变更
SetDefaultResults(Vec<TNTResultOutput>),
ApplyConfigToCalculator, // ← 跨 store 编排
ApplyWizardToCalculator, // ← 跨 store 编排
// ... 共 ~30 个变体
}两个 Tauri 命令:
get_app_state → 获取完整快照
dispatch_app_state_action → 派发单个 action → 返回新快照AppStateStore 是 Mutex<AppStateSnapshot>,通过 Tauri managed state 注入。前端任何时候都能拿到一致的状态快照。
关键设计在跨 store 编排 action:
ApplyConfigToCalculator:配置文件加载完 → 自动填充计算器输入(珍珠位置、炮台 Y、偏移量)ApplyWizardToConverter:配置向导完成 →convert_draft_to_config()→ 更新 config + bit template + multiplier → 自动填入计算器ApplyConfigToWizard:编辑已有配置 →convert_config_to_draft()→ 回填向导所有字段
这些编排不用前端手动串联,dispatch 一个 action 就行。
7.4 测试框架
v2.1 加了完整的回归测试,之前完全靠手工验证。
pearl_calculator_core/tests/
├── common.rs # 从 tests/input_cases/*.json 加载测试用例
├── gen_snapshot.rs # #[ignore] 生成 tests/fixtures/snapshot.json
├── regression_test.rs # 核心:回归 + solver 自愈验证
├── input_cases/
│ ├── case_a.json # 真实炮台配置
│ ├── case_b.json
│ └── case_c.json
└── fixtures/
└── snapshot.json # 黄金标准(生成后不应手动改)test_regression_against_snapshot() 做的事:
- 加载 snapshot.json 中所有测试用例
- 重新运行
calculate_pearl_trace(),检查:- 落点与 snapshot 偏差 < 1e-8
- tick=10 检查点偏差 < 1e-6
- solver 能反向恢复原始 TNT 数量(对 red < 100k 且 blue < 10k 的用例)
- 任何 case 失败 → 测试直接挂
test_standard_plane_intercept_solver_smoke() 是 Y-plane 的专项冒烟测试。
新增 snapshot 用例时:改 gen_snapshot.rs 的 #[ignore] → #[test] → 跑一遍 → 把输出的 snapshot.json 覆盖到 fixtures/ → 改回 #[ignore]。