From d08989b8cb2ad663a5c4edffc8184077e5093ecd Mon Sep 17 00:00:00 2001 From: xiaoxustudio Date: Sun, 14 Jun 2026 12:54:32 +0800 Subject: [PATCH 1/7] feat: add custom cursor --- packages/webgal/package.json | 1 + .../util/coreInitialFunction/infoFetcher.ts | 26 +++++++++- yarn.lock | 49 ++++++++++++++++++- 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/packages/webgal/package.json b/packages/webgal/package.json index 7a216c9f4..4569f102b 100644 --- a/packages/webgal/package.json +++ b/packages/webgal/package.json @@ -13,6 +13,7 @@ "@icon-park/react": "^1.4.2", "@reduxjs/toolkit": "^1.8.1", "angular-expressions": "^1.4.3", + "ani-cursor.js": "^1.0.2", "axios": "^1.13.5", "cloudlogjs": "^1.0.9", "gifuct-js": "^2.1.2", diff --git a/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts b/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts index 7a239ff32..ef889eb72 100644 --- a/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts +++ b/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts @@ -9,6 +9,7 @@ import { getFastSaveFromStorage, getSavesFromStorage } from '@/Core/controller/s import { logger } from '@/Core/util/logger'; import axios from 'axios'; import { IGameVar } from '@/Core/Modules/stage/stageInterface'; +import { setANICursor } from 'ani-cursor.js'; /** * 获取游戏信息 @@ -21,7 +22,7 @@ export const infoFetcher = (url: string): Promise => { let gameConfig = WebgalParser.parseConfig(gameConfigRaw); logger.info('获取到游戏信息', gameConfig); // 先把 key 找到并设置了 - const keyItem = gameConfig.find((e) => e.command === 'Game_key'); + const keyItem = gameConfig.find((e: any) => e.command === 'Game_key'); WebGAL.gameKey = (keyItem?.args?.[0] as string) ?? ''; initKey(); await getStorageAsync(); @@ -30,7 +31,7 @@ export const infoFetcher = (url: string): Promise => { // 存储 config.txt 中的配置,用于清除所有数据时还原配置 const gameConfigInit: IGameVar = {}; // 按照游戏的配置开始设置对应的状态 - gameConfig.forEach((e) => { + gameConfig.forEach((e: any) => { const { command, args } = e; if (args.length > 0) { if (args.length > 1) { @@ -67,6 +68,27 @@ export const infoFetcher = (url: string): Promise => { const appId = String(res); WebGAL.steam.initialize(appId); } + // 自定义光标 + if (command === 'Custom_Corsur') { + const group = String(res) + .split(/\s+/) + .filter((e) => e); + for (const token of group) { + const arr = token.split(','); + const url = `/game/${arr[0]}`; + const type = arr?.[1] ?? 'auto'; + if (String(arr[0]).endsWith('.ani')) { + const width = parseInt(arr?.[2] ?? '32'); + const height = parseInt(arr?.[3] ?? '32'); + setANICursor('html *', url, type, width, height); + } else { + const hotspot = `${arr?.[2] ?? ''} ${arr?.[3] ?? ''}`; + const cursorCss = document.createElement('style'); + cursorCss.innerHTML = `html * { cursor: url(${arr[0]}) ${hotspot}, ${type} !important; }`; + document.head.appendChild(cursorCss); + } + } + } } } }); diff --git a/yarn.lock b/yarn.lock index 9ef0a5ec1..40a903c97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1723,6 +1723,14 @@ angular-expressions@^1.4.3: resolved "https://registry.yarnpkg.com/angular-expressions/-/angular-expressions-1.4.3.tgz#24fb9e8e9e0278885dd0f7e28512ac88a74037f6" integrity sha512-r7j+dqOuHy0OYiR5AazDixU/Us3TDN2FfuxGX4Dq6d61Y2MhBQHMdUNBfkkLPjDqVm2Is394h31gC3bcBwy9zw== +ani-cursor.js@^1.0.2: + version "1.0.2" + resolved "https://registry.npmmirror.com/ani-cursor.js/-/ani-cursor.js-1.0.2.tgz#891342a98873e12e662699315a8c33285acb87a2" + integrity sha512-spNSKKZJxr9fj2uG27Hh9zxe025uK59KNEVLoAgtUk/CPOdzQOHPB612qwAI0GawwxZdp8lU5QEph7C8QeiY8g== + dependencies: + buffer "^6.0.3" + riff-file "^1.0.3" + ansi-escapes@^4.3.0: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -2056,11 +2064,28 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.npmmirror.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + builtin-modules@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== +byte-data@^18.0.3: + version "18.1.1" + resolved "https://registry.npmmirror.com/byte-data/-/byte-data-18.1.1.tgz#4eed09970921bf7d786b250e56615d8314bb6746" + integrity sha512-Kv/B0r7adgnCcrs/y703sac2XFLdHW5kPfis1j8+Ij/hmEcWhBKf+1pNTv+vsNqXb207Uiyri8bpnogNxR/4Lg== + dependencies: + endianness "^8.0.2" + ieee754-buffer "^2.0.0" + utf8-buffer "^1.0.0" + bytes@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" @@ -2594,6 +2619,11 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" +endianness@^8.0.2: + version "8.0.2" + resolved "https://registry.npmmirror.com/endianness/-/endianness-8.0.2.tgz#e35d16bbe80b6ff94fbc199168dd234d9f78168e" + integrity sha512-IU+77+jJ7lpw2qZ3NUuqBZFy3GuioNgXUdsL1L9tooDNTaw0TgOnwNuc+8Ns+haDaTifK97QLzmOANJtI/rGvw== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -3606,7 +3636,12 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -ieee754@^1.1.13: +ieee754-buffer@^2.0.0: + version "2.0.0" + resolved "https://registry.npmmirror.com/ieee754-buffer/-/ieee754-buffer-2.0.0.tgz#5648050d1a037df59a6cc68f441c6b0b467314f8" + integrity sha512-AXUAT0nMEi7h1Is8HXGXof3eejl/GabZFKSj8Ym6kVRUSwrAb52EkAXywiCQYSHGQMRn7lvfY7vhPMjVc+Kybg== + +ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -5231,6 +5266,13 @@ rfdc@^1.3.0: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.1.tgz#2b6d4df52dffe8bb346992a10ea9451f24373a8f" integrity sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg== +riff-file@^1.0.3: + version "1.0.3" + resolved "https://registry.npmmirror.com/riff-file/-/riff-file-1.0.3.tgz#95589f787e0c5fb2fa2937fc0a0e5e5d3131242b" + integrity sha512-Vv8wwGr0BCks7VMI3Lv0houZee4DaHFjjTT0LMhMJKio2YmLncLeIVpK63ydSverngNk8XQPU3fbeP3bWgSIig== + dependencies: + byte-data "^18.0.3" + rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -6079,6 +6121,11 @@ use-sync-external-store@^1.0.0: resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== +utf8-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.npmmirror.com/utf8-buffer/-/utf8-buffer-1.0.0.tgz#457e7d848d4d9cd873772b710d150565f6e543d9" + integrity sha512-ueuhzvWnp5JU5CiGSY4WdKbiN/PO2AZ/lpeLiz2l38qwdLy/cW40XobgyuIWucNyum0B33bVB0owjFCeGBSLqg== + util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" From 1738811137d696df5ffe1938e9e4e00c525612f3 Mon Sep 17 00:00:00 2001 From: xiaoxustudio Date: Sun, 14 Jun 2026 12:57:51 +0800 Subject: [PATCH 2/7] fix: trim whitespace in token split --- .../webgal/src/Core/util/coreInitialFunction/infoFetcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts b/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts index ef889eb72..73e7f4ac3 100644 --- a/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts +++ b/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts @@ -74,7 +74,7 @@ export const infoFetcher = (url: string): Promise => { .split(/\s+/) .filter((e) => e); for (const token of group) { - const arr = token.split(','); + const arr = token.split(',').map((e) => e.trim()); const url = `/game/${arr[0]}`; const type = arr?.[1] ?? 'auto'; if (String(arr[0]).endsWith('.ani')) { From 23647fbb8290d340dc9712e999a666dc7669a8a8 Mon Sep 17 00:00:00 2001 From: xiaoxustudio Date: Sun, 14 Jun 2026 13:35:54 +0800 Subject: [PATCH 3/7] fix: path process --- .../webgal/src/Core/util/coreInitialFunction/infoFetcher.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts b/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts index 73e7f4ac3..ec17731e1 100644 --- a/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts +++ b/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts @@ -77,14 +77,14 @@ export const infoFetcher = (url: string): Promise => { const arr = token.split(',').map((e) => e.trim()); const url = `/game/${arr[0]}`; const type = arr?.[1] ?? 'auto'; - if (String(arr[0]).endsWith('.ani')) { + if (String(url).endsWith('.ani')) { const width = parseInt(arr?.[2] ?? '32'); const height = parseInt(arr?.[3] ?? '32'); setANICursor('html *', url, type, width, height); } else { const hotspot = `${arr?.[2] ?? ''} ${arr?.[3] ?? ''}`; const cursorCss = document.createElement('style'); - cursorCss.innerHTML = `html * { cursor: url(${arr[0]}) ${hotspot}, ${type} !important; }`; + cursorCss.innerHTML = `html * { cursor: url(${url}) ${hotspot}, ${type} !important; }`; document.head.appendChild(cursorCss); } } From 382e8fd6c73ab7f158d8aa608070581266f529cf Mon Sep 17 00:00:00 2001 From: xiaoxustudio Date: Sun, 14 Jun 2026 13:42:36 +0800 Subject: [PATCH 4/7] fix: path process to safeUrl --- .../src/Core/util/coreInitialFunction/infoFetcher.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts b/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts index ec17731e1..48f77b95e 100644 --- a/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts +++ b/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts @@ -75,16 +75,16 @@ export const infoFetcher = (url: string): Promise => { .filter((e) => e); for (const token of group) { const arr = token.split(',').map((e) => e.trim()); - const url = `/game/${arr[0]}`; + const safeUrl = `/game/${arr[0]}`.replace(/\\/g, '\\\\'); const type = arr?.[1] ?? 'auto'; - if (String(url).endsWith('.ani')) { + if (String(safeUrl).endsWith('.ani')) { const width = parseInt(arr?.[2] ?? '32'); const height = parseInt(arr?.[3] ?? '32'); - setANICursor('html *', url, type, width, height); + setANICursor('html *', safeUrl, type, width, height); } else { const hotspot = `${arr?.[2] ?? ''} ${arr?.[3] ?? ''}`; const cursorCss = document.createElement('style'); - cursorCss.innerHTML = `html * { cursor: url(${url}) ${hotspot}, ${type} !important; }`; + cursorCss.innerHTML = `html * { cursor: url(${safeUrl}) ${hotspot}, ${type} !important; }`; document.head.appendChild(cursorCss); } } From 75a9e0c4fa913aedc396450ac9afb5ac4b4fe90b Mon Sep 17 00:00:00 2001 From: xiaoxustudio Date: Sun, 14 Jun 2026 13:44:52 +0800 Subject: [PATCH 5/7] fix: use textContent for head cursor element --- .../webgal/src/Core/util/coreInitialFunction/infoFetcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts b/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts index 48f77b95e..32ff5bc63 100644 --- a/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts +++ b/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts @@ -84,7 +84,7 @@ export const infoFetcher = (url: string): Promise => { } else { const hotspot = `${arr?.[2] ?? ''} ${arr?.[3] ?? ''}`; const cursorCss = document.createElement('style'); - cursorCss.innerHTML = `html * { cursor: url(${safeUrl}) ${hotspot}, ${type} !important; }`; + cursorCss.textContent = `html * { cursor: url(${safeUrl}) ${hotspot}, ${type} !important; }`; document.head.appendChild(cursorCss); } } From 9bfe9af8f6c5f3c06df6ce228ae4636c14a7526b Mon Sep 17 00:00:00 2001 From: xiaoxustudio Date: Tue, 16 Jun 2026 20:00:00 +0800 Subject: [PATCH 6/7] chore: upgrade ani-cursor.js to 1.0.3 --- packages/webgal/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/webgal/package.json b/packages/webgal/package.json index 4569f102b..aa6c608cb 100644 --- a/packages/webgal/package.json +++ b/packages/webgal/package.json @@ -13,7 +13,7 @@ "@icon-park/react": "^1.4.2", "@reduxjs/toolkit": "^1.8.1", "angular-expressions": "^1.4.3", - "ani-cursor.js": "^1.0.2", + "ani-cursor.js": "^1.0.3", "axios": "^1.13.5", "cloudlogjs": "^1.0.9", "gifuct-js": "^2.1.2", diff --git a/yarn.lock b/yarn.lock index 40a903c97..174fde5cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1723,10 +1723,10 @@ angular-expressions@^1.4.3: resolved "https://registry.yarnpkg.com/angular-expressions/-/angular-expressions-1.4.3.tgz#24fb9e8e9e0278885dd0f7e28512ac88a74037f6" integrity sha512-r7j+dqOuHy0OYiR5AazDixU/Us3TDN2FfuxGX4Dq6d61Y2MhBQHMdUNBfkkLPjDqVm2Is394h31gC3bcBwy9zw== -ani-cursor.js@^1.0.2: - version "1.0.2" - resolved "https://registry.npmmirror.com/ani-cursor.js/-/ani-cursor.js-1.0.2.tgz#891342a98873e12e662699315a8c33285acb87a2" - integrity sha512-spNSKKZJxr9fj2uG27Hh9zxe025uK59KNEVLoAgtUk/CPOdzQOHPB612qwAI0GawwxZdp8lU5QEph7C8QeiY8g== +ani-cursor.js@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/ani-cursor.js/-/ani-cursor.js-1.0.3.tgz#a0b390a887f2692da83c69c9251a043c7c17888f" + integrity sha512-asDn4Ts+iSSTHIQxyqn3iYQtf2FLk3Njid1aOb03QnSsEPY9qT9aDeuJUlzt+cc5LRTw6D+1vBp9WzBOceS+LQ== dependencies: buffer "^6.0.3" riff-file "^1.0.3" From 5ee5cbe2fb1dff0e4b48ebde6cac01ac99014240 Mon Sep 17 00:00:00 2001 From: xiaoxustudio Date: Tue, 16 Jun 2026 20:15:11 +0800 Subject: [PATCH 7/7] feat: support custom cursor size and hotspot --- .../util/coreInitialFunction/infoFetcher.ts | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts b/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts index 32ff5bc63..2c5e23b23 100644 --- a/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts +++ b/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts @@ -69,6 +69,24 @@ export const infoFetcher = (url: string): Promise => { WebGAL.steam.initialize(appId); } // 自定义光标 + const customCorsurSize = { width: 32, height: 32 }; + if (command === 'Custom_Corsur_Ani_Size') { + const corsurSize = String(res) + .split(/\s+/) + .filter((e) => e); + if (corsurSize.length === 2) { + const width = parseInt(corsurSize[0]); + const height = parseInt(corsurSize[1]); + // see to https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/cursor#icon_size_limits + if (width > 32 || height > 32) { + logger.warn('size to large, we suggest set to 32x32 size'); + } + if (width > 0 && height > 0) { + customCorsurSize.width = width; + customCorsurSize.height = height; + } + } + } if (command === 'Custom_Corsur') { const group = String(res) .split(/\s+/) @@ -78,9 +96,17 @@ export const infoFetcher = (url: string): Promise => { const safeUrl = `/game/${arr[0]}`.replace(/\\/g, '\\\\'); const type = arr?.[1] ?? 'auto'; if (String(safeUrl).endsWith('.ani')) { - const width = parseInt(arr?.[2] ?? '32'); - const height = parseInt(arr?.[3] ?? '32'); - setANICursor('html *', safeUrl, type, width, height); + const hotspotX = parseInt(arr?.[2]); + const hotspotY = parseInt(arr?.[3]); + setANICursor( + 'html *', + safeUrl, + type, + customCorsurSize.width, + customCorsurSize.height, + hotspotX > 0 ? hotspotX : undefined, + hotspotY > 0 ? hotspotY : undefined, + ); } else { const hotspot = `${arr?.[2] ?? ''} ${arr?.[3] ?? ''}`; const cursorCss = document.createElement('style');