cortico 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +47 -0
- package/package.json +43 -0
- package/src/boot.ts +46 -0
- package/src/bot.ts +1342 -0
- package/src/config-file.ts +41 -0
- package/src/core/README.md +92 -0
- package/src/core/billing.ts +26 -0
- package/src/core/blobs.ts +106 -0
- package/src/core/bus.ts +311 -0
- package/src/core/config-schema.ts +201 -0
- package/src/core/config.ts +262 -0
- package/src/core/core.ts +699 -0
- package/src/core/cost.ts +244 -0
- package/src/core/event-store.ts +304 -0
- package/src/core/fork.ts +113 -0
- package/src/core/generation.ts +118 -0
- package/src/core/instance-lock.ts +215 -0
- package/src/core/ipc-logger.ts +105 -0
- package/src/core/language.ts +62 -0
- package/src/core/log-context.ts +38 -0
- package/src/core/loop.ts +1858 -0
- package/src/core/markers.ts +48 -0
- package/src/core/prefix.ts +121 -0
- package/src/core/run.ts +95 -0
- package/src/core/secrets.ts +18 -0
- package/src/core/session.ts +56 -0
- package/src/core/sessions.ts +261 -0
- package/src/core/state.ts +104 -0
- package/src/core/template.ts +72 -0
- package/src/core/timers.ts +129 -0
- package/src/core/tool-log.ts +135 -0
- package/src/core/transcript.ts +92 -0
- package/src/core/truncate.ts +121 -0
- package/src/core/types.ts +1140 -0
- package/src/core/usage-log.ts +79 -0
- package/src/core/util.ts +386 -0
- package/src/deploy-listing.ts +160 -0
- package/src/deploy.ts +162 -0
- package/src/extensions/README.md +72 -0
- package/src/extensions/dry-mount.ts +390 -0
- package/src/extensions/manifest.ts +153 -0
- package/src/extensions/runtime.ts +42 -0
- package/src/extensions.ts +519 -0
- package/src/launcher.ts +285 -0
- package/src/paths.ts +155 -0
- package/src/protocol/open-responses/LICENSE +201 -0
- package/src/protocol/open-responses/README.md +14 -0
- package/src/protocol/open-responses/context-helpers.ts +27 -0
- package/src/protocol/open-responses/context-log.ts +59 -0
- package/src/protocol/open-responses/context.ts +89 -0
- package/src/protocol/open-responses/generated.ts +115 -0
- package/src/protocol/open-responses/index.ts +33 -0
- package/src/protocol/open-responses/openapi.json +4230 -0
- package/src/protocol/open-responses/stream.ts +146 -0
- package/src/protocol/open-responses/tokens.ts +31 -0
- package/src/providers/README.md +86 -0
- package/src/providers/base.ts +111 -0
- package/src/providers/configuration.ts +97 -0
- package/src/providers/console/config.ts +21 -0
- package/src/providers/console/settings.ts +443 -0
- package/src/providers/console/strings.ts +74 -0
- package/src/providers/console/types.ts +11 -0
- package/src/providers/llamacpp/archive.ts +148 -0
- package/src/providers/llamacpp/catalog.ts +247 -0
- package/src/providers/llamacpp/config.ts +36 -0
- package/src/providers/llamacpp/console/client.ts +6 -0
- package/src/providers/llamacpp/console/models-panel.ts +120 -0
- package/src/providers/llamacpp/console/runtime-panel.ts +173 -0
- package/src/providers/llamacpp/console/server.ts +153 -0
- package/src/providers/llamacpp/index.ts +95 -0
- package/src/providers/llamacpp/native.ts +51 -0
- package/src/providers/llamacpp/options.ts +117 -0
- package/src/providers/llamacpp/runtime-store.ts +139 -0
- package/src/providers/llamacpp/runtime.ts +214 -0
- package/src/providers/llamacpp/server.ts +286 -0
- package/src/providers/llamacpp/strings.ts +230 -0
- package/src/providers/openai-responses-compat/config.ts +20 -0
- package/src/providers/openai-responses-compat/index.ts +89 -0
- package/src/providers/openai-responses-compat/native.ts +127 -0
- package/src/providers/openai-responses-compat/strings.ts +19 -0
- package/src/providers/pricebook.ts +75 -0
- package/src/providers/registry.ts +137 -0
- package/src/providers/strings.ts +71 -0
- package/src/providers/transport/chat.ts +156 -0
- package/src/providers/transport/errors.ts +48 -0
- package/src/providers/transport/history.ts +68 -0
- package/src/providers/transport/native-input.ts +83 -0
- package/src/providers/transport/native-types.ts +13 -0
- package/src/providers/transport/response-assembly.ts +191 -0
- package/src/providers/transport/response-http.ts +196 -0
- package/src/providers/transport/response-meters.ts +27 -0
- package/src/providers/transport/responses-input.ts +56 -0
- package/src/web/README.md +84 -0
- package/src/web/client/console-pages/builtins/llm-settings/panel.ts +625 -0
- package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +168 -0
- package/src/web/client/console-pages/builtins/llm-settings/strings.ts +213 -0
- package/src/web/client/console-pages/builtins.ts +10 -0
- package/src/web/client/console-pages/context.ts +135 -0
- package/src/web/client/console-pages/host.ts +633 -0
- package/src/web/client/console-pages/loader.ts +132 -0
- package/src/web/client/console-pages/strings.ts +90 -0
- package/src/web/client/console-pages/tools/strings.ts +49 -0
- package/src/web/client/console-pages/tools/view.ts +215 -0
- package/src/web/client/core/api.ts +200 -0
- package/src/web/client/core/language.ts +56 -0
- package/src/web/client/core/lifecycle.ts +132 -0
- package/src/web/client/core/router.ts +199 -0
- package/src/web/client/core/stream.ts +157 -0
- package/src/web/client/core/websocket.ts +82 -0
- package/src/web/client/features/appearance/index.ts +333 -0
- package/src/web/client/features/appearance/strings.ts +87 -0
- package/src/web/client/features/config/index.ts +11 -0
- package/src/web/client/features/config/strings.ts +41 -0
- package/src/web/client/features/config/view.ts +428 -0
- package/src/web/client/features/core/events.ts +197 -0
- package/src/web/client/features/core/index.ts +254 -0
- package/src/web/client/features/core/run.ts +56 -0
- package/src/web/client/features/core/runlog.ts +214 -0
- package/src/web/client/features/core/sessions.ts +53 -0
- package/src/web/client/features/core/strings.ts +157 -0
- package/src/web/client/features/extensions/index.ts +422 -0
- package/src/web/client/features/extensions/strings.ts +164 -0
- package/src/web/client/features/feature.ts +93 -0
- package/src/web/client/features/live/context.ts +280 -0
- package/src/web/client/features/live/fork.ts +156 -0
- package/src/web/client/features/live/index.ts +392 -0
- package/src/web/client/features/live/onboarding.ts +113 -0
- package/src/web/client/features/live/protocol.ts +122 -0
- package/src/web/client/features/live/sessions.ts +71 -0
- package/src/web/client/features/live/status.ts +68 -0
- package/src/web/client/features/live/strings.ts +252 -0
- package/src/web/client/features/live/timeline.ts +590 -0
- package/src/web/client/features/prompts/editor.ts +292 -0
- package/src/web/client/features/prompts/index.ts +346 -0
- package/src/web/client/features/prompts/strings.ts +136 -0
- package/src/web/client/features/prompts/view.ts +256 -0
- package/src/web/client/features/providers/index.ts +100 -0
- package/src/web/client/features/providers/strings.ts +23 -0
- package/src/web/client/features/settings/general.ts +28 -0
- package/src/web/client/features/settings/index.ts +82 -0
- package/src/web/client/features/settings/strings.ts +33 -0
- package/src/web/client/features/storage/index.ts +1 -0
- package/src/web/client/features/storage/strings.ts +45 -0
- package/src/web/client/features/storage/view.ts +154 -0
- package/src/web/client/features/usage/chart.ts +486 -0
- package/src/web/client/features/usage/index.ts +407 -0
- package/src/web/client/features/usage/labels.ts +46 -0
- package/src/web/client/features/usage/range.ts +36 -0
- package/src/web/client/features/usage/state.ts +37 -0
- package/src/web/client/features/usage/strings.ts +191 -0
- package/src/web/client/features/usage/tooltip.ts +86 -0
- package/src/web/client/features/usage/types.ts +96 -0
- package/src/web/client/features/worlds/index.ts +424 -0
- package/src/web/client/features/worlds/strings.ts +121 -0
- package/src/web/client/main.ts +273 -0
- package/src/web/client/shell/avatar.ts +233 -0
- package/src/web/client/shell/index.ts +549 -0
- package/src/web/client/shell/strings.ts +89 -0
- package/src/web/client/theme/handoff.ts +43 -0
- package/src/web/client/theme/palette.ts +109 -0
- package/src/web/client/theme/registry.ts +130 -0
- package/src/web/client/theme/storage.ts +72 -0
- package/src/web/client/theme/strings.ts +123 -0
- package/src/web/client/theme/studio.ts +366 -0
- package/src/web/client/ui/actions.ts +109 -0
- package/src/web/client/ui/data.ts +185 -0
- package/src/web/client/ui/dom.ts +23 -0
- package/src/web/client/ui/fields.ts +191 -0
- package/src/web/client/ui/format.ts +75 -0
- package/src/web/client/ui/icons.ts +183 -0
- package/src/web/client/ui/images.ts +91 -0
- package/src/web/client/ui/index.ts +89 -0
- package/src/web/client/ui/lamp.ts +125 -0
- package/src/web/client/ui/log.ts +98 -0
- package/src/web/client/ui/overlay.ts +184 -0
- package/src/web/client/ui/page.ts +8 -0
- package/src/web/client/ui/prompt-input.tsx +311 -0
- package/src/web/client/ui/sheet.ts +131 -0
- package/src/web/client/ui/strings.ts +71 -0
- package/src/web/console-pages.ts +367 -0
- package/src/web/files.ts +107 -0
- package/src/web/path-picker.ts +296 -0
- package/src/web/public/index.html +17 -0
- package/src/web/public/styles.css +1715 -0
- package/src/web/server.ts +1883 -0
- package/src/web/shared/client-panel.ts +603 -0
- package/src/web/shared/console-protocol.ts +560 -0
- package/src/web/shared/css.d.ts +1 -0
- package/src/web/shared/path-picker.ts +23 -0
- package/src/web/shared/theme.ts +256 -0
- package/src/web/theme-store.ts +37 -0
- package/src/world.ts +449 -0
- package/src/worlds/bilibili/ENV_PROMPT.md +5 -0
- package/src/worlds/bilibili/README.md +176 -0
- package/src/worlds/bilibili/audience-admission.ts +995 -0
- package/src/worlds/bilibili/client.ts +528 -0
- package/src/worlds/bilibili/coalescing-buffer.ts +125 -0
- package/src/worlds/bilibili/config.ts +185 -0
- package/src/worlds/bilibili/console/client.ts +202 -0
- package/src/worlds/bilibili/definition.ts +35 -0
- package/src/worlds/bilibili/gift-frame.ts +131 -0
- package/src/worlds/bilibili/normalize.ts +452 -0
- package/src/worlds/bilibili/overlay/announcement.ts +76 -0
- package/src/worlds/bilibili/overlay/assets.ts +82 -0
- package/src/worlds/bilibili/overlay/model.ts +494 -0
- package/src/worlds/bilibili/overlay/project.ts +202 -0
- package/src/worlds/bilibili/overlay/server.ts +286 -0
- package/src/worlds/bilibili/overlay/types.ts +217 -0
- package/src/worlds/bilibili/overlay/web/app.js +690 -0
- package/src/worlds/bilibili/overlay/web/editor.css +297 -0
- package/src/worlds/bilibili/overlay/web/editor.html +102 -0
- package/src/worlds/bilibili/overlay/web/editor.js +2116 -0
- package/src/worlds/bilibili/overlay/web/overlay.html +13 -0
- package/src/worlds/bilibili/overlay/web/styles.css +258 -0
- package/src/worlds/bilibili/protobuf.ts +123 -0
- package/src/worlds/bilibili/wire.ts +94 -0
- package/src/worlds/bilibili/world.ts +1389 -0
- package/src/worlds/console-fixture/ENV_PROMPT.md +1 -0
- package/src/worlds/console-fixture/console/client.ts +72 -0
- package/src/worlds/console-fixture/world.ts +73 -0
- package/src/worlds/index.ts +15 -0
- package/src/worlds/minecraft/ENV_PROMPT.md +32 -0
- package/src/worlds/minecraft/ENV_PROMPT_CAMERA.md +2 -0
- package/src/worlds/minecraft/LICENSE-mineflayer-pathfinder.txt +21 -0
- package/src/worlds/minecraft/README.md +917 -0
- package/src/worlds/minecraft/blueprint-plan.ts +915 -0
- package/src/worlds/minecraft/blueprint-registry.ts +566 -0
- package/src/worlds/minecraft/blueprint-repair.ts +236 -0
- package/src/worlds/minecraft/blueprint-resource.ts +272 -0
- package/src/worlds/minecraft/blueprint.ts +492 -0
- package/src/worlds/minecraft/body-lease.ts +340 -0
- package/src/worlds/minecraft/bridge.ts +749 -0
- package/src/worlds/minecraft/check.ts +651 -0
- package/src/worlds/minecraft/chests.ts +292 -0
- package/src/worlds/minecraft/client-launch.ts +272 -0
- package/src/worlds/minecraft/client-options.ts +85 -0
- package/src/worlds/minecraft/client-skins.ts +164 -0
- package/src/worlds/minecraft/client-window.ps1 +91 -0
- package/src/worlds/minecraft/client.ts +423 -0
- package/src/worlds/minecraft/combat-context.ts +109 -0
- package/src/worlds/minecraft/combat.ts +1552 -0
- package/src/worlds/minecraft/config.ts +499 -0
- package/src/worlds/minecraft/console/access.ts +218 -0
- package/src/worlds/minecraft/console/client.ts +244 -0
- package/src/worlds/minecraft/console/log.ts +110 -0
- package/src/worlds/minecraft/console/mount.ts +420 -0
- package/src/worlds/minecraft/console/skin.ts +257 -0
- package/src/worlds/minecraft/console/style.css +154 -0
- package/src/worlds/minecraft/console/world.ts +449 -0
- package/src/worlds/minecraft/deaths.ts +135 -0
- package/src/worlds/minecraft/definition.ts +18 -0
- package/src/worlds/minecraft/engine-child.ts +274 -0
- package/src/worlds/minecraft/engine-ipc.ts +98 -0
- package/src/worlds/minecraft/entity-facts.ts +132 -0
- package/src/worlds/minecraft/escape.ts +407 -0
- package/src/worlds/minecraft/executor.ts +14494 -0
- package/src/worlds/minecraft/explored.ts +156 -0
- package/src/worlds/minecraft/geometry.ts +205 -0
- package/src/worlds/minecraft/goal-plan.ts +647 -0
- package/src/worlds/minecraft/item-break.ts +126 -0
- package/src/worlds/minecraft/item-facts.ts +146 -0
- package/src/worlds/minecraft/item-pick.ts +67 -0
- package/src/worlds/minecraft/level-dat.ts +176 -0
- package/src/worlds/minecraft/log.ts +132 -0
- package/src/worlds/minecraft/mineflayer-fixes.ts +1180 -0
- package/src/worlds/minecraft/names.ts +377 -0
- package/src/worlds/minecraft/pathfinder-lib.d.ts +13 -0
- package/src/worlds/minecraft/pathfinder-perf.ts +796 -0
- package/src/worlds/minecraft/piglin.ts +49 -0
- package/src/worlds/minecraft/policy.ts +547 -0
- package/src/worlds/minecraft/precheck.ts +655 -0
- package/src/worlds/minecraft/proxy.ts +445 -0
- package/src/worlds/minecraft/ranged.ts +635 -0
- package/src/worlds/minecraft/readouts.ts +102 -0
- package/src/worlds/minecraft/round.ts +26 -0
- package/src/worlds/minecraft/search-observation.ts +92 -0
- package/src/worlds/minecraft/server-config.ts +467 -0
- package/src/worlds/minecraft/server.ts +658 -0
- package/src/worlds/minecraft/show.ts +84 -0
- package/src/worlds/minecraft/skills.ts +1763 -0
- package/src/worlds/minecraft/terrain.ts +1735 -0
- package/src/worlds/minecraft/window.ts +96 -0
- package/src/worlds/minecraft/works.ts +179 -0
- package/src/worlds/minecraft/world.ts +6438 -0
- package/src/worlds/qq/ENV_PROMPT.md +5 -0
- package/src/worlds/qq/config.ts +108 -0
- package/src/worlds/qq/console/client.ts +126 -0
- package/src/worlds/qq/console/events.ts +155 -0
- package/src/worlds/qq/console/gate.ts +232 -0
- package/src/worlds/qq/console/roster.ts +203 -0
- package/src/worlds/qq/console/style.css +65 -0
- package/src/worlds/qq/conversation.ts +28 -0
- package/src/worlds/qq/definition.ts +62 -0
- package/src/worlds/qq/driver.ts +375 -0
- package/src/worlds/qq/history-tools.ts +209 -0
- package/src/worlds/qq/normalize.ts +323 -0
- package/src/worlds/qq/qface-map.ts +282 -0
- package/src/worlds/qq/vision-prompt.ts +27 -0
- package/src/worlds/qq/vision.ts +642 -0
- package/src/worlds/qq/vlm.ts +142 -0
- package/src/worlds/qq/world.ts +1503 -0
- package/src/worlds/terminal/ENV_PROMPT.md +11 -0
- package/src/worlds/terminal/config.ts +9 -0
- package/src/worlds/terminal/definition.ts +14 -0
- package/src/worlds/terminal/world.ts +700 -0
- package/src/worlds/websearch/ENV_PROMPT.md +1 -0
- package/src/worlds/websearch/brave-client.ts +261 -0
- package/src/worlds/websearch/config.ts +75 -0
- package/src/worlds/websearch/definition.ts +20 -0
- package/src/worlds/websearch/world.ts +171 -0
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 蓝图受理、施工中间表示(IR)编译、世界核验、材料统计与进度计算。
|
|
3
|
+
* 体量按 Palette 编码长度计;普通放置核验方块类型并报告属性差异,功能动作核验指定属性。
|
|
4
|
+
* 自底向上逐层编译,层内按行扫描,合并与 stopAfter 均不跨层。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
blockIdOf,
|
|
9
|
+
isAirState,
|
|
10
|
+
isStructureVoidState,
|
|
11
|
+
multiPartRole,
|
|
12
|
+
normalizeBlockName,
|
|
13
|
+
tryNormalizeBlueprint,
|
|
14
|
+
voxelMetrics,
|
|
15
|
+
type NormalizedBlueprint,
|
|
16
|
+
type PositionXYZ,
|
|
17
|
+
type SizeXYZ,
|
|
18
|
+
type VoxelMetrics,
|
|
19
|
+
} from './blueprint.ts';
|
|
20
|
+
import {
|
|
21
|
+
BLUEPRINT_TOOL_FAMILIES,
|
|
22
|
+
blueprintNeedsTilledSoil,
|
|
23
|
+
blueprintPlacementMethod,
|
|
24
|
+
blueprintSupportRules,
|
|
25
|
+
blueprintSupportViolation,
|
|
26
|
+
validateBlueprintPlaceability,
|
|
27
|
+
validateBlueprintRegistry,
|
|
28
|
+
type BlueprintStateFailure,
|
|
29
|
+
type BlueprintPlacementMethod,
|
|
30
|
+
} from './blueprint-registry.ts';
|
|
31
|
+
import {
|
|
32
|
+
completeBlueprintStateDefaults,
|
|
33
|
+
createRepairReport,
|
|
34
|
+
repairSubmissionShape,
|
|
35
|
+
type RepairReport,
|
|
36
|
+
} from './blueprint-repair.ts';
|
|
37
|
+
import type { Anchor } from './geometry.ts';
|
|
38
|
+
|
|
39
|
+
/** palette 编码交稿的最大字符数;分批只改变传输方式,不改变这条总预算。 */
|
|
40
|
+
export const BLUEPRINT_MAX_OUTPUT_CHARS = 131_072;
|
|
41
|
+
/** 单个底层 build 调用的体素上限;大平面会在 IR 内自动切步。 */
|
|
42
|
+
const BLUEPRINT_STEP_CELL_CAP = 256;
|
|
43
|
+
|
|
44
|
+
// ── 额外校验 ──────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/** 规范化蓝图按实际 palette 编码估算交稿长度。 */
|
|
47
|
+
export function blueprintOutputChars(blueprint: NormalizedBlueprint): number {
|
|
48
|
+
const index = new Map<string, number>();
|
|
49
|
+
const palette: string[] = [];
|
|
50
|
+
const layers = blueprint.layers.map((layer) => layer.map((row) => row.map((state) => {
|
|
51
|
+
let at = index.get(state);
|
|
52
|
+
if (at === undefined) {
|
|
53
|
+
at = palette.length;
|
|
54
|
+
index.set(state, at);
|
|
55
|
+
palette.push(state);
|
|
56
|
+
}
|
|
57
|
+
return at;
|
|
58
|
+
})));
|
|
59
|
+
return JSON.stringify({
|
|
60
|
+
site_mode: blueprint.site_mode,
|
|
61
|
+
size_xyz: blueprint.size_xyz,
|
|
62
|
+
axis_order: 'YZX',
|
|
63
|
+
palette,
|
|
64
|
+
layers,
|
|
65
|
+
}).length;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 按编码字符数限制提交大小。 */
|
|
69
|
+
export function validateBlueprintLimits(
|
|
70
|
+
blueprint: NormalizedBlueprint,
|
|
71
|
+
): BlueprintStateFailure[] {
|
|
72
|
+
const chars = blueprintOutputChars(blueprint);
|
|
73
|
+
return chars <= BLUEPRINT_MAX_OUTPUT_CHARS ? [] : [{
|
|
74
|
+
path: 'layers',
|
|
75
|
+
state: String(chars),
|
|
76
|
+
reason: `palette 编码后约 ${chars} 字符,总输出上限 ${BLUEPRINT_MAX_OUTPUT_CHARS} 字符`,
|
|
77
|
+
}];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── IR ────────────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/** 一个同状态长方体的施工步骤;from/to 是含端点的蓝图局部坐标。 */
|
|
83
|
+
export interface BlueprintStep {
|
|
84
|
+
/** 步序,0 起,即游标口径 */
|
|
85
|
+
index: number;
|
|
86
|
+
state: string;
|
|
87
|
+
/** 完成这一步要拿的方块、工具或可使用物品。 */
|
|
88
|
+
item: string;
|
|
89
|
+
/** 直接放置,或用物品把目标格加工出来。 */
|
|
90
|
+
method: BlueprintPlacementMethod;
|
|
91
|
+
from: PositionXYZ;
|
|
92
|
+
to: PositionXYZ;
|
|
93
|
+
cells: number;
|
|
94
|
+
/** 所在层;并块不跨层,所以 from[1] === to[1] === y */
|
|
95
|
+
y: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 门上半格、床头等从部件不单独生成步骤,仍参与主步骤的完成核验。 */
|
|
99
|
+
export interface BlueprintCheckCell {
|
|
100
|
+
pos: PositionXYZ;
|
|
101
|
+
state: string;
|
|
102
|
+
mainPos: PositionXYZ;
|
|
103
|
+
/** 主部件落在第几步 */
|
|
104
|
+
mainStep: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 需要在工地核验的图外条件;不阻止设计受理。 */
|
|
108
|
+
interface BlueprintAdvisory {
|
|
109
|
+
/** 同一条提醒第一次出现的位置 */
|
|
110
|
+
path: string;
|
|
111
|
+
state: string;
|
|
112
|
+
reason: string;
|
|
113
|
+
/** 这一条覆盖的格数 */
|
|
114
|
+
cells: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 现场前提那一段回执;失败清单(renderBlueprintFailures)同一个形状 */
|
|
118
|
+
export function renderBlueprintAdvisories(advisories: readonly BlueprintAdvisory[]): string {
|
|
119
|
+
const shown = advisories.slice(0, 6);
|
|
120
|
+
const lines = shown.map((a) => `· ${a.path}${a.state ? `(${a.state})` : ''}: ${a.reason}`
|
|
121
|
+
+ `${a.cells > 1 ? `(同样的还有 ${a.cells - 1} 格)` : ''}`);
|
|
122
|
+
const more = advisories.length > shown.length ? `\n(还有 ${advisories.length - shown.length} 条同类)` : '';
|
|
123
|
+
return `${lines.join('\n')}${more}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface BlueprintPlan {
|
|
127
|
+
steps: BlueprintStep[];
|
|
128
|
+
checks: BlueprintCheckCell[];
|
|
129
|
+
/** 要施工的格数(不含从部件)。 */
|
|
130
|
+
placeCells: number;
|
|
131
|
+
/** 编译期发现的设计问题(孤立从部件、放不出的状态、图内撑不住的支撑) */
|
|
132
|
+
failures: BlueprintStateFailure[];
|
|
133
|
+
/** 依赖图外现场的前提;不影响受理 */
|
|
134
|
+
advisories: BlueprintAdvisory[];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
type CellRole = 'air' | 'place' | 'secondary' | 'broken';
|
|
138
|
+
|
|
139
|
+
function cellPath(x: number, y: number, z: number): string {
|
|
140
|
+
return `layers[${y}][${z}][${x}]`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 图内支撑不符合规则时拒绝;图外或 structure_void 支撑只生成现场核验提示。
|
|
145
|
+
* 按状态、要求和违规类型归并,报告首个位置与数量。
|
|
146
|
+
*/
|
|
147
|
+
function reviewSupports(
|
|
148
|
+
blueprint: NormalizedBlueprint,
|
|
149
|
+
roles: readonly CellRole[][][],
|
|
150
|
+
): { failures: BlueprintStateFailure[]; advisories: BlueprintAdvisory[] } {
|
|
151
|
+
const [sizeX, sizeY, sizeZ] = blueprint.size_xyz;
|
|
152
|
+
const failures = new Map<string, BlueprintStateFailure & { cells: number }>();
|
|
153
|
+
const advisories = new Map<string, BlueprintAdvisory>();
|
|
154
|
+
|
|
155
|
+
for (let y = 0; y < sizeY; y++) {
|
|
156
|
+
for (let z = 0; z < sizeZ; z++) {
|
|
157
|
+
for (let x = 0; x < sizeX; x++) {
|
|
158
|
+
if (roles[y][z][x] !== 'place') continue;
|
|
159
|
+
const state = blueprint.layers[y][z][x];
|
|
160
|
+
for (const rule of blueprintSupportRules(state)) {
|
|
161
|
+
const [sx, sy, sz] = [x + rule.offset[0], y + rule.offset[1], z + rule.offset[2]];
|
|
162
|
+
const outside = sx < 0 || sy < 0 || sz < 0 || sx >= sizeX || sy >= sizeY || sz >= sizeZ;
|
|
163
|
+
const support = outside ? null : blueprint.layers[sy][sz][sx];
|
|
164
|
+
|
|
165
|
+
if (support === null || isStructureVoidState(support)) {
|
|
166
|
+
const key = `${state}|${rule.demand}`;
|
|
167
|
+
const seen = advisories.get(key);
|
|
168
|
+
if (seen === undefined) {
|
|
169
|
+
advisories.set(key, {
|
|
170
|
+
path: cellPath(x, y, z),
|
|
171
|
+
state,
|
|
172
|
+
reason: `${rule.demand};支撑那一格不在图里,施工前现场必须已经满足`,
|
|
173
|
+
cells: 1,
|
|
174
|
+
});
|
|
175
|
+
} else seen.cells++;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const violation = blueprintSupportViolation(rule, support);
|
|
180
|
+
if (violation === null) continue;
|
|
181
|
+
const key = `${state}|${rule.demand}|${violation}`;
|
|
182
|
+
const seen = failures.get(key);
|
|
183
|
+
if (seen === undefined) {
|
|
184
|
+
failures.set(key, {
|
|
185
|
+
path: cellPath(x, y, z),
|
|
186
|
+
state,
|
|
187
|
+
reason: `${rule.demand};${violation},原版放不下去`,
|
|
188
|
+
cells: 1,
|
|
189
|
+
});
|
|
190
|
+
} else seen.cells++;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
failures: [...failures.values()].map(({ cells, ...failure }) => (cells === 1
|
|
198
|
+
? failure
|
|
199
|
+
: { ...failure, reason: `${failure.reason}(同样的还有 ${cells - 1} 格)` })),
|
|
200
|
+
advisories: [...advisories.values()],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** 按层、从北向南及从西向东编译;同状态格先向东合并,再合并相邻整行。 */
|
|
205
|
+
export function compileBlueprint(blueprint: NormalizedBlueprint): BlueprintPlan {
|
|
206
|
+
const [sizeX, sizeY, sizeZ] = blueprint.size_xyz;
|
|
207
|
+
const failures: BlueprintStateFailure[] = [];
|
|
208
|
+
const roles: CellRole[][][] = [];
|
|
209
|
+
const pendingChecks: Array<{ pos: PositionXYZ; state: string; mainPos: PositionXYZ }> = [];
|
|
210
|
+
|
|
211
|
+
const stateAt = (x: number, y: number, z: number): string | null =>
|
|
212
|
+
x < 0 || y < 0 || z < 0 || x >= sizeX || y >= sizeY || z >= sizeZ
|
|
213
|
+
? null
|
|
214
|
+
: blueprint.layers[y][z][x];
|
|
215
|
+
|
|
216
|
+
for (let y = 0; y < sizeY; y++) {
|
|
217
|
+
const layer: CellRole[][] = [];
|
|
218
|
+
for (let z = 0; z < sizeZ; z++) {
|
|
219
|
+
const row: CellRole[] = [];
|
|
220
|
+
for (let x = 0; x < sizeX; x++) {
|
|
221
|
+
const state = blueprint.layers[y][z][x];
|
|
222
|
+
if (isAirState(state)) { row.push('air'); continue; }
|
|
223
|
+
|
|
224
|
+
const method = blueprintPlacementMethod(state);
|
|
225
|
+
if (method.item === null) {
|
|
226
|
+
failures.push({ path: cellPath(x, y, z), state, reason: method.reason });
|
|
227
|
+
row.push('broken');
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const role = multiPartRole(state);
|
|
232
|
+
if (role.role !== 'secondary') { row.push('place'); continue; }
|
|
233
|
+
|
|
234
|
+
const [dx, dy, dz] = role.mainOffset;
|
|
235
|
+
const mainPos: PositionXYZ = [x + dx, y + dy, z + dz];
|
|
236
|
+
const mainState = stateAt(mainPos[0], mainPos[1], mainPos[2]);
|
|
237
|
+
const mainRole = mainState === null ? null : multiPartRole(mainState);
|
|
238
|
+
const paired = mainState !== null
|
|
239
|
+
&& mainRole !== null
|
|
240
|
+
&& mainRole.role === 'main'
|
|
241
|
+
&& blockIdOf(mainState) === blockIdOf(state)
|
|
242
|
+
&& mainRole.secondaryOffset[0] === -dx
|
|
243
|
+
&& mainRole.secondaryOffset[1] === -dy
|
|
244
|
+
&& mainRole.secondaryOffset[2] === -dz;
|
|
245
|
+
if (!paired) {
|
|
246
|
+
failures.push({
|
|
247
|
+
path: cellPath(x, y, z),
|
|
248
|
+
state,
|
|
249
|
+
reason: `这是多格方块的从部件,主部件该在 ${mainPos.join(',')},那一格却是 ` +
|
|
250
|
+
`${mainState ?? '矩阵之外'}`,
|
|
251
|
+
});
|
|
252
|
+
row.push('broken');
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
pendingChecks.push({ pos: [x, y, z], state, mainPos });
|
|
256
|
+
row.push('secondary');
|
|
257
|
+
}
|
|
258
|
+
layer.push(row);
|
|
259
|
+
}
|
|
260
|
+
roles.push(layer);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const supports = reviewSupports(blueprint, roles);
|
|
264
|
+
failures.push(...supports.failures);
|
|
265
|
+
|
|
266
|
+
const steps: BlueprintStep[] = [];
|
|
267
|
+
const stepOfCell = new Map<string, number>();
|
|
268
|
+
const key = (x: number, y: number, z: number): string => `${x},${y},${z}`;
|
|
269
|
+
let placeCells = 0;
|
|
270
|
+
|
|
271
|
+
for (let y = 0; y < sizeY; y++) {
|
|
272
|
+
const claimed = roles[y].map((row) => row.map((role) => role !== 'place'));
|
|
273
|
+
for (let z = 0; z < sizeZ; z++) {
|
|
274
|
+
for (let x = 0; x < sizeX; x++) {
|
|
275
|
+
if (claimed[z][x]) continue;
|
|
276
|
+
const state = blueprint.layers[y][z][x];
|
|
277
|
+
|
|
278
|
+
let width = 1;
|
|
279
|
+
while (x + width < sizeX
|
|
280
|
+
&& !claimed[z][x + width]
|
|
281
|
+
&& blueprint.layers[y][z][x + width] === state) width++;
|
|
282
|
+
|
|
283
|
+
let depth = 1;
|
|
284
|
+
while (z + depth < sizeZ) {
|
|
285
|
+
let uniform = true;
|
|
286
|
+
for (let step = 0; step < width; step++) {
|
|
287
|
+
if (claimed[z + depth][x + step] || blueprint.layers[y][z + depth][x + step] !== state) {
|
|
288
|
+
uniform = false;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (!uniform) break;
|
|
293
|
+
depth++;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const method = blueprintPlacementMethod(state);
|
|
297
|
+
if (method.item === null) {
|
|
298
|
+
failures.push({ path: cellPath(x, y, z), state, reason: method.reason });
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
let zOffset = 0;
|
|
302
|
+
while (zOffset < depth) {
|
|
303
|
+
const chunkDepth = Math.min(
|
|
304
|
+
depth - zOffset,
|
|
305
|
+
Math.max(1, Math.floor(BLUEPRINT_STEP_CELL_CAP / Math.min(width, BLUEPRINT_STEP_CELL_CAP))),
|
|
306
|
+
);
|
|
307
|
+
let xOffset = 0;
|
|
308
|
+
while (xOffset < width) {
|
|
309
|
+
const chunkWidth = Math.min(
|
|
310
|
+
width - xOffset,
|
|
311
|
+
Math.floor(BLUEPRINT_STEP_CELL_CAP / chunkDepth),
|
|
312
|
+
);
|
|
313
|
+
const index = steps.length;
|
|
314
|
+
for (let dz = 0; dz < chunkDepth; dz++) {
|
|
315
|
+
for (let dx = 0; dx < chunkWidth; dx++) {
|
|
316
|
+
const cellX = x + xOffset + dx;
|
|
317
|
+
const cellZ = z + zOffset + dz;
|
|
318
|
+
claimed[cellZ][cellX] = true;
|
|
319
|
+
stepOfCell.set(key(cellX, y, cellZ), index);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const cells = chunkWidth * chunkDepth;
|
|
323
|
+
placeCells += cells;
|
|
324
|
+
steps.push({
|
|
325
|
+
index,
|
|
326
|
+
state,
|
|
327
|
+
item: method.item,
|
|
328
|
+
method,
|
|
329
|
+
from: [x + xOffset, y, z + zOffset],
|
|
330
|
+
to: [x + xOffset + chunkWidth - 1, y, z + zOffset + chunkDepth - 1],
|
|
331
|
+
cells,
|
|
332
|
+
y,
|
|
333
|
+
});
|
|
334
|
+
xOffset += chunkWidth;
|
|
335
|
+
}
|
|
336
|
+
zOffset += chunkDepth;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const checks: BlueprintCheckCell[] = pendingChecks.map((entry) => ({
|
|
343
|
+
pos: entry.pos,
|
|
344
|
+
state: entry.state,
|
|
345
|
+
mainPos: entry.mainPos,
|
|
346
|
+
mainStep: stepOfCell.get(key(entry.mainPos[0], entry.mainPos[1], entry.mainPos[2])) ?? -1,
|
|
347
|
+
}));
|
|
348
|
+
|
|
349
|
+
return { steps, checks, placeCells, failures, advisories: supports.advisories };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** 局部坐标 + 锚点 → 世界坐标。锚点是蓝图 [0,0,0] 落在世界的哪一格。 */
|
|
353
|
+
export function toWorld(anchor: PositionXYZ, local: PositionXYZ): PositionXYZ {
|
|
354
|
+
return [anchor[0] + local[0], anchor[1] + local[1], anchor[2] + local[2]];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** 将 IR 转为 box/solid 的绝对坐标 build 调用;needs 与 expect 由队列补充。 */
|
|
358
|
+
interface BlueprintBuildCall {
|
|
359
|
+
skill: 'build';
|
|
360
|
+
shape: 'box';
|
|
361
|
+
fill: 'solid';
|
|
362
|
+
anchors: [Anchor, Anchor];
|
|
363
|
+
material: string;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function stepToBuildCall(step: BlueprintStep, anchor: PositionXYZ): BlueprintBuildCall {
|
|
367
|
+
const from = toWorld(anchor, step.from);
|
|
368
|
+
const to = toWorld(anchor, step.to);
|
|
369
|
+
return {
|
|
370
|
+
skill: 'build',
|
|
371
|
+
shape: 'box',
|
|
372
|
+
fill: 'solid',
|
|
373
|
+
anchors: [[from[0], from[1], from[2]], [to[0], to[1], to[2]]],
|
|
374
|
+
material: step.method.kind === 'use' && step.method.baseItem
|
|
375
|
+
? step.method.baseItem[0]
|
|
376
|
+
: step.item,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ── 受理管线 ──────────────────────────────────────────────────────────────────
|
|
381
|
+
|
|
382
|
+
interface BlueprintAcceptance {
|
|
383
|
+
ok: boolean;
|
|
384
|
+
blueprint: NormalizedBlueprint | null;
|
|
385
|
+
plan: BlueprintPlan | null;
|
|
386
|
+
metrics: VoxelMetrics | null;
|
|
387
|
+
repair: RepairReport;
|
|
388
|
+
failures: BlueprintStateFailure[];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
interface AcceptOptions {
|
|
392
|
+
expectedSize?: SizeXYZ;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* 受理顺序:严格解析、格式修复、默认属性、注册表、体量和施工规则校验、IR 编译。
|
|
397
|
+
* 失败时返回已发现的精确错误路径。
|
|
398
|
+
*/
|
|
399
|
+
export function acceptBlueprint(
|
|
400
|
+
submission: unknown,
|
|
401
|
+
options: AcceptOptions = {},
|
|
402
|
+
): BlueprintAcceptance {
|
|
403
|
+
const repair = createRepairReport();
|
|
404
|
+
const empty = (failures: BlueprintStateFailure[]): BlueprintAcceptance =>
|
|
405
|
+
({ ok: false, blueprint: null, plan: null, metrics: null, repair, failures });
|
|
406
|
+
|
|
407
|
+
const strict = tryNormalizeBlueprint(submission, options);
|
|
408
|
+
repair.strictValid = strict.ok;
|
|
409
|
+
let normalized: NormalizedBlueprint;
|
|
410
|
+
|
|
411
|
+
if (strict.ok) {
|
|
412
|
+
normalized = strict.blueprint;
|
|
413
|
+
} else {
|
|
414
|
+
repair.strictError = `${strict.path}: ${strict.reason}`;
|
|
415
|
+
if (typeof submission !== 'object' || submission === null || Array.isArray(submission)) {
|
|
416
|
+
return empty([{ path: strict.path, state: '', reason: strict.reason }]);
|
|
417
|
+
}
|
|
418
|
+
const shaped = repairSubmissionShape(submission as Record<string, unknown>);
|
|
419
|
+
const retry = tryNormalizeBlueprint(shaped.submission, options);
|
|
420
|
+
if (!retry.ok) {
|
|
421
|
+
return empty([{ path: retry.path, state: '', reason: retry.reason }]);
|
|
422
|
+
}
|
|
423
|
+
repair.actions.push(...shaped.actions);
|
|
424
|
+
repair.applied = shaped.actions.length > 0;
|
|
425
|
+
normalized = retry.blueprint;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const completed = completeBlueprintStateDefaults(normalized);
|
|
429
|
+
if (completed.action !== null) {
|
|
430
|
+
repair.actions.push(completed.action);
|
|
431
|
+
repair.applied = true;
|
|
432
|
+
}
|
|
433
|
+
normalized = completed.blueprint;
|
|
434
|
+
|
|
435
|
+
const registryResult = validateBlueprintRegistry(normalized);
|
|
436
|
+
const failures: BlueprintStateFailure[] = [
|
|
437
|
+
...registryResult.failures,
|
|
438
|
+
...validateBlueprintLimits(normalized),
|
|
439
|
+
];
|
|
440
|
+
if (registryResult.valid) {
|
|
441
|
+
failures.push(...validateBlueprintPlaceability(normalized).failures);
|
|
442
|
+
}
|
|
443
|
+
if (failures.length > 0) return empty(failures);
|
|
444
|
+
|
|
445
|
+
const plan = compileBlueprint(normalized);
|
|
446
|
+
if (plan.failures.length > 0) return empty(plan.failures);
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
ok: true,
|
|
450
|
+
blueprint: normalized,
|
|
451
|
+
plan,
|
|
452
|
+
metrics: voxelMetrics(normalized),
|
|
453
|
+
repair,
|
|
454
|
+
failures: [],
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ── 世界对账 ──────────────────────────────────────────────────────────────────
|
|
459
|
+
|
|
460
|
+
/** 按世界坐标读取方块;null 表示读数不可用,不计入冲突或完成。 */
|
|
461
|
+
export type BlockNameReader = (x: number, y: number, z: number) => string | null;
|
|
462
|
+
|
|
463
|
+
type ConflictKind = 'wrong-block' | 'should-be-air';
|
|
464
|
+
|
|
465
|
+
export interface BlueprintConflict {
|
|
466
|
+
/** 世界坐标 */
|
|
467
|
+
pos: PositionXYZ;
|
|
468
|
+
/** 局部坐标 */
|
|
469
|
+
local: PositionXYZ;
|
|
470
|
+
expect: string;
|
|
471
|
+
actual: string;
|
|
472
|
+
kind: ConflictKind;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export interface BlueprintDiff {
|
|
476
|
+
/** 要施工的格数(不含从部件)。 */
|
|
477
|
+
total: number;
|
|
478
|
+
/** 已达到机械终态的格数。 */
|
|
479
|
+
matched: number;
|
|
480
|
+
/** 还没到位的格数 */
|
|
481
|
+
missing: number;
|
|
482
|
+
/** 读不到(区块没加载)的格数 */
|
|
483
|
+
unknown: number;
|
|
484
|
+
conflictCounts: Record<ConflictKind, number>;
|
|
485
|
+
/** 冲突样本,按 sampleLimit 截断 */
|
|
486
|
+
conflicts: BlueprintConflict[];
|
|
487
|
+
/** 每一格都已符合的步 */
|
|
488
|
+
doneSteps: number[];
|
|
489
|
+
/** 还没完成的步(不只是游标之后那些) */
|
|
490
|
+
remaining: BlueprintStep[];
|
|
491
|
+
/** 游标 = 从头连着已完成的步数 */
|
|
492
|
+
cursor: number;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** 一格是否达到该施工步的机械终态;功能动作只核对它明确保证的属性。 */
|
|
496
|
+
export function blueprintStepStateMatches(step: BlueprintStep, actual: string): boolean {
|
|
497
|
+
const normalized = normalizeBlockName(actual);
|
|
498
|
+
if (blockIdOf(normalized) !== blockIdOf(step.state)) return false;
|
|
499
|
+
const required = step.method.kind === 'place' ? step.method.postUse : step.method.verify;
|
|
500
|
+
if (!required) return true;
|
|
501
|
+
const properties = normalized.includes('[')
|
|
502
|
+
? new Map(normalized.slice(normalized.indexOf('[') + 1, -1).split(',').map((entry) => {
|
|
503
|
+
const separator = entry.indexOf('=');
|
|
504
|
+
return [entry.slice(0, separator), entry.slice(separator + 1)] as [string, string];
|
|
505
|
+
}))
|
|
506
|
+
: new Map<string, string>();
|
|
507
|
+
return properties.get(required.property) === required.value;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
interface DiffOptions {
|
|
511
|
+
/** 顺带核对"该是空气的地方现在有没有东西";默认核对 */
|
|
512
|
+
checkAir?: boolean;
|
|
513
|
+
sampleLimit?: number;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** 普通方块按类型核验;功能动作另外核验指定属性。 */
|
|
517
|
+
export function diffBlueprint(
|
|
518
|
+
blueprint: NormalizedBlueprint,
|
|
519
|
+
plan: BlueprintPlan,
|
|
520
|
+
anchor: PositionXYZ,
|
|
521
|
+
read: BlockNameReader,
|
|
522
|
+
options: DiffOptions = {},
|
|
523
|
+
): BlueprintDiff {
|
|
524
|
+
const { checkAir = true, sampleLimit = 12 } = options;
|
|
525
|
+
const conflicts: BlueprintConflict[] = [];
|
|
526
|
+
const conflictCounts: Record<ConflictKind, number> = { 'wrong-block': 0, 'should-be-air': 0 };
|
|
527
|
+
let matched = 0;
|
|
528
|
+
let missing = 0;
|
|
529
|
+
let unknown = 0;
|
|
530
|
+
|
|
531
|
+
const note = (
|
|
532
|
+
local: PositionXYZ,
|
|
533
|
+
pos: PositionXYZ,
|
|
534
|
+
expect: string,
|
|
535
|
+
actual: string,
|
|
536
|
+
kind: ConflictKind,
|
|
537
|
+
): void => {
|
|
538
|
+
conflictCounts[kind]++;
|
|
539
|
+
if (conflicts.length < sampleLimit) conflicts.push({ pos, local, expect, actual, kind });
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const done = new Set<number>();
|
|
543
|
+
for (const step of plan.steps) {
|
|
544
|
+
const expectId = blockIdOf(step.state);
|
|
545
|
+
// 可转换基材由执行器原地加工,不计清场冲突;豁免集包含全部等价源方块。
|
|
546
|
+
const baseIds = new Set(
|
|
547
|
+
step.method.kind === 'use' && step.method.baseItem
|
|
548
|
+
? step.method.baseItem.map((n) => `minecraft:${n}`)
|
|
549
|
+
: [],
|
|
550
|
+
);
|
|
551
|
+
let stepDone = true;
|
|
552
|
+
for (let y = step.from[1]; y <= step.to[1]; y++) {
|
|
553
|
+
for (let z = step.from[2]; z <= step.to[2]; z++) {
|
|
554
|
+
for (let x = step.from[0]; x <= step.to[0]; x++) {
|
|
555
|
+
const local: PositionXYZ = [x, y, z];
|
|
556
|
+
const pos = toWorld(anchor, local);
|
|
557
|
+
const raw = read(pos[0], pos[1], pos[2]);
|
|
558
|
+
if (raw === null) { unknown++; stepDone = false; continue; }
|
|
559
|
+
const actual = normalizeBlockName(raw);
|
|
560
|
+
if (blueprintStepStateMatches(step, actual)) { matched++; continue; }
|
|
561
|
+
stepDone = false;
|
|
562
|
+
missing++;
|
|
563
|
+
const actualId = blockIdOf(actual);
|
|
564
|
+
if (actualId !== expectId && !isAirState(actual) && !baseIds.has(actualId)) {
|
|
565
|
+
note(local, pos, expectId, actualId, 'wrong-block');
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (stepDone) done.add(step.index);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// 门上半格、床头等从部件不单独消耗物品,但仍是主步骤的终态。
|
|
574
|
+
for (const check of plan.checks) {
|
|
575
|
+
if (!done.has(check.mainStep)) continue;
|
|
576
|
+
const pos = toWorld(anchor, check.pos);
|
|
577
|
+
const raw = read(pos[0], pos[1], pos[2]);
|
|
578
|
+
if (raw === null) {
|
|
579
|
+
unknown++;
|
|
580
|
+
done.delete(check.mainStep);
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
const actual = normalizeBlockName(raw);
|
|
584
|
+
const expectId = blockIdOf(check.state);
|
|
585
|
+
if (blockIdOf(actual) === expectId) continue;
|
|
586
|
+
done.delete(check.mainStep);
|
|
587
|
+
if (!isAirState(actual)) note(check.pos, pos, expectId, blockIdOf(actual), 'wrong-block');
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (checkAir) {
|
|
591
|
+
const [sizeX, sizeY, sizeZ] = blueprint.size_xyz;
|
|
592
|
+
for (let y = 0; y < sizeY; y++) {
|
|
593
|
+
for (let z = 0; z < sizeZ; z++) {
|
|
594
|
+
for (let x = 0; x < sizeX; x++) {
|
|
595
|
+
const expected = blueprint.layers[y][z][x];
|
|
596
|
+
if (!isAirState(expected) || isStructureVoidState(expected)) continue;
|
|
597
|
+
const local: PositionXYZ = [x, y, z];
|
|
598
|
+
const pos = toWorld(anchor, local);
|
|
599
|
+
const raw = read(pos[0], pos[1], pos[2]);
|
|
600
|
+
if (raw === null) { unknown++; continue; }
|
|
601
|
+
const actual = normalizeBlockName(raw);
|
|
602
|
+
if (!isAirState(actual)) note(local, pos, 'minecraft:air', actual, 'should-be-air');
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const doneSteps = plan.steps.filter((step) => done.has(step.index)).map((step) => step.index);
|
|
609
|
+
const remaining = plan.steps.filter((step) => !done.has(step.index));
|
|
610
|
+
let cursor = 0;
|
|
611
|
+
while (done.has(cursor)) cursor++;
|
|
612
|
+
|
|
613
|
+
return {
|
|
614
|
+
total: plan.placeCells,
|
|
615
|
+
matched,
|
|
616
|
+
missing,
|
|
617
|
+
unknown,
|
|
618
|
+
conflictCounts,
|
|
619
|
+
conflicts,
|
|
620
|
+
doneSteps,
|
|
621
|
+
remaining,
|
|
622
|
+
cursor,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// ── 三分账单 ──────────────────────────────────────────────────────────────────
|
|
627
|
+
|
|
628
|
+
/** 物品名到数量的映射。 */
|
|
629
|
+
export type ItemTally = Readonly<Record<string, number>>;
|
|
630
|
+
|
|
631
|
+
interface BlueprintBillInput {
|
|
632
|
+
/** 随身 */
|
|
633
|
+
carried?: ItemTally;
|
|
634
|
+
/** 容器上次观测的合计数量。 */
|
|
635
|
+
stored?: ItemTally;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
export interface BlueprintBillLine {
|
|
639
|
+
item: string;
|
|
640
|
+
need: number;
|
|
641
|
+
carried: number;
|
|
642
|
+
stored: number;
|
|
643
|
+
/** 还缺 = need - carried - stored,不为负 */
|
|
644
|
+
missing: number;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
interface BlueprintBill {
|
|
648
|
+
lines: BlueprintBillLine[];
|
|
649
|
+
totalNeed: number;
|
|
650
|
+
totalMissing: number;
|
|
651
|
+
/** 只用随身的料,从头连着能盖完前几步 */
|
|
652
|
+
reachableSteps: number;
|
|
653
|
+
reachableCells: number;
|
|
654
|
+
/** 把箱里的也取来,从头连着能盖完前几步 */
|
|
655
|
+
reachableStepsWithStored: number;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function tallyOf(source: ItemTally | undefined, item: string): number {
|
|
659
|
+
/** 工具族按 _hoe/_shovel 后缀匹配;耗材要求精确物品名。 */
|
|
660
|
+
const family = BLUEPRINT_TOOL_FAMILIES.has(item);
|
|
661
|
+
let total = 0;
|
|
662
|
+
for (const [name, value] of Object.entries(source ?? {})) {
|
|
663
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) continue;
|
|
664
|
+
if (name !== item && !(family && name.endsWith(`_${item}`))) continue;
|
|
665
|
+
total += Math.max(0, Math.floor(value));
|
|
666
|
+
}
|
|
667
|
+
return total;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
interface BlueprintRequirement {
|
|
671
|
+
item: string;
|
|
672
|
+
count: number;
|
|
673
|
+
reusable: boolean;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** 一步需要的耗材与工具。可重复使用的工具在整份账里只计一件。 */
|
|
677
|
+
function blueprintStepRequirements(step: BlueprintStep): BlueprintRequirement[] {
|
|
678
|
+
if (step.method.kind === 'place') return [{ item: step.item, count: step.cells, reusable: false }];
|
|
679
|
+
return [
|
|
680
|
+
...(step.method.baseItem
|
|
681
|
+
? [{ item: step.method.baseItem[0], count: step.cells, reusable: false }]
|
|
682
|
+
: []),
|
|
683
|
+
{
|
|
684
|
+
item: step.method.item,
|
|
685
|
+
count: step.method.reusable ? 1 : step.cells,
|
|
686
|
+
reusable: step.method.reusable === true,
|
|
687
|
+
},
|
|
688
|
+
...(blueprintNeedsTilledSoil(step.state)
|
|
689
|
+
? [{ item: 'hoe', count: 1, reusable: true }]
|
|
690
|
+
: []),
|
|
691
|
+
];
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/** 手上的料从头连着能盖完前几步(遇到第一步不够就停,不跳步) */
|
|
695
|
+
function reachable(
|
|
696
|
+
steps: readonly BlueprintStep[],
|
|
697
|
+
stock: Map<string, number>,
|
|
698
|
+
): { steps: number; cells: number } {
|
|
699
|
+
const left = new Map(stock);
|
|
700
|
+
let count = 0;
|
|
701
|
+
let cells = 0;
|
|
702
|
+
for (const step of steps) {
|
|
703
|
+
const requirements = blueprintStepRequirements(step);
|
|
704
|
+
if (requirements.some((need) => (left.get(need.item) ?? 0) < need.count)) break;
|
|
705
|
+
for (const need of requirements) {
|
|
706
|
+
if (!need.reusable) left.set(need.item, (left.get(need.item) ?? 0) - need.count);
|
|
707
|
+
}
|
|
708
|
+
count++;
|
|
709
|
+
cells += step.cells;
|
|
710
|
+
}
|
|
711
|
+
return { steps: count, cells };
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/** 按剩余步骤顺序扣减材料,统计随身、容器与缺口;材料不足时停止计算可完成步数。 */
|
|
715
|
+
export function billForSteps(
|
|
716
|
+
steps: readonly BlueprintStep[],
|
|
717
|
+
input: BlueprintBillInput = {},
|
|
718
|
+
): BlueprintBill {
|
|
719
|
+
const need = new Map<string, number>();
|
|
720
|
+
for (const step of steps) {
|
|
721
|
+
for (const requirement of blueprintStepRequirements(step)) {
|
|
722
|
+
const prev = need.get(requirement.item) ?? 0;
|
|
723
|
+
need.set(requirement.item, requirement.reusable
|
|
724
|
+
? Math.max(prev, requirement.count)
|
|
725
|
+
: prev + requirement.count);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const lines: BlueprintBillLine[] = [...need].map(([item, count]) => {
|
|
730
|
+
const carried = tallyOf(input.carried, item);
|
|
731
|
+
const stored = tallyOf(input.stored, item);
|
|
732
|
+
return {
|
|
733
|
+
item,
|
|
734
|
+
need: count,
|
|
735
|
+
carried,
|
|
736
|
+
stored,
|
|
737
|
+
missing: Math.max(0, count - carried - stored),
|
|
738
|
+
};
|
|
739
|
+
}).sort((left, right) =>
|
|
740
|
+
right.missing - left.missing || right.need - left.need || left.item.localeCompare(right.item));
|
|
741
|
+
|
|
742
|
+
const carriedStock = new Map(lines.map((line) => [line.item, line.carried]));
|
|
743
|
+
const bothStock = new Map(lines.map((line) => [line.item, line.carried + line.stored]));
|
|
744
|
+
const byCarried = reachable(steps, carriedStock);
|
|
745
|
+
|
|
746
|
+
return {
|
|
747
|
+
lines,
|
|
748
|
+
totalNeed: lines.reduce((sum, line) => sum + line.need, 0),
|
|
749
|
+
totalMissing: lines.reduce((sum, line) => sum + line.missing, 0),
|
|
750
|
+
reachableSteps: byCarried.steps,
|
|
751
|
+
reachableCells: byCarried.cells,
|
|
752
|
+
reachableStepsWithStored: reachable(steps, bothStock).steps,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// ── 进度与按层分段 ────────────────────────────────────────────────────────────
|
|
757
|
+
|
|
758
|
+
interface BlueprintLayerSpan {
|
|
759
|
+
y: number;
|
|
760
|
+
/** 这一层第一步的步序 */
|
|
761
|
+
firstStep: number;
|
|
762
|
+
/** 这一层结束时的步数(= 最后一步的 index + 1),就是 stopAfter 该停的游标 */
|
|
763
|
+
endStep: number;
|
|
764
|
+
steps: number;
|
|
765
|
+
cells: number;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/** 按 y 层分段:每层从第几步到第几步。`stopAfter` 按层停就读 `endStep`。 */
|
|
769
|
+
export function layerSpans(steps: readonly BlueprintStep[]): BlueprintLayerSpan[] {
|
|
770
|
+
const spans: BlueprintLayerSpan[] = [];
|
|
771
|
+
for (const step of steps) {
|
|
772
|
+
const last = spans[spans.length - 1];
|
|
773
|
+
if (last !== undefined && last.y === step.y) {
|
|
774
|
+
last.endStep = step.index + 1;
|
|
775
|
+
last.steps++;
|
|
776
|
+
last.cells += step.cells;
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
spans.push({
|
|
780
|
+
y: step.y,
|
|
781
|
+
firstStep: step.index,
|
|
782
|
+
endStep: step.index + 1,
|
|
783
|
+
steps: 1,
|
|
784
|
+
cells: step.cells,
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
return spans;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* 「盖到第 y 层为止」对应走到第几步。该层一步都没有(整层空气)时向下取最近的一层;
|
|
792
|
+
* 比最低层还低就返回 0。
|
|
793
|
+
*/
|
|
794
|
+
export function stepCountThroughLayer(steps: readonly BlueprintStep[], y: number): number {
|
|
795
|
+
let count = 0;
|
|
796
|
+
for (const step of steps) {
|
|
797
|
+
if (step.y > y) break;
|
|
798
|
+
count = step.index + 1;
|
|
799
|
+
}
|
|
800
|
+
return count;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
interface BlueprintProgress {
|
|
804
|
+
steps: { done: number; total: number };
|
|
805
|
+
cells: { done: number; total: number };
|
|
806
|
+
/** 正在盖第几层(全盖完是 null),以及这一层的步数进度 */
|
|
807
|
+
layer: { y: number | null; done: number; total: number };
|
|
808
|
+
/** 0..1,两位小数,按格数算 */
|
|
809
|
+
ratio: number;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/** 游标 → 进度。游标口径 = 已完成 IR 步数(从头连着的那些)。 */
|
|
813
|
+
export function blueprintProgress(
|
|
814
|
+
steps: readonly BlueprintStep[],
|
|
815
|
+
cursor: number,
|
|
816
|
+
): BlueprintProgress {
|
|
817
|
+
const total = steps.length;
|
|
818
|
+
const done = Math.max(0, Math.min(total, Math.floor(cursor)));
|
|
819
|
+
const totalCells = steps.reduce((sum, step) => sum + step.cells, 0);
|
|
820
|
+
const doneCells = steps.slice(0, done).reduce((sum, step) => sum + step.cells, 0);
|
|
821
|
+
|
|
822
|
+
const current = done < total ? steps[done].y : null;
|
|
823
|
+
const span = current === null
|
|
824
|
+
? null
|
|
825
|
+
: layerSpans(steps).find((entry) => entry.y === current) ?? null;
|
|
826
|
+
|
|
827
|
+
return {
|
|
828
|
+
steps: { done, total },
|
|
829
|
+
cells: { done: doneCells, total: totalCells },
|
|
830
|
+
layer: {
|
|
831
|
+
y: current,
|
|
832
|
+
done: span === null ? 0 : done - span.firstStep,
|
|
833
|
+
total: span?.steps ?? 0,
|
|
834
|
+
},
|
|
835
|
+
ratio: totalCells === 0 ? 1 : Math.round((doneCells / totalCells) * 100) / 100,
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// ── 回读验收 ──────────────────────────────────────────────────────────────────
|
|
840
|
+
|
|
841
|
+
/** type 不符判失败;type 相同而 state 漂移单独报告,不判失败。 */
|
|
842
|
+
type ReadbackVerdict = 'exact' | 'state-drift' | 'failed';
|
|
843
|
+
|
|
844
|
+
export function classifyReadback(expected: string, actual: string | null): ReadbackVerdict {
|
|
845
|
+
if (actual === null) return 'failed';
|
|
846
|
+
const actualState = normalizeBlockName(actual);
|
|
847
|
+
if (actualState === expected) return 'exact';
|
|
848
|
+
return blockIdOf(actualState) === blockIdOf(expected) ? 'state-drift' : 'failed';
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
export interface ReadbackEntry {
|
|
852
|
+
pos: PositionXYZ;
|
|
853
|
+
expected: string;
|
|
854
|
+
/** 世界读回来的完整状态串;读不到给 null */
|
|
855
|
+
actual: string | null;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
interface ReadbackSummary {
|
|
859
|
+
total: number;
|
|
860
|
+
exact: number;
|
|
861
|
+
drift: number;
|
|
862
|
+
failed: number;
|
|
863
|
+
/** 漂了哪些属性:`minecraft:oak_stairs shape` → 次数 */
|
|
864
|
+
driftProperties: Record<string, number>;
|
|
865
|
+
driftSamples: ReadbackEntry[];
|
|
866
|
+
failedSamples: ReadbackEntry[];
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function driftedProperties(expected: string, actual: string): string[] {
|
|
870
|
+
const props = (state: string): Map<string, string> => {
|
|
871
|
+
const bracket = state.indexOf('[');
|
|
872
|
+
if (bracket === -1) return new Map();
|
|
873
|
+
return new Map(state.slice(bracket + 1, -1).split(',').map((pair) => {
|
|
874
|
+
const separator = pair.indexOf('=');
|
|
875
|
+
return [pair.slice(0, separator), pair.slice(separator + 1)] as [string, string];
|
|
876
|
+
}));
|
|
877
|
+
};
|
|
878
|
+
const left = props(expected);
|
|
879
|
+
const right = props(actual);
|
|
880
|
+
const names = new Set([...left.keys(), ...right.keys()]);
|
|
881
|
+
return [...names].filter((name) => left.get(name) !== right.get(name)).sort();
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/** 回读汇总;样本按 sampleLimit 截断,drift 顺带统计漂在哪个属性上 */
|
|
885
|
+
export function summarizeReadback(
|
|
886
|
+
entries: readonly ReadbackEntry[],
|
|
887
|
+
sampleLimit = 8,
|
|
888
|
+
): ReadbackSummary {
|
|
889
|
+
const summary: ReadbackSummary = {
|
|
890
|
+
total: entries.length,
|
|
891
|
+
exact: 0,
|
|
892
|
+
drift: 0,
|
|
893
|
+
failed: 0,
|
|
894
|
+
driftProperties: {},
|
|
895
|
+
driftSamples: [],
|
|
896
|
+
failedSamples: [],
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
for (const entry of entries) {
|
|
900
|
+
const verdict = classifyReadback(entry.expected, entry.actual);
|
|
901
|
+
if (verdict === 'exact') { summary.exact++; continue; }
|
|
902
|
+
if (verdict === 'failed') {
|
|
903
|
+
summary.failed++;
|
|
904
|
+
if (summary.failedSamples.length < sampleLimit) summary.failedSamples.push(entry);
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
summary.drift++;
|
|
908
|
+
if (summary.driftSamples.length < sampleLimit) summary.driftSamples.push(entry);
|
|
909
|
+
for (const name of driftedProperties(entry.expected, normalizeBlockName(entry.actual ?? ''))) {
|
|
910
|
+
const label = `${blockIdOf(entry.expected)} ${name}`;
|
|
911
|
+
summary.driftProperties[label] = (summary.driftProperties[label] ?? 0) + 1;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return summary;
|
|
915
|
+
}
|