@lily-cooperative/custom-farm 0.38.0 → 0.38.1

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
@@ -29,6 +29,7 @@
29
29
  | **状态自定义** | 数值状态、开关状态、条件规则、消耗行为(例如饥饿时自动吃背包里的东西) |
30
30
  | **道具自定义** | 种子/农作物/消耗品/装饰的买价卖价、能不能吃、吃掉后的状态效果、图标、摆放贴图、图层、缩放 |
31
31
  | **合成表** | 1~4 个材料合成 1 个产物,桌宠可以用 `farm_craft` 合成 |
32
+ | **性能** | 图层节流:不节流(默认)/ 自动 / 节流;改完自动重启图层 |
32
33
  | **设置模板 ZIP** | 导出/导入全部设置和自定义贴图(不含存档) |
33
34
  | **环境提示词** | 发给桌宠的静态提示词,写“饿了怎么办、什么时候提醒主人”等 |
34
35
 
@@ -116,13 +117,11 @@ Coopanion 只提供打包后的 `Coopanion.exe`,没有可复用的独立 `elec
116
117
 
117
118
  ### 备份与重置
118
119
 
119
- **游戏配置** 页底部有「存档与重置」:
120
+ **游戏配置** 页最上面有「存档与重置」,不再单独占页签:
120
121
 
121
122
  - **备份存档**:把 `state.json` 和 `custom.json` 复制到 `data\farm\backups\backup-时间\`
122
123
  - **重置一切**:先自动备份,再清空 `state.json` 和 `custom.json`,World 自动重启并生成默认目录;安装路径和已上传的 PNG 文件不会被删除
123
124
 
124
- 也可以从「存档与重置」页签操作同样的按钮。
125
-
126
125
  ## 桌面图层
127
126
 
128
127
  - 图层拆成 `bg` / `top` 两个透明窗口:`bg` 在桌宠下面,`top` 是桌宠的 owned window,永远在桌宠上面。
package/dist/webui.js CHANGED
@@ -37,7 +37,7 @@ if (typeof document !== 'undefined') {
37
37
 
38
38
  /** 调 World 的 manage 面板:备份 / 重置 / 自检。 */
39
39
  async function farmManageCall(method, args = []) {
40
- const res = await fetch('/api/webui/providers/' + encodeURIComponent('world:farm') + '/panels/manage/' + method, {
40
+ const res = await fetch('/api/webui/providers/' + encodeURIComponent('world:farm') + '/panels/customize/' + method, {
41
41
  method: 'POST',
42
42
  headers: { 'content-type': 'application/json' },
43
43
  body: JSON.stringify({ args }),
@@ -219,7 +219,7 @@ function injectFarmConfigActions() {
219
219
 
220
220
  field.append(backup, reset, status);
221
221
  row.append(label, field);
222
- view.append(section, desc, row);
222
+ view.prepend(section, desc, row);
223
223
  } catch { /* 不是宿主的配置页就算了 */ }
224
224
  }
225
225
 
@@ -2912,64 +2912,8 @@ function ecologyPanel(ctx) {
2912
2912
  reload();
2913
2913
  }
2914
2914
 
2915
- function managePanel(ctx) {
2916
- const { ui } = ctx;
2917
- const root = ui.h('div', 'cf-root');
2918
- ctx.root.appendChild(root);
2919
- const sheet = ui.sheet({
2920
- title: '存档与重置',
2921
- desc: '备份会复制 state.json 和 custom.json 到 data/farm/backups;重置会先备份,再清空存档和全部自定义设置,World 随后重启。',
2922
- });
2923
- const bar = ui.rowbar();
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
- });
2937
- const backup = ui.button('备份存档', {
2938
- variant: 'primary',
2939
- onClick: async () => {
2940
- backup.disabled = true;
2941
- status.textContent = '正在备份…';
2942
- try {
2943
- const r = await ctx.invoke('backup');
2944
- status.textContent = r.text || '已备份';
2945
- } catch (err) {
2946
- status.textContent = '备份失败:' + ((err && err.message) || err);
2947
- } finally { backup.disabled = false; }
2948
- },
2949
- });
2950
- const reset = ui.button('重置一切', {
2951
- onClick: async () => {
2952
- if (!window.confirm('确定重置一切?会清空存档和全部自定义设置(会先自动备份)。')) return;
2953
- reset.disabled = true;
2954
- status.textContent = '正在重置…';
2955
- try {
2956
- const r = await ctx.invoke('reset');
2957
- status.textContent = r.text || '已重置,World 正在重启…';
2958
- setTimeout(() => { try { location.reload(); } catch { /* ignore */ } }, 3000);
2959
- } catch (err) {
2960
- status.textContent = '重置失败:' + ((err && err.message) || err);
2961
- reset.disabled = false;
2962
- }
2963
- },
2964
- });
2965
- bar.append(check, backup, reset);
2966
- sheet.body.append(bar, status);
2967
- root.appendChild(sheet.el);
2968
- }
2969
-
2970
2915
  export default {
2971
2916
  panels: {
2972
- manage: { mount: managePanel },
2973
2917
  bag: { mount: bagPanel },
2974
2918
  shop: { mount: shopPanel },
2975
2919
  customize: { mount: panel },
@@ -2978,3 +2922,4 @@ export default {
2978
2922
  items: { mount: itemsPanel },
2979
2923
  },
2980
2924
  };
2925
+
@@ -9,7 +9,8 @@ param(
9
9
  [string]$LogPath = '',
10
10
  [int]$StripHeight = 320,
11
11
  [int]$LeaseSeconds = 20,
12
- [ValidateSet('bg', 'top')][string]$Layer = 'bg'
12
+ [ValidateSet('bg', 'top')][string]$Layer = 'bg',
13
+ [ValidateSet('off', 'auto', 'on')][string]$Throttle = 'off'
13
14
  )
14
15
 
15
16
  $ErrorActionPreference = 'Stop'
@@ -64,6 +65,8 @@ public static class FarmWebViewWin32 {
64
65
  [DllImport("user32.dll")] public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
65
66
  [DllImport("user32.dll")] public static extern bool EnumChildWindows(IntPtr hWnd, EnumProc cb, IntPtr lParam);
66
67
  [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr after, int x, int y, int cx, int cy, uint flags);
68
+ [DllImport("user32.dll")] public static extern IntPtr GetWindow(IntPtr hWnd, uint cmd);
69
+ public const uint GW_HWNDPREV = 3;
67
70
  [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
68
71
  [DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc cb, IntPtr lParam);
69
72
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetWindowTextW(IntPtr hWnd, System.Text.StringBuilder s, int n);
@@ -111,6 +114,7 @@ public static class FarmWebViewWin32 {
111
114
  '@
112
115
 
113
116
  $script:owner = [IntPtr]::Zero
117
+ $script:bgPet = [IntPtr]::Zero
114
118
  function Update-PetZOrder {
115
119
  try {
116
120
  $h = (New-Object System.Windows.Interop.WindowInteropHelper($win)).Handle
@@ -123,8 +127,11 @@ function Update-PetZOrder {
123
127
  $script:owner = $pet
124
128
  }
125
129
  } else {
126
- [void][FarmWebViewWin32]::SetWindowPos($h, $pet, 0, 0, 0, 0,
127
- ([FarmWebViewWin32]::SWP_NOMOVE -bor [FarmWebViewWin32]::SWP_NOSIZE -bor [FarmWebViewWin32]::SWP_NOACTIVATE))
130
+ if ($script:bgPet -ne $pet -or [FarmWebViewWin32]::GetWindow($h, [FarmWebViewWin32]::GW_HWNDPREV) -ne $pet) {
131
+ [void][FarmWebViewWin32]::SetWindowPos($h, $pet, 0, 0, 0, 0,
132
+ ([FarmWebViewWin32]::SWP_NOMOVE -bor [FarmWebViewWin32]::SWP_NOSIZE -bor [FarmWebViewWin32]::SWP_NOACTIVATE))
133
+ $script:bgPet = $pet
134
+ }
128
135
  }
129
136
  } catch { }
130
137
  }
@@ -152,6 +159,9 @@ $props = New-Object Microsoft.Web.WebView2.Wpf.CoreWebView2CreationProperties
152
159
  $ud = Join-Path (Split-Path -Parent $StatePath) ('webview2-' + $Layer)
153
160
  New-Item -ItemType Directory -Path $ud -Force | Out-Null
154
161
  $props.UserDataFolder = $ud
162
+ if ($Throttle -eq 'off') {
163
+ $props.AdditionalBrowserArguments = '--disable-backgrounding-occluded-windows --disable-renderer-backgrounding --disable-features=CalculateNativeWinOcclusion'
164
+ }
155
165
  $wv.CreationProperties = $props
156
166
  $win.Content = $wv
157
167
 
@@ -14,6 +14,8 @@ const screensPath = path.join(path.dirname(statePath), 'screens.json');
14
14
  const assetsDir = path.resolve(arg('assets-dir', path.join(__dirname, '..', 'assets', 'sprites')));
15
15
  const logPath = arg('log', '');
16
16
  const pidPath = arg('pid', '');
17
+ const rawThrottle = String(arg('throttle', 'off'));
18
+ const THROTTLE = ['off', 'auto', 'on'].includes(rawThrottle) ? rawThrottle : 'off';
17
19
  const logStream = logPath ? fs.createWriteStream(logPath, { flags: 'a' }) : null;
18
20
  function log(s) {
19
21
  const line = new Date().toISOString() + ' [electron] ' + s + '\n';
@@ -127,11 +129,12 @@ function createWindow(layer) {
127
129
  webPreferences: {
128
130
  nodeIntegration: true,
129
131
  contextIsolation: false,
130
- backgroundThrottling: false,
132
+ backgroundThrottling: THROTTLE !== 'off',
131
133
  },
132
134
  });
133
135
  win.setAlwaysOnTop(true, 'screen-saver');
134
136
  win.setIgnoreMouseEvents(true, { forward: true });
137
+ if (THROTTLE === 'off') win.webContents.setBackgroundThrottling(false);
135
138
  try { win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); } catch { }
136
139
  win.loadFile(path.join(__dirname, 'renderer.html'), { query: { state: statePath, assets: assetsDir, layer } });
137
140
  win.once('ready-to-show', () => {
@@ -162,6 +165,11 @@ function fit() {
162
165
  }
163
166
 
164
167
  app.commandLine.appendSwitch('enable-transparent-visuals');
168
+ if (THROTTLE === 'off') {
169
+ app.commandLine.appendSwitch('disable-backgrounding-occluded-windows');
170
+ app.commandLine.appendSwitch('disable-renderer-backgrounding');
171
+ app.commandLine.appendSwitch('disable-features', 'CalculateNativeWinOcclusion');
172
+ }
165
173
 
166
174
  if (!app.requestSingleInstanceLock()) {
167
175
  app.quit();
@@ -11,6 +11,7 @@ $ErrorActionPreference = 'SilentlyContinue'
11
11
  $bgPtr = [IntPtr]([Int64]$Bg)
12
12
  $topPtr = [IntPtr]([Int64]$Top)
13
13
  $script:owner = [IntPtr]::Zero
14
+ $script:bgPet = [IntPtr]::Zero
14
15
 
15
16
  Add-Type -TypeDefinition @'
16
17
  using System;
@@ -25,6 +26,8 @@ public static class FarmZ {
25
26
  [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
26
27
  [DllImport("user32.dll", EntryPoint="SetWindowLongPtrW", SetLastError=true)] public static extern IntPtr SetWindowLongPtr(IntPtr h, int n, IntPtr v);
27
28
  [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr h, IntPtr after, int x, int y, int cx, int cy, uint flags);
29
+ [DllImport("user32.dll")] public static extern IntPtr GetWindow(IntPtr h, uint cmd);
30
+ public const uint GW_HWNDPREV = 3;
28
31
  public const int GWLP_HWNDPARENT = -8;
29
32
  public const uint SWP_NOSIZE = 0x1;
30
33
  public const uint SWP_NOMOVE = 0x2;
@@ -54,11 +57,14 @@ while ([FarmZ]::IsWindow($bgPtr) -and [FarmZ]::IsWindow($topPtr)) {
54
57
  [void][FarmZ]::SetWindowLongPtr($topPtr, [FarmZ]::GWLP_HWNDPARENT, $pet)
55
58
  $script:owner = $pet
56
59
  }
57
- [void][FarmZ]::SetWindowPos($bgPtr, $pet, 0, 0, 0, 0,
58
- ([FarmZ]::SWP_NOMOVE -bor [FarmZ]::SWP_NOSIZE -bor [FarmZ]::SWP_NOACTIVATE))
60
+ if ($script:bgPet -ne $pet -or [FarmZ]::GetWindow($bgPtr, [FarmZ]::GW_HWNDPREV) -ne $pet) {
61
+ [void][FarmZ]::SetWindowPos($bgPtr, $pet, 0, 0, 0, 0,
62
+ ([FarmZ]::SWP_NOMOVE -bor [FarmZ]::SWP_NOSIZE -bor [FarmZ]::SWP_NOACTIVATE))
63
+ $script:bgPet = $pet
64
+ }
59
65
  }
60
- Start-Sleep -Milliseconds 500
66
+ Start-Sleep -Milliseconds 1000
61
67
  }
62
-
63
-
64
-
68
+
69
+
70
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lily-cooperative/custom-farm",
3
- "version": "0.38.0",
3
+ "version": "0.38.1",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "publishConfig": {
package/src/farm.js CHANGED
@@ -55,6 +55,8 @@ export const DEFAULTS = {
55
55
  overlayEngine: 'auto',
56
56
  /** 备用 electron.exe / electron 可执行文件路径;空 = 自动找。 */
57
57
  electronPath: '',
58
+ /** 图层节流:off = 不节流(默认);auto = 交给 Chromium;on = 允许后台/遮挡节流。 */
59
+ throttle: 'off',
58
60
  /** 最多同时种几棵(屏幕上不重叠地放得下的上限) */
59
61
  maxPlants: 12,
60
62
  /** 屏幕逻辑宽度,用来把"像素宽"换算成 x 间距 */
package/src/index.js CHANGED
@@ -76,6 +76,14 @@ export const farmDefinition = {
76
76
  { value: 'wpf', label: zh ? 'WPF/WebView2(不需要 Electron)' : 'WPF/WebView2 (no Electron)' },
77
77
  ];
78
78
  }
79
+ if (kind === 'farm-throttle') {
80
+ const zh = String(language).toLowerCase().startsWith('zh');
81
+ return [
82
+ { value: 'off', label: zh ? '不节流(默认,推荐)' : 'Off (default, recommended)' },
83
+ { value: 'auto', label: zh ? '自动(交给 Chromium)' : 'Auto (Chromium decides)' },
84
+ { value: 'on', label: zh ? '节流(后台/遮挡时降帧)' : 'On (throttle when backgrounded/occluded)' },
85
+ ];
86
+ }
79
87
  return [];
80
88
  },
81
89
  describe: {
package/src/world.js CHANGED
@@ -334,6 +334,24 @@ function farmConfigGroups(language = 'zh', defaultHomePath = '') {
334
334
  },
335
335
  },
336
336
  },
337
+ {
338
+ id: 'world:farm/performance',
339
+ owner: 'world:farm',
340
+ schema: {
341
+ type: 'object',
342
+ title: t('性能', 'Performance'),
343
+ description: t('桌面图层的后台/遮挡节流。默认不节流,收获和动画更跟手;如果 CPU/GPU 占用高,可改成自动或节流。', 'Background/occlusion throttling for the desktop layers. Off by default for responsive harvest animations; switch to Auto or Throttle if CPU/GPU usage is high.'),
344
+ properties: {
345
+ 'worlds.farm.throttle': {
346
+ type: 'string',
347
+ title: t('节流', 'Throttling'),
348
+ description: t('不节流:禁用后台/遮挡节流(推荐)。自动:交给 Chromium。节流:允许后台和遮挡时降帧。', 'Off disables background/occlusion throttling (recommended). Auto lets Chromium decide. On allows lower frame rates when backgrounded or occluded.'),
349
+ 'x-options': 'farm-throttle',
350
+ 'x-hot': true,
351
+ },
352
+ },
353
+ },
354
+ },
337
355
  ];
338
356
  }
339
357
 
@@ -410,6 +428,7 @@ export class FarmWorld {
410
428
  screen: String(this.cfg?.screen ?? ''),
411
429
  overlayEngine: ['auto', 'electron', 'wpf'].includes(String(this.cfg?.overlayEngine ?? 'auto')) ? String(this.cfg?.overlayEngine ?? 'auto') : 'auto',
412
430
  electronPath: String(this.cfg?.electronPath ?? ''),
431
+ throttle: ['off', 'auto', 'on'].includes(String(this.cfg?.throttle ?? 'off')) ? String(this.cfg?.throttle ?? 'off') : 'off',
413
432
  maxPlants: this.#num('maxPlants', 1, 24),
414
433
  screenWidth,
415
434
  zoom: this.#num('zoom', 1, 8),
@@ -1591,11 +1610,6 @@ export class FarmWorld {
1591
1610
  { label: '天数', value: started ? '第 ' + s.day + ' 天' : '—', tone: 'plain' },
1592
1611
  ],
1593
1612
  panels: [{
1594
- id: 'manage',
1595
- title: '存档与重置',
1596
- description: '备份存档,或把存档和全部自定义设置重置为默认。',
1597
- getMethods: ['state'],
1598
- }, {
1599
1613
  id: 'bag',
1600
1614
  title: '背包',
1601
1615
  description: '背包、农场预览、摆放道具与手动合成;装饰在这里输入坐标放置。',
@@ -1642,33 +1656,7 @@ export class FarmWorld {
1642
1656
  async invoke(panel, method, args = []) {
1643
1657
  return this.#serial(async () => {
1644
1658
  const cfg = this.#cfgAll();
1645
- if (panel === 'manage') {
1646
- if (method === 'backup') return this.#backupNow();
1647
- if (method === 'reset') return this.#resetAll();
1648
- if (method === 'check') return this.#envCheck();
1649
- if (method === 'setElectronPath') {
1650
- const file = String(args?.[0] ?? '').trim();
1651
- if (file && !existsSync(file)) throw new Error('找不到这个 Electron 文件:' + file);
1652
- this.persistConfig?.({ electronPath: file });
1653
- try { this.cfg.electronPath = file; } catch { /* cfg 可能是只读代理 */ }
1654
- await this.#stopLayers('Electron 路径变更');
1655
- this.#ensureLayers('Electron 路径变更');
1656
- this.#layerEngineKey = this.#engine() + '|' + file;
1657
- return this.#envCheck();
1658
- }
1659
- if (method === 'setEngine') {
1660
- const mode = String(args?.[0] ?? 'auto');
1661
- if (!['auto', 'electron', 'wpf'].includes(mode)) throw new Error('不认识的图层引擎:' + mode);
1662
- this.persistConfig?.({ overlayEngine: mode });
1663
- try { this.cfg.overlayEngine = mode; } catch { /* cfg 可能是只读代理 */ }
1664
- await this.#stopLayers('图层引擎变更');
1665
- this.#ensureLayers('图层引擎变更');
1666
- this.#layerEngineKey = this.#engine() + '|' + String(this.cfg?.electronPath ?? '');
1667
- return this.#envCheck();
1668
- }
1669
- if (method === 'state') return { ok: true };
1670
- throw new Error('不认识的存档操作:' + method);
1671
- }
1659
+ if (panel === 'manage') return this.#manageAction(method, args);
1672
1660
  if (panel === 'status') return this.#statusInvoke(method, args, cfg);
1673
1661
  if (panel === 'bag') return this.#bagInvoke(method, args);
1674
1662
  if (panel === 'shop') return this.#shopInvoke(method, args);
@@ -1729,8 +1717,39 @@ export class FarmWorld {
1729
1717
  };
1730
1718
  }
1731
1719
 
1720
+ async #manageAction(method, args = []) {
1721
+ if (method === 'backup') return this.#backupNow();
1722
+ if (method === 'reset') return this.#resetAll();
1723
+ if (method === 'check') return this.#envCheck();
1724
+ if (method === 'setElectronPath') {
1725
+ const file = String(args?.[0] ?? '').trim();
1726
+ if (file && !existsSync(file)) throw new Error('找不到这个 Electron 文件:' + file);
1727
+ this.persistConfig?.({ electronPath: file });
1728
+ try { this.cfg.electronPath = file; } catch { /* cfg 可能是只读代理 */ }
1729
+ await this.#stopLayers('Electron 路径变更');
1730
+ this.#ensureLayers('Electron 路径变更');
1731
+ this.#layerEngineKey = this.#engine() + '|' + file + '|' + String(this.cfg?.throttle ?? 'off');
1732
+ return this.#envCheck();
1733
+ }
1734
+ if (method === 'setEngine') {
1735
+ const mode = String(args?.[0] ?? 'auto');
1736
+ if (!['auto', 'electron', 'wpf'].includes(mode)) throw new Error('不认识的图层引擎:' + mode);
1737
+ this.persistConfig?.({ overlayEngine: mode });
1738
+ try { this.cfg.overlayEngine = mode; } catch { /* cfg 可能是只读代理 */ }
1739
+ await this.#stopLayers('图层引擎变更');
1740
+ this.#ensureLayers('图层引擎变更');
1741
+ this.#layerEngineKey = this.#engine() + '|' + String(this.cfg?.electronPath ?? '') + '|' + String(this.cfg?.throttle ?? 'off');
1742
+ return this.#envCheck();
1743
+ }
1744
+ if (method === 'state') return { ok: true };
1745
+ throw new Error('不认识的存档操作:' + method);
1746
+ }
1747
+
1732
1748
  /** 「农场自定义」页的数据面。统一返回最新 snapshot,面板不用再读一次。 */
1733
1749
  async #customInvoke(method, args, cfg) {
1750
+ if (['backup', 'reset', 'check', 'setEngine', 'setElectronPath'].includes(method)) {
1751
+ return this.#manageAction(method, args);
1752
+ }
1734
1753
  if (method === 'sprite') {
1735
1754
  const name = String(args?.[0] ?? '');
1736
1755
  return { $binary: this.#catalogOrThrow().spriteBase64(name) };
@@ -2413,7 +2432,7 @@ export class FarmWorld {
2413
2432
  }
2414
2433
 
2415
2434
  async #syncLayerEngine() {
2416
- const key = this.#engine() + '|' + String(this.cfg?.electronPath ?? '');
2435
+ const key = this.#engine() + '|' + String(this.cfg?.electronPath ?? '') + '|' + String(this.cfg?.throttle ?? 'off');
2417
2436
  if (!this.#layerEngineKey) { this.#layerEngineKey = key; return; }
2418
2437
  if (key === this.#layerEngineKey) return;
2419
2438
  this.#layerEngineKey = key;
@@ -2529,7 +2548,7 @@ export class FarmWorld {
2529
2548
  try {
2530
2549
  const launcher = join(paths.electronDir, 'launch.js');
2531
2550
  const pidFile = join(this.dataDir, 'farm', 'electron.pid');
2532
- const child = spawn(process.execPath, [launcher, '--electron', exe, '--state-path', paths.overlayState, '--assets-dir', paths.assetsDir, '--pid', pidFile, '--log', join(dirname(this.overlayFile()), 'electron.log')], { detached: true, stdio: 'ignore', windowsHide: true, env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' } });
2551
+ const child = spawn(process.execPath, [launcher, '--electron', exe, '--throttle', String(this.cfg?.throttle ?? 'off'), '--state-path', paths.overlayState, '--assets-dir', paths.assetsDir, '--pid', pidFile, '--log', join(dirname(this.overlayFile()), 'electron.log')], { detached: true, stdio: 'ignore', windowsHide: true, env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' } });
2533
2552
  child.unref();
2534
2553
  this.#layerChild = child;
2535
2554
  this.#layerAt = Date.now();
@@ -2568,7 +2587,7 @@ export class FarmWorld {
2568
2587
  const child = spawn('powershell.exe',
2569
2588
  ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-WindowStyle', 'Hidden', '-File', script,
2570
2589
  '-StatePath', paths.overlayState, '-HomeDir', paths.home, '-AssetsDir', paths.assetsDir,
2571
- '-PidPath', pidFile, '-LogPath', logFile, '-Layer', layer],
2590
+ '-PidPath', pidFile, '-LogPath', logFile, '-Layer', layer, '-Throttle', String(this.cfg?.throttle ?? 'off')],
2572
2591
  { stdio: 'ignore', windowsHide: true });
2573
2592
  child.unref();
2574
2593
  try { writeFileSync(pidFile, String(child.pid)); } catch { }
@@ -2807,8 +2826,11 @@ export class FarmWorld {
2807
2826
  this.#started = true;
2808
2827
  this.#schedule();
2809
2828
  this.#cmdTimer = setInterval(() => { void this.#drainCommands(); }, 1000);
2829
+ if (this.persistConfig && this.cfg?.throttle === undefined) {
2830
+ try { this.persistConfig({ throttle: 'off' }); this.cfg.throttle = 'off'; } catch { /* 默认仍是 off */ }
2831
+ }
2810
2832
  this.#hidden = this.#flag('hideFarm');
2811
- this.#layerEngineKey = this.#engine() + '|' + String(this.cfg?.electronPath ?? '');
2833
+ this.#layerEngineKey = this.#engine() + '|' + String(this.cfg?.electronPath ?? '') + '|' + String(this.cfg?.throttle ?? 'off');
2812
2834
  if (!this.#hidden) { try { this.#ensureLayers(); } catch { /* 忽略 */ } }
2813
2835
  this.#envCheckPending = Boolean(this.#host?.pushEvent);
2814
2836
  // 不做周期巡检:巡检会触发"杀掉再起",和正在启动的进程互相打断(这就是之前反复缺层的原因)