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,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 挂载面板聚合游戏服务器、观察者客户端和玩家客户端的独立控制面。
|
|
3
|
+
* 方法前缀标识控制面;就绪轮询随面板卸载结束。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ConsolePanelContext,
|
|
8
|
+
ConsolePanel,
|
|
9
|
+
} from '../../../web/shared/client-panel.ts';
|
|
10
|
+
import {
|
|
11
|
+
errText,
|
|
12
|
+
msgLine,
|
|
13
|
+
type MinecraftClientState,
|
|
14
|
+
type MinecraftMsgLine,
|
|
15
|
+
type MinecraftServerState,
|
|
16
|
+
} from './client.ts';
|
|
17
|
+
|
|
18
|
+
const DESC =
|
|
19
|
+
'开玩前把链路带起来。服务器目录首次使用要在本 World 配置里填好路径。'
|
|
20
|
+
+ '观察者客户端是可选的第二条:拉起一份真客户端以观察者模式跟着 bot,'
|
|
21
|
+
+ '画面(含光影/模组)随即换成它。玩家客户端是第三条:以普通玩家身份进同一个'
|
|
22
|
+
+ '服务器,自己跟她一起玩,进服时会被传送到她旁边。';
|
|
23
|
+
|
|
24
|
+
/** 世界生成 / 权重加载 / 客户端起窗口要几十秒到几分钟,轮到不再是 starting 为止。 */
|
|
25
|
+
const READY_TICKS = 90;
|
|
26
|
+
const READY_TICK_MS = 2000;
|
|
27
|
+
|
|
28
|
+
/** 三条链路在服务端的方法前缀。 */
|
|
29
|
+
type Lane = 'server' | 'client' | 'player';
|
|
30
|
+
|
|
31
|
+
/** 启停两颗钮认得的最小状态形状;`reachable` 与 `windowReady` 各链路只有一个。 */
|
|
32
|
+
interface LaneState {
|
|
33
|
+
phase: string;
|
|
34
|
+
detail: string | null;
|
|
35
|
+
pid: number | null;
|
|
36
|
+
reachable?: boolean;
|
|
37
|
+
windowReady?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 就绪判据:有端口的看端口,没端口的(客户端)看窗口。 */
|
|
41
|
+
function isUp(st: LaneState): boolean {
|
|
42
|
+
return st.reachable !== undefined ? st.reachable : st.windowReady === true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// 挂载行
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
type Tone = 'on' | 'warn' | 'off' | 'plain';
|
|
50
|
+
|
|
51
|
+
interface MountRow {
|
|
52
|
+
el: HTMLDivElement;
|
|
53
|
+
/** 右侧动作区,启停两颗钮塞这儿 */
|
|
54
|
+
acts: HTMLSpanElement;
|
|
55
|
+
set(tone: Tone, word: string, note?: string | null): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 一条链路一行(.mountrow:圆点、名字、状态词、细节、动作区)。 */
|
|
59
|
+
function mountRow(ctx: ConsolePanelContext, name: string): MountRow {
|
|
60
|
+
const { ui } = ctx;
|
|
61
|
+
const el = ui.h('div', 'mountrow');
|
|
62
|
+
const dot = ui.h('span', 'navdot');
|
|
63
|
+
const state = ui.h('span', 'mstate', '—');
|
|
64
|
+
const detail = ui.h('span', 'mdetail');
|
|
65
|
+
const acts = ui.h('span', 'macts');
|
|
66
|
+
el.append(dot, ui.h('span', 'mname', name), state, detail, acts);
|
|
67
|
+
return {
|
|
68
|
+
el,
|
|
69
|
+
acts,
|
|
70
|
+
set(tone, word, note) {
|
|
71
|
+
dot.className = `navdot${tone === 'on' ? ' on' : tone === 'warn' ? ' warn' : tone === 'off' ? ' bad' : ''}`;
|
|
72
|
+
state.className = `mstate mc-${tone}`;
|
|
73
|
+
state.textContent = word;
|
|
74
|
+
detail.textContent = note ?? '';
|
|
75
|
+
detail.title = note ?? '';
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 一条链路的三件套:行 + 启 + 停。 */
|
|
81
|
+
interface LaneView {
|
|
82
|
+
row: MountRow;
|
|
83
|
+
start: HTMLButtonElement;
|
|
84
|
+
stop: HTMLButtonElement;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function laneView(ctx: ConsolePanelContext, name: string): LaneView {
|
|
88
|
+
const row = mountRow(ctx, name);
|
|
89
|
+
const stop = ctx.ui.button('停止', { size: 'sm', variant: 'danger' });
|
|
90
|
+
const start = ctx.ui.button('启动', { size: 'sm', variant: 'primary' });
|
|
91
|
+
row.acts.append(stop, start);
|
|
92
|
+
return { row, start, stop };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 两行共用的渲染:reachable 时不管托管 phase 一律亮绿(外部起的也算就绪)。 */
|
|
96
|
+
function renderMount<T extends LaneState>(
|
|
97
|
+
view: LaneView,
|
|
98
|
+
st: T | null,
|
|
99
|
+
extra: (st: T) => string,
|
|
100
|
+
): void {
|
|
101
|
+
if (!st) {
|
|
102
|
+
view.row.set('plain', '不可用', '');
|
|
103
|
+
view.start.disabled = true;
|
|
104
|
+
view.stop.disabled = true;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
let tone: Tone;
|
|
108
|
+
let word: string;
|
|
109
|
+
if (isUp(st)) {
|
|
110
|
+
tone = 'on';
|
|
111
|
+
word = st.pid ? '运行中' : '运行中(外部)';
|
|
112
|
+
} else {
|
|
113
|
+
tone = st.phase === 'starting' ? 'warn' : 'off';
|
|
114
|
+
word = ({ starting: '启动中', error: '异常', stopped: '未启动' } as Record<string, string>)[st.phase]
|
|
115
|
+
?? st.phase;
|
|
116
|
+
}
|
|
117
|
+
view.row.set(tone, word, extra(st));
|
|
118
|
+
view.start.disabled = st.phase === 'starting' || isUp(st);
|
|
119
|
+
view.stop.disabled = st.pid === null && !['starting', 'running'].includes(st.phase);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
export const mountPanel: ConsolePanel = {
|
|
125
|
+
mount(ctx: ConsolePanelContext) {
|
|
126
|
+
const { ui, root } = ctx;
|
|
127
|
+
const card = ui.sheet({ title: '挂载', en: 'bring-up', desc: DESC });
|
|
128
|
+
|
|
129
|
+
const msg = msgLine(ctx, 'grow');
|
|
130
|
+
const btnAll = ui.button('⏻ 一键挂载', { variant: 'primary' });
|
|
131
|
+
const btnRefresh = ui.button('刷新', { size: 'sm' });
|
|
132
|
+
const bar = ui.rowbar();
|
|
133
|
+
bar.append(btnAll, msg.el, btnRefresh);
|
|
134
|
+
card.body.appendChild(bar);
|
|
135
|
+
|
|
136
|
+
const srv = laneView(ctx, '游戏服务器');
|
|
137
|
+
const cli = laneView(ctx, '观察者客户端');
|
|
138
|
+
const ply = laneView(ctx, '玩家客户端');
|
|
139
|
+
const pickServer = ui.button('选择目录…', { size: 'sm' });
|
|
140
|
+
const pickClient = ui.button('选择目录…', { size: 'sm' });
|
|
141
|
+
const pickPlayer = ui.button('选择目录…', { size: 'sm' });
|
|
142
|
+
srv.row.acts.prepend(pickServer);
|
|
143
|
+
cli.row.acts.prepend(pickClient);
|
|
144
|
+
ply.row.acts.prepend(pickPlayer);
|
|
145
|
+
// 传送是这一行独有的动作:自动那次挂在"进服"那一刻上,中途死了、退回主菜单
|
|
146
|
+
// 再进都够不着它
|
|
147
|
+
const btnTp = ui.button('传送到她旁边', { size: 'sm' });
|
|
148
|
+
ply.row.acts.prepend(btnTp);
|
|
149
|
+
card.body.append(srv.row.el, cli.row.el, ply.row.el);
|
|
150
|
+
root.appendChild(card.el);
|
|
151
|
+
|
|
152
|
+
const alive = (): boolean => !ctx.signal.aborted;
|
|
153
|
+
let serverState: MinecraftServerState | null = null;
|
|
154
|
+
let clientState: MinecraftClientState | null = null;
|
|
155
|
+
let playerState: MinecraftClientState | null = null;
|
|
156
|
+
|
|
157
|
+
// ---- 三条链路各自的渲染 ----
|
|
158
|
+
|
|
159
|
+
const renderSrv = (st: MinecraftServerState | null): void => {
|
|
160
|
+
if (!alive()) return;
|
|
161
|
+
serverState = st;
|
|
162
|
+
if (st && !st.configured && st.phase === 'stopped' && !st.reachable) {
|
|
163
|
+
srv.row.set('warn', '未配置', '在本 World 配置里填 worlds.minecraft.local.serverDir(含 server.jar 的目录)');
|
|
164
|
+
srv.start.disabled = false;
|
|
165
|
+
srv.stop.disabled = true;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
renderMount(srv, st, (s) =>
|
|
169
|
+
[s.pid ? `pid ${s.pid}` : null, s.address, s.detail].filter(Boolean).join(' · '));
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/** 客户端没有可探的端口,判据是"窗口出来了没"。 */
|
|
173
|
+
const renderCli = (st: MinecraftClientState | null): void => {
|
|
174
|
+
if (!alive()) return;
|
|
175
|
+
clientState = st;
|
|
176
|
+
if (!st) { renderMount<LaneState>(cli, null, () => ''); return; }
|
|
177
|
+
if (!st.configured) {
|
|
178
|
+
cli.row.set('warn', '未配置', st.detail || '在配置里填 worlds.minecraft.client.gameDir');
|
|
179
|
+
cli.start.disabled = true;
|
|
180
|
+
cli.stop.disabled = st.pid === null;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// 客户端那条的就绪判据是窗口,补一个 reachable 让三行走同一份渲染
|
|
184
|
+
const merged: MinecraftClientState & { reachable: boolean } = { ...st, reachable: st.windowReady };
|
|
185
|
+
renderMount(cli, merged, (c) => [
|
|
186
|
+
c.pid ? `pid ${c.pid}` : null,
|
|
187
|
+
c.versionId,
|
|
188
|
+
c.username,
|
|
189
|
+
c.enabled ? null : '配置未启用(可手动启停)',
|
|
190
|
+
c.detail,
|
|
191
|
+
].filter(Boolean).join(' · '));
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/** 玩家那条与摄像机同一套判据(窗口出来了没)。 */
|
|
195
|
+
const renderPly = (st: MinecraftClientState | null): void => {
|
|
196
|
+
if (!alive()) return;
|
|
197
|
+
playerState = st;
|
|
198
|
+
if (!st) { renderMount<LaneState>(ply, null, () => ''); btnTp.disabled = true; return; }
|
|
199
|
+
btnTp.disabled = !st.windowReady;
|
|
200
|
+
if (!st.configured) {
|
|
201
|
+
ply.row.set('warn', '未配置', st.detail || '在配置里填 worlds.minecraft.player.gameDir(或先配好观察者客户端那份)');
|
|
202
|
+
ply.start.disabled = true;
|
|
203
|
+
ply.stop.disabled = st.pid === null;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const merged: MinecraftClientState & { reachable: boolean } = { ...st, reachable: st.windowReady };
|
|
207
|
+
renderMount(ply, merged, (c) => [
|
|
208
|
+
c.pid ? `pid ${c.pid}` : null,
|
|
209
|
+
c.versionId,
|
|
210
|
+
c.username,
|
|
211
|
+
c.enabled ? null : '配置未启用(可手动启停)',
|
|
212
|
+
c.detail,
|
|
213
|
+
].filter(Boolean).join(' · '));
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// ---- 取数 ----
|
|
217
|
+
|
|
218
|
+
const fetchSrv = (): Promise<MinecraftServerState | null> =>
|
|
219
|
+
ctx.invoke<MinecraftServerState>('server.state').then(
|
|
220
|
+
(st) => { renderSrv(st); return st; },
|
|
221
|
+
() => { renderSrv(null); return null; },
|
|
222
|
+
);
|
|
223
|
+
const fetchCli = (): Promise<MinecraftClientState | null> =>
|
|
224
|
+
ctx.invoke<MinecraftClientState>('client.state').then(
|
|
225
|
+
(st) => { renderCli(st); return st; },
|
|
226
|
+
() => { renderCli(null); return null; },
|
|
227
|
+
);
|
|
228
|
+
const fetchPly = (): Promise<MinecraftClientState | null> =>
|
|
229
|
+
ctx.invoke<MinecraftClientState>('player.state').then(
|
|
230
|
+
(st) => { renderPly(st); return st; },
|
|
231
|
+
() => { renderPly(null); return null; },
|
|
232
|
+
);
|
|
233
|
+
const refresh = (): Promise<unknown> =>
|
|
234
|
+
Promise.all([fetchSrv(), fetchCli(), fetchPly()]);
|
|
235
|
+
const refreshServerAfterPath = async (): Promise<void> => {
|
|
236
|
+
const needsRestart = serverState !== null && (serverState.pid !== null || serverState.reachable);
|
|
237
|
+
await fetchSrv();
|
|
238
|
+
if (needsRestart) msg.say('路径已保存;停止并重新启动游戏服务器后采用新目录');
|
|
239
|
+
};
|
|
240
|
+
const refreshClientAfterPath = async (
|
|
241
|
+
state: MinecraftClientState | null,
|
|
242
|
+
fetchState: () => Promise<MinecraftClientState | null>,
|
|
243
|
+
name: string,
|
|
244
|
+
): Promise<void> => {
|
|
245
|
+
const needsRestart = state !== null && (state.pid !== null || state.windowReady);
|
|
246
|
+
await fetchState();
|
|
247
|
+
if (needsRestart) msg.say(`路径已保存;停止并重新启动${name}后采用新目录`);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const choose = async (
|
|
251
|
+
button: HTMLButtonElement,
|
|
252
|
+
options: Parameters<ConsolePanelContext['pickPath']>[0],
|
|
253
|
+
groupId: string,
|
|
254
|
+
key: string,
|
|
255
|
+
after: () => Promise<unknown>,
|
|
256
|
+
): Promise<void> => {
|
|
257
|
+
const lock = ui.disable(button);
|
|
258
|
+
try {
|
|
259
|
+
const path = await ctx.pickPath(options);
|
|
260
|
+
if (!path || !alive()) return;
|
|
261
|
+
await ctx.setConfig(groupId, { [key]: path });
|
|
262
|
+
msg.say(`已保存: ${path}`);
|
|
263
|
+
await after();
|
|
264
|
+
} catch (err) {
|
|
265
|
+
if (alive()) msg.say(`路径保存失败: ${errText(err)}`, true);
|
|
266
|
+
} finally {
|
|
267
|
+
lock.dispose();
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
pickServer.addEventListener('click', () => {
|
|
272
|
+
void choose(pickServer, {
|
|
273
|
+
kind: 'directory', title: '选择 Minecraft 服务器目录',
|
|
274
|
+
...(serverState?.serverDir ? { currentPath: serverState.serverDir } : {}),
|
|
275
|
+
recommendedDir: '../Cortico-Resources/minecraft/server',
|
|
276
|
+
}, 'world:minecraft', 'worlds.minecraft.local.serverDir', refreshServerAfterPath);
|
|
277
|
+
}, { signal: ctx.signal });
|
|
278
|
+
pickClient.addEventListener('click', () => {
|
|
279
|
+
void choose(pickClient, {
|
|
280
|
+
kind: 'directory', title: '选择观察者 Minecraft 客户端目录',
|
|
281
|
+
...(clientState?.gameDir ? { currentPath: clientState.gameDir } : {}),
|
|
282
|
+
recommendedDir: '../Cortico-Resources/minecraft/client',
|
|
283
|
+
}, 'world:minecraft:client', 'worlds.minecraft.client.gameDir',
|
|
284
|
+
() => refreshClientAfterPath(clientState, fetchCli, '观察者客户端'));
|
|
285
|
+
}, { signal: ctx.signal });
|
|
286
|
+
pickPlayer.addEventListener('click', () => {
|
|
287
|
+
void choose(pickPlayer, {
|
|
288
|
+
kind: 'directory', title: '选择玩家 Minecraft 客户端目录',
|
|
289
|
+
...(playerState?.gameDir ? { currentPath: playerState.gameDir } : {}),
|
|
290
|
+
recommendedDir: '../Cortico-Resources/minecraft/client-player',
|
|
291
|
+
}, 'world:minecraft:player', 'worlds.minecraft.player.gameDir',
|
|
292
|
+
() => refreshClientAfterPath(playerState, fetchPly, '玩家客户端'));
|
|
293
|
+
}, { signal: ctx.signal });
|
|
294
|
+
|
|
295
|
+
// ---- 启停 ----
|
|
296
|
+
|
|
297
|
+
const render = (lane: Lane, st: LaneState | null): void => {
|
|
298
|
+
if (lane === 'server') renderSrv(st as MinecraftServerState | null);
|
|
299
|
+
else if (lane === 'player') renderPly(st as MinecraftClientState | null);
|
|
300
|
+
else renderCli(st as MinecraftClientState | null);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* 等一条链路走完 starting。轮询走 `ctx.interval`,面板一卸载就停;
|
|
305
|
+
* 等待的 promise 由 `signal` 一并收束,不会永挂在那儿。
|
|
306
|
+
*/
|
|
307
|
+
const waitReady = (lane: Lane, name: string): Promise<string> =>
|
|
308
|
+
new Promise<string>((resolve) => {
|
|
309
|
+
let ticks = 0;
|
|
310
|
+
let probing = false;
|
|
311
|
+
const done = (note: string): void => { timer.dispose(); resolve(note); };
|
|
312
|
+
const timer = ctx.interval(() => {
|
|
313
|
+
if (probing) return;
|
|
314
|
+
if (++ticks > READY_TICKS) { done(`[失败] ${name} 启动超时`); return; }
|
|
315
|
+
probing = true;
|
|
316
|
+
void ctx.invoke<LaneState>(`${lane}.state`).then(
|
|
317
|
+
(st) => {
|
|
318
|
+
render(lane, st);
|
|
319
|
+
if (st.phase === 'starting') { probing = false; return; }
|
|
320
|
+
done(isUp(st) ? `${name}就绪` : `[失败] ${name} ${st.detail || st.phase}`);
|
|
321
|
+
},
|
|
322
|
+
(err: unknown) => { done(`[失败] ${name} ${errText(err)}`); },
|
|
323
|
+
);
|
|
324
|
+
}, READY_TICK_MS);
|
|
325
|
+
ctx.signal.addEventListener('abort', () => resolve(`${name}等待已中断`), { once: true });
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const bindStartStop = (lane: Lane, name: string, view: LaneView, startNote: string): void => {
|
|
329
|
+
view.start.addEventListener('click', () => { void doStart(); }, { signal: ctx.signal });
|
|
330
|
+
view.stop.addEventListener('click', () => { void doStop(); }, { signal: ctx.signal });
|
|
331
|
+
|
|
332
|
+
async function doStart(): Promise<void> {
|
|
333
|
+
msg.say(startNote);
|
|
334
|
+
try {
|
|
335
|
+
const st = await ctx.invoke<LaneState>(`${lane}.start`);
|
|
336
|
+
render(lane, st);
|
|
337
|
+
if (st.phase === 'error') { msg.say(`[失败] ${name} ${st.detail || ''}`, true); return; }
|
|
338
|
+
msg.say(await waitReady(lane, name));
|
|
339
|
+
} catch (err) {
|
|
340
|
+
if (alive()) msg.say(`启动失败: ${errText(err)}`, true);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async function doStop(): Promise<void> {
|
|
344
|
+
msg.say('');
|
|
345
|
+
try {
|
|
346
|
+
render(lane, await ctx.invoke<LaneState>(`${lane}.stop`));
|
|
347
|
+
msg.say(`${name}已停止`);
|
|
348
|
+
} catch (err) {
|
|
349
|
+
if (alive()) msg.say(`停止失败: ${errText(err)}`, true);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
bindStartStop('server', '服务器', srv, '启动中…(要加载世界)');
|
|
355
|
+
bindStartStop('client', '客户端', cli, '启动中…(真客户端,要加载资源与着色器)');
|
|
356
|
+
bindStartStop('player', '玩家客户端', ply, '启动中…(真客户端,要加载资源与着色器)');
|
|
357
|
+
|
|
358
|
+
btnTp.addEventListener('click', () => {
|
|
359
|
+
void ctx.invoke<MinecraftClientState>('player.teleport').then(
|
|
360
|
+
(st) => { msg.say(st.detail ?? ''); renderPly(st); },
|
|
361
|
+
(err: unknown) => { if (alive()) msg.say(`传送失败: ${errText(err)}`, true); },
|
|
362
|
+
);
|
|
363
|
+
}, { signal: ctx.signal });
|
|
364
|
+
|
|
365
|
+
btnRefresh.addEventListener('click', () => { msg.say(''); void refresh(); }, { signal: ctx.signal });
|
|
366
|
+
btnAll.addEventListener('click', () => { void mountAll(ctx, msg, btnAll, render, refresh, waitReady); },
|
|
367
|
+
{ signal: ctx.signal });
|
|
368
|
+
|
|
369
|
+
void refresh();
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// ---------------------------------------------------------------------------
|
|
374
|
+
// 一键挂载
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* 三条链路一起带起来。已经在跑的不重复启动;**两份客户端都很重,配置里没开就
|
|
379
|
+
* 不替人做主**,只带起已声明要用的那几条。
|
|
380
|
+
*/
|
|
381
|
+
async function mountAll(
|
|
382
|
+
ctx: ConsolePanelContext,
|
|
383
|
+
msg: MinecraftMsgLine,
|
|
384
|
+
btnAll: HTMLButtonElement,
|
|
385
|
+
render: (lane: Lane, st: LaneState | null) => void,
|
|
386
|
+
refresh: () => Promise<unknown>,
|
|
387
|
+
waitReady: (lane: Lane, name: string) => Promise<string>,
|
|
388
|
+
): Promise<void> {
|
|
389
|
+
const lock = ctx.ui.disable(btnAll);
|
|
390
|
+
msg.say('挂载中…');
|
|
391
|
+
try {
|
|
392
|
+
const jobs: Array<Promise<string>> = [];
|
|
393
|
+
const bring = async (lane: Lane, name: string, up: boolean): Promise<string> => {
|
|
394
|
+
if (up) return `${name}已在运行`;
|
|
395
|
+
const st = await ctx.invoke<LaneState>(`${lane}.start`);
|
|
396
|
+
render(lane, st);
|
|
397
|
+
if (st.phase === 'error') return `[失败] ${name} ${st.detail || ''}`;
|
|
398
|
+
return waitReady(lane, name);
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const srv = await ctx.invoke<MinecraftServerState>('server.state');
|
|
402
|
+
jobs.push(bring('server', '服务器', srv.reachable));
|
|
403
|
+
const cli = await ctx.invoke<MinecraftClientState>('client.state').catch(() => null);
|
|
404
|
+
if (cli && cli.enabled && cli.configured) {
|
|
405
|
+
jobs.push(bring('client', '客户端', cli.windowReady));
|
|
406
|
+
}
|
|
407
|
+
const ply = await ctx.invoke<MinecraftClientState>('player.state').catch(() => null);
|
|
408
|
+
if (ply && ply.enabled && ply.configured) {
|
|
409
|
+
jobs.push(bring('player', '玩家客户端', ply.windowReady));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const notes = await Promise.all(jobs);
|
|
413
|
+
await refresh();
|
|
414
|
+
if (!ctx.signal.aborted) msg.say(notes.join(' · '), notes.some((n) => n.startsWith('[失败]')));
|
|
415
|
+
} catch (err) {
|
|
416
|
+
if (!ctx.signal.aborted) msg.say(`挂载失败: ${errText(err)}`, true);
|
|
417
|
+
} finally {
|
|
418
|
+
lock.dispose();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 面板 `skin` —— 皮肤:她和玩家各穿哪一张。
|
|
3
|
+
*
|
|
4
|
+
* 离线服务器的玩家档案里没有材质这一格,所以这一屏做的事全在客户端一侧:选中的
|
|
5
|
+
* PNG 按账号名铺进两份游戏目录,由 CustomSkinLoader 在渲染时读。两份客户端各渲染
|
|
6
|
+
* 各的,所以两个账号的皮肤在两份目录里都要有——这也是"她的皮肤"与"玩家的皮肤"
|
|
7
|
+
* 在这里是同一屏里的两块,而不是分在两个面板的原因。
|
|
8
|
+
*
|
|
9
|
+
* 一个角色一颗按钮,点开就是文件选择器:图从哪儿来是人当场挑的,World 这边不设需要
|
|
10
|
+
* 人自己往里放文件的目录。
|
|
11
|
+
*
|
|
12
|
+
* 预览是把皮肤图上脸那 8x8 块(与帽子层)放大画进 canvas:选皮肤时要认的是哪张脸,
|
|
13
|
+
* 不是那张摊平的材质图。整张材质点开抽屉看。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
ConsolePanelContext,
|
|
18
|
+
ConsolePanel,
|
|
19
|
+
} from '../../../web/shared/client-panel.ts';
|
|
20
|
+
import { toDisposable } from '../../../web/shared/client-panel.ts';
|
|
21
|
+
import {
|
|
22
|
+
errText,
|
|
23
|
+
msgLine,
|
|
24
|
+
type MinecraftSkinRole,
|
|
25
|
+
type MinecraftSkinState,
|
|
26
|
+
} from './client.ts';
|
|
27
|
+
|
|
28
|
+
const DESC =
|
|
29
|
+
'离线服务器发不出皮肤材质,这里选的 PNG 由客户端侧的 CustomSkinLoader 按账号名'
|
|
30
|
+
+ '读——直播画面(摄像机窗口)与你自己那份客户端都看得到,别人的客户端看不到。'
|
|
31
|
+
+ '她和玩家各选各的。';
|
|
32
|
+
|
|
33
|
+
/** 两块的中文名与它们各自是谁 */
|
|
34
|
+
const ROLE_ZH: Record<MinecraftSkinRole['role'], { label: string; note: string }> = {
|
|
35
|
+
bot: { label: '她', note: '游戏内账号(worlds.minecraft.username)' },
|
|
36
|
+
player: { label: '玩家', note: '你自己进服那份客户端(worlds.minecraft.player.username)' },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const FACE_PX = 72;
|
|
40
|
+
|
|
41
|
+
export const skinPanel: ConsolePanel = {
|
|
42
|
+
mount(ctx: ConsolePanelContext) {
|
|
43
|
+
const { ui, root } = ctx;
|
|
44
|
+
|
|
45
|
+
const card = ui.sheet({ title: '皮肤', en: 'skins', desc: DESC });
|
|
46
|
+
const s = card.body;
|
|
47
|
+
root.appendChild(card.el);
|
|
48
|
+
|
|
49
|
+
const msg = msgLine(ctx);
|
|
50
|
+
s.appendChild(msg.el);
|
|
51
|
+
|
|
52
|
+
const roleBox = ui.h('div', 'mc-skins');
|
|
53
|
+
s.append(
|
|
54
|
+
ui.section('谁穿哪一张', '选中即铺进两份游戏目录;客户端正跑着的话,要它重进一次服务器才换得过来'),
|
|
55
|
+
roleBox,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const premise = ui.h('div');
|
|
59
|
+
s.append(ui.section('前提', '皮肤由客户端 mod 读;它不在游戏目录里就一切照旧'), premise);
|
|
60
|
+
|
|
61
|
+
/** 一个角色一个隐藏的 file input:点的是卡里那颗钮 */
|
|
62
|
+
const pickers = new Map<MinecraftSkinRole['role'], HTMLInputElement>();
|
|
63
|
+
for (const role of ['bot', 'player'] as const) {
|
|
64
|
+
const input = ui.h('input', 'mc-hidden');
|
|
65
|
+
input.type = 'file';
|
|
66
|
+
input.accept = 'image/png';
|
|
67
|
+
input.addEventListener('change', () => {
|
|
68
|
+
const file = input.files?.[0];
|
|
69
|
+
input.value = '';
|
|
70
|
+
if (!file) return;
|
|
71
|
+
void act(async () => {
|
|
72
|
+
const base64 = await toBase64(file);
|
|
73
|
+
return ctx.invoke<MinecraftSkinState>('set', [role, base64]);
|
|
74
|
+
}, `读取 ${file.name}…`);
|
|
75
|
+
}, { signal: ctx.signal });
|
|
76
|
+
pickers.set(role, input);
|
|
77
|
+
s.appendChild(input);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// -----------------------------------------------------------------------
|
|
81
|
+
// 材质:每换一张重取一次,面板活着期间同一张只取一次
|
|
82
|
+
// -----------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
const urls = new Map<string, Promise<string>>();
|
|
85
|
+
|
|
86
|
+
/** key 带上选中时刻:换了图就是另一个 key,不会拿旧的 ObjectURL 画新皮肤 */
|
|
87
|
+
function skinUrl(role: MinecraftSkinRole['role'], key: string): Promise<string> {
|
|
88
|
+
let pending = urls.get(key);
|
|
89
|
+
if (!pending) {
|
|
90
|
+
pending = ctx.invokeBinary('file', [role]).then((blob) => {
|
|
91
|
+
const url = URL.createObjectURL(blob);
|
|
92
|
+
ctx.own(toDisposable(() => URL.revokeObjectURL(url)));
|
|
93
|
+
return url;
|
|
94
|
+
});
|
|
95
|
+
urls.set(key, pending);
|
|
96
|
+
}
|
|
97
|
+
return pending;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 脸那 8x8 块放大,帽子层叠上去;点一下进抽屉看整张材质 */
|
|
101
|
+
function face(role: MinecraftSkinRole['role'], key: string): HTMLCanvasElement {
|
|
102
|
+
const cv = ui.h('canvas', 'mc-face');
|
|
103
|
+
cv.width = FACE_PX;
|
|
104
|
+
cv.height = FACE_PX;
|
|
105
|
+
void skinUrl(role, key).then((url) => {
|
|
106
|
+
if (ctx.signal.aborted) return;
|
|
107
|
+
const img = ui.h('img');
|
|
108
|
+
img.addEventListener('load', () => {
|
|
109
|
+
const cx = cv.getContext('2d');
|
|
110
|
+
if (!cx) return;
|
|
111
|
+
cx.imageSmoothingEnabled = false;
|
|
112
|
+
cx.drawImage(img, 8, 8, 8, 8, 0, 0, FACE_PX, FACE_PX);
|
|
113
|
+
cx.drawImage(img, 40, 8, 8, 8, 0, 0, FACE_PX, FACE_PX);
|
|
114
|
+
}, { signal: ctx.signal });
|
|
115
|
+
img.src = url;
|
|
116
|
+
cv.addEventListener('click', () => {
|
|
117
|
+
const big = ui.h('img', 'mc-shot-big');
|
|
118
|
+
big.src = url;
|
|
119
|
+
big.alt = `${ROLE_ZH[role].label}的皮肤`;
|
|
120
|
+
ui.drawer(`${ROLE_ZH[role].label}的皮肤`, big);
|
|
121
|
+
}, { signal: ctx.signal });
|
|
122
|
+
}, () => { /* 取不到就留一块空 canvas */ });
|
|
123
|
+
return cv;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// -----------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
let cur: MinecraftSkinState | null = null;
|
|
129
|
+
|
|
130
|
+
function roleCard(role: MinecraftSkinRole, st: MinecraftSkinState): HTMLDivElement {
|
|
131
|
+
const zh = ROLE_ZH[role.role];
|
|
132
|
+
const el = ui.h('div', 'mc-skin');
|
|
133
|
+
if (role.skin) el.classList.add('cur');
|
|
134
|
+
|
|
135
|
+
const head = ui.h('div', 'mc-skinhead');
|
|
136
|
+
head.appendChild(role.skin
|
|
137
|
+
? face(role.role, `${role.role}:${role.skin.at}`)
|
|
138
|
+
: ui.h('div', 'mc-face mc-facenone'));
|
|
139
|
+
|
|
140
|
+
const meta = ui.h('div');
|
|
141
|
+
const title = ui.h('div', 'mc-worldname', zh.label);
|
|
142
|
+
title.title = zh.note;
|
|
143
|
+
meta.appendChild(title);
|
|
144
|
+
meta.appendChild(ui.h('div', 'mc-worldmeta', role.username || '(没配账号名)'));
|
|
145
|
+
if (role.skin) {
|
|
146
|
+
const shared = st.dirs.camera === st.dirs.player;
|
|
147
|
+
const laid = role.installed.camera && (shared || role.installed.player);
|
|
148
|
+
const line = ui.h('div', 'mc-worldmeta');
|
|
149
|
+
line.append(
|
|
150
|
+
ui.h('span', null, `${role.skin.width}×${role.skin.height} · ${ui.fmt.bytes(role.skin.bytes)}`),
|
|
151
|
+
ui.h('span', null, `选于 ${ui.fmt.clock(role.skin.at)}`),
|
|
152
|
+
laid ? ui.pill('已铺好', 'on') : ui.pill('未铺到位', 'off'),
|
|
153
|
+
);
|
|
154
|
+
meta.appendChild(line);
|
|
155
|
+
} else {
|
|
156
|
+
meta.appendChild(ui.h('div', 'mc-worldmeta', '没选 · 原版随机皮肤'));
|
|
157
|
+
}
|
|
158
|
+
head.appendChild(meta);
|
|
159
|
+
el.appendChild(head);
|
|
160
|
+
|
|
161
|
+
const acts = ui.actions();
|
|
162
|
+
const btnPick = ui.button(role.skin ? '换一张…' : '选一张…', {
|
|
163
|
+
size: 'sm',
|
|
164
|
+
variant: role.skin ? 'plain' : 'primary',
|
|
165
|
+
});
|
|
166
|
+
btnPick.addEventListener(
|
|
167
|
+
'click',
|
|
168
|
+
() => pickers.get(role.role)?.click(),
|
|
169
|
+
{ signal: ctx.signal },
|
|
170
|
+
);
|
|
171
|
+
acts.append(btnPick, ui.h('span', 'grow'));
|
|
172
|
+
if (role.skin) {
|
|
173
|
+
const btnClear = ui.button('撤回', { size: 'sm', variant: 'danger' });
|
|
174
|
+
btnClear.addEventListener('click', () => {
|
|
175
|
+
void (async () => {
|
|
176
|
+
const ok = await ui.confirm({
|
|
177
|
+
title: `撤回${zh.label}的皮肤?`,
|
|
178
|
+
body: '那个账号回到原版随机皮肤。',
|
|
179
|
+
danger: true,
|
|
180
|
+
});
|
|
181
|
+
if (!ok) return;
|
|
182
|
+
await act(() => ctx.invoke<MinecraftSkinState>('clear', [role.role]), '撤回中…');
|
|
183
|
+
})();
|
|
184
|
+
}, { signal: ctx.signal });
|
|
185
|
+
acts.appendChild(btnClear);
|
|
186
|
+
}
|
|
187
|
+
el.appendChild(acts);
|
|
188
|
+
return el;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function renderPremise(st: MinecraftSkinState): void {
|
|
192
|
+
const shared = st.dirs.camera === st.dirs.player;
|
|
193
|
+
const modPill = (ok: boolean): HTMLSpanElement =>
|
|
194
|
+
ok ? ui.pill('已装', 'on') : ui.pill('没装 · 皮肤不会生效', 'off');
|
|
195
|
+
const rows = [
|
|
196
|
+
{ k: shared ? '客户端目录' : '摄像机目录', v: ui.h('span', null, st.dirs.camera) },
|
|
197
|
+
{ k: 'CustomSkinLoader', v: modPill(st.mod.camera) },
|
|
198
|
+
];
|
|
199
|
+
if (!shared) {
|
|
200
|
+
rows.push(
|
|
201
|
+
{ k: '玩家目录', v: ui.h('span', null, st.dirs.player) },
|
|
202
|
+
{ k: 'CustomSkinLoader(玩家)', v: modPill(st.mod.player) },
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
premise.replaceChildren(ui.kv(rows));
|
|
206
|
+
if (!st.mod.camera || (!shared && !st.mod.player)) {
|
|
207
|
+
premise.appendChild(ui.h(
|
|
208
|
+
'div',
|
|
209
|
+
'pagedesc',
|
|
210
|
+
'把 CustomSkinLoader 的 jar 放进该目录的 mods/ 再启动客户端。它是纯客户端 mod,'
|
|
211
|
+
+ '服务器不用装,也不影响 mineflayer 那条连接。',
|
|
212
|
+
));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function render(st: MinecraftSkinState | null): void {
|
|
217
|
+
if (ctx.signal.aborted) return;
|
|
218
|
+
cur = st;
|
|
219
|
+
if (!st) {
|
|
220
|
+
msg.say('皮肤设置不可用', true);
|
|
221
|
+
roleBox.replaceChildren(ui.placeholder('读不到皮肤设置'));
|
|
222
|
+
premise.replaceChildren();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
roleBox.replaceChildren(...st.roles.map((role) => roleCard(role, st)));
|
|
226
|
+
renderPremise(st);
|
|
227
|
+
msg.say(st.detail ?? '');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const refresh = (): Promise<void> =>
|
|
231
|
+
ctx.invoke<MinecraftSkinState>('state').then(render, () => render(null));
|
|
232
|
+
|
|
233
|
+
/** 一次动作:调服务端 → 拿回来的新状态重画。失败时重画旧状态,错误话留在最后一行 */
|
|
234
|
+
async function act(fn: () => Promise<MinecraftSkinState>, working: string): Promise<void> {
|
|
235
|
+
msg.say(working);
|
|
236
|
+
try {
|
|
237
|
+
render(await fn());
|
|
238
|
+
} catch (err) {
|
|
239
|
+
if (ctx.signal.aborted) return;
|
|
240
|
+
if (cur) render(cur);
|
|
241
|
+
msg.say(`操作失败: ${errText(err)}`, true);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** 原样上传:皮肤材质一个像素都不能重编码,浏览器这一侧只做搬运 */
|
|
246
|
+
function toBase64(file: File): Promise<string> {
|
|
247
|
+
return file.arrayBuffer().then((buf) => {
|
|
248
|
+
const bytes = new Uint8Array(buf);
|
|
249
|
+
let bin = '';
|
|
250
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
251
|
+
return btoa(bin);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
void refresh();
|
|
256
|
+
},
|
|
257
|
+
};
|