@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 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
- - **Windows 10/11**:默认 Electron/HTML 图层;Electron 不可用时回退 WPF。
105
- - **macOS / Linux**:核心 World、WebUI 和 Electron 图层可用;没有 Windows WPF 回退。
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
- - 独立 WPF 背包已移除;背包、商店、农场预览、摆放和手动合成都在 WebUI。
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
- - Electron `^45.1.0`(桌面图层依赖)
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 start = () => {
23
+ const refreshFarmConfigUi = () => {
24
24
  renameFarmConfigTab();
25
+ injectFarmEnvCheck();
25
26
  injectFarmConfigActions();
26
- new MutationObserver(() => { renameFarmConfigTab(); injectFarmConfigActions(); }).observe(document.body, { childList: true, subtree: true, characterData: true });
27
- window.addEventListener('hashchange', () => { renameFarmConfigTab(); injectFarmConfigActions(); });
28
- setInterval(() => { renameFarmConfigTab(); injectFarmConfigActions(); }, 1000);
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.