From 2f339f2a1cb22ce31d91e2d042639182fef03428 Mon Sep 17 00:00:00 2001 From: krypek Date: Tue, 1 Jul 2025 11:51:25 +0200 Subject: [PATCH 01/16] Add watch script --- build.mjs | 13 +++++++++++-- package.json | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/build.mjs b/build.mjs index 3e5d638..3ee6088 100644 --- a/build.mjs +++ b/build.mjs @@ -7,6 +7,8 @@ const config = { runtime: 'packages/runtime/src/_main.ts' }; +const isWatch = process.argv[2] == 'watch' + async function build(name, entry) { let outfile = path.join('./dist', `${name}.js`); if (name === "runtime") outfile = path.join('./dist', 'runtime', 'main.js'); @@ -39,8 +41,15 @@ async function build(name, entry) { }) ] }; - - await esbuild.build(esbuildConfig); + + const ctx = await esbuild.context(esbuildConfig) + + if (isWatch) { + await ctx.watch(); + } else { + await ctx.rebuild(); + process.exit() + } } const promises = []; diff --git a/package.json b/package.json index 0a6bd18..534ef19 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ }, "scripts": { "build": "node build.mjs", + "watch": "node build.mjs watch", "lint": "eslint . --ext .js,.ts --ignore-path .eslintignore", "check-fmt": "prettier --check '**/*.{js,ts,json,css,html}'", "format": "prettier --write **/*.{js,ts,json,css,html}" From 640800ee5b02ecf62a90056eec3ab3e0cc9785f5 Mon Sep 17 00:00:00 2001 From: krypek Date: Mon, 21 Jul 2025 13:29:04 +0200 Subject: [PATCH 02/16] Refactor build.ts and make it typescript --- build.mjs | 59 ------------------------------- build.ts | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 5 +-- 3 files changed, 101 insertions(+), 61 deletions(-) delete mode 100644 build.mjs create mode 100644 build.ts diff --git a/build.mjs b/build.mjs deleted file mode 100644 index 3ee6088..0000000 --- a/build.mjs +++ /dev/null @@ -1,59 +0,0 @@ -import * as esbuild from 'esbuild'; -import copyStaticFiles from 'esbuild-copy-static-files' -import path from 'path'; - -const config = { - core: 'packages/core/src/main.ts', - runtime: 'packages/runtime/src/_main.ts' -}; - -const isWatch = process.argv[2] == 'watch' - -async function build(name, entry) { - let outfile = path.join('./dist', `${name}.js`); - if (name === "runtime") outfile = path.join('./dist', 'runtime', 'main.js'); - - /** @type {import("esbuild").BuildOptions} */ - const esbuildConfig = { - entryPoints: [entry], - outfile, - - format: 'esm', - platform: 'node', - - treeShaking: true, - bundle: true, - minify: true, - sourcemap: 'inline', - - plugins: [ - copyStaticFiles({ - src: `./packages/runtime/ccmod.json`, - dest: `./dist/runtime/ccmod.json` - }), - copyStaticFiles({ - src: `./packages/runtime/media`, - dest: `./dist/runtime/media` - }), - copyStaticFiles({ - src: `./packages/runtime/assets`, - dest: `./dist/runtime/assets` - }) - ] - }; - - const ctx = await esbuild.context(esbuildConfig) - - if (isWatch) { - await ctx.watch(); - } else { - await ctx.rebuild(); - process.exit() - } -} - -const promises = []; -for (const [name, path] of Object.entries(config)) { - promises.push(build(name, path)); -} -void Promise.all(promises); diff --git a/build.ts b/build.ts new file mode 100644 index 0000000..c868f21 --- /dev/null +++ b/build.ts @@ -0,0 +1,98 @@ +import * as esbuild from 'esbuild'; +import copyStaticFiles from 'esbuild-copy-static-files'; +import path from 'path'; +import * as fs from 'fs'; + +const isWatch = process.argv[2] == 'watch'; + +function donePlugin(outfile: string, modifyCode?: (code: string) => string): esbuild.Plugin { + return { + name: 'done plugin', + setup(build) { + build.onEnd(async (res) => { + let code = res.outputFiles![0].text; + if (!code) return; // when compile errors + + if (modifyCode) code = modifyCode(code); + + await fs.promises.writeFile(outfile, code); + + const bytes = code.length; + const kb = bytes / 1024; + console.log(outfile, kb.toFixed(1) + 'kb'); + }); + }, + }; +} + + +function core(): esbuild.BuildOptions { + const outfile = path.join('./dist', `core.js`); + + return { + entryPoints: ['packages/core/src/main.ts'], + + format: 'esm', + platform: 'node', + + write: false, + bundle: true, + minify: false, + sourcemap: 'inline', + + plugins: [ + donePlugin(outfile), + ], + }; +} + +function runtime(): esbuild.BuildOptions { + const outfile = path.join('./dist', 'runtime', 'main.js'); + + return { + entryPoints: ['packages/runtime/src/_main.ts'], + + format: 'esm', + platform: 'node', + + write: false, + bundle: true, + minify: true, + sourcemap: 'inline', + + plugins: [ + copyStaticFiles({ + src: `./packages/runtime/ccmod.json`, + dest: `./dist/runtime/ccmod.json`, + }), + copyStaticFiles({ + src: `./packages/runtime/media`, + dest: `./dist/runtime/media`, + }), + copyStaticFiles({ + src: `./packages/runtime/assets`, + dest: `./dist/runtime/assets`, + }), + donePlugin(outfile), + ], + }; +} + +const modules: (() => esbuild.BuildOptions)[] = [core, runtime]; + +if (isWatch) { + console.clear(); + await Promise.all( + modules.map(async (module) => { + const ctx = await esbuild.context(module()); + await ctx.watch(); + }), + ); +} else { + await Promise.all( + modules.map(async (module) => { + await esbuild.build(module()); + }), + ); + process.exit(); // because esbuild keeps the process alive for some reason +} diff --git a/package.json b/package.json index 534ef19..53c816e 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "devDependencies": { "@types/archiver": "^6.0.3", + "@types/esbuild-copy-static-files": "^0.1.4", "@types/jquery": "^1.10.31", "@types/node": ">=24.0", "@types/semver": "6.2.x || 6.3.x", @@ -27,8 +28,8 @@ "ultimate-crosscode-typedefs": "github:CCDirectLink/ultimate-crosscode-typedefs" }, "scripts": { - "build": "node build.mjs", - "watch": "node build.mjs watch", + "build": "node --experimental-strip-types build.ts", + "watch": "node --experimental-strip-types build.ts watch", "lint": "eslint . --ext .js,.ts --ignore-path .eslintignore", "check-fmt": "prettier --check '**/*.{js,ts,json,css,html}'", "format": "prettier --write **/*.{js,ts,json,css,html}" From 6376bcef43b2960ead4964e7c3c6169c0b5bb869 Mon Sep 17 00:00:00 2001 From: krypek Date: Mon, 21 Jul 2025 13:29:30 +0200 Subject: [PATCH 03/16] Fix deprecated function being used --- packages/runtime/src/resources-injections.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime/src/resources-injections.ts b/packages/runtime/src/resources-injections.ts index 1b89ec0..5aa4f0a 100644 --- a/packages/runtime/src/resources-injections.ts +++ b/packages/runtime/src/resources-injections.ts @@ -104,7 +104,7 @@ impactModuleHooks.add('impact.base.image', () => { // assuming that there are no such JSON paths because JQuery automatically // encodes URIs before requesting and there are no instances of leading // whitespace because `ig.root` is not empty in the development version. - path = path.trimRight(); + path = path.trimEnd(); // NOTE: Strictly speaking, Impact's file forwarding must be applied // first, before trimming, as in the game, but I'm not sure if this will // break anything because the current behavior certainly haven't so far, From 0f966e23fd536348a29d48db31c676d10a6ceed5 Mon Sep 17 00:00:00 2001 From: krypek Date: Mon, 21 Jul 2025 20:26:14 +0200 Subject: [PATCH 04/16] Add .ccmod spaghetti (to refactor) --- .gitignore | 1 + build.ts | 60 +++++++--- package.json | 4 + packages/core/src/files.desktop.ts | 53 ++++++-- packages/core/src/files.ts | 7 +- packages/core/src/modloader.ts | 8 +- packages/core/src/service-worker-bridge.ts | 82 +++++++++++++ packages/core/src/service-worker.ts | 79 ++++++++++++ packages/runtime/src/resources.ts | 3 +- pnpm-lock.yaml | 133 ++++++++++++++++++++- 10 files changed, 395 insertions(+), 35 deletions(-) create mode 100644 packages/core/src/service-worker-bridge.ts create mode 100644 packages/core/src/service-worker.ts diff --git a/.gitignore b/.gitignore index e869fab..8f95227 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ dist/ *.tsbuildinfo /ultimate-crosscode-typedefs/ +/ccmod-service-worker.js ccloader_*_quick-install.* ccloader_*_package.* diff --git a/build.ts b/build.ts index c868f21..49d6c8e 100644 --- a/build.ts +++ b/build.ts @@ -25,6 +25,27 @@ function donePlugin(outfile: string, modifyCode?: (code: string) => string): esb }; } +const commonOptions = { + format: 'esm', + platform: 'node', + + write: false, + bundle: true, + minify: false, + sourcemap: 'inline', +} as const; + +function fixEsmImports(code: string): string { + code = code.replace( + /import \{ Buffer as (Buffer\d*) \} from "buffer";/g, + 'const $1 = require("buffer").Buffer;', + ); + code = code.replace( + /import \{ (createRequire) } from "module";/g, + 'const $1 = require("module").createRequire;', + ); + return code; +} function core(): esbuild.BuildOptions { const outfile = path.join('./dist', `core.js`); @@ -32,16 +53,13 @@ function core(): esbuild.BuildOptions { return { entryPoints: ['packages/core/src/main.ts'], - format: 'esm', - platform: 'node', - - write: false, - bundle: true, - minify: false, - sourcemap: 'inline', + ...commonOptions, plugins: [ - donePlugin(outfile), + donePlugin(outfile, (code) => { + code = fixEsmImports(code); + return code; + }), ], }; } @@ -52,13 +70,7 @@ function runtime(): esbuild.BuildOptions { return { entryPoints: ['packages/runtime/src/_main.ts'], - format: 'esm', - platform: 'node', - - write: false, - bundle: true, - minify: true, - sourcemap: 'inline', + ...commonOptions, plugins: [ copyStaticFiles({ @@ -73,12 +85,26 @@ function runtime(): esbuild.BuildOptions { src: `./packages/runtime/assets`, dest: `./dist/runtime/assets`, }), - donePlugin(outfile), + donePlugin(outfile, (code) => { + code = fixEsmImports(code); + return code; + }), ], }; } -const modules: (() => esbuild.BuildOptions)[] = [core, runtime]; +function ccmodServiceWorker(): esbuild.BuildOptions { + const outfile = path.join('./', 'ccmod-service-worker.js'); + return { + entryPoints: ['packages/core/src/service-worker.ts'], + + ...commonOptions, + + plugins: [donePlugin(outfile)], + }; +} + +const modules: (() => esbuild.BuildOptions)[] = [core, runtime, ccmodServiceWorker]; if (isWatch) { console.clear(); diff --git a/package.json b/package.json index 53c816e..f6893d4 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,12 @@ "@types/jquery": "^1.10.31", "@types/node": ">=24.0", "@types/semver": "6.2.x || 6.3.x", + "@types/serviceworker": "^0.0.145", "@typescript-eslint/eslint-plugin": "^6.20.0", "@typescript-eslint/parser": "^6.20.0", + "@zenfs/archives": "^1.2.3", + "@zenfs/core": "^2.3.4", + "@zenfs/dom": "^1.1.8", "archiver": "^7.0.1", "esbuild": "^0.25.8", "esbuild-copy-static-files": "^0.1.0", diff --git a/packages/core/src/files.desktop.ts b/packages/core/src/files.desktop.ts index 9324979..2878605 100644 --- a/packages/core/src/files.desktop.ts +++ b/packages/core/src/files.desktop.ts @@ -1,16 +1,36 @@ import * as utils from '@ccloader3/common/utils'; import { Config } from './config'; -const fs = (window.require('fs') as typeof import('fs')).promises; -const fsconst = (window.require('fs') as typeof import('fs')).constants; +import { fs as zenFs } from '@zenfs/core'; +import { isCCModPath } from './service-worker-bridge'; +const fs: typeof import('fs') = window.require?.('fs'); + +function preparePath(path: string) { + if (isCCModPath(path)) { + if (path[0] != '/') path = '/' + path; + } + return path; +} + +function getFs(path: string): typeof import('fs') | (typeof import('@zenfs/core'))['fs'] { + if (isCCModPath(path)) { + return zenFs; + } else { + return fs; + } +} export async function loadText(path: string): Promise { - return fs.readFile(path, 'utf8'); + path = preparePath(path); + const fs = getFs(path); + return fs.promises.readFile(path, 'utf8'); } export async function isReadable(path: string): Promise { + path = preparePath(path); + const fs = getFs(path); try { - await fs.access(path, fsconst.R_OK); + await fs.promises.access(path, fs.constants.R_OK); return true; } catch (_err) { return false; @@ -30,9 +50,12 @@ async function findRecursivelyInternal( relativePrefix: string, fileList: string[], ): Promise { + currentDir = preparePath(currentDir); + const fs = getFs(currentDir); + let contents: string[]; try { - contents = await fs.readdir(currentDir); + contents = await fs.promises.readdir(currentDir); } catch (err) { if (utils.errorHasCode(err) && err.code === 'ENOENT') return; throw err; @@ -41,7 +64,7 @@ async function findRecursivelyInternal( await Promise.all( contents.map(async (name) => { let fullPath = `${currentDir}/${name}`; - let stat = await fs.stat(fullPath); + let stat = await fs.promises.stat(fullPath); if (stat.isDirectory()) { await findRecursivelyInternal(fullPath, `${relativePrefix}${name}/`, fileList); } else { @@ -52,15 +75,18 @@ async function findRecursivelyInternal( } export async function getModDirectoriesIn(dir: string, _config: Config): Promise { + dir = preparePath(dir); + const fs = getFs(dir); + if (dir.endsWith('/')) dir = dir.slice(0, -1); let allContents: string[]; try { - allContents = await fs.readdir(dir); + allContents = await fs.promises.readdir(dir); } catch (err) { if (utils.errorHasCode(err) && err.code === 'ENOENT') { - await fs.mkdir(dir); - allContents = await fs.readdir(dir); + await fs.promises.mkdir(dir); + allContents = await fs.promises.readdir(dir); } throw err; } @@ -71,8 +97,9 @@ export async function getModDirectoriesIn(dir: string, _config: Config): Promise let fullPath = `${dir}/${name}`; // the `withFileTypes` option of `readdir` can't be used here because it // doesn't dereference symbolic links similarly to `stat` - let stat = await fs.stat(fullPath); - if (stat.isDirectory()) modDirectories.push(fullPath); + let stat = await fs.promises.stat(fullPath); + if (stat.isDirectory() || (stat.isFile() && name.endsWith('.ccmod'))) + modDirectories.push(fullPath); }), ); return modDirectories; @@ -86,7 +113,7 @@ export async function getInstalledExtensions(config: Config): Promise let allContents: string[]; try { - allContents = await fs.readdir(extensionsDir); + allContents = await fs.promises.readdir(extensionsDir); } catch (err) { // Older versions of the game simply don't have the support for extensions. if (utils.errorHasCode(err) && err.code === 'ENOENT') return []; @@ -114,7 +141,7 @@ export async function getInstalledExtensions(config: Config): Promise try { // The game also checks if the containing directory exists before // checking if there is a file inside, but it is redundant. - await fs.access(`${extensionsDir}/${name}/${name}.json`); + await fs.promises.access(`${extensionsDir}/${name}/${name}.json`); } catch (_err) { return; } diff --git a/packages/core/src/files.ts b/packages/core/src/files.ts index 1c445d2..7c13339 100644 --- a/packages/core/src/files.ts +++ b/packages/core/src/files.ts @@ -3,8 +3,11 @@ import * as filesBrowser from './files.browser'; import * as filesAndroid from './files.android'; import * as utils from '@ccloader3/common/utils'; -export const { loadText, isReadable, getModDirectoriesIn, getInstalledExtensions } = { +const modules = { [utils.PlatformType.DESKTOP]: filesDesktop, [utils.PlatformType.ANDROID]: filesAndroid, [utils.PlatformType.BROWSER]: filesBrowser, -}[utils.PLATFORM_TYPE]; +}; +const currentModule = modules[utils.PLATFORM_TYPE]; + +export const { loadText, isReadable, getModDirectoriesIn, getInstalledExtensions } = currentModule; diff --git a/packages/core/src/modloader.ts b/packages/core/src/modloader.ts index 97e379b..4f1d9e3 100644 --- a/packages/core/src/modloader.ts +++ b/packages/core/src/modloader.ts @@ -10,6 +10,7 @@ import * as modDataStorage from './mod-data-storage'; import { LegacyManifest, Manifest } from 'ultimate-crosscode-typedefs/file-types/mod-manifest'; import { LoadingStage, ModID } from 'ultimate-crosscode-typedefs/modloader/mod'; import * as consoleM from '@ccloader3/common/console'; +import { loadCCMods, loadServiceWorker } from './service-worker-bridge'; type ModsMap = Map; type ReadonlyModsMap = ReadonlyMap; @@ -17,6 +18,8 @@ type ReadonlyModsMap = ReadonlyMap; export async function boot(): Promise { consoleM.inject(); + const serviceWorker = await loadServiceWorker(); + let modloaderMetadata = await loadModloaderMetadata(); console.log(`CCLoader ${modloaderMetadata.version}`); @@ -57,7 +60,7 @@ export async function boot(): Promise { .set('crosscode', gameVersion) .set('ccloader', modloaderMetadata.version); if (typeof process !== 'undefined') { - virtualPackages.set('nw', new semver.SemVer(process.versions.nw)); + virtualPackages.set('nw', new semver.SemVer(process.versions.nw!)); } let installedExtensions = new Map(); @@ -137,6 +140,7 @@ export async function boot(): Promise { modDataStorage: modDataStorage.namespace, Mod: {}, _runtimeMod: runtimeMod, + serviceWorker, } as typeof modloader; console.log('beginning the game boot sequence...'); @@ -207,6 +211,8 @@ async function loadAllModMetadata(config: configM.Config, installedMods: ModsMap } } + await loadCCMods(allModsList); + let modsCountPerDir = new Map(); await Promise.all( allModsList.map(async ({ parentDir, dir }) => { diff --git a/packages/core/src/service-worker-bridge.ts b/packages/core/src/service-worker-bridge.ts new file mode 100644 index 0000000..8913845 --- /dev/null +++ b/packages/core/src/service-worker-bridge.ts @@ -0,0 +1,82 @@ +import { fs, configure } from '@zenfs/core'; +import { Zip } from '@zenfs/archives'; + +export type ServiceWorkerPacket = { + path: string; + data: Buffer; +}; + +export async function loadServiceWorker(): Promise { + const currentRegistration = await window.navigator.serviceWorker.getRegistration(); + if (currentRegistration) { + //Do not await update since the worker only performs a simple task. Even if there is a bugfix it should be enough to not crash. + currentRegistration.update(); + } else { + const path = 'ccmod-service-worker.js'; + await window.navigator.serviceWorker.register(path, { + updateViaCache: 'none', + type: 'module', + scope: '/ccloader3/', + }); + } + + const controller = window.navigator.serviceWorker.controller; + if (!controller || controller.state !== 'activated') { + window.location.reload(); + window.location.href = window.location.toString(); + history.go(0); + + throw new Error('(╯°□°)╯︵ ┻━┻'); + } + + setMessageHandling(); + + return controller; +} + +function sendMessage(packet: unknown) { + const controller = window.navigator.serviceWorker.controller; + controller?.postMessage(packet); +} + +function setMessageHandling() { + navigator.serviceWorker.onmessage = async (event) => { + const path: string = event.data; + + const data = await fs.promises.readFile(path); + + const packet: ServiceWorkerPacket = { + path, + data, + }; + sendMessage(packet); + }; +} + +let validCCModPaths: string[] | undefined +export function isCCModPath(path: string): boolean { + if (!validCCModPaths) return false; + + if (path[0] != '/') path = '/' + path; + return validCCModPaths.some( + (pathPrefix) => path.length > pathPrefix.length && path.startsWith(pathPrefix), + ); +} + +export async function loadCCMods(allModsList: Array<{ parentDir: string; dir: string }>) { + const ccmods: typeof allModsList = allModsList.filter((mod) => mod.dir.endsWith('.ccmod')); + const ccmodArrayBuffers = await Promise.all( + ccmods.map(async (mod) => (await fetch('../' + mod.dir)).arrayBuffer()), + ); + + const mounts: Parameters[0]['mounts'] = Object.fromEntries( + ccmods.map((mod, i) => [mod.dir, { backend: Zip, data: ccmodArrayBuffers[i] }]), + ); + + await configure({ + mounts, + }); + + validCCModPaths = ccmods.map((mod) => '/' + mod.dir); + sendMessage(validCCModPaths); +} diff --git a/packages/core/src/service-worker.ts b/packages/core/src/service-worker.ts new file mode 100644 index 0000000..fab7b97 --- /dev/null +++ b/packages/core/src/service-worker.ts @@ -0,0 +1,79 @@ +import type { ServiceWorkerPacket } from './service-worker-bridge'; + +self.addEventListener('activate', () => { + self.clients.claim(); +}); + +self.addEventListener('install', () => { + self.skipWaiting(); +}); + +const CONTENT_TYPES: Record = { + css: 'text/css', + js: 'text/javascript', + json: 'application/json', + png: 'image/png', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + html: 'text/html', + htm: 'text/html', +}; + +function contentType(url: string): string { + return CONTENT_TYPES[url.substring(url.lastIndexOf('.') + 1)] || 'text/plain'; +} + +async function post(data: unknown) { + const clients = await self.clients.matchAll(); + const client = clients[0]; + client.postMessage(data); +} + +const waitingFor: Map void> = new Map(); + +async function requestContents(path: string): Promise { + let resolve!: (packet: ServiceWorkerPacket) => void; + const promise: Promise = new Promise((res) => (resolve = res)); + await post(path); + + waitingFor.set(path, resolve); + + const { data } = await promise; + if (!data) { + return fetch(path); + } else { + return new Response(data, { + headers: { + 'Content-Type': contentType(path), + }, + status: 200, + statusText: 'ok', + }); + } +} + +let validPaths: string[] | undefined; + +self.addEventListener('message', async (event) => { + if (Array.isArray(event.data)) { + validPaths = event.data; + } else { + const packet: ServiceWorkerPacket = event.data; + const resolve = waitingFor.get(packet.path)!; + resolve(packet); + waitingFor.delete(packet.path); + } +}); + +self.addEventListener('fetch', async (event: FetchEvent) => { + if (!validPaths) return; + + const request = event.request; + const path = new URL(request.url).pathname; + + if ( + validPaths.some((pathPrefix) => path.length > pathPrefix.length && path.startsWith(pathPrefix)) + ) { + event.respondWith(requestContents(path)); + } +}); diff --git a/packages/runtime/src/resources.ts b/packages/runtime/src/resources.ts index acf7c49..a246f87 100644 --- a/packages/runtime/src/resources.ts +++ b/packages/runtime/src/resources.ts @@ -157,7 +157,8 @@ export async function loadImage( } if (shouldFetchRealData) { - data = await resourcesPlain.loadImage(wrapPathIntoURL(resolvedPath).href); + const url = '/' + resolvedPath + data = await resourcesPlain.loadImage(url); } if (requestedAsset != null) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d193ca..461f0d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,9 @@ importers: '@types/archiver': specifier: ^6.0.3 version: 6.0.3 + '@types/esbuild-copy-static-files': + specifier: ^0.1.4 + version: 0.1.4 '@types/jquery': specifier: ^1.10.31 version: 1.10.45 @@ -30,12 +33,24 @@ importers: '@types/semver': specifier: 6.2.x || 6.3.x version: 6.2.7 + '@types/serviceworker': + specifier: ^0.0.145 + version: 0.0.145 '@typescript-eslint/eslint-plugin': specifier: ^6.20.0 version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3) '@typescript-eslint/parser': specifier: ^6.20.0 version: 6.21.0(eslint@8.57.1)(typescript@5.8.3) + '@zenfs/archives': + specifier: ^1.2.3 + version: 1.2.3(@zenfs/core@2.3.4)(kerium@1.3.5)(memium@0.2.4)(utilium@2.3.9) + '@zenfs/core': + specifier: ^2.3.4 + version: 2.3.4 + '@zenfs/dom': + specifier: ^1.1.8 + version: 1.1.8(@zenfs/core@2.3.4)(kerium@1.3.5)(utilium@2.3.9) archiver: specifier: ^7.0.1 version: 7.0.1 @@ -67,6 +82,16 @@ importers: specifier: github:CCDirectLink/ultimate-crosscode-typedefs version: https://codeload.github.com/CCDirectLink/ultimate-crosscode-typedefs/tar.gz/a06a27682e34d07a7ea1a965f14f4138dedccbc7 + packages/ccmod-service-worker: + dependencies: + ultimate-crosscode-typedefs: + specifier: github:CCDirectLink/ultimate-crosscode-typedefs + version: https://codeload.github.com/CCDirectLink/ultimate-crosscode-typedefs/tar.gz/a06a27682e34d07a7ea1a965f14f4138dedccbc7 + devDependencies: + '@types/serviceworker': + specifier: ^0.0.145 + version: 0.0.145 + packages/common: dependencies: ultimate-crosscode-typedefs: @@ -309,6 +334,9 @@ packages: '@types/archiver@6.0.3': resolution: {integrity: sha512-a6wUll6k3zX6qs5KlxIggs1P1JcYJaTCx2gnlr+f0S1yd2DoaEwoIK10HmBaLnZwWneBz+JBm0dwcZu0zECBcQ==} + '@types/esbuild-copy-static-files@0.1.4': + resolution: {integrity: sha512-Xv4zewzzit6lccvS9sPcte2pe1xLDaKHm3KcjUEJuf1DV06oy2d0/DgjoGCHyM1bSMOrf1cFtB9pQmXJeVnJ0g==} + '@types/jquery@1.10.45': resolution: {integrity: sha512-JvoVtPbu1wrcOldn5gvuyJqr7DLX6I657N68Fqj5S7AUutdYZqabQjfP7dY6Gee7/Afo1uajwcORVlQfnsjkLQ==} @@ -318,6 +346,9 @@ packages: '@types/node@24.0.12': resolution: {integrity: sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g==} + '@types/node@24.0.15': + resolution: {integrity: sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==} + '@types/readdir-glob@1.1.5': resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==} @@ -327,6 +358,9 @@ packages: '@types/semver@7.7.0': resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==} + '@types/serviceworker@0.0.145': + resolution: {integrity: sha512-/DLt7KdKXqSXpDSAMu+qUtR25T07pLpw0HxAORmpSE4ytceJn/8EsPciFjiNtar67ArOjYmZsEN/Lt4LcAUMBQ==} + '@typescript-eslint/eslint-plugin@6.21.0': resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} engines: {node: ^16.0.0 || >=18.0.0} @@ -388,6 +422,31 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@xterm/xterm@5.5.0': + resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==} + + '@zenfs/archives@1.2.3': + resolution: {integrity: sha512-ZDirLB64Yj/6j2y5YTQJV/Wt1yDShpynr7LJZtqlB0NDCyReGtx40us6aAz+lfra8Y1mFs2plJ1FUXuLnpRQDg==} + engines: {node: '>= 20'} + peerDependencies: + '@zenfs/core': ^2.1.0 + kerium: ^1.3.4 + memium: ^0.2.3 + utilium: ^2.0.0 + + '@zenfs/core@2.3.4': + resolution: {integrity: sha512-stXq3miJ3yYwtcEtOWhqyyW19RZ5VTQqxRojARGjSog5BkAyP+Mc90IQFBk+2GAlvxi3LIiPgVCK4ucbXtxYMA==} + engines: {node: '>= 18'} + hasBin: true + + '@zenfs/dom@1.1.8': + resolution: {integrity: sha512-w/i+AfY9gGwEMtiNiJjYD+iQChVdl9ZdELWMY8dV594kxjGQGKz+Sl89MY8JAh+EbD/vUKSD1bTPFtYQP0oGmw==} + engines: {node: '>= 22'} + peerDependencies: + '@zenfs/core': ^2.1.0 + kerium: ^1.3.4 + utilium: ^2.0.0 + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -612,6 +671,9 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -635,6 +697,9 @@ packages: fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -780,6 +845,9 @@ packages: jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + kerium@1.3.5: + resolution: {integrity: sha512-V4hITm/a5l9SyPCPg//ENmbs24hHWVtEOQlyQaZX9bH/WBVY4kXwXaWPSobWGpNNOMOwe1FU/WgTd5dC/bIDKA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -807,6 +875,9 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + memium@0.2.4: + resolution: {integrity: sha512-ff9XlOqsHCLeh+I1HAap3dRbppxHznm8fX3XA3tevWcedwUHAixp7wh1D/W7hKs6VgFzCj2jmQxfnNv9A5WJ5Q==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1078,6 +1149,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utilium@2.3.9: + resolution: {integrity: sha512-zFmPNFpbPOqoLJ0St0SGjLhZ1+h+b6YRaOgeCIdUA+zNVrysX/ljUHN0J1j5lzqwzCRMLRwn2xdvHpthRMRGFQ==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1249,6 +1323,11 @@ snapshots: dependencies: '@types/readdir-glob': 1.1.5 + '@types/esbuild-copy-static-files@0.1.4': + dependencies: + '@types/node': 24.0.15 + esbuild: 0.25.8 + '@types/jquery@1.10.45': {} '@types/json-schema@7.0.15': {} @@ -1257,14 +1336,20 @@ snapshots: dependencies: undici-types: 7.8.0 + '@types/node@24.0.15': + dependencies: + undici-types: 7.8.0 + '@types/readdir-glob@1.1.5': dependencies: - '@types/node': 24.0.12 + '@types/node': 24.0.15 '@types/semver@6.2.7': {} '@types/semver@7.7.0': {} + '@types/serviceworker@0.0.145': {} + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -1353,6 +1438,33 @@ snapshots: '@ungap/structured-clone@1.3.0': {} + '@xterm/xterm@5.5.0': + optional: true + + '@zenfs/archives@1.2.3(@zenfs/core@2.3.4)(kerium@1.3.5)(memium@0.2.4)(utilium@2.3.9)': + dependencies: + '@zenfs/core': 2.3.4 + fflate: 0.8.2 + kerium: 1.3.5 + memium: 0.2.4 + utilium: 2.3.9 + + '@zenfs/core@2.3.4': + dependencies: + '@types/node': 24.0.15 + buffer: 6.0.3 + eventemitter3: 5.0.1 + kerium: 1.3.5 + memium: 0.2.4 + readable-stream: 4.7.0 + utilium: 2.3.9 + + '@zenfs/dom@1.1.8(@zenfs/core@2.3.4)(kerium@1.3.5)(utilium@2.3.9)': + dependencies: + '@zenfs/core': 2.3.4 + kerium: 1.3.5 + utilium: 2.3.9 + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -1622,6 +1734,8 @@ snapshots: event-target-shim@5.0.1: {} + eventemitter3@5.0.1: {} + events@3.3.0: {} fast-deep-equal@3.1.3: {} @@ -1644,6 +1758,8 @@ snapshots: dependencies: reusify: 1.1.0 + fflate@0.8.2: {} + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 @@ -1788,6 +1904,10 @@ snapshots: readable-stream: 2.3.8 setimmediate: 1.0.5 + kerium@1.3.5: + dependencies: + utilium: 2.3.9 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -1815,6 +1935,11 @@ snapshots: lru-cache@10.4.3: {} + memium@0.2.4: + dependencies: + kerium: 1.3.5 + utilium: 2.3.9 + merge2@1.4.1: {} micromatch@4.0.8: @@ -2057,6 +2182,12 @@ snapshots: util-deprecate@1.0.2: {} + utilium@2.3.9: + dependencies: + eventemitter3: 5.0.1 + optionalDependencies: + '@xterm/xterm': 5.5.0 + which@2.0.2: dependencies: isexe: 2.0.0 From 3dfd3228b6a713ce8e66161b6bbe031877ca3196 Mon Sep 17 00:00:00 2001 From: krypek Date: Mon, 21 Jul 2025 20:29:50 +0200 Subject: [PATCH 05/16] Fix build script ci failing --- build.ts | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/build.ts b/build.ts index 49d6c8e..8e7a23b 100644 --- a/build.ts +++ b/build.ts @@ -106,19 +106,24 @@ function ccmodServiceWorker(): esbuild.BuildOptions { const modules: (() => esbuild.BuildOptions)[] = [core, runtime, ccmodServiceWorker]; -if (isWatch) { - console.clear(); - await Promise.all( - modules.map(async (module) => { - const ctx = await esbuild.context(module()); - await ctx.watch(); - }), - ); -} else { - await Promise.all( - modules.map(async (module) => { - await esbuild.build(module()); - }), - ); - process.exit(); // because esbuild keeps the process alive for some reason +async function run() { + fs.promises.mkdir('./dist', { recursive: true }); + + if (isWatch) { + console.clear(); + await Promise.all( + modules.map(async (module) => { + const ctx = await esbuild.context(module()); + await ctx.watch(); + }), + ); + } else { + await Promise.all( + modules.map(async (module) => { + await esbuild.build(module()); + }), + ); + process.exit(); // because esbuild keeps the process alive for some reason + } } +run(); From 566b0322c3acd6de71c8b807748502fc7aa0bcf3 Mon Sep 17 00:00:00 2001 From: krypek Date: Mon, 21 Jul 2025 20:30:45 +0200 Subject: [PATCH 06/16] Fix formatting --- packages/core/src/service-worker-bridge.ts | 2 +- packages/runtime/src/resources.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/service-worker-bridge.ts b/packages/core/src/service-worker-bridge.ts index 8913845..ae0aa3c 100644 --- a/packages/core/src/service-worker-bridge.ts +++ b/packages/core/src/service-worker-bridge.ts @@ -53,7 +53,7 @@ function setMessageHandling() { }; } -let validCCModPaths: string[] | undefined +let validCCModPaths: string[] | undefined; export function isCCModPath(path: string): boolean { if (!validCCModPaths) return false; diff --git a/packages/runtime/src/resources.ts b/packages/runtime/src/resources.ts index a246f87..924e669 100644 --- a/packages/runtime/src/resources.ts +++ b/packages/runtime/src/resources.ts @@ -157,7 +157,7 @@ export async function loadImage( } if (shouldFetchRealData) { - const url = '/' + resolvedPath + const url = '/' + resolvedPath; data = await resourcesPlain.loadImage(url); } From 4a0a786519962555b3878bef0810a2e5914ae9c3 Mon Sep 17 00:00:00 2001 From: krypek Date: Mon, 21 Jul 2025 20:41:29 +0200 Subject: [PATCH 07/16] Fix lint --- .eslintignore | 1 + build.ts | 9 +++--- package.json | 4 --- packages/core/package.json | 8 ++++- packages/core/src/files.desktop.ts | 4 +-- packages/core/src/service-worker-bridge.ts | 26 ++++++++-------- packages/core/src/service-worker.ts | 18 ++++++----- packages/runtime/src/resources.ts | 2 +- pnpm-lock.yaml | 35 ++++++++-------------- 9 files changed, 52 insertions(+), 55 deletions(-) diff --git a/.eslintignore b/.eslintignore index 4d7921a..13ebe51 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,3 +2,4 @@ dist/ packages/common/src/vendor-libs ultimate-crosscode-typedefs/ +ccmod-service-worker.js diff --git a/build.ts b/build.ts index 8e7a23b..830c589 100644 --- a/build.ts +++ b/build.ts @@ -3,7 +3,7 @@ import copyStaticFiles from 'esbuild-copy-static-files'; import path from 'path'; import * as fs from 'fs'; -const isWatch = process.argv[2] == 'watch'; +const isWatch = process.argv[2] === 'watch'; function donePlugin(outfile: string, modifyCode?: (code: string) => string): esbuild.Plugin { return { @@ -19,7 +19,7 @@ function donePlugin(outfile: string, modifyCode?: (code: string) => string): esb const bytes = code.length; const kb = bytes / 1024; - console.log(outfile, kb.toFixed(1) + 'kb'); + console.log(outfile, `${kb.toFixed(1)}kb`); }); }, }; @@ -104,9 +104,9 @@ function ccmodServiceWorker(): esbuild.BuildOptions { }; } -const modules: (() => esbuild.BuildOptions)[] = [core, runtime, ccmodServiceWorker]; +const modules: Array<() => esbuild.BuildOptions> = [core, runtime, ccmodServiceWorker]; -async function run() { +async function run(): Promise { fs.promises.mkdir('./dist', { recursive: true }); if (isWatch) { @@ -123,6 +123,7 @@ async function run() { await esbuild.build(module()); }), ); + // eslint-disable-next-line no-process-exit process.exit(); // because esbuild keeps the process alive for some reason } } diff --git a/package.json b/package.json index f6893d4..53c816e 100644 --- a/package.json +++ b/package.json @@ -14,12 +14,8 @@ "@types/jquery": "^1.10.31", "@types/node": ">=24.0", "@types/semver": "6.2.x || 6.3.x", - "@types/serviceworker": "^0.0.145", "@typescript-eslint/eslint-plugin": "^6.20.0", "@typescript-eslint/parser": "^6.20.0", - "@zenfs/archives": "^1.2.3", - "@zenfs/core": "^2.3.4", - "@zenfs/dom": "^1.1.8", "archiver": "^7.0.1", "esbuild": "^0.25.8", "esbuild-copy-static-files": "^0.1.0", diff --git a/packages/core/package.json b/packages/core/package.json index 39fdc29..8efdf19 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -10,6 +10,12 @@ "dependencies": { "@ccloader3/common": "workspace:*", "@ccloader3/runtime": "workspace:*", - "ultimate-crosscode-typedefs": "github:CCDirectLink/ultimate-crosscode-typedefs" + "ultimate-crosscode-typedefs": "github:CCDirectLink/ultimate-crosscode-typedefs", + "@zenfs/archives": "^1.2.3", + "@zenfs/core": "^2.3.4", + "@zenfs/dom": "^1.1.8" + }, + "devDependencies": { + "@types/serviceworker": "^0.0.145" } } diff --git a/packages/core/src/files.desktop.ts b/packages/core/src/files.desktop.ts index 2878605..ec1d0cf 100644 --- a/packages/core/src/files.desktop.ts +++ b/packages/core/src/files.desktop.ts @@ -5,9 +5,9 @@ import { fs as zenFs } from '@zenfs/core'; import { isCCModPath } from './service-worker-bridge'; const fs: typeof import('fs') = window.require?.('fs'); -function preparePath(path: string) { +function preparePath(path: string): string { if (isCCModPath(path)) { - if (path[0] != '/') path = '/' + path; + if (!path.startsWith('/')) path = `/${ path}`; } return path; } diff --git a/packages/core/src/service-worker-bridge.ts b/packages/core/src/service-worker-bridge.ts index ae0aa3c..4925347 100644 --- a/packages/core/src/service-worker-bridge.ts +++ b/packages/core/src/service-worker-bridge.ts @@ -1,16 +1,16 @@ -import { fs, configure } from '@zenfs/core'; +import { configure, fs } from '@zenfs/core'; import { Zip } from '@zenfs/archives'; -export type ServiceWorkerPacket = { +export interface ServiceWorkerPacket { path: string; - data: Buffer; -}; + data: Buffer; +} export async function loadServiceWorker(): Promise { const currentRegistration = await window.navigator.serviceWorker.getRegistration(); if (currentRegistration) { //Do not await update since the worker only performs a simple task. Even if there is a bugfix it should be enough to not crash. - currentRegistration.update(); + void currentRegistration.update(); } else { const path = 'ccmod-service-worker.js'; await window.navigator.serviceWorker.register(path, { @@ -20,7 +20,7 @@ export async function loadServiceWorker(): Promise { }); } - const controller = window.navigator.serviceWorker.controller; + const {controller} = window.navigator.serviceWorker; if (!controller || controller.state !== 'activated') { window.location.reload(); window.location.href = window.location.toString(); @@ -34,12 +34,12 @@ export async function loadServiceWorker(): Promise { return controller; } -function sendMessage(packet: unknown) { - const controller = window.navigator.serviceWorker.controller; +function sendMessage(packet: unknown): void { + const {controller} = window.navigator.serviceWorker; controller?.postMessage(packet); } -function setMessageHandling() { +function setMessageHandling(): void { navigator.serviceWorker.onmessage = async (event) => { const path: string = event.data; @@ -57,16 +57,16 @@ let validCCModPaths: string[] | undefined; export function isCCModPath(path: string): boolean { if (!validCCModPaths) return false; - if (path[0] != '/') path = '/' + path; + if (!path.startsWith('/')) path = `/${ path}`; return validCCModPaths.some( (pathPrefix) => path.length > pathPrefix.length && path.startsWith(pathPrefix), ); } -export async function loadCCMods(allModsList: Array<{ parentDir: string; dir: string }>) { +export async function loadCCMods(allModsList: Array<{ parentDir: string; dir: string }>): Promise { const ccmods: typeof allModsList = allModsList.filter((mod) => mod.dir.endsWith('.ccmod')); const ccmodArrayBuffers = await Promise.all( - ccmods.map(async (mod) => (await fetch('../' + mod.dir)).arrayBuffer()), + ccmods.map(async (mod) => (await fetch(`../${ mod.dir}`)).arrayBuffer()), ); const mounts: Parameters[0]['mounts'] = Object.fromEntries( @@ -77,6 +77,6 @@ export async function loadCCMods(allModsList: Array<{ parentDir: string; dir: st mounts, }); - validCCModPaths = ccmods.map((mod) => '/' + mod.dir); + validCCModPaths = ccmods.map((mod) => `/${ mod.dir}`); sendMessage(validCCModPaths); } diff --git a/packages/core/src/service-worker.ts b/packages/core/src/service-worker.ts index fab7b97..9de438e 100644 --- a/packages/core/src/service-worker.ts +++ b/packages/core/src/service-worker.ts @@ -1,11 +1,11 @@ import type { ServiceWorkerPacket } from './service-worker-bridge'; self.addEventListener('activate', () => { - self.clients.claim(); + void self.clients.claim(); }); self.addEventListener('install', () => { - self.skipWaiting(); + void self.skipWaiting(); }); const CONTENT_TYPES: Record = { @@ -23,17 +23,19 @@ function contentType(url: string): string { return CONTENT_TYPES[url.substring(url.lastIndexOf('.') + 1)] || 'text/plain'; } -async function post(data: unknown) { +async function post(data: unknown): Promise { const clients = await self.clients.matchAll(); const client = clients[0]; client.postMessage(data); } -const waitingFor: Map void> = new Map(); +const waitingFor = new Map void>(); async function requestContents(path: string): Promise { let resolve!: (packet: ServiceWorkerPacket) => void; - const promise: Promise = new Promise((res) => (resolve = res)); + const promise = new Promise((res) => { + resolve = res; + }); await post(path); waitingFor.set(path, resolve); @@ -54,7 +56,7 @@ async function requestContents(path: string): Promise { let validPaths: string[] | undefined; -self.addEventListener('message', async (event) => { +self.addEventListener('message', (event) => { if (Array.isArray(event.data)) { validPaths = event.data; } else { @@ -65,10 +67,10 @@ self.addEventListener('message', async (event) => { } }); -self.addEventListener('fetch', async (event: FetchEvent) => { +self.addEventListener('fetch', (event: FetchEvent) => { if (!validPaths) return; - const request = event.request; + const { request } = event; const path = new URL(request.url).pathname; if ( diff --git a/packages/runtime/src/resources.ts b/packages/runtime/src/resources.ts index 924e669..7a2f4f5 100644 --- a/packages/runtime/src/resources.ts +++ b/packages/runtime/src/resources.ts @@ -157,7 +157,7 @@ export async function loadImage( } if (shouldFetchRealData) { - const url = '/' + resolvedPath; + const url = `/${resolvedPath}`; data = await resourcesPlain.loadImage(url); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 461f0d1..3d25b39 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,24 +33,12 @@ importers: '@types/semver': specifier: 6.2.x || 6.3.x version: 6.2.7 - '@types/serviceworker': - specifier: ^0.0.145 - version: 0.0.145 '@typescript-eslint/eslint-plugin': specifier: ^6.20.0 version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3) '@typescript-eslint/parser': specifier: ^6.20.0 version: 6.21.0(eslint@8.57.1)(typescript@5.8.3) - '@zenfs/archives': - specifier: ^1.2.3 - version: 1.2.3(@zenfs/core@2.3.4)(kerium@1.3.5)(memium@0.2.4)(utilium@2.3.9) - '@zenfs/core': - specifier: ^2.3.4 - version: 2.3.4 - '@zenfs/dom': - specifier: ^1.1.8 - version: 1.1.8(@zenfs/core@2.3.4)(kerium@1.3.5)(utilium@2.3.9) archiver: specifier: ^7.0.1 version: 7.0.1 @@ -82,16 +70,6 @@ importers: specifier: github:CCDirectLink/ultimate-crosscode-typedefs version: https://codeload.github.com/CCDirectLink/ultimate-crosscode-typedefs/tar.gz/a06a27682e34d07a7ea1a965f14f4138dedccbc7 - packages/ccmod-service-worker: - dependencies: - ultimate-crosscode-typedefs: - specifier: github:CCDirectLink/ultimate-crosscode-typedefs - version: https://codeload.github.com/CCDirectLink/ultimate-crosscode-typedefs/tar.gz/a06a27682e34d07a7ea1a965f14f4138dedccbc7 - devDependencies: - '@types/serviceworker': - specifier: ^0.0.145 - version: 0.0.145 - packages/common: dependencies: ultimate-crosscode-typedefs: @@ -106,9 +84,22 @@ importers: '@ccloader3/runtime': specifier: workspace:* version: link:../runtime + '@zenfs/archives': + specifier: ^1.2.3 + version: 1.2.3(@zenfs/core@2.3.4)(kerium@1.3.5)(memium@0.2.4)(utilium@2.3.9) + '@zenfs/core': + specifier: ^2.3.4 + version: 2.3.4 + '@zenfs/dom': + specifier: ^1.1.8 + version: 1.1.8(@zenfs/core@2.3.4)(kerium@1.3.5)(utilium@2.3.9) ultimate-crosscode-typedefs: specifier: github:CCDirectLink/ultimate-crosscode-typedefs version: https://codeload.github.com/CCDirectLink/ultimate-crosscode-typedefs/tar.gz/a06a27682e34d07a7ea1a965f14f4138dedccbc7 + devDependencies: + '@types/serviceworker': + specifier: ^0.0.145 + version: 0.0.145 packages/runtime: dependencies: From c4306a8e6ee8196a3c68f32db0c70d148b79760f Mon Sep 17 00:00:00 2001 From: krypek Date: Mon, 21 Jul 2025 20:43:16 +0200 Subject: [PATCH 08/16] Fix formatting (lint fix changed it) --- packages/core/src/files.desktop.ts | 2 +- packages/core/src/service-worker-bridge.ts | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/core/src/files.desktop.ts b/packages/core/src/files.desktop.ts index ec1d0cf..6246d5f 100644 --- a/packages/core/src/files.desktop.ts +++ b/packages/core/src/files.desktop.ts @@ -7,7 +7,7 @@ const fs: typeof import('fs') = window.require?.('fs'); function preparePath(path: string): string { if (isCCModPath(path)) { - if (!path.startsWith('/')) path = `/${ path}`; + if (!path.startsWith('/')) path = `/${path}`; } return path; } diff --git a/packages/core/src/service-worker-bridge.ts b/packages/core/src/service-worker-bridge.ts index 4925347..39c5ead 100644 --- a/packages/core/src/service-worker-bridge.ts +++ b/packages/core/src/service-worker-bridge.ts @@ -20,7 +20,7 @@ export async function loadServiceWorker(): Promise { }); } - const {controller} = window.navigator.serviceWorker; + const { controller } = window.navigator.serviceWorker; if (!controller || controller.state !== 'activated') { window.location.reload(); window.location.href = window.location.toString(); @@ -35,7 +35,7 @@ export async function loadServiceWorker(): Promise { } function sendMessage(packet: unknown): void { - const {controller} = window.navigator.serviceWorker; + const { controller } = window.navigator.serviceWorker; controller?.postMessage(packet); } @@ -57,16 +57,18 @@ let validCCModPaths: string[] | undefined; export function isCCModPath(path: string): boolean { if (!validCCModPaths) return false; - if (!path.startsWith('/')) path = `/${ path}`; + if (!path.startsWith('/')) path = `/${path}`; return validCCModPaths.some( (pathPrefix) => path.length > pathPrefix.length && path.startsWith(pathPrefix), ); } -export async function loadCCMods(allModsList: Array<{ parentDir: string; dir: string }>): Promise { +export async function loadCCMods( + allModsList: Array<{ parentDir: string; dir: string }>, +): Promise { const ccmods: typeof allModsList = allModsList.filter((mod) => mod.dir.endsWith('.ccmod')); const ccmodArrayBuffers = await Promise.all( - ccmods.map(async (mod) => (await fetch(`../${ mod.dir}`)).arrayBuffer()), + ccmods.map(async (mod) => (await fetch(`../${mod.dir}`)).arrayBuffer()), ); const mounts: Parameters[0]['mounts'] = Object.fromEntries( @@ -77,6 +79,6 @@ export async function loadCCMods(allModsList: Array<{ parentDir: string; dir: st mounts, }); - validCCModPaths = ccmods.map((mod) => `/${ mod.dir}`); + validCCModPaths = ccmods.map((mod) => `/${mod.dir}`); sendMessage(validCCModPaths); } From e0142e8043b32bd2374fc4baa289ffe0a0ef9939 Mon Sep 17 00:00:00 2001 From: krypek Date: Tue, 22 Jul 2025 10:06:30 +0200 Subject: [PATCH 09/16] Rename service worker dist script --- .eslintignore | 2 +- .gitignore | 2 +- build.ts | 2 +- packages/core/src/service-worker-bridge.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.eslintignore b/.eslintignore index 13ebe51..7fa3107 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,4 +2,4 @@ dist/ packages/common/src/vendor-libs ultimate-crosscode-typedefs/ -ccmod-service-worker.js +dist-ccmod-service-worker.js diff --git a/.gitignore b/.gitignore index 8f95227..9893c46 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ dist/ *.tsbuildinfo /ultimate-crosscode-typedefs/ -/ccmod-service-worker.js +/dist-ccmod-service-worker.js ccloader_*_quick-install.* ccloader_*_package.* diff --git a/build.ts b/build.ts index 830c589..7b02665 100644 --- a/build.ts +++ b/build.ts @@ -94,7 +94,7 @@ function runtime(): esbuild.BuildOptions { } function ccmodServiceWorker(): esbuild.BuildOptions { - const outfile = path.join('./', 'ccmod-service-worker.js'); + const outfile = path.join('./', 'dist-ccmod-service-worker.js'); return { entryPoints: ['packages/core/src/service-worker.ts'], diff --git a/packages/core/src/service-worker-bridge.ts b/packages/core/src/service-worker-bridge.ts index 39c5ead..2601ece 100644 --- a/packages/core/src/service-worker-bridge.ts +++ b/packages/core/src/service-worker-bridge.ts @@ -12,7 +12,7 @@ export async function loadServiceWorker(): Promise { //Do not await update since the worker only performs a simple task. Even if there is a bugfix it should be enough to not crash. void currentRegistration.update(); } else { - const path = 'ccmod-service-worker.js'; + const path = 'dist-ccmod-service-worker.js'; await window.navigator.serviceWorker.register(path, { updateViaCache: 'none', type: 'module', From 944b7e11c7956cfbac3da936e05bc917b580c1d9 Mon Sep 17 00:00:00 2001 From: krypek Date: Tue, 22 Jul 2025 13:07:42 +0200 Subject: [PATCH 10/16] Improve browser compatibility --- packages/common/src/console.ts | 2 +- packages/common/src/require.ts | 4 ++-- packages/core/src/files.browser.ts | 3 ++- packages/core/src/mod-data-storage.ts | 4 ++-- packages/runtime/src/_postload.ts | 1 + .../runtime/src/font-race-condition-fix.ts | 21 +++++++++++++++++++ packages/runtime/src/greenworks-fix.ts | 4 ++-- 7 files changed, 31 insertions(+), 8 deletions(-) create mode 100644 packages/runtime/src/font-race-condition-fix.ts diff --git a/packages/common/src/console.ts b/packages/common/src/console.ts index 1d9bc59..e87d259 100644 --- a/packages/common/src/console.ts +++ b/packages/common/src/console.ts @@ -1,6 +1,6 @@ import * as utils from './utils'; -const nodejsUtil = window.require('util') as typeof import('util'); +const nodejsUtil = window.require?.('util') as typeof import('util'); export enum LogLevel { LOG = 2, diff --git a/packages/common/src/require.ts b/packages/common/src/require.ts index b5b8a22..59b024e 100644 --- a/packages/common/src/require.ts +++ b/packages/common/src/require.ts @@ -5,8 +5,8 @@ import * as utils from './utils'; let requireFixed: NodeRequire = null!; -if (typeof require === 'function') { - const paths = require('path') as typeof import('path'); +if (typeof window.require === 'function') { + const paths = window.require('path') as typeof import('path'); requireFixed = ((id) => { try { diff --git a/packages/core/src/files.browser.ts b/packages/core/src/files.browser.ts index 704fc74..3d7be06 100644 --- a/packages/core/src/files.browser.ts +++ b/packages/core/src/files.browser.ts @@ -57,6 +57,7 @@ export async function getInstalledExtensions(config: Config): Promise if (utils.errorHasMessage(err)) { err.message = `Failed to send request to '${extensionsApiUrl}': ${err.message}`; } - throw err; + // throw err; + return [] } } diff --git a/packages/core/src/mod-data-storage.ts b/packages/core/src/mod-data-storage.ts index ab527d1..7d57526 100644 --- a/packages/core/src/mod-data-storage.ts +++ b/packages/core/src/mod-data-storage.ts @@ -11,8 +11,8 @@ import { FileData } from 'ultimate-crosscode-typedefs/file-types/mod-data-storag import { ModEntry } from 'ultimate-crosscode-typedefs/file-types/mod-data-storage/v1'; import { ModID } from 'ultimate-crosscode-typedefs/modloader/mod'; -const fs = (window.require('fs') as typeof import('fs')).promises; -const pathsNative = window.require('path') as typeof import('path'); +const fs = (window.require?.('fs') as typeof import('fs'))?.promises; +const pathsNative = window.require?.('path') as typeof import('path'); export const FILE_NAME = 'cc-mod-settings.json'; diff --git a/packages/runtime/src/_postload.ts b/packages/runtime/src/_postload.ts index 7a4b11b..16316c2 100644 --- a/packages/runtime/src/_postload.ts +++ b/packages/runtime/src/_postload.ts @@ -1,4 +1,5 @@ import './font'; +import './font-race-condition-fix'; import './options'; import './input'; diff --git a/packages/runtime/src/font-race-condition-fix.ts b/packages/runtime/src/font-race-condition-fix.ts new file mode 100644 index 0000000..55ff4eb --- /dev/null +++ b/packages/runtime/src/font-race-condition-fix.ts @@ -0,0 +1,21 @@ +ig.module('ccloader-runtime.stdlib.font-race-condition-fix') + .requires('impact.base.font', 'game.feature.font.font-system') + .defines(() => { + // this._doCallback() (that calls this.loadingFinished(true)) is called + // before the labels are applied to ig.lang.labels, so very rarely + // (for some reason I've only encourneted this issue in browsers, + // it may happen in nwjs, but I'm not sure) gui code is executed before + // the labels are applied, resulting in 'UNKNOWN LABEL' + // (on the title screen, for example) + ig.Lang.inject({ + onload(...args) { + const backup = this._doCallback; + this._doCallback = () => {}; + this.parent(...args); + + this._doCallback = backup; + + this._doCallback(); + }, + }); + }); diff --git a/packages/runtime/src/greenworks-fix.ts b/packages/runtime/src/greenworks-fix.ts index ab6c6b3..300695d 100644 --- a/packages/runtime/src/greenworks-fix.ts +++ b/packages/runtime/src/greenworks-fix.ts @@ -3,8 +3,8 @@ import * as impactModuleHooks from './impact-module-hooks'; import * as resources from './resources'; -if (typeof require !== 'undefined') { - const pathsNative = require('path') as typeof import('path'); +if (typeof window.require !== 'undefined') { + const pathsNative = window.require('path') as typeof import('path'); const GAME_ASSETS_FS_PATH = pathsNative.join( process.cwd(), From 63dd5d4d1b2a72f010fcda184208672ac34b8889 Mon Sep 17 00:00:00 2001 From: krypek Date: Tue, 22 Jul 2025 15:46:07 +0200 Subject: [PATCH 11/16] Refactor .ccmod code and use fflate instead of zenfs --- build.ts | 2 +- packages/core/package.json | 6 +- packages/core/src/files.android.ts | 2 +- packages/core/src/files.browser.ts | 12 ++- packages/core/src/files.ccmod.ts | 71 +++++++++++++++++ packages/core/src/files.desktop.ts | 27 +------ packages/core/src/files.ts | 22 ++++- packages/core/src/mod.ts | 6 +- packages/core/src/modloader.ts | 3 +- packages/core/src/service-worker-bridge.ts | 41 ++-------- pnpm-lock.yaml | 93 +--------------------- 11 files changed, 121 insertions(+), 164 deletions(-) create mode 100644 packages/core/src/files.ccmod.ts diff --git a/build.ts b/build.ts index 7b02665..86d009b 100644 --- a/build.ts +++ b/build.ts @@ -10,7 +10,7 @@ function donePlugin(outfile: string, modifyCode?: (code: string) => string): esb name: 'done plugin', setup(build) { build.onEnd(async (res) => { - let code = res.outputFiles![0].text; + let code = res.outputFiles![0]?.text; if (!code) return; // when compile errors if (modifyCode) code = modifyCode(code); diff --git a/packages/core/package.json b/packages/core/package.json index 8efdf19..bea2fc9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -10,10 +10,8 @@ "dependencies": { "@ccloader3/common": "workspace:*", "@ccloader3/runtime": "workspace:*", - "ultimate-crosscode-typedefs": "github:CCDirectLink/ultimate-crosscode-typedefs", - "@zenfs/archives": "^1.2.3", - "@zenfs/core": "^2.3.4", - "@zenfs/dom": "^1.1.8" + "fflate": "^0.8.2", + "ultimate-crosscode-typedefs": "github:CCDirectLink/ultimate-crosscode-typedefs" }, "devDependencies": { "@types/serviceworker": "^0.0.145" diff --git a/packages/core/src/files.android.ts b/packages/core/src/files.android.ts index 10b6ba3..ed87ab7 100644 --- a/packages/core/src/files.android.ts +++ b/packages/core/src/files.android.ts @@ -1,7 +1,7 @@ import * as filesBrowser from './files.browser'; import { Config } from './config'; -export { isReadable, loadText } from './files.browser'; +export { isReadable, readFile, loadText } from './files.browser'; export async function getModDirectoriesIn(dir: string, config: Config): Promise { if (dir === `${config.gameAssetsDir}mods/`) { diff --git a/packages/core/src/files.browser.ts b/packages/core/src/files.browser.ts index 3d7be06..6633ffe 100644 --- a/packages/core/src/files.browser.ts +++ b/packages/core/src/files.browser.ts @@ -2,11 +2,11 @@ import * as utils from '@ccloader3/common/utils'; import * as paths from '@ccloader3/common/paths'; import { Config } from './config'; -export async function loadText(path: string): Promise { +async function request(path: string): Promise { try { let res = await fetch(utils.cwdFilePathToURL(path).href); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); - return await res.text(); + return res; } catch (err) { if (utils.errorHasMessage(err)) { err.message = `Failed to load file '${path}': ${err.message}`; @@ -15,6 +15,14 @@ export async function loadText(path: string): Promise { } } +export async function readFile(path: string): Promise { + return (await request(path)).arrayBuffer(); +} + +export async function loadText(path: string): Promise { + return (await request(path)).text(); +} + export async function isReadable(path: string): Promise { try { let res = await fetch(utils.cwdFilePathToURL(path).href, { method: 'HEAD' }); diff --git a/packages/core/src/files.ccmod.ts b/packages/core/src/files.ccmod.ts new file mode 100644 index 0000000..c1a7ff1 --- /dev/null +++ b/packages/core/src/files.ccmod.ts @@ -0,0 +1,71 @@ +import { unzipSync, type Unzipped } from 'fflate/browser'; +import { sendServiceWorkerMessage } from './service-worker-bridge'; + +const files = new Map(); + +function findPrefix(path: string): [string, string] | undefined { + if (path[0] == '/') path = path.substring(1); + const prefix = [...files.keys()].find((dir) => path.startsWith(dir)); + + if (!prefix) return; + + path = path.substring(prefix.length + 1); + return [path, prefix]; +} + +export async function isReadable(path: string): Promise { + return !!findPrefix(path); +} + +export async function loadText(path: string): Promise { + const buf = await readFile(path); + if (!buf) return; + + return new TextDecoder('utf-8').decode(buf); +} + +export async function readFile(path: string): Promise { + const prefixObj = findPrefix(path); + if (!prefixObj) return; + const [relativePath, prefix] = prefixObj; + + const unzipped = files.get(prefix)!; + + const data = unzipped[relativePath]; + + return data.buffer; +} + +export async function findRecursively(dir: string): Promise { + const prefixObj = findPrefix(dir); + if (!prefixObj) return; + const [relativePath, prefix] = prefixObj; + + const unzipped = files.get(prefix)!; + + const dirs = Object.keys(unzipped) + .filter((path) => !path.endsWith('/') && path.startsWith(relativePath)) + .map((dir) => dir.substring(relativePath.length)); + return dirs; +} + +export async function loadCCMods( + allModsList: Array<{ parentDir: string; dir: string }>, +): Promise { + const ccmods: typeof allModsList = allModsList.filter((mod) => mod.dir.endsWith('.ccmod')); + const ccmodArrayBuffers = await Promise.all( + ccmods.map(async (mod) => (await fetch(`../${mod.dir}`)).bytes()), + ); + + // console.time('uncompress'); + const uncompressed = ccmodArrayBuffers.map((buf) => unzipSync(buf)); + // console.timeEnd('uncompress'); + + for (let i = 0; i < ccmods.length; i++) { + const mod = ccmods[i]; + const buf = uncompressed[i]; + files.set(mod.dir, buf); + } + + sendServiceWorkerMessage(ccmods.map((mod) => `/${mod.dir}`)); +} diff --git a/packages/core/src/files.desktop.ts b/packages/core/src/files.desktop.ts index 6246d5f..5ff1e08 100644 --- a/packages/core/src/files.desktop.ts +++ b/packages/core/src/files.desktop.ts @@ -1,34 +1,17 @@ import * as utils from '@ccloader3/common/utils'; import { Config } from './config'; -import { fs as zenFs } from '@zenfs/core'; -import { isCCModPath } from './service-worker-bridge'; const fs: typeof import('fs') = window.require?.('fs'); -function preparePath(path: string): string { - if (isCCModPath(path)) { - if (!path.startsWith('/')) path = `/${path}`; - } - return path; -} - -function getFs(path: string): typeof import('fs') | (typeof import('@zenfs/core'))['fs'] { - if (isCCModPath(path)) { - return zenFs; - } else { - return fs; - } +export async function readFile(path: string): Promise { + return fs.promises.readFile(path); } export async function loadText(path: string): Promise { - path = preparePath(path); - const fs = getFs(path); return fs.promises.readFile(path, 'utf8'); } export async function isReadable(path: string): Promise { - path = preparePath(path); - const fs = getFs(path); try { await fs.promises.access(path, fs.constants.R_OK); return true; @@ -50,9 +33,6 @@ async function findRecursivelyInternal( relativePrefix: string, fileList: string[], ): Promise { - currentDir = preparePath(currentDir); - const fs = getFs(currentDir); - let contents: string[]; try { contents = await fs.promises.readdir(currentDir); @@ -75,9 +55,6 @@ async function findRecursivelyInternal( } export async function getModDirectoriesIn(dir: string, _config: Config): Promise { - dir = preparePath(dir); - const fs = getFs(dir); - if (dir.endsWith('/')) dir = dir.slice(0, -1); let allContents: string[]; diff --git a/packages/core/src/files.ts b/packages/core/src/files.ts index 7c13339..1c8c53a 100644 --- a/packages/core/src/files.ts +++ b/packages/core/src/files.ts @@ -1,6 +1,7 @@ import * as filesDesktop from './files.desktop'; import * as filesBrowser from './files.browser'; import * as filesAndroid from './files.android'; +import * as filesCCMod from './files.ccmod'; import * as utils from '@ccloader3/common/utils'; const modules = { @@ -10,4 +11,23 @@ const modules = { }; const currentModule = modules[utils.PLATFORM_TYPE]; -export const { loadText, isReadable, getModDirectoriesIn, getInstalledExtensions } = currentModule; +export const { getModDirectoriesIn, getInstalledExtensions } = currentModule; + +export async function isReadable(path: string): Promise { + return (await filesCCMod.isReadable(path)) || currentModule.isReadable(path); +} + +export async function readFile(path: string): Promise { + return (await filesCCMod.readFile(path)) || currentModule.readFile(path); +} + +export async function loadText(path: string) { + return (await filesCCMod.loadText(path)) || currentModule.loadText(path); +} + +export async function findRecursively(dir: string): Promise { + return ( + (await filesCCMod.findRecursively(dir)) || + ('findRecursively' in currentModule ? currentModule.findRecursively(dir) : undefined) + ); +} diff --git a/packages/core/src/mod.ts b/packages/core/src/mod.ts index de03f76..e55d295 100644 --- a/packages/core/src/mod.ts +++ b/packages/core/src/mod.ts @@ -1,7 +1,7 @@ import semver from '@ccloader3/common/vendor-libs/semver'; import * as paths from '@ccloader3/common/paths'; import * as utils from '@ccloader3/common/utils'; -import * as filesDesktop from './files.desktop'; +import * as files from './files'; import { Manifest } from 'ultimate-crosscode-typedefs/file-types/mod-manifest'; import { Dependency, @@ -77,8 +77,8 @@ export class Mod implements ModPublic { let assets: string[] = []; if (this.manifest.assets != null) { assets = this.manifest.assets.map((path) => paths.jailRelative(path)); - } else if (utils.PLATFORM_TYPE === utils.PlatformType.DESKTOP) { - assets = await filesDesktop.findRecursively(this.assetsDirectory); + } else { + assets = (await files.findRecursively(this.assetsDirectory)) ?? []; } this.assets = new Set(assets); } diff --git a/packages/core/src/modloader.ts b/packages/core/src/modloader.ts index 4f1d9e3..ccc3ce3 100644 --- a/packages/core/src/modloader.ts +++ b/packages/core/src/modloader.ts @@ -10,7 +10,8 @@ import * as modDataStorage from './mod-data-storage'; import { LegacyManifest, Manifest } from 'ultimate-crosscode-typedefs/file-types/mod-manifest'; import { LoadingStage, ModID } from 'ultimate-crosscode-typedefs/modloader/mod'; import * as consoleM from '@ccloader3/common/console'; -import { loadCCMods, loadServiceWorker } from './service-worker-bridge'; +import { loadServiceWorker } from './service-worker-bridge'; +import { loadCCMods } from './files.ccmod'; type ModsMap = Map; type ReadonlyModsMap = ReadonlyMap; diff --git a/packages/core/src/service-worker-bridge.ts b/packages/core/src/service-worker-bridge.ts index 2601ece..f13e5fd 100644 --- a/packages/core/src/service-worker-bridge.ts +++ b/packages/core/src/service-worker-bridge.ts @@ -1,9 +1,8 @@ -import { configure, fs } from '@zenfs/core'; -import { Zip } from '@zenfs/archives'; +import * as filesCCMod from './files.ccmod'; export interface ServiceWorkerPacket { path: string; - data: Buffer; + data: Buffer | undefined; } export async function loadServiceWorker(): Promise { @@ -34,7 +33,7 @@ export async function loadServiceWorker(): Promise { return controller; } -function sendMessage(packet: unknown): void { +export function sendServiceWorkerMessage(packet: unknown): void { const { controller } = window.navigator.serviceWorker; controller?.postMessage(packet); } @@ -43,42 +42,12 @@ function setMessageHandling(): void { navigator.serviceWorker.onmessage = async (event) => { const path: string = event.data; - const data = await fs.promises.readFile(path); + const data = await filesCCMod.readFile(path); const packet: ServiceWorkerPacket = { path, data, }; - sendMessage(packet); + sendServiceWorkerMessage(packet); }; } - -let validCCModPaths: string[] | undefined; -export function isCCModPath(path: string): boolean { - if (!validCCModPaths) return false; - - if (!path.startsWith('/')) path = `/${path}`; - return validCCModPaths.some( - (pathPrefix) => path.length > pathPrefix.length && path.startsWith(pathPrefix), - ); -} - -export async function loadCCMods( - allModsList: Array<{ parentDir: string; dir: string }>, -): Promise { - const ccmods: typeof allModsList = allModsList.filter((mod) => mod.dir.endsWith('.ccmod')); - const ccmodArrayBuffers = await Promise.all( - ccmods.map(async (mod) => (await fetch(`../${mod.dir}`)).arrayBuffer()), - ); - - const mounts: Parameters[0]['mounts'] = Object.fromEntries( - ccmods.map((mod, i) => [mod.dir, { backend: Zip, data: ccmodArrayBuffers[i] }]), - ); - - await configure({ - mounts, - }); - - validCCModPaths = ccmods.map((mod) => `/${mod.dir}`); - sendMessage(validCCModPaths); -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d25b39..4b6fa69 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,15 +84,9 @@ importers: '@ccloader3/runtime': specifier: workspace:* version: link:../runtime - '@zenfs/archives': - specifier: ^1.2.3 - version: 1.2.3(@zenfs/core@2.3.4)(kerium@1.3.5)(memium@0.2.4)(utilium@2.3.9) - '@zenfs/core': - specifier: ^2.3.4 - version: 2.3.4 - '@zenfs/dom': - specifier: ^1.1.8 - version: 1.1.8(@zenfs/core@2.3.4)(kerium@1.3.5)(utilium@2.3.9) + fflate: + specifier: ^0.8.2 + version: 0.8.2 ultimate-crosscode-typedefs: specifier: github:CCDirectLink/ultimate-crosscode-typedefs version: https://codeload.github.com/CCDirectLink/ultimate-crosscode-typedefs/tar.gz/a06a27682e34d07a7ea1a965f14f4138dedccbc7 @@ -413,31 +407,6 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@xterm/xterm@5.5.0': - resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==} - - '@zenfs/archives@1.2.3': - resolution: {integrity: sha512-ZDirLB64Yj/6j2y5YTQJV/Wt1yDShpynr7LJZtqlB0NDCyReGtx40us6aAz+lfra8Y1mFs2plJ1FUXuLnpRQDg==} - engines: {node: '>= 20'} - peerDependencies: - '@zenfs/core': ^2.1.0 - kerium: ^1.3.4 - memium: ^0.2.3 - utilium: ^2.0.0 - - '@zenfs/core@2.3.4': - resolution: {integrity: sha512-stXq3miJ3yYwtcEtOWhqyyW19RZ5VTQqxRojARGjSog5BkAyP+Mc90IQFBk+2GAlvxi3LIiPgVCK4ucbXtxYMA==} - engines: {node: '>= 18'} - hasBin: true - - '@zenfs/dom@1.1.8': - resolution: {integrity: sha512-w/i+AfY9gGwEMtiNiJjYD+iQChVdl9ZdELWMY8dV594kxjGQGKz+Sl89MY8JAh+EbD/vUKSD1bTPFtYQP0oGmw==} - engines: {node: '>= 22'} - peerDependencies: - '@zenfs/core': ^2.1.0 - kerium: ^1.3.4 - utilium: ^2.0.0 - abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -662,9 +631,6 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} - eventemitter3@5.0.1: - resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} - events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -836,9 +802,6 @@ packages: jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} - kerium@1.3.5: - resolution: {integrity: sha512-V4hITm/a5l9SyPCPg//ENmbs24hHWVtEOQlyQaZX9bH/WBVY4kXwXaWPSobWGpNNOMOwe1FU/WgTd5dC/bIDKA==} - keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -866,9 +829,6 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - memium@0.2.4: - resolution: {integrity: sha512-ff9XlOqsHCLeh+I1HAap3dRbppxHznm8fX3XA3tevWcedwUHAixp7wh1D/W7hKs6VgFzCj2jmQxfnNv9A5WJ5Q==} - merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1140,9 +1100,6 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - utilium@2.3.9: - resolution: {integrity: sha512-zFmPNFpbPOqoLJ0St0SGjLhZ1+h+b6YRaOgeCIdUA+zNVrysX/ljUHN0J1j5lzqwzCRMLRwn2xdvHpthRMRGFQ==} - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1429,33 +1386,6 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@xterm/xterm@5.5.0': - optional: true - - '@zenfs/archives@1.2.3(@zenfs/core@2.3.4)(kerium@1.3.5)(memium@0.2.4)(utilium@2.3.9)': - dependencies: - '@zenfs/core': 2.3.4 - fflate: 0.8.2 - kerium: 1.3.5 - memium: 0.2.4 - utilium: 2.3.9 - - '@zenfs/core@2.3.4': - dependencies: - '@types/node': 24.0.15 - buffer: 6.0.3 - eventemitter3: 5.0.1 - kerium: 1.3.5 - memium: 0.2.4 - readable-stream: 4.7.0 - utilium: 2.3.9 - - '@zenfs/dom@1.1.8(@zenfs/core@2.3.4)(kerium@1.3.5)(utilium@2.3.9)': - dependencies: - '@zenfs/core': 2.3.4 - kerium: 1.3.5 - utilium: 2.3.9 - abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -1725,8 +1655,6 @@ snapshots: event-target-shim@5.0.1: {} - eventemitter3@5.0.1: {} - events@3.3.0: {} fast-deep-equal@3.1.3: {} @@ -1895,10 +1823,6 @@ snapshots: readable-stream: 2.3.8 setimmediate: 1.0.5 - kerium@1.3.5: - dependencies: - utilium: 2.3.9 - keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -1926,11 +1850,6 @@ snapshots: lru-cache@10.4.3: {} - memium@0.2.4: - dependencies: - kerium: 1.3.5 - utilium: 2.3.9 - merge2@1.4.1: {} micromatch@4.0.8: @@ -2173,12 +2092,6 @@ snapshots: util-deprecate@1.0.2: {} - utilium@2.3.9: - dependencies: - eventemitter3: 5.0.1 - optionalDependencies: - '@xterm/xterm': 5.5.0 - which@2.0.2: dependencies: isexe: 2.0.0 From c240e3b7d4cf1d5c2be7cc2615bed44314f7ad6a Mon Sep 17 00:00:00 2001 From: krypek Date: Tue, 22 Jul 2025 15:57:13 +0200 Subject: [PATCH 12/16] Fix lint --- packages/core/src/files.browser.ts | 2 +- packages/core/src/files.ccmod.ts | 26 +++++++++++++++----------- packages/core/src/files.ts | 6 +++--- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/core/src/files.browser.ts b/packages/core/src/files.browser.ts index 6633ffe..71cc1be 100644 --- a/packages/core/src/files.browser.ts +++ b/packages/core/src/files.browser.ts @@ -66,6 +66,6 @@ export async function getInstalledExtensions(config: Config): Promise err.message = `Failed to send request to '${extensionsApiUrl}': ${err.message}`; } // throw err; - return [] + return []; } } diff --git a/packages/core/src/files.ccmod.ts b/packages/core/src/files.ccmod.ts index c1a7ff1..6508f50 100644 --- a/packages/core/src/files.ccmod.ts +++ b/packages/core/src/files.ccmod.ts @@ -1,32 +1,34 @@ -import { unzipSync, type Unzipped } from 'fflate/browser'; +import { type Unzipped, unzipSync } from 'fflate/browser'; import { sendServiceWorkerMessage } from './service-worker-bridge'; const files = new Map(); -function findPrefix(path: string): [string, string] | undefined { - if (path[0] == '/') path = path.substring(1); +function findPrefix(path: string): [string, string] | null { + if (path.startsWith('/')) path = path.substring(1); const prefix = [...files.keys()].find((dir) => path.startsWith(dir)); - if (!prefix) return; + if (!prefix) return null; path = path.substring(prefix.length + 1); return [path, prefix]; } +// eslint-disable-next-line @typescript-eslint/require-await export async function isReadable(path: string): Promise { - return !!findPrefix(path); + return Boolean(findPrefix(path)); } -export async function loadText(path: string): Promise { +export async function loadText(path: string): Promise { const buf = await readFile(path); - if (!buf) return; + if (!buf) return null; return new TextDecoder('utf-8').decode(buf); } -export async function readFile(path: string): Promise { +// eslint-disable-next-line @typescript-eslint/require-await +export async function readFile(path: string): Promise { const prefixObj = findPrefix(path); - if (!prefixObj) return; + if (!prefixObj) return null; const [relativePath, prefix] = prefixObj; const unzipped = files.get(prefix)!; @@ -36,9 +38,10 @@ export async function readFile(path: string): Promise { return data.buffer; } -export async function findRecursively(dir: string): Promise { +// eslint-disable-next-line @typescript-eslint/require-await +export async function findRecursively(dir: string): Promise { const prefixObj = findPrefix(dir); - if (!prefixObj) return; + if (!prefixObj) return null; const [relativePath, prefix] = prefixObj; const unzipped = files.get(prefix)!; @@ -46,6 +49,7 @@ export async function findRecursively(dir: string): Promise !path.endsWith('/') && path.startsWith(relativePath)) .map((dir) => dir.substring(relativePath.length)); + return dirs; } diff --git a/packages/core/src/files.ts b/packages/core/src/files.ts index 1c8c53a..7642dbc 100644 --- a/packages/core/src/files.ts +++ b/packages/core/src/files.ts @@ -21,13 +21,13 @@ export async function readFile(path: string): Promise { return (await filesCCMod.readFile(path)) || currentModule.readFile(path); } -export async function loadText(path: string) { +export async function loadText(path: string): Promise { return (await filesCCMod.loadText(path)) || currentModule.loadText(path); } -export async function findRecursively(dir: string): Promise { +export async function findRecursively(dir: string): Promise { return ( (await filesCCMod.findRecursively(dir)) || - ('findRecursively' in currentModule ? currentModule.findRecursively(dir) : undefined) + ('findRecursively' in currentModule ? currentModule.findRecursively(dir) : null) ); } From 7cd4786a7daa85f197f664b71ff253fbb1326b60 Mon Sep 17 00:00:00 2001 From: krypek Date: Tue, 22 Jul 2025 16:31:03 +0200 Subject: [PATCH 13/16] Use a handler model for fetch injects --- packages/core/src/files.ccmod.ts | 7 +++-- packages/core/src/service-worker-bridge.ts | 32 ++++++++++++++++------ packages/core/src/service-worker.ts | 30 ++++++++++---------- 3 files changed, 43 insertions(+), 26 deletions(-) diff --git a/packages/core/src/files.ccmod.ts b/packages/core/src/files.ccmod.ts index 6508f50..2a1eebc 100644 --- a/packages/core/src/files.ccmod.ts +++ b/packages/core/src/files.ccmod.ts @@ -1,5 +1,5 @@ import { type Unzipped, unzipSync } from 'fflate/browser'; -import { sendServiceWorkerMessage } from './service-worker-bridge'; +import { addFetchHandler } from './service-worker-bridge'; const files = new Map(); @@ -71,5 +71,8 @@ export async function loadCCMods( files.set(mod.dir, buf); } - sendServiceWorkerMessage(ccmods.map((mod) => `/${mod.dir}`)); + addFetchHandler( + ccmods.map((mod) => mod.dir), + readFile, + ); } diff --git a/packages/core/src/service-worker-bridge.ts b/packages/core/src/service-worker-bridge.ts index f13e5fd..95f5b4b 100644 --- a/packages/core/src/service-worker-bridge.ts +++ b/packages/core/src/service-worker-bridge.ts @@ -1,10 +1,3 @@ -import * as filesCCMod from './files.ccmod'; - -export interface ServiceWorkerPacket { - path: string; - data: Buffer | undefined; -} - export async function loadServiceWorker(): Promise { const currentRegistration = await window.navigator.serviceWorker.getRegistration(); if (currentRegistration) { @@ -33,16 +26,37 @@ export async function loadServiceWorker(): Promise { return controller; } -export function sendServiceWorkerMessage(packet: unknown): void { +function sendServiceWorkerMessage(packet: unknown): void { const { controller } = window.navigator.serviceWorker; controller?.postMessage(packet); } +export type FetchHandler = (path: string) => Promise; +const fetchHandlers: FetchHandler[] = []; +const validPathPrefixes: string[] = []; + +export function addFetchHandler(pathPrefixes: string[], handler: FetchHandler): void { + fetchHandlers.push(handler); + validPathPrefixes.push(...pathPrefixes); + + sendServiceWorkerMessage(validPathPrefixes.map((path) => `/${path}`)); +} + +export interface ServiceWorkerPacket { + path: string; + data: ArrayBuffer; +} + function setMessageHandling(): void { navigator.serviceWorker.onmessage = async (event) => { const path: string = event.data; - const data = await filesCCMod.readFile(path); + let data: ArrayBuffer | null = null; + for (const handler of fetchHandlers) { + data = await handler(path); + if (data) break; + } + if (!data) throw new Error(`path: "${path}" was not handled!`); const packet: ServiceWorkerPacket = { path, diff --git a/packages/core/src/service-worker.ts b/packages/core/src/service-worker.ts index 9de438e..b879180 100644 --- a/packages/core/src/service-worker.ts +++ b/packages/core/src/service-worker.ts @@ -41,24 +41,22 @@ async function requestContents(path: string): Promise { waitingFor.set(path, resolve); const { data } = await promise; - if (!data) { - return fetch(path); - } else { - return new Response(data, { - headers: { - 'Content-Type': contentType(path), - }, - status: 200, - statusText: 'ok', - }); - } + if (!data) throw new Error('not possible'); + + return new Response(data, { + headers: { + 'Content-Type': contentType(path), + }, + status: 200, + statusText: 'ok', + }); } -let validPaths: string[] | undefined; +let validPathPrefixes: string[] | undefined; self.addEventListener('message', (event) => { if (Array.isArray(event.data)) { - validPaths = event.data; + validPathPrefixes = event.data; } else { const packet: ServiceWorkerPacket = event.data; const resolve = waitingFor.get(packet.path)!; @@ -68,13 +66,15 @@ self.addEventListener('message', (event) => { }); self.addEventListener('fetch', (event: FetchEvent) => { - if (!validPaths) return; + if (!validPathPrefixes) return; const { request } = event; const path = new URL(request.url).pathname; if ( - validPaths.some((pathPrefix) => path.length > pathPrefix.length && path.startsWith(pathPrefix)) + validPathPrefixes.some( + (pathPrefix) => path.length > pathPrefix.length && path.startsWith(pathPrefix), + ) ) { event.respondWith(requestContents(path)); } From 9e1b916c22304ab521f157042046f5192c2a4098 Mon Sep 17 00:00:00 2001 From: krypek Date: Tue, 22 Jul 2025 16:33:22 +0200 Subject: [PATCH 14/16] Remove unused build code --- build.ts | 28 +++------------------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/build.ts b/build.ts index 86d009b..9fcd767 100644 --- a/build.ts +++ b/build.ts @@ -5,7 +5,7 @@ import * as fs from 'fs'; const isWatch = process.argv[2] === 'watch'; -function donePlugin(outfile: string, modifyCode?: (code: string) => string): esbuild.Plugin { +function donePlugin(outfile: string): esbuild.Plugin { return { name: 'done plugin', setup(build) { @@ -13,8 +13,6 @@ function donePlugin(outfile: string, modifyCode?: (code: string) => string): esb let code = res.outputFiles![0]?.text; if (!code) return; // when compile errors - if (modifyCode) code = modifyCode(code); - await fs.promises.writeFile(outfile, code); const bytes = code.length; @@ -35,18 +33,6 @@ const commonOptions = { sourcemap: 'inline', } as const; -function fixEsmImports(code: string): string { - code = code.replace( - /import \{ Buffer as (Buffer\d*) \} from "buffer";/g, - 'const $1 = require("buffer").Buffer;', - ); - code = code.replace( - /import \{ (createRequire) } from "module";/g, - 'const $1 = require("module").createRequire;', - ); - return code; -} - function core(): esbuild.BuildOptions { const outfile = path.join('./dist', `core.js`); @@ -55,12 +41,7 @@ function core(): esbuild.BuildOptions { ...commonOptions, - plugins: [ - donePlugin(outfile, (code) => { - code = fixEsmImports(code); - return code; - }), - ], + plugins: [donePlugin(outfile)], }; } @@ -85,10 +66,7 @@ function runtime(): esbuild.BuildOptions { src: `./packages/runtime/assets`, dest: `./dist/runtime/assets`, }), - donePlugin(outfile, (code) => { - code = fixEsmImports(code); - return code; - }), + donePlugin(outfile), ], }; } From 6d211d7f980ae3227d1478ba6b5f178525a43d1c Mon Sep 17 00:00:00 2001 From: krypek Date: Wed, 23 Jul 2025 18:05:05 +0200 Subject: [PATCH 15/16] Small changes --- packages/core/src/files.ccmod.ts | 16 +++++++++------ packages/core/src/service-worker-bridge.ts | 24 ++++++++++++++-------- packages/core/src/service-worker.ts | 8 ++++++-- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/packages/core/src/files.ccmod.ts b/packages/core/src/files.ccmod.ts index 2a1eebc..a7e7b98 100644 --- a/packages/core/src/files.ccmod.ts +++ b/packages/core/src/files.ccmod.ts @@ -1,11 +1,12 @@ import { type Unzipped, unzipSync } from 'fflate/browser'; import { addFetchHandler } from './service-worker-bridge'; +import * as files from './files'; -const files = new Map(); +const fileMap = new Map(); function findPrefix(path: string): [string, string] | null { if (path.startsWith('/')) path = path.substring(1); - const prefix = [...files.keys()].find((dir) => path.startsWith(dir)); + const prefix = [...fileMap.keys()].find((dir) => path.startsWith(dir)); if (!prefix) return null; @@ -31,7 +32,7 @@ export async function readFile(path: string): Promise { if (!prefixObj) return null; const [relativePath, prefix] = prefixObj; - const unzipped = files.get(prefix)!; + const unzipped = fileMap.get(prefix)!; const data = unzipped[relativePath]; @@ -44,7 +45,7 @@ export async function findRecursively(dir: string): Promise { if (!prefixObj) return null; const [relativePath, prefix] = prefixObj; - const unzipped = files.get(prefix)!; + const unzipped = fileMap.get(prefix)!; const dirs = Object.keys(unzipped) .filter((path) => !path.endsWith('/') && path.startsWith(relativePath)) @@ -58,7 +59,10 @@ export async function loadCCMods( ): Promise { const ccmods: typeof allModsList = allModsList.filter((mod) => mod.dir.endsWith('.ccmod')); const ccmodArrayBuffers = await Promise.all( - ccmods.map(async (mod) => (await fetch(`../${mod.dir}`)).bytes()), + ccmods.map(async (mod) => { + const url = `./${mod.dir}`; + return new Uint8Array(await files.readFile(url)); + }), ); // console.time('uncompress'); @@ -68,7 +72,7 @@ export async function loadCCMods( for (let i = 0; i < ccmods.length; i++) { const mod = ccmods[i]; const buf = uncompressed[i]; - files.set(mod.dir, buf); + fileMap.set(mod.dir, buf); } addFetchHandler( diff --git a/packages/core/src/service-worker-bridge.ts b/packages/core/src/service-worker-bridge.ts index 95f5b4b..a5f0605 100644 --- a/packages/core/src/service-worker-bridge.ts +++ b/packages/core/src/service-worker-bridge.ts @@ -8,7 +8,6 @@ export async function loadServiceWorker(): Promise { await window.navigator.serviceWorker.register(path, { updateViaCache: 'none', type: 'module', - scope: '/ccloader3/', }); } @@ -22,6 +21,7 @@ export async function loadServiceWorker(): Promise { } setMessageHandling(); + updateServiceWorkerValidPathPrefixes(); return controller; } @@ -31,32 +31,38 @@ function sendServiceWorkerMessage(packet: unknown): void { controller?.postMessage(packet); } -export type FetchHandler = (path: string) => Promise; +export type FetchHandler = (path: string) => Promise; const fetchHandlers: FetchHandler[] = []; const validPathPrefixes: string[] = []; +function updateServiceWorkerValidPathPrefixes(): void { + sendServiceWorkerMessage(validPathPrefixes.map((path) => `/${path}`)); +} + export function addFetchHandler(pathPrefixes: string[], handler: FetchHandler): void { - fetchHandlers.push(handler); + fetchHandlers.unshift(handler); validPathPrefixes.push(...pathPrefixes); - - sendServiceWorkerMessage(validPathPrefixes.map((path) => `/${path}`)); + updateServiceWorkerValidPathPrefixes(); } export interface ServiceWorkerPacket { path: string; - data: ArrayBuffer; + data: ArrayBufferLike | null; } function setMessageHandling(): void { navigator.serviceWorker.onmessage = async (event) => { const path: string = event.data; - let data: ArrayBuffer | null = null; + let data: ArrayBufferLike | null = null; for (const handler of fetchHandlers) { - data = await handler(path); + try { + data = await handler(path); + } catch (e) { + console.error(`error while handing fetch of ${path}:`, e); + } if (data) break; } - if (!data) throw new Error(`path: "${path}" was not handled!`); const packet: ServiceWorkerPacket = { path, diff --git a/packages/core/src/service-worker.ts b/packages/core/src/service-worker.ts index b879180..10bdbdc 100644 --- a/packages/core/src/service-worker.ts +++ b/packages/core/src/service-worker.ts @@ -41,7 +41,9 @@ async function requestContents(path: string): Promise { waitingFor.set(path, resolve); const { data } = await promise; - if (!data) throw new Error('not possible'); + if (!data) { + return new Response(null, { status: 404 }); + } return new Response(data, { headers: { @@ -66,7 +68,9 @@ self.addEventListener('message', (event) => { }); self.addEventListener('fetch', (event: FetchEvent) => { - if (!validPathPrefixes) return; + if (!validPathPrefixes) { + return; + } const { request } = event; const path = new URL(request.url).pathname; From 62a6e409576aa3b54a4c8abb8efe1afa0847f7ee Mon Sep 17 00:00:00 2001 From: krypek Date: Wed, 23 Jul 2025 18:05:23 +0200 Subject: [PATCH 16/16] Fix paths with spaces not resolving properly in .ccmod's --- packages/core/src/service-worker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/service-worker.ts b/packages/core/src/service-worker.ts index 10bdbdc..aa4d3c5 100644 --- a/packages/core/src/service-worker.ts +++ b/packages/core/src/service-worker.ts @@ -73,7 +73,7 @@ self.addEventListener('fetch', (event: FetchEvent) => { } const { request } = event; - const path = new URL(request.url).pathname; + const path = decodeURI(new URL(request.url).pathname); if ( validPathPrefixes.some(