diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f539f52..98fc807 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,8 +23,6 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4.2.0 - with: - version: 10.13.1 - name: Setup Node uses: actions/setup-node@v4 diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..cb2c84d --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm lint-staged diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000..10bed33 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1 @@ +pnpm typecheck && pnpm test diff --git a/RELEASE.md b/RELEASE.md index 7a35293..2e3090b 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,58 +1,22 @@ -## OpenCore Framework v1.0.5-beta.2 +## OpenCore Framework v1.0.5 -### Highlights -- Added an explicit server adapter API for platform-specific runtimes. -- Player creation and remote hydration now support adapter-owned subclasses while preserving the public `Player` type. -- Added an explicit client adapter API and removed the built-in `ClientPlayer` singleton. -- Added client UI bridges for markers, blips, and notifications. -- Added lifecycle services for NPC and Vehicle management. -- Improved Player management with spawn, teleport, and respawn actions. +### Added +- Added new client adapter ports for camera, ped, vehicle, progress, spawn, local player, runtime bridge, and WebView integration, with matching node runtime implementations. +- Added support for WebView chat mode, richer client UI/runtime abstractions, and cleaner adapter-facing contracts/exports. +- Added server-side improvements for command handling, including command validation, default function parameter support, and standardized system event names. +- Added more coverage around parallel compute, vehicle modification, vehicle sync state, player state sync, adapters, and command execution flows. +- Added Husky pre-commit and pre-push hooks for local quality checks. -### New Features -- `Server.init()` now accepts `adapter` to install a single server adapter during bootstrap. -- Added public server adapter helpers in `@open-core/framework/server` for custom adapter packages. -- Added adapter-aware Player serialization hooks for CORE/RESOURCE flows. -- `Client.init()` now accepts `adapter` to install a single client adapter during bootstrap. -- Added client runtime bridge contracts so event processors, WebView callbacks, key mappings, and ticks no longer depend directly on CFX globals. -- Added client UI bridges for markers, blips, and notifications. -- Added lifecycle services and contracts for NPC and Vehicle management. -- Added `ISpawnActions` interface and implementation for managing player spawn, teleport, and respawn actions. -- Added `ClientLoggerBridge` to abstract client-side logging from direct console calls. -- Added `playerCommand` runtime event. -- Added RedM-specific ped appearance adapter and client services for RDR3 profile appearance logic. -- Added runtime platform and game profile detection with duplicate DI registration prevention. -- Added `useAdapter()` function to pre-set the client adapter before initialization. -- Added project-level adapter injection and runtime hints for server and client adapters. -- Added WebView abstraction for client UI interactions. -- Renamed routing bucket methods to dimension. -- Added dedicated client and server contract files with updated exports and package entry points. +### Changed +- Refactored client services to rely on explicit adapter ports instead of direct runtime assumptions, especially for camera, ped, progress, spawn, and vehicle flows. +- Refactored logging so logger writes use string log levels and runtime log domain labels are derived dynamically from the active resource. +- Refactored worker execution to use inline worker scripts with performance tracking in the parallel compute pipeline. +- Updated package/tooling setup to TypeScript 6 and refreshed package exports, scripts, and dependency configuration. -### Breaking Changes -- Server bootstrap now defaults to the built-in Node adapter when no explicit runtime adapter is provided. -- Platform-specific Player APIs should move into adapter packages through Player subclassing/module augmentation. -- `ClientPlayer` is no longer exported from `@open-core/framework/client`. -- Client bootstrap no longer uses `register-client-capabilities`; external adapters should be installed through `Client.init({ adapter })`. -- `WebViewBridge` is now the preferred embedded UI abstraction; `OnView` now represents WebView callbacks directly, while `NuiBridge` and `NUI` remain as deprecated compatibility aliases. - -### Bug Fixes -- Fixed lint issues and removed unused variables. -- Fixed exportation issues. -- Added tests for lint and unused variable fixes. +### Fixed +- Fixed command schema handling so exported/remote commands support default parameters more reliably. +- Fixed transport/event contract alignment across node events and RPC layers. +- Fixed several test, lint, and export consistency issues while expanding automated coverage. ### Notes -- Migration path for external adapters: - 1. Create an adapter with `defineServerAdapter({ name, register(ctx) { ... } })`. - 2. Register platform contracts inside `register(ctx)` with `bindSingleton`, `bindInstance`, or `bindMessagingTransport`. - 3. If you extend `Player`, provide `ctx.usePlayerAdapter({ createLocal, createRemote, serialize, hydrate })`. - 4. Pass the adapter to `Server.init({ mode, adapter })` in both CORE and RESOURCE resources. -- RESOURCE hydration now validates adapter identity before rebuilding remote `Player` instances. -- Client adapter migration path: - 1. Create an adapter with `defineClientAdapter({ name, register(ctx) { ... } })`. - 2. Register transport, appearance, hashing, and runtime bridge contracts inside `register(ctx)`. - 3. Pass the adapter to `Client.init({ mode, adapter })`. -- Client files now safe to remove from core once moved to external adapter packages: - - `src/adapters/register-client-capabilities.ts` - - `src/adapters/fivem/fivem-ped-appearance-client.ts` - - `src/adapters/redm/redm-ped-appearance-client.ts` - - `src/adapters/node/node-ped-appearance-client.ts` - - Any remaining client-only transport/runtime bindings that your external adapter reimplements. +- This release covers the full `master...v1` delta and keeps the release notes compact by grouping related adapter/runtime refactors instead of listing each port separately. diff --git a/biome.json b/biome.json index baac75f..120a343 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", + "$schema": "https://biomejs.dev/schemas/2.4.8/schema.json", "root": true, "files": { diff --git a/package.json b/package.json index 9074b66..ac18ae4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@open-core/framework", - "version": "1.0.5-beta.2", + "version": "1.0.5", "description": "Secure, event-driven TypeScript Framework & Runtime engine for CitizenFX (Cfx).", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -75,12 +75,15 @@ "bench": "npx tsx benchmark/index.ts", "bench:core": "npx tsx benchmark/index.ts --core", "bench:load": "npx vitest run --project benchmark", - "bench:all": "npx tsx benchmark/index.ts --all" + "bench:all": "npx tsx benchmark/index.ts --all", + "validate": "pnpm check && pnpm typecheck && pnpm test", + "lint-staged": "lint-staged", + "prepare": "husky" }, "keywords": [ "framework", "opencore", - "cfx", + "ragemp", "citizenfx", "redm", "typescript", @@ -88,7 +91,7 @@ ], "author": "OpenCore Team", "license": "MPL-2.0", - "packageManager": "pnpm@10.13.1", + "packageManager": "pnpm@10.33.0", "peerDependencies": { "reflect-metadata": "^0.2.2", "tsyringe": "^4.10.0", @@ -98,16 +101,23 @@ "uuid": "^13.0.0" }, "devDependencies": { - "@biomejs/biome": "^2.3.11", - "@types/node": "^25.0.3", - "@vitest/coverage-v8": "^4.0.16", - "dependency-cruiser": "^17.3.6", + "@biomejs/biome": "^2.4.8", + "@types/node": "^25.5.0", + "@vitest/coverage-v8": "^4.1.1", + "dependency-cruiser": "^17.3.9", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", "graphviz": "^0.0.9", - "tinybench": "^2.9.0", + "husky": "^9.1.7", + "lint-staged": "^16.2.6", + "tinybench": "^6.0.0", "tsx": "^4.21.0", - "typescript": "^5.9.3", - "vitest": "^4.0.16" + "typescript": "^6.0.2", + "vitest": "^4.1.1" + }, + "lint-staged": { + "*.{js,cjs,mjs,ts,tsx,json,md}": [ + "biome check --write --no-errors-on-unmatched" + ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37b152c..87caa04 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,20 +19,20 @@ importers: version: 13.0.0 zod: specifier: ^4.3.5 - version: 4.3.5 + version: 4.3.6 devDependencies: '@biomejs/biome': - specifier: ^2.3.11 - version: 2.3.11 + specifier: ^2.4.8 + version: 2.4.8 '@types/node': - specifier: ^25.0.3 - version: 25.0.3 + specifier: ^25.5.0 + version: 25.5.0 '@vitest/coverage-v8': - specifier: ^4.0.16 - version: 4.0.16(vitest@4.0.16(@types/node@25.0.3)(tsx@4.21.0)) + specifier: ^4.1.1 + version: 4.1.1(vitest@4.1.1(@types/node@25.5.0)(vite@7.3.0(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3))) dependency-cruiser: - specifier: ^17.3.6 - version: 17.3.6 + specifier: ^17.3.9 + version: 17.3.9 eslint-config-prettier: specifier: ^10.1.8 version: 10.1.8(eslint@9.39.1) @@ -42,18 +42,24 @@ importers: graphviz: specifier: ^0.0.9 version: 0.0.9 + husky: + specifier: ^9.1.7 + version: 9.1.7 + lint-staged: + specifier: ^16.2.6 + version: 16.4.0 tinybench: - specifier: ^2.9.0 - version: 2.9.0 + specifier: ^6.0.0 + version: 6.0.0 tsx: specifier: ^4.21.0 version: 4.21.0 typescript: - specifier: ^5.9.3 - version: 5.9.3 + specifier: ^6.0.2 + version: 6.0.2 vitest: - specifier: ^4.0.16 - version: 4.0.16(@types/node@25.0.3)(tsx@4.21.0) + specifier: ^4.1.1 + version: 4.1.1(@types/node@25.5.0)(vite@7.3.0(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)) packages: @@ -65,224 +71,228 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} - '@babel/parser@7.28.5': - resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/types@7.28.5': - resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@biomejs/biome@2.3.11': - resolution: {integrity: sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ==} + '@biomejs/biome@2.4.8': + resolution: {integrity: sha512-ponn0oKOky1oRXBV+rlSaUlixUxf1aZvWC19Z41zBfUOUesthrQqL3OtiAlSB1EjFjyWpn98Q64DHelhA6jNlA==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.3.11': - resolution: {integrity: sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA==} + '@biomejs/cli-darwin-arm64@2.4.8': + resolution: {integrity: sha512-ARx0tECE8I7S2C2yjnWYLNbBdDoPdq3oyNLhMglmuctThwUsuzFWRKrHmIGwIRWKz0Mat9DuzLEDp52hGnrxGQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.3.11': - resolution: {integrity: sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg==} + '@biomejs/cli-darwin-x64@2.4.8': + resolution: {integrity: sha512-Jg9/PsB9vDCJlANE8uhG7qDhb5w0Ix69D7XIIc8IfZPUoiPrbLm33k2Ig3NOJ/7nb3UbesFz3D1aDKm9DvzjhQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.3.11': - resolution: {integrity: sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg==} + '@biomejs/cli-linux-arm64-musl@2.4.8': + resolution: {integrity: sha512-Zo9OhBQDJ3IBGPlqHiTISloo5H0+FBIpemqIJdW/0edJ+gEcLR+MZeZozcUyz3o1nXkVA7++DdRKQT0599j9jA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [musl] - '@biomejs/cli-linux-arm64@2.3.11': - resolution: {integrity: sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g==} + '@biomejs/cli-linux-arm64@2.4.8': + resolution: {integrity: sha512-5CdrsJct76XG2hpKFwXnEtlT1p+4g4yV+XvvwBpzKsTNLO9c6iLlAxwcae2BJ7ekPGWjNGw9j09T5KGPKKxQig==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [glibc] - '@biomejs/cli-linux-x64-musl@2.3.11': - resolution: {integrity: sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw==} + '@biomejs/cli-linux-x64-musl@2.4.8': + resolution: {integrity: sha512-Gi8quv8MEuDdKaPFtS2XjEnMqODPsRg6POT6KhoP+VrkNb+T2ywunVB+TvOU0LX1jAZzfBr+3V1mIbBhzAMKvw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [musl] - '@biomejs/cli-linux-x64@2.3.11': - resolution: {integrity: sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg==} + '@biomejs/cli-linux-x64@2.4.8': + resolution: {integrity: sha512-PdKXspVEaMCQLjtZCn6vfSck/li4KX9KGwSDbZdgIqlrizJ2MnMcE3TvHa2tVfXNmbjMikzcfJpuPWH695yJrw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [glibc] - '@biomejs/cli-win32-arm64@2.3.11': - resolution: {integrity: sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw==} + '@biomejs/cli-win32-arm64@2.4.8': + resolution: {integrity: sha512-LoFatS0tnHv6KkCVpIy3qZCih+MxUMvdYiPWLHRri7mhi2vyOOs8OrbZBcLTUEWCS+ktO72nZMy4F96oMhkOHQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.3.11': - resolution: {integrity: sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg==} + '@biomejs/cli-win32-x64@2.4.8': + resolution: {integrity: sha512-vAn7iXDoUbqFXqVocuq1sMYAd33p8+mmurqJkWl6CtIhobd/O6moe4rY5AJvzbunn/qZCdiDVcveqtkFh1e7Hg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] - '@esbuild/aix-ppc64@0.27.2': - resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.2': - resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.2': - resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.2': - resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.2': - resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.2': - resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.2': - resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.2': - resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.2': - resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.2': - resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.2': - resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.2': - resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.2': - resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.2': - resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.2': - resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.2': - resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.2': - resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.2': - resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.2': - resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.2': - resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.2': - resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.2': - resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.2': - resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.2': - resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.2': - resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.2': - resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -297,8 +307,8 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.21.1': - resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/config-helpers@0.4.2': @@ -309,8 +319,8 @@ packages: resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.3': - resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/js@9.39.1': @@ -351,128 +361,141 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@rollup/rollup-android-arm-eabi@4.55.1': - resolution: {integrity: sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==} + '@rollup/rollup-android-arm-eabi@4.60.0': + resolution: {integrity: sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.55.1': - resolution: {integrity: sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==} + '@rollup/rollup-android-arm64@4.60.0': + resolution: {integrity: sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.55.1': - resolution: {integrity: sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==} + '@rollup/rollup-darwin-arm64@4.60.0': + resolution: {integrity: sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.55.1': - resolution: {integrity: sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==} + '@rollup/rollup-darwin-x64@4.60.0': + resolution: {integrity: sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.55.1': - resolution: {integrity: sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==} + '@rollup/rollup-freebsd-arm64@4.60.0': + resolution: {integrity: sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.55.1': - resolution: {integrity: sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==} + '@rollup/rollup-freebsd-x64@4.60.0': + resolution: {integrity: sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.55.1': - resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} + '@rollup/rollup-linux-arm-gnueabihf@4.60.0': + resolution: {integrity: sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==} cpu: [arm] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.55.1': - resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} + '@rollup/rollup-linux-arm-musleabihf@4.60.0': + resolution: {integrity: sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==} cpu: [arm] os: [linux] + libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.55.1': - resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} + '@rollup/rollup-linux-arm64-gnu@4.60.0': + resolution: {integrity: sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==} cpu: [arm64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.55.1': - resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} + '@rollup/rollup-linux-arm64-musl@4.60.0': + resolution: {integrity: sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==} cpu: [arm64] os: [linux] + libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.55.1': - resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} + '@rollup/rollup-linux-loong64-gnu@4.60.0': + resolution: {integrity: sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==} cpu: [loong64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.55.1': - resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} + '@rollup/rollup-linux-loong64-musl@4.60.0': + resolution: {integrity: sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==} cpu: [loong64] os: [linux] + libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.55.1': - resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} + '@rollup/rollup-linux-ppc64-gnu@4.60.0': + resolution: {integrity: sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==} cpu: [ppc64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.55.1': - resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} + '@rollup/rollup-linux-ppc64-musl@4.60.0': + resolution: {integrity: sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==} cpu: [ppc64] os: [linux] + libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.55.1': - resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} + '@rollup/rollup-linux-riscv64-gnu@4.60.0': + resolution: {integrity: sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==} cpu: [riscv64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.55.1': - resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} + '@rollup/rollup-linux-riscv64-musl@4.60.0': + resolution: {integrity: sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==} cpu: [riscv64] os: [linux] + libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.55.1': - resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} + '@rollup/rollup-linux-s390x-gnu@4.60.0': + resolution: {integrity: sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==} cpu: [s390x] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.55.1': - resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} + '@rollup/rollup-linux-x64-gnu@4.60.0': + resolution: {integrity: sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==} cpu: [x64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.55.1': - resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} + '@rollup/rollup-linux-x64-musl@4.60.0': + resolution: {integrity: sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==} cpu: [x64] os: [linux] + libc: [musl] - '@rollup/rollup-openbsd-x64@4.55.1': - resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} + '@rollup/rollup-openbsd-x64@4.60.0': + resolution: {integrity: sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.55.1': - resolution: {integrity: sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==} + '@rollup/rollup-openharmony-arm64@4.60.0': + resolution: {integrity: sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.55.1': - resolution: {integrity: sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==} + '@rollup/rollup-win32-arm64-msvc@4.60.0': + resolution: {integrity: sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.55.1': - resolution: {integrity: sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==} + '@rollup/rollup-win32-ia32-msvc@4.60.0': + resolution: {integrity: sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.55.1': - resolution: {integrity: sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==} + '@rollup/rollup-win32-x64-gnu@4.60.0': + resolution: {integrity: sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.55.1': - resolution: {integrity: sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==} + '@rollup/rollup-win32-x64-msvc@4.60.0': + resolution: {integrity: sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==} cpu: [x64] os: [win32] @@ -497,46 +520,46 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/node@25.0.3': - resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} + '@types/node@25.5.0': + resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} - '@vitest/coverage-v8@4.0.16': - resolution: {integrity: sha512-2rNdjEIsPRzsdu6/9Eq0AYAzYdpP6Bx9cje9tL3FE5XzXRQF1fNU9pe/1yE8fCrS0HD+fBtt6gLPh6LI57tX7A==} + '@vitest/coverage-v8@4.1.1': + resolution: {integrity: sha512-nZ4RWwGCoGOQRMmU/Q9wlUY540RVRxJZ9lxFsFfy0QV7Zmo5VVBhB6Sl9Xa0KIp2iIs3zWfPlo9LcY1iqbpzCw==} peerDependencies: - '@vitest/browser': 4.0.16 - vitest: 4.0.16 + '@vitest/browser': 4.1.1 + vitest: 4.1.1 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@4.0.16': - resolution: {integrity: sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==} + '@vitest/expect@4.1.1': + resolution: {integrity: sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A==} - '@vitest/mocker@4.0.16': - resolution: {integrity: sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==} + '@vitest/mocker@4.1.1': + resolution: {integrity: sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A==} peerDependencies: msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@4.0.16': - resolution: {integrity: sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==} + '@vitest/pretty-format@4.1.1': + resolution: {integrity: sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ==} - '@vitest/runner@4.0.16': - resolution: {integrity: sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==} + '@vitest/runner@4.1.1': + resolution: {integrity: sha512-f7+FPy75vN91QGWsITueq0gedwUZy1fLtHOCMeQpjs8jTekAHeKP80zfDEnhrleviLHzVSDXIWuCIOFn3D3f8A==} - '@vitest/snapshot@4.0.16': - resolution: {integrity: sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==} + '@vitest/snapshot@4.1.1': + resolution: {integrity: sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg==} - '@vitest/spy@4.0.16': - resolution: {integrity: sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==} + '@vitest/spy@4.1.1': + resolution: {integrity: sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA==} - '@vitest/utils@4.0.16': - resolution: {integrity: sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==} + '@vitest/utils@4.1.1': + resolution: {integrity: sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==} acorn-jsx-walk@2.0.0: resolution: {integrity: sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==} @@ -550,22 +573,34 @@ packages: resolution: {integrity: sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==} engines: {node: '>=0.4.0'} - acorn-walk@8.3.4: - resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} engines: {node: '>=0.4.0'} - acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} hasBin: true - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -597,8 +632,8 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - ast-v8-to-istanbul@0.3.10: - resolution: {integrity: sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==} + ast-v8-to-istanbul@1.0.0: + resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} async-function@1.0.0: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} @@ -638,6 +673,14 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} + engines: {node: '>=20'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -645,13 +688,19 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - commander@14.0.2: - resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -696,8 +745,8 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} - dependency-cruiser@17.3.6: - resolution: {integrity: sha512-k0mXZaNvR2hZC0Dh2y4NGZZGqWLM6AbxQXmKL0T2m+KRE19nK5gAhm+PdV7f1L+AhdwwSOnebok7C6P27l2xXA==} + dependency-cruiser@17.3.9: + resolution: {integrity: sha512-LwaotlB9bZ8zhdFGGYf/g2oYkYj7YNxlqx1btL/XIYGob/aKRArsSwkLKo+ZrHiegsEArQVg4ZQ3NhAh8uk+hg==} engines: {node: ^20.12||^22||>=24} hasBin: true @@ -709,10 +758,17 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - enhanced-resolve@5.18.4: - resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + enhanced-resolve@5.20.0: + resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==} engines: {node: '>=10.13.0'} + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + es-abstract@1.24.1: resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} engines: {node: '>= 0.4'} @@ -725,8 +781,8 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} @@ -744,8 +800,8 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - esbuild@0.27.2: - resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} engines: {node: '>=18'} hasBin: true @@ -838,6 +894,9 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -872,8 +931,8 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} @@ -898,6 +957,10 @@ packages: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -910,8 +973,8 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} - get-tsconfig@4.13.0: - resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + get-tsconfig@4.13.7: + resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} @@ -970,6 +1033,11 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1038,6 +1106,10 @@ packages: resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} engines: {node: '>= 0.4'} + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + is-generator-function@1.1.2: resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} engines: {node: '>= 0.4'} @@ -1116,16 +1188,12 @@ packages: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} - istanbul-lib-source-maps@5.0.6: - resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} - engines: {node: '>=10'} - istanbul-reports@3.2.0: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} - js-tokens@9.0.1: - resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} @@ -1160,6 +1228,15 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lint-staged@16.4.0: + resolution: {integrity: sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==} + engines: {node: '>=20.17'} + hasBin: true + + listr2@9.0.5: + resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} + engines: {node: '>=20.0.0'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1167,11 +1244,15 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - magicast@0.5.1: - resolution: {integrity: sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==} + magicast@0.5.2: + resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} @@ -1181,8 +1262,12 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -1225,6 +1310,10 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1266,12 +1355,16 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -1317,8 +1410,15 @@ packages: engines: {node: '>= 0.4'} hasBin: true - rollup@4.55.1: - resolution: {integrity: sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==} + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup@4.60.0: + resolution: {integrity: sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -1341,8 +1441,8 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} hasBin: true @@ -1385,9 +1485,21 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} + engines: {node: '>=18'} + + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} + engines: {node: '>=20'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1395,13 +1507,25 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.0: + resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} + engines: {node: '>=20'} + string.prototype.trim@1.2.10: resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} engines: {node: '>= 0.4'} @@ -1414,6 +1538,10 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -1430,8 +1558,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - tapable@2.3.0: - resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + tapable@2.3.2: + resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} engines: {node: '>=6'} temp@0.4.0: @@ -1441,16 +1569,20 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.0.2: - resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + tinybench@6.0.0: + resolution: {integrity: sha512-BWlWpVbbZXaYjRV0twGLNQO00Zj4HA/sjLOQP2IvzQqGwRGp+2kh7UU3ijyJ3ywFRogYDRbiHDMrUOfaMnN56g==} + engines: {node: '>=20.0.0'} + + tinyexec@1.0.4: + resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} engines: {node: '>=18'} tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tinyrainbow@3.0.3: - resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} tsconfig-paths-webpack-plugin@4.2.0: @@ -1496,8 +1628,8 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + typescript@6.0.2: + resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} engines: {node: '>=14.17'} hasBin: true @@ -1505,8 +1637,8 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - undici-types@7.16.0: - resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -1555,20 +1687,21 @@ packages: yaml: optional: true - vitest@4.0.16: - resolution: {integrity: sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==} + vitest@4.1.1: + resolution: {integrity: sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.0.16 - '@vitest/browser-preview': 4.0.16 - '@vitest/browser-webdriverio': 4.0.16 - '@vitest/ui': 4.0.16 + '@vitest/browser-playwright': 4.1.1 + '@vitest/browser-preview': 4.1.1 + '@vitest/browser-webdriverio': 4.1.1 + '@vitest/ui': 4.1.1 happy-dom: '*' jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -1589,8 +1722,8 @@ packages: jsdom: optional: true - watskeburt@5.0.0: - resolution: {integrity: sha512-fEMhfIzu9WOuAJdDcTT+aPjn0JHI2+UeJ+zWSEs/tgMvc+MFDZVmhlZ8C1uJWXax1ETYc4trUnHFHyx2DrG0jQ==} + watskeburt@5.0.3: + resolution: {integrity: sha512-g9CXukMjazlJJVQ3OHzXsnG25KFYgSgKMIyoJrD8ggr0DbS9UNF7OzIqWmmKKBMedkxj3T01uqEaGnn+y7QhMA==} engines: {node: ^20.12||^22.13||>=24.0} hasBin: true @@ -1606,8 +1739,8 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} - which-typed-array@1.1.19: - resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} engines: {node: '>= 0.4'} which@2.0.2: @@ -1624,12 +1757,21 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zod@4.3.5: - resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} snapshots: @@ -1637,128 +1779,128 @@ snapshots: '@babel/helper-validator-identifier@7.28.5': {} - '@babel/parser@7.28.5': + '@babel/parser@7.29.2': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 - '@babel/types@7.28.5': + '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 '@bcoe/v8-coverage@1.0.2': {} - '@biomejs/biome@2.3.11': + '@biomejs/biome@2.4.8': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.3.11 - '@biomejs/cli-darwin-x64': 2.3.11 - '@biomejs/cli-linux-arm64': 2.3.11 - '@biomejs/cli-linux-arm64-musl': 2.3.11 - '@biomejs/cli-linux-x64': 2.3.11 - '@biomejs/cli-linux-x64-musl': 2.3.11 - '@biomejs/cli-win32-arm64': 2.3.11 - '@biomejs/cli-win32-x64': 2.3.11 - - '@biomejs/cli-darwin-arm64@2.3.11': + '@biomejs/cli-darwin-arm64': 2.4.8 + '@biomejs/cli-darwin-x64': 2.4.8 + '@biomejs/cli-linux-arm64': 2.4.8 + '@biomejs/cli-linux-arm64-musl': 2.4.8 + '@biomejs/cli-linux-x64': 2.4.8 + '@biomejs/cli-linux-x64-musl': 2.4.8 + '@biomejs/cli-win32-arm64': 2.4.8 + '@biomejs/cli-win32-x64': 2.4.8 + + '@biomejs/cli-darwin-arm64@2.4.8': optional: true - '@biomejs/cli-darwin-x64@2.3.11': + '@biomejs/cli-darwin-x64@2.4.8': optional: true - '@biomejs/cli-linux-arm64-musl@2.3.11': + '@biomejs/cli-linux-arm64-musl@2.4.8': optional: true - '@biomejs/cli-linux-arm64@2.3.11': + '@biomejs/cli-linux-arm64@2.4.8': optional: true - '@biomejs/cli-linux-x64-musl@2.3.11': + '@biomejs/cli-linux-x64-musl@2.4.8': optional: true - '@biomejs/cli-linux-x64@2.3.11': + '@biomejs/cli-linux-x64@2.4.8': optional: true - '@biomejs/cli-win32-arm64@2.3.11': + '@biomejs/cli-win32-arm64@2.4.8': optional: true - '@biomejs/cli-win32-x64@2.3.11': + '@biomejs/cli-win32-x64@2.4.8': optional: true - '@esbuild/aix-ppc64@0.27.2': + '@esbuild/aix-ppc64@0.27.4': optional: true - '@esbuild/android-arm64@0.27.2': + '@esbuild/android-arm64@0.27.4': optional: true - '@esbuild/android-arm@0.27.2': + '@esbuild/android-arm@0.27.4': optional: true - '@esbuild/android-x64@0.27.2': + '@esbuild/android-x64@0.27.4': optional: true - '@esbuild/darwin-arm64@0.27.2': + '@esbuild/darwin-arm64@0.27.4': optional: true - '@esbuild/darwin-x64@0.27.2': + '@esbuild/darwin-x64@0.27.4': optional: true - '@esbuild/freebsd-arm64@0.27.2': + '@esbuild/freebsd-arm64@0.27.4': optional: true - '@esbuild/freebsd-x64@0.27.2': + '@esbuild/freebsd-x64@0.27.4': optional: true - '@esbuild/linux-arm64@0.27.2': + '@esbuild/linux-arm64@0.27.4': optional: true - '@esbuild/linux-arm@0.27.2': + '@esbuild/linux-arm@0.27.4': optional: true - '@esbuild/linux-ia32@0.27.2': + '@esbuild/linux-ia32@0.27.4': optional: true - '@esbuild/linux-loong64@0.27.2': + '@esbuild/linux-loong64@0.27.4': optional: true - '@esbuild/linux-mips64el@0.27.2': + '@esbuild/linux-mips64el@0.27.4': optional: true - '@esbuild/linux-ppc64@0.27.2': + '@esbuild/linux-ppc64@0.27.4': optional: true - '@esbuild/linux-riscv64@0.27.2': + '@esbuild/linux-riscv64@0.27.4': optional: true - '@esbuild/linux-s390x@0.27.2': + '@esbuild/linux-s390x@0.27.4': optional: true - '@esbuild/linux-x64@0.27.2': + '@esbuild/linux-x64@0.27.4': optional: true - '@esbuild/netbsd-arm64@0.27.2': + '@esbuild/netbsd-arm64@0.27.4': optional: true - '@esbuild/netbsd-x64@0.27.2': + '@esbuild/netbsd-x64@0.27.4': optional: true - '@esbuild/openbsd-arm64@0.27.2': + '@esbuild/openbsd-arm64@0.27.4': optional: true - '@esbuild/openbsd-x64@0.27.2': + '@esbuild/openbsd-x64@0.27.4': optional: true - '@esbuild/openharmony-arm64@0.27.2': + '@esbuild/openharmony-arm64@0.27.4': optional: true - '@esbuild/sunos-x64@0.27.2': + '@esbuild/sunos-x64@0.27.4': optional: true - '@esbuild/win32-arm64@0.27.2': + '@esbuild/win32-arm64@0.27.4': optional: true - '@esbuild/win32-ia32@0.27.2': + '@esbuild/win32-ia32@0.27.4': optional: true - '@esbuild/win32-x64@0.27.2': + '@esbuild/win32-x64@0.27.4': optional: true '@eslint-community/eslint-utils@4.9.1(eslint@9.39.1)': @@ -1768,11 +1910,11 @@ snapshots: '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.21.1': + '@eslint/config-array@0.21.2': dependencies: '@eslint/object-schema': 2.1.7 debug: 4.4.3 - minimatch: 3.1.2 + minimatch: 3.1.5 transitivePeerDependencies: - supports-color @@ -1784,16 +1926,16 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.3': + '@eslint/eslintrc@3.3.5': dependencies: - ajv: 6.12.6 + ajv: 6.14.0 debug: 4.4.3 espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 js-yaml: 4.1.1 - minimatch: 3.1.2 + minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color @@ -1827,79 +1969,79 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@rollup/rollup-android-arm-eabi@4.55.1': + '@rollup/rollup-android-arm-eabi@4.60.0': optional: true - '@rollup/rollup-android-arm64@4.55.1': + '@rollup/rollup-android-arm64@4.60.0': optional: true - '@rollup/rollup-darwin-arm64@4.55.1': + '@rollup/rollup-darwin-arm64@4.60.0': optional: true - '@rollup/rollup-darwin-x64@4.55.1': + '@rollup/rollup-darwin-x64@4.60.0': optional: true - '@rollup/rollup-freebsd-arm64@4.55.1': + '@rollup/rollup-freebsd-arm64@4.60.0': optional: true - '@rollup/rollup-freebsd-x64@4.55.1': + '@rollup/rollup-freebsd-x64@4.60.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.55.1': + '@rollup/rollup-linux-arm-gnueabihf@4.60.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.55.1': + '@rollup/rollup-linux-arm-musleabihf@4.60.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.55.1': + '@rollup/rollup-linux-arm64-gnu@4.60.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.55.1': + '@rollup/rollup-linux-arm64-musl@4.60.0': optional: true - '@rollup/rollup-linux-loong64-gnu@4.55.1': + '@rollup/rollup-linux-loong64-gnu@4.60.0': optional: true - '@rollup/rollup-linux-loong64-musl@4.55.1': + '@rollup/rollup-linux-loong64-musl@4.60.0': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.55.1': + '@rollup/rollup-linux-ppc64-gnu@4.60.0': optional: true - '@rollup/rollup-linux-ppc64-musl@4.55.1': + '@rollup/rollup-linux-ppc64-musl@4.60.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.55.1': + '@rollup/rollup-linux-riscv64-gnu@4.60.0': optional: true - '@rollup/rollup-linux-riscv64-musl@4.55.1': + '@rollup/rollup-linux-riscv64-musl@4.60.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.55.1': + '@rollup/rollup-linux-s390x-gnu@4.60.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.55.1': + '@rollup/rollup-linux-x64-gnu@4.60.0': optional: true - '@rollup/rollup-linux-x64-musl@4.55.1': + '@rollup/rollup-linux-x64-musl@4.60.0': optional: true - '@rollup/rollup-openbsd-x64@4.55.1': + '@rollup/rollup-openbsd-x64@4.60.0': optional: true - '@rollup/rollup-openharmony-arm64@4.55.1': + '@rollup/rollup-openharmony-arm64@4.60.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.55.1': + '@rollup/rollup-win32-arm64-msvc@4.60.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.55.1': + '@rollup/rollup-win32-ia32-msvc@4.60.0': optional: true - '@rollup/rollup-win32-x64-gnu@4.55.1': + '@rollup/rollup-win32-x64-gnu@4.60.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.55.1': + '@rollup/rollup-win32-x64-msvc@4.60.0': optional: true '@rtsao/scc@1.1.0': {} @@ -1919,93 +2061,100 @@ snapshots: '@types/json5@0.0.29': {} - '@types/node@25.0.3': + '@types/node@25.5.0': dependencies: - undici-types: 7.16.0 + undici-types: 7.18.2 - '@vitest/coverage-v8@4.0.16(vitest@4.0.16(@types/node@25.0.3)(tsx@4.21.0))': + '@vitest/coverage-v8@4.1.1(vitest@4.1.1(@types/node@25.5.0)(vite@7.3.0(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.0.16 - ast-v8-to-istanbul: 0.3.10 + '@vitest/utils': 4.1.1 + ast-v8-to-istanbul: 1.0.0 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 istanbul-reports: 3.2.0 - magicast: 0.5.1 + magicast: 0.5.2 obug: 2.1.1 - std-env: 3.10.0 - tinyrainbow: 3.0.3 - vitest: 4.0.16(@types/node@25.0.3)(tsx@4.21.0) - transitivePeerDependencies: - - supports-color + std-env: 4.0.0 + tinyrainbow: 3.1.0 + vitest: 4.1.1(@types/node@25.5.0)(vite@7.3.0(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/expect@4.0.16': + '@vitest/expect@4.1.1': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.0.16 - '@vitest/utils': 4.0.16 + '@vitest/spy': 4.1.1 + '@vitest/utils': 4.1.1 chai: 6.2.2 - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 - '@vitest/mocker@4.0.16(vite@7.3.0(@types/node@25.0.3)(tsx@4.21.0))': + '@vitest/mocker@4.1.1(vite@7.3.0(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@vitest/spy': 4.0.16 + '@vitest/spy': 4.1.1 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.0(@types/node@25.0.3)(tsx@4.21.0) + vite: 7.3.0(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/pretty-format@4.0.16': + '@vitest/pretty-format@4.1.1': dependencies: - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 - '@vitest/runner@4.0.16': + '@vitest/runner@4.1.1': dependencies: - '@vitest/utils': 4.0.16 + '@vitest/utils': 4.1.1 pathe: 2.0.3 - '@vitest/snapshot@4.0.16': + '@vitest/snapshot@4.1.1': dependencies: - '@vitest/pretty-format': 4.0.16 + '@vitest/pretty-format': 4.1.1 + '@vitest/utils': 4.1.1 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.0.16': {} + '@vitest/spy@4.1.1': {} - '@vitest/utils@4.0.16': + '@vitest/utils@4.1.1': dependencies: - '@vitest/pretty-format': 4.0.16 - tinyrainbow: 3.0.3 + '@vitest/pretty-format': 4.1.1 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 acorn-jsx-walk@2.0.0: {} - acorn-jsx@5.3.2(acorn@8.15.0): + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: - acorn: 8.15.0 + acorn: 8.16.0 acorn-loose@8.5.2: dependencies: - acorn: 8.15.0 + acorn: 8.16.0 - acorn-walk@8.3.4: + acorn-walk@8.3.5: dependencies: - acorn: 8.15.0 + acorn: 8.16.0 - acorn@8.15.0: {} + acorn@8.16.0: {} - ajv@6.12.6: + ajv@6.14.0: dependencies: fast-deep-equal: 3.1.3 fast-json-stable-stringify: 2.1.0 json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + + ansi-regex@6.2.2: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 + ansi-styles@6.2.3: {} + argparse@2.0.1: {} array-buffer-byte-length@1.0.2: @@ -2060,11 +2209,11 @@ snapshots: assertion-error@2.0.1: {} - ast-v8-to-istanbul@0.3.10: + ast-v8-to-istanbul@1.0.0: dependencies: '@jridgewell/trace-mapping': 0.3.31 estree-walker: 3.0.3 - js-tokens: 9.0.1 + js-tokens: 10.0.0 async-function@1.0.0: {} @@ -2105,16 +2254,29 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-truncate@5.2.0: + dependencies: + slice-ansi: 8.0.0 + string-width: 8.2.0 + color-convert@2.0.1: dependencies: color-name: 1.1.4 color-name@1.1.4: {} - commander@14.0.2: {} + colorette@2.0.20: {} + + commander@14.0.3: {} concat-map@0.0.1: {} + convert-source-map@2.0.0: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2161,15 +2323,15 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 - dependency-cruiser@17.3.6: + dependency-cruiser@17.3.9: dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) acorn-jsx-walk: 2.0.0 acorn-loose: 8.5.2 - acorn-walk: 8.3.4 - commander: 14.0.2 - enhanced-resolve: 5.18.4 + acorn-walk: 8.3.5 + commander: 14.0.3 + enhanced-resolve: 5.20.0 ignore: 7.0.5 interpret: 3.1.1 is-installed-globally: 1.0.0 @@ -2178,9 +2340,9 @@ snapshots: prompts: 2.4.2 rechoir: 0.8.0 safe-regex: 2.1.1 - semver: 7.7.3 + semver: 7.7.4 tsconfig-paths-webpack-plugin: 4.2.0 - watskeburt: 5.0.0 + watskeburt: 5.0.3 doctrine@2.1.0: dependencies: @@ -2192,10 +2354,14 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - enhanced-resolve@5.18.4: + emoji-regex@10.6.0: {} + + enhanced-resolve@5.20.0: dependencies: graceful-fs: 4.2.11 - tapable: 2.3.0 + tapable: 2.3.2 + + environment@1.1.0: {} es-abstract@1.24.1: dependencies: @@ -2252,13 +2418,13 @@ snapshots: typed-array-byte-offset: 1.0.4 typed-array-length: 1.0.7 unbox-primitive: 1.1.0 - which-typed-array: 1.1.19 + which-typed-array: 1.1.20 es-define-property@1.0.1: {} es-errors@1.3.0: {} - es-module-lexer@1.7.0: {} + es-module-lexer@2.0.0: {} es-object-atoms@1.1.1: dependencies: @@ -2281,34 +2447,34 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild@0.27.2: + esbuild@0.27.4: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.2 - '@esbuild/android-arm': 0.27.2 - '@esbuild/android-arm64': 0.27.2 - '@esbuild/android-x64': 0.27.2 - '@esbuild/darwin-arm64': 0.27.2 - '@esbuild/darwin-x64': 0.27.2 - '@esbuild/freebsd-arm64': 0.27.2 - '@esbuild/freebsd-x64': 0.27.2 - '@esbuild/linux-arm': 0.27.2 - '@esbuild/linux-arm64': 0.27.2 - '@esbuild/linux-ia32': 0.27.2 - '@esbuild/linux-loong64': 0.27.2 - '@esbuild/linux-mips64el': 0.27.2 - '@esbuild/linux-ppc64': 0.27.2 - '@esbuild/linux-riscv64': 0.27.2 - '@esbuild/linux-s390x': 0.27.2 - '@esbuild/linux-x64': 0.27.2 - '@esbuild/netbsd-arm64': 0.27.2 - '@esbuild/netbsd-x64': 0.27.2 - '@esbuild/openbsd-arm64': 0.27.2 - '@esbuild/openbsd-x64': 0.27.2 - '@esbuild/openharmony-arm64': 0.27.2 - '@esbuild/sunos-x64': 0.27.2 - '@esbuild/win32-arm64': 0.27.2 - '@esbuild/win32-ia32': 0.27.2 - '@esbuild/win32-x64': 0.27.2 + '@esbuild/aix-ppc64': 0.27.4 + '@esbuild/android-arm': 0.27.4 + '@esbuild/android-arm64': 0.27.4 + '@esbuild/android-x64': 0.27.4 + '@esbuild/darwin-arm64': 0.27.4 + '@esbuild/darwin-x64': 0.27.4 + '@esbuild/freebsd-arm64': 0.27.4 + '@esbuild/freebsd-x64': 0.27.4 + '@esbuild/linux-arm': 0.27.4 + '@esbuild/linux-arm64': 0.27.4 + '@esbuild/linux-ia32': 0.27.4 + '@esbuild/linux-loong64': 0.27.4 + '@esbuild/linux-mips64el': 0.27.4 + '@esbuild/linux-ppc64': 0.27.4 + '@esbuild/linux-riscv64': 0.27.4 + '@esbuild/linux-s390x': 0.27.4 + '@esbuild/linux-x64': 0.27.4 + '@esbuild/netbsd-arm64': 0.27.4 + '@esbuild/netbsd-x64': 0.27.4 + '@esbuild/openbsd-arm64': 0.27.4 + '@esbuild/openbsd-x64': 0.27.4 + '@esbuild/openharmony-arm64': 0.27.4 + '@esbuild/sunos-x64': 0.27.4 + '@esbuild/win32-arm64': 0.27.4 + '@esbuild/win32-ia32': 0.27.4 + '@esbuild/win32-x64': 0.27.4 escape-string-regexp@4.0.0: {} @@ -2348,7 +2514,7 @@ snapshots: hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 - minimatch: 3.1.2 + minimatch: 3.1.5 object.fromentries: 2.0.8 object.groupby: 1.0.3 object.values: 1.2.1 @@ -2373,17 +2539,17 @@ snapshots: dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.1) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.1 + '@eslint/config-array': 0.21.2 '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.3 + '@eslint/eslintrc': 3.3.5 '@eslint/js': 9.39.1 '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 - ajv: 6.12.6 + ajv: 6.14.0 chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.3 @@ -2402,7 +2568,7 @@ snapshots: is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 lodash.merge: 4.6.2 - minimatch: 3.1.2 + minimatch: 3.1.5 natural-compare: 1.4.0 optionator: 0.9.4 transitivePeerDependencies: @@ -2410,8 +2576,8 @@ snapshots: espree@10.4.0: dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) eslint-visitor-keys: 4.2.1 esquery@1.7.0: @@ -2430,6 +2596,8 @@ snapshots: esutils@2.0.3: {} + eventemitter3@5.0.4: {} + expect-type@1.3.0: {} fast-deep-equal@3.1.3: {} @@ -2438,9 +2606,9 @@ snapshots: fast-levenshtein@2.0.6: {} - fdir@6.5.0(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.4 file-entry-cache@8.0.0: dependencies: @@ -2453,10 +2621,10 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.3.3 + flatted: 3.4.2 keyv: 4.5.4 - flatted@3.3.3: {} + flatted@3.4.2: {} for-each@0.3.5: dependencies: @@ -2480,6 +2648,8 @@ snapshots: generator-function@2.0.1: {} + get-east-asian-width@1.5.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -2504,7 +2674,7 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 - get-tsconfig@4.13.0: + get-tsconfig@4.13.7: dependencies: resolve-pkg-maps: 1.0.0 @@ -2555,6 +2725,8 @@ snapshots: html-escaper@2.0.2: {} + husky@9.1.7: {} + ignore@5.3.2: {} ignore@7.0.5: {} @@ -2622,6 +2794,10 @@ snapshots: dependencies: call-bound: 1.0.4 + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.5.0 + is-generator-function@1.1.2: dependencies: call-bound: 1.0.4 @@ -2676,7 +2852,7 @@ snapshots: is-typed-array@1.1.15: dependencies: - which-typed-array: 1.1.19 + which-typed-array: 1.1.20 is-weakmap@2.0.2: {} @@ -2701,20 +2877,12 @@ snapshots: make-dir: 4.0.0 supports-color: 7.2.0 - istanbul-lib-source-maps@5.0.6: - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - debug: 4.4.3 - istanbul-lib-coverage: 3.2.2 - transitivePeerDependencies: - - supports-color - istanbul-reports@3.2.0: dependencies: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - js-tokens@9.0.1: {} + js-tokens@10.0.0: {} js-yaml@4.1.1: dependencies: @@ -2743,29 +2911,57 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lint-staged@16.4.0: + dependencies: + commander: 14.0.3 + listr2: 9.0.5 + picomatch: 4.0.4 + string-argv: 0.3.2 + tinyexec: 1.0.4 + yaml: 2.8.3 + + listr2@9.0.5: + dependencies: + cli-truncate: 5.2.0 + colorette: 2.0.20 + eventemitter3: 5.0.4 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.2 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 lodash.merge@4.6.2: {} + log-update@6.1.0: + dependencies: + ansi-escapes: 7.3.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.2 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - magicast@0.5.1: + magicast@0.5.2: dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 source-map-js: 1.2.1 make-dir@4.0.0: dependencies: - semver: 7.7.3 + semver: 7.7.4 math-intrinsics@1.1.0: {} - minimatch@3.1.2: + mimic-function@5.0.1: {} + + minimatch@3.1.5: dependencies: brace-expansion: 1.1.12 @@ -2812,6 +3008,10 @@ snapshots: obug@2.1.1: {} + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -2851,9 +3051,11 @@ snapshots: picomatch@4.0.3: {} + picomatch@4.0.4: {} + possible-typed-array-names@1.1.0: {} - postcss@8.5.6: + postcss@8.5.8: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -2906,35 +3108,42 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - rollup@4.55.1: + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + rfdc@1.4.1: {} + + rollup@4.60.0: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.55.1 - '@rollup/rollup-android-arm64': 4.55.1 - '@rollup/rollup-darwin-arm64': 4.55.1 - '@rollup/rollup-darwin-x64': 4.55.1 - '@rollup/rollup-freebsd-arm64': 4.55.1 - '@rollup/rollup-freebsd-x64': 4.55.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.55.1 - '@rollup/rollup-linux-arm-musleabihf': 4.55.1 - '@rollup/rollup-linux-arm64-gnu': 4.55.1 - '@rollup/rollup-linux-arm64-musl': 4.55.1 - '@rollup/rollup-linux-loong64-gnu': 4.55.1 - '@rollup/rollup-linux-loong64-musl': 4.55.1 - '@rollup/rollup-linux-ppc64-gnu': 4.55.1 - '@rollup/rollup-linux-ppc64-musl': 4.55.1 - '@rollup/rollup-linux-riscv64-gnu': 4.55.1 - '@rollup/rollup-linux-riscv64-musl': 4.55.1 - '@rollup/rollup-linux-s390x-gnu': 4.55.1 - '@rollup/rollup-linux-x64-gnu': 4.55.1 - '@rollup/rollup-linux-x64-musl': 4.55.1 - '@rollup/rollup-openbsd-x64': 4.55.1 - '@rollup/rollup-openharmony-arm64': 4.55.1 - '@rollup/rollup-win32-arm64-msvc': 4.55.1 - '@rollup/rollup-win32-ia32-msvc': 4.55.1 - '@rollup/rollup-win32-x64-gnu': 4.55.1 - '@rollup/rollup-win32-x64-msvc': 4.55.1 + '@rollup/rollup-android-arm-eabi': 4.60.0 + '@rollup/rollup-android-arm64': 4.60.0 + '@rollup/rollup-darwin-arm64': 4.60.0 + '@rollup/rollup-darwin-x64': 4.60.0 + '@rollup/rollup-freebsd-arm64': 4.60.0 + '@rollup/rollup-freebsd-x64': 4.60.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.0 + '@rollup/rollup-linux-arm-musleabihf': 4.60.0 + '@rollup/rollup-linux-arm64-gnu': 4.60.0 + '@rollup/rollup-linux-arm64-musl': 4.60.0 + '@rollup/rollup-linux-loong64-gnu': 4.60.0 + '@rollup/rollup-linux-loong64-musl': 4.60.0 + '@rollup/rollup-linux-ppc64-gnu': 4.60.0 + '@rollup/rollup-linux-ppc64-musl': 4.60.0 + '@rollup/rollup-linux-riscv64-gnu': 4.60.0 + '@rollup/rollup-linux-riscv64-musl': 4.60.0 + '@rollup/rollup-linux-s390x-gnu': 4.60.0 + '@rollup/rollup-linux-x64-gnu': 4.60.0 + '@rollup/rollup-linux-x64-musl': 4.60.0 + '@rollup/rollup-openbsd-x64': 4.60.0 + '@rollup/rollup-openharmony-arm64': 4.60.0 + '@rollup/rollup-win32-arm64-msvc': 4.60.0 + '@rollup/rollup-win32-ia32-msvc': 4.60.0 + '@rollup/rollup-win32-x64-gnu': 4.60.0 + '@rollup/rollup-win32-x64-msvc': 4.60.0 fsevents: 2.3.3 safe-array-concat@1.1.3: @@ -2962,7 +3171,7 @@ snapshots: semver@6.3.1: {} - semver@7.7.3: {} + semver@7.7.4: {} set-function-length@1.2.2: dependencies: @@ -3022,19 +3231,44 @@ snapshots: siginfo@2.0.0: {} + signal-exit@4.1.0: {} + sisteransi@1.0.5: {} + slice-ansi@7.1.2: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + slice-ansi@8.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + source-map-js@1.2.1: {} stackback@0.0.2: {} - std-env@3.10.0: {} + std-env@4.0.0: {} stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 internal-slot: 1.1.0 + string-argv@0.3.2: {} + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + + string-width@8.2.0: + dependencies: + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + string.prototype.trim@1.2.10: dependencies: call-bind: 1.0.8 @@ -3058,6 +3292,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + strip-bom@3.0.0: {} strip-json-comments@3.1.1: {} @@ -3068,26 +3306,28 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - tapable@2.3.0: {} + tapable@2.3.2: {} temp@0.4.0: {} tinybench@2.9.0: {} - tinyexec@1.0.2: {} + tinybench@6.0.0: {} + + tinyexec@1.0.4: {} tinyglobby@0.2.15: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 - tinyrainbow@3.0.3: {} + tinyrainbow@3.1.0: {} tsconfig-paths-webpack-plugin@4.2.0: dependencies: chalk: 4.1.2 - enhanced-resolve: 5.18.4 - tapable: 2.3.0 + enhanced-resolve: 5.20.0 + tapable: 2.3.2 tsconfig-paths: 4.2.0 tsconfig-paths@3.15.0: @@ -3107,8 +3347,8 @@ snapshots: tsx@4.21.0: dependencies: - esbuild: 0.27.2 - get-tsconfig: 4.13.0 + esbuild: 0.27.4 + get-tsconfig: 4.13.7 optionalDependencies: fsevents: 2.3.3 @@ -3153,7 +3393,7 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript@5.9.3: {} + typescript@6.0.2: {} unbox-primitive@1.1.0: dependencies: @@ -3162,7 +3402,7 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - undici-types@7.16.0: {} + undici-types@7.18.2: {} uri-js@4.4.1: dependencies: @@ -3170,57 +3410,48 @@ snapshots: uuid@13.0.0: {} - vite@7.3.0(@types/node@25.0.3)(tsx@4.21.0): + vite@7.3.0(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: - esbuild: 0.27.2 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.55.1 + esbuild: 0.27.4 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.8 + rollup: 4.60.0 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.0 fsevents: 2.3.3 tsx: 4.21.0 - - vitest@4.0.16(@types/node@25.0.3)(tsx@4.21.0): - dependencies: - '@vitest/expect': 4.0.16 - '@vitest/mocker': 4.0.16(vite@7.3.0(@types/node@25.0.3)(tsx@4.21.0)) - '@vitest/pretty-format': 4.0.16 - '@vitest/runner': 4.0.16 - '@vitest/snapshot': 4.0.16 - '@vitest/spy': 4.0.16 - '@vitest/utils': 4.0.16 - es-module-lexer: 1.7.0 + yaml: 2.8.3 + + vitest@4.1.1(@types/node@25.5.0)(vite@7.3.0(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.1 + '@vitest/mocker': 4.1.1(vite@7.3.0(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.1 + '@vitest/runner': 4.1.1 + '@vitest/snapshot': 4.1.1 + '@vitest/spy': 4.1.1 + '@vitest/utils': 4.1.1 + es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.10.0 + picomatch: 4.0.4 + std-env: 4.0.0 tinybench: 2.9.0 - tinyexec: 1.0.2 + tinyexec: 1.0.4 tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 - vite: 7.3.0(@types/node@25.0.3)(tsx@4.21.0) + tinyrainbow: 3.1.0 + vite: 7.3.0(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.0 transitivePeerDependencies: - - jiti - - less - - lightningcss - msw - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml - watskeburt@5.0.0: {} + watskeburt@5.0.3: {} which-boxed-primitive@1.1.1: dependencies: @@ -3244,7 +3475,7 @@ snapshots: isarray: 2.0.5 which-boxed-primitive: 1.1.1 which-collection: 1.0.2 - which-typed-array: 1.1.19 + which-typed-array: 1.1.20 which-collection@1.0.2: dependencies: @@ -3253,7 +3484,7 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 - which-typed-array@1.1.19: + which-typed-array@1.1.20: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.8 @@ -3274,6 +3505,14 @@ snapshots: word-wrap@1.2.5: {} + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + yaml@2.8.3: {} + yocto-queue@0.1.0: {} - zod@4.3.5: {} + zod@4.3.6: {} diff --git a/src/adapters/contracts/IEngineEvents.ts b/src/adapters/contracts/IEngineEvents.ts index 4d8f336..a4f1672 100644 --- a/src/adapters/contracts/IEngineEvents.ts +++ b/src/adapters/contracts/IEngineEvents.ts @@ -7,9 +7,15 @@ export abstract class IEngineEvents { * @param eventName - The event name to listen for * @param handler - The callback to invoke when the event is emitted */ - abstract on(eventName: string, handler?: (...args: any[]) => void): void + abstract on( + eventName: string, + handler?: (...args: TArgs) => void, + ): void - onRuntime(eventName: RuntimeEventName, handler?: (...args: any[]) => void): void { + onRuntime( + eventName: RuntimeEventName, + handler?: (...args: TArgs) => void, + ): void { this.on(this.getRuntimeEventMap()[eventName] ?? eventName, handler) } @@ -27,5 +33,5 @@ export abstract class IEngineEvents { * @param eventName - The event name to emit * @param args - Arguments to pass to event handlers */ - abstract emit(eventName: string, ...args: any[]): void + abstract emit(eventName: string, ...args: TArgs): void } diff --git a/src/adapters/contracts/IExports.ts b/src/adapters/contracts/IExports.ts index 5014c64..c31d685 100644 --- a/src/adapters/contracts/IExports.ts +++ b/src/adapters/contracts/IExports.ts @@ -1,4 +1,4 @@ export abstract class IExports { - abstract register(exportName: string, handler: (...args: any[]) => any): void - abstract getResource(resourceName: string): T | undefined + abstract register(exportName: string, handler: (...args: unknown[]) => unknown): void + abstract getResource(resourceName: string): T | undefined } diff --git a/src/adapters/contracts/client/IClientLocalPlayerBridge.ts b/src/adapters/contracts/client/IClientLocalPlayerBridge.ts index a9b41c9..b9b5b21 100644 --- a/src/adapters/contracts/client/IClientLocalPlayerBridge.ts +++ b/src/adapters/contracts/client/IClientLocalPlayerBridge.ts @@ -1,5 +1,29 @@ import type { Vector3 } from '../../../kernel/utils/vector3' +/** + * Port describing the local player from the client perspective. + * + * Adapters expose the local player's handle and spatial context so framework + * services do not need to reach into low-level platform primitives. + */ export abstract class IClientLocalPlayerBridge { + /** + * Returns the current runtime handle for the local player entity. + */ + abstract getHandle(): number + + /** + * Returns the current world position for the local player. + */ + abstract getPosition(): Vector3 + + /** + * Returns the current heading for the local player. + */ + abstract getHeading(): number + + /** + * Moves the local player to a new position and optional heading. + */ abstract setPosition(position: Vector3, heading?: number): void } diff --git a/src/adapters/contracts/client/IClientRuntimeBridge.ts b/src/adapters/contracts/client/IClientRuntimeBridge.ts index 1258612..e45a297 100644 --- a/src/adapters/contracts/client/IClientRuntimeBridge.ts +++ b/src/adapters/contracts/client/IClientRuntimeBridge.ts @@ -1,9 +1,12 @@ export abstract class IClientRuntimeBridge { abstract getCurrentResourceName(): string - abstract on(eventName: string, handler: (...args: any[]) => void | Promise): void + abstract on( + eventName: string, + handler: (...args: TArgs) => void | Promise, + ): void abstract registerCommand( commandName: string, - handler: (...args: any[]) => void, + handler: (...args: unknown[]) => void, restricted: boolean, ): void abstract registerKeyMapping( @@ -18,7 +21,7 @@ export abstract class IClientRuntimeBridge { registerWebViewCallback( eventName: string, - handler: (data: any, cb: (response: unknown) => void) => void | Promise, + handler: (data: unknown, cb: (response: unknown) => void) => void | Promise, ): void { this.registerNuiCallback(eventName, handler) } @@ -37,10 +40,13 @@ export abstract class IClientRuntimeBridge { abstract registerNuiCallback( eventName: string, - handler: (data: any, cb: (response: unknown) => void) => void | Promise, + handler: (data: unknown, cb: (response: unknown) => void) => void | Promise, ): void abstract sendNuiMessage(message: string): void abstract setNuiFocus(hasFocus: boolean, hasCursor: boolean): void abstract setNuiFocusKeepInput(keepInput: boolean): void - abstract registerExport(exportName: string, handler: (...args: any[]) => any): void + abstract registerExport( + exportName: string, + handler: (...args: TArgs) => TResult, + ): void } diff --git a/src/adapters/contracts/client/camera/IClientCameraPort.ts b/src/adapters/contracts/client/camera/IClientCameraPort.ts new file mode 100644 index 0000000..6b387ea --- /dev/null +++ b/src/adapters/contracts/client/camera/IClientCameraPort.ts @@ -0,0 +1,133 @@ +import type { Vector3 } from '../../../../kernel/utils/vector3' + +/** + * Euler rotation used by scripted camera implementations. + */ +export interface ClientCameraRotation { + x: number + y: number + z: number +} + +/** + * Complete transform payload for a scripted camera. + */ +export interface ClientCameraTransform { + position: Vector3 + rotation?: ClientCameraRotation + fov?: number +} + +/** + * Options for creating a new scripted camera. + */ +export interface ClientCameraCreateOptions { + camName?: string + active?: boolean + transform?: ClientCameraTransform +} + +/** + * Options for enabling or disabling camera rendering. + */ +export interface ClientCameraRenderOptions { + ease?: boolean + easeTimeMs?: number +} + +/** + * Shake parameters for a scripted camera. + */ +export interface ClientCameraShakeOptions { + type: string + amplitude: number +} + +/** + * Intent-oriented client camera port. + * + * The framework asks for high-level camera actions through this port and the + * active adapter decides how those actions are achieved for the runtime. + */ +export abstract class IClientCameraPort { + /** + * Creates a new scripted camera and returns its runtime handle. + */ + abstract create(options?: ClientCameraCreateOptions): number + + /** + * Activates or deactivates a specific camera. + */ + abstract setActive(camera: number, active: boolean): void + + /** + * Enables or disables scripted camera rendering. + */ + abstract render(enable: boolean, options?: ClientCameraRenderOptions): void + + /** + * Destroys a single scripted camera. + */ + abstract destroy(camera: number, destroyActiveCamera?: boolean): void + + /** + * Destroys every scripted camera controlled by the adapter/runtime. + */ + abstract destroyAll(destroyActiveCamera?: boolean): void + + /** + * Applies a transform to a camera. + */ + abstract setTransform(camera: number, transform: ClientCameraTransform): void + + /** + * Updates only the position of a camera. + */ + abstract setPosition(camera: number, position: Vector3): void + + /** + * Updates only the rotation of a camera. + */ + abstract setRotation(camera: number, rotation: ClientCameraRotation, rotationOrder?: number): void + + /** + * Updates only the field of view of a camera. + */ + abstract setFov(camera: number, fov: number): void + + /** + * Makes the camera point at a world position. + */ + abstract pointAtCoords(camera: number, position: Vector3): void + + /** + * Makes the camera point at an entity with an optional offset. + */ + abstract pointAtEntity(camera: number, entity: number, offset?: Vector3): void + + /** + * Stops any pointing target on the camera. + */ + abstract stopPointing(camera: number): void + + /** + * Interpolates from one camera to another. + */ + abstract interpolate( + fromCamera: number, + toCamera: number, + durationMs: number, + easeLocation?: boolean, + easeRotation?: boolean, + ): void + + /** + * Starts a camera shake effect. + */ + abstract shake(camera: number, options: ClientCameraShakeOptions): void + + /** + * Stops any active shake effect on a camera. + */ + abstract stopShaking(camera: number, stopImmediately?: boolean): void +} diff --git a/src/adapters/contracts/client/camera/index.ts b/src/adapters/contracts/client/camera/index.ts new file mode 100644 index 0000000..9985c65 --- /dev/null +++ b/src/adapters/contracts/client/camera/index.ts @@ -0,0 +1 @@ +export * from './IClientCameraPort' diff --git a/src/adapters/contracts/client/index.ts b/src/adapters/contracts/client/index.ts index 9094d14..aef3a46 100644 --- a/src/adapters/contracts/client/index.ts +++ b/src/adapters/contracts/client/index.ts @@ -1,7 +1,11 @@ +export * from './camera' export * from './IClientLogConsole' export * from './IClientLocalPlayerBridge' export * from './IClientPlatformBridge' export * from './IClientRuntimeBridge' export * from './IGtaPedAppearanceBridge' +export * from './ped' +export * from './progress' export * from './spawn' export * from './ui' +export * from './vehicle' diff --git a/src/adapters/contracts/client/ped/IClientPedPort.ts b/src/adapters/contracts/client/ped/IClientPedPort.ts new file mode 100644 index 0000000..c84265e --- /dev/null +++ b/src/adapters/contracts/client/ped/IClientPedPort.ts @@ -0,0 +1,71 @@ +import type { Vector3 } from '../../../../kernel/utils/vector3' + +/** + * High-level options for spawning a ped. + */ +export interface ClientPedSpawnOptions { + model: string + position: Vector3 + heading?: number + networked?: boolean + missionEntity?: boolean + relationshipGroup?: string + blockEvents?: boolean +} + +/** + * High-level animation request for a ped. + */ +export interface ClientPedAnimationOptions { + dict: string + anim: string + blendInSpeed?: number + blendOutSpeed?: number + duration?: number + flags?: number + playbackRate?: number +} + +/** + * Intent-oriented client ped port. + */ +export abstract class IClientPedPort { + /** Spawns a ped and returns its runtime handle. */ + abstract spawn(options: ClientPedSpawnOptions): Promise + /** Deletes a ped handle if it exists. */ + abstract delete(handle: number): void + /** Returns whether a ped handle exists. */ + abstract exists(handle: number): boolean + /** Plays an animation on a ped. */ + abstract playAnimation(handle: number, options: ClientPedAnimationOptions): Promise + /** Stops the current animation/task on a ped. */ + abstract stopAnimation(handle: number): void + /** Stops the current animation/task immediately on a ped. */ + abstract stopAnimationImmediately(handle: number): void + /** Freezes or unfreezes a ped. */ + abstract freeze(handle: number, freeze: boolean): void + /** Sets invincibility for a ped. */ + abstract setInvincible(handle: number, invincible: boolean): void + /** Gives a weapon to a ped. */ + abstract giveWeapon( + handle: number, + weapon: string, + ammo?: number, + hidden?: boolean, + forceInHand?: boolean, + ): void + /** Removes all weapons from a ped. */ + abstract removeAllWeapons(handle: number): void + /** Returns the closest ped to the local player. */ + abstract getClosest(radius?: number, excludeLocalPlayer?: boolean): number | null + /** Returns nearby ped handles. */ + abstract getNearby(position: Vector3, radius: number, excludeEntity?: number): number[] + /** Makes a ped look at an entity. */ + abstract lookAtEntity(handle: number, entity: number, duration?: number): void + /** Makes a ped look at coordinates. */ + abstract lookAtCoords(handle: number, position: Vector3, duration?: number): void + /** Makes a ped walk to coordinates. */ + abstract walkTo(handle: number, position: Vector3, speed?: number): void + /** Sets basic combat intent flags for a ped. */ + abstract setCombatAttributes(handle: number, canFight: boolean, canUseCover?: boolean): void +} diff --git a/src/adapters/contracts/client/ped/index.ts b/src/adapters/contracts/client/ped/index.ts new file mode 100644 index 0000000..2c0701a --- /dev/null +++ b/src/adapters/contracts/client/ped/index.ts @@ -0,0 +1 @@ +export * from './IClientPedPort' diff --git a/src/adapters/contracts/client/progress/IClientProgressPort.ts b/src/adapters/contracts/client/progress/IClientProgressPort.ts new file mode 100644 index 0000000..dc8719d --- /dev/null +++ b/src/adapters/contracts/client/progress/IClientProgressPort.ts @@ -0,0 +1,56 @@ +import type { Vector3 } from '../../../../kernel/utils/vector3' + +/** + * Progress task options used by client adapters. + */ +export interface ClientProgressOptions { + label: string + duration: number + useCircular?: boolean + canCancel?: boolean + disableControls?: boolean + disableMovement?: boolean + disableCombat?: boolean + animation?: { + dict: string + anim: string + flags?: number + } + prop?: { + model: string + bone: number + offset: Vector3 + rotation: Vector3 + } +} + +/** + * Runtime snapshot for an active progress task. + */ +export interface ClientProgressState { + active: boolean + progress: number + label: string + startTime: number + duration: number + options: ClientProgressOptions +} + +/** + * Intent-oriented client progress port. + * + * Adapters own the platform-specific implementation for task animations, + * props, controls, and HUD rendering while the framework exposes a stable API. + */ +export abstract class IClientProgressPort { + /** Starts a progress task and resolves to true when completed. */ + abstract start(options: ClientProgressOptions): Promise + /** Cancels the active task if one exists. */ + abstract cancel(): void + /** Returns whether a task is active. */ + abstract isActive(): boolean + /** Returns the current progress percentage. */ + abstract getProgress(): number + /** Returns the active task snapshot. */ + abstract getState(): ClientProgressState | null +} diff --git a/src/adapters/contracts/client/progress/index.ts b/src/adapters/contracts/client/progress/index.ts new file mode 100644 index 0000000..650a46c --- /dev/null +++ b/src/adapters/contracts/client/progress/index.ts @@ -0,0 +1 @@ +export * from './IClientProgressPort' diff --git a/src/adapters/contracts/client/spawn/IClientSpawnBridge.ts b/src/adapters/contracts/client/spawn/IClientSpawnBridge.ts index bc5f6ac..6a19f25 100644 --- a/src/adapters/contracts/client/spawn/IClientSpawnBridge.ts +++ b/src/adapters/contracts/client/spawn/IClientSpawnBridge.ts @@ -1,8 +1,6 @@ -import type { RespawnRequest, SpawnRequest, TeleportRequest } from './types' +import { IClientSpawnPort } from './IClientSpawnPort' -export abstract class IClientSpawnBridge { - abstract waitUntilReady(timeoutMs?: number): Promise - abstract spawn(request: SpawnRequest): Promise - abstract respawn(request: RespawnRequest): Promise - abstract teleport(request: TeleportRequest): Promise -} +/** + * @deprecated Use IClientSpawnPort for new runtime integrations. + */ +export abstract class IClientSpawnBridge extends IClientSpawnPort {} diff --git a/src/adapters/contracts/client/spawn/IClientSpawnPort.ts b/src/adapters/contracts/client/spawn/IClientSpawnPort.ts new file mode 100644 index 0000000..7553251 --- /dev/null +++ b/src/adapters/contracts/client/spawn/IClientSpawnPort.ts @@ -0,0 +1,23 @@ +import type { RespawnRequest, SpawnExecutionResult, SpawnRequest, TeleportRequest } from './types' + +export abstract class IClientSpawnPort { + /** + * Waits until the runtime reports that spawning can safely occur. + */ + abstract waitUntilReady(timeoutMs?: number): Promise + + /** + * Performs a full local-player spawn. + */ + abstract spawn(request: SpawnRequest): Promise + + /** + * Performs a respawn flow for the local player. + */ + abstract respawn(request: RespawnRequest): Promise + + /** + * Repositions the local player without a full spawn sequence. + */ + abstract teleport(request: TeleportRequest): Promise +} diff --git a/src/adapters/contracts/client/spawn/index.ts b/src/adapters/contracts/client/spawn/index.ts index 0fb4764..285f6ad 100644 --- a/src/adapters/contracts/client/spawn/index.ts +++ b/src/adapters/contracts/client/spawn/index.ts @@ -1,2 +1,4 @@ +export * from './IClientSpawnPort' export * from './IClientSpawnBridge' export * from './types' +export * from './types' diff --git a/src/adapters/contracts/client/spawn/types.ts b/src/adapters/contracts/client/spawn/types.ts index 3853a0f..57f6895 100644 --- a/src/adapters/contracts/client/spawn/types.ts +++ b/src/adapters/contracts/client/spawn/types.ts @@ -6,6 +6,10 @@ export interface SpawnRequest { heading?: number } +export interface SpawnExecutionResult { + localPlayerHandle?: number +} + export interface TeleportRequest { position: Vector3 heading?: number diff --git a/src/adapters/contracts/client/ui/webview/IClientWebViewBridge.ts b/src/adapters/contracts/client/ui/webview/IClientWebViewBridge.ts index 0cb324d..dfbd890 100644 --- a/src/adapters/contracts/client/ui/webview/IClientWebViewBridge.ts +++ b/src/adapters/contracts/client/ui/webview/IClientWebViewBridge.ts @@ -14,6 +14,7 @@ export abstract class IClientWebViewBridge { abstract hide(viewId: string): void abstract focus(viewId: string, options?: WebViewFocusOptions): void abstract blur(viewId: string): void + abstract markAsChat(viewId: string): void abstract send(viewId: string, event: string, payload: unknown): void abstract onMessage(handler: (message: WebViewMessage) => void | Promise): () => void } diff --git a/src/adapters/contracts/client/ui/webview/types.ts b/src/adapters/contracts/client/ui/webview/types.ts index 83c7e5d..fc3d5b3 100644 --- a/src/adapters/contracts/client/ui/webview/types.ts +++ b/src/adapters/contracts/client/ui/webview/types.ts @@ -5,6 +5,7 @@ export interface WebViewCapabilities { supportsBidirectionalMessaging: boolean supportsExecute: boolean supportsHeadless: boolean + supportsChatMode: boolean } export interface WebViewDefinition { @@ -14,6 +15,7 @@ export interface WebViewDefinition { focused?: boolean cursor?: boolean inputPassthrough?: boolean + chatMode?: boolean } export interface WebViewFocusOptions { diff --git a/src/adapters/contracts/client/vehicle/IClientVehiclePort.ts b/src/adapters/contracts/client/vehicle/IClientVehiclePort.ts new file mode 100644 index 0000000..62795c6 --- /dev/null +++ b/src/adapters/contracts/client/vehicle/IClientVehiclePort.ts @@ -0,0 +1,195 @@ +import type { Vector3 } from '../../../../kernel/utils/vector3' + +/** + * High-level vehicle creation options understood by client adapters. + */ +export interface ClientVehicleSpawnOptions { + model: string + position: Vector3 + heading?: number + placeOnGround?: boolean + warpIntoVehicle?: boolean + seatIndex?: number + primaryColor?: number + secondaryColor?: number + plate?: string + networked?: boolean +} + +/** + * Common vehicle modification payload used by the framework. + */ +export interface ClientVehicleMods { + spoiler?: number + frontBumper?: number + rearBumper?: number + sideSkirt?: number + exhaust?: number + frame?: number + grille?: number + hood?: number + fender?: number + rightFender?: number + roof?: number + engine?: number + brakes?: number + transmission?: number + horns?: number + suspension?: number + armor?: number + turbo?: boolean + xenon?: boolean + wheelType?: number + wheels?: number + windowTint?: number + livery?: number + plateStyle?: number + neonEnabled?: [boolean, boolean, boolean, boolean] + neonColor?: [number, number, number] + extras?: Record + pearlescentColor?: number + wheelColor?: number +} + +/** + * Intent-oriented client vehicle port. + * + * The framework requests vehicle operations through this port and the adapter + * decides how to fulfill them for the active runtime. + */ +export abstract class IClientVehiclePort { + /** + * Spawns a vehicle and returns its runtime handle. + */ + abstract spawn(options: ClientVehicleSpawnOptions): Promise + + /** + * Deletes an existing vehicle handle. + */ + abstract delete(vehicle: number): void + + /** + * Repairs a vehicle and restores it to a drivable state. + */ + abstract repair(vehicle: number): void + + /** + * Sets normalized fuel in the 0..1 range unless the adapter documents otherwise. + */ + abstract setFuel(vehicle: number, level: number): void + + /** + * Gets normalized fuel in the 0..1 range unless the adapter documents otherwise. + */ + abstract getFuel(vehicle: number): number + + /** + * Returns the closest vehicle around the local player. + */ + abstract getClosest(radius?: number): number | null + + /** + * Returns whether the local player is currently inside any vehicle. + */ + abstract isLocalPlayerInVehicle(): boolean + + /** + * Returns the vehicle currently occupied by the local player. + */ + abstract getCurrentForLocalPlayer(): number | null + + /** + * Returns the last vehicle used by the local player. + */ + abstract getLastForLocalPlayer(): number | null + + /** + * Returns whether the local player is driving the provided vehicle. + */ + abstract isLocalPlayerDriver(vehicle: number): boolean + + /** + * Warps the local player into a seat. + */ + abstract warpLocalPlayerInto(vehicle: number, seatIndex?: number): void + + /** + * Makes the local player leave the provided vehicle. + */ + abstract leaveLocalPlayerVehicle(vehicle: number, flags?: number): void + + /** + * Applies visual/performance modifications to a vehicle. + */ + abstract applyMods(vehicle: number, mods: ClientVehicleMods): void + + /** + * Updates door lock state. + */ + abstract setDoorsLocked(vehicle: number, locked: boolean): void + + /** + * Starts or stops the engine. + */ + abstract setEngineRunning(vehicle: number, running: boolean, instant?: boolean): void + + /** + * Sets invincibility state for the vehicle entity. + */ + abstract setInvincible(vehicle: number, invincible: boolean): void + + /** + * Returns speed in meters per second. + */ + abstract getSpeed(vehicle: number): number + + /** + * Sets heading for a vehicle. + */ + abstract setHeading(vehicle: number, heading: number): void + + /** + * Teleports a vehicle to a world position. + */ + abstract teleport(vehicle: number, position: Vector3, heading?: number): void + + /** + * Returns whether a vehicle handle exists. + */ + abstract exists(vehicle: number): boolean + + /** + * Resolves the runtime network identifier for a vehicle. + */ + abstract getNetworkId(vehicle: number): number + + /** + * Resolves a vehicle handle from a runtime network identifier. + */ + abstract getFromNetworkId(networkId: number): number + + /** + * Reads a runtime state bag/state entry from a vehicle. + */ + abstract getState(vehicle: number, key: string): T | undefined + + /** + * Returns the world position of a vehicle. + */ + abstract getPosition(vehicle: number): Vector3 | null + + /** + * Returns the heading of a vehicle. + */ + abstract getHeading(vehicle: number): number + + /** + * Returns the model hash of a vehicle. + */ + abstract getModel(vehicle: number): number + + /** + * Returns the current number plate text. + */ + abstract getPlate(vehicle: number): string +} diff --git a/src/adapters/contracts/client/vehicle/index.ts b/src/adapters/contracts/client/vehicle/index.ts new file mode 100644 index 0000000..088e866 --- /dev/null +++ b/src/adapters/contracts/client/vehicle/index.ts @@ -0,0 +1 @@ +export * from './IClientVehiclePort' diff --git a/src/adapters/contracts/transport/events.api.ts b/src/adapters/contracts/transport/events.api.ts index ceb6fed..1b9e5ec 100644 --- a/src/adapters/contracts/transport/events.api.ts +++ b/src/adapters/contracts/transport/events.api.ts @@ -2,8 +2,8 @@ import { EventContext, RuntimeContext } from './context' import { Player } from '../../../runtime/server/entities/player' type EmitArgs = C extends 'server' - ? [target: Player | number | number[] | 'all', ...args: any[]] - : [...args: any[]] + ? [target: Player | number | number[] | 'all', ...args: unknown[]] + : [...args: unknown[]] /** * broadcast and listen to events without relying on runtime. The adapter will be used. @@ -17,9 +17,9 @@ export abstract class EventsAPI { * Client: * - triggered by server */ - abstract on( + abstract on( event: string, - handler: (ctx: EventContext, ...args: any[]) => void | Promise, + handler: (ctx: EventContext, ...args: TArgs) => void | Promise, ): void /** @@ -30,7 +30,7 @@ export abstract class EventsAPI { * Client: * - sends to server, targetOrArg will be ignored */ - abstract emit(event: string, target: Player | number | number[] | 'all', ...args: any[]): void + abstract emit(event: string, target: Player | number | number[] | 'all', ...args: unknown[]): void /** * Emit an event. diff --git a/src/adapters/contracts/transport/rpc.api.ts b/src/adapters/contracts/transport/rpc.api.ts index 627584e..eaf3146 100644 --- a/src/adapters/contracts/transport/rpc.api.ts +++ b/src/adapters/contracts/transport/rpc.api.ts @@ -18,12 +18,12 @@ export type RpcTarget = number | number[] | 'all' export type RpcCallTarget = number | number[] type RpcCallArgs = C extends 'server' - ? [target: RpcCallTarget, ...args: any[]] - : [...args: any[]] + ? [target: RpcCallTarget, ...args: unknown[]] + : [...args: unknown[]] type RpcNotifyArgs = C extends 'server' - ? [target: RpcTarget, ...args: any[]] - : [...args: any[]] + ? [target: RpcTarget, ...args: unknown[]] + : [...args: unknown[]] /** * Remote Procedure Call API. @@ -56,7 +56,7 @@ export abstract class RpcAPI { * The handler receives a {@link RpcContext}. In server environments this usually includes * the `clientId` of the caller (via {@link EventContext}). */ - abstract on( + abstract on( name: string, handler: (ctx: RpcContext, ...args: TArgs) => TResult | Promise, ): void diff --git a/src/adapters/node/transport/node.events.ts b/src/adapters/node/transport/node.events.ts index 4a04f70..5ad4e78 100644 --- a/src/adapters/node/transport/node.events.ts +++ b/src/adapters/node/transport/node.events.ts @@ -8,18 +8,18 @@ type NodeTarget = number | number[] | 'all' export class NodeEvents extends EventsAPI { private readonly emitter = new EventEmitter() - on( + on( event: string, - handler: (ctx: { clientId?: number; raw?: unknown }, ...args: any[]) => any, + handler: (ctx: { clientId?: number; raw?: unknown }, ...args: TArgs) => unknown, ): void { - this.emitter.on(event, (ctx: { clientId?: number; raw?: unknown }, ...args: any[]) => { - void Promise.resolve(handler(ctx, ...args)).catch((err) => { + this.emitter.on(event, (ctx: { clientId?: number; raw?: unknown }, ...args: unknown[]) => { + void Promise.resolve(handler(ctx, ...(args as unknown as TArgs))).catch((err) => { loggers.netEvent.error(`handler error for '${event}'`, {}, err) }) }) } - emit(event: string, targetOrArg?: NodeTarget | any, ...args: any[]): void { + emit(event: string, targetOrArg?: NodeTarget | unknown, ...args: unknown[]): void { if (targetOrArg === 'all' || typeof targetOrArg === 'number' || Array.isArray(targetOrArg)) { const target = (targetOrArg ?? 'all') as NodeTarget const payloadArgs = args @@ -43,7 +43,7 @@ export class NodeEvents extends EventsAPI { this.emitter.emit(event, { clientId: -1, raw: -1 }, targetOrArg, ...args) } - simulateClientEvent(event: string, clientId: number, ...args: any[]): void { + simulateClientEvent(event: string, clientId: number, ...args: unknown[]): void { this.emitter.emit(event, { clientId, raw: clientId }, ...args) } diff --git a/src/adapters/node/transport/node.rpc.ts b/src/adapters/node/transport/node.rpc.ts index 9d2615e..b8f75c0 100644 --- a/src/adapters/node/transport/node.rpc.ts +++ b/src/adapters/node/transport/node.rpc.ts @@ -5,29 +5,29 @@ import type { RuntimeContext } from '../../contracts/transport/context' export class NodeRpc extends RpcAPI { private readonly handlers = new Map< string, - (ctx: { requestId: string; clientId?: number; raw?: unknown }, ...args: any[]) => unknown + (ctx: { requestId: string; clientId?: number; raw?: unknown }, ...args: unknown[]) => unknown >() constructor(private readonly context: C) { super() } - on( + on( name: string, handler: ( ctx: { requestId: string; clientId?: number; raw?: unknown }, ...args: TArgs ) => TResult | Promise, ): void { - this.handlers.set(name, handler as any) + this.handlers.set(name, (ctx, ...args) => handler(ctx, ...(args as unknown as TArgs))) } - call(name: string, ...args: any[]): Promise { + call(name: string, ...args: unknown[]): Promise { const { target, payload } = this.normalizeInvocation(name, 'call', args) return this.executeCall(name, payload, target) } - notify(name: string, ...args: any[]): Promise { + notify(name: string, ...args: unknown[]): Promise { const { target, payload } = this.normalizeInvocation(name, 'notify', args) return this.executeNotify(name, payload, target) } @@ -35,8 +35,8 @@ export class NodeRpc extends RpcAPI extends RpcAPI( name: string, - payload: any[], + payload: readonly unknown[], _target?: RpcTarget, ): Promise { const handler = this.handlers.get(name) @@ -88,7 +88,11 @@ export class NodeRpc extends RpcAPI { + private async executeNotify( + name: string, + payload: readonly unknown[], + _target?: RpcTarget, + ): Promise { const handler = this.handlers.get(name) if (!handler) { return diff --git a/src/contracts.ts b/src/contracts.ts index cf5c389..8e22545 100644 --- a/src/contracts.ts +++ b/src/contracts.ts @@ -2,3 +2,4 @@ export * from './adapters/contracts/IHasher' export * from './adapters/contracts/transport' export * from './adapters/contracts/types' export * from './adapters/contracts/runtime' +export * from './runtime/shared/types/system-types' diff --git a/src/index.ts b/src/index.ts index 16be3cc..1d737f2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,3 +3,4 @@ import 'reflect-metadata' export * from './kernel' export * from './runtime/core' export * from './contracts' +export * from './runtime/shared/types/system-types' diff --git a/src/kernel/logger/client-log-console.ts b/src/kernel/logger/client-log-console.ts index d60af6a..f739718 100644 --- a/src/kernel/logger/client-log-console.ts +++ b/src/kernel/logger/client-log-console.ts @@ -15,32 +15,36 @@ class DefaultClientLogConsole extends IClientLogConsole { } trace(message: string, details?: unknown): void { - this.write(console.debug, message, details) + this.write('debug', message, details) } debug(message: string, details?: unknown): void { - this.write(console.debug, message, details) + this.write('debug', message, details) } info(message: string, details?: unknown): void { - this.write(console.info, message, details) + this.write('info', message, details) } warn(message: string, details?: unknown): void { - this.write(console.warn, message, details) + this.write('warn', message, details) } error(message: string, details?: unknown): void { - this.write(console.error, message, details) + this.write('error', message, details) } - private write(method: (...args: unknown[]) => void, message: string, details?: unknown): void { + private write( + level: 'debug' | 'info' | 'warn' | 'error', + message: string, + details?: unknown, + ): void { if (details === undefined) { - method(message) + console[level](message) return } - method(message, details) + console[level](message, details) } } diff --git a/src/kernel/logger/index.ts b/src/kernel/logger/index.ts index af969e0..5998b50 100644 --- a/src/kernel/logger/index.ts +++ b/src/kernel/logger/index.ts @@ -22,7 +22,14 @@ export { export { ChildLogger, LoggerService } from './logger.service' export type { ClientLogConsoleCapabilities } from '../../adapters/contracts/client/IClientLogConsole' export type { LogContext, LogEntry } from './logger.types' -export { LogDomain, LogDomainLabels, LogLevel, LogLevelLabels, parseLogLevel } from './logger.types' +export { + getLogDomainLabel, + LogDomain, + LogDomainLabels, + LogLevel, + LogLevelLabels, + parseLogLevel, +} from './logger.types' export type { BufferedTransportOptions, LogOutputFormat } from './transports/buffered.transport' export { BufferedTransport } from './transports/buffered.transport' export type { ConsoleTransportOptions } from './transports/console.transport' diff --git a/src/kernel/logger/logger.types.ts b/src/kernel/logger/logger.types.ts index 54aa0d3..ed5508d 100644 --- a/src/kernel/logger/logger.types.ts +++ b/src/kernel/logger/logger.types.ts @@ -65,6 +65,52 @@ export const LogDomainLabels: Record = { [LogDomain.EXTERNAL]: 'EXTERNAL', } +declare const __OPENCORE_RESOURCE_NAME__: string | undefined + +function normalizeResourceName(resourceName: string): string { + return resourceName.trim().replace(/^\[(.*)\]$/, '$1') +} + +function getInjectedResourceName(): string | undefined { + if ( + typeof __OPENCORE_RESOURCE_NAME__ === 'string' && + normalizeResourceName(__OPENCORE_RESOURCE_NAME__).length > 0 + ) { + return normalizeResourceName(__OPENCORE_RESOURCE_NAME__) + } + + const fn = (globalThis as { GetCurrentResourceName?: unknown }).GetCurrentResourceName + if (typeof fn === 'function') { + try { + const value = fn() + if (typeof value === 'string' && normalizeResourceName(value).length > 0) { + return normalizeResourceName(value) + } + } catch { + // Ignore runtime lookup failures and fall back to default labels. + } + } + + return undefined +} + +export function getLogDomainLabel(domain: LogDomain): string { + if (domain !== LogDomain.FRAMEWORK) { + return LogDomainLabels[domain] + } + + const resourceName = getInjectedResourceName() + if (!resourceName) { + return LogDomainLabels[domain] + } + + if (resourceName.toLowerCase() === 'core') { + return 'CORE' + } + + return resourceName.toUpperCase() +} + /** * Additional contextual information that can be attached to any log entry. * Useful for tracing, debugging, and correlation. diff --git a/src/kernel/logger/transports/buffered.transport.ts b/src/kernel/logger/transports/buffered.transport.ts index 72a9b51..b4bde3e 100644 --- a/src/kernel/logger/transports/buffered.transport.ts +++ b/src/kernel/logger/transports/buffered.transport.ts @@ -1,4 +1,4 @@ -import { LogDomainLabels, type LogEntry, LogLevel, LogLevelLabels } from '../logger.types' +import { getLogDomainLabel, type LogEntry, LogLevel, LogLevelLabels } from '../logger.types' import { LogTransport } from './transport.interface' /** @@ -132,7 +132,7 @@ export class BufferedTransport implements LogTransport { const entries = this.buffer.map((entry) => ({ timestamp: entry.timestamp, level: LogLevelLabels[entry.level], - domain: LogDomainLabels[entry.domain], + domain: getLogDomainLabel(entry.domain), source: entry.context?.source, message: entry.message, context: this.cleanContext(entry.context), @@ -152,7 +152,7 @@ export class BufferedTransport implements LogTransport { .map((entry) => { const time = entry.timestamp.replace('T', ' ').slice(0, 23) const level = LogLevelLabels[entry.level].padEnd(5) - const domain = LogDomainLabels[entry.domain].padEnd(8) + const domain = getLogDomainLabel(entry.domain).padEnd(8) const source = entry.context?.source ? `[${entry.context.source}]` : '' let line = `${time} | ${domain} | ${level} | ${source} ${entry.message}` @@ -176,7 +176,7 @@ export class BufferedTransport implements LogTransport { const rows = this.buffer.map((entry) => [ entry.timestamp, LogLevelLabels[entry.level], - LogDomainLabels[entry.domain], + getLogDomainLabel(entry.domain), entry.context?.source ?? '', `"${entry.message.replace(/"/g, '""')}"`, JSON.stringify(this.cleanContext(entry.context)), diff --git a/src/kernel/logger/transports/console.transport.ts b/src/kernel/logger/transports/console.transport.ts index 6ece79b..f027544 100644 --- a/src/kernel/logger/transports/console.transport.ts +++ b/src/kernel/logger/transports/console.transport.ts @@ -1,6 +1,6 @@ import { + getLogDomainLabel, LogDomain, - LogDomainLabels, type LogEntry, LogLevel, LogLevelLabels, @@ -95,7 +95,7 @@ export class ConsoleTransport implements LogTransport { const { level, domain, message, timestamp, context, error } = entry const levelLabel = LogLevelLabels[level].padEnd(5) - const domainLabel = LogDomainLabels[domain] + const domainLabel = getLogDomainLabel(domain) const levelColor = this.colors ? LEVEL_COLORS[level] : '' const domainColor = this.colors ? DOMAIN_COLORS[domain] : '' const reset = this.colors ? COLORS.reset : '' diff --git a/src/kernel/logger/transports/simple-console.transport.ts b/src/kernel/logger/transports/simple-console.transport.ts index 2b2ccfd..952e66d 100644 --- a/src/kernel/logger/transports/simple-console.transport.ts +++ b/src/kernel/logger/transports/simple-console.transport.ts @@ -1,4 +1,4 @@ -import { LogDomainLabels, type LogEntry, LogLevel, LogLevelLabels } from '../logger.types' +import { getLogDomainLabel, type LogEntry, LogLevel, LogLevelLabels } from '../logger.types' import { getClientLogConsole } from '../client-log-console' import { LogTransport } from './transport.interface' @@ -52,7 +52,7 @@ export class SimpleConsoleTransport implements LogTransport { const capabilities = output.getCapabilities() const levelLabel = LogLevelLabels[level].padEnd(5) - const domainLabel = LogDomainLabels[domain] + const domainLabel = getLogDomainLabel(domain) // Build the log line without ANSI codes const parts: string[] = [] diff --git a/src/runtime/client/adapter/index.ts b/src/runtime/client/adapter/index.ts index 35857e3..bc821dd 100644 --- a/src/runtime/client/adapter/index.ts +++ b/src/runtime/client/adapter/index.ts @@ -1,6 +1,11 @@ export * from './client-adapter' +export * from '../../../adapters/contracts/client/camera/IClientCameraPort' export * from '../../../adapters/contracts/client/IClientLogConsole' +export * from '../../../adapters/contracts/client/ped/IClientPedPort' +export * from '../../../adapters/contracts/client/progress/IClientProgressPort' +export * from '../../../adapters/contracts/client/spawn/IClientSpawnPort' export * from '../../../adapters/contracts/client/spawn/IClientSpawnBridge' +export * from '../../../adapters/contracts/client/vehicle/IClientVehiclePort' export * from '../../../adapters/contracts/client/ui/IClientBlipBridge' export * from '../../../adapters/contracts/client/ui/IClientMarkerBridge' export * from '../../../adapters/contracts/client/ui/IClientNotificationBridge' diff --git a/src/runtime/client/adapter/node-camera-port.ts b/src/runtime/client/adapter/node-camera-port.ts new file mode 100644 index 0000000..f749f55 --- /dev/null +++ b/src/runtime/client/adapter/node-camera-port.ts @@ -0,0 +1,51 @@ +import { injectable } from 'tsyringe' +import { + type ClientCameraCreateOptions, + type ClientCameraRotation, + type ClientCameraRenderOptions, + type ClientCameraShakeOptions, + type ClientCameraTransform, + IClientCameraPort, +} from '../../../adapters/contracts/client/camera/IClientCameraPort' +import type { Vector3 } from '../../../kernel/utils/vector3' + +@injectable() +export class NodeClientCameraPort extends IClientCameraPort { + create(_options?: ClientCameraCreateOptions): number { + return 0 + } + + setActive(_camera: number, _active: boolean): void {} + + render(_enable: boolean, _options?: ClientCameraRenderOptions): void {} + + destroy(_camera: number, _destroyActiveCamera?: boolean): void {} + + destroyAll(_destroyActiveCamera?: boolean): void {} + + setTransform(_camera: number, _transform: ClientCameraTransform): void {} + + setPosition(_camera: number, _position: Vector3): void {} + + setRotation(_camera: number, _rotation: ClientCameraRotation, _rotationOrder?: number): void {} + + setFov(_camera: number, _fov: number): void {} + + pointAtCoords(_camera: number, _position: Vector3): void {} + + pointAtEntity(_camera: number, _entity: number, _offset?: Vector3): void {} + + stopPointing(_camera: number): void {} + + interpolate( + _fromCamera: number, + _toCamera: number, + _durationMs: number, + _easeLocation?: boolean, + _easeRotation?: boolean, + ): void {} + + shake(_camera: number, _options: ClientCameraShakeOptions): void {} + + stopShaking(_camera: number, _stopImmediately?: boolean): void {} +} diff --git a/src/runtime/client/adapter/node-client-adapter.ts b/src/runtime/client/adapter/node-client-adapter.ts index ecb5786..922c92e 100644 --- a/src/runtime/client/adapter/node-client-adapter.ts +++ b/src/runtime/client/adapter/node-client-adapter.ts @@ -6,20 +6,29 @@ import { NodeClientLocalPlayerBridge } from './node-local-player-bridge' import { NodeClientNotificationBridge } from './node-notification-bridge' import { IClientLogConsole } from '../../../adapters/contracts/client/IClientLogConsole' import { IClientSpawnBridge } from '../../../adapters/contracts/client/spawn/IClientSpawnBridge' +import { IClientSpawnPort } from '../../../adapters/contracts/client/spawn/IClientSpawnPort' import { IClientBlipBridge } from '../../../adapters/contracts/client/ui/IClientBlipBridge' import { IClientMarkerBridge } from '../../../adapters/contracts/client/ui/IClientMarkerBridge' import { IClientNotificationBridge } from '../../../adapters/contracts/client/ui/IClientNotificationBridge' import { IClientWebViewBridge } from '../../../adapters/contracts/client/ui/webview/IClientWebViewBridge' import { installNodeClientLogConsole, NodeClientLogConsole } from './node-log-console' import { NodeClientBlipBridge } from './node-blip-bridge' +import { NodeClientCameraPort } from './node-camera-port' import { NodeClientMarkerBridge } from './node-marker-bridge' +import { NodeClientPedPort } from './node-ped-port' import { NodeClientPlatformBridge } from './node-platform-bridge' +import { NodeClientProgressPort } from './node-progress-port' import { NodeClientSpawnBridge } from './node-spawn-bridge' +import { NodeClientVehiclePort } from './node-vehicle-port' import { NodeClientWebViewBridge } from './node-webview-bridge' import { IClientPlatformBridge } from './platform-bridge' import { NodeClientRuntimeBridge } from './node-runtime-bridge' import { defineClientAdapter, type OpenCoreClientAdapter } from './client-adapter' import { IClientRuntimeBridge } from './runtime-bridge' +import { IClientCameraPort } from '../../../adapters/contracts/client/camera/IClientCameraPort' +import { IClientPedPort } from '../../../adapters/contracts/client/ped/IClientPedPort' +import { IClientProgressPort } from '../../../adapters/contracts/client/progress/IClientProgressPort' +import { IClientVehiclePort } from '../../../adapters/contracts/client/vehicle/IClientVehiclePort' /** * Default client adapter used when no runtime adapter is provided. @@ -55,8 +64,21 @@ export function createNodeClientAdapter(): OpenCoreClientAdapter { NodeClientPlatformBridge, ) ctx.bindSingleton( - IClientSpawnBridge as InjectionToken, - NodeClientSpawnBridge, + IClientCameraPort as InjectionToken, + NodeClientCameraPort, + ) + ctx.bindSingleton( + IClientVehiclePort as InjectionToken, + NodeClientVehiclePort, + ) + ctx.bindSingleton(IClientPedPort as InjectionToken, NodeClientPedPort) + ctx.bindSingleton( + IClientProgressPort as InjectionToken, + NodeClientProgressPort, + ) + ctx.bindSingleton(IClientSpawnPort as InjectionToken, NodeClientSpawnBridge) + ctx.bindFactory(IClientSpawnBridge as InjectionToken, () => + ctx.container.resolve(IClientSpawnPort as InjectionToken), ) ctx.bindSingleton( IClientBlipBridge as InjectionToken, diff --git a/src/runtime/client/adapter/node-local-player-bridge.ts b/src/runtime/client/adapter/node-local-player-bridge.ts index 01b9cae..7358d3b 100644 --- a/src/runtime/client/adapter/node-local-player-bridge.ts +++ b/src/runtime/client/adapter/node-local-player-bridge.ts @@ -7,5 +7,17 @@ import { IClientLocalPlayerBridge } from './local-player-bridge' */ @injectable() export class NodeClientLocalPlayerBridge extends IClientLocalPlayerBridge { + getHandle(): number { + return 0 + } + + getPosition(): Vector3 { + return { x: 0, y: 0, z: 0 } + } + + getHeading(): number { + return 0 + } + setPosition(_position: Vector3, _heading?: number): void {} } diff --git a/src/runtime/client/adapter/node-log-console.ts b/src/runtime/client/adapter/node-log-console.ts index 9cdd87a..b70a80d 100644 --- a/src/runtime/client/adapter/node-log-console.ts +++ b/src/runtime/client/adapter/node-log-console.ts @@ -18,32 +18,36 @@ export class NodeClientLogConsole extends IClientLogConsole { } trace(message: string, details?: unknown): void { - this.write(console.debug, message, details) + this.write('debug', message, details) } debug(message: string, details?: unknown): void { - this.write(console.debug, message, details) + this.write('debug', message, details) } info(message: string, details?: unknown): void { - this.write(console.info, message, details) + this.write('info', message, details) } warn(message: string, details?: unknown): void { - this.write(console.warn, message, details) + this.write('warn', message, details) } error(message: string, details?: unknown): void { - this.write(console.error, message, details) + this.write('error', message, details) } - private write(method: (...args: unknown[]) => void, message: string, details?: unknown): void { + private write( + level: 'debug' | 'info' | 'warn' | 'error', + message: string, + details?: unknown, + ): void { if (details === undefined) { - method(message) + console[level](message) return } - method(message, details) + console[level](message, details) } } diff --git a/src/runtime/client/adapter/node-ped-port.ts b/src/runtime/client/adapter/node-ped-port.ts new file mode 100644 index 0000000..e863156 --- /dev/null +++ b/src/runtime/client/adapter/node-ped-port.ts @@ -0,0 +1,41 @@ +import { injectable } from 'tsyringe' +import { + type ClientPedAnimationOptions, + type ClientPedSpawnOptions, + IClientPedPort, +} from '../../../adapters/contracts/client/ped/IClientPedPort' +import type { Vector3 } from '../../../kernel/utils/vector3' + +@injectable() +export class NodeClientPedPort extends IClientPedPort { + async spawn(_options: ClientPedSpawnOptions): Promise { + return 0 + } + delete(_handle: number): void {} + exists(_handle: number): boolean { + return false + } + async playAnimation(_handle: number, _options: ClientPedAnimationOptions): Promise {} + stopAnimation(_handle: number): void {} + stopAnimationImmediately(_handle: number): void {} + freeze(_handle: number, _freeze: boolean): void {} + setInvincible(_handle: number, _invincible: boolean): void {} + giveWeapon( + _handle: number, + _weapon: string, + _ammo = 100, + _hidden = false, + _forceInHand = true, + ): void {} + removeAllWeapons(_handle: number): void {} + getClosest(_radius = 10, _excludeLocalPlayer = true): number | null { + return null + } + getNearby(_position: Vector3, _radius: number, _excludeEntity?: number): number[] { + return [] + } + lookAtEntity(_handle: number, _entity: number, _duration = -1): void {} + lookAtCoords(_handle: number, _position: Vector3, _duration = -1): void {} + walkTo(_handle: number, _position: Vector3, _speed = 1): void {} + setCombatAttributes(_handle: number, _canFight: boolean, _canUseCover = true): void {} +} diff --git a/src/runtime/client/adapter/node-progress-port.ts b/src/runtime/client/adapter/node-progress-port.ts new file mode 100644 index 0000000..c87af85 --- /dev/null +++ b/src/runtime/client/adapter/node-progress-port.ts @@ -0,0 +1,27 @@ +import { injectable } from 'tsyringe' +import { + type ClientProgressOptions, + type ClientProgressState, + IClientProgressPort, +} from '../../../adapters/contracts/client/progress/IClientProgressPort' + +@injectable() +export class NodeClientProgressPort extends IClientProgressPort { + start(_options: ClientProgressOptions): Promise { + return Promise.resolve(true) + } + + cancel(): void {} + + isActive(): boolean { + return false + } + + getProgress(): number { + return 0 + } + + getState(): ClientProgressState | null { + return null + } +} diff --git a/src/runtime/client/adapter/node-runtime-bridge.ts b/src/runtime/client/adapter/node-runtime-bridge.ts index dbfcc1a..27b8d8c 100644 --- a/src/runtime/client/adapter/node-runtime-bridge.ts +++ b/src/runtime/client/adapter/node-runtime-bridge.ts @@ -2,9 +2,7 @@ import { EventEmitter } from 'node:events' import { injectable } from 'tsyringe' import { IClientRuntimeBridge } from './runtime-bridge' -type RuntimeHandler = (...args: readonly unknown[]) => void | Promise type WebViewCallback = (data: unknown, cb: (response: unknown) => void) => void | Promise -type RuntimeExport = (...args: readonly unknown[]) => unknown /** * Node fallback runtime bridge used in tests and standalone execution. @@ -18,9 +16,12 @@ export class NodeClientRuntimeBridge extends IClientRuntimeBridge { return process.env.RESOURCE_NAME || 'default' } - on(eventName: string, handler: RuntimeHandler): void { + on( + eventName: string, + handler: (...args: TArgs) => void | Promise, + ): void { this.events.on(eventName, (...args) => { - void handler(...args) + void handler(...(args as unknown as TArgs)) }) } @@ -68,7 +69,10 @@ export class NodeClientRuntimeBridge extends IClientRuntimeBridge { setNuiFocusKeepInput(_keepInput: boolean): void {} - registerExport(exportName: string, handler: RuntimeExport): void { + registerExport( + exportName: string, + handler: (...args: TArgs) => TResult, + ): void { ;(globalThis as Record)[`__client_export:${exportName}`] = handler } } diff --git a/src/runtime/client/adapter/node-spawn-bridge.ts b/src/runtime/client/adapter/node-spawn-bridge.ts index 81ded5c..ac578cb 100644 --- a/src/runtime/client/adapter/node-spawn-bridge.ts +++ b/src/runtime/client/adapter/node-spawn-bridge.ts @@ -1,18 +1,23 @@ import { injectable } from 'tsyringe' -import { IClientSpawnBridge } from '../../../adapters/contracts/client/spawn/IClientSpawnBridge' +import { IClientSpawnPort } from '../../../adapters/contracts/client/spawn/IClientSpawnPort' import type { RespawnRequest, + SpawnExecutionResult, SpawnRequest, TeleportRequest, } from '../../../adapters/contracts/client/spawn/types' @injectable() -export class NodeClientSpawnBridge extends IClientSpawnBridge { +export class NodeClientSpawnBridge extends IClientSpawnPort { async waitUntilReady(_timeoutMs?: number): Promise {} - async spawn(_request: SpawnRequest): Promise {} + async spawn(_request: SpawnRequest): Promise { + return {} + } - async respawn(_request: RespawnRequest): Promise {} + async respawn(_request: RespawnRequest): Promise { + return {} + } async teleport(_request: TeleportRequest): Promise {} } diff --git a/src/runtime/client/adapter/node-vehicle-port.ts b/src/runtime/client/adapter/node-vehicle-port.ts new file mode 100644 index 0000000..77e1d5f --- /dev/null +++ b/src/runtime/client/adapter/node-vehicle-port.ts @@ -0,0 +1,70 @@ +import { injectable } from 'tsyringe' +import { + type ClientVehicleMods, + type ClientVehicleSpawnOptions, + IClientVehiclePort, +} from '../../../adapters/contracts/client/vehicle/IClientVehiclePort' +import type { Vector3 } from '../../../kernel/utils/vector3' + +@injectable() +export class NodeClientVehiclePort extends IClientVehiclePort { + async spawn(_options: ClientVehicleSpawnOptions): Promise { + return 0 + } + delete(_vehicle: number): void {} + repair(_vehicle: number): void {} + setFuel(_vehicle: number, _level: number): void {} + getFuel(_vehicle: number): number { + return 0 + } + getClosest(_radius = 10): number | null { + return null + } + isLocalPlayerInVehicle(): boolean { + return false + } + getCurrentForLocalPlayer(): number | null { + return null + } + getLastForLocalPlayer(): number | null { + return null + } + isLocalPlayerDriver(_vehicle: number): boolean { + return false + } + warpLocalPlayerInto(_vehicle: number, _seatIndex = -1): void {} + leaveLocalPlayerVehicle(_vehicle: number, _flags = 16): void {} + applyMods(_vehicle: number, _mods: ClientVehicleMods): void {} + setDoorsLocked(_vehicle: number, _locked: boolean): void {} + setEngineRunning(_vehicle: number, _running: boolean, _instant = false): void {} + setInvincible(_vehicle: number, _invincible: boolean): void {} + getSpeed(_vehicle: number): number { + return 0 + } + setHeading(_vehicle: number, _heading: number): void {} + teleport(_vehicle: number, _position: Vector3, _heading?: number): void {} + exists(_vehicle: number): boolean { + return false + } + getNetworkId(_vehicle: number): number { + return 0 + } + getFromNetworkId(_networkId: number): number { + return 0 + } + getState(_vehicle: number, _key: string): T | undefined { + return undefined + } + getPosition(_vehicle: number): Vector3 | null { + return null + } + getHeading(_vehicle: number): number { + return 0 + } + getModel(_vehicle: number): number { + return 0 + } + getPlate(_vehicle: number): string { + return '' + } +} diff --git a/src/runtime/client/adapter/node-webview-bridge.ts b/src/runtime/client/adapter/node-webview-bridge.ts index aec03fb..7e37d2d 100644 --- a/src/runtime/client/adapter/node-webview-bridge.ts +++ b/src/runtime/client/adapter/node-webview-bridge.ts @@ -14,6 +14,7 @@ const NODE_WEBVIEW_CAPABILITIES: WebViewCapabilities = { supportsBidirectionalMessaging: false, supportsExecute: false, supportsHeadless: false, + supportsChatMode: false, } @injectable() @@ -35,6 +36,7 @@ export class NodeClientWebViewBridge extends IClientWebViewBridge { hide(_viewId: string): void {} focus(_viewId: string, _options?: WebViewFocusOptions): void {} blur(_viewId: string): void {} + markAsChat(_viewId: string): void {} send(_viewId: string, _event: string, _payload: unknown): void {} onMessage(_handler: (message: WebViewMessage) => void | Promise): () => void { return () => {} diff --git a/src/runtime/client/controllers/appearance.controller.ts b/src/runtime/client/controllers/appearance.controller.ts index 0d25e3c..2efa991 100644 --- a/src/runtime/client/controllers/appearance.controller.ts +++ b/src/runtime/client/controllers/appearance.controller.ts @@ -1,7 +1,8 @@ import { inject } from 'tsyringe' import { PlayerAppearance } from '../../../kernel/shared' +import { SYSTEM_EVENTS } from '../../shared/types/system-types' import { Controller, OnNet } from '../decorators' -import { IClientPlatformBridge } from '../adapter/platform-bridge' +import { IClientLocalPlayerBridge } from '../adapter/local-player-bridge' import { AppearanceService } from '../services/appearance.service' import { loggers } from '../../../kernel/logger' @@ -9,20 +10,20 @@ import { loggers } from '../../../kernel/logger' export class AppearanceTestClientController { constructor( @inject(AppearanceService as any) private readonly appearanceService: AppearanceService, - @inject(IClientPlatformBridge as any) private readonly platform: IClientPlatformBridge, + @inject(IClientLocalPlayerBridge as any) private readonly localPlayer: IClientLocalPlayerBridge, ) {} - @OnNet('opencore:appearance:apply') + @OnNet(SYSTEM_EVENTS.appearance.apply) async onApply(appearance: PlayerAppearance): Promise { loggers.netEvent.debug('appearance:apply received', { appearance, }) - const ped = this.platform.getLocalPlayerPed() + const ped = this.localPlayer.getHandle() await this.appearanceService.applyAppearance(ped, appearance) } - @OnNet('opencore:appearance:reset') + @OnNet(SYSTEM_EVENTS.appearance.reset) onReset(): void { loggers.netEvent.debug('appearance:reset received') - const ped = this.platform.getLocalPlayerPed() + const ped = this.localPlayer.getHandle() this.appearanceService.setDefaultAppearance(ped) this.appearanceService.clearTattoos(ped) } diff --git a/src/runtime/client/controllers/spawner.controller.ts b/src/runtime/client/controllers/spawner.controller.ts index db6b1e6..ae533ac 100644 --- a/src/runtime/client/controllers/spawner.controller.ts +++ b/src/runtime/client/controllers/spawner.controller.ts @@ -1,5 +1,6 @@ import { Vector3 } from '../../../kernel/utils/vector3' import { loggers } from '../../../kernel/logger' +import { SYSTEM_EVENTS } from '../../shared/types/system-types' import { Controller, OnNet } from '../decorators' import { SpawnService } from '../services' @@ -7,18 +8,18 @@ import { SpawnService } from '../services' export class SpawnerController { constructor(private readonly spawnService: SpawnService) {} - @OnNet('opencore:spawner:spawn') + @OnNet(SYSTEM_EVENTS.spawner.spawn) async handleSpawn(data: { position: Vector3; model: string }) { loggers.spawn.debug('Spawn event received', data) await this.spawnService.spawn(data.position, data.model) } - @OnNet('opencore:spawner:respawn') + @OnNet(SYSTEM_EVENTS.spawner.respawn) async handleRespawn(position: Vector3, heading = 0.0): Promise { await this.spawnService.respawn(position, heading) } - @OnNet('opencore:spawner:teleport') + @OnNet(SYSTEM_EVENTS.spawner.teleport) async handleTeleport(position: Vector3, heading?: number): Promise { await this.spawnService.teleportTo(position, heading) } diff --git a/src/runtime/client/services/camera.ts b/src/runtime/client/services/camera.ts index dac9639..ee81327 100644 --- a/src/runtime/client/services/camera.ts +++ b/src/runtime/client/services/camera.ts @@ -1,33 +1,20 @@ import { inject, injectable } from 'tsyringe' import { Vector3 } from '../../../kernel/utils/vector3' -import { IClientPlatformBridge } from '../adapter/platform-bridge' - -export interface CameraRotation { - x: number - y: number - z: number -} - -export interface CameraTransform { - position: Vector3 - rotation?: CameraRotation - fov?: number -} - -export interface CameraCreateOptions { - camName?: string - active?: boolean - transform?: CameraTransform -} - -export interface CameraRenderOptions { - ease?: boolean - easeTimeMs?: number -} - -export interface CameraShakeOptions { - type: string - amplitude: number +import { + type ClientCameraCreateOptions as CameraCreateOptions, + type ClientCameraRenderOptions as CameraRenderOptions, + type ClientCameraRotation as CameraRotation, + type ClientCameraShakeOptions as CameraShakeOptions, + type ClientCameraTransform as CameraTransform, + IClientCameraPort, +} from '../../../adapters/contracts/client/camera/IClientCameraPort' + +export type { + CameraCreateOptions, + CameraRenderOptions, + CameraRotation, + CameraShakeOptions, + CameraTransform, } @injectable() @@ -35,17 +22,10 @@ export class Camera { private activeCam: number | null = null private rendering = false - constructor( - @inject(IClientPlatformBridge as any) private readonly platform: IClientPlatformBridge, - ) {} + constructor(@inject(IClientCameraPort as any) private readonly cameras: IClientCameraPort) {} create(options: CameraCreateOptions = {}): number { - const cam = this.platform.createCam( - options.camName ?? 'DEFAULT_SCRIPTED_CAMERA', - options.active ?? false, - ) - - if (options.transform) this.setTransform(cam, options.transform) + const cam = this.cameras.create(options) if (options.active) this.activeCam = cam return cam } @@ -55,19 +35,13 @@ export class Camera { } setActive(cam: number, active: boolean): void { - this.platform.setCamActive(cam, active) + this.cameras.setActive(cam, active) if (active) this.activeCam = cam else if (this.activeCam === cam) this.activeCam = null } render(enable: boolean, options: CameraRenderOptions = {}): void { - this.platform.renderScriptCams( - enable, - options.ease ?? false, - options.easeTimeMs ?? 0, - true, - true, - ) + this.cameras.render(enable, options) this.rendering = enable } @@ -76,43 +50,41 @@ export class Camera { } destroy(cam: number, destroyActiveCam = false): void { - this.platform.destroyCam(cam, destroyActiveCam) + this.cameras.destroy(cam, destroyActiveCam) if (this.activeCam === cam) this.activeCam = null } destroyAll(destroyActiveCam = false): void { - this.platform.destroyAllCams(destroyActiveCam) + this.cameras.destroyAll(destroyActiveCam) this.activeCam = null } setPosition(cam: number, position: Vector3): void { - this.platform.setCamCoord(cam, position) + this.cameras.setPosition(cam, position) } setRotation(cam: number, rotation: CameraRotation, rotationOrder = 2): void { - this.platform.setCamRot(cam, rotation, rotationOrder) + this.cameras.setRotation(cam, rotation, rotationOrder) } setFov(cam: number, fov: number): void { - this.platform.setCamFov(cam, fov) + this.cameras.setFov(cam, fov) } setTransform(cam: number, transform: CameraTransform): void { - this.setPosition(cam, transform.position) - if (transform.rotation) this.setRotation(cam, transform.rotation) - if (typeof transform.fov === 'number') this.setFov(cam, transform.fov) + this.cameras.setTransform(cam, transform) } pointAtCoords(cam: number, position: Vector3): void { - this.platform.pointCamAtCoord(cam, position) + this.cameras.pointAtCoords(cam, position) } pointAtEntity(cam: number, entity: number, offset: Vector3 = { x: 0, y: 0, z: 0 }): void { - this.platform.pointCamAtEntity(cam, entity, offset) + this.cameras.pointAtEntity(cam, entity, offset) } stopPointing(cam: number): void { - this.platform.stopCamPointing(cam) + this.cameras.stopPointing(cam) } interpolate( @@ -122,22 +94,16 @@ export class Camera { easeLocation = true, easeRotation = true, ): void { - this.platform.setCamActiveWithInterp( - toCam, - fromCam, - durationMs, - easeLocation ? 1 : 0, - easeRotation ? 1 : 0, - ) + this.cameras.interpolate(fromCam, toCam, durationMs, easeLocation, easeRotation) this.activeCam = toCam } shake(cam: number, options: CameraShakeOptions): void { - this.platform.shakeCam(cam, options.type, options.amplitude) + this.cameras.shake(cam, options) } stopShaking(cam: number, stopImmediately = true): void { - this.platform.stopCamShaking(cam, stopImmediately) + this.cameras.stopShaking(cam, stopImmediately) } reset(options: CameraRenderOptions = {}): void { diff --git a/src/runtime/client/services/notification.service.ts b/src/runtime/client/services/notification.service.ts index c77250a..b2aaa68 100644 --- a/src/runtime/client/services/notification.service.ts +++ b/src/runtime/client/services/notification.service.ts @@ -3,7 +3,7 @@ import { IClientNotificationBridge, type ClientNotificationDefinition, } from '../../../adapters/contracts/client/ui/IClientNotificationBridge' -import { IClientPlatformBridge } from '../adapter/platform-bridge' +import { IClientLocalPlayerBridge } from '../adapter/local-player-bridge' export type NotificationType = 'info' | 'success' | 'warning' | 'error' @@ -21,7 +21,7 @@ export class NotificationService { constructor( @inject(IClientNotificationBridge as any) private readonly notifications: IClientNotificationBridge, - @inject(IClientPlatformBridge as any) private readonly platform: IClientPlatformBridge, + @inject(IClientLocalPlayerBridge as any) private readonly localPlayer: IClientLocalPlayerBridge, ) {} show(message: string, blink = false): void { @@ -64,7 +64,7 @@ export class NotificationService { this.notifications.show({ kind: 'floating', message, - worldPosition: this.platform.getEntityCoords(this.platform.getLocalPlayerPed()), + worldPosition: this.localPlayer.getPosition(), }) } diff --git a/src/runtime/client/services/ped.service.ts b/src/runtime/client/services/ped.service.ts index ffcb592..1eed01c 100644 --- a/src/runtime/client/services/ped.service.ts +++ b/src/runtime/client/services/ped.service.ts @@ -1,26 +1,13 @@ import { inject, injectable } from 'tsyringe' import { Vector3 } from '../../../kernel/utils/vector3' -import { IClientPlatformBridge } from '../adapter/platform-bridge' +import { + type ClientPedAnimationOptions as PedAnimationOptions, + type ClientPedSpawnOptions as PedSpawnOptions, + IClientPedPort, +} from '../../../adapters/contracts/client/ped/IClientPedPort' +import { IClientLocalPlayerBridge } from '../adapter/local-player-bridge' -export interface PedSpawnOptions { - model: string - position: Vector3 - heading?: number - networked?: boolean - missionEntity?: boolean - relationshipGroup?: string - blockEvents?: boolean -} - -export interface PedAnimationOptions { - dict: string - anim: string - blendInSpeed?: number - blendOutSpeed?: number - duration?: number - flags?: number - playbackRate?: number -} +export type { PedSpawnOptions, PedAnimationOptions } export interface ManagedPed { id: string @@ -35,58 +22,29 @@ export class PedService { private idCounter = 0 constructor( - @inject(IClientPlatformBridge as any) private readonly platform: IClientPlatformBridge, + @inject(IClientPedPort as any) private readonly pedsPort: IClientPedPort, + @inject(IClientLocalPlayerBridge as any) private readonly localPlayer: IClientLocalPlayerBridge, ) {} async spawn(options: PedSpawnOptions): Promise<{ id: string; handle: number }> { - const { - model, - position, - heading = 0, - networked = false, - missionEntity = true, - blockEvents = true, - } = options - - const modelHash = this.platform.getHashKey(model) - if (!this.platform.isModelInCdimage(modelHash) || !this.platform.isModelValid(modelHash)) { - throw new Error(`Invalid ped model: ${model}`) - } - - this.platform.requestModel(modelHash) - while (!this.platform.hasModelLoaded(modelHash)) { - await new Promise((r) => setTimeout(r, 0)) - } - - const ped = this.platform.createPed(4, modelHash, position, heading, networked, true) - this.platform.setModelAsNoLongerNeeded(modelHash) + const ped = await this.pedsPort.spawn(options) if (!ped || ped === 0) throw new Error('Failed to create ped') - if (missionEntity) this.platform.setEntityAsMissionEntity(ped, true, true) - if (blockEvents) this.platform.setBlockingOfNonTemporaryEvents(ped, true) - this.platform.setPedRelationshipGroupHash(ped, this.platform.getHashKey('CIVMALE')) - const id = `ped_${++this.idCounter}` - this.peds.set(id, { id, handle: ped, model, position }) + this.peds.set(id, { id, handle: ped, model: options.model, position: options.position }) return { id, handle: ped } } delete(id: string): boolean { const ped = this.peds.get(id) if (!ped) return false - if (this.platform.doesEntityExist(ped.handle)) { - this.platform.setEntityAsMissionEntity(ped.handle, true, true) - this.platform.deletePed(ped.handle) - } + this.pedsPort.delete(ped.handle) this.peds.delete(id) return true } deleteByHandle(handle: number): void { - if (this.platform.doesEntityExist(handle)) { - this.platform.setEntityAsMissionEntity(handle, true, true) - this.platform.deletePed(handle) - } + this.pedsPort.delete(handle) for (const [id, ped] of this.peds) { if (ped.handle === handle) { this.peds.delete(id) @@ -97,112 +55,69 @@ export class PedService { deleteAll(): void { for (const ped of this.peds.values()) { - if (this.platform.doesEntityExist(ped.handle)) { - this.platform.setEntityAsMissionEntity(ped.handle, true, true) - this.platform.deletePed(ped.handle) - } + this.pedsPort.delete(ped.handle) } this.peds.clear() } async playAnimation(handle: number, options: PedAnimationOptions): Promise { - const { - dict, - anim, - blendInSpeed = 8.0, - blendOutSpeed = -8.0, - duration = -1, - flags = 1, - playbackRate = 0.0, - } = options - if (!this.platform.doesEntityExist(handle)) return - this.platform.requestAnimDict(dict) - while (!this.platform.hasAnimDictLoaded(dict)) { - await new Promise((r) => setTimeout(r, 0)) - } - this.platform.taskPlayAnim( - handle, - dict, - anim, - blendInSpeed, - blendOutSpeed, - duration, - flags, - playbackRate, - ) + await this.pedsPort.playAnimation(handle, options) } stopAnimation(handle: number): void { - if (!this.platform.doesEntityExist(handle)) return - this.platform.clearPedTasks(handle) + this.pedsPort.stopAnimation(handle) } stopAnimationImmediately(handle: number): void { - if (!this.platform.doesEntityExist(handle)) return - this.platform.clearPedTasksImmediately(handle) + this.pedsPort.stopAnimationImmediately(handle) } freeze(handle: number, freeze: boolean): void { - if (!this.platform.doesEntityExist(handle)) return - this.platform.freezeEntityPosition(handle, freeze) + this.pedsPort.freeze(handle, freeze) } setInvincible(handle: number, invincible: boolean): void { - if (!this.platform.doesEntityExist(handle)) return - this.platform.setEntityInvincible(handle, invincible) + this.pedsPort.setInvincible(handle, invincible) } giveWeapon(handle: number, weapon: string, ammo = 100, hidden = false, forceInHand = true): void { - if (!this.platform.doesEntityExist(handle)) return - this.platform.giveWeaponToPed( - handle, - this.platform.getHashKey(weapon), - ammo, - hidden, - forceInHand, - ) + this.pedsPort.giveWeapon(handle, weapon, ammo, hidden, forceInHand) } removeAllWeapons(handle: number): void { - if (!this.platform.doesEntityExist(handle)) return - this.platform.removeAllPedWeapons(handle, true) + this.pedsPort.removeAllWeapons(handle) } getClosest(radius = 10.0, excludePlayer = true): number | null { - const playerPed = this.platform.getLocalPlayerPed() - const handle = this.platform.getClosestPed(this.platform.getEntityCoords(playerPed), radius) + const playerPed = this.localPlayer.getHandle() + const handle = this.pedsPort.getClosest(radius, excludePlayer) if (!handle) return null if (excludePlayer && handle === playerPed) return null return handle } getNearby(position: Vector3, radius: number, excludePlayer = true): number[] { - return this.platform.getNearbyPeds( + return this.pedsPort.getNearby( position, radius, - excludePlayer ? this.platform.getLocalPlayerPed() : undefined, + excludePlayer ? this.localPlayer.getHandle() : undefined, ) } lookAtEntity(handle: number, entity: number, duration = -1): void { - if (!this.platform.doesEntityExist(handle)) return - this.platform.taskLookAtEntity(handle, entity, duration) + this.pedsPort.lookAtEntity(handle, entity, duration) } lookAtCoords(handle: number, position: Vector3, duration = -1): void { - if (!this.platform.doesEntityExist(handle)) return - this.platform.taskLookAtCoord(handle, position, duration) + this.pedsPort.lookAtCoords(handle, position, duration) } walkTo(handle: number, position: Vector3, speed = 1.0): void { - if (!this.platform.doesEntityExist(handle)) return - this.platform.taskGoStraightToCoord(handle, position, speed) + this.pedsPort.walkTo(handle, position, speed) } setCombatAttributes(handle: number, canFight: boolean, canUseCover = true): void { - if (!this.platform.doesEntityExist(handle)) return - this.platform.setPedCombatAttributes(handle, 46, canFight) - this.platform.setPedCombatAttributes(handle, 0, canUseCover) + this.pedsPort.setCombatAttributes(handle, canFight, canUseCover) } get(id: string): ManagedPed | undefined { @@ -213,6 +128,6 @@ export class PedService { } exists(id: string): boolean { const ped = this.peds.get(id) - return ped ? this.platform.doesEntityExist(ped.handle) : false + return ped ? this.pedsPort.exists(ped.handle) : false } } diff --git a/src/runtime/client/services/progress.service.ts b/src/runtime/client/services/progress.service.ts index 12608f3..76f955b 100644 --- a/src/runtime/client/services/progress.service.ts +++ b/src/runtime/client/services/progress.service.ts @@ -1,225 +1,33 @@ import { inject, injectable } from 'tsyringe' -import { IClientPlatformBridge } from '../adapter/platform-bridge' -import { IClientRuntimeBridge } from '../adapter/runtime-bridge' +import { + type ClientProgressOptions as ProgressOptions, + type ClientProgressState as ProgressState, + IClientProgressPort, +} from '../../../adapters/contracts/client/progress/IClientProgressPort' -export interface ProgressOptions { - label: string - duration: number - useCircular?: boolean - canCancel?: boolean - disableControls?: boolean - disableMovement?: boolean - disableCombat?: boolean - animation?: { - dict: string - anim: string - flags?: number - } - prop?: { - model: string - bone: number - offset: { x: number; y: number; z: number } - rotation: { x: number; y: number; z: number } - } -} - -export interface ProgressState { - active: boolean - progress: number - label: string - startTime: number - duration: number - options: ProgressOptions -} - -type ProgressCallback = (completed: boolean) => void +export type { ProgressOptions, ProgressState } @injectable() export class ProgressService { - private state: ProgressState | null = null - private tickHandle: unknown = null - private callback: ProgressCallback | null = null - private propHandle: number | null = null - constructor( - @inject(IClientPlatformBridge as any) private readonly platform: IClientPlatformBridge, - @inject(IClientRuntimeBridge as any) private readonly runtime: IClientRuntimeBridge, + @inject(IClientProgressPort as any) private readonly progressPort: IClientProgressPort, ) {} async start(options: ProgressOptions): Promise { - if (this.state?.active) return false - return new Promise((resolve) => { - this.state = { - active: true, - progress: 0, - label: options.label, - startTime: this.runtime.getGameTimer(), - duration: options.duration, - options, - } - this.callback = resolve - void this.startProgress() - }) + return this.progressPort.start(options) } cancel(): void { - if (!this.state?.active) return - this.cleanup(false) + this.progressPort.cancel() } isActive(): boolean { - return this.state?.active ?? false + return this.progressPort.isActive() } getProgress(): number { - return this.state?.progress ?? 0 + return this.progressPort.getProgress() } getState(): ProgressState | null { - return this.state - } - - private async startProgress(): Promise { - if (!this.state) return - const { options } = this.state - const ped = this.platform.getLocalPlayerPed() - - if (options.animation) { - await this.loadAnimDict(options.animation.dict) - this.platform.taskPlayAnim( - ped, - options.animation.dict, - options.animation.anim, - 8.0, - -8.0, - options.duration, - options.animation.flags ?? 1, - 0.0, - ) - } - - if (options.prop) { - await this.loadModel(options.prop.model) - const propHash = this.platform.getHashKey(options.prop.model) - const coords = this.platform.getEntityCoords(ped) - this.propHandle = this.platform.createObject(propHash, coords, true, true, true) - this.platform.attachEntityToEntity( - this.propHandle, - ped, - this.platform.getPedBoneIndex(ped, options.prop.bone), - options.prop.offset, - options.prop.rotation, - ) - } - - this.tickHandle = this.runtime.setTick(() => { - if (!this.state) return - - const elapsed = this.runtime.getGameTimer() - this.state.startTime - this.state.progress = Math.min((elapsed / this.state.duration) * 100, 100) - - if (options.disableControls) { - this.platform.disableAllControlActions(0) - } else { - if (options.disableMovement) { - this.platform.disableControlAction(0, 30, true) - this.platform.disableControlAction(0, 31, true) - this.platform.disableControlAction(0, 21, true) - this.platform.disableControlAction(0, 22, true) - } - if (options.disableCombat) { - this.platform.disableControlAction(0, 24, true) - this.platform.disableControlAction(0, 25, true) - this.platform.disableControlAction(0, 47, true) - this.platform.disableControlAction(0, 58, true) - this.platform.disableControlAction(0, 263, true) - this.platform.disableControlAction(0, 264, true) - } - } - - if (options.canCancel && this.platform.isControlJustPressed(0, 200)) { - this.cancel() - return - } - - this.drawProgressBar() - - if (elapsed >= this.state.duration) this.cleanup(true) - }) - } - - private drawProgressBar(): void { - if (!this.state) return - const { label, progress, options } = this.state - - if (options.useCircular) { - this.platform.beginTextCommandBusyspinnerOn('STRING') - this.platform.addTextComponentString(label) - this.platform.endTextCommandBusyspinnerOn(4) - return - } - - const barWidth = 0.15 - const barHeight = 0.015 - const x = 0.5 - barWidth / 2 - const y = 0.88 - this.platform.drawRect(0.5, y + barHeight / 2, barWidth, barHeight, 0, 0, 0, 180) - const fillWidth = (barWidth * progress) / 100 - this.platform.drawRect( - x + fillWidth / 2, - y + barHeight / 2, - fillWidth, - barHeight, - 255, - 255, - 255, - 255, - ) - this.platform.setTextFont(4) - this.platform.setTextScale(0.35) - this.platform.setTextColour({ r: 255, g: 255, b: 255, a: 255 }) - this.platform.setTextCentre(true) - this.platform.beginTextCommandDisplayText('STRING') - this.platform.addTextComponentString(`${label} (${Math.floor(progress)}%)`) - this.platform.endTextCommandDisplayText(0.5, y - 0.03) - } - - private cleanup(completed: boolean): void { - const ped = this.platform.getLocalPlayerPed() - if (this.state?.options.animation) { - this.platform.stopAnimTask( - ped, - this.state.options.animation.dict, - this.state.options.animation.anim, - 1.0, - ) - } - if (this.propHandle) { - this.platform.deleteEntity(this.propHandle) - this.propHandle = null - } - if (this.tickHandle !== null) { - this.runtime.clearTick(this.tickHandle) - this.tickHandle = null - } - if (this.state?.options.useCircular) this.platform.busyspinnerOff() - this.state = null - if (this.callback) { - this.callback(completed) - this.callback = null - } - } - - private async loadAnimDict(dict: string): Promise { - this.platform.requestAnimDict(dict) - while (!this.platform.hasAnimDictLoaded(dict)) { - await new Promise((r) => setTimeout(r, 0)) - } - } - - private async loadModel(model: string): Promise { - const hash = this.platform.getHashKey(model) - this.platform.requestModel(hash) - while (!this.platform.hasModelLoaded(hash)) { - await new Promise((r) => setTimeout(r, 0)) - } + return this.progressPort.getState() } } diff --git a/src/runtime/client/services/session-bridge.service.ts b/src/runtime/client/services/session-bridge.service.ts index 32c3750..ed9f22b 100644 --- a/src/runtime/client/services/session-bridge.service.ts +++ b/src/runtime/client/services/session-bridge.service.ts @@ -2,6 +2,7 @@ import { inject, injectable } from 'tsyringe' import { EventsAPI } from '../../../adapters/contracts/transport/events.api' import { coreLogger, LogDomain } from '../../../kernel/logger' import { Vec3 } from '../../../kernel/utils/vector3' +import { SYSTEM_EVENTS } from '../../shared/types/system-types' import { IClientLocalPlayerBridge } from '../adapter/local-player-bridge' import { IClientRuntimeBridge } from '../adapter/runtime-bridge' @@ -29,14 +30,17 @@ export class ClientSessionBridgeService { clientSession.debug('Client session bridge initialized') }) - this.events.on('core:playerSessionInit', (_ctx, data: { playerId: string }) => { + this.events.on(SYSTEM_EVENTS.session.playerInit, (_ctx, data: { playerId: string }) => { this.playerId = data.playerId clientSession.info('Player session initialized', { playerId: data.playerId }) }) - this.events.on('core:teleportTo', (_ctx, x: number, y: number, z: number, heading?: number) => { - this.localPlayer.setPosition(Vec3.create(x, y, z), heading) - }) + this.events.on( + SYSTEM_EVENTS.session.teleportTo, + (_ctx, x: number, y: number, z: number, heading?: number) => { + this.localPlayer.setPosition(Vec3.create(x, y, z), heading) + }, + ) } getPlayerId(): string | undefined { diff --git a/src/runtime/client/services/spawn.service.ts b/src/runtime/client/services/spawn.service.ts index 94bd1cf..28aada2 100644 --- a/src/runtime/client/services/spawn.service.ts +++ b/src/runtime/client/services/spawn.service.ts @@ -1,9 +1,8 @@ import { inject, injectable } from 'tsyringe' import { loggers, PlayerAppearance } from '../../../kernel' import { Vector3 } from '../../../kernel/utils/vector3' -import { IClientPlatformBridge } from '../adapter/platform-bridge' import { AppearanceService } from './appearance.service' -import { IClientSpawnBridge } from '../../../adapters/contracts/client/spawn/IClientSpawnBridge' +import { IClientSpawnPort } from '../../../adapters/contracts/client/spawn/IClientSpawnPort' interface SpawnOptions { appearance?: PlayerAppearance @@ -16,8 +15,7 @@ export class SpawnService { constructor( private readonly appearanceService: AppearanceService, - @inject(IClientSpawnBridge as any) private readonly spawnBridge: IClientSpawnBridge, - @inject(IClientPlatformBridge as any) private readonly platform: IClientPlatformBridge, + @inject(IClientSpawnPort as any) private readonly spawnPort: IClientSpawnPort, ) {} async init(): Promise { @@ -39,11 +37,11 @@ export class SpawnService { try { loggers.spawn.debug('Waiting for spawn bridge readiness') - await this.spawnBridge.waitUntilReady() + await this.spawnPort.waitUntilReady() loggers.spawn.debug('Spawn bridge ready, executing spawn', { position, model, heading }) - await this.spawnBridge.spawn({ position, model, heading }) + const outcome = await this.spawnPort.spawn({ position, model, heading }) - const ped = this.platform.getLocalPlayerPed() + const ped = outcome.localPlayerHandle ?? 0 if (ped !== 0) { if (options?.appearance) { loggers.spawn.debug('Applying post-spawn appearance', { ped }) @@ -69,12 +67,12 @@ export class SpawnService { } async teleportTo(position: Vector3, heading?: number): Promise { - await this.spawnBridge.teleport({ position, heading }) + await this.spawnPort.teleport({ position, heading }) } async respawn(position: Vector3, heading = 0.0): Promise { - await this.spawnBridge.waitUntilReady() - await this.spawnBridge.respawn({ position, heading }) + await this.spawnPort.waitUntilReady() + await this.spawnPort.respawn({ position, heading }) this.spawned = true loggers.spawn.info('Player respawned', { position: { x: position.x, y: position.y, z: position.z }, diff --git a/src/runtime/client/services/vehicle-client.service.ts b/src/runtime/client/services/vehicle-client.service.ts index c1c9cc1..90c3584 100644 --- a/src/runtime/client/services/vehicle-client.service.ts +++ b/src/runtime/client/services/vehicle-client.service.ts @@ -1,7 +1,8 @@ import { inject, injectable } from 'tsyringe' +import { IClientVehiclePort } from '../../../adapters/contracts/client/vehicle/IClientVehiclePort' import { EventsAPI } from '../../../adapters/contracts/transport/events.api' import { Vector3 } from '../../../kernel/utils/vector3' -import { IClientPlatformBridge } from '../adapter/platform-bridge' +import { SYSTEM_EVENTS } from '../../shared/types/system-types' import { IClientRuntimeBridge } from '../adapter/runtime-bridge' import { SerializedVehicleData, @@ -20,7 +21,7 @@ export class VehicleClientService { constructor( @inject(EventsAPI as any) private readonly events: EventsAPI<'client'>, - @inject(IClientPlatformBridge as any) private readonly platform: IClientPlatformBridge, + @inject(IClientVehiclePort as any) private readonly vehicles: IClientVehiclePort, @inject(IClientRuntimeBridge as any) private readonly runtime: IClientRuntimeBridge, ) { this.registerEventHandlers() @@ -32,7 +33,7 @@ export class VehicleClientService { return new Promise((resolve) => { const requestId = this.requestIdCounter++ this.pendingCreations.set(requestId, resolve) - this.events.emit('opencore:vehicle:create', { ...options, _requestId: requestId }) + this.events.emit(SYSTEM_EVENTS.vehicle.create, { ...options, _requestId: requestId }) setTimeout(() => { if (!this.pendingCreations.has(requestId)) return this.pendingCreations.delete(requestId) @@ -44,7 +45,7 @@ export class VehicleClientService { async deleteVehicle(networkId: number): Promise { return new Promise((resolve) => { this.pendingDeletes.set(networkId, resolve) - this.events.emit('opencore:vehicle:delete', networkId) + this.events.emit(SYSTEM_EVENTS.vehicle.delete, networkId) setTimeout(() => { if (!this.pendingDeletes.has(networkId)) return this.pendingDeletes.delete(networkId) @@ -56,7 +57,7 @@ export class VehicleClientService { async repairVehicle(networkId: number): Promise { return new Promise((resolve) => { this.pendingRepairs.set(networkId, resolve) - this.events.emit('opencore:vehicle:repair', networkId) + this.events.emit(SYSTEM_EVENTS.vehicle.repair, networkId) setTimeout(() => { if (!this.pendingRepairs.has(networkId)) return this.pendingRepairs.delete(networkId) @@ -66,58 +67,51 @@ export class VehicleClientService { } getClosestVehicle(radius = 10.0): number | null { - const playerPed = this.platform.getLocalPlayerPed() - return this.platform.getClosestVehicle(this.platform.getEntityCoords(playerPed), radius) + return this.vehicles.getClosest(radius) } isPlayerInVehicle(): boolean { - return this.platform.isPedInAnyVehicle(this.platform.getLocalPlayerPed()) + return this.vehicles.isLocalPlayerInVehicle() } getCurrentVehicle(): number | null { - const ped = this.platform.getLocalPlayerPed() - if (!this.platform.isPedInAnyVehicle(ped)) return null - return this.platform.getVehiclePedIsIn(ped, false) + return this.vehicles.getCurrentForLocalPlayer() } getLastVehicle(): number | null { - return this.platform.getVehiclePedIsIn(this.platform.getLocalPlayerPed(), true) + return this.vehicles.getLastForLocalPlayer() } isPlayerDriver(): boolean { const vehicle = this.getCurrentVehicle() if (!vehicle) return false - return this.platform.getPedInVehicleSeat(vehicle, -1) === this.platform.getLocalPlayerPed() + return this.vehicles.isLocalPlayerDriver(vehicle) } getSpeed(vehicle: number): number { - if (!this.platform.doesEntityExist(vehicle)) return 0 - return this.platform.getEntitySpeed(vehicle) * 3.6 + return this.vehicles.getSpeed(vehicle) * 3.6 } getNetworkId(vehicle: number): number { - if (!this.platform.doesEntityExist(vehicle)) return 0 - return this.platform.networkGetNetworkIdFromEntity(vehicle) + return this.vehicles.getNetworkId(vehicle) } getVehicleFromNetworkId(networkId: number): number { - if (!this.platform.networkDoesEntityExistWithNetworkId(networkId)) return 0 - return this.platform.networkGetEntityFromNetworkId(networkId) + return this.vehicles.getFromNetworkId(networkId) } getVehicleState(vehicle: number, key: string): T | undefined { - if (!this.platform.doesEntityExist(vehicle)) return undefined - return this.platform.getEntityState(vehicle, key) + return this.vehicles.getState(vehicle, key) } setDoorsLocked(networkId: number, locked: boolean): void { - this.events.emit('opencore:vehicle:setLocked', networkId, locked) + this.events.emit(SYSTEM_EVENTS.vehicle.setLocked, networkId, locked) } async getVehicleData(networkId: number): Promise { return new Promise((resolve) => { this.pendingData.set(networkId, resolve) - this.events.emit('opencore:vehicle:getData', networkId) + this.events.emit(SYSTEM_EVENTS.vehicle.getData, networkId) setTimeout(() => { if (!this.pendingData.has(networkId)) return this.pendingData.delete(networkId) @@ -129,7 +123,7 @@ export class VehicleClientService { async getPlayerVehicles(): Promise { return new Promise((resolve) => { this.pendingPlayerVehicles = resolve - this.events.emit('opencore:vehicle:getPlayerVehicles') + this.events.emit(SYSTEM_EVENTS.vehicle.getPlayerVehicles) setTimeout(() => { if (!this.pendingPlayerVehicles) return this.pendingPlayerVehicles = null @@ -139,115 +133,44 @@ export class VehicleClientService { } warpIntoVehicle(vehicle: number, seatIndex: number = -1): void { - if (!this.platform.doesEntityExist(vehicle)) return - this.platform.taskWarpPedIntoVehicle(this.platform.getLocalPlayerPed(), vehicle, seatIndex) + this.vehicles.warpLocalPlayerInto(vehicle, seatIndex) } setHeading(vehicle: number, heading: number): void { - if (!this.platform.doesEntityExist(vehicle)) return - this.platform.setEntityHeading(vehicle, heading) + this.vehicles.setHeading(vehicle, heading) } getPosition(vehicle: number): Vector3 | null { - if (!this.platform.doesEntityExist(vehicle)) return null - return this.platform.getEntityCoords(vehicle) + return this.vehicles.getPosition(vehicle) } getHeading(vehicle: number): number { - if (!this.platform.doesEntityExist(vehicle)) return 0 - return this.platform.getEntityHeading(vehicle) + return this.vehicles.getHeading(vehicle) } getModel(vehicle: number): number { - if (!this.platform.doesEntityExist(vehicle)) return 0 - return this.platform.getEntityModel(vehicle) + return this.vehicles.getModel(vehicle) } getPlate(vehicle: number): string { - if (!this.platform.doesEntityExist(vehicle)) return '' - return this.platform.getVehicleNumberPlateText(vehicle) + return this.vehicles.getPlate(vehicle) } applyMods(vehicle: number, mods: Record): void { - if (!this.platform.doesEntityExist(vehicle)) return - this.platform.setVehicleModKit(vehicle, 0) - if (mods.spoiler !== undefined) this.platform.setVehicleMod(vehicle, 0, mods.spoiler, false) - if (mods.frontBumper !== undefined) - this.platform.setVehicleMod(vehicle, 1, mods.frontBumper, false) - if (mods.rearBumper !== undefined) - this.platform.setVehicleMod(vehicle, 2, mods.rearBumper, false) - if (mods.sideSkirt !== undefined) this.platform.setVehicleMod(vehicle, 3, mods.sideSkirt, false) - if (mods.exhaust !== undefined) this.platform.setVehicleMod(vehicle, 4, mods.exhaust, false) - if (mods.frame !== undefined) this.platform.setVehicleMod(vehicle, 5, mods.frame, false) - if (mods.grille !== undefined) this.platform.setVehicleMod(vehicle, 6, mods.grille, false) - if (mods.hood !== undefined) this.platform.setVehicleMod(vehicle, 7, mods.hood, false) - if (mods.fender !== undefined) this.platform.setVehicleMod(vehicle, 8, mods.fender, false) - if (mods.rightFender !== undefined) - this.platform.setVehicleMod(vehicle, 9, mods.rightFender, false) - if (mods.roof !== undefined) this.platform.setVehicleMod(vehicle, 10, mods.roof, false) - if (mods.engine !== undefined) this.platform.setVehicleMod(vehicle, 11, mods.engine, false) - if (mods.brakes !== undefined) this.platform.setVehicleMod(vehicle, 12, mods.brakes, false) - if (mods.transmission !== undefined) - this.platform.setVehicleMod(vehicle, 13, mods.transmission, false) - if (mods.horns !== undefined) this.platform.setVehicleMod(vehicle, 14, mods.horns, false) - if (mods.suspension !== undefined) - this.platform.setVehicleMod(vehicle, 15, mods.suspension, false) - if (mods.armor !== undefined) this.platform.setVehicleMod(vehicle, 16, mods.armor, false) - if (mods.turbo !== undefined) this.platform.toggleVehicleMod(vehicle, 18, mods.turbo) - if (mods.xenon !== undefined) this.platform.toggleVehicleMod(vehicle, 22, mods.xenon) - if (mods.wheelType !== undefined) this.platform.setVehicleWheelType(vehicle, mods.wheelType) - if (mods.wheels !== undefined) this.platform.setVehicleMod(vehicle, 23, mods.wheels, false) - if (mods.windowTint !== undefined) this.platform.setVehicleWindowTint(vehicle, mods.windowTint) - if (mods.livery !== undefined) this.platform.setVehicleLivery(vehicle, mods.livery) - if (mods.plateStyle !== undefined) - this.platform.setVehicleNumberPlateTextIndex(vehicle, mods.plateStyle) - if (mods.neonEnabled !== undefined) { - this.platform.setVehicleNeonLightEnabled(vehicle, 0, mods.neonEnabled[0]) - this.platform.setVehicleNeonLightEnabled(vehicle, 1, mods.neonEnabled[1]) - this.platform.setVehicleNeonLightEnabled(vehicle, 2, mods.neonEnabled[2]) - this.platform.setVehicleNeonLightEnabled(vehicle, 3, mods.neonEnabled[3]) - } - if (mods.neonColor !== undefined) { - this.platform.setVehicleNeonLightsColour( - vehicle, - mods.neonColor[0], - mods.neonColor[1], - mods.neonColor[2], - ) - } - if (mods.extras) { - for (const [extraId, enabled] of Object.entries(mods.extras)) { - this.platform.setVehicleExtra(vehicle, Number(extraId), !enabled) - } - } - if (mods.pearlescentColor !== undefined || mods.wheelColor !== undefined) { - const [currentPearl, currentWheel] = this.platform.getVehicleExtraColours(vehicle) - this.platform.setVehicleExtraColours( - vehicle, - mods.pearlescentColor ?? currentPearl, - mods.wheelColor ?? currentWheel, - ) - } + this.vehicles.applyMods(vehicle, mods) } repair(vehicle: number): void { - if (!this.platform.doesEntityExist(vehicle)) return - this.platform.setVehicleFixed(vehicle) - this.platform.setVehicleDeformationFixed(vehicle) - this.platform.setVehicleUndriveable(vehicle, false) - this.platform.setVehicleEngineOn(vehicle, true, true, false) - this.platform.setVehicleEngineHealth(vehicle, 1000.0) - this.platform.setVehiclePetrolTankHealth(vehicle, 1000.0) + this.vehicles.repair(vehicle) } setFuel(vehicle: number, level: number): void { - if (!this.platform.doesEntityExist(vehicle)) return - this.platform.setVehicleFuelLevel(vehicle, Math.max(0, Math.min(100, level))) + this.vehicles.setFuel(vehicle, level) } private registerEventHandlers(): void { this.events.on( - 'opencore:vehicle:createResult', + SYSTEM_EVENTS.vehicle.createResult, (_ctx, result: VehicleSpawnResult & { _requestId?: number }) => { if (result._requestId === undefined) return const callback = this.pendingCreations.get(result._requestId) @@ -258,7 +181,7 @@ export class VehicleClientService { ) this.events.on( - 'opencore:vehicle:deleteResult', + SYSTEM_EVENTS.vehicle.deleteResult, (_ctx, result: { networkId: number; success: boolean }) => { const callback = this.pendingDeletes.get(result.networkId) if (!callback) return @@ -268,7 +191,7 @@ export class VehicleClientService { ) this.events.on( - 'opencore:vehicle:repairResult', + SYSTEM_EVENTS.vehicle.repairResult, (_ctx, result: { networkId: number; success: boolean }) => { const callback = this.pendingRepairs.get(result.networkId) if (!callback) return @@ -277,7 +200,7 @@ export class VehicleClientService { }, ) - this.events.on('opencore:vehicle:dataResult', (_ctx, data: SerializedVehicleData | null) => { + this.events.on(SYSTEM_EVENTS.vehicle.dataResult, (_ctx, data: SerializedVehicleData | null) => { if (!data) return const callback = this.pendingData.get(data.networkId) if (!callback) return @@ -286,32 +209,35 @@ export class VehicleClientService { }) this.events.on( - 'opencore:vehicle:playerVehiclesResult', + SYSTEM_EVENTS.vehicle.playerVehiclesResult, (_ctx, vehicles: SerializedVehicleData[]) => { this.pendingPlayerVehicles?.(vehicles) this.pendingPlayerVehicles = null }, ) - this.events.on('opencore:vehicle:created', async (_ctx, data: SerializedVehicleData) => { + this.events.on(SYSTEM_EVENTS.vehicle.created, async (_ctx, data: SerializedVehicleData) => { const veh = await this.waitForVehicle(data.networkId) if (!veh) return if (data.mods && Object.keys(data.mods).length > 0) this.applyMods(veh, data.mods) if (data.metadata?.fuel !== undefined) this.setFuel(veh, data.metadata.fuel) }) - this.events.on('opencore:vehicle:modified', (_ctx, data: { networkId: number; mods: any }) => { - const veh = this.getVehicleFromNetworkId(data.networkId) - if (veh && this.platform.doesEntityExist(veh)) this.applyMods(veh, data.mods) - }) + this.events.on( + SYSTEM_EVENTS.vehicle.modified, + (_ctx, data: { networkId: number; mods: any }) => { + const veh = this.getVehicleFromNetworkId(data.networkId) + if (veh && this.vehicles.exists(veh)) this.applyMods(veh, data.mods) + }, + ) - this.events.on('opencore:vehicle:repaired', (_ctx, networkId: number) => { + this.events.on(SYSTEM_EVENTS.vehicle.repaired, (_ctx, networkId: number) => { const veh = this.getVehicleFromNetworkId(networkId) - if (veh && this.platform.doesEntityExist(veh)) this.repair(veh) + if (veh && this.vehicles.exists(veh)) this.repair(veh) }) this.events.on( - 'opencore:vehicle:warpInto', + SYSTEM_EVENTS.vehicle.warpInto, async (_ctx, networkId: number, seatIndex: number = -1) => { const veh = await this.waitForVehicle(networkId) if (veh) this.warpIntoVehicle(veh, seatIndex) @@ -323,7 +249,7 @@ export class VehicleClientService { const started = this.runtime.getGameTimer() while (this.runtime.getGameTimer() - started < 5000) { const veh = this.getVehicleFromNetworkId(networkId) - if (veh && this.platform.doesEntityExist(veh)) return veh + if (veh && this.vehicles.exists(veh)) return veh await new Promise((r) => setTimeout(r, 0)) } return null diff --git a/src/runtime/client/services/vehicle.service.ts b/src/runtime/client/services/vehicle.service.ts index 1e64de0..2afe284 100644 --- a/src/runtime/client/services/vehicle.service.ts +++ b/src/runtime/client/services/vehicle.service.ts @@ -1,220 +1,92 @@ import { inject, injectable } from 'tsyringe' import { Vector3 } from '../../../kernel/utils/vector3' -import { IClientPlatformBridge } from '../adapter/platform-bridge' - -export interface VehicleSpawnOptions { - model: string - position: Vector3 - heading?: number - placeOnGround?: boolean - warpIntoVehicle?: boolean - seatIndex?: number - primaryColor?: number - secondaryColor?: number - plate?: string - networked?: boolean -} +import { + type ClientVehicleMods as VehicleMods, + type ClientVehicleSpawnOptions as VehicleSpawnOptions, + IClientVehiclePort, +} from '../../../adapters/contracts/client/vehicle/IClientVehiclePort' -export interface VehicleMods { - spoiler?: number - frontBumper?: number - rearBumper?: number - sideSkirt?: number - exhaust?: number - frame?: number - grille?: number - hood?: number - fender?: number - rightFender?: number - roof?: number - engine?: number - brakes?: number - transmission?: number - horns?: number - suspension?: number - armor?: number - turbo?: boolean - xenon?: boolean - wheelType?: number - wheels?: number - windowTint?: number - livery?: number - plateStyle?: number -} +export type { VehicleSpawnOptions, VehicleMods } @injectable() export class VehicleService { - constructor( - @inject(IClientPlatformBridge as any) private readonly platform: IClientPlatformBridge, - ) {} + constructor(@inject(IClientVehiclePort as any) private readonly vehicles: IClientVehiclePort) {} async spawn(options: VehicleSpawnOptions): Promise { - const { - model, - position, - heading = 0, - placeOnGround = true, - warpIntoVehicle = false, - seatIndex = -1, - primaryColor, - secondaryColor, - plate, - networked = true, - } = options - - const modelHash = this.platform.getHashKey(model) - if (!this.platform.isModelInCdimage(modelHash) || !this.platform.isModelAVehicle(modelHash)) { - throw new Error(`Invalid vehicle model: ${model}`) - } - - this.platform.requestModel(modelHash) - while (!this.platform.hasModelLoaded(modelHash)) { - await new Promise((r) => setTimeout(r, 0)) - } - - const vehicle = this.platform.createVehicle(modelHash, position, heading, networked, false) - this.platform.setModelAsNoLongerNeeded(modelHash) - if (!vehicle || vehicle === 0) throw new Error('Failed to create vehicle') - - if (placeOnGround) this.platform.setVehicleOnGroundProperly(vehicle) - if (primaryColor !== undefined || secondaryColor !== undefined) { - const [currentPrimary, currentSecondary] = this.platform.getVehicleColours(vehicle) - this.platform.setVehicleColours( - vehicle, - primaryColor ?? currentPrimary, - secondaryColor ?? currentSecondary, - ) - } - if (plate) this.platform.setVehicleNumberPlateText(vehicle, plate) - if (warpIntoVehicle) - this.platform.taskWarpPedIntoVehicle(this.platform.getLocalPlayerPed(), vehicle, seatIndex) - return vehicle + return this.vehicles.spawn(options) } delete(vehicle: number): void { - if (this.platform.doesEntityExist(vehicle)) { - this.platform.setEntityAsMissionEntity(vehicle, true, true) - this.platform.deleteVehicle(vehicle) - } + this.vehicles.delete(vehicle) } deleteCurrentVehicle(): void { const vehicle = this.getCurrentVehicle() if (vehicle) { - this.platform.taskLeaveVehicle(this.platform.getLocalPlayerPed(), vehicle, 16) + this.vehicles.leaveLocalPlayerVehicle(vehicle, 16) setTimeout(() => this.delete(vehicle), 1000) } } repair(vehicle: number): void { - if (!this.platform.doesEntityExist(vehicle)) return - this.platform.setVehicleFixed(vehicle) - this.platform.setVehicleDeformationFixed(vehicle) - this.platform.setVehicleUndriveable(vehicle, false) - this.platform.setVehicleEngineOn(vehicle, true, true, false) - this.platform.setVehicleEngineHealth(vehicle, 1000.0) - this.platform.setVehiclePetrolTankHealth(vehicle, 1000.0) + this.vehicles.repair(vehicle) } setFuel(vehicle: number, level: number): void { - if (!this.platform.doesEntityExist(vehicle)) return - this.platform.setVehicleFuelLevel(vehicle, Math.max(0, Math.min(100, level * 100))) + this.vehicles.setFuel(vehicle, level) } getFuel(vehicle: number): number { - if (!this.platform.doesEntityExist(vehicle)) return 0 - return this.platform.getVehicleFuelLevel(vehicle) / 100 + return this.vehicles.getFuel(vehicle) } getClosest(radius = 10.0): number | null { - const playerPed = this.platform.getLocalPlayerPed() - return this.platform.getClosestVehicle(this.platform.getEntityCoords(playerPed), radius) + return this.vehicles.getClosest(radius) } isPlayerInVehicle(): boolean { - return this.platform.isPedInAnyVehicle(this.platform.getLocalPlayerPed()) + return this.vehicles.isLocalPlayerInVehicle() } getCurrentVehicle(): number | null { - const ped = this.platform.getLocalPlayerPed() - if (!this.platform.isPedInAnyVehicle(ped)) return null - return this.platform.getVehiclePedIsIn(ped, false) + return this.vehicles.getCurrentForLocalPlayer() } getLastVehicle(): number | null { - return this.platform.getVehiclePedIsIn(this.platform.getLocalPlayerPed(), true) + return this.vehicles.getLastForLocalPlayer() } isPlayerDriver(): boolean { const vehicle = this.getCurrentVehicle() if (!vehicle) return false - return this.platform.getPedInVehicleSeat(vehicle, -1) === this.platform.getLocalPlayerPed() + return this.vehicles.isLocalPlayerDriver(vehicle) } setMods(vehicle: number, mods: VehicleMods): void { - if (!this.platform.doesEntityExist(vehicle)) return - this.platform.setVehicleModKit(vehicle, 0) - if (mods.spoiler !== undefined) this.platform.setVehicleMod(vehicle, 0, mods.spoiler, false) - if (mods.frontBumper !== undefined) - this.platform.setVehicleMod(vehicle, 1, mods.frontBumper, false) - if (mods.rearBumper !== undefined) - this.platform.setVehicleMod(vehicle, 2, mods.rearBumper, false) - if (mods.sideSkirt !== undefined) this.platform.setVehicleMod(vehicle, 3, mods.sideSkirt, false) - if (mods.exhaust !== undefined) this.platform.setVehicleMod(vehicle, 4, mods.exhaust, false) - if (mods.frame !== undefined) this.platform.setVehicleMod(vehicle, 5, mods.frame, false) - if (mods.grille !== undefined) this.platform.setVehicleMod(vehicle, 6, mods.grille, false) - if (mods.hood !== undefined) this.platform.setVehicleMod(vehicle, 7, mods.hood, false) - if (mods.fender !== undefined) this.platform.setVehicleMod(vehicle, 8, mods.fender, false) - if (mods.rightFender !== undefined) - this.platform.setVehicleMod(vehicle, 9, mods.rightFender, false) - if (mods.roof !== undefined) this.platform.setVehicleMod(vehicle, 10, mods.roof, false) - if (mods.engine !== undefined) this.platform.setVehicleMod(vehicle, 11, mods.engine, false) - if (mods.brakes !== undefined) this.platform.setVehicleMod(vehicle, 12, mods.brakes, false) - if (mods.transmission !== undefined) - this.platform.setVehicleMod(vehicle, 13, mods.transmission, false) - if (mods.horns !== undefined) this.platform.setVehicleMod(vehicle, 14, mods.horns, false) - if (mods.suspension !== undefined) - this.platform.setVehicleMod(vehicle, 15, mods.suspension, false) - if (mods.armor !== undefined) this.platform.setVehicleMod(vehicle, 16, mods.armor, false) - if (mods.turbo !== undefined) this.platform.toggleVehicleMod(vehicle, 18, mods.turbo) - if (mods.xenon !== undefined) this.platform.toggleVehicleMod(vehicle, 22, mods.xenon) - if (mods.wheelType !== undefined) this.platform.setVehicleWheelType(vehicle, mods.wheelType) - if (mods.wheels !== undefined) this.platform.setVehicleMod(vehicle, 23, mods.wheels, false) - if (mods.windowTint !== undefined) this.platform.setVehicleWindowTint(vehicle, mods.windowTint) - if (mods.livery !== undefined) this.platform.setVehicleLivery(vehicle, mods.livery) - if (mods.plateStyle !== undefined) - this.platform.setVehicleNumberPlateTextIndex(vehicle, mods.plateStyle) + this.vehicles.applyMods(vehicle, mods) } setDoorsLocked(vehicle: number, locked: boolean): void { - if (!this.platform.doesEntityExist(vehicle)) return - this.platform.setVehicleDoorsLocked(vehicle, locked ? 2 : 0) + this.vehicles.setDoorsLocked(vehicle, locked) } setEngineRunning(vehicle: number, running: boolean, instant = false): void { - if (!this.platform.doesEntityExist(vehicle)) return - this.platform.setVehicleEngineOn(vehicle, running, instant, true) + this.vehicles.setEngineRunning(vehicle, running, instant) } setInvincible(vehicle: number, invincible: boolean): void { - if (!this.platform.doesEntityExist(vehicle)) return - this.platform.setEntityInvincible(vehicle, invincible) + this.vehicles.setInvincible(vehicle, invincible) } getSpeed(vehicle: number): number { - if (!this.platform.doesEntityExist(vehicle)) return 0 - return this.platform.getEntitySpeed(vehicle) * 3.6 + return this.vehicles.getSpeed(vehicle) * 3.6 } setHeading(vehicle: number, heading: number): void { - if (!this.platform.doesEntityExist(vehicle)) return - this.platform.setEntityHeading(vehicle, heading) + this.vehicles.setHeading(vehicle, heading) } teleport(vehicle: number, position: Vector3, heading?: number): void { - if (!this.platform.doesEntityExist(vehicle)) return - this.platform.setEntityCoords(vehicle, position) - if (heading !== undefined) this.platform.setEntityHeading(vehicle, heading) - this.platform.setVehicleOnGroundProperly(vehicle) + this.vehicles.teleport(vehicle, position, heading) } } diff --git a/src/runtime/client/webview-bridge.ts b/src/runtime/client/webview-bridge.ts index 78f6bc5..83e521c 100644 --- a/src/runtime/client/webview-bridge.ts +++ b/src/runtime/client/webview-bridge.ts @@ -26,6 +26,7 @@ export class WebViewBridge< focused?: boolean cursor?: boolean inputPassthrough?: boolean + chatMode?: boolean } = {}, ): void { this.service.create({ id: this.viewId, url, ...options }) @@ -38,6 +39,10 @@ export class WebViewBridge< return this.service.exists(this.viewId) } + getCapabilities() { + return this.service.getCapabilities() + } + send(action: K, data: TSend[K]): void { this.service.send(this.viewId, action, data) } @@ -75,6 +80,9 @@ export class WebViewBridge< blur(): void { this.service.blur(this.viewId) } + markAsChat(): void { + this.service.markAsChat(this.viewId) + } setVisible(visible: boolean): void { visible ? this.service.show(this.viewId) : this.service.hide(this.viewId) } diff --git a/src/runtime/client/webview.service.ts b/src/runtime/client/webview.service.ts index adb26c7..068674d 100644 --- a/src/runtime/client/webview.service.ts +++ b/src/runtime/client/webview.service.ts @@ -19,6 +19,7 @@ const FALLBACK_CAPABILITIES: WebViewCapabilities = { supportsBidirectionalMessaging: true, supportsExecute: false, supportsHeadless: false, + supportsChatMode: false, } function createFallbackBridge(): IClientWebViewBridge { @@ -38,6 +39,7 @@ function createFallbackBridge(): IClientWebViewBridge { runtime.setWebViewInputPassthrough(options?.inputPassthrough ?? false) }, blur: () => runtime.setWebViewFocus(false, false), + markAsChat: () => {}, send: (viewId, event, payload) => { runtime.sendWebViewMessage( JSON.stringify({ __opencoreWebView: true, viewId, action: event, data: payload }), @@ -98,6 +100,9 @@ export class WebViewService { blur(viewId: string): void { this.bridge.blur(viewId) } + markAsChat(viewId: string): void { + this.bridge.markAsChat(viewId) + } send(viewId: string, event: string, payload: unknown): void { this.bridge.send(viewId, event, payload) } diff --git a/src/runtime/server/adapter/node-player-appearance-lifecycle-server.ts b/src/runtime/server/adapter/node-player-appearance-lifecycle-server.ts index 240977b..af2819e 100644 --- a/src/runtime/server/adapter/node-player-appearance-lifecycle-server.ts +++ b/src/runtime/server/adapter/node-player-appearance-lifecycle-server.ts @@ -5,6 +5,7 @@ import { IPlayerAppearanceLifecycleServer } from '../../../adapters/contracts/se import { IPlayerServer } from '../../../adapters/contracts/server/IPlayerServer' import { Players } from '../ports/players.api-port' import { PlayerAppearance } from '../../../kernel/shared' +import { SYSTEM_EVENTS } from '../../shared/types/system-types' @injectable() export class NodePlayerAppearanceLifecycleServer extends IPlayerAppearanceLifecycleServer { @@ -32,7 +33,7 @@ export class NodePlayerAppearanceLifecycleServer extends IPlayerAppearanceLifecy return { success: false, errors: ['Player not found'] } } - this.events.emit('opencore:appearance:apply', target, appearance) + this.events.emit(SYSTEM_EVENTS.appearance.apply, target, appearance) return { success: true, appearance } } @@ -52,7 +53,7 @@ export class NodePlayerAppearanceLifecycleServer extends IPlayerAppearanceLifecy this.pedAdapter.setDefaultComponentVariation(ped) const target = this.resolveTarget(playerSrc) if (!target) return false - this.events.emit('opencore:appearance:reset', target) + this.events.emit(SYSTEM_EVENTS.appearance.reset, target) return true } diff --git a/src/runtime/server/adapter/node-player-lifecycle-server.ts b/src/runtime/server/adapter/node-player-lifecycle-server.ts index 945e3a2..ee15d13 100644 --- a/src/runtime/server/adapter/node-player-lifecycle-server.ts +++ b/src/runtime/server/adapter/node-player-lifecycle-server.ts @@ -6,6 +6,7 @@ import type { SpawnPlayerRequest, TeleportPlayerRequest, } from '../../../adapters/contracts/server/player-lifecycle/types' +import { SYSTEM_EVENTS } from '../../shared/types/system-types' @injectable() export class NodePlayerLifecycleServer extends IPlayerLifecycleServer { @@ -17,7 +18,7 @@ export class NodePlayerLifecycleServer extends IPlayerLifecycleServer { const target = this.resolveTarget(playerSrc) if (!target) return - this.events.emit('opencore:spawner:spawn', target, { + this.events.emit(SYSTEM_EVENTS.spawner.spawn, target, { position: request.position, model: request.model, heading: request.heading, @@ -29,14 +30,14 @@ export class NodePlayerLifecycleServer extends IPlayerLifecycleServer { const target = this.resolveTarget(playerSrc) if (!target) return - this.events.emit('opencore:spawner:teleport', target, request.position, request.heading) + this.events.emit(SYSTEM_EVENTS.spawner.teleport, target, request.position, request.heading) } respawn(playerSrc: string, request: RespawnPlayerRequest): void { const target = this.resolveTarget(playerSrc) if (!target) return - this.events.emit('opencore:spawner:respawn', target, request.position, request.heading) + this.events.emit(SYSTEM_EVENTS.spawner.respawn, target, request.position, request.heading) } private resolveTarget(playerSrc: string) { diff --git a/src/runtime/server/adapter/node-vehicle-lifecycle-server.ts b/src/runtime/server/adapter/node-vehicle-lifecycle-server.ts index 8fe38fb..1f0c17e 100644 --- a/src/runtime/server/adapter/node-vehicle-lifecycle-server.ts +++ b/src/runtime/server/adapter/node-vehicle-lifecycle-server.ts @@ -8,6 +8,7 @@ import type { import { IPlatformContext } from '../../../adapters/contracts/IPlatformContext' import { IVehicleServer } from '../../../adapters/contracts/server/IVehicleServer' import { EventsAPI } from '../../../adapters/contracts/transport/events.api' +import { SYSTEM_EVENTS } from '../../shared/types/system-types' @injectable() export class NodeVehicleLifecycleServer extends IVehicleLifecycleServer { @@ -48,6 +49,6 @@ export class NodeVehicleLifecycleServer extends IVehicleLifecycleServer { warpPlayerIntoVehicle(request: WarpPlayerIntoVehicleRequest): void { const clientId = Number(request.playerSrc) if (Number.isNaN(clientId)) return - this.events.emit('opencore:vehicle:warpInto', clientId, request.networkId, request.seatIndex) + this.events.emit(SYSTEM_EVENTS.vehicle.warpInto, clientId, request.networkId, request.seatIndex) } } diff --git a/src/runtime/server/apis/chat.api.ts b/src/runtime/server/apis/chat.api.ts index f4e8f81..7016aa1 100644 --- a/src/runtime/server/apis/chat.api.ts +++ b/src/runtime/server/apis/chat.api.ts @@ -1,6 +1,7 @@ import { inject, injectable } from 'tsyringe' import { EventsAPI } from '../../../adapters/contracts/transport/events.api' import { RGB } from '../../../kernel/utils/rgb' +import { SYSTEM_EVENTS } from '../../shared/types/system-types' import { Player } from '../entities/player' import { Players } from '../ports/players.api-port' @@ -25,7 +26,7 @@ export class Chat { * @param color - Message color (RGB). Defaults to white. */ broadcast(message: string, author: string = 'SYSTEM', color: RGB = { r: 255, g: 255, b: 255 }) { - this.events.emit('core:chat:message', 'all', { + this.events.emit(SYSTEM_EVENTS.chat.message, 'all', { args: [author, message], color: color, }) @@ -45,7 +46,7 @@ export class Chat { author: string = 'Private', color: RGB = { r: 200, g: 200, b: 200 }, ) { - this.events.emit('core:chat:addMessage', player.clientID, { + this.events.emit(SYSTEM_EVENTS.chat.addMessage, player.clientID, { args: [author, message], color: color, }) @@ -57,7 +58,7 @@ export class Chat { * @param player - Target player. */ clearChat(player: Player) { - this.events.emit('core:chat:clear', player.clientID) + this.events.emit(SYSTEM_EVENTS.chat.clear, player.clientID) } /** @@ -75,7 +76,7 @@ export class Chat { color: RGB = { r: 255, g: 255, b: 255 }, ) { const targetIds = players.map((p) => (typeof p === 'number' ? p : p.clientID)) - this.events.emit('core:chat:addMessage', targetIds, { + this.events.emit(SYSTEM_EVENTS.chat.addMessage, targetIds, { args: [author, message], color: color, }) @@ -130,6 +131,6 @@ export class Chat { * Clear chat for all connected players. */ clearChatAll() { - this.events.emit('core:chat:clear', 'all') + this.events.emit(SYSTEM_EVENTS.chat.clear, 'all') } } diff --git a/src/runtime/server/apis/npcs.api.ts b/src/runtime/server/apis/npcs.api.ts index 45432a7..e265cbc 100644 --- a/src/runtime/server/apis/npcs.api.ts +++ b/src/runtime/server/apis/npcs.api.ts @@ -8,6 +8,7 @@ import { IPedServer } from '../../../adapters/contracts/server/IPedServer' import { coreLogger } from '../../../kernel/logger' import { Vector3 } from '../../../kernel/utils/vector3' import { WorldContext } from '../../core/world' +import { SYSTEM_EVENTS } from '../../shared/types/system-types' import { NPC, NpcAdapters, NpcSession } from '../entities/npc' import { NpcSpawnOptions, NpcSpawnResult, SerializedNpcData } from '../types/npc.types' @@ -334,7 +335,7 @@ export class Npcs { remainingNpcs: this.npcById.size, }) - this.events.emit('opencore:npc:deleted', 'all', npc.npcId) + this.events.emit(SYSTEM_EVENTS.npc.deleted, 'all', npc.npcId) return true } diff --git a/src/runtime/server/apis/parallel-compute.api.ts b/src/runtime/server/apis/parallel-compute.api.ts index b0cdd56..baccd3b 100644 --- a/src/runtime/server/apis/parallel-compute.api.ts +++ b/src/runtime/server/apis/parallel-compute.api.ts @@ -1,3 +1,4 @@ +import { performance } from 'node:perf_hooks' import { injectable } from 'tsyringe' import { v4 as uuid } from 'uuid' import { Vector3 } from '../../../kernel' diff --git a/src/runtime/server/apis/vehicle-modification.api.ts b/src/runtime/server/apis/vehicle-modification.api.ts index c276c46..d34c76d 100644 --- a/src/runtime/server/apis/vehicle-modification.api.ts +++ b/src/runtime/server/apis/vehicle-modification.api.ts @@ -1,7 +1,9 @@ -import { inject, injectable } from 'tsyringe' +import { inject } from 'tsyringe' import { EventsAPI } from '../../../adapters/contracts/transport/events.api' import { coreLogger } from '../../../kernel/logger' +import { SYSTEM_EVENTS } from '../../shared/types/system-types' import { VehicleModificationOptions, VehicleMods } from '../types/vehicle.types' +import { Bind } from '../decorators/bind' import { Vehicles } from './vehicles.api' /** @@ -16,7 +18,7 @@ import { Vehicles } from './vehicles.api' * - Modification limits * - Audit logging */ -@injectable() +@Bind('singleton') export class VehicleModification { constructor( @inject(Vehicles) private readonly vehicleService: Vehicles, @@ -70,7 +72,7 @@ export class VehicleModification { mods: Object.keys(validatedMods), }) - this.events.emit('opencore:vehicle:modified', 'all', { + this.events.emit(SYSTEM_EVENTS.vehicle.modified, 'all', { networkId, mods: validatedMods, }) @@ -217,7 +219,7 @@ export class VehicleModification { coreLogger.info('Vehicle modifications reset', { networkId, requestedBy }) - this.events.emit('opencore:vehicle:modified', 'all', { + this.events.emit(SYSTEM_EVENTS.vehicle.modified, 'all', { networkId, mods: defaultMods, }) diff --git a/src/runtime/server/apis/vehicles.api.ts b/src/runtime/server/apis/vehicles.api.ts index 4ec19fb..6e3f9f8 100644 --- a/src/runtime/server/apis/vehicles.api.ts +++ b/src/runtime/server/apis/vehicles.api.ts @@ -1,4 +1,4 @@ -import { inject, injectable } from 'tsyringe' +import { inject } from 'tsyringe' import { IHasher } from '../../../adapters/contracts/IHasher' import { EventsAPI } from '../../../adapters/contracts/transport/events.api' import { IEntityServer } from '../../../adapters/contracts/server/IEntityServer' @@ -6,6 +6,7 @@ import { IVehicleLifecycleServer } from '../../../adapters/contracts/server/vehi import { IVehicleServer } from '../../../adapters/contracts/server/IVehicleServer' import { coreLogger } from '../../../kernel/logger' import { Vector3 } from '../../../kernel/utils/vector3' +import { SYSTEM_EVENTS } from '../../shared/types/system-types' import { Player } from '../entities/player' import { Vehicle, type VehicleAdapters } from '../entities/vehicle' import { @@ -14,6 +15,7 @@ import { VehicleSpawnResult, } from '../types/vehicle.types' import { Players } from '../ports/players.api-port' +import { Bind } from '../decorators/bind' export interface VehicleCreateForPlayerOptions extends Omit { @@ -36,7 +38,7 @@ const DEFAULT_PLAYER_VEHICLE_OFFSET: Vector3 = { x: 0, y: 3, z: 0 } * All vehicle creation MUST go through this service to ensure security. * Uses CreateVehicleServerSetter for server-authoritative spawning. */ -@injectable() +@Bind('singleton') export class Vehicles { /** * Internal registry of all managed vehicles indexed by Network ID @@ -172,7 +174,7 @@ export class Vehicles { totalVehicles: this.vehiclesByNetworkId.size, }) - this.events.emit('opencore:vehicle:created', 'all', vehicle.serialize()) + this.events.emit(SYSTEM_EVENTS.vehicle.created, 'all', vehicle.serialize()) return { networkId, @@ -229,6 +231,7 @@ export class Vehicles { ...options, position: spawnPosition, heading: spawnHeading, + plate: ownership.type === 'player' ? player.name : 'anom', ownership, }) if (result.success) { @@ -358,7 +361,7 @@ export class Vehicles { totalVehicles: this.vehiclesByNetworkId.size, }) - this.events.emit('opencore:vehicle:deleted', 'all', networkId) + this.events.emit(SYSTEM_EVENTS.vehicle.deleted, 'all', networkId) return true } diff --git a/src/runtime/server/bootstrap.ts b/src/runtime/server/bootstrap.ts index 87d0319..8dcb809 100644 --- a/src/runtime/server/bootstrap.ts +++ b/src/runtime/server/bootstrap.ts @@ -19,6 +19,7 @@ import { BinaryProcessManager } from './system/managers/binary-process.manager' import { SessionRecoveryService } from './services/session-recovery.local' import type { PluginInstallContext, PluginRegistry } from './library/plugin' import { registerServicesServer } from './services/services.register' +import { SYSTEM_EVENTS } from '../shared/types/system-types' import { METADATA_KEYS } from './system/metadata-server.keys' import { registerSystemServer } from './system/processors.register' @@ -207,15 +208,15 @@ export async function initServer( const events = GLOBAL_CONTAINER.resolve(EventsAPI as any) as EventsAPI<'server'> // 1. Broadast to resources already running - engineEvents.emit('core:ready') - events.emit('core:ready', 'all') + engineEvents.emit(SYSTEM_EVENTS.core.ready) + events.emit(SYSTEM_EVENTS.core.ready, 'all') - // 2. Listen for 'core:request-ready' for resources starting late (hot-reload) - engineEvents.on('core:request-ready', () => { - engineEvents.emit('core:ready') + // 2. Listen for '_systemcore:request-ready' for resources starting late (hot-reload) + engineEvents.on(SYSTEM_EVENTS.core.requestReady, () => { + engineEvents.emit(SYSTEM_EVENTS.core.ready) }) - loggers.bootstrap.info(`'core:ready' logic initialized and broadcasted`) + loggers.bootstrap.info(`'${SYSTEM_EVENTS.core.ready}' logic initialized and broadcasted`) } const logLevelLabel = LogLevelLabels[getLogLevel()] @@ -241,13 +242,15 @@ function createCoreDependency(coreName: string): Promise { const onReady = () => { if (!resolved) { - loggers.bootstrap.debug(`Core '${coreName}' detected via 'core:ready' event!`) + loggers.bootstrap.debug( + `Core '${coreName}' detected via '${SYSTEM_EVENTS.core.ready}' event!`, + ) cleanup() resolve() } } - engineEvents.on('core:ready', onReady) - loggers.bootstrap.debug(`Listening for 'core:ready' event from Core`) + engineEvents.on(SYSTEM_EVENTS.core.ready, onReady) + loggers.bootstrap.debug(`Listening for '${SYSTEM_EVENTS.core.ready}' event from Core`) // 2. Check if already ready via export (Polling) const checkReady = () => { @@ -273,8 +276,10 @@ function createCoreDependency(coreName: string): Promise { // 3. Request status (for hot-reload cases where Core is already up) // This is sent AFTER registering the listener so we can receive the response if (!resolved) { - loggers.bootstrap.debug(`Requesting Core status via 'core:request-ready' event`) - engineEvents.emit('core:request-ready') + loggers.bootstrap.debug( + `Requesting Core status via '${SYSTEM_EVENTS.core.requestReady}' event`, + ) + engineEvents.emit(SYSTEM_EVENTS.core.requestReady) } // 4. Timeout protection @@ -286,7 +291,7 @@ function createCoreDependency(coreName: string): Promise { cleanup() reject( new Error( - `[OpenCore] Timeout waiting for CORE '${coreName}'. The Core did not emit 'core:ready' or expose 'isCoreReady' within ${CORE_WAIT_TIMEOUT}ms.`, + `[OpenCore] Timeout waiting for CORE '${coreName}'. The Core did not emit '${SYSTEM_EVENTS.core.ready}' or expose 'isCoreReady' within ${CORE_WAIT_TIMEOUT}ms.`, ), ) } diff --git a/src/runtime/server/controllers/command-export.controller.ts b/src/runtime/server/controllers/command-export.controller.ts index e206886..32f42a6 100644 --- a/src/runtime/server/controllers/command-export.controller.ts +++ b/src/runtime/server/controllers/command-export.controller.ts @@ -1,7 +1,9 @@ import { inject } from 'tsyringe' +import { z } from 'zod' import { IEngineEvents } from '../../../adapters/contracts/IEngineEvents' import { AppError, SecurityError } from '../../../kernel/error' import { loggers } from '../../../kernel/logger' +import { buildRemoteCommandExecuteEventName, SYSTEM_EVENTS } from '../../shared/types/system-types' import { CommandErrorObserverContract } from '../contracts/security/command-error-observer.contract' import { Controller, Export, Public } from '../decorators' import { OnNet } from '../decorators/onNet' @@ -76,7 +78,7 @@ export class CommandExportController implements InternalCommandsExports { * (registered by RESOURCE mode instances). */ @Public() - @OnNet('core:execute-command') + @OnNet(SYSTEM_EVENTS.command.execute, z.tuple([z.string().min(1), z.array(z.string())])) async onCommandReceived(player: Player, command: string, args: string[]) { try { if (command.startsWith('/')) command = command.slice(1) @@ -238,7 +240,7 @@ export class CommandExportController implements InternalCommandsExports { await this.validateSecurity(player, commandName, remoteEntry.metadata.security) // Delegate to resource via local event (server-to-server, not network) - const eventName = `opencore:command:execute:${remoteEntry.resourceName}` + const eventName = buildRemoteCommandExecuteEventName(remoteEntry.resourceName) this.engineEvents.emit(eventName, clientID, commandName, args) loggers.command.debug(`Delegated remote command execution to ${remoteEntry.resourceName}`, { command: commandName, diff --git a/src/runtime/server/controllers/remote-command-execution.controller.ts b/src/runtime/server/controllers/remote-command-execution.controller.ts index 305366a..013d21f 100644 --- a/src/runtime/server/controllers/remote-command-execution.controller.ts +++ b/src/runtime/server/controllers/remote-command-execution.controller.ts @@ -2,6 +2,7 @@ import { inject } from 'tsyringe' import { IEngineEvents } from '../../../adapters/contracts/IEngineEvents' import { IResourceInfo } from '../../../adapters/contracts/IResourceInfo' import { loggers } from '../../../kernel/logger' +import { buildRemoteCommandExecuteEventName } from '../../shared/types/system-types' import { CommandErrorObserverContract } from '../contracts/security/command-error-observer.contract' import { Controller } from '../decorators' import { normalizeToAppError } from '../helpers/normalize-app-error' @@ -61,7 +62,7 @@ export class RemoteCommandExecutionController { */ private registerEventHandler(): void { const resourceName = this.resourceInfo.getCurrentResourceName() - const eventName = `opencore:command:execute:${resourceName}` + const eventName = buildRemoteCommandExecuteEventName(resourceName) this.engineEvents.on( eventName, diff --git a/src/runtime/server/controllers/vehicle.controller.ts b/src/runtime/server/controllers/vehicle.controller.ts index 084ab69..2f8a36b 100644 --- a/src/runtime/server/controllers/vehicle.controller.ts +++ b/src/runtime/server/controllers/vehicle.controller.ts @@ -1,5 +1,6 @@ import { inject } from 'tsyringe' import { EventsAPI } from '../../../adapters/contracts/transport/events.api' +import { SYSTEM_EVENTS } from '../../shared/types/system-types' import { Controller, OnNet } from '../decorators' import { Player } from '../entities/player' import { Vehicles } from '../apis/vehicles.api' @@ -19,16 +20,16 @@ export class VehicleController { /** * Handles client request to get vehicle data. */ - @OnNet('opencore:vehicle:getData') + @OnNet(SYSTEM_EVENTS.vehicle.getData) handleGetData(player: Player, networkId: number) { const vehicle = this.vehicleService.getByNetworkId(networkId) if (!vehicle) { - this.events.emit('opencore:vehicle:dataResult', player.clientID, null) + this.events.emit(SYSTEM_EVENTS.vehicle.dataResult, player.clientID, null) return null } const data = vehicle.serialize() - this.events.emit('opencore:vehicle:dataResult', player.clientID, data) + this.events.emit(SYSTEM_EVENTS.vehicle.dataResult, player.clientID, data) return data } @@ -36,12 +37,12 @@ export class VehicleController { /** * Handles client request to get all player vehicles. */ - @OnNet('opencore:vehicle:getPlayerVehicles') + @OnNet(SYSTEM_EVENTS.vehicle.getPlayerVehicles) handleGetPlayerVehicles(player: Player) { const vehicles = this.vehicleService.getPlayerVehicles(player.clientID) const serialized = vehicles.map((v) => v.serialize()) - this.events.emit('opencore:vehicle:playerVehiclesResult', player.clientID, serialized) + this.events.emit(SYSTEM_EVENTS.vehicle.playerVehiclesResult, player.clientID, serialized) return serialized } diff --git a/src/runtime/server/decorators/command.ts b/src/runtime/server/decorators/command.ts index 67e899b..ffee82d 100644 --- a/src/runtime/server/decorators/command.ts +++ b/src/runtime/server/decorators/command.ts @@ -1,7 +1,11 @@ import { z } from 'zod' import { ClassConstructor } from '../../../kernel/di/class-constructor' import { Player } from '../entities/player' -import { getParameterNames, getSpreadParameterIndices } from '../helpers/function-helper' +import { + getDefaultParameterIndices, + getParameterNames, + getSpreadParameterIndices, +} from '../helpers/function-helper' import { METADATA_KEYS } from '../system/metadata-server.keys' import { SecurityMetadata } from '../types/core-exports.types' @@ -40,6 +44,8 @@ export interface CommandMetadata extends CommandConfig { security?: SecurityMetadata /** True if the last parameter uses the spread operator (...args) */ hasSpreadParam?: boolean + /** True when a parameter defines a JS default value. */ + defaultParams?: boolean[] } type ServerCommandHandler = (() => any) | ((player: Player, ...args: any[]) => any) @@ -150,6 +156,7 @@ export function Command(configOrName: string | CommandConfig, schema?: z.ZodType } const paramNames = getParameterNames(descriptor.value) + const defaultParams = getDefaultParameterIndices(descriptor.value) const spreadIndices = getSpreadParameterIndices(descriptor.value) const hasSpreadParam = spreadIndices.length > 0 && spreadIndices[spreadIndices.length - 1] @@ -161,6 +168,7 @@ export function Command(configOrName: string | CommandConfig, schema?: z.ZodType paramNames, expectsPlayer, hasSpreadParam, + defaultParams, } Reflect.defineMetadata(METADATA_KEYS.COMMAND, metadata, target, propertyKey) diff --git a/src/runtime/server/entities/npc.ts b/src/runtime/server/entities/npc.ts index 99ade35..f9586f7 100644 --- a/src/runtime/server/entities/npc.ts +++ b/src/runtime/server/entities/npc.ts @@ -1,4 +1,3 @@ -import { NativeHandle } from 'src/runtime/core/nativehandle' import { IEntityServer } from '../../../adapters/contracts/server/IEntityServer' import { INpcLifecycleServer } from '../../../adapters/contracts/server/npc-lifecycle/INpcLifecycleServer' import { IPedServer } from '../../../adapters/contracts/server/IPedServer' @@ -6,6 +5,7 @@ import { Vector3 } from '../../../kernel/utils/vector3' import { BaseEntity } from '../../core/entity' import { Spatial } from '../../core/spatial' import { SerializedNpcData } from '../types/npc.types' +import { NativeHandle } from '../../core' export interface NpcAdapters { entityServer: IEntityServer diff --git a/src/runtime/server/entities/player.ts b/src/runtime/server/entities/player.ts index 99b30fb..d1e4a2e 100644 --- a/src/runtime/server/entities/player.ts +++ b/src/runtime/server/entities/player.ts @@ -14,10 +14,11 @@ import { loggers } from '../../../kernel/logger' import { Vector3 } from '../../../kernel/utils/vector3' import { BaseEntity } from '../../core/entity' import { Spatial } from '../../core/spatial' +import { SYSTEM_EVENTS } from '../../shared/types/system-types' import { LinkedID } from '../types/linked-id' import { PlayerSession } from '../types/player-session.types' import { SerializedPlayerData } from '../types/core-exports.types' -import { NativeHandle } from 'src/runtime/core/nativehandle' +import { NativeHandle } from '../../core' /** * Adapter bundle for player operations. @@ -64,10 +65,14 @@ export class Player extends BaseEntity implements Spatial, NativeHandle { } getHeading(): number { - return this.adapters.entityServer.getHeading(this.clientID) + const ped = this.adapters.playerServer.getPed(this.clientID.toString()) + if (!ped || ped === 0) return 0 + return this.adapters.entityServer.getHeading(ped) } setHeading(heading: number): void { - this.adapters.entityServer.setHeading(this.clientID, heading) + const ped = this.adapters.playerServer.getPed(this.clientID.toString()) + if (!ped || ped === 0) return + this.adapters.entityServer.setHeading(ped, heading) } getHandle(): number { @@ -168,7 +173,7 @@ export class Player extends BaseEntity implements Spatial, NativeHandle { * @param type - Message type for styling */ send(message: string, type: 'chat' | 'error' | 'success' | 'warning' = 'chat'): void { - this.emit('core:chat:send', message, type) + this.emit(SYSTEM_EVENTS.chat.send, message, type) } // ───────────────────────────────────────────────────────────────── diff --git a/src/runtime/server/helpers/command-validation.helper.ts b/src/runtime/server/helpers/command-validation.helper.ts index ad8976d..b179dd5 100644 --- a/src/runtime/server/helpers/command-validation.helper.ts +++ b/src/runtime/server/helpers/command-validation.helper.ts @@ -12,19 +12,23 @@ export async function validateAndExecuteCommand( handler: (...args: any[]) => any, ): Promise { const paramNames = meta.expectsPlayer ? meta.paramNames.slice(1) : meta.paramNames + const defaultParams = meta.expectsPlayer + ? (meta.defaultParams ?? []).slice(1) + : (meta.defaultParams ?? []) let schema: z.ZodTypeAny | undefined = meta.schema if (!meta.expectsPlayer) { if (args.length > 0) { - throw new AppError('GAME:BAD_REQUEST', `Incorrect usage, use: ${meta.usage}`, 'client', { - usage: meta.usage, + const usage = resolveCommandUsage(meta, paramNames, defaultParams) + throw new AppError('GAME:BAD_REQUEST', `Incorrect usage, use: ${usage}`, 'client', { + usage, }) } return await handler() } if (!schema) { - schema = generateSchemaFromTypes(meta.paramTypes) + schema = generateSchemaFromTypes(meta.paramTypes, meta.defaultParams) if (!schema) { if (paramNames.length > 0) { @@ -67,8 +71,9 @@ export async function validateAndExecuteCommand( } const validated = await schema.parseAsync(inputObj).catch(() => { - throw new AppError('GAME:BAD_REQUEST', `Incorrect usage, use: ${meta.usage}`, 'client', { - usage: meta.usage, + const usage = resolveCommandUsage(meta, paramNames, defaultParams) + throw new AppError('GAME:BAD_REQUEST', `Incorrect usage, use: ${usage}`, 'client', { + usage, }) }) @@ -82,8 +87,9 @@ export async function validateAndExecuteCommand( const processedArgs = processTupleSchema(schema, args) const validated = await schema.parseAsync(processedArgs).catch(() => { - throw new AppError('GAME:BAD_REQUEST', `Incorrect usage, use: ${meta.usage}`, 'client', { - usage: meta.usage, + const usage = resolveCommandUsage(meta, paramNames, defaultParams) + throw new AppError('GAME:BAD_REQUEST', `Incorrect usage, use: ${usage}`, 'client', { + usage, }) }) @@ -108,3 +114,19 @@ export async function validateAndExecuteCommand( // fallback return await handler(player) } + +function resolveCommandUsage( + meta: CommandMetadata, + paramNames: string[], + defaultParams: boolean[], +): string { + if (meta.usage?.trim()) { + return meta.usage + } + + const renderedParams = paramNames.map((name, index) => + defaultParams[index] ? `[${name}]` : `<${name}>`, + ) + + return `/${meta.command}${renderedParams.length > 0 ? ` ${renderedParams.join(' ')}` : ''}` +} diff --git a/src/runtime/server/helpers/function-helper.ts b/src/runtime/server/helpers/function-helper.ts index a5bfb6e..9deb13a 100644 --- a/src/runtime/server/helpers/function-helper.ts +++ b/src/runtime/server/helpers/function-helper.ts @@ -1,16 +1,11 @@ export function getParameterNames(func: (...args: any[]) => any): string[] { - const stripped = func - .toString() - .replace(/\/\/.*$/gm, '') - .replace(/\/\*[\s\S]*?\*\//gm, '') - - const args = stripped - .slice(stripped.indexOf('(') + 1, stripped.indexOf(')')) - .split(',') + return parseParameterTokens(func) .map((arg) => arg.replace(/=[\s\S]*/, '').trim()) .filter(Boolean) +} - return args +export function getDefaultParameterIndices(func: (...args: any[]) => any): boolean[] { + return parseParameterTokens(func).map((arg) => arg.includes('=')) } /** @@ -31,3 +26,16 @@ export function getSpreadParameterIndices(func: (...args: any[]) => any): boolea return args.map((arg) => arg.startsWith('...')) } + +function parseParameterTokens(func: (...args: any[]) => any): string[] { + const stripped = func + .toString() + .replace(/\/\/.*$/gm, '') + .replace(/\/\*[\s\S]*?\*\//gm, '') + + return stripped + .slice(stripped.indexOf('(') + 1, stripped.indexOf(')')) + .split(',') + .map((arg) => arg.trim()) + .filter(Boolean) +} diff --git a/src/runtime/server/implementations/local/channel.local.ts b/src/runtime/server/implementations/local/channel.local.ts index ce2b8a8..7e2ae8f 100644 --- a/src/runtime/server/implementations/local/channel.local.ts +++ b/src/runtime/server/implementations/local/channel.local.ts @@ -1,6 +1,7 @@ import { EventsAPI } from '../../../../adapters/contracts/transport/events.api' -import { RGB } from 'src/kernel' +import { RGB } from '../../../../kernel/utils' import { inject, injectable } from 'tsyringe' +import { SYSTEM_EVENTS } from '../../../shared/types/system-types' import { Player } from '../../entities' import { Channels } from '../../ports/channel.api-port' import { IChannelValidator, ChannelMetadata, ChannelType } from '../../types' @@ -117,7 +118,7 @@ export class LocalChannelImplementation extends Channels { return } - this.events.emit('core:chat:addMessage', targetIds, { + this.events.emit(SYSTEM_EVENTS.chat.addMessage, targetIds, { args: [author ?? sender.name, message], color: color, }) @@ -141,7 +142,7 @@ export class LocalChannelImplementation extends Channels { return } - this.events.emit('core:chat:addMessage', targetIds, { + this.events.emit(SYSTEM_EVENTS.chat.addMessage, targetIds, { args: [author, message], color: color, }) diff --git a/src/runtime/server/ports/channel.api-port.ts b/src/runtime/server/ports/channel.api-port.ts index 8e9c3a4..1abdf40 100644 --- a/src/runtime/server/ports/channel.api-port.ts +++ b/src/runtime/server/ports/channel.api-port.ts @@ -1,4 +1,4 @@ -import { RGB } from 'src/kernel' +import { RGB } from '../../../kernel/utils' import { Channel } from '../concepts/channel' import { Player } from '../entities' import { ChannelMetadata, ChannelType, IChannelValidator } from '../types' diff --git a/src/runtime/server/services/parallel/worker-pool.ts b/src/runtime/server/services/parallel/worker-pool.ts index 839b8a1..e6e7fde 100644 --- a/src/runtime/server/services/parallel/worker-pool.ts +++ b/src/runtime/server/services/parallel/worker-pool.ts @@ -6,7 +6,6 @@ * structured-cloned messages. */ -import * as path from 'node:path' import { Worker } from 'node:worker_threads' import { WorkerInfo, @@ -86,7 +85,7 @@ class NativeWorker { constructor( id: number, - workerScriptPath: string, + workerSource: string, callbacks: { onExit: (workerId: number, code: number | null) => void onError: (workerId: number, error: Error) => void @@ -94,7 +93,7 @@ class NativeWorker { ) { this.id = id - this.worker = new Worker(workerScriptPath) + this.worker = new Worker(workerSource, { eval: true }) this.worker.on('message', (response: WorkerResponse) => { const handlers = this.pendingResponses.get(response.id) if (handlers) { @@ -196,13 +195,46 @@ export class WorkerPool extends SimpleEventEmitter { private workerIdCounter = 0 private cleanupInterval: ReturnType | null = null private isShuttingDown = false - private workerScriptPath: string + private workerSource: string constructor(config: Partial = {}) { super() this.config = { ...DEFAULT_CONFIG, ...config } - this.workerScriptPath = path.join(__dirname, 'native-worker.entry.js') + this.workerSource = ` +const { parentPort } = require('worker_threads') +const { performance } = require('perf_hooks') + +if (!parentPort) { + throw new Error('native worker must be executed inside a Worker thread') +} + +function executeCompute(functionBody, input) { + const fn = new Function('input', 'return (' + functionBody + ')(input)') + return fn(input) +} + +parentPort.on('message', (message) => { + const startTime = performance.now() + + try { + const result = executeCompute(message.functionBody, message.input) + parentPort.postMessage({ + id: message.id, + success: true, + result, + executionTime: performance.now() - startTime, + }) + } catch (error) { + parentPort.postMessage({ + id: message.id, + success: false, + error: error instanceof Error ? error.message : String(error), + executionTime: performance.now() - startTime, + }) + } +}) +` // Start cleanup interval this.cleanupInterval = setInterval(() => this.cleanupIdleWorkers(), 10000) @@ -327,7 +359,7 @@ export class WorkerPool extends SimpleEventEmitter { private spawnWorker(): NativeWorker | null { try { const id = this.workerIdCounter++ - const worker = new NativeWorker(id, this.workerScriptPath, { + const worker = new NativeWorker(id, this.workerSource, { onExit: (workerId, code) => { const existing = this.workers.get(workerId) if (!existing) return diff --git a/src/runtime/server/services/parallel/worker.ts b/src/runtime/server/services/parallel/worker.ts index 302d60b..ccfa78f 100644 --- a/src/runtime/server/services/parallel/worker.ts +++ b/src/runtime/server/services/parallel/worker.ts @@ -12,6 +12,7 @@ * - Do not pass untrusted code as compute functions. */ +import { performance } from 'node:perf_hooks' import { WorkerMessage, WorkerResponse } from '../../types/parallel.types' /** diff --git a/src/runtime/server/system/schema-generator.ts b/src/runtime/server/system/schema-generator.ts index 8c19e48..8a0cff8 100644 --- a/src/runtime/server/system/schema-generator.ts +++ b/src/runtime/server/system/schema-generator.ts @@ -21,7 +21,14 @@ function typeToZodSchema(type: any): z.ZodType | undefined { } } -export function generateSchemaFromTypes(paramTypes: any[]): z.ZodTuple | undefined { +function applyOptional(schema: z.ZodTypeAny, optional: boolean): z.ZodTypeAny { + return optional ? schema.optional() : schema +} + +export function generateSchemaFromTypes( + paramTypes: any[], + defaultParams: boolean[] = [], +): z.ZodTuple | undefined { if (!paramTypes || paramTypes.length === 0) return z.tuple([]) if (paramTypes[0] !== Player) { throw new AppError( @@ -33,10 +40,10 @@ export function generateSchemaFromTypes(paramTypes: any[]): z.ZodTuple | undefin if (paramTypes.length === 1) return z.tuple([]) const argSchemas: z.ZodTypeAny[] = [] - for (const t of paramTypes.slice(1)) { + for (const [index, t] of paramTypes.slice(1).entries()) { const s = typeToZodSchema(t) if (!s) return undefined - argSchemas.push(s) + argSchemas.push(applyOptional(s, defaultParams[index + 1] ?? false)) } return z.tuple(argSchemas as any) diff --git a/src/runtime/shared/helpers/process-tuple-schema.ts b/src/runtime/shared/helpers/process-tuple-schema.ts index d55f230..ec4c569 100644 --- a/src/runtime/shared/helpers/process-tuple-schema.ts +++ b/src/runtime/shared/helpers/process-tuple-schema.ts @@ -32,5 +32,9 @@ export function processTupleSchema(schema: z.ZodTuple, args: unknown[]): unknown } } + if (args.length < items.length) { + return [...args, ...Array.from({ length: items.length - args.length }, () => undefined)] + } + return args } diff --git a/src/runtime/shared/types/system-types.ts b/src/runtime/shared/types/system-types.ts new file mode 100644 index 0000000..014af42 --- /dev/null +++ b/src/runtime/shared/types/system-types.ts @@ -0,0 +1,69 @@ +type ValueOf = T[keyof T] + +const SYSTEM_EVENT_NAMESPACE = 'opencore' +const SYSTEM_CORE_EVENT_NAMESPACE = '_systemcore' + +const systemEvent = (scope: string, action: string) => + `${SYSTEM_EVENT_NAMESPACE}:${scope}:${action}` as const + +const systemCoreEvent = (action: string) => `${SYSTEM_CORE_EVENT_NAMESPACE}:${action}` as const + +export type RemoteCommandExecuteEventName = + `${typeof SYSTEM_EVENT_NAMESPACE}:command:execute:${string}` + +export const buildRemoteCommandExecuteEventName = ( + resourceName: string, +): RemoteCommandExecuteEventName => + `${SYSTEM_EVENTS.command.execute}:${resourceName}` as RemoteCommandExecuteEventName + +export const SYSTEM_EVENTS = { + core: { + ready: systemCoreEvent('ready'), + requestReady: systemCoreEvent('request-ready'), + }, + chat: { + message: systemEvent('chat', 'message'), + addMessage: systemEvent('chat', 'addMessage'), + send: systemEvent('chat', 'send'), + clear: systemEvent('chat', 'clear'), + }, + command: { + execute: systemEvent('command', 'execute'), + }, + spawner: { + spawn: systemEvent('spawner', 'spawn'), + teleport: systemEvent('spawner', 'teleport'), + respawn: systemEvent('spawner', 'respawn'), + }, + appearance: { + apply: systemEvent('appearance', 'apply'), + reset: systemEvent('appearance', 'reset'), + }, + vehicle: { + create: systemEvent('vehicle', 'create'), + createResult: systemEvent('vehicle', 'createResult'), + delete: systemEvent('vehicle', 'delete'), + deleteResult: systemEvent('vehicle', 'deleteResult'), + repair: systemEvent('vehicle', 'repair'), + repairResult: systemEvent('vehicle', 'repairResult'), + repaired: systemEvent('vehicle', 'repaired'), + setLocked: systemEvent('vehicle', 'setLocked'), + getData: systemEvent('vehicle', 'getData'), + dataResult: systemEvent('vehicle', 'dataResult'), + getPlayerVehicles: systemEvent('vehicle', 'getPlayerVehicles'), + playerVehiclesResult: systemEvent('vehicle', 'playerVehiclesResult'), + created: systemEvent('vehicle', 'created'), + deleted: systemEvent('vehicle', 'deleted'), + modified: systemEvent('vehicle', 'modified'), + warpInto: systemEvent('vehicle', 'warpInto'), + }, + npc: { + deleted: systemEvent('npc', 'deleted'), + }, + session: { + playerInit: systemEvent('player', 'sessionInit'), + teleportTo: systemEvent('player', 'teleportTo'), + }, +} as const + +export type SystemEventName = ValueOf> diff --git a/tests/integration/server/command-ports.test.ts b/tests/integration/server/command-ports.test.ts index 5be9fe6..e8ac5a9 100644 --- a/tests/integration/server/command-ports.test.ts +++ b/tests/integration/server/command-ports.test.ts @@ -1,6 +1,7 @@ import 'reflect-metadata' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { IEngineEvents } from '../../../src/adapters/contracts/IEngineEvents' +import { buildRemoteCommandExecuteEventName } from '../../../src/runtime/shared/types/system-types' import type { CommandErrorObserverContract } from '../../../src/runtime/server/contracts/security/command-error-observer.contract' import { CommandExportController } from '../../../src/runtime/server/controllers/command-export.controller' import type { CommandMetadata } from '../../../src/runtime/server/decorators/command' @@ -8,7 +9,7 @@ import { Player } from '../../../src/runtime/server/entities/player' import { LocalCommandImplementation } from '../../../src/runtime/server/implementations/local/command.local' import type { Players } from '../../../src/runtime/server/ports/players.api-port' import { createMockPlayerAdapters } from '../../helpers' -import { CommandExecutionPort } from 'src/runtime/server/ports/internal/command-execution.port' +import { CommandExecutionPort } from '../../../src/runtime/server/services' // Mock getRuntimeContext vi.mock('../../../src/runtime/server/runtime', () => ({ @@ -103,7 +104,7 @@ describe('Command Ports Integration', () => { await exportController.executeCommand(1, 'remote-heal', ['arg']) expect(mockEngineEvents.emit).toHaveBeenCalledWith( - 'opencore:command:execute:medical-resource', + buildRemoteCommandExecuteEventName('medical-resource'), 1, 'remote-heal', ['arg'], @@ -157,7 +158,7 @@ describe('Command Ports Integration', () => { // Execute police command await exportController.executeCommand(1, 'police-arrest', []) expect(mockEngineEvents.emit).toHaveBeenCalledWith( - 'opencore:command:execute:police-resource', + buildRemoteCommandExecuteEventName('police-resource'), expect.any(Number), 'police-arrest', expect.any(Array), @@ -168,7 +169,7 @@ describe('Command Ports Integration', () => { // Execute medical command await exportController.executeCommand(1, 'medical-revive', []) expect(mockEngineEvents.emit).toHaveBeenCalledWith( - 'opencore:command:execute:medical-resource', + buildRemoteCommandExecuteEventName('medical-resource'), expect.any(Number), 'medical-revive', expect.any(Array), @@ -234,7 +235,7 @@ describe('Command Ports Integration', () => { // Verify CORE emitted local event to resource (not network event) expect(mockEngineEvents.emit).toHaveBeenCalledWith( - 'opencore:command:execute:test-resource', + buildRemoteCommandExecuteEventName('test-resource'), 1, 'resource-cmd', ['arg1'], @@ -444,7 +445,7 @@ describe('Command Ports Integration', () => { // Should delegate to resource via local event expect(mockEngineEvents.emit).toHaveBeenCalledWith( - 'opencore:command:execute:police-resource', + buildRemoteCommandExecuteEventName('police-resource'), 1, 'arrest', ['player123'], diff --git a/tests/setup.ts b/tests/setup.ts index aa4332c..c1d0bae 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -3,10 +3,8 @@ import { beforeEach, vi } from 'vitest' import { resetContainer } from './helpers/di.helper' import { installGlobalMocks, resetCitizenFxMocks } from './mocks/citizenfx' -// Install FiveM API mocks globally installGlobalMocks() -// Reset state before each test beforeEach(() => { resetCitizenFxMocks() resetContainer() diff --git a/tests/unit/runtime/client/adapter-bootstrap.test.ts b/tests/unit/runtime/client/adapter-bootstrap.test.ts index 677c2ff..5e2ce4e 100644 --- a/tests/unit/runtime/client/adapter-bootstrap.test.ts +++ b/tests/unit/runtime/client/adapter-bootstrap.test.ts @@ -60,6 +60,18 @@ class CustomRuntimeBridge extends IClientRuntimeBridge { } class NoopLocalPlayerBridge extends IClientLocalPlayerBridge { + getHandle(): number { + return 0 + } + + getPosition(): Vector3 { + return { x: 0, y: 0, z: 0 } + } + + getHeading(): number { + return 0 + } + setPosition(_position: Vector3, _heading?: number): void {} } diff --git a/tests/unit/server/adapter/node-player-state-sync-server.test.ts b/tests/unit/server/adapter/node-player-state-sync-server.test.ts new file mode 100644 index 0000000..a809bc7 --- /dev/null +++ b/tests/unit/server/adapter/node-player-state-sync-server.test.ts @@ -0,0 +1,33 @@ +import 'reflect-metadata' +import { describe, expect, it, vi } from 'vitest' +import { NodePlayerStateSyncServer } from '../../../../src/runtime/server/adapter/node-player-state-sync-server' + +describe('NodePlayerStateSyncServer', () => { + it('writes health and armor to the entity state bag', () => { + const state = new Map() + const entityServer = { + getHealth: vi.fn(() => 200), + setHealth: vi.fn(), + getArmor: vi.fn(() => 0), + setArmor: vi.fn(), + getStateBag: vi.fn(() => ({ + set: (key: string, value: unknown) => state.set(key, value), + get: (key: string) => state.get(key), + })), + } + + const playerServer = { + getPed: vi.fn(() => 77), + } + + const service = new NodePlayerStateSyncServer(playerServer as any, entityServer as any) + + service.setHealth('1', 175) + service.setArmor('1', 80) + + expect(entityServer.setHealth).toHaveBeenCalledWith(77, 175) + expect(entityServer.setArmor).toHaveBeenCalledWith(77, 80) + expect(state.get('health')).toBe(175) + expect(state.get('armor')).toBe(80) + }) +}) diff --git a/tests/unit/server/apis/parallel-compute.api.test.ts b/tests/unit/server/apis/parallel-compute.api.test.ts new file mode 100644 index 0000000..83a5015 --- /dev/null +++ b/tests/unit/server/apis/parallel-compute.api.test.ts @@ -0,0 +1,172 @@ +import 'reflect-metadata' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { + ParallelCompute, + defineBatchFilter, + defineBatchReduce, + defineBatchTransform, + defineTask, + getParallelComputeService, + shutdownParallelCompute, +} from '../../../../src/runtime/server/apis/parallel-compute.api' + +describe('ParallelCompute', () => { + afterEach(async () => { + await shutdownParallelCompute() + }) + + it('runs synchronously below the worker threshold', async () => { + const service = new ParallelCompute() + + const result = await service.run( + { + name: 'sum-sync', + estimateCost: (input: number[]) => input.length, + workerThreshold: 10, + compute: (input: number[]) => input.reduce((sum, value) => sum + value, 0), + }, + [1, 2, 3], + ) + + expect(result.result).toBe(6) + expect(result.mode).toBe('sync') + expect(service.getMetrics()).toMatchObject({ + totalTasks: 1, + syncTasks: 1, + parallelTasks: 0, + failedTasks: 0, + }) + }) + + it('runs in parallel when a worker pool is available', async () => { + const service = new ParallelCompute() + const execute = vi.fn(async (message: { input: number[] }) => { + ;(service as any).updateMetrics(4, 'parallel') + return message.input.reduce((sum, value) => sum + value, 0) + }) + + ;(service as any).pool = { + execute, + isNative: true, + getStats: () => ({ totalWorkers: 2 }), + shutdown: vi.fn(async () => undefined), + } + ;(service as any).isInitialized = true + + const result = await service.run( + { + name: 'sum-parallel', + estimateCost: (input: number[]) => input.length, + workerThreshold: 2, + compute: (input: number[]) => input.reduce((sum, value) => sum + value, 0), + }, + [1, 2, 3, 4], + ) + + expect(result.result).toBe(10) + expect(result.mode).toBe('parallel') + expect(execute).toHaveBeenCalledTimes(1) + }) + + it('supports distributed execution with chunking and merging', async () => { + const service = new ParallelCompute() + const execute = vi.fn(async (message: { input: number[] }) => + message.input.map((value) => value * 2), + ) + + ;(service as any).pool = { + execute, + isNative: true, + getStats: () => ({ totalWorkers: 2 }), + shutdown: vi.fn(async () => undefined), + } + ;(service as any).isInitialized = true + + const result = await service.distributed( + { + name: 'distributed-double', + compute: (input: number[]) => input.map((value) => value * 2), + chunker: (input: number[], workerCount: number) => { + const chunkSize = Math.ceil(input.length / workerCount) + const chunks: number[][] = [] + for (let index = 0; index < input.length; index += chunkSize) { + chunks.push(input.slice(index, index + chunkSize)) + } + return chunks + }, + merger: (results: number[][]) => results.flat(), + }, + [1, 2, 3, 4], + 2, + ) + + expect(result.result).toEqual([2, 4, 6, 8]) + expect(result.mode).toBe('distributed') + expect(result.workerCount).toBe(2) + expect(execute).toHaveBeenCalledTimes(2) + expect(service.getMetrics().parallelTasks).toBe(1) + }) + + it('falls back to sync execution when the pool is unavailable', async () => { + const service = new ParallelCompute() + + const result = await service.parallel( + { + name: 'fallback', + compute: (input: number) => input * 3, + }, + 7, + ) + + expect(result).toMatchObject({ result: 21, mode: 'sync' }) + }) + + it('tracks failed tasks and can reset metrics', () => { + const service = new ParallelCompute() + + expect(() => + service.sync( + { + name: 'boom', + compute: () => { + throw new Error('boom') + }, + }, + undefined, + ), + ).toThrow('boom') + + expect(service.getMetrics().failedTasks).toBe(1) + service.resetMetrics() + expect(service.getMetrics()).toMatchObject({ + totalTasks: 0, + syncTasks: 0, + parallelTasks: 0, + failedTasks: 0, + }) + }) + + it('exposes the global task helpers', async () => { + const globalService = getParallelComputeService() + + const batchTransform = defineBatchTransform('square', (value: number) => value * value) + const batchFilter = defineBatchFilter('even', (value: number) => value % 2 === 0) + const batchReduce = defineBatchReduce( + 'sum', + (sum: number, value: number) => sum + value, + 0, + (results: number[]) => results.reduce((sum, value) => sum + value, 0), + ) + const task = defineTask({ + name: 'identity', + compute: (value: number) => value, + estimateCost: () => 0, + }) + + expect(batchTransform.sync([2, 3])).toEqual([4, 9]) + expect(batchFilter.sync([1, 2, 3, 4])).toEqual([2, 4]) + expect(batchReduce.sync([1, 2, 3])).toBe(6) + await expect(task.run(9)).resolves.toBe(9) + expect(globalService.initialized).toBe(false) + }) +}) diff --git a/tests/unit/server/apis/vehicle-modification.api.test.ts b/tests/unit/server/apis/vehicle-modification.api.test.ts new file mode 100644 index 0000000..2c564d8 --- /dev/null +++ b/tests/unit/server/apis/vehicle-modification.api.test.ts @@ -0,0 +1,96 @@ +import 'reflect-metadata' +import { describe, expect, it, vi } from 'vitest' +import { SYSTEM_EVENTS } from '../../../../src/runtime/shared/types/system-types' +import { VehicleModification } from '../../../../src/runtime/server/apis/vehicle-modification.api' + +describe('VehicleModification', () => { + function createService(options: { exists?: boolean; owns?: boolean; near?: boolean } = {}) { + const vehicle = { + exists: options.exists ?? true, + ownership: { clientID: 7 }, + mods: {}, + } + + const vehicles = { + getByNetworkId: vi.fn(() => vehicle), + validateOwnership: vi.fn(() => options.owns ?? true), + validateProximity: vi.fn(() => options.near ?? true), + } + + const events = { + emit: vi.fn(), + } + + return { + service: new VehicleModification(vehicles as any, events as any), + vehicles, + events, + } + } + + it('applies validated modifications and emits the clamped payload', () => { + const { service, events } = createService() + + const success = service.applyModifications({ + networkId: 10, + requestedBy: 7, + mods: { + spoiler: 999, + windowTint: 20, + primaryColor: 500, + extras: { 1: true, 21: true }, + }, + }) + + expect(success).toBe(true) + expect(events.emit).toHaveBeenCalledWith(SYSTEM_EVENTS.vehicle.modified, 'all', { + networkId: 10, + mods: { + spoiler: 50, + windowTint: 6, + primaryColor: 160, + extras: { 1: true }, + }, + }) + }) + + it('blocks unauthorized modification attempts', () => { + const { service, events, vehicles } = createService({ owns: false }) + + const success = service.setTurbo(10, true, 99) + + expect(success).toBe(false) + expect(vehicles.validateOwnership).toHaveBeenCalledWith(10, 99) + expect(events.emit).not.toHaveBeenCalled() + }) + + it('blocks modification attempts when the player is too far away', () => { + const { service, events, vehicles } = createService({ near: false }) + + const success = service.setColors(10, 1, 2, 7) + + expect(success).toBe(false) + expect(vehicles.validateProximity).toHaveBeenCalledWith(10, 7, 15) + expect(events.emit).not.toHaveBeenCalled() + }) + + it('resets modifications to framework defaults', () => { + const { service, events } = createService() + + const success = service.resetModifications(15, 7) + + expect(success).toBe(true) + expect(events.emit).toHaveBeenCalledWith( + SYSTEM_EVENTS.vehicle.modified, + 'all', + expect.objectContaining({ + networkId: 15, + mods: expect.objectContaining({ + spoiler: -1, + turbo: false, + windowTint: 0, + }), + }), + ) + }) +}) diff --git a/tests/unit/server/controllers/command-export.controller.test.ts b/tests/unit/server/controllers/command-export.controller.test.ts index 9efced0..cecc1a1 100644 --- a/tests/unit/server/controllers/command-export.controller.test.ts +++ b/tests/unit/server/controllers/command-export.controller.test.ts @@ -1,12 +1,13 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { IEngineEvents } from '../../../../src/adapters/contracts/IEngineEvents' import { AppError } from '../../../../src/kernel/error' +import { buildRemoteCommandExecuteEventName } from '../../../../src/runtime/shared/types/system-types' import type { CommandErrorObserverContract } from '../../../../src/runtime/server/contracts/security/command-error-observer.contract' import { CommandExportController } from '../../../../src/runtime/server/controllers/command-export.controller' import type { Players } from '../../../../src/runtime/server/ports/players.api-port' import type { CommandRegistrationDto } from '../../../../src/runtime/server/types/core-exports.types' import { createAuthenticatedPlayer, createTestPlayer } from '../../../helpers' -import { CommandExecutionPort } from 'src/runtime/server/services' +import { CommandExecutionPort } from '../../../../src/runtime/server/services' describe('CommandExportController', () => { let controller: CommandExportController @@ -166,7 +167,7 @@ describe('CommandExportController', () => { // Should emit event to resource via adapter expect(mockEngineEvents.emit).toHaveBeenCalledWith( - 'opencore:command:execute:my-resource', + buildRemoteCommandExecuteEventName('my-resource'), 1, 'remotecmd', ['arg1', 'arg2'], @@ -192,7 +193,7 @@ describe('CommandExportController', () => { await controller.executeCommand(1, 'remote', []) expect(mockEngineEvents.emit).toHaveBeenCalledWith( - 'opencore:command:execute:my-resource', + buildRemoteCommandExecuteEventName('my-resource'), 1, 'remote', [], @@ -241,7 +242,7 @@ describe('CommandExportController', () => { await controller.executeCommand(1, 'noargs', []) expect(mockEngineEvents.emit).toHaveBeenCalledWith( - 'opencore:command:execute:test-resource', + buildRemoteCommandExecuteEventName('test-resource'), 1, 'noargs', [], @@ -369,7 +370,7 @@ describe('CommandExportController', () => { await controller.executeCommand(1, 'cmd1', []) expect(mockEngineEvents.emit).toHaveBeenCalledWith( - 'opencore:command:execute:resource-a', + buildRemoteCommandExecuteEventName('resource-a'), expect.any(Number), 'cmd1', expect.any(Array), @@ -379,7 +380,7 @@ describe('CommandExportController', () => { await controller.executeCommand(1, 'cmd2', []) expect(mockEngineEvents.emit).toHaveBeenCalledWith( - 'opencore:command:execute:resource-b', + buildRemoteCommandExecuteEventName('resource-b'), expect.any(Number), 'cmd2', expect.any(Array), @@ -613,7 +614,7 @@ describe('CommandExportController', () => { // Should delegate to resource expect(mockEngineEvents.emit).toHaveBeenCalledWith( - 'opencore:command:execute:test-resource', + buildRemoteCommandExecuteEventName('test-resource'), 1, 'test', ['arg1'], diff --git a/tests/unit/server/controllers/remote-command-execution.controller.test.ts b/tests/unit/server/controllers/remote-command-execution.controller.test.ts index 9dc76ab..34988ef 100644 --- a/tests/unit/server/controllers/remote-command-execution.controller.test.ts +++ b/tests/unit/server/controllers/remote-command-execution.controller.test.ts @@ -2,11 +2,12 @@ import 'reflect-metadata' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { IEngineEvents } from '../../../../src/adapters/contracts/IEngineEvents' import type { IResourceInfo } from '../../../../src/adapters/contracts/IResourceInfo' +import { buildRemoteCommandExecuteEventName } from '../../../../src/runtime/shared/types/system-types' import type { CommandErrorObserverContract } from '../../../../src/runtime/server/contracts/security/command-error-observer.contract' import { RemoteCommandExecutionController } from '../../../../src/runtime/server/controllers/remote-command-execution.controller' import type { Players } from '../../../../src/runtime/server/ports/players.api-port' import { createTestPlayer } from '../../../helpers' -import { CommandExecutionPort } from 'src/runtime/server/services' +import { CommandExecutionPort } from '../../../../src/runtime/server/services' vi.mock('../../../../src/runtime/server/runtime', () => ({ getRuntimeContext: vi.fn(() => ({ mode: 'RESOURCE' })), @@ -68,7 +69,7 @@ describe('RemoteCommandExecutionController', () => { describe('event registration', () => { it('should register event handler with correct event name', () => { expect(mockEngineEvents.on).toHaveBeenCalledWith( - 'opencore:command:execute:test-resource', + buildRemoteCommandExecuteEventName('test-resource'), expect.any(Function), ) }) diff --git a/tests/unit/server/entities/vehicle.syncbag.test.ts b/tests/unit/server/entities/vehicle.syncbag.test.ts new file mode 100644 index 0000000..9e482bb --- /dev/null +++ b/tests/unit/server/entities/vehicle.syncbag.test.ts @@ -0,0 +1,83 @@ +import 'reflect-metadata' +import { describe, expect, it, vi } from 'vitest' +import type { + EntityStateBag, + IEntityServer, +} from '../../../../src/adapters/contracts/server/IEntityServer' +import type { IVehicleServer } from '../../../../src/adapters/contracts/server/IVehicleServer' +import { Vehicle } from '../../../../src/runtime/server/entities/vehicle' + +describe('Vehicle state bag synchronization', () => { + function createVehicle() { + const state = new Map() + const stateBag: EntityStateBag = { + set: vi.fn((key: string, value: unknown) => state.set(key, value)), + get: vi.fn((key: string) => state.get(key)), + } + + const entityServer: IEntityServer = { + doesExist: vi.fn(() => true), + getCoords: vi.fn(() => ({ x: 1, y: 2, z: 3 })), + setPosition: vi.fn(), + setCoords: vi.fn(), + getHeading: vi.fn(() => 90), + setHeading: vi.fn(), + getModel: vi.fn(() => 123), + delete: vi.fn(), + setOrphanMode: vi.fn(), + setDimension: vi.fn(), + getDimension: vi.fn(() => 0), + getStateBag: vi.fn(() => stateBag), + getHealth: vi.fn(() => 1000), + setHealth: vi.fn(), + getArmor: vi.fn(() => 50), + setArmor: vi.fn(), + } + + const vehicleServer: IVehicleServer = { + getNumberPlateText: vi.fn(() => 'TEST'), + setNumberPlateText: vi.fn(), + getColours: vi.fn(() => [0, 0]), + setColours: vi.fn(), + setDoorsLocked: vi.fn(), + getNetworkIdFromEntity: vi.fn(() => 55), + } as any + + return { + state, + stateBag, + entityServer, + vehicleServer, + vehicle: new Vehicle( + 99, + 55, + { clientID: 1, accountID: 'acc-1', type: 'player' }, + { entityServer, vehicleServer }, + false, + 0, + 'adder', + 123, + ), + } + } + + it('syncs ownership, metadata, mods and flags into the state bag', () => { + const { state, stateBag, vehicle, vehicleServer } = createVehicle() + + vehicle.setOwnership({ accountID: 'acc-2' }) + vehicle.setMods({ turbo: true, spoiler: 2 }) + vehicle.setMetadata('garage', 'city') + vehicle.setFuel(135) + vehicle.setDoorsLocked(true) + + expect(stateBag.set).toHaveBeenCalled() + expect(state.get('ownership')).toEqual({ clientID: 1, accountID: 'acc-2', type: 'player' }) + expect(state.get('mods')).toEqual({ turbo: true, spoiler: 2 }) + expect(state.get('meta_garage')).toBe('city') + expect(state.get('fuel')).toBe(100) + expect(state.get('locked')).toBe(true) + expect(vehicle.getMetadata('garage')).toBe('city') + expect(vehicle.getFuel()).toBe(100) + expect(vehicleServer.setDoorsLocked).toHaveBeenCalledWith(99, 2) + }) +}) diff --git a/tests/unit/server/services/channel.service.test.ts b/tests/unit/server/services/channel.service.test.ts index b6fa050..089e2b8 100644 --- a/tests/unit/server/services/channel.service.test.ts +++ b/tests/unit/server/services/channel.service.test.ts @@ -1,6 +1,7 @@ import 'reflect-metadata' import { beforeEach, describe, expect, it, vi } from 'vitest' import { EventsAPI } from '../../../../src/adapters/contracts/transport/events.api' +import { SYSTEM_EVENTS } from '../../../../src/runtime/shared/types/system-types' import { Player } from '../../../../src/runtime/server/entities/player' import { Players } from '../../../../src/runtime/server/ports/players.api-port' import { ChannelType } from '../../../../src/runtime/server/types/channel.types' @@ -184,7 +185,7 @@ describe('ChannelService', () => { it('should broadcast message to channel subscribers', () => { channelService.broadcast('test-channel', mockPlayer1, 'Hello everyone!') - expect(mockEventsAPI.emit).toHaveBeenCalledWith('core:chat:addMessage', [1, 2], { + expect(mockEventsAPI.emit).toHaveBeenCalledWith(SYSTEM_EVENTS.chat.addMessage, [1, 2], { args: ['Player1', 'Hello everyone!'], color: { r: 255, g: 255, b: 255 }, }) @@ -197,7 +198,7 @@ describe('ChannelService', () => { b: 200, }) - expect(mockEventsAPI.emit).toHaveBeenCalledWith('core:chat:addMessage', [1, 2], { + expect(mockEventsAPI.emit).toHaveBeenCalledWith(SYSTEM_EVENTS.chat.addMessage, [1, 2], { args: ['CustomAuthor', 'Test message'], color: { r: 100, g: 150, b: 200 }, }) @@ -206,7 +207,7 @@ describe('ChannelService', () => { it('should broadcast system message', () => { channelService.broadcastSystem('test-channel', 'System announcement') - expect(mockEventsAPI.emit).toHaveBeenCalledWith('core:chat:addMessage', [1, 2], { + expect(mockEventsAPI.emit).toHaveBeenCalledWith(SYSTEM_EVENTS.chat.addMessage, [1, 2], { args: ['SYSTEM', 'System announcement'], color: { r: 0, g: 191, b: 255 }, }) diff --git a/tests/unit/transport/node-events.test.ts b/tests/unit/transport/node-events.test.ts index 8ab4f68..5b4f2f7 100644 --- a/tests/unit/transport/node-events.test.ts +++ b/tests/unit/transport/node-events.test.ts @@ -23,8 +23,8 @@ describe('NodeEvents', () => { // ═══════════════════════════════════════════════════════════════════════════ describe('on + emit', () => { it('should receive events emitted to a specific clientId target', async () => { - let receivedCtx: any - let receivedArgs: any[] = [] + let receivedCtx: { clientId?: number; raw?: unknown } | undefined + let receivedArgs: readonly unknown[] = [] events.on('test:event', (ctx, ...args) => { receivedCtx = ctx @@ -36,12 +36,12 @@ describe('NodeEvents', () => { await waitForEventProcessing() expect(receivedCtx).toBeDefined() - expect(receivedCtx.clientId).toBe(42) + expect(receivedCtx?.clientId).toBe(42) expect(receivedArgs).toEqual(['hello', 'world']) }) it('should receive events emitted to "all"', async () => { - let receivedCtx: any + let receivedCtx: { clientId?: number; raw?: unknown } | undefined events.on('broadcast', (ctx) => { receivedCtx = ctx @@ -52,7 +52,7 @@ describe('NodeEvents', () => { await waitForEventProcessing() expect(receivedCtx).toBeDefined() - expect(receivedCtx.clientId).toBe(-1) + expect(receivedCtx?.clientId).toBe(-1) }) it('should emit to each clientId in an array target', async () => { @@ -72,7 +72,7 @@ describe('NodeEvents', () => { }) it('should treat non-target first arg as payload', async () => { - let receivedArgs: any[] = [] + let receivedArgs: readonly unknown[] = [] events.on('payload:first', (_ctx, ...args) => { receivedArgs = args @@ -92,8 +92,8 @@ describe('NodeEvents', () => { // ═══════════════════════════════════════════════════════════════════════════ describe('simulateClientEvent', () => { it('should simulate an event from a specific client', async () => { - let receivedCtx: any - let receivedArgs: any[] = [] + let receivedCtx: { clientId?: number; raw?: unknown } | undefined + let receivedArgs: readonly unknown[] = [] events.on('client:action', (ctx, ...args) => { receivedCtx = ctx @@ -104,8 +104,8 @@ describe('NodeEvents', () => { await waitForEventProcessing() - expect(receivedCtx.clientId).toBe(7) - expect(receivedCtx.raw).toBe(7) + expect(receivedCtx?.clientId).toBe(7) + expect(receivedCtx?.raw).toBe(7) expect(receivedArgs).toEqual(['arg1', 'arg2']) }) diff --git a/tsconfig.json b/tsconfig.json index 48071c0..d97c79b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,8 +13,6 @@ "experimentalDecorators": true, "emitDecoratorMetadata": true, - "baseUrl": ".", - "types": ["@types/node"], "noEmit": true },