返回提交历史
Modified
README.md
+4
-2
Modified
docs/API.md
+44
-3
Modified
src/XFE.SeAgent.Plugin/Game/GameDebugApi.cs
+1
-1
Modified
src/XFE.SeAgent.Plugin/Game/WorldQueries.cs
+76
-2
SpaceEngineersModDev/XFE.SpaceEngineers.AgentBridge
增强网格/方块完整度与连接器遥测统计
为 grids.get、blocks.get、telemetry.snapshot 增加完整度与损伤统计,支持返回受损方块详情。补充文档说明完整度字段及用法。显著增强连接器遥测信息,便于诊断与自动化测试。
0a8b350
代码差异
4 个文件
+125
-8
@@ -56,13 +56,13 @@ $cli = './src/XFE.SeAgent.Cli/bin/Release/net10.0/xfe-se.exe'
56
56
| --- | --- |
57
57
| `agent.ping` / `agent.capabilities` / `agent.events` | 协议探测、能力查询、带序号的事件日志 |
58
58
| `world.status` / `world.load` / `world.save` / `world.pause` / `world.exit` | 世界就绪状态、测试副本加载/保存、由插件持有的暂停,以及保存成功后正常退出游戏 |
59
| `grids.list` / `grids.get` | 网格标识、位置和朝向、包围盒、速度及物理状态 |
59
| `grids.list` / `grids.get` | 网格标识、位置和朝向、包围盒、速度及物理状态;`grids.get` 另含装甲在内的船体完整度与损伤汇总 |
60
60
| `blocks.list` / `blocks.get` | 设备、所属刚性网格、CustomData 与详细信息 |
61
61
| `blocks.actions` / `blocks.action` | 枚举和执行实际终端动作 |
62
62
| `blocks.properties` / `blocks.setProperty` | 枚举并设置受支持的终端属性类型 |
63
63
| `pb.read` / `pb.inspect` | 源码、哈希、配置、持久数据、Echo、编译错误、运行预算以及受限的脚本实例字段快照 |
64
64
| `pb.deploy` / `pb.run` | 带预期哈希校验和备份的源码部署、真实编译、运行参数 |
65
| `telemetry.snapshot` | 电池、气罐、推力和覆盖、陀螺、连接器、相机、库存、文本屏幕、编程块、舰船质量和速度 |
65
| `telemetry.snapshot` | 电池、气罐、推力和覆盖、陀螺、连接器约束点与吸附状态、相机、库存、文本屏幕、编程块、舰船质量、速度和完整度 |
66
66
| `cameras.scan` | 使用指定实际摄像机的充能和视场进行探测,返回命中位置与实体 |
67
67
| `grid.stop` | 测试网格清速度、推力/陀螺覆盖;可同时禁用其编程块,防止下一帧再次施力 |
68
68
| `debug.screenshot` | 请求引擎输出游戏截图,便于与遥测核对 |
@@ -71,6 +71,8 @@ PB 部署备份保存在测试存档的 `Storage/XFE.AgentBridge/Backups` 中,
71
71
72
72
`pb.inspect` 只遍历脚本程序集自身字段和有限集合,深度最多 3 层、总节点和字符串均有限制。不会执行任意 C#、调用用户指定的反射方法或遍历整个游戏对象图。
73
73
74
对接诊断可读取连接器的实际 `constraintPosition`、约束对端和距离、可连接/吸附状态及交易保护,避免把方块中心当作连接点。船体 `integrity` 区分已生效损伤、待结算伤害和未完成建造,并记录变形数量;每网格最多检查 20000 块,最多列出 32 个受损位置,超出时明确标记截断。普通 `grids.list` 不进行完整度汇总。用相同网格的飞行前后基线核对方块数量与完整度,具体字段见 [API 参考](docs/API.md)。
75
74
76
## 通信和执行语义
75
77
76
78
管道名为 `XFE.SE.Agent.<PID>`,ACL 仅允许当前 Windows 用户并拒绝网络登录身份,不开 TCP 端口。每个连接发送一行 UTF-8 JSON,获得一行响应:
@@ -73,7 +73,7 @@ JSON 字段名区分大小写。请求最大深度 32,不接受重复属性名
73
73
| 结构 | 字段 |
74
74
| --- | --- |
75
75
| `Grid` | `entityId`, `name`, `isStatic`, `gridSize`, `blockCount`, `pose`, `bounds`;存在物理对象时还有 `linearVelocity`, `angularVelocity`, `speed`。 |
76
| `Block` | `entityId`, `gridId`, `name`, `type`, `subtype`, `functional`, `working`, `ownerId`, `positionInGrid`, `pose`, `bounds`, `inventoryCount`;功能方块另有 `enabled`。 |
76
| `Block` | `entityId`, `gridId`, `name`, `type`, `subtype`, `functional`, `working`, `ownerId`, `positionInGrid`, `pose`, `bounds`, `inventoryCount`;功能方块另有 `enabled`,可取得底层方块时另有下述 `integrity`。 |
77
77
| 详细 `Block` | `Block` 加 `customData`, `detailedInfo`, `customInfo`。后两项各保留前 32768 字符,超出追加 `…`;`customData` 不截断,仍受响应总大小限制。 |
78
78
79
79
## 3. Agent 与世界生命周期
@@ -99,7 +99,7 @@ JSON 字段名区分大小写。请求最大深度 32,不接受重复属性名
99
99
| 方法 | 参数 | `result` |
100
100
| --- | --- | --- |
101
101
| `grids.list` | `name`:可选名称子串;`offset`:默认 0、负数按 0;`limit`:默认 256、夹在 1–2048 | `{grids:[Grid],total,offset,truncated}`。过滤后的网格按实体 ID 排序;名称不区分大小写。 |
102
| `grids.get` | `entityId`:必填网格 ID | `Grid`。 |
102
| `grids.get` | `entityId`:必填网格 ID | `Grid` 加下述网格 `integrity` 汇总,包含普通装甲块。 |
103
103
| `blocks.list` | 下述选择参数;`offset`、`limit` 同 `grids.list` | `{blocks:[Block],total,offset,truncated}`。只列终端方块,不包含普通装甲块。 |
104
104
| `blocks.get` | `entityId`:必填终端方块 ID | 详细 `Block`。 |
105
105
| `grid.stop` | `entityId`:必填网格 ID;`disableProgrammableBlocks`:可选布尔,默认 false;写权限 | `Grid` 加 `clearedThrusters`, `clearedGyros`, `disabledProgrammableBlocks`, `note`。清零该网格推进器覆盖,清零陀螺仪 yaw/pitch/roll 并关闭覆盖,清除该网格速度;选项为 true 时禁用该网格 PB。 |
@@ -117,6 +117,32 @@ JSON 字段名区分大小写。请求最大深度 32,不接受重复属性名
117
117
118
118
`grid.stop` 是一次性操作,不会持续冻结网格,不会禁用所有推进器,也不会停止其他相连网格。仍在运行的控制器/PB 可以再次施力;调试时按需显式设置 `disableProgrammableBlocks:true` 并查询相关网格。
119
119
120
### 完整度与船体损伤
121
122
单个 `Block.integrity` 直接读取游戏方块的完整度与损伤,数值保留游戏精度,不是百分比:
123
124
| 字段 | 含义 |
125
| --- | --- |
126
| `current`, `build`, `maximum` | 当前完整度、已经建造的组件完整度、完整建造时的上限。 |
127
| `currentDamage` | `build - current`,已生效的损伤;不会把尚未建造的组件算作损伤。 |
128
| `accumulatedDamage` | 已累积、尚待游戏结算的伤害。 |
129
| `hasDeformation` | 是否存在骨骼变形。 |
130
131
`grids.get` 和 `telemetry.snapshot.result.grids[]` 返回网格 `integrity`。它遍历该**单个网格**的所有方块类型,包含装甲,字段如下:
132
133
| 字段 | 含义 |
134
| --- | --- |
135
| `inspectedBlockCount`, `truncated` | 本次实际检查数量,以及是否存在未检查方块。每网格最多检查 **20000 块**;`truncated:true` 时以下总量只代表已检查部分。完整方块数仍见 `Grid.blockCount`。 |
136
| `current`, `build`, `maximum`, `currentDamage`, `accumulatedDamage` | 已检查方块对应数值的累加。 |
137
| `damagedBlockCount` | `currentDamage > 0` 或 `accumulatedDamage > 0` 的方块数量。 |
138
| `deformedBlockCount`, `incompleteBlockCount` | 发生变形的数量,以及 `build < maximum` 的数量;同一块可能同时计入多个类别。 |
139
| `damageLocations` | 最多 32 个受损或变形方块;每项为单块 `integrity` 字段加 `positionInGrid`。以网格坐标定位,不依赖返回顺序。 |
140
| `damageLocationsTruncated` | 已检查部分中,是否还有未列出的受损/变形位置;网格扫描是否完整另查 `truncated`。 |
141
142
普通 `grids.list` 和 `grid.stop` 的 `Grid` 不进行这项汇总。遥测只汇总本次实际返回方块所属的网格,不自动包含连接器或机械连接的其他网格。高频观察时用精确 `entityIds` 或 `gridId` 限定范围。
143
144
核对飞行损伤时,先保存相同网格 ID 的基线,再比较 `blockCount`、完整度、伤害和变形数量;被摧毁并移除的方块不会继续出现在当前损伤列表。完整度没有变化只能说明这些指标未记录到损伤,不能单独证明没有发生接触。
145
120
146
## 5. 终端动作和属性
121
147
122
148
| 方法 | 参数 | `result` |
@@ -233,7 +259,7 @@ JSON 字段名区分大小写。请求最大深度 32,不接受重复属性名
233
259
| `battery` | `storedMWh`, `maxStoredMWh`, `inputMW`, `outputMW`, `chargeMode`, `charging`。 |
234
260
| `thrust` | `currentN`, `maximumN`, `maxEffectiveN`, `overrideN`, `overrideRatio`, `gridDirection`, `forceDirection`。`forceDirection` 为该推进器世界矩阵的 Backward。 |
235
261
| `gyro` | `override`, `power`, `yaw`, `pitch`, `roll`。 |
236
| `connector` | `status`, `otherConnectorId`(无对端为 null), `throwOut`, `collectAll`, `pullStrength`。 |
262
| `connector` | `status`, `connected`, `connectable`, `otherConnectorId`(无对端为 null), `throwOut`, `collectAll`, `pullStrength`;实际游戏连接器另有下述约束点、吸附和交易字段。 |
237
263
| `camera` | `enabledRaycast`, `availableScanRange`, `coneLimitDegrees`, `distanceLimit`。 |
238
264
| `gasTank` | `capacity`, `filledRatio`, `stockpile`。 |
239
265
| `flight` | `linearVelocity`, `angularVelocity`, `speed`, `naturalGravity`, `artificialGravity`, `totalMassKg`, `physicalMassKg`, `baseMassKg`, `centerOfMass`, `dampeners`, `underControl`, `controlThrusters`, `moveIndicator`, `rotationIndicator:{x,y}`, `rollIndicator`。仅船舶控制器提供。 |
@@ -242,6 +268,21 @@ JSON 字段名区分大小写。请求最大深度 32,不接受重复属性名
242
268
| `cargoInventory` | 有库存方块的分类标记;仅货箱、钻头、连接器为 true。反应堆、氢氧制造机等仍可返回库存,但此标记为 false;统计矿机待卸货库存时可据此排除燃料/生产库存。 |
243
269
| `screens` | 每方块最多 16 个屏幕,每项 `index`, `name`, `displayName`, `contentType`, `text`, `script`, `surfaceSize:{x,y}`;`text` 最多 8192 字符后加 `…`。没有屏幕时可缺省整个字段。 |
244
270
271
### 连接器精确遥测
272
273
| 字段 | 含义 |
274
| --- | --- |
275
| `connected`, `connectable` | 分别表示 `status` 为 `Connected`、`Connectable`。 |
276
| `constraintPosition` | `MyShipConnector.ConstraintPositionWorld()` 返回的世界坐标,单位米;用于比较连接约束点,不能用 `pose.position` 的方块中心替代。 |
277
| `inConstraint` | 游戏的 `InConstraint` 标志。 |
278
| `magnetized` | 本桥计算的 `inConstraint && !connected`,表示已进入约束但还未锁定;不表示存在磁力范围内的所有候选接口。 |
279
| `constraintOtherConnectorId` | 游戏实际 `Other` 的实体 ID,无对端时为 null;吸附阶段也可能已有值。原有 `otherConnectorId` 来自脚本接口的 `OtherConnector`,不应用它代替本字段判断吸附对端。 |
280
| `otherConstraintPosition`, `constraintDistance` | 仅实际 `Other` 非空时返回,对端约束点及两个约束点的欧氏距离(米)。还未吸附时要在同一次查询中明确选择两端连接器,比较各自 `constraintPosition`。 |
281
| `isSmallConnector` | 游戏的小型接口类别标志,不等同于“安装在小网格上”。 |
282
| `tradingEnabled`, `protectedFromLockingByTrading` | 实际交易开关,以及游戏当前是否因交易保护而阻止锁定。 |
283
284
这些字段通过固定的游戏 API 只读取得,不主动寻找、吸附或连接接口。约束点距离只是捕获条件之一;还须结合状态、功能/供电、所有者、接口类型、方向与交易保护判断。捕获阈值属于当前游戏实现,插件不将某个固定距离当成通用的“可以连接”结论。
285
245
286
`screens[].text` 是文本接口读到的字符串,不是 Sprite/图形画面的 OCR。遥测是一次游戏线程采样;需要趋势时用 CLI `watch`。库存物品、屏幕文本或大量详细对象可能触发 4 MiB 响应上限,先减少数量或关闭可选展开项。
246
287
247
288
## 9. CLI 调用
@@ -45,7 +45,7 @@ namespace XFE.SeAgent.Plugin.Game
45
45
case "world.exit": RequireWritableWorld(); return ExitWorld(args);
46
46
case "world.pause": RequireWritableWorld(); return PauseWorld(args);
47
47
case "grids.list": RequireWorld(); return ListGrids(args);
48
case "grids.get": RequireWorld(); return DescribeGrid(Grid(Id(args, "entityId")));
48
case "grids.get": RequireWorld(); return DescribeGridWithIntegrity(Grid(Id(args, "entityId")));
49
49
case "blocks.list": RequireWorld(); return ListBlocks(args);
50
50
case "blocks.get": RequireWorld(); return DescribeBlock(Block(Id(args, "entityId")), true);
51
51
case "blocks.actions": RequireWorld(); return ListActions(args);
@@ -7,6 +7,8 @@ using Sandbox.ModAPI.Ingame;
7
7
using VRage.Game.ModAPI.Ingame;
8
8
using VRageMath;
9
9
using Terminal = Sandbox.ModAPI.Ingame.IMyTerminalBlock;
10
using SlimBlock = Sandbox.Game.Entities.Cube.MySlimBlock;
11
using ShipConnector = Sandbox.Game.Entities.Cube.MyShipConnector;
10
12
11
13
namespace XFE.SeAgent.Plugin.Game
12
14
{
@@ -35,6 +37,49 @@ namespace XFE.SeAgent.Plugin.Game
35
37
return result;
36
38
}
37
39
40
private static JObject DescribeGridWithIntegrity(MyCubeGrid grid)
41
{
42
var result = DescribeGrid(grid);
43
// Include armor as well as terminal blocks. Bound work on unusually large grids;
44
// ordinary grids.list queries do not perform this scan.
45
const int maximumBlocks = 20000;
46
int inspected = 0, damaged = 0, deformed = 0, incomplete = 0, affected = 0;
47
double integrity = 0, buildIntegrity = 0, maxIntegrity = 0, currentDamage = 0, pendingDamage = 0;
48
var damageLocations = new JArray();
49
foreach (var block in grid.GetBlocks())
50
{
51
if (inspected >= maximumBlocks) break;
52
inspected++;
53
integrity += block.Integrity; buildIntegrity += block.BuildIntegrity; maxIntegrity += block.MaxIntegrity;
54
currentDamage += block.CurrentDamage; pendingDamage += block.AccumulatedDamage;
55
bool isDamaged = block.CurrentDamage > 0 || block.AccumulatedDamage > 0;
56
if (isDamaged) damaged++;
57
if (block.HasDeformation) deformed++;
58
if (block.BuildIntegrity < block.MaxIntegrity) incomplete++;
59
if (isDamaged || block.HasDeformation) affected++;
60
if ((isDamaged || block.HasDeformation) && damageLocations.Count < 32)
61
{
62
var detail = DescribeIntegrity(block);
63
detail["positionInGrid"] = Vec(block.Position);
64
damageLocations.Add(detail);
65
}
66
}
67
result["integrity"] = new JObject { ["inspectedBlockCount"] = inspected,
68
["truncated"] = inspected < grid.GetBlocks().Count, ["current"] = integrity,
69
["build"] = buildIntegrity, ["maximum"] = maxIntegrity, ["currentDamage"] = currentDamage,
70
["accumulatedDamage"] = pendingDamage, ["damagedBlockCount"] = damaged,
71
["deformedBlockCount"] = deformed, ["incompleteBlockCount"] = incomplete,
72
["damageLocations"] = damageLocations, ["damageLocationsTruncated"] = affected > damageLocations.Count };
73
return result;
74
}
75
76
private static JObject DescribeIntegrity(SlimBlock block)
77
{
78
return new JObject { ["current"] = block.Integrity, ["build"] = block.BuildIntegrity,
79
["maximum"] = block.MaxIntegrity, ["currentDamage"] = block.CurrentDamage,
80
["accumulatedDamage"] = block.AccumulatedDamage, ["hasDeformation"] = block.HasDeformation };
81
}
82
38
83
private IEnumerable<Terminal> SelectBlocks(JObject args)
39
84
{
40
85
if (args["entityIds"] is JArray ids)
@@ -65,6 +110,8 @@ namespace XFE.SeAgent.Plugin.Game
65
110
["working"] = block.IsWorking, ["ownerId"] = Sid(block.OwnerId), ["positionInGrid"] = Vec(block.Position), ["pose"] = Pose(block.WorldMatrix),
66
111
["bounds"] = Bounds(block.WorldAABB), ["inventoryCount"] = block.InventoryCount };
67
112
if (block is IMyFunctionalBlock functional) result["enabled"] = functional.Enabled;
113
var slim = (block.CubeGrid as MyCubeGrid)?.GetCubeBlock(block.Position);
114
if (slim != null) result["integrity"] = DescribeIntegrity(slim);
68
115
if (details) { result["customData"] = block.CustomData ?? ""; result["detailedInfo"] = Clip(block.DetailedInfo, 32768); result["customInfo"] = Clip(block.CustomInfo, 32768); }
69
116
return result;
70
117
}
@@ -82,7 +129,7 @@ namespace XFE.SeAgent.Plugin.Game
82
129
catch (Exception error) { item["telemetryError"] = error.GetType().Name + ": " + error.Message; }
83
130
result.Add(item);
84
131
}
85
var grids = blocks.Select(b => b.CubeGrid.EntityId).Distinct().Select(id => DescribeGrid(Grid(id)));
132
var grids = blocks.Select(b => b.CubeGrid.EntityId).Distinct().Select(id => DescribeGridWithIntegrity(Grid(id)));
86
133
return new JObject { ["utc"] = DateTime.UtcNow, ["frame"] = RequireWorld().GameplayFrameCounter, ["grids"] = new JArray(grids), ["blocks"] = result, ["total"] = selected.Count, ["truncated"] = selected.Count > limit };
87
134
}
88
135
@@ -91,7 +138,34 @@ namespace XFE.SeAgent.Plugin.Game
91
138
if (block is IMyBatteryBlock battery) item["battery"] = new JObject { ["storedMWh"] = battery.CurrentStoredPower, ["maxStoredMWh"] = battery.MaxStoredPower, ["inputMW"] = battery.CurrentInput, ["outputMW"] = battery.CurrentOutput, ["chargeMode"] = battery.ChargeMode.ToString(), ["charging"] = battery.IsCharging };
92
139
if (block is IMyThrust thrust) item["thrust"] = new JObject { ["currentN"] = thrust.CurrentThrust, ["maximumN"] = thrust.MaxThrust, ["maxEffectiveN"] = thrust.MaxEffectiveThrust, ["overrideN"] = thrust.ThrustOverride, ["overrideRatio"] = thrust.ThrustOverridePercentage, ["gridDirection"] = Vec(thrust.GridThrustDirection), ["forceDirection"] = Vec(thrust.WorldMatrix.Backward) };
93
140
if (block is IMyGyro gyro) item["gyro"] = new JObject { ["override"] = gyro.GyroOverride, ["power"] = gyro.GyroPower, ["yaw"] = gyro.Yaw, ["pitch"] = gyro.Pitch, ["roll"] = gyro.Roll };
94
if (block is IMyShipConnector connector) item["connector"] = new JObject { ["status"] = connector.Status.ToString(), ["otherConnectorId"] = connector.OtherConnector == null ? null : Sid(connector.OtherConnector.EntityId), ["throwOut"] = connector.ThrowOut, ["collectAll"] = connector.CollectAll, ["pullStrength"] = connector.PullStrength };
141
if (block is IMyShipConnector connector)
142
{
143
var status = connector.Status;
144
var value = new JObject { ["status"] = status.ToString(),
145
["connected"] = status == MyShipConnectorStatus.Connected,
146
["connectable"] = status == MyShipConnectorStatus.Connectable,
147
["otherConnectorId"] = connector.OtherConnector == null ? null : Sid(connector.OtherConnector.EntityId),
148
["throwOut"] = connector.ThrowOut, ["collectAll"] = connector.CollectAll, ["pullStrength"] = connector.PullStrength };
149
item["connector"] = value;
150
if (block is ShipConnector actual)
151
{
152
var point = actual.ConstraintPositionWorld();
153
value["constraintPosition"] = Vec(point);
154
value["inConstraint"] = actual.InConstraint;
155
value["magnetized"] = actual.InConstraint && status != MyShipConnectorStatus.Connected;
156
value["isSmallConnector"] = actual.IsSmallConnector;
157
value["tradingEnabled"] = actual.TradingEnabled.Value;
158
value["protectedFromLockingByTrading"] = actual.IsProtectedFromLockingByTrading();
159
var other = actual.Other;
160
value["constraintOtherConnectorId"] = other == null ? null : Sid(other.EntityId);
161
if (other != null)
162
{
163
var otherPoint = other.ConstraintPositionWorld();
164
value["otherConstraintPosition"] = Vec(otherPoint);
165
value["constraintDistance"] = Vector3D.Distance(point, otherPoint);
166
}
167
}
168
}
95
169
if (block is IMyCameraBlock camera) item["camera"] = new JObject { ["enabledRaycast"] = camera.EnableRaycast, ["availableScanRange"] = camera.AvailableScanRange, ["coneLimitDegrees"] = camera.RaycastConeLimit, ["distanceLimit"] = camera.RaycastDistanceLimit };
96
170
if (block is IMyGasTank tank) item["gasTank"] = new JObject { ["capacity"] = tank.Capacity, ["filledRatio"] = tank.FilledRatio, ["stockpile"] = tank.Stockpile };
97
171
if (block is IMyShipController controller)