cortico-world-wardrobe-studio 0.3.0 → 0.4.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/CHANGELOG.md +9 -0
- package/README.en.md +5 -0
- package/README.md +119 -23
- package/README_zh.md +122 -21
- package/docs/lifecycle.md +4 -2
- package/docs/native-integration-prototype.md +50 -0
- package/docs/native-integration.md +53 -0
- package/docs/native-integration.zh.md +57 -0
- package/docs/verification.md +7 -1
- package/package.json +3 -2
- package/src/index.js +15 -9
- package/src/native-adapter.js +56 -0
- package/src/native-feed.js +32 -0
- package/src/native-restore.js +9 -0
- package/web/native-client.js +22 -0
- package/web/native-controls.js +43 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
- Add an opt-in native wardrobe compatibility adapter for the checksum-verified desktop-pet 0.2.0 page: outfits followed by outfit-specific controls.
|
|
6
|
+
- Keep standard native appearance persistence, Coo/unconfigured figures, preview and import/removal flows.
|
|
7
|
+
- Back up before patching; restore on normal stop, recover interrupted installation and refuse overwriting external changes.
|
|
8
|
+
- Use an origin/token-checked, read-only loopback feed without weakening CSP or modifying figure resources.
|
|
9
|
+
- Document supported versions, explicit configuration, uninstall/recovery and the distinction from an official host API.
|
|
10
|
+
- Refresh bilingual project documentation and make the GitHub README English by default.
|
|
11
|
+
|
|
3
12
|
## 0.3.0
|
|
4
13
|
|
|
5
14
|
- Default to Chinese with a shared, persistent Chinese/English panel selector; preserve pending choices and JSON drafts when switching.
|
package/README.en.md
ADDED
package/README.md
CHANGED
|
@@ -1,46 +1,142 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<img src="assets/icon.svg" width="72" height="72" alt="Wardrobe Studio">
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
# Wardrobe Studio
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**Choose an outfit. Configure what belongs to it.**
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Outfit-specific controls and a continuous-animation authoring guide for Coopanion.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
[](https://www.npmjs.com/package/cortico-world-wardrobe-studio)
|
|
12
|
+
[](https://github.com/FelixHenrikChristian/cortico-world-wardrobe-studio/actions/workflows/check.yml)
|
|
13
|
+
[](LICENSE)
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
**English** · [简体中文](README_zh.md)
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
2. Keep the desktop-pet World available. The default service origin is `http://127.0.0.1:7797`; change it in the extension configuration and restart this World if necessary.
|
|
17
|
-
3. Read Format reference in the import area, download the fictional example/JSON Schema, inspect installed IDs and Check configuration before importing. Then choose an outfit/options and Apply. See the [profile format](docs/profile-format.md) and the fictional example.
|
|
18
|
-
4. Before uninstalling, use Restore previous appearance, verify success, disable the World and uninstall it. Normal stopping also attempts restoration. External appearance changes are preserved. See [lifecycle and recovery](docs/lifecycle.md).
|
|
17
|
+
[Get started](#get-started) · [Create a character](docs/agent-workflow.md) · [Profile format](docs/profile-format.md) · [Downloads](https://github.com/FelixHenrikChristian/cortico-world-wardrobe-studio/releases)
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
</div>
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
---
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
## One outfit, its own controls
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
A character's outfits do not need to share every option. Wardrobe Studio groups existing complete figure schemes and displays only the controls that belong to the selected outfit.
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
| You want to… | Wardrobe Studio provides… |
|
|
28
|
+
| :--- | :--- |
|
|
29
|
+
| Organize a wardrobe | Outfit-specific options; unavailable combinations disabled |
|
|
30
|
+
| Choose a combination | Standard appearance persistence and local thumbnails |
|
|
31
|
+
| Use the native wardrobe | Optional, reversible compatibility adapter in 0.4 |
|
|
32
|
+
| Create a character | Reference prompts, continuous video, playback gates and temporal matting guidance |
|
|
33
|
+
| Write an import profile | Field tables, fictional examples, JSON Schema and installed-ID checks |
|
|
34
|
+
|
|
35
|
+
> [!IMPORTANT]
|
|
36
|
+
> Native integration is an **experimental opt-in compatibility patch**, not an official World slot. It temporarily patches the checksum-verified desktop-pet page and restores it on normal stop. Unsupported versions are refused. [Supported host, setup and recovery →](docs/native-integration.md)
|
|
37
|
+
|
|
38
|
+
<details>
|
|
39
|
+
<summary><strong>Separate-panel preview · entirely fictional artwork</strong></summary>
|
|
40
|
+
<br>
|
|
41
|
+
<div align="center"><img src="assets/wardrobe.png" width="500" alt="A wholly fictional Moss Fox wardrobe"></div>
|
|
42
|
+
|
|
43
|
+
Moss Fox has cape/footwear options for trail outfits, hood/colour for rain outfits, and a visor for stargazer outfits. This screenshot illustrates the separate panel, not native compatibility mode.
|
|
44
|
+
|
|
45
|
+
</details>
|
|
46
|
+
|
|
47
|
+
## Get started
|
|
48
|
+
|
|
49
|
+
### 1 · Install and enable
|
|
50
|
+
|
|
51
|
+
Find **Wardrobe Studio** / `cortico-world-wardrobe-studio` in Coopanion's World extension market. Restart Core when requested and enable it. Alternatively, download a [GitHub release](https://github.com/FelixHenrikChristian/cortico-world-wardrobe-studio/releases), extract it and select the folder containing `package.json` in the manual installer.
|
|
52
|
+
|
|
53
|
+
### 2 · Prepare figure resources
|
|
54
|
+
|
|
55
|
+
Keep the desktop-pet World running and install/verify your figure pack first. The default service is `http://127.0.0.1:7797`; change it in the extension configuration if necessary, then restart this World.
|
|
56
|
+
|
|
57
|
+
**A wardrobe JSON groups existing resources; it contains no images or videos.** Every mapped combination must already be playable.
|
|
58
|
+
|
|
59
|
+
### 3 · Check and import
|
|
60
|
+
|
|
61
|
+
Open **Wardrobe Studio → Wardrobe → Import profile**. Inspect installed IDs, download the fictional example or JSON Schema, fill in your figure/schemes, run **Check configuration**, then import. Choose an outfit/options and **Apply**.
|
|
62
|
+
|
|
63
|
+
The extension UI defaults to Chinese. Its **中文 / English** selector is persistent and independent of the host language; imported names remain your own text.
|
|
64
|
+
|
|
65
|
+
[Complete profile format and example →](docs/profile-format.md)
|
|
66
|
+
|
|
67
|
+
### 4 · Optional native integration
|
|
68
|
+
|
|
69
|
+
Configure the installed desktop-pet `web` directory, enable **Native wardrobe compatibility patch**, and restart this World. Reopen the native wardrobe page. Configured figures show outfit/part rows; Coo and unconfigured figures retain their controls.
|
|
70
|
+
|
|
71
|
+
This mode is off by default. Read the [compatibility and rollback guide](docs/native-integration.md) before enabling it.
|
|
29
72
|
|
|
30
|
-
|
|
73
|
+
### 5 · Disable or uninstall
|
|
31
74
|
|
|
32
|
-
|
|
75
|
+
For changes made in the separate panel, use **Restore previous appearance** first. Disable the World and verify any native patch was restored before uninstalling. Native-page selections are ordinary host selections and remain valid after removal.
|
|
33
76
|
|
|
34
|
-
|
|
77
|
+
[Lifecycle and recovery →](docs/lifecycle.md)
|
|
35
78
|
|
|
36
|
-
##
|
|
79
|
+
## Create your own character
|
|
80
|
+
|
|
81
|
+
The guide panel, agent tool and downloadable manuals share the same source documents.
|
|
82
|
+
|
|
83
|
+
| Stage | Deliverable and acceptance |
|
|
84
|
+
| :--- | :--- |
|
|
85
|
+
| ① Plan | Identity, wardrobe/action matrix and reversible environment record |
|
|
86
|
+
| ② Reference | Fully dressed reference for each supported combination |
|
|
87
|
+
| ③ Continuous video | Dressed reference plus motion guidance; validate one short action first |
|
|
88
|
+
| ④ Raw-video review | Complete normal-speed, half-speed and loop playback |
|
|
89
|
+
| ⑤ Temporal matting | Correct masks, propagate over time and review on contrasting backgrounds |
|
|
90
|
+
| ⑥ Figure package | Coordinates, movement, transitions and actual expression coverage |
|
|
91
|
+
| ⑦ Wardrobe profile | Map installed complete schemes, validate and import |
|
|
92
|
+
|
|
93
|
+
[Read the detailed workflow →](docs/agent-workflow.md)
|
|
94
|
+
|
|
95
|
+
## Documentation
|
|
96
|
+
|
|
97
|
+
| Document | What it covers |
|
|
98
|
+
| :--- | :--- |
|
|
99
|
+
| [Authoring workflow](docs/agent-workflow.md) | Inputs, outputs, prompts, video/alpha requirements and figure interface |
|
|
100
|
+
| [Profile format](docs/profile-format.md) | Required fields, limits, full example and missing combinations |
|
|
101
|
+
| [Native integration](docs/native-integration.md) | Opt-in setup, exact compatibility, file changes and recovery |
|
|
102
|
+
| [Troubleshooting](docs/troubleshooting.md) | Stale resource URLs, connections and runtime resources |
|
|
103
|
+
| [Verification](docs/verification.md) | Checks performed and compatibility boundaries |
|
|
104
|
+
| [Changelog](CHANGELOG.md) | Version history |
|
|
105
|
+
|
|
106
|
+
## Data and compatibility
|
|
107
|
+
|
|
108
|
+
- Profiles, language and recovery records stay in this extension's local namespace. No conversation reading or internet requests.
|
|
109
|
+
- Figure manifests and animation assets are never modified. The optional native adapter temporarily changes only its documented page files.
|
|
110
|
+
- Native integration requires the exact reviewed desktop-pet page. Host updates can disable it; the independent panel remains available.
|
|
111
|
+
- World API 6, figure manifest/API 2, Node.js ≥22; no runtime npm dependencies or model downloads.
|
|
112
|
+
- Guidance provides instructions, not automatic media generation or missing animation resources.
|
|
113
|
+
|
|
114
|
+
<details>
|
|
115
|
+
<summary><strong>Development and agent tools</strong></summary>
|
|
37
116
|
|
|
38
117
|
```sh
|
|
39
|
-
npm test
|
|
40
118
|
npm run check
|
|
119
|
+
npm test
|
|
41
120
|
npm run preview
|
|
42
121
|
```
|
|
43
122
|
|
|
44
|
-
The preview uses
|
|
123
|
+
The preview uses isolated temporary data and fictional artwork. Tests exercise recovery and origin restrictions against disposable fixtures.
|
|
124
|
+
|
|
125
|
+
| Tool | Purpose |
|
|
126
|
+
| :--- | :--- |
|
|
127
|
+
| `wardrobe_list` | Inspect local profiles and current appearance |
|
|
128
|
+
| `wardrobe_choose` | Apply an existing valid combination |
|
|
129
|
+
| `wardrobe_restore` | Restore a separately leased appearance |
|
|
130
|
+
| `wardrobe_authoring_guide` | Read workflow, format and troubleshooting guidance |
|
|
131
|
+
|
|
132
|
+
Public examples are entirely fictional. Keep personal figures, profiles and production records outside this repository.
|
|
133
|
+
|
|
134
|
+
</details>
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
<div align="center">
|
|
139
|
+
|
|
140
|
+
[MIT License](LICENSE) · Independent extension · Not an official Coopanion / Pal AI Lab component
|
|
45
141
|
|
|
46
|
-
|
|
142
|
+
</div>
|
package/README_zh.md
CHANGED
|
@@ -1,37 +1,121 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<img src="assets/icon.svg" width="72" height="72" alt="装扮工作室">
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
# 装扮工作室
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**先选服装,再选搭配。**
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
为 Coopanion 整理角色衣橱,提供从参考图到连续动画的制作指南。
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
[](https://www.npmjs.com/package/cortico-world-wardrobe-studio)
|
|
12
|
+
[](https://github.com/FelixHenrikChristian/cortico-world-wardrobe-studio/actions/workflows/check.yml)
|
|
13
|
+
[](LICENSE)
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
2. 保持桌宠 World 可用。默认地址为 `http://127.0.0.1:7797`;端口不同可在扩展配置中修改,之后重启此 World。
|
|
15
|
-
3. 打开扩展的“衣橱”面板,在导入区查看“格式说明”,下载虚构示例/JSON Schema,查看本机 ID,先“校验配置”再导入属于本机角色的 JSON 分组配置,选择服装和搭配,点击“应用装扮”。[配置格式](docs/profile-format.zh.md)包含完整虚构示例。
|
|
16
|
-
4. 卸载前点击“恢复原外观”,确认成功后停用并卸载扩展。正常停用也会尝试恢复;在原生页另做的选择会被保留。[生命周期和异常恢复](docs/lifecycle.md)说明边界。
|
|
15
|
+
**简体中文** · [English](README.md)
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
[开始使用](#开始使用) · [制作自己的角色](docs/agent-workflow.zh.md) · [JSON 格式](docs/profile-format.zh.md) · [下载版本](https://github.com/FelixHenrikChristian/cortico-world-wardrobe-studio/releases)
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
</div>
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
---
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
## 一套服装,一组自己的选项
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
同一角色的不同服装不必共用所有配置项。装扮工作室把角色包已有的完整外观重新分组,只显示当前服装支持的搭配。
|
|
26
|
+
|
|
27
|
+
| 你要做的事 | 扩展提供什么 |
|
|
28
|
+
| :--- | :--- |
|
|
29
|
+
| 整理衣橱 | 按服装显示独立选项,禁用没有完整资源的组合 |
|
|
30
|
+
| 切换搭配 | 使用桌宠标准外观接口,读取本机角色缩略图 |
|
|
31
|
+
| 制作角色 | 参考图提示词、连续视频、播放验收、连续抠像、形象包接入指南 |
|
|
32
|
+
| 编写配置 | 完整字段表、虚构示例、JSON Schema、本机 ID 查询和只读校验 |
|
|
33
|
+
| 切换语言 | 默认中文,可选 English,记住选择并保留编辑草稿 |
|
|
34
|
+
|
|
35
|
+
> [!IMPORTANT]
|
|
36
|
+
> **0.4 新增可选的原生装扮页兼容补丁,默认关闭。** 这不是官方 World 插槽,会临时修改经过校验的原生页面,正常停用时恢复。不兼容版本会拒绝修改。[支持版本、设置和回滚说明 →](docs/native-integration.zh.md)
|
|
37
|
+
|
|
38
|
+
<details>
|
|
39
|
+
<summary><strong>查看独立衣橱预览 · 完全虚构角色</strong></summary>
|
|
40
|
+
<br>
|
|
41
|
+
<div align="center"><img src="assets/wardrobe.png" width="500" alt="虚构苔星狐的独立衣橱面板"></div>
|
|
42
|
+
|
|
43
|
+
示例“苔星狐”的探险装有斗篷和足部装备,雨林装有雨帽与雨披颜色,观星装只有护目镜。公开素材仅包含虚构示例。
|
|
44
|
+
|
|
45
|
+
</details>
|
|
46
|
+
|
|
47
|
+
## 开始使用
|
|
48
|
+
|
|
49
|
+
### 1 · 安装并启用
|
|
50
|
+
|
|
51
|
+
在 Coopanion 的 **World 扩展市场**搜索 `装扮工作室` 或 `cortico-world-wardrobe-studio`,安装后按宿主提示重启 Core,再启用扩展。
|
|
52
|
+
|
|
53
|
+
也可从 [GitHub Releases](https://github.com/FelixHenrikChristian/cortico-world-wardrobe-studio/releases) 下载归档,解压后在手动安装中选择含 `package.json` 的目录。
|
|
54
|
+
|
|
55
|
+
### 2 · 准备已有的角色资源
|
|
56
|
+
|
|
57
|
+
保持桌宠 World 运行,并先安装、验收自己的形象包。默认本机服务地址为 `http://127.0.0.1:7797`;端口不同可在扩展配置中修改,之后重启此 World。
|
|
58
|
+
|
|
59
|
+
**衣橱 JSON 负责分组,不包含图片或视频。** 每个有效搭配需要先有能实际播放的角色资源。
|
|
60
|
+
|
|
61
|
+
### 3 · 校验并导入衣橱
|
|
62
|
+
|
|
63
|
+
打开 **装扮工作室 → 衣橱 → 导入衣橱配置**:
|
|
64
|
+
|
|
65
|
+
1. 查看本机装扮 ID,下载虚构示例或 JSON Schema。
|
|
66
|
+
2. 按自己的角色填写 `figure`、服装选项和已有 `scheme`。
|
|
67
|
+
3. 点击 **校验配置**,修复结构或 ID 报错后再导入。
|
|
68
|
+
4. 选择服装与搭配,点击 **应用装扮**。
|
|
69
|
+
|
|
70
|
+
[完整 JSON 字段与可复制示例 →](docs/profile-format.zh.md)
|
|
27
71
|
|
|
28
|
-
|
|
72
|
+
### 4 · 可选:在原生装扮页选择
|
|
29
73
|
|
|
30
|
-
|
|
74
|
+
导入配置后,在扩展配置填写已安装 desktop-pet 包的 `web` 绝对目录,开启「原生装扮页兼容补丁」,重启此 World 并重新打开原生装扮页。配置过的角色显示「服装 → 当前服装的部件」;其他角色与 Coo 保持原控件。启用前请阅读[兼容与回滚说明](docs/native-integration.zh.md)。
|
|
31
75
|
|
|
32
|
-
|
|
76
|
+
### 5 · 停用或卸载
|
|
33
77
|
|
|
34
|
-
|
|
78
|
+
独立面板应用的外观先点 **恢复原外观**并确认成功,再停用 World,确认原生页兼容补丁已恢复后卸载。正常停用也会尝试恢复仍由扩展接管的外观;原生页选择按普通宿主装扮保存,停用后保留。
|
|
79
|
+
|
|
80
|
+
[生命周期、异常恢复与边界 →](docs/lifecycle.md)
|
|
81
|
+
|
|
82
|
+
## 制作自己的角色
|
|
83
|
+
|
|
84
|
+
手册与扩展内“角色制作指导”使用同一份内容,可以下载完整 Markdown 版。
|
|
85
|
+
|
|
86
|
+
| 阶段 | 交付与检查 |
|
|
87
|
+
| :--- | :--- |
|
|
88
|
+
| ① 规划 | 角色设定、服装/动作清单、环境与回退记录 |
|
|
89
|
+
| ② 参考图 | 每个有效搭配的完整穿衣图、统一轮廓与画风 |
|
|
90
|
+
| ③ 连续视频 | 完整穿衣参考 + 动作驱动,先验证一个短动作 |
|
|
91
|
+
| ④ 原片验收 | 完整原速、半速及循环接缝检查 |
|
|
92
|
+
| ⑤ 连续抠像 | 首帧纠正、时序传播、多背景透明度播放检查 |
|
|
93
|
+
| ⑥ 形象包 | 坐标、移动、动作衔接及真实表情覆盖验收 |
|
|
94
|
+
| ⑦ 衣橱配置 | 映射已安装搭配,校验后导入 |
|
|
95
|
+
|
|
96
|
+
[阅读详细制作指南 →](docs/agent-workflow.zh.md)
|
|
97
|
+
|
|
98
|
+
## 文档导航
|
|
99
|
+
|
|
100
|
+
| 文档 | 内容 |
|
|
101
|
+
| :--- | :--- |
|
|
102
|
+
| [从零制作](docs/agent-workflow.zh.md) | 输入输出、提示词、视频与透明素材要求、形象接口骨架 |
|
|
103
|
+
| [衣橱 JSON](docs/profile-format.zh.md) | 必填字段、ID 规则、完整示例、缺失组合和常见报错 |
|
|
104
|
+
| [故障排查](docs/troubleshooting.zh.md) | 旧资源地址、连接与预览、配置通过但动画缺失 |
|
|
105
|
+
| [原生页兼容](docs/native-integration.zh.md) | 实验集成、精确版本限制、文件变更与卸载恢复 |
|
|
106
|
+
| [验证范围](docs/verification.md) | 实际检查过什么、兼容性边界 |
|
|
107
|
+
| [更新记录](CHANGELOG.md) | 已发布版本变更 |
|
|
108
|
+
|
|
109
|
+
## 数据与兼容
|
|
110
|
+
|
|
111
|
+
- **本机存储**:配置、语言偏好和外观恢复记录保存在扩展自己的目录。
|
|
112
|
+
- **资源保护**:角色 manifest 与动画素材始终不改写;原生页兼容选项开启后临时修改其文档列出的页面文件。
|
|
113
|
+
- **恢复边界**:强制结束进程、服务不可用或旧角色被删除时,可能需要下次启用后恢复;卸载前应确认结果。
|
|
114
|
+
- **运行要求**:World API 6、形象包 manifest/API 2、Node.js ≥22,零运行时 npm 依赖。
|
|
115
|
+
- **能力范围**:指导工具提供步骤,不自动安装模型、生成素材或补齐未制作的动作。Coo 自带配件继续使用原生页。
|
|
116
|
+
|
|
117
|
+
<details>
|
|
118
|
+
<summary><strong>开发与 agent 工具</strong></summary>
|
|
35
119
|
|
|
36
120
|
```sh
|
|
37
121
|
npm test
|
|
@@ -39,6 +123,23 @@ npm run check
|
|
|
39
123
|
npm run preview
|
|
40
124
|
```
|
|
41
125
|
|
|
42
|
-
|
|
126
|
+
开发预览使用独立临时数据与虚构角色。原生集成原型是另一个隔离服务,启动方式见其文档。
|
|
127
|
+
|
|
128
|
+
| 工具 | 作用 |
|
|
129
|
+
| :--- | :--- |
|
|
130
|
+
| `wardrobe_list` | 读取本地衣橱和当前外观 |
|
|
131
|
+
| `wardrobe_choose` | 切换已有合法搭配 |
|
|
132
|
+
| `wardrobe_restore` | 恢复扩展接管前外观 |
|
|
133
|
+
| `wardrobe_authoring_guide` | 读取制作流程、格式与排查说明 |
|
|
134
|
+
|
|
135
|
+
公共源码只使用完全虚构的示例。个人角色、衣橱配置和生产记录应放在仓库之外。
|
|
136
|
+
|
|
137
|
+
</details>
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
<div align="center">
|
|
142
|
+
|
|
143
|
+
[MIT License](LICENSE) · 第三方独立扩展 · 非 Coopanion / Pal AI Lab 官方组件
|
|
43
144
|
|
|
44
|
-
|
|
145
|
+
</div>
|
package/docs/lifecycle.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Appearance ownership and lifecycle · `src/bridge.js`, `src/store.js`, `src/index.js`
|
|
2
2
|
|
|
3
|
-
Wardrobe Studio reads a local desktop-pet HTTP service at a configurable loopback origin. Runtime requests use `/api/figures`, `/api/state`, `/api/skin` and read-only thumbnail assets declared by installed packs. Thumbnail URLs must remain within the same loopback origin and pack asset directory; redirects are rejected, image MIME types are restricted and responses are limited to 2 MiB. The state response is reduced to appearance data before it is returned by the extension. The extension
|
|
3
|
+
Wardrobe Studio reads a local desktop-pet HTTP service at a configurable loopback origin. Runtime requests use `/api/figures`, `/api/state`, `/api/skin` and read-only thumbnail assets declared by installed packs. Thumbnail URLs must remain within the same loopback origin and pack asset directory; redirects are rejected, image MIME types are restricted and responses are limited to 2 MiB. The state response is reduced to appearance data before it is returned by the extension. The extension makes no internet request. When explicitly enabled, the [native compatibility adapter](native-integration.md) opens a read-only, origin/token-checked loopback WebSocket feed; otherwise no listening production service is opened.
|
|
4
4
|
|
|
5
5
|
## What changes
|
|
6
6
|
|
|
7
|
-
Importing a profile writes `wardrobe-studio/profiles.json` under the extension's supplied data directory. Applying a combination uses the desktop pet's standard appearance API; this changes its selected figure/scheme as an ordinary wardrobe selection does.
|
|
7
|
+
Importing a profile writes `wardrobe-studio/profiles.json` under the extension's supplied data directory. Applying a combination uses the desktop pet's standard appearance API; this changes its selected figure/scheme as an ordinary wardrobe selection does. Figure manifests and animation assets are never edited. The optional native adapter writes a backup and temporarily patches only the documented native page files; it is not an official host hook.
|
|
8
8
|
|
|
9
9
|
The extension defaults to Chinese independently of the host language. Its Chinese/English selector stores only `{language}` in its own `wardrobe-studio/preferences.json`, shared by both panels and retained across restart. It does not change the host language, profiles or appearance. Imported names remain user-provided text. Switching panel language preserves the pending outfit selection and JSON draft.
|
|
10
10
|
|
|
@@ -16,6 +16,8 @@ The Restore button and World `stop()` compare the current figure/scheme with the
|
|
|
16
16
|
|
|
17
17
|
The standard uninstall sequence is: Restore, disable the World, then uninstall it from World extensions. Normal stopping also attempts restoration. The resulting figure remains a standard installed pack with no dependency on this extension. Cortico may retain the extension's own configuration and local profiles; they do not alter original figure data.
|
|
18
18
|
|
|
19
|
+
In native compatibility mode, ordinary native selections are not leased by this extension: they remain selected after stopping. The adapter closes its feed and conditionally restores its owned page files on stop. Verify rollback before uninstalling. Forced termination, external page edits and host upgrades require the recovery procedure in the native integration guide; unknown files are never overwritten automatically.
|
|
20
|
+
|
|
19
21
|
If the process is forcibly killed, the desktop service is unavailable during shutdown, or the previous figure has been removed, automatic restoration cannot be guaranteed. The lease remains for recovery on the next activation, and restoration problems are reported in the panel and shutdown verification. Restore while the desktop service is available before uninstalling. Retained schemes are existing valid schemes, so removing the extension does not make the original figure unloadable.
|
|
20
22
|
|
|
21
23
|
The desktop API has no compare-and-swap contract. Ownership is checked immediately before restoration, but simultaneous writes from another client cannot be made atomic by an external extension. Avoid changing the same appearance from two panels at the same instant.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# 原生装扮页集成 · 隔离原型
|
|
2
|
+
|
|
3
|
+
## 状态
|
|
4
|
+
|
|
5
|
+
此原型验证“原生装扮页 → 服装 → 当前服装的部件选项”的交互。**这是 0.3 开发阶段的历史原型;0.4 的可选兼容实现见[正式设置说明](native-integration.zh.md)。** 此原型本身没有安装到真实 Coopanion,也没有修改其程序或角色文件。
|
|
6
|
+
|
|
7
|
+
原生页目前直接遍历角色包的 axes/options,没有供外部 World 接管这一块的正式接口。原型在独立 HTTP 服务中只读加载本机原生页面,对发送给浏览器的 dress.js 副本增加一个明确的渲染钩子;磁盘上的源文件保持不变。没有把未知脚本注入正在使用的窗口。
|
|
8
|
+
|
|
9
|
+
## 验证的交互
|
|
10
|
+
|
|
11
|
+
| 场景 | 预期行为 |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| 已启用,角色有合法衣橱配置 | 隐藏原配置轴,显示服装与该服装自己的部件 |
|
|
14
|
+
| 没有资源的组合 | 对应部件选项禁用 |
|
|
15
|
+
| 换到无配置角色或 Coo | 保留其原生控件 |
|
|
16
|
+
| 停用、模拟卸载或提供方离线 | 恢复原配置轴与完整搭配列表 |
|
|
17
|
+
| 选择已有搭配 | 原生预览切换,预览 iframe 保持存在 |
|
|
18
|
+
|
|
19
|
+
原型中的外观修改只写入该进程的内存。停用集成只撤回分组 UI;已经选中的合法 scheme 仍可由原生页显示。未来正式原生集成应将用户点击视为宿主的普通装扮选择,不依赖插件文件才能继续播放。独立衣橱面板已有的外观接管/恢复机制保持其原有定义。
|
|
20
|
+
|
|
21
|
+
## 运行
|
|
22
|
+
|
|
23
|
+
需要现有的 Node.js 与已安装 desktop-pet 包。脚本复用该包已有的 ws 模块,不下载依赖、模型或角色资源。
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
node prototypes/native-dress/server.mjs --web "<desktop-pet 包的 web 目录>" --port 65088
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
浏览器打开终端打印的回环地址。默认只使用 Coo 和本仓库原创虚构狐狸。狐狸只有用于界面验证的轻微待机运动,不是完整动画资源包,也不代表生成素材通过了验收。
|
|
30
|
+
|
|
31
|
+
页面上方可模拟启用、停用、卸载、重新注册及接口离线。原生安装/删除入口在隔离服务中不可用。
|
|
32
|
+
|
|
33
|
+
也可以明确指定自己的私有配置及本机角色资源目录:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
node prototypes/native-dress/server.mjs --web "<desktop-pet/web>" --profile "<私有 wardrobe.json>" --figure-root "<本机 figures 目录>" --seed "http://127.0.0.1:7797" --port 65089
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
此模式只在启动时 GET 读取本机形象列表和外观,之后使用隔离内存状态;角色文件只读、按需加载,不复制整套素材。个人配置、截图、日志和绝对路径不要提交到公开仓库。
|
|
40
|
+
|
|
41
|
+
## 原型边界与正式接入
|
|
42
|
+
|
|
43
|
+
本轮已手动检查虚构角色的服装/部件切换、原生预览、Coo 配件保留、停用、模拟卸载、重新注册和接口离线回退。另在隔离状态中检查了已有本机角色及不完整组合禁用;个人配置和该轮截图不在公开仓库中。安装目录的原生源文件哈希保持不变。
|
|
44
|
+
|
|
45
|
+
- 复制页中的渲染钩子不是现有 World API 的能力。正式接入需要 desktop-pet 或 Coopanion 增加受控接口。
|
|
46
|
+
- 建议宿主接收声明式衣橱配置,由宿主校验 figure/scheme 并绘制控件;提供方失效时立即回退原 UI。
|
|
47
|
+
- 资源选择继续由宿主外观接口保存,保留原生预览、其他角色与 Coo 配件。
|
|
48
|
+
- 提供方注册、撤销、冲突优先级和当前角色作用域需要正式定义;不靠全局 DOM 注入或修改角色 manifest 接管界面。
|
|
49
|
+
- 脚本检查原生源文件的三个明确锚点。源版本改变时会停止,需人工复核;这不是承诺所有宿主版本兼容的安装器。
|
|
50
|
+
- prototypes 目录不在 npm 发布文件清单中。这里的开发原型不会随着当前插件包自动执行。
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Native wardrobe compatibility · 0.4
|
|
2
|
+
|
|
3
|
+
This is an **experimental, explicit opt-in compatibility patch**, disabled by default. It is not an official Coopanion World slot. The separate wardrobe and authoring guide remain available without it.
|
|
4
|
+
|
|
5
|
+
## Behaviour
|
|
6
|
+
|
|
7
|
+
For a figure with an imported profile, the native wardrobe shows outfits followed by the selected outfit's own controls. Missing combinations are disabled. Unconfigured figures and Coo retain their native controls. Preview, import, removal and theme stay with the original page.
|
|
8
|
+
|
|
9
|
+
Native selections use the ordinary desktop-pet appearance API and remain selected when the extension is disabled. The separate panel's Apply button retains its own appearance lease/restoration behaviour. Do not operate both simultaneously.
|
|
10
|
+
|
|
11
|
+
## Supported host
|
|
12
|
+
|
|
13
|
+
Only the reviewed `cortico-world-desktop-pet@0.2.0` shipped with Coopanion 0.2.17, **and** this exact `web/dress.js` SHA-256, are supported:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
b594171051e90e4f368c635b4fd008405f6e41e6dfd3dbd907fdbffa767fbe9f
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Matching the version number alone is insufficient. Unknown checksums are refused without patching.
|
|
20
|
+
|
|
21
|
+
## Enable
|
|
22
|
+
|
|
23
|
+
1. Install 0.4.0, restart Core as required and retain/import your local wardrobe profile.
|
|
24
|
+
2. In Wardrobe Studio → Configuration, enter the absolute **web directory of the installed desktop-pet package**, not a figure directory. The extension does not scan the disk.
|
|
25
|
+
3. Enable **Native wardrobe compatibility patch**. The loopback port defaults to `17891`; choose another 1024–65535 port if occupied. Save and restart this World.
|
|
26
|
+
4. Check its native compatibility status lamp, then reopen the native wardrobe page to clear old module instances.
|
|
27
|
+
5. Check outfit/part changes, missing combinations and unchanged controls on other figures.
|
|
28
|
+
|
|
29
|
+
Before changing the directory or port, disable the World and verify rollback. Then edit configuration, reactivate and reopen the native page. Do not move or overwrite the application installation while the adapter runs.
|
|
30
|
+
|
|
31
|
+
## Files and connection
|
|
32
|
+
|
|
33
|
+
The extension writes a private write-ahead backup under its own `wardrobe-studio/native-adapter.json` before changing application files. Do not publish that record. Only three checked locations in `web/dress.js` and three `wardrobe-studio-*.js` helper modules are affected. No figure manifests, animation assets or other application files are modified.
|
|
34
|
+
|
|
35
|
+
A read-only WebSocket server binds only to `127.0.0.1`. It sends profile metadata and language, verifies the exact desktop-page Origin plus a random token, and provides no commands, filesystem or appearance-writing RPCs. All HTTP requests return 404. CSP is not weakened; no internet scripts, dependencies or models are downloaded. Disconnection restores native rows; an abnormal stale connection has a roughly 6.5-second watchdog.
|
|
36
|
+
|
|
37
|
+
## Disable, uninstall and recovery
|
|
38
|
+
|
|
39
|
+
Normal stopping closes the feed, restores the original page only when its checksum still matches the owned patch, and removes only matching owned helpers. Stopping is idempotent. Reopen the native page to confirm the original layout before uninstalling.
|
|
40
|
+
|
|
41
|
+
A forced process termination can leave the disk patch installed. Loaded pages fall back when the feed goes offline, and the next activation first recovers the previous patch. Disable normally and verify restoration before removing extension files.
|
|
42
|
+
|
|
43
|
+
If Core cannot start, quit Coopanion first and use your existing Node.js to run the recovery tool inside the installed extension package:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
node src/native-restore.js --data-dir "<World dataDir>" --web-dir "<desktop-pet/web>"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The data directory is the parent containing `wardrobe-studio`, not that subdirectory itself. The tool uses the private backup and refuses to overwrite externally changed pages or delete modified helpers. Preserve recovery records after a host upgrade and inspect the new official files before repairing; never overwrite an unknown new version with the old backup. Disable compatibility to continue using the separate panel.
|
|
50
|
+
|
|
51
|
+
## Verification boundary
|
|
52
|
+
|
|
53
|
+
Disposable tests cover rollback, repeated stopping, crash recovery, external changes, filename collisions and feed origin/token checks. Real-window acceptance is performed only with permission. Compatibility can break after a host update; this adapter does not claim a general official host hook.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# 原生装扮页兼容集成 · 0.4
|
|
2
|
+
|
|
3
|
+
这是**默认关闭、显式启用的实验兼容补丁**,不是 Coopanion 正式提供的 World 插槽。独立衣橱与制作指导仍可照常使用。
|
|
4
|
+
|
|
5
|
+
## 效果
|
|
6
|
+
|
|
7
|
+
在原生「装扮」页选择已导入衣橱配置的角色后,完整搭配列表变成「服装 → 当前服装的部件」。缺少资源的组合禁用。无配置角色和 Coo 使用原有控件,预览、导入、删除及主题由原页面负责。
|
|
8
|
+
|
|
9
|
+
在这里点击搭配是普通原生装扮选择,由桌宠标准接口保存;停用扩展只恢复原页面布局,保留这次选择。独立衣橱面板的「应用」仍使用其外观恢复机制,两者不要同时操作。
|
|
10
|
+
|
|
11
|
+
## 支持版本
|
|
12
|
+
|
|
13
|
+
仅支持已验收的 Coopanion 0.2.17 所带 `cortico-world-desktop-pet@0.2.0`,还必须匹配此 `web/dress.js` SHA-256:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
b594171051e90e4f368c635b4fd008405f6e41e6dfd3dbd907fdbffa767fbe9f
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
仅版本号相同不代表兼容。文件校验不符时不会修补,不会自动扩大支持范围。
|
|
20
|
+
|
|
21
|
+
## 启用
|
|
22
|
+
|
|
23
|
+
1. 安装 0.4.0,按宿主要求重启 Core,保留已经导入的衣橱配置。
|
|
24
|
+
2. 打开「装扮工作室 → 配置」,填写 **desktop-pet 包的 web 绝对目录**。这是已安装 `cortico-world-desktop-pet` 下的 `web` 文件夹,不是角色素材目录。扩展不会搜索磁盘。
|
|
25
|
+
3. 开启 **原生装扮页兼容补丁**;默认本机端口 `17891`。冲突时可指定另一个 1024–65535 端口。保存后重启此 World。
|
|
26
|
+
4. 检查「原生页兼容」状态灯。成功后重新打开原生「装扮」页面;已经打开的页面可能仍缓存旧模块。
|
|
27
|
+
5. 检查服装切换、该服装的部件、缺失组合禁用和其他角色的原有功能。
|
|
28
|
+
|
|
29
|
+
修改路径或端口前,先停用 World 并确认回滚成功;修改配置后再启用并重新打开原生页。不要在补丁仍运行时移动安装目录或覆盖安装程序。
|
|
30
|
+
|
|
31
|
+
## 写入与通信范围
|
|
32
|
+
|
|
33
|
+
- 先在扩展自己的 `wardrobe-studio/native-adapter.json` 记录原始字节、校验值和恢复信息,再修改页面。此文件是私有本机记录,不要上传到公开仓库。
|
|
34
|
+
- 只临时修改 `web/dress.js` 的三个已检查位置,另写入三个 `wardrobe-studio-*.js` 辅助模块。不会改其他程序文件、角色 manifest 或动画素材。
|
|
35
|
+
- 开启一个仅监听 `127.0.0.1` 的**只读** WebSocket 接口,向原生页发送衣橱分组和语言。不提供命令、文件读取或外观写入 RPC。检查精确桌宠 Origin 和随机令牌;HTTP 路径一律返回 404。
|
|
36
|
+
- 不放宽宿主 CSP,不载入外网脚本,不安装依赖或模型。
|
|
37
|
+
- 提供方断开时回到原控件;异常失联由约 6.5 秒看门狗兜底。恢复连接后可以重新分组。配置内容变化不需要改写页面。
|
|
38
|
+
|
|
39
|
+
## 停用、卸载与恢复
|
|
40
|
+
|
|
41
|
+
正常停用先关闭只读接口,再按校验值恢复原始 `dress.js`,移除仍匹配本扩展校验值的辅助文件。重复停用是幂等的。重新打开原生页可确认恢复。最后再卸载扩展。
|
|
42
|
+
|
|
43
|
+
若程序被强制结束,磁盘补丁可能暂留;接口离线后,已经加载的原生页会使用原控件。下次启用会先恢复上次补丁,再重新安装。卸载前应正常停用并确认恢复,不能仅删除扩展目录。
|
|
44
|
+
|
|
45
|
+
若无法启动 Core,可先退出 Coopanion,使用已有 Node.js 执行已安装扩展包内的恢复工具:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
node src/native-restore.js --data-dir "<World 的 dataDir>" --web-dir "<desktop-pet/web>"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`--data-dir` 是包含 `wardrobe-studio` 文件夹的父目录,不是该文件夹本身。工具只根据私有备份恢复已知原页,拒绝覆盖外部更新过的页面或删除外部修改的辅助文件。不要手动把未知文件当成原始备份。
|
|
52
|
+
|
|
53
|
+
宿主升级后若出现「外部修改/回滚拒绝」,先保留私有恢复记录并检查更新后的官方文件,再决定是否需要修复或重新安装宿主;不要盲目用旧文件覆盖新版本。关闭此兼容选项后仍可使用独立面板。
|
|
54
|
+
|
|
55
|
+
## 验证边界
|
|
56
|
+
|
|
57
|
+
发布前必须在临时目录测试备份、回滚、重复停止、崩溃后恢复、外部改动保护、文件名冲突、接口 Origin/令牌拒绝;再经本人授权检查真实原生窗口。无需生成或改写人物素材。此兼容实现会随宿主更新失效,不代表已存在通用的官方接口。
|
package/docs/verification.md
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
# Verification
|
|
1
|
+
# Verification
|
|
2
|
+
|
|
3
|
+
## Package 0.4.0
|
|
4
|
+
|
|
5
|
+
Twenty-six isolated tests pass locally, including seven native-adapter tests for exact anchors, unknown-source refusal, backup/rollback and repeated stop, interrupted installation recovery, external page changes, helper collisions and read-only feed origin/token checks. Disposable original fixtures substitute for the host source only inside the tests; the production adapter requires the documented exact checksum. The 0.3 checks below remain relevant to the separate panel. The optional adapter has a narrow compatibility boundary, not general host API support.
|
|
6
|
+
|
|
7
|
+
## Package 0.3.0
|
|
2
8
|
|
|
3
9
|
The initial local checks ran on Windows with Node.js 26.3.1. No runtime dependency installation was required. The packaged World was also installed and activated in a real Coopanion 0.2.17/Core deployment with the person's permission.
|
|
4
10
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cortico-world-wardrobe-studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Outfit-specific wardrobe controls and a continuous-animation authoring guide for Coopanion figure packs.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"README.md",
|
|
36
36
|
"README_zh.md",
|
|
37
37
|
"CHANGELOG.md",
|
|
38
|
-
"LICENSE"
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"README.en.md"
|
|
39
40
|
],
|
|
40
41
|
"scripts": {
|
|
41
42
|
"test": "node --test test/*.test.js",
|
package/src/index.js
CHANGED
|
@@ -3,9 +3,10 @@ import {PetBridge,AppearanceLease,localBase} from './bridge.js';
|
|
|
3
3
|
import {validateProfile,validateAgainstPack,outfitOf,selectVariant} from './profile.js';
|
|
4
4
|
import {authoringGuide} from './guide.js';
|
|
5
5
|
import {TOOL_SPECS,DESCRIBE} from './tools.js';
|
|
6
|
+
import {NativeAdapter} from './native-adapter.js';
|
|
6
7
|
import profileExample from '../examples/forest-fox.profile.json' with {type:'json'};
|
|
7
8
|
import profileSchema from '../docs/wardrobe-profile.schema.json' with {type:'json'};
|
|
8
|
-
export const DEFAULTS=Object.freeze({enabled:false,desktopPetUrl:'http://127.0.0.1:7797'});
|
|
9
|
+
export const DEFAULTS=Object.freeze({enabled:false,desktopPetUrl:'http://127.0.0.1:7797',nativeIntegration:false,nativeWebDir:'',nativePort:17891});
|
|
9
10
|
const appearance=s=>({figure:s.figure,scheme:s.scheme});
|
|
10
11
|
export class WardrobeWorld{
|
|
11
12
|
id='wardrobe-studio';
|
|
@@ -14,9 +15,14 @@ export class WardrobeWorld{
|
|
|
14
15
|
async start(host){
|
|
15
16
|
this.host=host;this.profiles=(await this.store.read('profiles.json',[])).map(validateProfile);this.language=(await this.store.read('preferences.json',{}))?.language==='en'?'en':'zh';this.started=true;
|
|
16
17
|
try{this.restoration=await this.lease.restore();}catch(e){this.error=String(e.message);}
|
|
18
|
+
if(this.ctx.cfg.nativeIntegration){
|
|
19
|
+
this.native=new NativeAdapter({webDir:this.ctx.cfg.nativeWebDir,store:this.store,origin:this.bridge.base,port:this.ctx.cfg.nativePort??17891,snapshot:()=>({contract:1,enabled:this.started,language:this.language,profiles:this.profiles})});
|
|
20
|
+
try{await this.native.start();}catch(e){this.native.status={state:'error',error:String(e.message)};this.host?.log?.warn?.('Native wardrobe compatibility adapter is off; see its panel status');}
|
|
21
|
+
}
|
|
17
22
|
}
|
|
18
23
|
async stop(){
|
|
19
24
|
this.started=false;
|
|
25
|
+
try{await this.native?.stop();}catch(e){this.native.status={state:'error',error:String(e.message)};this.host?.log?.warn?.('Native wardrobe rollback requires attention');}
|
|
20
26
|
await this.serial(async()=>{try{this.restoration=await this.lease.restore();this.error=null;}catch(e){this.error=String(e.message);this.host?.log?.warn?.('Wardrobe appearance restoration requires attention');}});
|
|
21
27
|
}
|
|
22
28
|
envPromptVars(){return null;}
|
|
@@ -32,12 +38,12 @@ export class WardrobeWorld{
|
|
|
32
38
|
async state(){
|
|
33
39
|
let packs=[],current=null,connectionError=null;
|
|
34
40
|
try{packs=(await this.bridge.packs()).filter(p=>p.id!=='coo').map(p=>({id:p.id,name:p.name,axes:p.axes,presets:p.presets??[]}));current=appearance(await this.bridge.skin());}catch(e){connectionError=String(e.message);}
|
|
35
|
-
return{profiles:this.profiles,packs,current,desktopPetUrl:this.bridge.base,connected:!connectionError,error:connectionError??this.error,lease:await this.store.read('lease.json',null),restoration:this.restoration};
|
|
41
|
+
return{profiles:this.profiles,packs,current,desktopPetUrl:this.bridge.base,connected:!connectionError,error:connectionError??this.error,lease:await this.store.read('lease.json',null),restoration:this.restoration,nativeIntegration:this.native?.status??{state:'off'}};
|
|
36
42
|
}
|
|
37
43
|
async importProfile(raw){return this.serial(async()=>{
|
|
38
44
|
if(!this.started)throw Error('Wardrobe World is not active');
|
|
39
45
|
const profile=validateProfile(raw);await this.bridge.validate(profile);
|
|
40
|
-
const next=this.profiles.filter(p=>p.id!==profile.id);next.push(profile);await this.store.write('profiles.json',next);this.profiles=next;return profile;
|
|
46
|
+
const next=this.profiles.filter(p=>p.id!==profile.id);next.push(profile);await this.store.write('profiles.json',next);this.profiles=next;this.native?.broadcast();return profile;
|
|
41
47
|
});}
|
|
42
48
|
async checkProfile(raw){
|
|
43
49
|
const p=validateProfile(raw);await this.bridge.validate(p);
|
|
@@ -46,11 +52,11 @@ export class WardrobeWorld{
|
|
|
46
52
|
async setLanguage(language){return this.serial(async()=>{
|
|
47
53
|
if(!this.started)throw Error('Wardrobe World is not active');
|
|
48
54
|
if(!['zh','en'].includes(language))throw Error('Unsupported wardrobe language');
|
|
49
|
-
await this.store.write('preferences.json',{language});this.language=language;return{language};
|
|
55
|
+
await this.store.write('preferences.json',{language});this.language=language;this.native?.broadcast();return{language};
|
|
50
56
|
});}
|
|
51
57
|
async removeProfile(id){return this.serial(async()=>{
|
|
52
58
|
if(!this.started)throw Error('Wardrobe World is not active');
|
|
53
|
-
const next=this.profiles.filter(p=>p.id!==id);await this.store.write('profiles.json',next);this.profiles=next;return{removed:id};
|
|
59
|
+
const next=this.profiles.filter(p=>p.id!==id);await this.store.write('profiles.json',next);this.profiles=next;this.native?.broadcast();return{removed:id};
|
|
54
60
|
});}
|
|
55
61
|
async choose({profile:id,outfit,pick={}}){return this.serial(async()=>{
|
|
56
62
|
if(!this.started)throw Error('Wardrobe World is not active');
|
|
@@ -69,7 +75,7 @@ export class WardrobeWorld{
|
|
|
69
75
|
webUi(language='zh'){
|
|
70
76
|
const zh=this.language!=='en';
|
|
71
77
|
return{label:zh?'装扮工作室':'Wardrobe Studio',panels:[{id:'wardrobe',title:zh?'衣橱':'Wardrobe'},{id:'guide',title:zh?'角色制作指导':'Authoring guide'}],
|
|
72
|
-
lamps:[{label:zh?'外观恢复':'Appearance restoration',state:this.error?'error':'online',hint:this.error??(zh?'角色包文件保持不变':'Figure pack files remain unchanged')}],
|
|
78
|
+
lamps:[{label:zh?'外观恢复':'Appearance restoration',state:this.error?'error':'online',hint:this.error??(zh?'角色包文件保持不变':'Figure pack files remain unchanged')},{label:zh?'原生页兼容':'Native compatibility',state:this.native?.status.state==='error'?'error':this.native?.status.state==='active'?'online':'offline',hint:this.native?.status.error??(this.native?.status.state==='active'?(zh?'已接入原生装扮页;停用时回滚补丁':'Native wardrobe connected; stopping restores the patch'):(zh?'未启用原生页兼容补丁':'Native compatibility patch is off'))}],
|
|
73
79
|
invoke:async(panel,method,args)=>{
|
|
74
80
|
if((panel==='guide'||panel==='wardrobe')&&method==='locale')return{language:this.language};
|
|
75
81
|
if((panel==='guide'||panel==='wardrobe')&&method==='setLanguage')return this.setLanguage(args[0]);
|
|
@@ -87,9 +93,9 @@ export class WardrobeWorld{
|
|
|
87
93
|
if(method==='restore')return this.restore();
|
|
88
94
|
throw Error('Unknown wardrobe method');
|
|
89
95
|
},
|
|
90
|
-
config:[{id:'world:wardrobe-studio',owner:'world:wardrobe-studio',schema:{type:'object',title:zh?'
|
|
96
|
+
config:[{id:'world:wardrobe-studio',owner:'world:wardrobe-studio',schema:{type:'object',title:zh?'桌宠与原生页兼容':'Desktop pet and native compatibility',properties:{'worlds.wardrobe-studio.desktopPetUrl':{type:'string',title:zh?'本机桌宠地址':'Local desktop pet address',description:zh?'修改后重启此 World。只接受本机 HTTP 地址。':'Restart this World after changing the loopback HTTP origin.','x-hot':false},'worlds.wardrobe-studio.nativeIntegration':{type:'boolean',title:zh?'启用原生装扮页兼容补丁':'Enable native wardrobe compatibility patch',description:zh?'实验功能,会备份并临时修改指定桌宠包的装扮页面。停用 World 时恢复。修改后重启此 World。':'Experimental: backs up and temporarily patches the specified native wardrobe page. Stopping restores it. Restart this World after changes.','x-hot':false},'worlds.wardrobe-studio.nativeWebDir':{type:'string',title:zh?'desktop-pet 包的 web 绝对目录':'Absolute desktop-pet web directory',description:zh?'仅支持已校验版本;留空不自动搜索磁盘。修改后重启此 World。':'Only the verified version is supported; an empty path does not scan the disk. Restart this World after changes.','x-hot':false},'worlds.wardrobe-studio.nativePort':{type:'integer',minimum:1024,maximum:65535,title:zh?'兼容接口本机端口':'Compatibility loopback port','x-hot':false}}}}]
|
|
91
97
|
};
|
|
92
98
|
}
|
|
93
|
-
shutdownVerification(){return[{key:'appearance',label:'Wardrobe appearance',status:this.error?'unknown':'verified-ended',detail:this.error??this.restoration?.status??'unchanged',manualAction:this.error?'Enable Wardrobe Studio and use Restore after the desktop pet is available.':''}];}
|
|
99
|
+
shutdownVerification(){return[{key:'appearance',label:'Wardrobe appearance',status:this.error?'unknown':'verified-ended',detail:this.error??this.restoration?.status??'unchanged',manualAction:this.error?'Enable Wardrobe Studio and use Restore after the desktop pet is available.':''},{key:'native-adapter',label:'Native wardrobe patch',status:this.native?.status.state==='error'?'unknown':'verified-ended',detail:this.native?.status.error??this.native?.status.state??'off',manualAction:this.native?.status.state==='error'?'Read the native integration recovery guide before uninstalling.':''}];}
|
|
94
100
|
}
|
|
95
|
-
export default {id:'wardrobe-studio',label:'Wardrobe Studio',defaults:()=>({...DEFAULTS}),preflight:ctx=>{localBase(ctx.cfg.desktopPetUrl);},create:ctx=>new WardrobeWorld(ctx),describe:DESCRIBE};
|
|
101
|
+
export default {id:'wardrobe-studio',label:'Wardrobe Studio',defaults:()=>({...DEFAULTS}),preflight:ctx=>{localBase(ctx.cfg.desktopPetUrl);if(ctx.cfg.nativeIntegration&&(!Number.isInteger(ctx.cfg.nativePort??17891)||(ctx.cfg.nativePort??17891)<1024||(ctx.cfg.nativePort??17891)>65535))throw Error('Native compatibility port must be between 1024 and 65535');},create:ctx=>new WardrobeWorld(ctx),describe:DESCRIBE};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {readFile,writeFile,rename,realpath,unlink,mkdir} from 'node:fs/promises';
|
|
2
|
+
import {join,dirname,basename,isAbsolute} from 'node:path';
|
|
3
|
+
import {createHash,randomBytes,randomUUID} from 'node:crypto';
|
|
4
|
+
import {NativeFeed} from './native-feed.js';
|
|
5
|
+
export const SUPPORTED_DRESS_SHA256='b594171051e90e4f368c635b4fd008405f6e41e6dfd3dbd907fdbffa767fbe9f';
|
|
6
|
+
const hash=data=>createHash('sha256').update(data).digest('hex');
|
|
7
|
+
const MAIN='dress.js',FILES=['wardrobe-studio-controls.js','wardrobe-studio-profile.js','wardrobe-studio-native.js'];
|
|
8
|
+
const readMaybe=async file=>{try{return await readFile(file);}catch(e){if(e.code==='ENOENT')return null;throw e;}};
|
|
9
|
+
async function atomic(file,bytes){const tmp=file+'.'+randomUUID()+'.tmp';try{await writeFile(tmp,bytes,{flag:'wx',mode:0o600});await rename(tmp,file);}finally{await unlink(tmp).catch(e=>{if(e.code!=='ENOENT')throw e;});}}
|
|
10
|
+
export function patchDress(source){
|
|
11
|
+
const edits=[
|
|
12
|
+
["import { ICONS, themeButton } from './ui.js';","import { ICONS, themeButton } from './ui.js';\nimport {nativeWardrobe as wardrobe} from './wardrobe-studio-native.js';"],
|
|
13
|
+
[' const picks = picksOf(pack, skin.scheme);',' if (wardrobe.render({pack,skin,after:box,apply,language:language()})) return;\n const picks = picksOf(pack, skin.scheme);'],
|
|
14
|
+
['void ready.then(() => { connect(); apply(skin, false); });',"wardrobe.onChange(() => render());\nvoid ready.then(() => { connect(); apply(skin, false); });"]
|
|
15
|
+
];
|
|
16
|
+
for(const [before,after] of edits){if(source.split(before).length!==2)throw Error('Unsupported native wardrobe source anchors');source=source.replace(before,after);}return source;
|
|
17
|
+
}
|
|
18
|
+
/** Explicit opt-in compatibility adapter, not an official World API hook. */
|
|
19
|
+
export class NativeAdapter{
|
|
20
|
+
constructor({webDir,store,origin,port=17891,snapshot}){this.webDir=webDir;this.store=store;this.origin=origin;this.port=port;this.snapshot=snapshot;this.status={state:'off'};}
|
|
21
|
+
async readRecord(){const bytes=await readMaybe(join(this.store.root,'native-adapter.json'));return bytes?JSON.parse(bytes):null;}
|
|
22
|
+
async saveRecord(record){await mkdir(this.store.root,{recursive:true});await atomic(join(this.store.root,'native-adapter.json'),JSON.stringify(record,null,2)+'\n');}
|
|
23
|
+
verifyOriginal(bytes){if(hash(bytes)!==SUPPORTED_DRESS_SHA256)throw Error('Unsupported dress.js checksum; native integration left off');}
|
|
24
|
+
async start(){
|
|
25
|
+
if(!isAbsolute(this.webDir))throw Error('Native web directory must be an explicit absolute path');
|
|
26
|
+
const web=await realpath(this.webDir),meta=JSON.parse(await readFile(join(dirname(web),'package.json'),'utf8'));
|
|
27
|
+
if(basename(web)!=='web'||meta.name!=='cortico-world-desktop-pet'||meta.version!=='0.2.0')throw Error('Unsupported desktop-pet package; native integration left off');
|
|
28
|
+
this.web=web;let record=await this.readRecord();
|
|
29
|
+
if(record){if(record.web!==web)throw Error('Restore the previous native adapter before changing its directory');await this.restoreFiles(record);}
|
|
30
|
+
const original=await readFile(join(web,MAIN));this.verifyOriginal(original);
|
|
31
|
+
const token=record?.token??randomBytes(32).toString('hex');
|
|
32
|
+
this.feed=new NativeFeed({origin:this.origin,token,port:this.port,snapshot:this.snapshot});await this.feed.start();
|
|
33
|
+
try{
|
|
34
|
+
const controls=(await readFile(new URL('../web/native-controls.js',import.meta.url),'utf8')).replace("'./profile.js'","'./wardrobe-studio-profile.js'");
|
|
35
|
+
const client=(await readFile(new URL('../web/native-client.js',import.meta.url),'utf8')).replace("'./native-controls.js'","'./wardrobe-studio-controls.js'").replace('__WARDROBE_SOCKET__',`ws://127.0.0.1:${this.feed.port}/wardrobe-view?key=${token}`);
|
|
36
|
+
const payloads={[MAIN]:Buffer.from(patchDress(original.toString('utf8'))),[FILES[0]]:Buffer.from(controls),[FILES[1]]:await readFile(new URL('./profile.js',import.meta.url)),[FILES[2]]:Buffer.from(client)};
|
|
37
|
+
for(const name of FILES)if(await readMaybe(join(web,name)))throw Error('Native adapter helper filename already exists');
|
|
38
|
+
record={contract:1,web,token,original:original.toString('base64'),originalHash:hash(original),patchedHash:hash(payloads[MAIN]),files:Object.fromEntries(FILES.map(name=>[name,hash(payloads[name])]))};
|
|
39
|
+
// Write-ahead recovery record precedes every application-file mutation.
|
|
40
|
+
await this.saveRecord(record);
|
|
41
|
+
for(const name of FILES)await atomic(join(web,name),payloads[name]);
|
|
42
|
+
if(hash(await readFile(join(web,MAIN)))!==record.originalHash)throw Error('Native page changed during installation');
|
|
43
|
+
await atomic(join(web,MAIN),payloads[MAIN]);this.status={state:'active',compatibility:'desktop-pet 0.2.0 / verified dress.js',port:this.feed.port};
|
|
44
|
+
}catch(error){await this.feed.stop();if(record)await this.restoreFiles(record);throw error;}
|
|
45
|
+
}
|
|
46
|
+
async restoreFiles(record){
|
|
47
|
+
if(record.web!==await realpath(this.webDir)||record.contract!==1||hash(Buffer.from(record.original,'base64'))!==record.originalHash)throw Error('Invalid native adapter backup');
|
|
48
|
+
this.verifyOriginal(Buffer.from(record.original,'base64'));
|
|
49
|
+
const file=join(record.web,MAIN),current=await readFile(file),currentHash=hash(current);
|
|
50
|
+
if(currentHash===record.patchedHash)await atomic(file,Buffer.from(record.original,'base64'));
|
|
51
|
+
else if(currentHash!==record.originalHash)throw Error('Native page changed externally; automatic rollback refused');
|
|
52
|
+
for(const name of FILES){const bytes=await readMaybe(join(record.web,name));if(bytes&&hash(bytes)===record.files[name])await unlink(join(record.web,name));else if(bytes)throw Error('Native helper changed externally; automatic deletion refused');}
|
|
53
|
+
}
|
|
54
|
+
async stop(){await this.feed?.stop();const record=await this.readRecord();if(record)await this.restoreFiles(record);this.status={state:'off',restored:true};}
|
|
55
|
+
broadcast(){this.feed?.broadcast();}
|
|
56
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {createServer} from 'node:http';
|
|
2
|
+
import {createHash,timingSafeEqual} from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
/** Read-only, origin-checked loopback feed. No command or filesystem RPCs. */
|
|
5
|
+
export class NativeFeed {
|
|
6
|
+
constructor({origin,token,port=17891,snapshot}){this.origin=origin;this.token=token;this.port=port;this.snapshot=snapshot;this.clients=new Set();}
|
|
7
|
+
async start(){
|
|
8
|
+
const server=this.server=createServer((_req,res)=>res.writeHead(404).end());
|
|
9
|
+
server.headersTimeout=3000;
|
|
10
|
+
server.on('upgrade',(req,socket,head)=>{
|
|
11
|
+
const reject=()=>{socket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');};
|
|
12
|
+
const key=req.headers['sec-websocket-key'];let url;try{url=new URL(req.url,'http://127.0.0.1');}catch{return reject();}
|
|
13
|
+
const supplied=Buffer.from(url.searchParams.get('key')??''),expected=Buffer.from(this.token);
|
|
14
|
+
if(req.headers.origin!==this.origin||req.headers.host!==`127.0.0.1:${this.port}`||url.pathname!=='/wardrobe-view'||supplied.length!==expected.length||!timingSafeEqual(supplied,expected)||req.headers['sec-websocket-version']!=='13'||typeof key!=='string'||!/^[A-Za-z0-9+/]{22}==$/.test(key)||head.length)return reject();
|
|
15
|
+
const accept=createHash('sha1').update(key+'258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64');
|
|
16
|
+
socket.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\n\r\n`);
|
|
17
|
+
this.clients.add(socket);socket.on('error',()=>{});socket.on('close',()=>this.clients.delete(socket));
|
|
18
|
+
// Clients never send commands; even unsolicited application frames close the connection.
|
|
19
|
+
socket.on('data',()=>socket.destroy());this.send(socket);
|
|
20
|
+
});
|
|
21
|
+
await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(this.port,'127.0.0.1',()=>{server.off('error',reject);this.port=server.address().port;resolve();});});
|
|
22
|
+
this.timer=setInterval(()=>this.broadcast(),2000);this.timer.unref();
|
|
23
|
+
}
|
|
24
|
+
send(socket){
|
|
25
|
+
let body;try{body=Buffer.from(JSON.stringify(this.snapshot()));}catch{socket.destroy();return;}
|
|
26
|
+
if(body.length>1024*1024||socket.writableLength>2*1024*1024){socket.destroy();return;}
|
|
27
|
+
let header;if(body.length<126)header=Buffer.from([0x81,body.length]);else if(body.length<65536){header=Buffer.alloc(4);header[0]=0x81;header[1]=126;header.writeUInt16BE(body.length,2);}else{header=Buffer.alloc(10);header[0]=0x81;header[1]=127;header.writeBigUInt64BE(BigInt(body.length),2);}
|
|
28
|
+
socket.write(Buffer.concat([header,body]));
|
|
29
|
+
}
|
|
30
|
+
broadcast(){for(const socket of this.clients)this.send(socket);}
|
|
31
|
+
async stop(){clearInterval(this.timer);for(const s of this.clients)s.destroy();this.clients.clear();if(this.server?.listening)await new Promise(resolve=>this.server.close(resolve));}
|
|
32
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import {NativeAdapter} from './native-adapter.js';
|
|
2
|
+
import {Store} from './store.js';
|
|
3
|
+
const args=process.argv.slice(2);
|
|
4
|
+
if(args.length!==4||args[0]!=='--data-dir'||args[2]!=='--web-dir'){
|
|
5
|
+
console.error('Usage: node src/native-restore.js --data-dir <World dataDir> --web-dir <desktop-pet/web>');process.exitCode=1;
|
|
6
|
+
}else{
|
|
7
|
+
try{await new NativeAdapter({webDir:args[3],store:new Store(args[1])}).stop();console.log('Native adapter files restored; reopen the native wardrobe page.');}
|
|
8
|
+
catch(error){console.error(error.message);process.exitCode=1;}
|
|
9
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {createWardrobeControls} from './native-controls.js';
|
|
2
|
+
let snapshot={enabled:false,profiles:[]},changed=()=>{},socket,retry,watchdog,closed=false,last='';
|
|
3
|
+
export const nativeWardrobe=createWardrobeControls({load:async()=>snapshot,onChange:()=>changed()});
|
|
4
|
+
nativeWardrobe.onChange=fn=>{changed=fn;};
|
|
5
|
+
const render=nativeWardrobe.render;
|
|
6
|
+
nativeWardrobe.render=args=>render({...args,language:snapshot.language??'zh'});
|
|
7
|
+
function accept(next){
|
|
8
|
+
snapshot=next;const value=JSON.stringify(next);
|
|
9
|
+
if(value!==last){last=value;void nativeWardrobe.refresh();}
|
|
10
|
+
}
|
|
11
|
+
function offline(){clearTimeout(watchdog);accept({enabled:false,profiles:[]});}
|
|
12
|
+
function connect(){
|
|
13
|
+
if(closed)return;
|
|
14
|
+
socket=new WebSocket('__WARDROBE_SOCKET__');
|
|
15
|
+
socket.onmessage=e=>{
|
|
16
|
+
try{if(typeof e.data!=='string'||e.data.length>1024*1024)throw Error('Invalid provider frame');const next=JSON.parse(e.data);if(next.contract!==1||next.enabled!==true||!Array.isArray(next.profiles))throw Error('Invalid provider');accept(next);clearTimeout(watchdog);watchdog=setTimeout(()=>{offline();socket.close();},6500);}catch{offline();socket.close();}
|
|
17
|
+
};
|
|
18
|
+
socket.onerror=()=>socket.close();
|
|
19
|
+
socket.onclose=()=>{offline();if(!closed)retry=setTimeout(connect,2000);};
|
|
20
|
+
}
|
|
21
|
+
connect();
|
|
22
|
+
addEventListener('pagehide',()=>{closed=true;clearTimeout(retry);clearTimeout(watchdog);socket?.close();},{once:true});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {validateProfile,validateAgainstPack,selectVariant,availableOptions} from './profile.js';
|
|
2
|
+
|
|
3
|
+
/** Compatibility rendering hook, restricted to the native outfit rows. */
|
|
4
|
+
export function createWardrobeControls({load,onChange,onError}){
|
|
5
|
+
let provider={enabled:false,profiles:[]},request=0;
|
|
6
|
+
return{
|
|
7
|
+
async refresh(){
|
|
8
|
+
const epoch=++request;
|
|
9
|
+
try{const next=await load();if(epoch!==request)return;provider={enabled:next.enabled===true,profiles:(next.profiles??[]).map(validateProfile)};}
|
|
10
|
+
catch(error){if(epoch!==request)return;provider={enabled:false,profiles:[]};onError?.(error);}
|
|
11
|
+
onChange();
|
|
12
|
+
},
|
|
13
|
+
render({pack,skin,after,apply,language='zh'}){
|
|
14
|
+
if(!provider.enabled||pack.id==='coo')return false;
|
|
15
|
+
const profile=provider.profiles.find(p=>p.figure===pack.id);if(!profile)return false;
|
|
16
|
+
try{validateAgainstPack(profile,pack);}catch(error){onError?.(error);return false;}
|
|
17
|
+
const outfit=profile.outfits.find(o=>o.variants.some(v=>v.scheme===skin.scheme))??profile.outfits[0];
|
|
18
|
+
const active=outfit.variants.find(v=>v.scheme===skin.scheme)??selectVariant(outfit);
|
|
19
|
+
const doc=after.ownerDocument,el=(tag,cls,text)=>{const e=doc.createElement(tag);e.className=cls;if(text!==undefined)e.textContent=text;return e;};
|
|
20
|
+
let cursor=after;
|
|
21
|
+
function row(id,label,choices){
|
|
22
|
+
const title=el('div','row-label pack-axis',label),slot=el('div','slot pack-axis'),list=el('div','opts');title.id='wardrobe-'+id;slot.setAttribute('role','group');slot.setAttribute('aria-labelledby',title.id);
|
|
23
|
+
for(const choice of choices){
|
|
24
|
+
const button=el('button','opt wide');button.type='button';button.setAttribute('aria-pressed',String(choice.selected));button.disabled=!!choice.disabled;
|
|
25
|
+
if(choice.disabled)button.title=language==='en'?'No complete resources for this combination':'此搭配暂无完整资源';
|
|
26
|
+
if(choice.thumb){const image=el('img','');image.alt='';image.src=pack.base+choice.thumb;button.append(image);}
|
|
27
|
+
button.append(el('span','',choice.label));button.addEventListener('click',()=>{if(!button.disabled)apply({...skin,scheme:choice.scheme},true);});list.append(button);
|
|
28
|
+
}
|
|
29
|
+
slot.append(list);cursor.after(title,slot);cursor=slot;
|
|
30
|
+
}
|
|
31
|
+
const thumb=scheme=>{
|
|
32
|
+
const preset=pack.presets?.find(p=>p.id===scheme),parts=scheme.split('-');
|
|
33
|
+
return [...pack.axes].reverse().map((a,j)=>a.options.find(o=>o.id===(preset?.pick[a.id]??parts[pack.axes.length-1-j]))?.thumb).find(Boolean)??pack.thumb;
|
|
34
|
+
};
|
|
35
|
+
row('outfit',language==='en'?'Outfit':'服装',profile.outfits.map(o=>{const v=o.id===outfit.id?active:selectVariant(o);return{label:o.label,selected:o.id===outfit.id,thumb:thumb(v.scheme),scheme:v.scheme};}));
|
|
36
|
+
for(const control of outfit.controls){const valid=new Set(availableOptions(outfit,active.pick,control.id).map(o=>o.id));
|
|
37
|
+
row(control.id,control.label,control.options.map(option=>({label:option.label,selected:active.pick[control.id]===option.id,disabled:!valid.has(option.id),scheme:valid.has(option.id)?selectVariant(outfit,{...active.pick,[control.id]:option.id}).scheme:skin.scheme})));
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|