@lily-cooperative/custom-farm 0.37.0 → 0.38.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/README.md +37 -8
- package/describe.json +8 -0
- package/dist/webui.js +147 -8
- package/overlay/README.md +4 -0
- package/overlay/overlay-webview.ps1 +260 -0
- package/overlay/webview2/LICENSE.txt +27 -0
- package/overlay/webview2/Microsoft.Web.WebView2.Core.dll +0 -0
- package/overlay/webview2/Microsoft.Web.WebView2.Wpf.dll +0 -0
- package/overlay/webview2/NOTICE.txt +85 -0
- package/overlay/webview2/WebView2Loader.dll +0 -0
- package/overlay-electron/launch.js +43 -30
- package/overlay-electron/main.js +77 -33
- package/overlay-electron/renderer.js +39 -11
- package/overlay-electron/zorder.ps1 +64 -0
- package/package.json +2 -7
- package/src/catalog.js +3 -0
- package/src/farm.js +5 -0
- package/src/index.js +20 -1
- package/src/paths.js +2 -1
- package/src/world.js +218 -53
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
| 面板 | 能改什么 |
|
|
22
22
|
|---|---|
|
|
23
23
|
| **农场自定义 → 农场** | 地面贴图、地表高度、农田大小、导入自己的 PNG |
|
|
24
|
-
| **农场自定义 → 农作物** |
|
|
24
|
+
| **农场自定义 → 农作物** | 增删作物、阶段数、每阶段天数、产物卖价、种子买价、各阶段/种子/产物贴图、上下图层 |
|
|
25
25
|
| **农场自定义 → 杂草** | 增删杂草、阶段与天数、出现权重、上下图层、收获物名字/价值/贴图 |
|
|
26
26
|
| **农场自定义 → 浇水特效 / 枯萎** | 浇水帧序列、水花帧序列、帧间隔、枯萎贴图 |
|
|
27
27
|
| **生态自定义** | 一个游戏日多长、离线最多补几天、刷新间隔、每天是否推事件、浇水蒸发/补水/枯萎天数、天气 |
|
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
|
|
62
62
|
扩展包自带 `overlay` / `overlay-electron` / `assets`,第一次启动会把它们复制到安装目录,不需要另外准备项目文件夹。
|
|
63
63
|
|
|
64
|
+
**扩展不再把 Electron 作为 npm 依赖下载。** 安装时只装扩展本身;有没有 Electron 运行时都不会影响安装。桌面图层按下面的「环境自检 / 图层引擎」选择。
|
|
65
|
+
|
|
64
66
|
### 本地开发
|
|
65
67
|
|
|
66
68
|
```powershell
|
|
@@ -73,6 +75,28 @@ Invoke-RestMethod -Method Post http://127.0.0.1:17788/api/run/restart
|
|
|
73
75
|
|
|
74
76
|
## 游戏配置
|
|
75
77
|
|
|
78
|
+
### 环境自检 / 图层引擎
|
|
79
|
+
|
|
80
|
+
**游戏配置页顶部**有「环境自检」按钮,会检查:
|
|
81
|
+
|
|
82
|
+
- 当前平台和 Node 版本
|
|
83
|
+
- 是否能找到 `electron.exe`
|
|
84
|
+
- 安装目录里的 `overlay-webview.ps1`、WebView2 库和 WebView2 运行时是否可用
|
|
85
|
+
|
|
86
|
+
自检会明确显示「缺少 Electron 运行时」还是「WPF/WebView2 图层可用」。下面的「图层引擎」切选单可以选:
|
|
87
|
+
|
|
88
|
+
- **自动**:有备用 Electron 就用,没有就用 WPF/WebView2(推荐,不会下载 Electron)
|
|
89
|
+
- **备用 Electron**:只使用检测到的 Electron,需要 `electron.exe`
|
|
90
|
+
- **WPF(WebView2)**:完全不使用 Electron,但加载和 Electron 相同的 `renderer.html/renderer.js`
|
|
91
|
+
|
|
92
|
+
WPF 回退现在是 WebView2 宿主,不再用另一套 WPF 自绘;因此缩放、精灵位置、阴影、粒子、水滴、晃动等视觉效果和 Electron 基本完全一致。需要 Windows 10/11 自带的 WebView2 Runtime。
|
|
93
|
+
|
|
94
|
+
自检里的「Electron 包」下拉会列出检测到的路径;也可以在「备用 Electron」配置项里手动选 `electron.exe`。
|
|
95
|
+
|
|
96
|
+
第一次启用时还会自动做一次自检;如果当前引擎缺运行时,会推一次 `farm.env` 事件,让桌宠把“缺少 Electron 运行时 / 怎么修”转告主人。之后不会重复提醒。
|
|
97
|
+
|
|
98
|
+
Coopanion 只提供打包后的 `Coopanion.exe`,没有可复用的独立 `electron.exe`;所以默认不再下载第二份 Electron。备用 Electron 可以放在安装目录的 `node_modules\electron\dist\electron.exe`,或在「备用 Electron」里手动选择路径。
|
|
99
|
+
|
|
76
100
|
### 安装路径
|
|
77
101
|
|
|
78
102
|
在 Coopanion 的 **游戏配置 → 安装** 里可以直接看到和修改安装路径:
|
|
@@ -88,7 +112,7 @@ Invoke-RestMethod -Method Post http://127.0.0.1:17788/api/run/restart
|
|
|
88
112
|
|
|
89
113
|
- 「自动」使用主屏
|
|
90
114
|
- 下拉里会列出当前所有显示器,改完约 1 秒内移动到目标屏幕
|
|
91
|
-
- 这个设置作用于 Electron 图层;WPF
|
|
115
|
+
- 这个设置作用于 Electron 图层;WPF/WebView2 回退使用主屏(与现有 WPF 行为一致)
|
|
92
116
|
|
|
93
117
|
### 备份与重置
|
|
94
118
|
|
|
@@ -101,10 +125,13 @@ Invoke-RestMethod -Method Post http://127.0.0.1:17788/api/run/restart
|
|
|
101
125
|
|
|
102
126
|
## 桌面图层
|
|
103
127
|
|
|
104
|
-
-
|
|
105
|
-
-
|
|
128
|
+
- 图层拆成 `bg` / `top` 两个透明窗口:`bg` 在桌宠下面,`top` 是桌宠的 owned window,永远在桌宠上面。
|
|
129
|
+
- 野草、装饰、作物、掉落物和特效按 `layer` 设置分配到两个窗口;上层野草/装饰可以正常遮挡桌宠,下层作物在桌宠后面。
|
|
130
|
+
- 层级守护每 500ms 重新确认桌宠窗口;桌宠重新置顶、显示/隐藏、被点击或窗口重建后,会自动重新挂 owner / 把 `bg` 放回桌宠下面,防止图层整体翻转。
|
|
131
|
+
- **Windows 10/11**:默认「自动」——检测到备用 Electron 就用 Electron,否则用 WPF/WebView2;不会为了安装扩展下载 Electron。两个引擎都使用同一套 Canvas renderer,缩放和特效一致。
|
|
132
|
+
- **macOS / Linux**:暂不支持;`os` 已收紧为 `win32`。
|
|
106
133
|
- 图层跟着桌宠显示/隐藏/退出;任务栏换边、换屏会重新计算位置。
|
|
107
|
-
-
|
|
134
|
+
- 背包、商店、农场预览、摆放和手动合成都在 WebUI。
|
|
108
135
|
|
|
109
136
|
## 工具
|
|
110
137
|
|
|
@@ -124,7 +151,7 @@ Invoke-RestMethod -Method Post http://127.0.0.1:17788/api/run/restart
|
|
|
124
151
|
| `farm_sell` | 卖背包里的道具 |
|
|
125
152
|
| `farm_place` | 把装饰放到地上 |
|
|
126
153
|
|
|
127
|
-
事件:`farm.ripe`、`farm.dry`、`farm.day`、`farm.state`。`farm.day` 默认关,`dailyEvent=true`
|
|
154
|
+
事件:`farm.ripe`、`farm.dry`、`farm.day`、`farm.state`、`farm.env`。`farm.day` 默认关,`dailyEvent=true` 才推;`farm.env` 只在第一次启用自检失败时推一次,让桌宠把环境问题转告主人。
|
|
128
155
|
|
|
129
156
|
## 默认参数
|
|
130
157
|
|
|
@@ -155,7 +182,7 @@ Invoke-RestMethod -Method Post http://127.0.0.1:17788/api/run/restart
|
|
|
155
182
|
| `dist/webui.js` / `.css` | 「自定义农场」面板 |
|
|
156
183
|
| `icon.png` | 扩展图标(安装页显示) |
|
|
157
184
|
| `describe.json` | 工具/事件自描述 |
|
|
158
|
-
| `overlay/` | WPF
|
|
185
|
+
| `overlay/` | WPF/WebView2 回退(同一套渲染器) |
|
|
159
186
|
| `overlay-electron/` | Electron 图层 |
|
|
160
187
|
| `assets/` | 内置作物/杂草/地面/装饰/图标 |
|
|
161
188
|
|
|
@@ -169,7 +196,9 @@ Invoke-RestMethod -Method Post http://127.0.0.1:17788/api/run/restart
|
|
|
169
196
|
|
|
170
197
|
- Coopanion `>= 0.2.13`,World API `6`
|
|
171
198
|
- Node.js `>= 22`(Coopanion 自带)
|
|
172
|
-
-
|
|
199
|
+
- 仅 Windows 10/11;macOS / Linux 暂不支持,等后续测试
|
|
200
|
+
- Electron 不是 npm 依赖;备用 Electron 可选,Windows 没有时用 WPF/WebView2
|
|
201
|
+
- WPF 回退需要 Windows 10/11 的 WebView2 Runtime(系统一般自带)
|
|
173
202
|
- Windows 10/11 已实机测试;macOS / Linux 未实机测试
|
|
174
203
|
|
|
175
204
|
## 许可
|
package/describe.json
CHANGED
|
@@ -340,6 +340,14 @@
|
|
|
340
340
|
"en": "A custom state triggered a consume action: e.g. hunger ate a crop (the text says what and the state change), or asks the pet to use farm_consume; also fires when nothing edible is in the bag."
|
|
341
341
|
},
|
|
342
342
|
"trigger": "debounce"
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
"type": "farm.env",
|
|
346
|
+
"description": {
|
|
347
|
+
"zh": "第一次启用时环境自检发现 Electron/WPF 运行时缺失;正文说明缺什么、怎么在「游戏配置 → 环境自检」里修复,让桌宠转告主人。",
|
|
348
|
+
"en": "The first-start environment check found a missing Electron/WPF runtime; the text says what is missing and how to fix it in Game Config → Environment check, so the pet can tell its owner."
|
|
349
|
+
},
|
|
350
|
+
"trigger": "debounce"
|
|
343
351
|
}
|
|
344
352
|
]
|
|
345
353
|
}
|
package/dist/webui.js
CHANGED
|
@@ -20,29 +20,137 @@ function renameFarmConfigTab() {
|
|
|
20
20
|
} catch { /* 不是浏览器环境就算了 */ }
|
|
21
21
|
}
|
|
22
22
|
if (typeof document !== 'undefined') {
|
|
23
|
-
const
|
|
23
|
+
const refreshFarmConfigUi = () => {
|
|
24
24
|
renameFarmConfigTab();
|
|
25
|
+
injectFarmEnvCheck();
|
|
25
26
|
injectFarmConfigActions();
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
};
|
|
28
|
+
const start = () => {
|
|
29
|
+
refreshFarmConfigUi();
|
|
30
|
+
new MutationObserver(refreshFarmConfigUi).observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
31
|
+
window.addEventListener('hashchange', refreshFarmConfigUi);
|
|
32
|
+
setInterval(refreshFarmConfigUi, 1000);
|
|
29
33
|
};
|
|
30
34
|
if (document.body) start();
|
|
31
35
|
else document.addEventListener('DOMContentLoaded', start, { once: true });
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
/** 调 World 的 manage 面板:备份 /
|
|
35
|
-
async function farmManageCall(method) {
|
|
38
|
+
/** 调 World 的 manage 面板:备份 / 重置 / 自检。 */
|
|
39
|
+
async function farmManageCall(method, args = []) {
|
|
36
40
|
const res = await fetch('/api/webui/providers/' + encodeURIComponent('world:farm') + '/panels/manage/' + method, {
|
|
37
41
|
method: 'POST',
|
|
38
42
|
headers: { 'content-type': 'application/json' },
|
|
39
|
-
body: JSON.stringify({ args
|
|
43
|
+
body: JSON.stringify({ args }),
|
|
40
44
|
});
|
|
41
45
|
const data = await res.json().catch(() => ({}));
|
|
42
46
|
if (!res.ok) throw new Error(data.error || ('HTTP ' + res.status));
|
|
43
47
|
return data;
|
|
44
48
|
}
|
|
45
49
|
|
|
50
|
+
/** 游戏配置顶部的环境自检 + Electron/WPF 切选单。 */
|
|
51
|
+
function injectFarmEnvCheck() {
|
|
52
|
+
try {
|
|
53
|
+
const hash = String(location.hash || '');
|
|
54
|
+
if (!hash.includes('world%3Afarm') && !hash.includes('world:farm')) return;
|
|
55
|
+
const view = document.querySelector('.configview');
|
|
56
|
+
if (!view || view.querySelector('[data-farm-env]')) return;
|
|
57
|
+
|
|
58
|
+
const section = document.createElement('div');
|
|
59
|
+
section.className = 'tsection';
|
|
60
|
+
section.dataset.farmEnv = '1';
|
|
61
|
+
section.textContent = '环境自检';
|
|
62
|
+
|
|
63
|
+
const desc = document.createElement('div');
|
|
64
|
+
desc.className = 'tsecdesc';
|
|
65
|
+
desc.textContent = '检查图层运行时是否齐全。Coopanion 不提供独立的 electron.exe;没有备用 Electron 时会用 WPF 图层,不会为了安装扩展再下载 Electron。';
|
|
66
|
+
|
|
67
|
+
const row = document.createElement('div');
|
|
68
|
+
row.className = 'trow';
|
|
69
|
+
const label = document.createElement('span');
|
|
70
|
+
label.className = 'tlabel';
|
|
71
|
+
label.textContent = '图层引擎';
|
|
72
|
+
const field = document.createElement('div');
|
|
73
|
+
field.className = 'tfield';
|
|
74
|
+
field.style.display = 'flex';
|
|
75
|
+
field.style.gap = '8px';
|
|
76
|
+
field.style.flexWrap = 'wrap';
|
|
77
|
+
|
|
78
|
+
const select = document.createElement('select');
|
|
79
|
+
select.className = 'field';
|
|
80
|
+
for (const [value, text] of [['auto', '自动(有备用 Electron 就用,否则 WPF/WebView2)'], ['electron', '备用 Electron(需要 electron.exe)'], ['wpf', 'WPF/WebView2(不需要 Electron)']]) {
|
|
81
|
+
const opt = document.createElement('option');
|
|
82
|
+
opt.value = value;
|
|
83
|
+
opt.textContent = text;
|
|
84
|
+
select.appendChild(opt);
|
|
85
|
+
}
|
|
86
|
+
const checkBtn = document.createElement('button');
|
|
87
|
+
checkBtn.type = 'button';
|
|
88
|
+
checkBtn.className = 'btn sm primary';
|
|
89
|
+
checkBtn.textContent = '环境自检';
|
|
90
|
+
const electronSelect = document.createElement('select');
|
|
91
|
+
electronSelect.className = 'field';
|
|
92
|
+
const electronAuto = document.createElement('option');
|
|
93
|
+
electronAuto.value = '';
|
|
94
|
+
electronAuto.textContent = 'Electron:自动查找';
|
|
95
|
+
electronSelect.appendChild(electronAuto);
|
|
96
|
+
const status = document.createElement('div');
|
|
97
|
+
status.className = 'tdesc';
|
|
98
|
+
status.style.whiteSpace = 'pre-line';
|
|
99
|
+
status.style.marginTop = '6px';
|
|
100
|
+
|
|
101
|
+
const render = (data) => {
|
|
102
|
+
if (!data) return;
|
|
103
|
+
if (data.engine) select.value = data.engine;
|
|
104
|
+
const current = data.electron && data.electron.path ? data.electron.path : '';
|
|
105
|
+
electronSelect.replaceChildren(electronAuto);
|
|
106
|
+
electronAuto.textContent = 'Electron:自动查找';
|
|
107
|
+
for (const row of data.electronCandidates || []) {
|
|
108
|
+
const opt = document.createElement('option');
|
|
109
|
+
opt.value = row.path;
|
|
110
|
+
opt.textContent = 'Electron:' + row.label + (row.ok ? '' : '(缺失)');
|
|
111
|
+
opt.disabled = !row.ok;
|
|
112
|
+
electronSelect.appendChild(opt);
|
|
113
|
+
}
|
|
114
|
+
if (current) electronSelect.value = current;
|
|
115
|
+
if (!current && !(data.electronCandidates || []).some((r) => r.ok)) {
|
|
116
|
+
const opt = document.createElement('option');
|
|
117
|
+
opt.value = '';
|
|
118
|
+
opt.textContent = 'Electron:未找到';
|
|
119
|
+
opt.disabled = true;
|
|
120
|
+
electronSelect.appendChild(opt);
|
|
121
|
+
}
|
|
122
|
+
status.textContent = data.message || data.text || '';
|
|
123
|
+
status.style.color = data.ok ? '' : 'var(--extension-alert, #c0392b)';
|
|
124
|
+
};
|
|
125
|
+
const check = async () => {
|
|
126
|
+
checkBtn.disabled = true;
|
|
127
|
+
try { render(await farmManageCall('check')); } catch (err) { status.textContent = '自检失败:' + ((err && err.message) || err); } finally { checkBtn.disabled = false; }
|
|
128
|
+
};
|
|
129
|
+
checkBtn.addEventListener('click', check);
|
|
130
|
+
select.addEventListener('change', async () => {
|
|
131
|
+
status.textContent = '正在切换图层引擎…';
|
|
132
|
+
try {
|
|
133
|
+
render(await farmManageCall('setEngine', [select.value]));
|
|
134
|
+
} catch (err) {
|
|
135
|
+
status.textContent = '切换失败:' + ((err && err.message) || err);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
electronSelect.addEventListener('change', async () => {
|
|
139
|
+
status.textContent = '正在切换 Electron…';
|
|
140
|
+
try {
|
|
141
|
+
render(await farmManageCall('setElectronPath', [electronSelect.value]));
|
|
142
|
+
} catch (err) {
|
|
143
|
+
status.textContent = '切换失败:' + ((err && err.message) || err);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
field.append(select, electronSelect, checkBtn);
|
|
148
|
+
row.append(label, field);
|
|
149
|
+
view.prepend(section, desc, row, status);
|
|
150
|
+
void check();
|
|
151
|
+
} catch { /* 不是宿主的配置页就算了 */ }
|
|
152
|
+
}
|
|
153
|
+
|
|
46
154
|
/** 往宿主的「游戏配置」页里塞「存档与重置」按钮。 */
|
|
47
155
|
function injectFarmConfigActions() {
|
|
48
156
|
try {
|
|
@@ -169,6 +277,9 @@ const TEXT = {
|
|
|
169
277
|
weedLayer: '图层',
|
|
170
278
|
weedLayerTop: '上(和作物/桌宠同前台层)',
|
|
171
279
|
weedLayerBottom: '下(背景层,作物后方)',
|
|
280
|
+
cropLayer: '图层',
|
|
281
|
+
cropLayerTop: '上(和桌宠同前台层)',
|
|
282
|
+
cropLayerBottom: '下(背景层,桌宠后方)',
|
|
172
283
|
addWeed: '添加杂草',
|
|
173
284
|
name: '名字',
|
|
174
285
|
stages: '阶段数',
|
|
@@ -273,6 +384,9 @@ const TEXT = {
|
|
|
273
384
|
weedLayer: 'Layer',
|
|
274
385
|
weedLayerTop: 'Top (foreground with crops/pet)',
|
|
275
386
|
weedLayerBottom: 'Bottom (background, behind crops)',
|
|
387
|
+
cropLayer: 'Layer',
|
|
388
|
+
cropLayerTop: 'Top (foreground with pet)',
|
|
389
|
+
cropLayerBottom: 'Bottom (background, behind pet)',
|
|
276
390
|
addWeed: 'Add weed',
|
|
277
391
|
name: 'Name',
|
|
278
392
|
stages: 'Stages',
|
|
@@ -568,6 +682,7 @@ function panel(ctx) {
|
|
|
568
682
|
preview: [...c.preview],
|
|
569
683
|
previewSeed: c.previewSeed,
|
|
570
684
|
previewItem: c.previewItem,
|
|
685
|
+
layer: c.layer === 'top' ? 'top' : 'bottom',
|
|
571
686
|
};
|
|
572
687
|
const details = ui.h('details', 'cf-card');
|
|
573
688
|
if (!c.builtin) details.open = false;
|
|
@@ -578,10 +693,20 @@ function panel(ctx) {
|
|
|
578
693
|
ui.pill(c.builtin ? T.builtin : T.custom, c.builtin ? 'plain' : 'on'),
|
|
579
694
|
ui.chip(c.stages + ' 阶段', 'plain'),
|
|
580
695
|
ui.chip(T.daysTotal(c.totalDays), 'plain'),
|
|
696
|
+
ui.chip(c.layer === 'top' ? T.cropLayerTop : T.cropLayerBottom, c.layer === 'top' ? 'on' : 'plain'),
|
|
581
697
|
);
|
|
582
698
|
const body = ui.h('div', 'cf-body');
|
|
583
699
|
details.append(summary, body);
|
|
584
700
|
|
|
701
|
+
const layerSel = ui.select({
|
|
702
|
+
value: draft.layer,
|
|
703
|
+
options: [
|
|
704
|
+
{ value: 'top', label: T.cropLayerTop },
|
|
705
|
+
{ value: 'bottom', label: T.cropLayerBottom },
|
|
706
|
+
],
|
|
707
|
+
});
|
|
708
|
+
layerSel.addEventListener('change', () => { draft.layer = layerSel.value; }, { signal: ctx.signal });
|
|
709
|
+
|
|
585
710
|
const row1 = ui.h('div', 'cf-row');
|
|
586
711
|
row1.append(
|
|
587
712
|
ui.field(T.name, textInput(draft.name, (v) => { draft.name = v; title.textContent = v || c.key; })),
|
|
@@ -591,6 +716,7 @@ function panel(ctx) {
|
|
|
591
716
|
resizeSprites(draft);
|
|
592
717
|
paintStages();
|
|
593
718
|
})),
|
|
719
|
+
ui.field(T.cropLayer, layerSel),
|
|
594
720
|
);
|
|
595
721
|
|
|
596
722
|
const stagesBox = ui.h('div', 'cf-stages');
|
|
@@ -651,6 +777,7 @@ function panel(ctx) {
|
|
|
651
777
|
sprites: draft.sprites,
|
|
652
778
|
seedSprite: draft.seedSprite,
|
|
653
779
|
itemSprite: draft.itemSprite,
|
|
780
|
+
layer: draft.layer,
|
|
654
781
|
}], { toast: T.saved });
|
|
655
782
|
off.dispose();
|
|
656
783
|
})(),
|
|
@@ -2795,6 +2922,18 @@ function managePanel(ctx) {
|
|
|
2795
2922
|
});
|
|
2796
2923
|
const bar = ui.rowbar();
|
|
2797
2924
|
const status = ui.h('p', 'cf-note', '');
|
|
2925
|
+
const check = ui.button('环境自检', {
|
|
2926
|
+
onClick: async () => {
|
|
2927
|
+
check.disabled = true;
|
|
2928
|
+
status.textContent = '正在检查…';
|
|
2929
|
+
try {
|
|
2930
|
+
const r = await ctx.invoke('check');
|
|
2931
|
+
status.textContent = r.message || r.text || '';
|
|
2932
|
+
} catch (err) {
|
|
2933
|
+
status.textContent = '自检失败:' + ((err && err.message) || err);
|
|
2934
|
+
} finally { check.disabled = false; }
|
|
2935
|
+
},
|
|
2936
|
+
});
|
|
2798
2937
|
const backup = ui.button('备份存档', {
|
|
2799
2938
|
variant: 'primary',
|
|
2800
2939
|
onClick: async () => {
|
|
@@ -2823,7 +2962,7 @@ function managePanel(ctx) {
|
|
|
2823
2962
|
}
|
|
2824
2963
|
},
|
|
2825
2964
|
});
|
|
2826
|
-
bar.append(backup, reset);
|
|
2965
|
+
bar.append(check, backup, reset);
|
|
2827
2966
|
sheet.body.append(bar, status);
|
|
2828
2967
|
root.appendChild(sheet.el);
|
|
2829
2968
|
}
|
package/overlay/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# 桌面图层(overlay)· 使用说明
|
|
2
2
|
|
|
3
|
+
> 运行时 WPF 回退已改为 WebView2 宿主 `overlay-webview.ps1`,加载与 Electron 相同的
|
|
4
|
+
> `overlay-electron/renderer.html` / `renderer.js`;本文档描述的是保留的旧 WPF 自绘
|
|
5
|
+
> 实现(`overlay.ps1`),正常启动不再使用它。
|
|
6
|
+
>
|
|
3
7
|
> v4:作物种在任务栏上沿的全宽透明条带上;统一缩放、掉落物、摇晃、浇水/枯萎特效、
|
|
4
8
|
> 收割的作物**不发光**。图层只渲染,状态全从 `state.json` 读。
|
|
5
9
|
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# WebView2 host for the farm overlay.
|
|
2
|
+
# It renders the same overlay-electron/renderer.html + renderer.js as the
|
|
3
|
+
# Electron layer, so WPF fallback has the same geometry, sprites and effects.
|
|
4
|
+
param(
|
|
5
|
+
[Parameter(Mandatory = $true)][string]$StatePath,
|
|
6
|
+
[Parameter(Mandatory = $true)][string]$HomeDir,
|
|
7
|
+
[Parameter(Mandatory = $true)][string]$AssetsDir,
|
|
8
|
+
[string]$PidPath = '',
|
|
9
|
+
[string]$LogPath = '',
|
|
10
|
+
[int]$StripHeight = 320,
|
|
11
|
+
[int]$LeaseSeconds = 20,
|
|
12
|
+
[ValidateSet('bg', 'top')][string]$Layer = 'bg'
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
$ErrorActionPreference = 'Stop'
|
|
16
|
+
Add-Type -AssemblyName PresentationFramework
|
|
17
|
+
Add-Type -AssemblyName PresentationCore
|
|
18
|
+
Add-Type -AssemblyName WindowsBase
|
|
19
|
+
Add-Type -AssemblyName System.Drawing
|
|
20
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
21
|
+
|
|
22
|
+
function Write-Log([string]$s) {
|
|
23
|
+
if (-not $LogPath) { return }
|
|
24
|
+
try {
|
|
25
|
+
$line = (Get-Date).ToString('o') + ' [webview] ' + $s
|
|
26
|
+
Add-Content -LiteralPath $LogPath -Value $line -Encoding UTF8
|
|
27
|
+
} catch { }
|
|
28
|
+
}
|
|
29
|
+
function Read-State {
|
|
30
|
+
try { return Get-Content -LiteralPath $StatePath -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
|
|
31
|
+
}
|
|
32
|
+
function Write-Pid {
|
|
33
|
+
if (-not $PidPath) { return }
|
|
34
|
+
try {
|
|
35
|
+
$dir = Split-Path -Parent $PidPath
|
|
36
|
+
if ($dir -and -not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
|
|
37
|
+
Set-Content -LiteralPath $PidPath -Value $PID -Encoding ascii
|
|
38
|
+
} catch { }
|
|
39
|
+
}
|
|
40
|
+
function Clear-Pid {
|
|
41
|
+
if (-not $PidPath) { return }
|
|
42
|
+
try {
|
|
43
|
+
if ((Get-Content -LiteralPath $PidPath -Raw -ErrorAction SilentlyContinue).Trim() -eq [string]$PID) {
|
|
44
|
+
Remove-Item -LiteralPath $PidPath -Force -ErrorAction SilentlyContinue
|
|
45
|
+
}
|
|
46
|
+
} catch { }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
$here = Split-Path -Parent $PSCommandPath
|
|
50
|
+
$dllDir = Join-Path $here 'webview2'
|
|
51
|
+
$env:PATH = $dllDir + ';' + $env:PATH
|
|
52
|
+
Add-Type -Path (Join-Path $dllDir 'Microsoft.Web.WebView2.Core.dll')
|
|
53
|
+
Add-Type -Path (Join-Path $dllDir 'Microsoft.Web.WebView2.Wpf.dll')
|
|
54
|
+
|
|
55
|
+
if (-not (Test-Path -LiteralPath $AssetsDir)) { throw ('assets dir not found: ' + $AssetsDir) }
|
|
56
|
+
if (-not (Test-Path -LiteralPath $StatePath)) { throw ('state not found: ' + $StatePath) }
|
|
57
|
+
|
|
58
|
+
Add-Type -TypeDefinition @'
|
|
59
|
+
using System;
|
|
60
|
+
using System.Runtime.InteropServices;
|
|
61
|
+
public static class FarmWebViewWin32 {
|
|
62
|
+
public delegate bool EnumProc(IntPtr hWnd, IntPtr lParam);
|
|
63
|
+
[DllImport("user32.dll")] public static extern int GetWindowLong(IntPtr hWnd, int nIndex);
|
|
64
|
+
[DllImport("user32.dll")] public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
|
|
65
|
+
[DllImport("user32.dll")] public static extern bool EnumChildWindows(IntPtr hWnd, EnumProc cb, IntPtr lParam);
|
|
66
|
+
[DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr after, int x, int y, int cx, int cy, uint flags);
|
|
67
|
+
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
|
|
68
|
+
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc cb, IntPtr lParam);
|
|
69
|
+
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetWindowTextW(IntPtr hWnd, System.Text.StringBuilder s, int n);
|
|
70
|
+
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
|
|
71
|
+
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
|
|
72
|
+
[DllImport("user32.dll", EntryPoint="SetWindowLongPtrW", SetLastError=true)] public static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr value);
|
|
73
|
+
public const int GWLP_HWNDPARENT = -8;
|
|
74
|
+
public static IntPtr FindPet(bool visibleOnly) {
|
|
75
|
+
IntPtr found = IntPtr.Zero;
|
|
76
|
+
EnumWindows(delegate(IntPtr h, IntPtr l) {
|
|
77
|
+
if (visibleOnly && !IsWindowVisible(h)) return true;
|
|
78
|
+
var sb = new System.Text.StringBuilder(512);
|
|
79
|
+
GetWindowTextW(h, sb, 512);
|
|
80
|
+
if (sb.ToString().IndexOf("桌宠") < 0) return true;
|
|
81
|
+
uint pid; GetWindowThreadProcessId(h, out pid);
|
|
82
|
+
try {
|
|
83
|
+
if (System.Diagnostics.Process.GetProcessById((int)pid).ProcessName.StartsWith("Coopanion")) { found = h; return false; }
|
|
84
|
+
} catch { }
|
|
85
|
+
return true;
|
|
86
|
+
}, IntPtr.Zero);
|
|
87
|
+
return found;
|
|
88
|
+
}
|
|
89
|
+
public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
|
|
90
|
+
public static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
|
|
91
|
+
public const uint SWP_NOSIZE = 0x1;
|
|
92
|
+
public const uint SWP_NOMOVE = 0x2;
|
|
93
|
+
public const uint SWP_NOACTIVATE = 0x10;
|
|
94
|
+
const int GWL_EXSTYLE = -20;
|
|
95
|
+
const int WS_EX_TRANSPARENT = 0x20;
|
|
96
|
+
public static bool TopMost(IntPtr hWnd) {
|
|
97
|
+
if (hWnd == IntPtr.Zero) return false;
|
|
98
|
+
return SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | 0x40);
|
|
99
|
+
}
|
|
100
|
+
public static void MakeClickThrough(IntPtr hWnd) {
|
|
101
|
+
if (hWnd == IntPtr.Zero) return;
|
|
102
|
+
int ex = GetWindowLong(hWnd, GWL_EXSTYLE);
|
|
103
|
+
SetWindowLong(hWnd, GWL_EXSTYLE, ex | WS_EX_TRANSPARENT);
|
|
104
|
+
EnumChildWindows(hWnd, (h, l) => {
|
|
105
|
+
int e = GetWindowLong(h, GWL_EXSTYLE);
|
|
106
|
+
SetWindowLong(h, GWL_EXSTYLE, e | WS_EX_TRANSPARENT);
|
|
107
|
+
return true;
|
|
108
|
+
}, IntPtr.Zero);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
'@
|
|
112
|
+
|
|
113
|
+
$script:owner = [IntPtr]::Zero
|
|
114
|
+
function Update-PetZOrder {
|
|
115
|
+
try {
|
|
116
|
+
$h = (New-Object System.Windows.Interop.WindowInteropHelper($win)).Handle
|
|
117
|
+
if ($h -eq [IntPtr]::Zero) { return }
|
|
118
|
+
$pet = [FarmWebViewWin32]::FindPet($false)
|
|
119
|
+
if ($pet -eq [IntPtr]::Zero) { return }
|
|
120
|
+
if ($Layer -eq 'top') {
|
|
121
|
+
if ($script:owner -ne $pet) {
|
|
122
|
+
[void][FarmWebViewWin32]::SetWindowLongPtr($h, [FarmWebViewWin32]::GWLP_HWNDPARENT, $pet)
|
|
123
|
+
$script:owner = $pet
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
[void][FarmWebViewWin32]::SetWindowPos($h, $pet, 0, 0, 0, 0,
|
|
127
|
+
([FarmWebViewWin32]::SWP_NOMOVE -bor [FarmWebViewWin32]::SWP_NOSIZE -bor [FarmWebViewWin32]::SWP_NOACTIVATE))
|
|
128
|
+
}
|
|
129
|
+
} catch { }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
$state = Read-State
|
|
133
|
+
$h = [Math]::Min(800, [Math]::Max(120, $(if ($null -ne $state.stripHeight) { [int]$state.stripHeight } else { $StripHeight })))
|
|
134
|
+
$wa = [System.Windows.SystemParameters]::WorkArea
|
|
135
|
+
|
|
136
|
+
$win = New-Object System.Windows.Window
|
|
137
|
+
$win.Width = [Math]::Max(1, $wa.Width)
|
|
138
|
+
$win.Height = $h
|
|
139
|
+
$win.Left = $wa.Left
|
|
140
|
+
$win.Top = $wa.Top + $wa.Height - $h
|
|
141
|
+
$win.WindowStyle = 'None'
|
|
142
|
+
$win.AllowsTransparency = $true
|
|
143
|
+
$win.Background = [System.Windows.Media.Brushes]::Transparent
|
|
144
|
+
$win.Topmost = $true
|
|
145
|
+
$win.ShowInTaskbar = $false
|
|
146
|
+
$win.ResizeMode = 'NoResize'
|
|
147
|
+
$win.Focusable = $false
|
|
148
|
+
|
|
149
|
+
$wv = New-Object Microsoft.Web.WebView2.Wpf.WebView2
|
|
150
|
+
$wv.DefaultBackgroundColor = [System.Drawing.Color]::Transparent
|
|
151
|
+
$props = New-Object Microsoft.Web.WebView2.Wpf.CoreWebView2CreationProperties
|
|
152
|
+
$ud = Join-Path (Split-Path -Parent $StatePath) ('webview2-' + $Layer)
|
|
153
|
+
New-Item -ItemType Directory -Path $ud -Force | Out-Null
|
|
154
|
+
$props.UserDataFolder = $ud
|
|
155
|
+
$wv.CreationProperties = $props
|
|
156
|
+
$win.Content = $wv
|
|
157
|
+
|
|
158
|
+
$wv.add_NavigationCompleted({
|
|
159
|
+
param($s, $e)
|
|
160
|
+
Write-Log ('navigation completed success=' + $e.IsSuccess + ' status=' + $e.WebErrorStatus)
|
|
161
|
+
})
|
|
162
|
+
$wv.add_CoreWebView2InitializationCompleted({
|
|
163
|
+
param($s, $e)
|
|
164
|
+
Write-Log ('core initialized success=' + $e.IsSuccess)
|
|
165
|
+
if ($e.IsSuccess) {
|
|
166
|
+
try { $wv.CoreWebView2.add_ConsoleMessageReceived({ param($cs, $ce) Write-Log ('console: ' + $ce.Message) }) } catch { }
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
Write-Pid
|
|
171
|
+
$win.Show()
|
|
172
|
+
|
|
173
|
+
$script:coreTask = $null
|
|
174
|
+
$script:navigated = $false
|
|
175
|
+
$script:lastMtime = 0
|
|
176
|
+
$script:lastHeight = $h
|
|
177
|
+
$script:lastLease = Get-Date
|
|
178
|
+
$script:lastStateMtime = (Get-Item -LiteralPath $StatePath).LastWriteTimeUtc
|
|
179
|
+
$script:bootApply = 30
|
|
180
|
+
$script:zTick = 0
|
|
181
|
+
|
|
182
|
+
$timer = New-Object System.Windows.Threading.DispatcherTimer
|
|
183
|
+
$timer.Interval = [TimeSpan]::FromMilliseconds(100)
|
|
184
|
+
$timer.add_Tick({
|
|
185
|
+
try {
|
|
186
|
+
if ($null -eq $script:coreTask) {
|
|
187
|
+
$script:coreTask = $wv.EnsureCoreWebView2Async()
|
|
188
|
+
Write-Log 'initializing'
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
if ($script:coreTask.Status -eq 'Faulted') {
|
|
192
|
+
Write-Log ('init failed: ' + $script:coreTask.Exception.InnerException.Message)
|
|
193
|
+
$win.Close()
|
|
194
|
+
[System.Windows.Threading.Dispatcher]::ExitAllFrames()
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
if ($script:coreTask.Status -ne 'RanToCompletion') { return }
|
|
198
|
+
if (-not $script:navigated) {
|
|
199
|
+
$script:navigated = $true
|
|
200
|
+
$wv.CoreWebView2.Settings.AreDefaultContextMenusEnabled = $false
|
|
201
|
+
$wv.CoreWebView2.Settings.AreDevToolsEnabled = $false
|
|
202
|
+
$wv.CoreWebView2.SetVirtualHostNameToFolderMapping('farm.local', $HomeDir, [Microsoft.Web.WebView2.Core.CoreWebView2HostResourceAccessKind]::Allow)
|
|
203
|
+
$wv.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("window.__farmHost=true;window.__farmAssetsBase='https://farm.local/assets/sprites/';") | Out-Null
|
|
204
|
+
$wv.Source = [Uri]('https://farm.local/overlay-electron/renderer.html?layer=' + $Layer)
|
|
205
|
+
$hwnd = (New-Object System.Windows.Interop.WindowInteropHelper($win)).Handle
|
|
206
|
+
[FarmWebViewWin32]::MakeClickThrough($hwnd)
|
|
207
|
+
$top = [FarmWebViewWin32]::TopMost($hwnd)
|
|
208
|
+
Update-PetZOrder
|
|
209
|
+
Write-Log ('navigating layer=' + $Layer + ' hwnd=' + $hwnd + ' top=' + $top + ' visible=' + $win.IsVisible + ' top=' + $win.Top + ' left=' + $win.Left + ' w=' + $win.ActualWidth + ' h=' + $win.ActualHeight)
|
|
210
|
+
$rc = New-Object FarmWebViewWin32+RECT
|
|
211
|
+
if ([FarmWebViewWin32]::GetWindowRect($hwnd, [ref]$rc)) {
|
|
212
|
+
Write-Log ('window rect ' + $rc.Left + ',' + $rc.Top + ' ' + ($rc.Right - $rc.Left) + 'x' + ($rc.Bottom - $rc.Top))
|
|
213
|
+
}
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
$script:zTick++
|
|
218
|
+
if (($script:zTick % 5) -eq 0) { Update-PetZOrder }
|
|
219
|
+
$mtime = (Get-Item -LiteralPath $StatePath).LastWriteTimeUtc
|
|
220
|
+
$force = $script:bootApply -gt 0
|
|
221
|
+
if ($force) { $script:bootApply-- }
|
|
222
|
+
if ($force -or $mtime -ne $script:lastStateMtime) {
|
|
223
|
+
$script:lastStateMtime = $mtime
|
|
224
|
+
$json = Get-Content -LiteralPath $StatePath -Raw -Encoding UTF8
|
|
225
|
+
if ($json) { $wv.CoreWebView2.ExecuteScriptAsync('window.__farmApplyState(' + $json + ');') | Out-Null }
|
|
226
|
+
$st = Read-State
|
|
227
|
+
$nh = [Math]::Min(800, [Math]::Max(120, $(if ($null -ne $st.stripHeight) { [int]$st.stripHeight } else { $StripHeight })))
|
|
228
|
+
if ($nh -ne $script:lastHeight) {
|
|
229
|
+
$script:lastHeight = $nh
|
|
230
|
+
$win.Height = $nh
|
|
231
|
+
$win.Top = $wa.Top + $wa.Height - $nh
|
|
232
|
+
}
|
|
233
|
+
} if (((Get-Date) - $script:lastLease).TotalSeconds -ge 5) {
|
|
234
|
+
$script:lastLease = Get-Date
|
|
235
|
+
if ($LeaseSeconds -gt 0 -and ((([DateTime]::UtcNow) - $mtime).TotalSeconds -gt $LeaseSeconds)) {
|
|
236
|
+
Write-Log 'state lease expired'
|
|
237
|
+
$win.Close()
|
|
238
|
+
[System.Windows.Threading.Dispatcher]::ExitAllFrames()
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
$hwnd2 = (New-Object System.Windows.Interop.WindowInteropHelper($win)).Handle
|
|
242
|
+
[FarmWebViewWin32]::MakeClickThrough($hwnd2)
|
|
243
|
+
if ($Layer -eq 'top') { [void][FarmWebViewWin32]::TopMost($hwnd2) }
|
|
244
|
+
Update-PetZOrder
|
|
245
|
+
}
|
|
246
|
+
} catch {
|
|
247
|
+
Write-Log ('tick failed: ' + $_.Exception.Message)
|
|
248
|
+
try { $win.Close() } catch { }
|
|
249
|
+
[System.Windows.Threading.Dispatcher]::ExitAllFrames()
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
$win.add_Closed({
|
|
253
|
+
$timer.Stop()
|
|
254
|
+
Clear-Pid
|
|
255
|
+
[System.Windows.Threading.Dispatcher]::ExitAllFrames()
|
|
256
|
+
})
|
|
257
|
+
$timer.Start()
|
|
258
|
+
[System.Windows.Threading.Dispatcher]::Run()
|
|
259
|
+
|
|
260
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Copyright (C) Microsoft Corporation. All rights reserved.
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without
|
|
4
|
+
modification, are permitted provided that the following conditions are
|
|
5
|
+
met:
|
|
6
|
+
|
|
7
|
+
* Redistributions of source code must retain the above copyright
|
|
8
|
+
notice, this list of conditions and the following disclaimer.
|
|
9
|
+
* Redistributions in binary form must reproduce the above
|
|
10
|
+
copyright notice, this list of conditions and the following disclaimer
|
|
11
|
+
in the documentation and/or other materials provided with the
|
|
12
|
+
distribution.
|
|
13
|
+
* The name of Microsoft Corporation, or the names of its contributors
|
|
14
|
+
may not be used to endorse or promote products derived from this
|
|
15
|
+
software without specific prior written permission.
|
|
16
|
+
|
|
17
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
18
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
19
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
20
|
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
21
|
+
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
22
|
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
23
|
+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
24
|
+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
25
|
+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
26
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
27
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
Binary file
|
|
Binary file
|