@lily-cooperative/custom-farm 0.34.1 → 0.35.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/src/index.js CHANGED
@@ -63,6 +63,7 @@ export const farmDefinition = {
63
63
  dataDir: ctx.dataDir,
64
64
  botDir: ctx.botDir,
65
65
  packageDir: ctx.packageDir,
66
+ persist: (patch) => ctx.persist(patch),
66
67
  }),
67
68
  describe: {
68
69
  tools: describeTools(FARM_TOOL_DECLS),
package/src/paths.js CHANGED
@@ -1,50 +1,47 @@
1
- /**
2
- * 路径解析:所有东西都从扩展自己的位置推出来,不写死本机绝对路径。
3
- *
4
- * <Pet>\custom-farm\src\paths.js ← 本文件
5
- * <Pet>\overlay\overlay.ps1
6
- * <Pet>\bag\bag.ps1
7
- * <Pet>\assets\sprites\
8
- *
9
- * 所以「整个 Pet 文件夹」拷到另一台机器(或换个盘)都能直接用。
10
- * 扩展被复制进 Coopanion 的 extensions 目录(而不是 link)时,用环境变量
11
- * PET_HOME 或 World 配置里的 petRoot 指定 <Pet> 在哪。
12
- */
13
- import { existsSync } from 'node:fs';
14
- import { dirname, join, resolve } from 'node:path';
15
- import { fileURLToPath } from 'node:url';
16
-
17
- const SRC_DIR = dirname(fileURLToPath(import.meta.url));
18
- export const EXTENSION_DIR = resolve(SRC_DIR, '..');
19
-
20
- const looksLikePetHome = (dir) => Boolean(dir) && (
21
- existsSync(join(dir, 'overlay', 'overlay.ps1')) ||
22
- (existsSync(join(dir, 'overlay-electron', 'main.js')) && existsSync(join(dir, 'assets', 'sprites')))
23
- );
24
-
25
- /** 找到 <Pet> 根目录;找不到就返回 null(图层和背包会被自动关掉,不影响农场本身)。 */
26
- export function resolvePetHome(cfgRoot) {
27
- const candidates = [];
28
- if (typeof cfgRoot === 'string' && cfgRoot.trim()) candidates.push(resolve(cfgRoot.trim()));
29
- const env = process.env.PET_HOME;
30
- if (env && env.trim()) candidates.push(resolve(env.trim()));
31
- candidates.push(EXTENSION_DIR);
32
- candidates.push(resolve(EXTENSION_DIR, '..'));
33
- for (const dir of candidates) {
34
- if (looksLikePetHome(dir)) return dir;
35
- }
36
- return null;
37
- }
38
-
39
- export function makePaths(petHome) {
40
- const home = petHome ?? resolve(EXTENSION_DIR, '..');
41
- return {
42
- home,
43
- overlayDir: join(home, 'overlay'),
44
- overlayScript: join(home, 'overlay', 'overlay.ps1'),
45
- overlayState: join(home, 'overlay', 'runtime', 'state.json'),
46
- electronDir: join(home, 'overlay-electron'),
47
- bagScript: join(home, 'bag', 'bag.ps1'),
48
- assetsDir: join(home, 'assets', 'sprites'),
49
- };
50
- }
1
+ /**
2
+ * 路径解析:home 目录里放 overlay / overlay-electron / assets。
3
+ *
4
+ * 默认 home 是 <dataDir>/custom-farm;扩展从 npm 安装时,包内也自带这三个目录,
5
+ * 首次启动会把它们复制过去。之后在「游戏配置 → 安装」里改路径,World 会把 home
6
+ * 搬到新位置并重启桌面图层。
7
+ */
8
+ import { existsSync } from 'node:fs';
9
+ import { dirname, join, resolve } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const SRC_DIR = dirname(fileURLToPath(import.meta.url));
13
+ export const EXTENSION_DIR = resolve(SRC_DIR, '..');
14
+
15
+ export const HOME_NAME = 'custom-farm';
16
+
17
+ /** 一个目录是否已经有可用的桌面图层和素材。 */
18
+ export function looksLikeHome(dir) {
19
+ if (!dir) return false;
20
+ return existsSync(join(dir, 'overlay', 'overlay.ps1'))
21
+ || (existsSync(join(dir, 'overlay-electron', 'main.js')) && existsSync(join(dir, 'assets', 'sprites')));
22
+ }
23
+
24
+ /** 默认 home:Coopanion 数据目录下的 custom-farm。 */
25
+ export function defaultHome(dataDir) {
26
+ return join(dataDir, HOME_NAME);
27
+ }
28
+
29
+ /** 扩展包自带或开发目录里的 overlay / assets 源。 */
30
+ export function bundleHome(packageDir) {
31
+ const candidates = [packageDir, resolve(packageDir, '..'), EXTENSION_DIR, resolve(EXTENSION_DIR, '..')];
32
+ for (const dir of candidates) {
33
+ if (looksLikeHome(dir)) return dir;
34
+ }
35
+ return packageDir;
36
+ }
37
+
38
+ export function makePaths(home) {
39
+ return {
40
+ home,
41
+ overlayDir: join(home, 'overlay'),
42
+ overlayScript: join(home, 'overlay', 'overlay.ps1'),
43
+ overlayState: join(home, 'overlay', 'runtime', 'state.json'),
44
+ electronDir: join(home, 'overlay-electron'),
45
+ assetsDir: join(home, 'assets', 'sprites'),
46
+ };
47
+ }
package/src/shop.js CHANGED
@@ -101,7 +101,7 @@ export function buyPriceOf(itemId) {
101
101
  /**
102
102
  * 家具总开关:2026-10-27 起暂时下架(摆放方案还没定)。
103
103
  * false = 商店不列、farm_buy 不卖家具、farm_place 报错;存档里已经有的家具保留不删。
104
- * 恢复:改回 true,并把 bag\bag.ps1 里的 $ShowFurniture 改成 $true。
104
+ * 恢复:改回 true。
105
105
  */
106
106
  export const FURNITURE_ENABLED = false;
107
107
 
package/src/world.js CHANGED
@@ -16,17 +16,17 @@
16
16
  * 「新的一天」要 dailyEvent=true 才推,绝不因刷新本身唤醒模型。
17
17
  */
18
18
  import { spawn } from 'node:child_process';
19
- import { existsSync, readFileSync, unlinkSync } from 'node:fs';
19
+ import { cpSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
20
20
  import { execFileSync } from 'node:child_process';
21
21
  import { mkdir, readFile, readdir, rename, unlink, writeFile } from 'node:fs/promises';
22
- import { dirname, join } from 'node:path';
22
+ import { dirname, join, resolve, sep } from 'node:path';
23
23
  import { fileURLToPath } from 'node:url';
24
24
 
25
25
  import { CROPS, ROTATION } from './crops.js';
26
26
  import { FarmCatalog } from './catalog.js';
27
27
  import { live } from './catalog-state.js';
28
28
  import { buildZip, readZip } from './zip.js';
29
- import { makePaths, resolvePetHome } from './paths.js';
29
+ import { bundleHome, defaultHome, makePaths } from './paths.js';
30
30
  import { CROP_PRICES, FURNITURE, FURNITURE_ENABLED, SEED_BY_CROP, SEED_BY_ID, SEEDS, sellPriceOf } from './shop.js';
31
31
  import { normalizeWeathers, pickWeather, weatherById, weatherFxOf } from './weather.js';
32
32
  import {
@@ -213,12 +213,6 @@ export const FARM_TOOL_DECLS = [
213
213
  additionalProperties: false,
214
214
  },
215
215
  },
216
- {
217
- name: 'farm_bag',
218
- tags: ['write'],
219
- description: '打开(或唤到前面)背包面板。背包里是收下来的作物,可以看每种有多少;面板本身只读,东西还是靠种/收/捡进出。',
220
- parameters: { type: 'object', properties: {} },
221
- },
222
216
  {
223
217
  name: 'farm_collect',
224
218
  tags: ['write'],
@@ -258,7 +252,7 @@ export const FARM_TOOL_DECLS = [
258
252
  * 键是 `worlds.farm.*`,必须与 `DEFAULTS` 里的键一一对应(dry-mount 会检查)。
259
253
  * 都是 hot:写回 config.json 后,下一次 tick 就按新值跑,不需要重启。
260
254
  */
261
- function farmConfigGroups(language = 'zh') {
255
+ function farmConfigGroups(language = 'zh', defaultHomePath = '') {
262
256
  const zh = String(language).toLowerCase().startsWith('zh');
263
257
  const t = (a, b) => (zh ? a : b);
264
258
  const num = (title, description, minimum, maximum, suffix) => ({
@@ -270,8 +264,30 @@ function farmConfigGroups(language = 'zh') {
270
264
  ...(suffix ? { 'x-suffix': suffix } : {}),
271
265
  });
272
266
  const bool = (title, description) => ({ type: 'boolean', title, description, 'x-hot': true });
267
+ const dir = (title, description) => ({
268
+ type: 'string',
269
+ title,
270
+ description,
271
+ 'x-hot': true,
272
+ 'x-path': { kind: 'directory', ...(defaultHomePath ? { recommendedDir: defaultHomePath } : {}) },
273
+ });
273
274
 
274
275
  return [
276
+ {
277
+ id: 'world:farm/install',
278
+ owner: 'world:farm',
279
+ schema: {
280
+ type: 'object',
281
+ title: t('安装', 'Installation'),
282
+ description: t('桌面图层、素材和运行文件放在这里。修改路径会自动停图层、搬过去再重启。', 'Desktop layers, sprites and runtime files live here. Changing the path stops the layers, moves the folder and restarts them.'),
283
+ properties: {
284
+ 'worlds.farm.installPath': dir(
285
+ t('安装路径', 'Install path'),
286
+ t('默认 <Coopanion 数据目录>\\custom-farm;改完自动搬过去。', 'Defaults to <Coopanion data>\\custom-farm; the folder is moved automatically when changed.'),
287
+ ),
288
+ },
289
+ },
290
+ },
275
291
  {
276
292
  id: 'world:farm/field',
277
293
  owner: 'world:farm',
@@ -296,6 +312,8 @@ export class FarmWorld {
296
312
  #timer = null;
297
313
  #queue = Promise.resolve();
298
314
  #paths = null;
315
+ #homeChanged = false;
316
+ #previousHome = null;
299
317
  #cmdTimer = null;
300
318
  #layerChild = null;
301
319
  #layerAt = 0;
@@ -312,6 +330,7 @@ export class FarmWorld {
312
330
  this.dataDir = opts.dataDir ?? process.cwd();
313
331
  this.botDir = opts.botDir ?? process.cwd();
314
332
  this.packageDir = opts.packageDir ?? process.cwd();
333
+ this.persistConfig = typeof opts.persist === 'function' ? opts.persist : null;
315
334
  // 是否由这个 World 管桌面图层的启停(单元测试里传 false,避免碰到真实图层)
316
335
  this.manageLayers = opts.manageLayers !== false;
317
336
  this.language = 'zh';
@@ -390,15 +409,146 @@ export class FarmWorld {
390
409
  return Math.round(this.#num('tickIntervalMs', 1000, 3600000));
391
410
  }
392
411
 
393
- /** 自定义目录:custom.json + assets\sprites。找不到 <Pet> 时退回扩展上一级。 */
412
+ // ------------------------------------------------------------------ 安装目录
413
+ #installMarker() {
414
+ return join(this.dataDir, 'farm', 'install-home.txt');
415
+ }
416
+
417
+ #readInstallMarker() {
418
+ try {
419
+ const value = readFileSync(this.#installMarker(), 'utf8').trim();
420
+ return value || null;
421
+ } catch { return null; }
422
+ }
423
+
424
+ #writeInstallMarker(home) {
425
+ try {
426
+ mkdirSync(join(this.dataDir, 'farm'), { recursive: true });
427
+ writeFileSync(this.#installMarker(), home, 'utf8');
428
+ } catch { /* 只是个提示文件,写不了不影响 */ }
429
+ }
430
+
431
+ /** 把扩展包自带的 overlay / overlay-electron / assets 同步到 home。 */
432
+ #copyBundleHome(target) {
433
+ const source = bundleHome(this.packageDir);
434
+ if (!source || resolve(source) === resolve(target)) return;
435
+ for (const name of ['overlay', 'overlay-electron', 'assets']) {
436
+ const src = join(source, name);
437
+ const dst = join(target, name);
438
+ if (!existsSync(src)) continue;
439
+ try {
440
+ mkdirSync(dirname(dst), { recursive: true });
441
+ // 图层代码每次覆盖(升级扩展后能拿到新代码);素材只补缺失文件,保留用户上传的 cust_*。
442
+ cpSync(src, dst, {
443
+ recursive: true,
444
+ force: name !== 'assets',
445
+ filter: (path) => {
446
+ const normalized = path.replace(/\\/g, '/');
447
+ if (normalized.includes('/overlay/runtime')) return false;
448
+ if (normalized.includes('/overlay-electron/node_modules')) return false;
449
+ return !/\.(log|pid)$/i.test(normalized);
450
+ },
451
+ });
452
+ } catch (err) {
453
+ this.#host?.log?.warn?.('自定义农场:复制安装文件失败', { from: src, to: dst, err: msg(err) });
454
+ }
455
+ }
456
+ // Electron 的 node_modules 很大,不复制;用 junction 指回扩展包里的那一份。
457
+ const modules = join(source, 'overlay-electron', 'node_modules');
458
+ const targetModules = join(target, 'overlay-electron', 'node_modules');
459
+ if (existsSync(modules) && !existsSync(targetModules)) {
460
+ try {
461
+ symlinkSync(modules, targetModules, process.platform === 'win32' ? 'junction' : 'dir');
462
+ } catch { /* 没有 junction 时 launch.js 会从包的 node_modules 找 */ }
463
+ }
464
+ }
465
+
466
+ /** 旧 home 搬到新 home;只搬三个目录,不搬存档和 custom.json。 */
467
+ #moveHome(oldHome, target) {
468
+ if (!oldHome || resolve(oldHome) === resolve(target) || !existsSync(oldHome)) return;
469
+ const source = bundleHome(this.packageDir);
470
+ if (resolve(oldHome) === resolve(source)) return; // 扩展包本身不能搬
471
+ const oldResolved = resolve(oldHome);
472
+ const targetResolved = resolve(target);
473
+ if (targetResolved.startsWith(oldResolved + sep) || oldResolved.startsWith(targetResolved + sep)) return;
474
+ mkdirSync(target, { recursive: true });
475
+ for (const name of ['overlay', 'overlay-electron', 'assets']) {
476
+ const src = join(oldHome, name);
477
+ const dst = join(target, name);
478
+ if (!existsSync(src) || existsSync(dst)) continue;
479
+ try {
480
+ renameSync(src, dst);
481
+ } catch {
482
+ try {
483
+ cpSync(src, dst, { recursive: true });
484
+ rmSync(src, { recursive: true, force: true });
485
+ } catch (err) {
486
+ this.#host?.log?.warn?.('自定义农场:搬运安装目录失败', { from: src, to: dst, err: msg(err) });
487
+ }
488
+ }
489
+ }
490
+ }
491
+
492
+ /** 解析配置里的安装路径:留空用 <dataDir>/custom-farm,并写回配置方便在游戏配置里看到。 */
493
+ #prepareInstallHome() {
494
+ const configured = String(this.cfg?.installPath ?? '').trim();
495
+ // 测试/嵌入式使用时没有 persistConfig:直接用扩展包旁边的 home,不复制、不写配置。
496
+ if (!configured && !this.persistConfig) {
497
+ this.#paths = makePaths(bundleHome(this.packageDir));
498
+ return this.#paths;
499
+ }
500
+ const fallback = defaultHome(this.dataDir);
501
+ const target = resolve(configured || fallback);
502
+ if (!configured && this.persistConfig) {
503
+ try { this.persistConfig({ installPath: target }); } catch { /* 配置写不了就先用默认路径 */ }
504
+ }
505
+ const marker = this.#readInstallMarker();
506
+ if (marker && resolve(marker) !== target) {
507
+ this.#previousHome = resolve(marker);
508
+ this.#homeChanged = true;
509
+ this.#moveHome(marker, target);
510
+ } else if (!marker && !existsSync(target)) {
511
+ this.#homeChanged = true;
512
+ const source = bundleHome(this.packageDir);
513
+ if (source && resolve(source) !== target) this.#previousHome = resolve(source);
514
+ }
515
+ this.#copyBundleHome(target);
516
+ this.#writeInstallMarker(target);
517
+ this.#paths = makePaths(target);
518
+ return this.#paths;
519
+ }
520
+
521
+ #homePath() {
522
+ return this.#paths ?? this.#prepareInstallHome();
523
+ }
524
+
525
+ /** 游戏配置里改了安装路径:停图层 → 搬运 → 重建目录 → 重启图层。 */
526
+ async #syncInstallHome() {
527
+ const configured = String(this.cfg?.installPath ?? '').trim();
528
+ const target = resolve(configured || defaultHome(this.dataDir));
529
+ if (!this.#paths) { this.#prepareInstallHome(); return; }
530
+ if (resolve(this.#paths.home) === target) return;
531
+ await this.#stopLayers('安装路径变更');
532
+ this.#moveHome(this.#paths.home, target);
533
+ this.#copyBundleHome(target);
534
+ this.#writeInstallMarker(target);
535
+ this.#paths = makePaths(target);
536
+ if (this.#catalog) {
537
+ this.#catalog.spritesDir = this.#paths.assetsDir;
538
+ try { this.#catalog.load(); } catch (err) {
539
+ this.#host?.log?.warn?.('自定义农场:新安装目录的素材读不出来', { err: msg(err) });
540
+ }
541
+ }
542
+ this.#host?.log?.info?.('自定义农场:安装路径已切换', { home: this.#paths.home });
543
+ this.#ensureLayers('安装路径变更');
544
+ }
545
+
546
+ /** 自定义目录:custom.json + 安装目录下的 assets\sprites。 */
394
547
  #createCatalog() {
395
- const petHome = resolvePetHome(this.cfg?.petRoot);
396
- const spritesDir = petHome
397
- ? join(petHome, 'assets', 'sprites')
398
- : join(this.packageDir, '..', 'assets', 'sprites');
548
+ const home = this.#homePath();
399
549
  return new FarmCatalog({
400
550
  storePath: join(this.dataDir, 'farm', 'custom.json'),
401
- spritesDir,
551
+ spritesDir: home.assetsDir,
402
552
  pointsPerDay: this.#num('pointsPerDay', 1, 100000),
403
553
  waterDefaults: {
404
554
  dryPerDay: this.#num('dryPerDay', 0, 20),
@@ -417,17 +567,11 @@ export class FarmWorld {
417
567
  return join(this.dataDir, 'farm', 'state.json');
418
568
  }
419
569
 
420
- /** 图层状态文件。配置里给了就用配置,否则按扩展位置推(换机器不用改配置)。 */
570
+ /** 图层状态文件。配置里给了就用配置,否则用安装目录。 */
421
571
  overlayFile() {
422
572
  const p = this.cfg?.overlayStatePath;
423
573
  if (typeof p === 'string' && p.trim()) return p.trim();
424
- this.#paths ??= makePaths(resolvePetHome(this.cfg?.petRoot));
425
- return this.#paths.overlayState;
426
- }
427
-
428
- bagScript() {
429
- this.#paths ??= makePaths(resolvePetHome(this.cfg?.petRoot));
430
- return this.#paths.bagScript;
574
+ return this.#homePath().overlayState;
431
575
  }
432
576
 
433
577
  envPromptVars() {
@@ -460,7 +604,6 @@ export class FarmWorld {
460
604
  case 'farm_collect': return await this.#collectTool();
461
605
  case 'farm_consume': return await this.#consumeTool(args);
462
606
  case 'farm_craft': return await this.#craftTool(args);
463
- case 'farm_bag': return await this.#bagTool();
464
607
  case 'farm_pull': return await this.#pullTool(args);
465
608
  case 'farm_sell': return await this.#sellTool(args);
466
609
  case 'farm_shop': return this.#shopTool(args);
@@ -825,23 +968,6 @@ export class FarmWorld {
825
968
  (r.cleared ? ',清掉了 ' + r.cleared + ' 丛杂草' : '') + '。' };
826
969
  }
827
970
 
828
- async #bagTool() {
829
- if (process.platform !== 'win32') {
830
- return { text: '背包窗口是 Windows WPF;当前平台请在 Coopanion WebUI 的「背包」页查看。' };
831
- }
832
- const script = this.bagScript();
833
- if (!existsSync(script)) {
834
- throw new Error('找不到背包脚本:' + script + '(整个 Pet 文件夹要一起拷贝)');
835
- }
836
- // 注意:Windows 上不能加 detached——那会用 DETACHED_PROCESS 起进程,
837
- // PowerShell 5.1 在那种情况下起不来(表现为窗口一直没出现、日志也没有)。不加 detached 也一样能活过父进程。
838
- const child = spawn('powershell.exe',
839
- ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-WindowStyle', 'Hidden', '-File', script],
840
- { stdio: 'ignore', windowsHide: true });
841
- child.unref();
842
- return { text: '背包面板叫出来了(' + script + ')。它只读,里面有多少就是背包里有多少。' };
843
- }
844
-
845
971
  #requireHere() {
846
972
  const at = this.#state.standingAt;
847
973
  if (!Number.isFinite(Number(at))) {
@@ -1203,6 +1329,7 @@ export class FarmWorld {
1203
1329
 
1204
1330
  async #tick() {
1205
1331
  if (!this.#started || !this.#state) return;
1332
+ await this.#syncInstallHome();
1206
1333
  const cfg = this.#cfgAll();
1207
1334
  this.#syncLayerVisibility();
1208
1335
  const now = this.now();
@@ -1364,7 +1491,7 @@ export class FarmWorld {
1364
1491
  }],
1365
1492
  // 状态用 lamp + badges 展示,玩法参数放到下面的 config,走框架自带的「配置」页签。
1366
1493
  invoke: (panel, method, args) => this.invoke(panel, method, args),
1367
- config: farmConfigGroups(language),
1494
+ config: farmConfigGroups(language, defaultHome(this.dataDir)),
1368
1495
  promptDocs: [{
1369
1496
  key: 'worlds.' + this.id + '.env',
1370
1497
  title: '环境提示词',
@@ -2129,7 +2256,7 @@ export class FarmWorld {
2129
2256
  else this.#ensureLayers('显示农场');
2130
2257
  }
2131
2258
 
2132
- #stopLayers(reason) {
2259
+ #stopLayers(reason, home = null) {
2133
2260
  if (!this.manageLayers) { return Promise.resolve(); }
2134
2261
  try {
2135
2262
  const pidFile = join(this.dataDir, 'farm', 'electron.pid');
@@ -2154,7 +2281,7 @@ export class FarmWorld {
2154
2281
  let done = false;
2155
2282
  const finish = () => { if (!done) { done = true; resolve(); } };
2156
2283
  try {
2157
- const paths = this.#paths ?? (this.#paths = makePaths(resolvePetHome(this.cfg?.petRoot)));
2284
+ const paths = home ? makePaths(home) : this.#homePath();
2158
2285
  const script = join(paths.overlayDir, 'restart-layers.ps1');
2159
2286
  if (!existsSync(script)) { finish(); return; }
2160
2287
  const child = spawn('powershell.exe',
@@ -2217,7 +2344,7 @@ export class FarmWorld {
2217
2344
 
2218
2345
  #ensureLayers(reason) {
2219
2346
  if (!this.manageLayers || this.#flag('hideFarm')) { return; }
2220
- const paths0 = this.#paths ?? (this.#paths = makePaths(resolvePetHome(this.cfg?.petRoot)));
2347
+ const paths0 = this.#homePath();
2221
2348
  if (this.#electronReady(paths0)) {
2222
2349
  if (this.#electronPidAlive()) return;
2223
2350
  if (process.platform === 'win32') {
@@ -2236,7 +2363,7 @@ export class FarmWorld {
2236
2363
  }
2237
2364
  if (process.platform !== 'win32') {
2238
2365
  try {
2239
- const paths = this.#paths ?? (this.#paths = makePaths(resolvePetHome(this.cfg?.petRoot)));
2366
+ const paths = this.#homePath();
2240
2367
  const launcher = join(paths.electronDir, 'launch.js');
2241
2368
  const localElectron = join(paths.electronDir, 'node_modules', 'electron');
2242
2369
  const packageElectron = join(paths.home, 'node_modules', 'electron');
@@ -2252,7 +2379,7 @@ export class FarmWorld {
2252
2379
  return;
2253
2380
  }
2254
2381
  try {
2255
- const paths = this.#paths ?? (this.#paths = makePaths(resolvePetHome(this.cfg?.petRoot)));
2382
+ const paths = this.#homePath();
2256
2383
  const script = join(paths.overlayDir, 'restart-layers.ps1');
2257
2384
  if (!existsSync(script)) return;
2258
2385
  const child = spawn('powershell.exe',
@@ -2350,6 +2477,11 @@ export class FarmWorld {
2350
2477
  if (this.#started) return;
2351
2478
  this.#host = host;
2352
2479
  try {
2480
+ this.#prepareInstallHome();
2481
+ if (this.#homeChanged) {
2482
+ if (this.#previousHome) await this.#stopLayers('安装路径迁移', this.#previousHome);
2483
+ await this.#stopLayers('安装路径迁移');
2484
+ }
2353
2485
  this.#catalog = this.#createCatalog();
2354
2486
  try {
2355
2487
  this.#catalog.load();
package/bag/README.md DELETED
@@ -1,60 +0,0 @@
1
- # 背包窗口(bag)
2
-
3
- 桌宠右键菜单里的「背包」打开的就是它。
4
-
5
- ![背包](shot.png)
6
-
7
- ## 长什么样
8
-
9
- 照游戏里的 UI 做法:**圆角面板 + 斜面边框 + 物品格栅**,标题栏带农场图标,格子里的数量压在右下角,
10
- 空槽也画出来(固定 16 格)。
11
-
12
- ## 美术怎么来的
13
-
14
- 游戏里的 UI 形状是「白色蒙版 + 运行时染色」,所以底板和格子也是用游戏自己的形状拼的:
15
-
16
- | 素材 | 用途 |
17
- |---|---|
18
- | `assets\sprites\ui_background_default24_border.png` | 面板外框的圆角形状 |
19
- | `assets\sprites\ui_background_item.png` | 物品格的形状 |
20
- | `assets\sprites\ui_menuicon_farm.png` | 标题栏图标 |
21
- | `assets\sprites\icon_item_*.png` | 格子里的物品图标 |
22
-
23
- `bag\art\panel.png`(360×400)、`bag\art\slot.png`(64×64)已生成好;旧生成脚本已删除。
24
- 都是 2 倍最近邻放大,跟图层同一个像素刻度。
25
-
26
- **想换风格**:直接替换 `bag\art\panel.png` / `slot.png`,
27
- 或者直接替换 `art\panel.png` 和 `art\slot.png` 这两张图,脚本一行都不用动。
28
-
29
- ## 行为
30
-
31
- - 只读 `Pet\overlay\runtime\state.json`(farm World 每 5 秒写一次),每秒刷新
32
- - 拖动面板移动,右上角 × 或 Esc 关闭
33
- - 单实例:已经开着再点菜单不会再开一个
34
-
35
- ## 单独跑
36
-
37
- ```powershell
38
- Set-ExecutionPolicy -Scope Process Bypass -Force
39
- .\bag.ps1 # × 或 Esc 关闭
40
- .\bag.ps1 -Seconds 5 # 5 秒后自动关(自检用)
41
- ```
42
-
43
- ## 它怎么被打开
44
-
45
- ```
46
- 右键菜单「背包」 tile
47
- → pet-app.js send({ t: 'control', action: 'bag' })
48
- → src/world.ts onControl('bag') → spawn powershell bag.ps1
49
- → 本窗口
50
- ```
51
-
52
- 前两步是**本地补丁**,打在 Coopanion 自己的桌宠扩展包里:
53
-
54
- | 文件 | 改了什么 |
55
- |---|---|
56
- | `node_modules\cortico-world-desktop-pet\web\ui.js` | `ICONS.bag` 图标 |
57
- | `...\web\pet-app.js` | 菜单的 `ACTIONS` 表加一项 `bag` |
58
- | `...\src\world.ts` | `onControl` 加 `action === 'bag'` → spawn 本脚本 |
59
-
60
- **Coopanion 升级会覆盖这三个文件**,重跑 `..\scripts\repatch-pet-menu.ps1` 即可(幂等);`-Restore` 可还原。
package/bag/art/panel.png DELETED
Binary file
package/bag/art/slot.png DELETED
Binary file