From cc47e529cc931cd55c77e004daf0a4282808940d Mon Sep 17 00:00:00 2001 From: Miho931 <98314142+Miho931@users.noreply.github.com> Date: Mon, 30 Jun 2025 21:56:25 +0200 Subject: [PATCH 1/3] Yaca Install --- resources/[voice]/yaca-voice/.deepsource.toml | 19 + resources/[voice]/yaca-voice/README.md | 723 +++++++ .../yaca-voice/apps/yaca-client/build.js | 25 + .../yaca-voice/apps/yaca-client/package.json | 22 + .../apps/yaca-client/pnpm-lock.yaml | 1088 +++++++++++ .../apps/yaca-client/src/bridge/saltychat.ts | 181 ++ .../yaca-voice/apps/yaca-client/src/index.ts | 8 + .../apps/yaca-client/src/utils/cache.ts | 79 + .../apps/yaca-client/src/utils/index.ts | 40 + .../apps/yaca-client/src/utils/props.ts | 49 + .../apps/yaca-client/src/utils/redm.ts | 62 + .../apps/yaca-client/src/utils/streaming.ts | 58 + .../apps/yaca-client/src/utils/vectors.ts | 38 + .../apps/yaca-client/src/utils/vehicle.ts | 93 + .../apps/yaca-client/src/utils/websocket.ts | 87 + .../apps/yaca-client/src/yaca/data.ts | 97 + .../apps/yaca-client/src/yaca/index.ts | 6 + .../apps/yaca-client/src/yaca/intercom.ts | 59 + .../apps/yaca-client/src/yaca/main.ts | 1733 +++++++++++++++++ .../apps/yaca-client/src/yaca/megaphone.ts | 218 +++ .../apps/yaca-client/src/yaca/phone.ts | 288 +++ .../apps/yaca-client/src/yaca/radio.ts | 1124 +++++++++++ .../yaca-voice/apps/yaca-client/tsconfig.json | 9 + .../yaca-voice/apps/yaca-server/build.js | 23 + .../yaca-voice/apps/yaca-server/package.json | 25 + .../apps/yaca-server/pnpm-lock.yaml | 1136 +++++++++++ .../apps/yaca-server/src/bridge/saltychat.ts | 165 ++ .../yaca-voice/apps/yaca-server/src/index.ts | 5 + .../apps/yaca-server/src/utils/cache.ts | 8 + .../apps/yaca-server/src/utils/events.ts | 23 + .../apps/yaca-server/src/utils/generator.ts | 34 + .../apps/yaca-server/src/utils/index.ts | 4 + .../yaca-server/src/utils/versioncheck.ts | 57 + .../apps/yaca-server/src/yaca/index.ts | 4 + .../apps/yaca-server/src/yaca/main.ts | 394 ++++ .../apps/yaca-server/src/yaca/megaphone.ts | 86 + .../apps/yaca-server/src/yaca/phone.ts | 278 +++ .../apps/yaca-server/src/yaca/radio.ts | 379 ++++ .../yaca-voice/apps/yaca-server/tsconfig.json | 9 + .../assets/yaca-voice/config/server.json5 | 17 + .../assets/yaca-voice/config/shared.json5 | 174 ++ .../assets/yaca-voice/config/towers.json5 | 519 +++++ .../assets/yaca-voice/locales/de.json | 33 + .../assets/yaca-voice/locales/en.json | 33 + .../assets/yaca-voice/web/index.html | 13 + .../assets/yaca-voice/web/script.js | 85 + resources/[voice]/yaca-voice/biome.json | 47 + resources/[voice]/yaca-voice/package.json | 27 + .../yaca-voice/packages/common/package.json | 23 + .../yaca-voice/packages/common/pnpm-lock.yaml | 30 + .../yaca-voice/packages/common/src/bridge.ts | 11 + .../yaca-voice/packages/common/src/config.ts | 71 + .../packages/common/src/constants.ts | 5 + .../packages/common/src/errorlevel.ts | 20 + .../yaca-voice/packages/common/src/index.ts | 58 + .../yaca-voice/packages/common/src/locale.ts | 91 + .../yaca-voice/packages/common/tsconfig.json | 8 + .../yaca-voice/packages/tsconfig/fivem.json | 21 + .../yaca-voice/packages/tsconfig/package.json | 3 + .../yaca-voice/packages/types/package.json | 10 + .../yaca-voice/packages/types/src/config.ts | 305 +++ .../yaca-voice/packages/types/src/enums.ts | 40 + .../yaca-voice/packages/types/src/index.ts | 3 + .../yaca-voice/packages/types/src/types.ts | 98 + resources/[voice]/yaca-voice/pnpm-lock.yaml | 622 ++++++ .../[voice]/yaca-voice/pnpm-workspace.yaml | 3 + .../yaca-voice/scripts/create-resource.js | 67 + resources/[voice]/yaca-voice/turbo.json | 19 + 68 files changed, 11192 insertions(+) create mode 100644 resources/[voice]/yaca-voice/.deepsource.toml create mode 100644 resources/[voice]/yaca-voice/README.md create mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/build.js create mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/package.json create mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/pnpm-lock.yaml create mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/bridge/saltychat.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/index.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/utils/cache.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/utils/index.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/utils/props.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/utils/redm.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/utils/streaming.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/utils/vectors.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/utils/vehicle.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/utils/websocket.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/data.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/index.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/intercom.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/main.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/megaphone.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/phone.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/radio.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/tsconfig.json create mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/build.js create mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/package.json create mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/pnpm-lock.yaml create mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/src/bridge/saltychat.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/src/index.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/src/utils/cache.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/src/utils/events.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/src/utils/generator.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/src/utils/index.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/src/utils/versioncheck.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/index.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/main.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/megaphone.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/phone.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/radio.ts create mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/tsconfig.json create mode 100644 resources/[voice]/yaca-voice/assets/yaca-voice/config/server.json5 create mode 100644 resources/[voice]/yaca-voice/assets/yaca-voice/config/shared.json5 create mode 100644 resources/[voice]/yaca-voice/assets/yaca-voice/config/towers.json5 create mode 100644 resources/[voice]/yaca-voice/assets/yaca-voice/locales/de.json create mode 100644 resources/[voice]/yaca-voice/assets/yaca-voice/locales/en.json create mode 100644 resources/[voice]/yaca-voice/assets/yaca-voice/web/index.html create mode 100644 resources/[voice]/yaca-voice/assets/yaca-voice/web/script.js create mode 100644 resources/[voice]/yaca-voice/biome.json create mode 100644 resources/[voice]/yaca-voice/package.json create mode 100644 resources/[voice]/yaca-voice/packages/common/package.json create mode 100644 resources/[voice]/yaca-voice/packages/common/pnpm-lock.yaml create mode 100644 resources/[voice]/yaca-voice/packages/common/src/bridge.ts create mode 100644 resources/[voice]/yaca-voice/packages/common/src/config.ts create mode 100644 resources/[voice]/yaca-voice/packages/common/src/constants.ts create mode 100644 resources/[voice]/yaca-voice/packages/common/src/errorlevel.ts create mode 100644 resources/[voice]/yaca-voice/packages/common/src/index.ts create mode 100644 resources/[voice]/yaca-voice/packages/common/src/locale.ts create mode 100644 resources/[voice]/yaca-voice/packages/common/tsconfig.json create mode 100644 resources/[voice]/yaca-voice/packages/tsconfig/fivem.json create mode 100644 resources/[voice]/yaca-voice/packages/tsconfig/package.json create mode 100644 resources/[voice]/yaca-voice/packages/types/package.json create mode 100644 resources/[voice]/yaca-voice/packages/types/src/config.ts create mode 100644 resources/[voice]/yaca-voice/packages/types/src/enums.ts create mode 100644 resources/[voice]/yaca-voice/packages/types/src/index.ts create mode 100644 resources/[voice]/yaca-voice/packages/types/src/types.ts create mode 100644 resources/[voice]/yaca-voice/pnpm-lock.yaml create mode 100644 resources/[voice]/yaca-voice/pnpm-workspace.yaml create mode 100644 resources/[voice]/yaca-voice/scripts/create-resource.js create mode 100644 resources/[voice]/yaca-voice/turbo.json diff --git a/resources/[voice]/yaca-voice/.deepsource.toml b/resources/[voice]/yaca-voice/.deepsource.toml new file mode 100644 index 000000000..6ac43a34b --- /dev/null +++ b/resources/[voice]/yaca-voice/.deepsource.toml @@ -0,0 +1,19 @@ +version = 1 + +exclude_patterns = [ + "resources/**", + "dist/**" +] + +[[analyzers]] +name = "secrets" + +[[analyzers]] +name = "javascript" + + [analyzers.meta] + environment = [ + "jquery", + "nodejs", + "browser" + ] \ No newline at end of file diff --git a/resources/[voice]/yaca-voice/README.md b/resources/[voice]/yaca-voice/README.md new file mode 100644 index 000000000..dff0df44d --- /dev/null +++ b/resources/[voice]/yaca-voice/README.md @@ -0,0 +1,723 @@ +# [yaca.systems](https://yaca.systems/) for [FiveM](https://fivem.net/) & [RedM](https://redm.net/) + +This is a example implementation for [FiveM](https://fivem.net/) & [RedM](https://redm.net/). +Feel free to report bugs via issues or contribute via pull requests. + +Join our [Discord](http://discord.yaca.systems/) to get help or make suggestions and start +using [yaca.systems](https://yaca.systems/) today! + +# Setup Steps + +Before you start, make sure you have OneSync enabled and your server artifacts are up to date. + +1. Download and install the latest [release](https://github.com/yaca-systems/fivem-yaca-typescript/releases) of this + resource. +2. Add `start yaca-voice` into your `server.cfg`. +3. Open `config/server.json5` and adjust the variables to your needs. +4. Open `config/shared.json5` and adjust the variables to your needs. + +# Exports + +
+Client + +### General + +#### `getVoiceRange(): int` + +Get the current voice range of the player as `int`. + +#### `getVoiceRanges(): int[]` + +Get all voice ranges as `int[]`. + +#### `changeVoiceRange(increase: boolean): void` + +Change the voice range of the player to the next range. + +#### `setVoiceRange(range: number): void` + +Set the voice range of the player. + +#### `setVoiceRangeChangeAllowedState(state: boolean): void` + +Enable or disable the possibility to change the voice range. + +| Parameter | Type | Description | +|-----------|-----------|------------------------------------------------| +| state | `boolean` | `true` to allow the voice range change, `false` to disable | + +#### `getVoiceRangeChangeAllowedState(): boolean` + +Get the voice range change allowed state of the player as `boolean`. + +#### `setMaxVoiceRange(range: number): void` + +Set the maximum allowed voice range of the player in meters to limit the voice range temporarily. + +| Parameter | Type | Description | +|-----------|-----------|------------------------------------------------| +| range | `number` | `-1` to disable the limit, or a number in meters to set the limit | + +#### `getMaxVoiceRange(): number` + +Get the maximum allowed voice range of the player in meters. + +#### `getMicrophoneMuteState(): boolean` + +Get the microphone mute state of the player as `boolean`. + +#### `getMicrophoneDisabledState(): boolean` + +Get the microphone disabled state of the player as `boolean`. + +#### `getSoundMuteState(): boolean` + +Get the sound mute state of the player as `boolean`. + +#### `getSoundDisabledState(): boolean` + +Get the sound disabled state of the player as `boolean`. + +#### `getPluginState(): string` + +Get the current plugin state as `string`. + +The state can be one of the following: + +- `"NOT_CONNECTED"`: The plugin is not connected +- `"CONNECTED`: The plugin is connected +- `"OUTDATED_VERSION"`: The plugin is not the version set in the dashboard +- `"WRONG_TS_SERVER"`: The user is connected to the wrong Teamspeak server +- `"IN_INGAME_CHANNEL"`: The user is in the ingame channel +- `"IN_EXCLUDED_CHANNEL"`: The user is in an excluded channel + +#### `getGlobalErrorLevel(): number` + +Get the global error level as `number`. + +#### `setSpectatingPlayer(playerId: number | false)` + +Set the player to spectate. + +| Parameter | Type | Description | +|-----------|-------------------|-------------------| +| playerId | `number \| false` | the player to set | + +#### `getSpectatingPlayer(): number` + +Get the player the user is spectating as `number`. + +#### `setVoiceRangeMarkerColor(red: number, green: number, blue: number, alpha: number)` + +Set the voice range marker color. + +#### `getVoiceRangeMarkerColor(): [number, number, number, number]` + +Get the voice range marker color as `[red, green, blue, alpha]`. + +#### `resetVoiceRangeMarkerColor()` + +Reset the voice range marker color to the default color defined in the config. + +### Radio + +#### `enableRadio(state: boolean)` + +Enables or disables the radio system. + +| Parameter | Type | Description | +|-----------|-----------|------------------------------------------------| +| state | `boolean` | `true` to enable the radio, `false` to disable | + +#### `isRadioEnabled(): boolean` + +Returns whether the radio system is enabled as `boolean`. + +#### `changeRadioFrequency(frequency: string)` + +Changes the radio frequency of the active channel. + +| Parameter | Type | Description | +|-----------|----------|--------------------------------------------| +| frequency | `string` | The frequency to set the active channel to | + +#### `changeRadioFrequencyRaw(channel: number, frequency: string)` + +Changes the radio frequency. + +| Parameter | Type | Description | +|-----------|----------|---------------------------------------------------------------------------------------| +| channel? | `number` | the channel number. Defaults to the current active channel when no channel is passed. | +| frequency | `string` | the frequency to set the channel to | + +#### `getRadioFrequency(channel: number): string` + +Returns the frequency of a radio channel as `string`. + +| Parameter | Type | Description | +|-----------|----------|---------------------------------------------------------------------------------------| +| channel? | `number` | the channel number. Defaults to the current active channel when no channel is passed. | + +#### `muteRadioChannel(state?: boolean)` + +Mutes the current active radio channel. + +| Parameter | Type | Description | +|-----------|-----------|-----------------------------------------------------------------------------| +| state? | `boolean` | `true` to mute the channel, `false` to unmute. Defaults to switch if not defined | + +#### `muteRadioChannelRaw(channel: number, state?: boolean)` + +Mutes a radio channel. + +| Parameter | Type | Description | +|-----------|----------|----------------------------------------------------------------------------------------| +| channel? | `number` | the channel to mute. Defaults to the current active channel when no channel is passed. | +| state? | `boolean` | `true` to mute the channel, `false` to unmute. Defaults to switch if not defined | + +#### `isRadioChannelMuted(channel: number): boolean` + +Returns whether a radio channel is muted as `boolean`. + +| Parameter | Type | Description | +|-----------|----------|--------------------| +| channel | `number` | the channel number | + +#### `setActiveRadioChannel(channel: number): bool` + +Changes the active radio channel. Returns whether the operation was successful as `bool`. + +| Parameter | Type | Description | +|-----------|----------|-----------------------| +| channel | `number` | the new radio channel | + +#### `getActiveRadioChannel(): number` + +Returns the active radio channel as `number`. + +#### `setSecondaryRadioChannel(channel: number): bool` + +Changes the secondary radio channel. Returns whether the operation was successful as `bool`. + +| Parameter | Type | Description | +|-----------|----------|-----------------------| +| channel | `number` | the new radio channel | + +#### `getSecondaryRadioChannel(): number` + +Returns the secondary radio channel as `number`. + +#### `changeRadioChannelVolume(higher: boolean): bool` + +Changes the volume of the active radio channel. Returns whether the operation was successful as `bool`. + +| Parameter | Type | Description | +|-----------|-----------|--------------------------------| +| higher | `boolean` | whether to increase the volume | + +#### `changeRadioChannelVolumeRaw(channel: number, volume: number): bool` + +Changes the volume of a radio channel. Returns whether the operation was successful as `bool`. + +| Parameter | Type | Description | +|-----------|----------|--------------------| +| channel | `number` | the channel number | +| volume | `number` | the volume to set | + +#### `getRadioChannelVolume(channel: number): number` + +Returns the volume of a radio channel as `number`. + +| Parameter | Type | Description | +|-----------|----------|--------------------| +| channel | `number` | the channel number | + +#### `changeRadioChannelStereo(): bool` + +Changes the stereo mode of the active radio channel. Returns whether the operation was successful as `bool`. + +#### `changeRadioChannelStereoRaw(channel: number, stereo: string): bool` + +Changes the stereo mode of a radio channel. Returns whether the operation was successful as `bool`. + +| Parameter | Type | Description | +|-----------|----------|---------------------------------------------------------------| +| channel | `number` | the channel number | +| stereo | `string` | the stereo mode (`"MONO_LEFT"`, `"MONO_RIGHT"` or `"STEREO"`) | + +#### `getRadioChannelStereo(channel: number): string` + +Returns the stereo mode of a radio channel as `string`. + +| Parameter | Type | Description | +|-----------|----------|--------------------| +| channel | `number` | the channel number | + +#### `radioTalkingStart(state: boolean, channel: number)` + +Starts or stops talking on the radio. + +| Parameter | Type | Description | +|-----------|-----------|------------------------------------------| +| state | `boolean` | `true` to start talking, `false` to stop | +| channel | `number` | the channel to talk on | + +#### `setRadioMode(mode: string)` + +Sets the radio mode. + +| Parameter | Type | Description | +|-----------|-----------------------------------------------------------------------------| +| mode | `string` | the radio mode to set. Can be either `None`, `Direct` or `Tower` | + +#### `getRadioMode(): string` + +Returns the radio mode as `string`. + +### Phone + +#### `isInCall(): boolean` + +Returns whether the player is in a phone call as a `boolean`. + +### Megaphone + +#### `getCanUseMegaphone(): boolean` + +Returns whether the player can use the megaphone as a `boolean`. + +#### `setCanUseMegaphone(state: boolean)` + +Sets whether the player can use the megaphone. + +| Parameter | Type | Description | +|-----------|-----------|---------------------------------------------------------| +| state | `boolean` | `true` to allow using of megaphone, `false` to disallow | + +### `useMegaphone(state: boolean)` + +Starts or stops using the megaphone. + +| Parameter | Type | Description | +|-----------|-----------|----------------------------------------| +| state | `boolean` | `true` to start using, `false` to stop | + +
+ +
+Server + +### General + +#### `connectToVoice(source: number)` +Connects a player to the voice system. + +| Parameter | Type | Description | +|-----------|----------|-------------------| +| source | `number` | the player source | + +#### `getPlayerAliveStatus(source: number): bool` + +Get the alive status of a player as `bool`. + +| Parameter | Type | Description | +|-----------|----------|-------------------| +| source | `number` | the player source | + +#### `setPlayerAliveStatus(source: number, state: bool)` + +Set the alive status of a player. + +| Parameter | Type | Description | +|-----------|-----------|---------------------| +| source | `number` | the player source | +| state | `boolean` | the new alive state | + +#### `getPlayerVoiceRange(source: number): number` + +Get the voice range of a player as `number`. + +| Parameter | Type | Description | +|-----------|----------|-------------------| +| source | `number` | the player source | + +#### `setPlayerVoiceRange(source: number, range: number)` + +Set the voice range of a player. + +| Parameter | Type | Description | +|-----------|----------|---------------------------------------------------------------------------| +| source | `number` | the player source | +| range | `number` | The new voice range. Defaults to the default voice range if not provided. | + +### Radio + +#### `getPlayersInRadioFrequency(frequency: string): int[]` + +Returns all players in a radio frequency as `int[]`. + +| Parameter | Type | Description | +|-----------|----------|----------------------| +| frequency | `string` | the frequency to get | + +#### `setPlayerRadioChannel(source: number, channel: number, frequency: string)` + +Sets the radio channel of a player. + +| Parameter | Type | Description | +|-----------|----------|----------------------| +| source | `number` | the player source | +| channel | `number` | the channel to set | +| frequency | `string` | the frequency to set | + +#### `getPlayerHasLongRange(source: number): bool` + +Returns whether a player has long range enabled as `bool`. + +| Parameter | Type | Description | +|-----------|----------|-------------------| +| source | `number` | the player source | + +#### `setPlayerHasLongRange(source: number, state: bool)` + +Sets the long range state of a player. + +| Parameter | Type | Description | +|-----------|-----------|----------------------| +| source | `number` | the player source | +| state | `boolean` | the long range state | + +### Phone + +#### `callPlayer(source: number, target: number, state: bool)` + +Creates a phone call between two players. + +| Parameter | Type | Description | +|-----------|-----------|--------------------------| +| source | `number` | the player source | +| target | `number` | the target player source | +| state | `boolean` | the state of the call | + +#### `callPlayerOldEffect(source: number, target: number, state: bool)` + +Creates a phone call between two players with the old effect. + +| Parameter | Type | Description | +|-----------|-----------|--------------------------| +| source | `number` | the player source | +| target | `number` | the target player source | +| state | `boolean` | the state of the call | + +#### `muteOnPhone(source: number, state: bool)` + +Mutes the player when using the phone. + +| Parameter | Type | Description | +|-----------|-----------|-------------------| +| source | `number` | the player source | +| state | `boolean` | the mute state | + +#### `enablePhoneSpeaker(source: number, state: bool)` + +Enable or disable the phone speaker for a player. + +| Parameter | Type | Description | +|-----------|-----------|-------------------------| +| source | `number` | the player source | +| state | `boolean` | the phone speaker state | + +#### `isPlayerInCall(source: number): [bool, number[]]` + +Returns whether a player is in a phone call as `[bool, number[]]`. + +| Parameter | Type | Description | +|-----------|----------|-------------------| +| source | `number` | the player source | + +#### `setGlobalErrorLevel(level: number)` + +Sets the global error level. + +| Parameter | Type | Description | +|-----------|----------|-----------------| +| level | `number` | the error level | + +#### `getGlobalErrorLevel(): number` + +Returns the global error level as `number`. + +
+ +# Events + +
+Client + +### yaca:external:pluginInitialized + +The event is triggered when the plugin is initialized. + +| Parameter | Type | Description | +|-----------|-------|----------------------------------------------| +| clientId | `int` | the client id of the local user in teamspeak | + +### yaca:external:pluginStateChanged + +The event is triggered when the plugin state changes. + +| Parameter | Type | Description | +|-----------|----------|----------------------------------------------| +| state | `string` | the current plugin state, as explained below | + +The state can be one of the following: + +- `"NOT_CONNECTED"`: The plugin is not connected +- `"CONNECTED`: The plugin is connected +- `"OUTDATED_VERSION"`: The plugin is not the version set in the dashboard +- `"WRONG_TS_SERVER"`: The user is connected to the wrong Teamspeak server +- `"IN_INGAME_CHANNEL"`: The user is in the ingame channel +- `"IN_EXCLUDED_CHANNEL"`: The user is in an excluded channel + +### yaca:external:voiceRangeUpdate + +This event is triggered when the voice range of a player is updated. + +| Parameter | Type | Description | +|------------|-------|---------------------------| +| range | `int` | the newly set voice range | +| rangeIndex | `int` | the index of the range | + +### yaca:external:muteStateChanged + +DEPRECATED: Use `yaca:external:microphoneMuteStateChanged` instead. +The event is triggered when the mute state of a player changes. + +| Parameter | Type | Description | +|-----------|-----------|--------------------| +| state | `boolean` | the new mute state | + +### yaca:external:microphoneMuteStateChanged + +The event is triggered when the microphone mute state of a player changes. + +| Parameter | Type | Description | +|-----------|-----------|--------------------| +| state | `boolean` | the new mute state | + +### yaca:external:microphoneDisabledStateChanged + +The event is triggered when the microphone disabled state of a player changes. + +| Parameter | Type | Description | +|-----------|-----------|--------------------| +| state | `boolean` | the new mute state | + +### yaca:external:soundMuteStateChanged + +The event is triggered when the sound mute state of a player changes. + +| Parameter | Type | Description | +|-----------|-----------|--------------------| +| state | `boolean` | the new mute state | + +### yaca:external:soundDisabledStateChanged + +The event is triggered when the sound disabled state of a player changes. + +| Parameter | Type | Description | +|-----------|-----------|--------------------| +| state | `boolean` | the new mute state | + +### yaca:external:isTalking + +The event is triggered when a player starts or stops talking. + +| Parameter | Type | Description | +|-----------|-----------|-----------------------| +| state | `boolean` | the new talking state | + +### yaca:external:megaphoneState + +The event is triggered when the megaphone state of a player changes. + +| Parameter | Type | Description | +|-----------|-----------|-------------------------| +| state | `boolean` | the new megaphone state | + +### yaca:external:setRadioMuteState + +The event is triggered when the radio mute state of a player changes. + +| Parameter | Type | Description | +|-----------|-----------|---------------------------------------------| +| channel | `number` | the channel where the mute state is changed | +| state | `boolean` | the new mute state | + +### yaca:external:isRadioEnabled + +The event is triggered when the radio state of a player changes. + +| Parameter | Type | Description | +|-----------|-----------|----------------------------------------------------------------------| +| state | `boolean` | `true` when the radio is enabled, `false` when the radio is disabled | + +### yaca:external:changedActiveRadioChannel + +The event is triggered when the active radio channel of a player changes. + +| Parameter | Type | Description | +|-----------|----------|------------------------------| +| channel | `number` | the new active radio channel | + +### yaca:external:changedSecondaryRadioChannel + +The event is triggered when the secondary radio channel of a player changes. + +| Parameter | Type | Description | +|-----------|----------|---------------------------------------------------| +| channel | `number` | the new active radio channel, or `-1` if disabled | + +### yaca:external:setRadioVolume + +The event is triggered when the radio volume of a player changes. + +| Parameter | Type | Description | +|-----------|----------|-----------------------| +| channel | `number` | the channel to change | +| volume | `number` | the new volume to set | + +### yaca:external:setRadioChannelStereo + +The event is triggered when the stereo mode of a radio channel changes. + +| Parameter | Type | Description | +|-----------|----------|-----------------------------------------------------------------------------------------------| +| channel | `number` | the channel to change | +| stereo | `string` | `"MONO_LEFT"` for the left ear, `"MONO_RIGHT"` for the right ear and `"STEREO"` for both ears | + +### yaca:external:setRadioFrequency + +The event is triggered when the radio frequency of a player changes. + +| Parameter | Type | Description | +|-----------|----------|----------------------| +| channel | `number` | the channel to set | +| frequency | `string` | the frequency to set | + +### yaca:external:isRadioTalking + +The event is triggered when a player starts or stops talking on the radio. + +| Parameter | Type | Description | +|-----------|-----------|--------------------------------------------| +| state | `boolean` | the new talking state | +| channel | `number` | the channel where the player is talking at | + +### yaca:external:isRadioReceiving + +The event is triggered when a player starts or stops receiving on the radio. + +| Parameter | Type | Description | +|-----------|-----------|------------------------------------------------| +| state | `boolean` | the new receiver state | +| channel | `number` | the channel from which the player is receiving | + +### yaca:external:notification + +The event is triggered when a notification should be shown. + +| Parameter | Type | Description | +|-----------|----------|--------------------------------------------------------------| +| message | `string` | the message to show | +| type | `string` | the type of the message (`"inform"`, `"error"`, `"success"`) | + +Example for custom notification: + +```lua +AddEventHandler('yaca:external:notification', function (message, type) + -- Call your Notifications System here. +end) +``` + +### yaca:external:channelChanged + +The event is triggered when the player changes the channel to the ingame or excluded channel. + +| Parameter | Type | Description | +|-------------|----------|------------------------------------------------------------------------------------------------------------------| +| channelType | `string` | `INGAME_CHANNEL` when moving into the ingame channel and `EXCLUDED_CHANNEL` when moving into a excluded channel. | + +
+ +
+Server + +### yaca:external:changeMegaphoneState + +The event is triggered when the megaphone state of a player changes. + +| Parametr | Type | Description | +|----------|-----------|-------------------------| +| source | `int` | the player source | +| state | `boolean` | the new megaphone state | + +### yaca:external:phoneCall + +The event is triggered when a phone call is started or ended. + +| Parameter | Type | Description | +|-----------|------------------|---------------------------------------------------------------------------------| +| source | `int` | the player source | +| target | `int` | the target player source | +| state | `boolean` | the new phone call state | +| filter | `YacaFilterEnum` | the used filter for the phone call, can be either `PHONE` or `PHONE_HISTORICAL` | + +### yaca:external:phoneSpeaker + +The event is triggered when the phone speaker state of a player changes. + +| Parameter | Type | Description | +|-----------|-----------|-----------------------------| +| source | `int` | the player source | +| state | `boolean` | the new phone speaker state | + +### yaca:external:changedRadioFrequency + +The event is triggered when the radio frequency of a player changes. + +| Parameter | Type | Description | +|-----------|----------|-----------------------------------------| +| source | `int` | the player source | +| channel | `int` | the channel where the frequency was set | +| frequency | `string` | the frequency to set | + +### yaca:external:changedRadioMuteState + +The event is triggered when the radio mute state of a player changes. + +| Parameter | Type | Description | +|-----------|-----------|----------------------------------------------| +| source | `int` | the player source | +| channel | `int` | the channel where the mute state was changed | +| state | `boolean` | the new mute state | + +
+ +# Developers + +If you want to contribute to this project, feel free to do so. We are happy about every contribution. If you have any +questions, feel free to ask in our [Discord](http://discord.yaca.systems/). + +## Building the resource + +To build the resource, you need to have [Node.js](https://nodejs.org/) installed. After that, you can run the following +commands to build the resource: + +```bash +pnpm install +pnpm run build +``` + +The built resource will be located in the `resource` folder, which you can then use in your FiveM server. diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/build.js b/resources/[voice]/yaca-voice/apps/yaca-client/build.js new file mode 100644 index 000000000..175b4f4e2 --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-client/build.js @@ -0,0 +1,25 @@ +import { build } from 'esbuild' + +const production = process.argv.includes('--mode=production') + +build({ + entryPoints: ['src/index.ts'], + outfile: './dist/client.js', + bundle: true, + loader: { + '.ts': 'ts', + '.js': 'js', + }, + write: true, + platform: 'browser', + target: 'es2021', + format: 'iife', + minify: production, + sourcemap: production ? false : 'inline', + dropLabels: production ? ['DEV'] : undefined, +}) + .then(() => { + console.log('Client built successfully') + }) + // skipcq: JS-0263 + .catch(() => process.exit(1)) diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/package.json b/resources/[voice]/yaca-voice/apps/yaca-client/package.json new file mode 100644 index 000000000..da6c9f1c8 --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-client/package.json @@ -0,0 +1,22 @@ +{ + "name": "yaca-client", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "node build.js --mode=production", + "dev": "node build.js", + "typecheck": "tsc --project tsconfig.json" + }, + "dependencies": { + "eventemitter2": "^6.4.9" + }, + "devDependencies": { + "@citizenfx/client": "latest", + "@types/luxon": "^3.4.2", + "@types/node": "^20.16.10", + "@yaca-voice/common": "workspace:*", + "@yaca-voice/types": "workspace:*", + "@yaca-voice/typescript-config": "workspace:*" + } +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/pnpm-lock.yaml b/resources/[voice]/yaca-voice/apps/yaca-client/pnpm-lock.yaml new file mode 100644 index 000000000..8015206fc --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-client/pnpm-lock.yaml @@ -0,0 +1,1088 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@overextended/ox_lib': + specifier: ^3.23.1 + version: 3.24.0 + devDependencies: + '@citizenfx/server': + specifier: latest + version: 2.0.9235-1 + '@eslint/js': + specifier: ^8.57.0 + version: 8.57.0 + '@types/luxon': + specifier: ^3.4.2 + version: 3.4.2 + '@types/node': + specifier: ^20.14.10 + version: 20.14.14 + esbuild: + specifier: ^0.20.2 + version: 0.20.2 + eslint: + specifier: ^8.57.0 + version: 8.57.0 + +packages: + + '@citizenfx/server@2.0.9235-1': + resolution: {integrity: sha512-x5RgQKm+nIgmABzxQpOVvrZOTTsSWDKOkT87gbjnILW3zOWokq3LU7Ag+Ki1HvPXAe43laJdy9wUUej27KhsRQ==} + + '@esbuild/aix-ppc64@0.20.2': + resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.20.2': + resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.20.2': + resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.20.2': + resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.20.2': + resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.20.2': + resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.20.2': + resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.20.2': + resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.20.2': + resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.20.2': + resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.20.2': + resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.20.2': + resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.20.2': + resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.20.2': + resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.20.2': + resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.20.2': + resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.20.2': + resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.20.2': + resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.20.2': + resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.20.2': + resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.20.2': + resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.20.2': + resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.20.2': + resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.4.0': + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.11.0': + resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.0': + resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@humanwhocodes/config-array@0.11.14': + resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@nativewrappers/client@1.7.33': + resolution: {integrity: sha512-phuBBGdDPxZiZyw5CaFs1XWfvllnEtwATMdLaNucwMofVg/O/FjlP1bTUq4SOm4qhSZ4Zdo351ijHzBSIbZs6g==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@overextended/ox_lib@3.24.0': + resolution: {integrity: sha512-diI+hvmGfDBoYzuFeKrVLrdXff1yYIgARNP22i8oQqrzlBig46HJMXm7YRmwsE+8Z/w6TwQv9UL9j/vzVGpnnQ==} + + '@types/luxon@3.4.2': + resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} + + '@types/node@20.14.14': + resolution: {integrity: sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==} + + '@ungap/structured-clone@1.2.0': + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + debug@4.3.6: + resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + esbuild@0.20.2: + resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} + engines: {node: '>=12'} + hasBin: true + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@8.57.0: + resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-printf@1.6.9: + resolution: {integrity: sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg==} + engines: {node: '>=10.0'} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + ignore@5.3.1: + resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} + engines: {node: '>= 4'} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + typescript@5.5.4: + resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@citizenfx/server@2.0.9235-1': {} + + '@esbuild/aix-ppc64@0.20.2': + optional: true + + '@esbuild/android-arm64@0.20.2': + optional: true + + '@esbuild/android-arm@0.20.2': + optional: true + + '@esbuild/android-x64@0.20.2': + optional: true + + '@esbuild/darwin-arm64@0.20.2': + optional: true + + '@esbuild/darwin-x64@0.20.2': + optional: true + + '@esbuild/freebsd-arm64@0.20.2': + optional: true + + '@esbuild/freebsd-x64@0.20.2': + optional: true + + '@esbuild/linux-arm64@0.20.2': + optional: true + + '@esbuild/linux-arm@0.20.2': + optional: true + + '@esbuild/linux-ia32@0.20.2': + optional: true + + '@esbuild/linux-loong64@0.20.2': + optional: true + + '@esbuild/linux-mips64el@0.20.2': + optional: true + + '@esbuild/linux-ppc64@0.20.2': + optional: true + + '@esbuild/linux-riscv64@0.20.2': + optional: true + + '@esbuild/linux-s390x@0.20.2': + optional: true + + '@esbuild/linux-x64@0.20.2': + optional: true + + '@esbuild/netbsd-x64@0.20.2': + optional: true + + '@esbuild/openbsd-x64@0.20.2': + optional: true + + '@esbuild/sunos-x64@0.20.2': + optional: true + + '@esbuild/win32-arm64@0.20.2': + optional: true + + '@esbuild/win32-ia32@0.20.2': + optional: true + + '@esbuild/win32-x64@0.20.2': + optional: true + + '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': + dependencies: + eslint: 8.57.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.11.0': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.3.6 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.0': {} + + '@humanwhocodes/config-array@0.11.14': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.3.6 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@nativewrappers/client@1.7.33': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@overextended/ox_lib@3.24.0': + dependencies: + '@nativewrappers/client': 1.7.33 + csstype: 3.1.3 + fast-printf: 1.6.9 + typescript: 5.5.4 + + '@types/luxon@3.4.2': {} + + '@types/node@20.14.14': + dependencies: + undici-types: 5.26.5 + + '@ungap/structured-clone@1.2.0': {} + + acorn-jsx@5.3.2(acorn@8.12.1): + dependencies: + acorn: 8.12.1 + + acorn@8.12.1: {} + + ajv@6.12.6: + 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-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + balanced-match@1.0.2: {} + + boolean@3.2.0: {} + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + callsites@3.1.0: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.1.3: {} + + debug@4.3.6: + dependencies: + ms: 2.1.2 + + deep-is@0.1.4: {} + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + esbuild@0.20.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.20.2 + '@esbuild/android-arm': 0.20.2 + '@esbuild/android-arm64': 0.20.2 + '@esbuild/android-x64': 0.20.2 + '@esbuild/darwin-arm64': 0.20.2 + '@esbuild/darwin-x64': 0.20.2 + '@esbuild/freebsd-arm64': 0.20.2 + '@esbuild/freebsd-x64': 0.20.2 + '@esbuild/linux-arm': 0.20.2 + '@esbuild/linux-arm64': 0.20.2 + '@esbuild/linux-ia32': 0.20.2 + '@esbuild/linux-loong64': 0.20.2 + '@esbuild/linux-mips64el': 0.20.2 + '@esbuild/linux-ppc64': 0.20.2 + '@esbuild/linux-riscv64': 0.20.2 + '@esbuild/linux-s390x': 0.20.2 + '@esbuild/linux-x64': 0.20.2 + '@esbuild/netbsd-x64': 0.20.2 + '@esbuild/openbsd-x64': 0.20.2 + '@esbuild/sunos-x64': 0.20.2 + '@esbuild/win32-arm64': 0.20.2 + '@esbuild/win32-ia32': 0.20.2 + '@esbuild/win32-x64': 0.20.2 + + escape-string-regexp@4.0.0: {} + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint@8.57.0: + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/regexpp': 4.11.0 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.0 + '@humanwhocodes/config-array': 0.11.14 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.6 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.1 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.12.1 + acorn-jsx: 5.3.2(acorn@8.12.1) + eslint-visitor-keys: 3.4.3 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-printf@1.6.9: + dependencies: + boolean: 3.2.0 + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.3.1: {} + + fs.realpath@1.0.0: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + ignore@5.3.1: {} + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-path-inside@3.0.3: {} + + isexe@2.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + ms@2.1.2: {} + + natural-compare@1.4.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + prelude-ls@1.2.1: {} + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + resolve-from@4.0.0: {} + + reusify@1.0.4: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + text-table@0.2.0: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + typescript@5.5.4: {} + + undici-types@5.26.5: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrappy@1.0.2: {} + + yocto-queue@0.1.0: {} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/bridge/saltychat.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/bridge/saltychat.ts new file mode 100644 index 000000000..b93198f51 --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-client/src/bridge/saltychat.ts @@ -0,0 +1,181 @@ +import { saltyChatExport, sleep } from '@yaca-voice/common' +import { YacaPluginStates } from '@yaca-voice/types' +import { cache } from '../utils' +import type { YaCAClientModule } from '../yaca' + +/** + * The SaltyChat bridge for the client. + */ +export class YaCAClientSaltyChatBridge { + private clientModule: YaCAClientModule + + private currentPluginState = -1 + + private isPrimarySending = false + private isSecondarySending = false + + private isPrimaryReceiving = false + private isSecondaryReceiving = false + + /** + * Creates an instance of the SaltyChat bridge. + * + * @param {YaCAClientModule} clientModule - The client module. + */ + constructor(clientModule: YaCAClientModule) { + this.clientModule = clientModule + + this.registerSaltyChatExports() + this.enableRadio().then() + + console.log('[YaCA] SaltyChat bridge loaded') + + on('onResourceStop', (resourceName: string) => { + if (cache.resource !== resourceName) { + return + } + + emit('onClientResourceStop', 'saltychat') + }) + } + + /** + * Enables the radio on bridge load. + */ + async enableRadio() { + while (!this.clientModule.isPluginInitialized(true)) { + await sleep(1000) + } + + this.clientModule.radioModule.enableRadio(true) + } + + /** + * Register SaltyChat exports. + */ + registerSaltyChatExports() { + saltyChatExport('GetVoiceRange', () => this.clientModule.getVoiceRange()) + + saltyChatExport('GetRadioChannel', (primary: boolean) => { + const channel = primary ? 1 : 2 + + const currentFrequency = this.clientModule.radioModule.getRadioFrequency(channel) + + if (currentFrequency === '0') { + return '' + } + + return currentFrequency + }) + + saltyChatExport('GetRadioVolume', () => { + return this.clientModule.radioModule.getRadioChannelVolume(1) + }) + + saltyChatExport('GetRadioSpeaker', () => { + console.warn('GetRadioSpeaker is not implemented in YaCA') + return false + }) + + saltyChatExport('GetMicClick', () => { + console.warn('GetMicClick is not implemented in YaCA') + return false + }) + + saltyChatExport('SetRadioChannel', (radioChannelName: string, primary: boolean) => { + const channel = primary ? 1 : 2 + const newRadioChannelName = radioChannelName === '' ? '0' : radioChannelName + + this.clientModule.radioModule.changeRadioFrequencyRaw(newRadioChannelName, channel) + }) + + saltyChatExport('SetRadioVolume', (volume: number) => { + this.clientModule.radioModule.changeRadioChannelVolumeRaw(volume, 1) + this.clientModule.radioModule.changeRadioChannelVolumeRaw(volume, 2) + }) + + saltyChatExport('SetRadioSpeaker', () => { + console.warn('SetRadioSpeaker is not implemented in YaCA') + }) + + saltyChatExport('SetMicClick', () => { + console.warn('SetMicClick is not implemented in YaCA') + }) + + saltyChatExport('GetPluginState', () => { + return this.currentPluginState + }) + } + + /** + * Handles the plugin state change. + * + * @param response - The last response code. + */ + handleChangePluginState(response: YacaPluginStates) { + let state = 0 + + switch (response) { + case YacaPluginStates.IN_EXCLUDED_CHANNEL: + state = 3 + break + case YacaPluginStates.IN_INGAME_CHANNEL: + state = 2 + break + case YacaPluginStates.CONNECTED: + state = 1 + break + case YacaPluginStates.WRONG_TS_SERVER: + case YacaPluginStates.OUTDATED_VERSION: + state = 0 + break + case YacaPluginStates.NOT_CONNECTED: + state = -1 + break + default: + return + } + + emit('SaltyChat_PluginStateChanged', state) + this.currentPluginState = state + } + + /** + * Sends the radio talking state. + */ + sendRadioTalkingState() { + emit('SaltyChat_RadioTrafficStateChanged', this.isPrimaryReceiving, this.isPrimarySending, this.isSecondaryReceiving, this.isSecondarySending) + } + + /** + * Handle radio talking state change. + * + * @param state - The state of the radio talking. + * @param channel - The radio channel. + */ + handleRadioTalkingStateChange(state: boolean, channel: number) { + if (channel === 1) { + this.isPrimarySending = state + } else { + this.isSecondarySending = state + } + + this.sendRadioTalkingState() + } + + /** + * Handle radio receiving state change. + * + * @param state - The state of the radio receiving. + * @param channel - The radio channel. + */ + handleRadioReceivingStateChange(state: boolean, channel: number) { + if (channel === 1) { + this.isPrimaryReceiving = state + } else { + this.isSecondaryReceiving = state + } + + this.sendRadioTalkingState() + } +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/index.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/index.ts new file mode 100644 index 000000000..57efb413e --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-client/src/index.ts @@ -0,0 +1,8 @@ +/// + +import { initCache } from './utils' +import { YaCAClientModule } from './yaca' + +initCache() + +new YaCAClientModule() diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/cache.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/cache.ts new file mode 100644 index 000000000..e71135166 --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/cache.ts @@ -0,0 +1,79 @@ +import type { ClientCache } from '@yaca-voice/types' + +const playerId = PlayerId() + +/** + * Cached values for the client. + */ +const cache: ClientCache = new Proxy( + { + playerId, + serverId: GetPlayerServerId(playerId), + ped: PlayerPedId(), + vehicle: false, + seat: false, + resource: GetCurrentResourceName(), + game: GetGameName() as 'fivem' | 'redm', + }, + { + set(target: ClientCache, key: keyof ClientCache, value: never) { + if (target[key] === value) return true + + target[key] = value + emit(`yaca:cache:${key}`, value) + return true + }, + get(target: ClientCache, key: keyof ClientCache) { + return target[key] + }, + }, +) + +/** + * Initializes the cache and starts updating it. + */ +function initCache() { + /** + * This function will update the cache every 100ms. + */ + const updateCache = () => { + const ped = PlayerPedId() + cache.ped = ped + + const vehicle = GetVehiclePedIsIn(ped, false) + + if (vehicle > 0) { + if (vehicle !== cache.vehicle) { + cache.seat = false + } + + cache.vehicle = vehicle + + if (!cache.seat || GetPedInVehicleSeat(vehicle, cache.seat) !== ped) { + for (let i = -1; i < GetVehicleMaxNumberOfPassengers(vehicle) - 1; i++) { + if (GetPedInVehicleSeat(vehicle, i) === ped) { + cache.seat = i + break + } + } + } + } else { + cache.vehicle = false + cache.seat = false + } + } + + setInterval(updateCache, 100) +} + +/** + * Listen for cache updates. + * + * @param key - The cache key to listen for. + * @param cb - The callback to execute when the cache updates. + */ +export const onCache = (key: keyof ClientCache, cb: (value: T) => void) => { + on(`yaca:cache:${key}`, cb) +} + +export { initCache, cache } diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/index.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/index.ts new file mode 100644 index 000000000..55058e291 --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/index.ts @@ -0,0 +1,40 @@ +import { cache } from './cache' + +export * from './cache' +export * from './props' +export * from './redm' +export * from './streaming' +export * from './vectors' +export * from './vehicle' +export * from './websocket' + +/** + * Rounds a float to a specified number of decimal places. + * Defaults to 2 decimal places if not provided. + * + * @param {number} num - The number to round. + * @param {number} decimalPlaces - The number of decimal places to round to. + * + * @returns {number} The rounded number. + */ +export function roundFloat(num: number, decimalPlaces = 17): number { + return Number.parseFloat(num.toFixed(decimalPlaces)) +} + +/** + * Convert camera rotation to direction vector. + * + * @returns {x: number, y: number, z: number} The direction vector. + */ +export function getCamDirection(): { x: number; y: number; z: number } { + const rotVector = GetGameplayCamRot(0) + const num = rotVector[2] * 0.0174532924 + const num2 = rotVector[0] * 0.0174532924 + const num3 = Math.abs(Math.cos(num2)) + + return { + x: roundFloat(-Math.sin(num) * num3), + y: roundFloat(Math.cos(num) * num3), + z: roundFloat(GetEntityForwardVector(cache.ped)[2]), + } +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/props.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/props.ts new file mode 100644 index 000000000..760b5e610 --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/props.ts @@ -0,0 +1,49 @@ +import { cache } from './cache' +import { requestModel } from './streaming' + +export const joaat = (input: string, ignore_casing = true) => { + input = !ignore_casing ? input.toLowerCase() : input + const length = input.length + + let hash: number + let i: number + + for (hash = i = 0; i < length; i++) { + hash += input.charCodeAt(i) + hash += hash << 10 + hash ^= hash >>> 6 + } + + hash += hash << 3 + hash ^= hash >>> 11 + hash += hash << 15 + + return hash >>> 0 +} + +/** + * Create a prop and attach it to the player. + * + * @param model - The model of the prop. + * @param boneId - The bone id to attach the prop to. + * @param offset - The offset of the prop. + * @param rotation - The rotation of the prop. + */ +export const createProp = async ( + model: string | number, + boneId: number, + offset: [number, number, number] = [0.0, 0.0, 0.0], + rotation: [number, number, number] = [0.0, 0.0, 0.0], +) => { + const modelHash = await requestModel(model) + if (!modelHash) return + + const [x, y, z] = GetEntityCoords(cache.ped, true) + const [ox, oy, oz] = offset + const [rx, ry, rz] = rotation + const object = CreateObject(modelHash, x, y, z, true, true, false) + SetEntityCollision(object, false, false) + AttachEntityToEntity(object, cache.ped, GetPedBoneIndex(cache.ped, boneId), ox, oy, oz, rx, ry, rz, true, false, false, true, 2, true) + + return object +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/redm.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/redm.ts new file mode 100644 index 000000000..fc5398cba --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/redm.ts @@ -0,0 +1,62 @@ +import { REDM_KEY_TO_HASH } from '../yaca' +import { requestAnimDict } from './streaming' + +/** + * Play a facial animation on a ped. + * + * @param ped - The ped to play the facial animation on. + * @param animName - The animation name to use. + * @param animDict - The animation dictionary to use. + */ +export const playRdrFacialAnim = async (ped: number, animName: string, animDict: string) => { + const loadedAnimDict = await requestAnimDict(animDict) + if (!loadedAnimDict) return + + SetFacialIdleAnimOverride(ped, animName, loadedAnimDict) +} + +/** + * Display a notification in RDR. + * + * @param text - The text to display. + * @param duration - The duration to display the notification for. + */ +export const displayRdrNotification = (text: string, duration: number) => { + // @ts-expect-error VarString is a redm native + const str = VarString(10, 'LITERAL_STRING', text) + + const struct1 = new DataView(new ArrayBuffer(96)) + struct1.setUint32(0, duration, true) + + const struct2 = new DataView(new ArrayBuffer(8 + 8)) + struct2.setBigUint64(8, BigInt(str), true) + + Citizen.invokeNative('0x049D5C615BD38BAD', struct1, struct2, 1) +} + +/** + * Register a keybind for RDR. + * + * @param key - The key to bind. + * @param onPressed - The function to call when the key is pressed. + * @param onReleased - The function to call when the key is released. + */ +export const registerRdrKeyBind = (key: string, onPressed?: () => void, onReleased?: () => void) => { + const keyHash = REDM_KEY_TO_HASH[key] + + if (!keyHash) { + console.error(`[YaCA] No key hash available for ${key}, please choose another keybind`) + return + } + + setTick(() => { + DisableControlAction(0, keyHash, true) + if (onPressed && IsDisabledControlJustPressed(0, keyHash)) { + onPressed() + } + + if (onReleased && IsDisabledControlJustReleased(0, keyHash)) { + onReleased() + } + }) +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/streaming.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/streaming.ts new file mode 100644 index 000000000..0b8eff2c5 --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/streaming.ts @@ -0,0 +1,58 @@ +import { waitFor } from '@yaca-voice/common' +import { joaat } from './props' + +/** + * Request an asset and wait for it to load. + * + * @param request - The function to request the asset + * @param hasLoaded - The function to check if the asset has loaded + * @param assetType - The type of the asset + * @param asset - The asset to request + * @param timeout - The timeout in ms + */ +async function streamingRequest( + request: (asset: T) => unknown, + hasLoaded: (asset: T) => boolean, + assetType: string, + asset: T, + timeout = 30000, +) { + if (hasLoaded(asset)) return asset + + request(asset) + + return waitFor( + () => { + if (hasLoaded(asset)) return asset + }, + `failed to load ${assetType} '${asset}' - this may be caused by\n- too many loaded assets\n- oversized, invalid, or corrupted assets`, + timeout, + ) +} + +/** + * Request a animation dictionary. + * + * @param animDict - The animation dictionary to request. + * @returns A promise that resolves to the animation dictionary once it is loaded. + * @throws Will throw an error if the animation dictionary is not valid or if the animation dictionary fails to load within the timeout. + */ +export const requestAnimDict = (animDict: string) => { + if (!DoesAnimDictExist(animDict)) throw new Error(`attempted to load invalid animDict '${animDict}'`) + + return streamingRequest(RequestAnimDict, HasAnimDictLoaded, 'animDict', animDict) +} + +/** + * Loads a model by its name or hash key. + * + * @param modelName - The name or hash key of the model to load. + * @returns A promise that resolves to the model hash key once the model is loaded. + * @throws Will throw an error if the model is not valid or if the model fails to load within the timeout. + */ +export const requestModel = (modelName: string | number) => { + if (typeof modelName !== 'number') modelName = joaat(modelName) + if (!IsModelValid(modelName)) throw new Error(`attempted to load invalid model '${modelName}'`) + + return streamingRequest(RequestModel, HasModelLoaded, 'model', modelName) +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/vectors.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/vectors.ts new file mode 100644 index 000000000..9f6ab4ff0 --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/vectors.ts @@ -0,0 +1,38 @@ +import { roundFloat } from './index' + +/** + * Calculate the distance between two points in 3D space + * + * @param firstPoint - The first point + * @param secondPoint - The second point + */ +export function calculateDistanceVec3(firstPoint: number[], secondPoint: number[]) { + return Math.sqrt((firstPoint[0] - secondPoint[0]) ** 2 + (firstPoint[1] - secondPoint[1]) ** 2 + (firstPoint[2] - secondPoint[2]) ** 2) +} + +/** + * Calculate the distance between two points in 2D space + * + * @param firstPoint - The first point + * @param secondPoint - The second point + */ +export function calculateDistanceVec2(firstPoint: number[], secondPoint: number[]) { + return Math.sqrt((firstPoint[0] - secondPoint[0]) ** 2 + (firstPoint[1] - secondPoint[1]) ** 2) +} + +/** + * Convert an array of numbers to an object with x, y, and z properties + * + * @param array - The array to convert + */ +export function convertNumberArrayToXYZ(array: number[]): { + x: number + y: number + z: number +} { + return { + x: roundFloat(array[0]), + y: roundFloat(array[1]), + z: roundFloat(array[2]), + } +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/vehicle.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/vehicle.ts new file mode 100644 index 000000000..98533d211 --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/vehicle.ts @@ -0,0 +1,93 @@ +/** + * Checks if the vehicle has a window. + * + * @param vehicle - The vehicle. + * @param windowId - The window ID to check. + * @returns {boolean} - Whether the vehicle has a window. + */ +export function hasWindow(vehicle: number, windowId: number): boolean { + switch (windowId) { + case 0: + return GetEntityBoneIndexByName(vehicle, 'window_lf') !== -1 + case 1: + return GetEntityBoneIndexByName(vehicle, 'window_rf') !== -1 + case 2: + return GetEntityBoneIndexByName(vehicle, 'window_lr') !== -1 + case 3: + return GetEntityBoneIndexByName(vehicle, 'window_rr') !== -1 + default: + return false + } +} + +/** + * Checks if the vehicle has a door. + * + * @param vehicle - The vehicle. + * @param doorId - The door ID to check. + * @returns {boolean} - Whether the vehicle has a door. + */ +export function hasDoor(vehicle: number, doorId: number): boolean { + switch (doorId) { + case 0: + return GetEntityBoneIndexByName(vehicle, 'door_dside_f') !== -1 + case 1: + return GetEntityBoneIndexByName(vehicle, 'door_pside_f') !== -1 + case 2: + return GetEntityBoneIndexByName(vehicle, 'door_dside_r') !== -1 + case 3: + return GetEntityBoneIndexByName(vehicle, 'door_pside_r') !== -1 + case 4: + return GetEntityBoneIndexByName(vehicle, 'bonnet') !== -1 + case 5: + return GetEntityBoneIndexByName(vehicle, 'boot') !== -1 + default: + return false + } +} + +/** + * Checks if the vehicle has an opening. + * + * @param vehicle - The vehicle. + * @returns {boolean} - Whether the vehicle has an opening. + */ +export function vehicleHasOpening(vehicle: number): boolean { + const doors = [] + for (let i = 0; i < 6; i++) { + if (i === 4 || !hasDoor(vehicle, i)) continue + doors.push(i) + } + + if (doors.length === 0) return true + for (const door of doors) { + const doorAngle = GetVehicleDoorAngleRatio(vehicle, door) + if (doorAngle > 0) { + return true + } + + if (IsVehicleDoorDamaged(vehicle, door)) { + return true + } + } + + if (!AreAllVehicleWindowsIntact(vehicle)) { + return true + } + + for (let i = 0; i < 8 /* max windows */; i++) { + const hasWindows = hasWindow(vehicle, i) + if (hasWindows && !IsVehicleWindowIntact(vehicle, i)) { + return true + } + } + + if (IsVehicleAConvertible(vehicle, false)) { + const roofState = GetConvertibleRoofState(vehicle) + if (roofState !== 0) { + return true + } + } + + return false +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/websocket.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/websocket.ts new file mode 100644 index 000000000..87ac1c48d --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/websocket.ts @@ -0,0 +1,87 @@ +import { sleep } from '@yaca-voice/common' +import EventEmitter2 from 'eventemitter2' + +/** + * The WebSocket class handles the communication between the nui and the client. + */ +export class WebSocket extends EventEmitter2 { + public readyState = 0 + nuiReady = false + initialized = false + + /** + * Creates an instance of the WebSocket class. + */ + constructor() { + super() + + RegisterNuiCallbackType('YACA_OnMessage') + RegisterNuiCallbackType('YACA_OnConnected') + RegisterNuiCallbackType('YACA_OnDisconnected') + + on('__cfx_nui:YACA_OnMessage', (data: object, cb: (data: unknown) => void) => { + this.emit('message', data) + cb({}) + }) + + on('__cfx_nui:YACA_OnConnected', (_: unknown, cb: (data: unknown) => void) => { + this.readyState = 1 + this.emit('open') + cb({}) + }) + + on('__cfx_nui:YACA_OnDisconnected', (data: { code: number; reason: string }, cb: (data: unknown) => void) => { + this.readyState = 3 + this.emit('close', data.code, data.reason) + cb({}) + }) + } + + /** + * Sends the message to the nui that the websocket should connect. + */ + async start() { + while (!this.nuiReady) { + await sleep(100) + } + + SendNuiMessage( + JSON.stringify({ + action: 'connect', + }), + ) + } + + /** + * Sends the message to the nui that the websocket should disconnect. + * + * @param data - The data to send. + */ + send(data: object) { + if (this.readyState !== 1) { + return + } + + SendNuiMessage( + JSON.stringify({ + action: 'command', + data, + }), + ) + } + + /** + * Sends the message to the nui that the websocket should disconnect. + */ + close() { + if (this.readyState === 3) { + return + } + + SendNuiMessage( + JSON.stringify({ + action: 'close', + }), + ) + } +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/data.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/data.ts new file mode 100644 index 000000000..99e46b357 --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/data.ts @@ -0,0 +1,97 @@ +const localLipSyncAnimations: Record<'fivem' | 'redm', Record> = { + fivem: { + true: { + name: 'mic_chatter', + dict: 'mp_facial', + }, + false: { + name: 'mood_normal_1', + dict: 'facials@gen_male@variations@normal', + }, + }, + redm: { + true: { + name: 'mood_talking_normal', + dict: 'face_human@gen_male@base', + }, + false: { + name: 'mood_normal', + dict: 'face_human@gen_male@base', + }, + }, +} + +const REDM_KEY_TO_HASH: Record = { + // Letters + A: 0x7065027d, + B: 0x4cc0e2fe, + C: 0x9959a6f0, + D: 0xb4e465b4, + E: 0xcefd9220, + F: 0xb2f377e8, + G: 0x760a9c6f, + H: 0x24978a28, + I: 0xc1989f95, + J: 0xf3830d8e, + K: null, + L: 0x80f28e95, + M: 0xe31c6a41, + N: 0x4bc9dabb, // (Push to Talk) + O: 0xf1301666, + P: 0xd82e0bd2, + Q: 0xde794e3e, + R: 0xe30cd707, + S: 0xd27782e3, + T: null, + U: 0xd8f73058, + V: 0x7f8d09b8, + W: 0x8fd015d8, + X: 0x8cc9cd42, + Y: null, + Z: 0x26e9dc00, + + // Symbol Keys + RIGHTBRACKET: 0xa5bdcd3c, + LEFTBRACKET: 0x430593aa, + + // Mouse buttons + MOUSE1: 0x07ce1e61, + MOUSE2: 0xf84fa74f, + MOUSE3: 0xcee12b50, + MWUP: 0x3076e97c, + + // Modifier Keys + CTRL: 0xdb096b85, + TAB: 0xb238fe0b, + SHIFT: 0x8ffc75d6, + SPACEBAR: 0xd9d0e1c0, + ENTER: 0xc7b5340a, + BACKSPACE: 0x156f7119, + LALT: 0x8aaa0ad4, + DEL: 0x4af4d473, + PGUP: 0x446258b6, + PGDN: 0x3c3dd371, + + // Function Keys + F1: 0xa8e3f467, + F4: 0x1f6d95e5, + F6: 0x3c0a40f2, + + // Number Keys + '1': 0xe6f612e4, + '2': 0x1ce6d9eb, + '3': 0x4f49cc4c, + '4': 0x8f9f9e58, + '5': 0xab62e997, + '6': 0xa1fde2a6, + '7': 0xb03a913b, + '8': 0x42385422, + + // Arrow Keys + DOWN: 0x05ca7c52, + UP: 0x6319db71, + LEFT: 0xa65ebab4, + RIGHT: 0xdeb34313, +} + +export { localLipSyncAnimations, REDM_KEY_TO_HASH } diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/index.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/index.ts new file mode 100644 index 000000000..0e576c9aa --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/index.ts @@ -0,0 +1,6 @@ +export * from './data' +export * from './intercom' +export * from './main' +export * from './megaphone' +export * from './phone' +export * from './radio' diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/intercom.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/intercom.ts new file mode 100644 index 000000000..516ea7e3a --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/intercom.ts @@ -0,0 +1,59 @@ +import { CommDeviceMode, YacaFilterEnum, type YacaPlayerData } from '@yaca-voice/types' +import type { YaCAClientModule } from './main' + +/** + * The intercom module for the client. + */ +export class YaCAClientIntercomModule { + clientModule: YaCAClientModule + + /** + * Creates an instance of the intercom module. + * + * @param clientModule - The client module. + */ + constructor(clientModule: YaCAClientModule) { + this.clientModule = clientModule + + this.registerEvents() + } + + /** + * Register the intercom events. + */ + registerEvents() { + /** + * Handles the "client:yaca:addRemovePlayerIntercomFilter" server event. + * + * @param {number[] | number} playerIDs - The IDs of the players to be added or removed from the intercom filter. + * @param {boolean} state - The state indicating whether to add or remove the players. + */ + onNet('client:yaca:addRemovePlayerIntercomFilter', (playerIDs: number | number[], state: boolean) => { + if (!Array.isArray(playerIDs)) { + playerIDs = [playerIDs] + } + + const playersToAddRemove: Set = new Set() + for (const playerID of playerIDs) { + const player = this.clientModule.getPlayerByID(playerID) + if (!player) { + continue + } + playersToAddRemove.add(player) + } + + if (playersToAddRemove.size < 1) { + return + } + this.clientModule.setPlayersCommType( + Array.from(playersToAddRemove), + YacaFilterEnum.INTERCOM, + state, + undefined, + undefined, + CommDeviceMode.TRANSCEIVER, + CommDeviceMode.TRANSCEIVER, + ) + }) + } +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/main.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/main.ts new file mode 100644 index 000000000..fbc0a93a7 --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/main.ts @@ -0,0 +1,1733 @@ +import { + clamp, + GLOBAL_ERROR_LEVEL_STATE_NAME, + initLocale, + LIP_SYNC_STATE_NAME, + loadConfig, + locale, + MEGAPHONE_STATE_NAME, + VOICE_RANGE_STATE_NAME, +} from '@yaca-voice/common' +import { + CommDeviceMode, + type DataObject, + defaultSharedConfig, + defaultTowerConfig, + type YacaClient, + YacaFilterEnum, + YacaNotificationType, + type YacaPlayerData, + type YacaPluginPlayerData, + YacaPluginStates, + type YacaProtocol, + type YacaResponse, + type YacaSharedConfig, + type YacaSoundStateMessage, + type YacaStereoMode, + type YacaTowerConfig, +} from '@yaca-voice/types' +import { YaCAClientSaltyChatBridge } from '../bridge/saltychat' +import { + cache, + calculateDistanceVec3, + convertNumberArrayToXYZ, + displayRdrNotification, + getCamDirection, + joaat, + playRdrFacialAnim, + registerRdrKeyBind, + vehicleHasOpening, + WebSocket, +} from '../utils' +import { localLipSyncAnimations } from './data' +import { YaCAClientIntercomModule } from './intercom' +import { YaCAClientMegaphoneModule } from './megaphone' +import { YaCAClientPhoneModule } from './phone' +import { YaCAClientRadioModule } from './radio' + +/** + * The YaCA client module. + * This module is responsible for handling the client side of the voice plugin. + * It also handles the websocket connection to the voice plugin. + */ +export class YaCAClientModule { + websocket: WebSocket + + sharedConfig: YacaSharedConfig + towerConfig: YacaTowerConfig + + mufflingVehicleWhitelistHash = new Set() + allPlayers = new Map() + firstConnect = true + + radioModule: YaCAClientRadioModule + phoneModule: YaCAClientPhoneModule + megaphoneModule: YaCAClientMegaphoneModule + intercomModule: YaCAClientIntercomModule + + saltyChatBridge?: YaCAClientSaltyChatBridge + + canChangeVoiceRange = true + defaultVoiceRange = 1 + maxVoiceRange = -1 + rangeIndex: number + rangeInterval: CitizenTimer | null = null + visualVoiceRangeTimeout: CitizenTimer | null = null + visualVoiceRangeTick: CitizenTimer | null = null + voiceRangeViaMouseWheelTick: CitizenTimer | null = null + + isTalking = false + useWhisper = false + spectatingPlayer: number | false = false + notificationTimeout: Map = new Map() + + isMicrophoneMuted = false + isMicrophoneDisabled = false + isSoundMuted = false + isSoundDisabled = false + + currentlyPhoneSpeakerApplied = new Set() + currentlySendingPhoneSpeakerSender = new Set() + phoneHearNearbyPlayer = new Set() + + isFiveM = cache.game === 'fivem' + isRedM = cache.game === 'redm' + + private currentPluginState: YacaPluginStates + + /** + * Sets the current plugin state and emits an event. + * + * @param state - The new plugin state. + */ + setCurrentPluginState(state: YacaPluginStates) { + if (this.currentPluginState === state) { + return + } + + this.currentPluginState = state + emit('yaca:external:pluginStateChanged', state) + + this.saltyChatBridge?.handleChangePluginState(state) + } + + /** + * Sends a radar notification. + * + * @param {string} message - The message to be sent in the notification. + * @param {YacaNotificationType} type - The type of the notification, e.g. error, inform, success. + */ + notification(message: string, type: YacaNotificationType) { + if (this.sharedConfig.notifications.oxLib) { + emit('ox_lib:notify', { + id: 'yaca', + title: 'YaCA', + description: message, + type, + }) + } + + if (this.sharedConfig.notifications.okoknotify && GetResourceState('okokNotify') === 'started') { + const okType = type === YacaNotificationType.INFO ? 'info' : type + exports.okokNotify.Alert('YaCA', message, 2000, okType) + } + + if (this.sharedConfig.notifications.gta) { + if (this.isFiveM) { + BeginTextCommandThefeedPost('STRING') + AddTextComponentSubstringPlayerName(`YaCA: ${message}`) + if (type === YacaNotificationType.ERROR) { + ThefeedSetNextPostBackgroundColor(6) + } + EndTextCommandThefeedPostTicker(false, false) + } else { + console.warn('[YaCA] GTA notification is only available in FiveM.') + } + } + + if (this.sharedConfig.notifications.redm) { + if (this.isRedM) { + displayRdrNotification(`YaCA: ${message}`, 2000) + } else { + console.warn('[YaCA] RedM notification is only available in RedM.') + } + } + + if (this.sharedConfig.notifications.own) { + emit('yaca:external:notification', message, type) + } + } + + constructor() { + this.sharedConfig = loadConfig('config/shared.json5', defaultSharedConfig) + this.towerConfig = loadConfig('config/tower.json5', defaultTowerConfig) + initLocale(this.sharedConfig.locale) + + this.rangeIndex = this.sharedConfig.voiceRange.defaultIndex + if (this.sharedConfig.voiceRange.ranges[this.rangeIndex]) { + this.defaultVoiceRange = this.sharedConfig.voiceRange.ranges[this.rangeIndex] + } else { + this.defaultVoiceRange = 1 + this.rangeIndex = 0 + this.sharedConfig.voiceRange.ranges = [1] + + console.error('[YaCA] Default voice range is not set correctly in the config.') + } + + if (this.isFiveM) { + for (const vehicleModel of this.sharedConfig.mufflingSettings.vehicleMuffling.vehicleWhitelist) { + this.mufflingVehicleWhitelistHash.add(joaat(vehicleModel)) + } + } + + this.websocket = new WebSocket() + this.setCurrentPluginState(YacaPluginStates.NOT_CONNECTED) + + /** + * Register the NUI callback types. + */ + RegisterNuiCallbackType('YACA_OnNuiReady') + on('__cfx_nui:YACA_OnNuiReady', (_: unknown, cb: (data: unknown) => void) => { + this.websocket.nuiReady = true + + if (this.sharedConfig.autoConnectOnJoin) { + setTimeout(() => { + emitNet('server:yaca:nuiReady') + }, 5000) + } + + // skipcq: JS-0255 + cb({}) + }) + + this.registerExports() + this.registerEvents() + if (this.isFiveM) { + this.registerKeybindings() + } else if (this.isRedM) { + this.registerRdrKeybindings() + } + + this.intercomModule = new YaCAClientIntercomModule(this) + this.megaphoneModule = new YaCAClientMegaphoneModule(this) + this.phoneModule = new YaCAClientPhoneModule(this) + this.radioModule = new YaCAClientRadioModule(this) + + if (!this.sharedConfig.useLocalLipSync) { + /** + * Add a state bag change handler for the lip sync state bag. + * Which is used to override the talking state of the player. + */ + AddStateBagChangeHandler(LIP_SYNC_STATE_NAME, '', (bagName: string, _: string, value: boolean, __: number) => { + const playerId = GetPlayerFromStateBagName(bagName) + if (playerId === 0) { + return + } + + SetPlayerTalkingOverride(playerId, value) + }) + + /** + * Add a state bag change handler for the global error level state bag. + * Which is used to override the global error level. + */ + AddStateBagChangeHandler(GLOBAL_ERROR_LEVEL_STATE_NAME, '', (_bagName: string, _key: string, _value: number, __: number) => { + setImmediate(() => { + this.phoneModule.enablePhoneCall(Array.from(this.phoneModule.inCallWith), true) + }) + }) + } + + if (this.sharedConfig.saltyChatBridge) { + this.radioModule.secondaryRadioChannel = 2 + this.saltyChatBridge = new YaCAClientSaltyChatBridge(this) + } + + console.log('[Client] YaCA Client loaded.') + } + + registerExports() { + /** + * Get the current voice range. + * + * @returns {number} The current voice range. + */ + exports('getVoiceRange', () => this.getVoiceRange()) + + /** + * Get all voice ranges. + * + * @returns {number[]} All available voice ranges. + */ + exports('getVoiceRanges', () => this.sharedConfig.voiceRange.ranges) + + /** + * Change the voice range to the next range. + * + * @param {boolean} increase - If the voice range should be increased or decreased. + */ + exports('changeVoiceRange', (increase = true) => { + this.changeVoiceRange(increase) + }) + + /** + * Set the voice range to the given value. + * + * @param {number} range - The voice range to set + */ + exports('setVoiceRange', (range: number) => { + this.setVoiceRange(range) + }) + + /** + * Enable or disable the voice range change function. + * + * @param {boolean} enable - If the voice range change function should be enabled or disabled. + */ + exports('setVoiceRangeChangeAllowedState', (enable: boolean) => { + this.canChangeVoiceRange = enable + }) + + /** + * Get the voice range change allowed state. + * + * @returns {boolean} The voice range change allowed state. + */ + exports('getVoiceRangeChangeAllowedState', () => this.canChangeVoiceRange) + + /** + * Set the maximum voice range. + * + * @param {number} maxVoiceRange - The maximum voice range, -1 to disable. + */ + exports('setMaxVoiceRange', (maxVoiceRange: number) => { + this.maxVoiceRange = maxVoiceRange + }) + + /** + * Get the maximum allowed voice range. + * + * @returns {number} The maximum voice range. + */ + exports('getMaxVoiceRange', () => this.maxVoiceRange) + + /** + * Get microphone mute state. + * + * @returns {boolean} The microphone mute state. + */ + exports('getMicrophoneMuteState', () => this.isMicrophoneMuted) + + /** + * Get microphone disabled state. + * + * @returns {boolean} The microphone disabled state. + */ + exports('getMicrophoneDisabledState', () => this.isMicrophoneDisabled) + + /** + * Get sound mute state. + * + * @returns {boolean} + */ + exports('getSoundMuteState', () => this.isSoundMuted) + + /** + * Get sound disabled state. + * + * @returns {boolean} + */ + exports('getSoundDisabledState', () => this.isSoundDisabled) + + /** + * Get the plugin state. + * + * @returns {YacaPluginStates} The current plugin state. + */ + exports('getPluginState', () => this.currentPluginState ?? YacaPluginStates.NOT_CONNECTED) + + /** + * Get the global error level. + * + * @returns {number} The global error level. + */ + exports('getGlobalErrorLevel', () => GlobalState[GLOBAL_ERROR_LEVEL_STATE_NAME] ?? 0) + + /** + * Set the player that should be spectated. + * + * @param {number | false} player - The player to be spectated. + */ + exports('setSpectatingPlayer', (player: number | false) => { + this.spectatingPlayer = player + }) + + /** + * Get the player that is currently spectated. + * + * @returns {number | false} The player that is currently spectated. False if no player is spectated. + */ + exports('getSpectatingPlayer', () => this.spectatingPlayer) + + /** + * Set the voice range marker color. + * + * @param {number} r - The red component of the color. + * @param {number} g - The green component of the color. + * @param {number} b - The blue component of the color. + * @param {number} a - The alpha component of the color. + */ + exports('setVoiceRangeMarkerColor', (r: number, g: number, b: number, a: number) => { + if (typeof r !== 'number' || typeof g !== 'number' || typeof b !== 'number' || typeof a !== 'number') { + console.error('[YaCA] Invalid color value in setVoiceRangeMarkerColor') + return + } + + this.sharedConfig.voiceRange.markerColor.r = r + this.sharedConfig.voiceRange.markerColor.g = g + this.sharedConfig.voiceRange.markerColor.b = b + this.sharedConfig.voiceRange.markerColor.a = a + }) + + /** + * Get the voice range marker color. + * + * @returns {number[]} The voice range marker color as an array of [r, g, b, a]. + */ + exports('getVoiceRangeMarkerColor', () => { + const { r, g, b, a } = this.sharedConfig.voiceRange.markerColor + return [r, g, b, a] + }) + + /** + * Reset the voice range marker color to the default value. + */ + exports('resetVoiceRangeMarkerColor', () => { + const defaultColor = defaultSharedConfig.voiceRange.markerColor + this.sharedConfig.voiceRange.markerColor.r = defaultColor.r + this.sharedConfig.voiceRange.markerColor.g = defaultColor.g + this.sharedConfig.voiceRange.markerColor.b = defaultColor.b + this.sharedConfig.voiceRange.markerColor.a = defaultColor.a + }) + } + + /** + * Registers the keybindings for the plugin. + * This is only available in FiveM. + */ + registerKeybindings() { + if (this.sharedConfig.keyBinds.increaseVoiceRange !== false) { + /** + * Registers the "yaca:increaseVoiceRange" command and keybinding. + * This command is used to change the voice range. + */ + RegisterCommand( + 'yaca:increaseVoiceRange', + () => { + this.changeVoiceRange(true) + }, + false, + ) + RegisterKeyMapping('yaca:increaseVoiceRange', locale('change_voice_range_increase'), 'keyboard', this.sharedConfig.keyBinds.increaseVoiceRange) + } + + if (this.sharedConfig.keyBinds.decreaseVoiceRange !== false) { + /** + * Registers the "yaca:decreaseVoiceRange" command and keybinding. + * This command is used to change the voice range. + */ + RegisterCommand( + 'yaca:decreaseVoiceRange', + () => { + this.changeVoiceRange(false) + }, + false, + ) + RegisterKeyMapping('yaca:decreaseVoiceRange', locale('change_voice_range_decrease'), 'keyboard', this.sharedConfig.keyBinds.decreaseVoiceRange) + } + + if (this.sharedConfig.keyBinds.voiceRangeWithMouseWheel !== false) { + /** + * Registers the "+yaca:changeVoiceRangeWithMousewheel" command and keybinding. + * This command is used to change the voice range. + */ + RegisterCommand( + '+yaca:changeVoiceRangeWithMousewheel', + () => { + this.voiceRangeViaMouseWheelTick = setInterval(() => { + this.handleVoiceRangeViaMouseWheel() + }) + }, + false, + ) + + RegisterCommand( + '-yaca:changeVoiceRangeWithMousewheel', + () => { + if (this.voiceRangeViaMouseWheelTick) { + clearInterval(this.voiceRangeViaMouseWheelTick) + this.voiceRangeViaMouseWheelTick = null + } + }, + false, + ) + + RegisterKeyMapping( + '+yaca:changeVoiceRangeWithMousewheel', + locale('change_voice_range_via_mousewheel'), + 'keyboard', + this.sharedConfig.keyBinds.voiceRangeWithMouseWheel, + ) + } + } + + /** + * Registers the keybindings for RedM. + * This is only available in RedM. + */ + registerRdrKeybindings() { + if (this.sharedConfig.keyBinds.increaseVoiceRange !== false) { + /** + * Registers the keybinding for changing the voice Range. + */ + registerRdrKeyBind(this.sharedConfig.keyBinds.increaseVoiceRange, () => { + this.changeVoiceRange() + }) + } + + if (this.sharedConfig.keyBinds.decreaseVoiceRange !== false) { + /** + * Registers the keybinding for changing the voice Range. + */ + registerRdrKeyBind(this.sharedConfig.keyBinds.decreaseVoiceRange, () => { + this.changeVoiceRange(false) + }) + } + + if (this.sharedConfig.keyBinds.voiceRangeWithMouseWheel !== false) { + /** + * Registers the "+yaca:changeVoiceRangeWithScroll" command and keybinding. + * This command is used to change the voice range. + */ + + registerRdrKeyBind( + this.sharedConfig.keyBinds.voiceRangeWithMouseWheel, + () => { + this.voiceRangeViaMouseWheelTick = setInterval(() => { + this.handleVoiceRangeViaMouseWheel() + }) + }, + () => { + if (this.voiceRangeViaMouseWheelTick) { + clearInterval(this.voiceRangeViaMouseWheelTick) + this.voiceRangeViaMouseWheelTick = null + } + }, + ) + } + } + + /** + * Registers the events for the plugin. + */ + registerEvents() { + /** + * Handles the "onPlayerJoining" server event. + * + * @param {number} target - The ID of the target. + */ + onNet('onPlayerJoining', (target: number) => { + const player = this.getPlayerByID(target) + if (!player) { + return + } + + const frequency = this.radioModule?.playersWithShortRange.get(target) + if (frequency) { + const channel = this.radioModule?.findRadioChannelByFrequency(frequency) + if (channel) { + this.setPlayersCommType( + player, + YacaFilterEnum.RADIO, + true, + channel, + undefined, + CommDeviceMode.RECEIVER, + CommDeviceMode.SENDER, + GlobalState[GLOBAL_ERROR_LEVEL_STATE_NAME] ?? undefined, + ) + this.saltyChatBridge?.handleRadioReceivingStateChange(true, channel) + } + } + }) + + /** + * Handles the "onPlayerDropped" server event. + * + * @param {number} target - The ID of the target. + */ + onNet('onPlayerDropped', (target: number) => { + const player = this.getPlayerByID(target) + if (!player) { + return + } + + this.phoneModule.removePhoneSpeakerFromEntity(target) + + const frequency = this.radioModule?.playersWithShortRange.get(target) + if (frequency) { + const channel = this.radioModule?.findRadioChannelByFrequency(frequency) + if (channel) { + this.setPlayersCommType( + player, + YacaFilterEnum.RADIO, + false, + channel, + undefined, + CommDeviceMode.RECEIVER, + CommDeviceMode.SENDER, + GlobalState[GLOBAL_ERROR_LEVEL_STATE_NAME] ?? undefined, + ) + + if (this.saltyChatBridge) { + const inRadio = this.radioModule?.playersInRadioChannel.get(channel) + if (inRadio) { + const inRadioArray = [...inRadio].filter((id) => id !== target) + const state = inRadioArray.length > 0 + this.saltyChatBridge.handleRadioReceivingStateChange(state, channel) + } + } + } + } + }) + + /** + * Handles the "onResourceStop" event. + * + * @param {string} resourceName - The name of the resource that has started. + */ + on('onResourceStop', (resourceName: string) => { + if (cache.resource !== resourceName) { + return + } + + if (this.websocket.initialized) { + this.websocket.close() + } + }) + + /** + * Handles the "client:yaca:init" server event. + * + * @param {DataObject} dataObj - The data object to be initialized. + */ + onNet('client:yaca:init', async (dataObj: DataObject) => { + if (this.rangeInterval) { + clearInterval(this.rangeInterval) + this.rangeInterval = null + } + + if (!this.websocket.initialized) { + this.websocket.initialized = true + + this.websocket.on('message', (msg: string) => { + this.handleResponse(msg) + }) + + this.websocket.on('close', (code: number, reason: string) => { + this.setCurrentPluginState(YacaPluginStates.NOT_CONNECTED) + + console.error('[YACA-Websocket]: client disconnected', code, reason) + }) + + this.websocket.on('open', () => { + this.setCurrentPluginState(YacaPluginStates.CONNECTED) + + if (this.firstConnect) { + this.initRequest(dataObj) + this.firstConnect = false + } else { + emitNet('server:yaca:wsReady') + } + + console.log('[YACA-Websocket]: Successfully connected to the voice plugin') + }) + + await this.websocket.start() + } + + if (this.firstConnect) { + return + } + + this.initRequest(dataObj) + }) + + /** + * Handles the "client:yaca:disconnect" server event. + * + * @param {number} remoteId - The remote ID of the player to be disconnected. + * + */ + onNet('client:yaca:disconnect', (remoteId: number) => { + this.phoneModule.handleDisconnect(remoteId) + this.allPlayers.delete(remoteId) + }) + + /** + * Handles the "client:yaca:addPlayers" server event. + * + * @param {DataObject | DataObject[]} dataObjects - The data object or objects to be added. + */ + onNet('client:yaca:addPlayers', (dataObjects: DataObject | DataObject[]) => { + if (!Array.isArray(dataObjects)) { + dataObjects = [dataObjects] + } + + const newPlayers: number[] = [] + for (const dataObj of dataObjects) { + if (!dataObj || typeof dataObj.clientId === 'undefined' || typeof dataObj.playerId === 'undefined') { + continue + } + + const currentData = this.getPlayerByID(dataObj.playerId) + + this.allPlayers.set(dataObj.playerId, { + remoteID: dataObj.playerId, + clientId: dataObj.clientId, + forceMuted: dataObj.forceMuted || false, + phoneCallMemberIds: currentData?.phoneCallMemberIds || undefined, + mutedOnPhone: dataObj.mutedOnPhone || false, + }) + + newPlayers.push(dataObj.playerId) + } + + this.phoneModule.reestablishCalls(newPlayers) + }) + + /** + * Handles the "client:yaca:muteTarget" server event. + * + * @param {number} target - The target to be muted. + * @param {boolean} muted - The mute status. + */ + onNet('client:yaca:muteTarget', (target: number, muted: boolean) => { + const player = this.getPlayerByID(target) + if (!player) return + + player.forceMuted = muted + }) + + /** + * Handles the "client:yaca:changeOwnVoiceRange" server event. + * + * @param {number} range - The new voice range. + */ + onNet('client:yaca:changeVoiceRange', (range: number) => { + emit('yaca:external:voiceRangeUpdate', range, this.rangeIndex) + // SaltyChat bridge + if (this.saltyChatBridge) { + emit('SaltyChat_VoiceRangeChanged', range.toFixed(1), this.rangeIndex, this.sharedConfig.voiceRange.ranges.length) + } + }) + + /** + * Handles the "client:yaca:notification" server event. + * + * @param {string} message - The message to be sent in the notification. + * @param {YacaNotificationType} type - The type of the notification, e.g. error, inform, success. + */ + onNet('client:yaca:notification', (message: string, type: YacaNotificationType) => { + this.notification(message, type) + }) + + /** + * Handles the "txcl:spectate:start" server event. + * + * @param {number} targetServerId - The ID of the target server that is spectated. + */ + onNet('txcl:spectate:start', (targetServerId: number) => { + this.spectatingPlayer = targetServerId + }) + + /** + * Handles the "txcl:spectate:stop" server event. + */ + onNet('client:yaca:txadmin:stopspectate', () => { + this.spectatingPlayer = false + }) + } + + /** + * Get the player by remote ID. + * + * @param remoteId The remote ID of the player. + */ + getPlayerByID(remoteId: number) { + return this.allPlayers.get(remoteId) + } + + /** + * Get the player by client ID. + * + * @param clientId The client ID (TeamSpeak) of the player. + * @returns The player data. + */ + getPlayerByClientId(clientId: number) { + for (const player of this.allPlayers.values()) { + if (player.clientId === clientId) { + return player + } + } + + return null + } + + /** + * Initializes the plugin. + * + * @param {DataObject} dataObj - The data object to initialize the plugin with. + */ + initRequest(dataObj: DataObject) { + if ( + !dataObj || + !dataObj.suid || + typeof dataObj.chid !== 'number' || + !dataObj.deChid || + !dataObj.ingameName || + typeof dataObj.channelPassword === 'undefined' + ) { + console.log('[YACA-Websocket]: Error while initializing plugin') + this.notification(locale('connect_error'), YacaNotificationType.ERROR) + return + } + + this.sendWebsocket({ + base: { request_type: 'INIT' }, + server_guid: dataObj.suid, + ingame_name: dataObj.ingameName, + ingame_channel: dataObj.chid, + default_channel: dataObj.deChid, + ingame_channel_password: dataObj.channelPassword, + excluded_channels: dataObj.excludeChannels, + muffling_range: this.sharedConfig.mufflingSettings.mufflingRange, + build_type: this.sharedConfig.buildType, + unmute_delay: this.sharedConfig.unmuteDelay, + operation_mode: dataObj.useWhisper ? 1 : 0, + }) + + this.useWhisper = dataObj.useWhisper ?? false + } + + /** + * Checks if the plugin is initialized. + * + * @returns {boolean} Returns true if the plugin is initialized, false otherwise. + */ + isPluginInitialized(silent = false): boolean { + const initialized = Boolean(this.getPlayerByID(cache.serverId)) + + if (!initialized && !silent) { + this.notification(locale('plugin_not_initialized'), YacaNotificationType.ERROR) + } + + return initialized + } + + /** + * Sends a message to the voice plugin via websocket. + * + * @param {object} msg - The message to be sent. + */ + sendWebsocket(msg: object) { + if (!this.websocket) { + console.error('[Voice-Websocket]: No websocket created') + return + } + + this.websocket.send(msg) + } + + /** + * Handles messages from the voice plugin. + * + * @param {string} payload - The response from the voice plugin. + */ + handleResponse(payload: string) { + if (!payload) { + return + } + + let parsedPayload: YacaResponse + + try { + parsedPayload = JSON.parse(payload) + } catch (e) { + console.error('[YaCA-Websocket]: Error while parsing message: ', e) + return + } + + switch (parsedPayload.code) { + case 'OK': + if (parsedPayload.requestType === 'JOIN') { + const clientId = Number.parseInt(parsedPayload.message) + emitNet('server:yaca:addPlayer', clientId) + + if (this.rangeInterval) { + clearInterval(this.rangeInterval) + this.rangeInterval = null + } + + this.rangeInterval = setInterval(this.calcPlayers.bind(this), 250) + + // Set radio settings on reconnect only, else on first opening + if (this.radioModule.radioInitialized) { + this.radioModule.initRadioSettings() + } + + emit('yaca:external:pluginInitialized', clientId) + return + } + + return + case 'TALK_STATE': + this.handleTalkState(parsedPayload) + return + case 'SOUND_STATE': + this.handleSoundState(parsedPayload) + return + case 'OTHER_TALK_STATE': + this.handleOtherTalkState(parsedPayload) + return + case 'MOVED_CHANNEL': + this.handleMovedChannel(parsedPayload.message) + return + case 'WRONG_TS_SERVER': { + this.setCurrentPluginState(YacaPluginStates.WRONG_TS_SERVER) + + const currentTimeout = this.notificationTimeout.get('WRONG_TS_SERVER') + if (currentTimeout && currentTimeout > Date.now()) { + return + } + + this.notificationTimeout.set('WRONG_TS_SERVER', Date.now() + 10000) + this.notification(locale('wrong_ts_server') ?? 'You are connected to the wrong teamspeak server!', YacaNotificationType.ERROR) + return + } + case 'OUTDATED_VERSION': + this.setCurrentPluginState(YacaPluginStates.OUTDATED_VERSION) + this.notification( + locale('outdated_plugin', parsedPayload.message) ?? `Your plugin is outdated, please update to version ${parsedPayload.message}!`, + YacaNotificationType.ERROR, + ) + return + case 'MAX_PLAYER_COUNT_REACHED': + this.notification( + locale('max_players_reached') ?? 'Your license reached the maximum player count. Please upgrade your license.', + YacaNotificationType.ERROR, + ) + return + case 'LICENSE_SERVER_TIMED_OUT': + this.notification( + locale('license_server_timed_out') ?? 'The connection to the license server timed out, while verifying the license. Please wait a moment.', + YacaNotificationType.ERROR, + ) + return + case 'MOVE_ERROR': + this.notification(locale('move_error') ?? 'You are not connected to the teamspeak server!', YacaNotificationType.ERROR) + return + case 'WAIT_GAME_INIT': + case 'HEARTBEAT': + case 'MUTE_STATE': + return + default: + console.log(`[YaCA-Websocket]: Unknown error code: ${parsedPayload.code}`) + this.notification(locale('unknown_error', parsedPayload.code) ?? `Unknown error code: ${parsedPayload.code}`, YacaNotificationType.ERROR) + return + } + } + + /** + * Sets a variable for a player. + * + * @param {string} player - The player for whom the variable is to be set. + * @param {string} variable - The name of the variable. + * @param {*} value - The value to be set for the variable. + */ + setPlayerVariable(player: number, variable: string, value: unknown) { + const currentData = this.getPlayerByID(player) + if (!currentData) return + + // @ts-expect-error Object cannot be undefined + currentData[variable] = value + } + + /** + * Get the current voice range. + * + * @returns {number} The current voice range. + */ + getVoiceRange(): number { + return LocalPlayer.state[VOICE_RANGE_STATE_NAME] ?? this.defaultVoiceRange + } + + /** + * Changes the voice range to the next range. + * + * @param {boolean} increase - If the voice range should be increased or decreased. + */ + changeVoiceRange(increase = true) { + if (!this.canChangeVoiceRange) return + + const currentVoiceRange = this.getVoiceRange() + if (increase) { + const newIndex = this.sharedConfig.voiceRange.ranges.findIndex( + (range) => ((this.maxVoiceRange !== -1 && range <= this.maxVoiceRange) || this.maxVoiceRange === -1) && range > currentVoiceRange, + ) + this.rangeIndex = newIndex !== -1 ? newIndex : 0 + } else { + const newIndex = this.sharedConfig.voiceRange.ranges + .slice() + .reverse() + .findIndex((range) => range < currentVoiceRange) + this.rangeIndex = newIndex !== -1 ? this.sharedConfig.voiceRange.ranges.length - 1 - newIndex : this.sharedConfig.voiceRange.ranges.length - 1 + + // If maxrange is defined and the range is higher than maxrange, set the range to maxrange + if (this.maxVoiceRange !== -1 && this.sharedConfig.voiceRange.ranges[this.rangeIndex] > this.maxVoiceRange) { + const newIndex = this.sharedConfig.voiceRange.ranges + .slice() + .reverse() + .findIndex((range) => range <= this.maxVoiceRange) + this.rangeIndex = newIndex !== -1 ? this.sharedConfig.voiceRange.ranges.length - 1 - newIndex : this.sharedConfig.voiceRange.ranges.length - 1 + } + } + + const voiceRange = this.sharedConfig.voiceRange.ranges[this.rangeIndex] ?? 1 + this.changeVoiceRangeInternal(voiceRange) + } + + /** + * Set the voice range to the given value. + * + * @param voiceRange - The voice range to set + */ + setVoiceRange(voiceRange: number) { + this.rangeIndex = -1 + this.changeVoiceRangeInternal(voiceRange) + } + + /** + * Internal function to change the voice range. + * + * @param voiceRange - The voice range to set + * @private + */ + private changeVoiceRangeInternal(voiceRange: number) { + if (!this.canChangeVoiceRange) return + if (this.maxVoiceRange !== -1 && voiceRange > this.maxVoiceRange) return + + this.showRangeVisual(voiceRange) + + LocalPlayer.state.set(VOICE_RANGE_STATE_NAME, voiceRange, true) + + emit('yaca:external:voiceRangeUpdate', voiceRange, this.rangeIndex) + // SaltyChat bridge + if (this.saltyChatBridge) { + emit('SaltyChat_VoiceRangeChanged', voiceRange.toFixed(1), this.rangeIndex, this.sharedConfig.voiceRange.ranges.length) + } + } + + /** + * Shows the voice range visuals. + * + * @param newVoiceRange - The new voice range + */ + showRangeVisual(newVoiceRange: number) { + if (this.visualVoiceRangeTimeout) { + clearTimeout(this.visualVoiceRangeTimeout) + this.visualVoiceRangeTimeout = null + } + + if (this.visualVoiceRangeTick) { + clearInterval(this.visualVoiceRangeTick) + this.visualVoiceRangeTick = null + } + + if (this.sharedConfig.voiceRange.sendNotification) { + this.notification(locale('voice_range_changed', newVoiceRange), YacaNotificationType.INFO) + } + + if (this.sharedConfig.voiceRange.markerColor.enabled) { + const red = this.sharedConfig.voiceRange.markerColor.r + const green = this.sharedConfig.voiceRange.markerColor.g + const blue = this.sharedConfig.voiceRange.markerColor.b + const alpha = this.sharedConfig.voiceRange.markerColor.a + const duration = this.sharedConfig.voiceRange.markerColor.duration + + this.visualVoiceRangeTimeout = setTimeout(() => { + if (this.visualVoiceRangeTick) { + clearInterval(this.visualVoiceRangeTick) + this.visualVoiceRangeTick = null + } + + this.visualVoiceRangeTimeout = null + }, duration) + + if (!this.isFiveM && this.sharedConfig.voiceRange.markerColor.type < 1000) { + this.sharedConfig.voiceRange.markerColor.type = 0x94fdae17 + console.warn('[YaCA] Marker type is not supported in RedM. Using default marker type.') + } + + this.visualVoiceRangeTick = setInterval(() => { + const entity = cache.vehicle || cache.ped + const pos = GetEntityCoords(entity, false) + const posZ = cache.vehicle ? pos[2] - 0.6 : pos[2] - 0.98 + + DrawMarker( + this.sharedConfig.voiceRange.markerColor.type, + pos[0], + pos[1], + posZ, + 0, + 0, + 0, + 0, + 0, + 0, + newVoiceRange * 2, + newVoiceRange * 2, + 1, + red, + green, + blue, + alpha, + false, + true, + 2, + this.sharedConfig.voiceRange.markerColor.rotate, + // @ts-expect-error Type error in the native + null, + null, + false, + ) + }) + } + } + + /** + * Checks if the communication type is valid. + * + * @param {string} type - The type of communication to be validated. + * @returns {boolean} Returns true if the type is valid, false otherwise. + */ + static isCommTypeValid(type: string): boolean { + const valid = type in YacaFilterEnum + if (!valid) { + console.error(`[YaCA-Websocket]: Invalid comm type: ${type}`) + } + + return valid + } + + /** + * Set the communication type for the given players. + * + * @param {YacaPlayerData | YacaPlayerData[]} players - The player or players for whom the communication type is to be set. + * @param {YacaFilterEnum} type - The type of communication. + * @param {boolean} state - The state of the communication. + * @param {number} channel - The channel for the communication. Optional. + * @param {number} range - The range for the communication. Optional. + * @param {CommDeviceMode} ownMode - The mode for the player. Optional. + * @param {CommDeviceMode} otherPlayersMode - The mode for the other players. Optional. + * @param {number} errorLevel - The error level for the communication. Optional. + */ + setPlayersCommType( + players: { clientId: number } | { clientId: number }[], + type: YacaFilterEnum, + state: boolean, + channel?: number | null, + range?: number | null, + ownMode?: CommDeviceMode, + otherPlayersMode?: CommDeviceMode, + errorLevel?: number | null, + ) { + if (!Array.isArray(players)) { + players = [players] + } + + const clientIds: YacaClient[] = [] + if (typeof ownMode !== 'undefined') { + clientIds.push({ + client_id: this.getPlayerByID(cache.serverId)?.clientId, + mode: ownMode, + }) + } + + for (const player of players) { + if (!player) { + continue + } + + const clientProtocol: YacaClient = { + client_id: player.clientId, + mode: otherPlayersMode, + } + + if (typeof errorLevel !== 'undefined' && errorLevel !== null) { + clientProtocol.errorLevel = errorLevel + } + + clientIds.push(clientProtocol) + } + + const protocol: YacaProtocol = { + on: state, + comm_type: type, + members: clientIds, + } + + if (typeof channel !== 'undefined' && channel !== null) { + protocol.channel = channel + } + if (typeof range !== 'undefined' && range !== null) { + protocol.range = range + } + + this.sendWebsocket({ + base: { request_type: 'INGAME' }, + comm_device: protocol, + }) + } + + /** + * Update the volume for a specific communication type. + * + * @param {string} type - The type of communication. + * @param {number} volume - The volume to be set. + * @param {number} channel - The channel for the communication. + */ + setCommDeviceVolume(type: YacaFilterEnum, volume: number, channel?: number) { + if (!YaCAClientModule.isCommTypeValid(type)) { + return + } + + const protocol: YacaProtocol = { + comm_type: type, + volume: clamp(volume, 0, 1), + } + + if (typeof channel !== 'undefined') { + protocol.channel = channel + } + + this.sendWebsocket({ + base: { request_type: 'INGAME' }, + comm_device_settings: protocol, + }) + } + + /** + * Update the stereo mode for a specific communication type. + * + * @param {YacaFilterEnum} type - The type of communication. + * @param {YacaStereoMode} mode - The stereo mode to be set. + * @param {number} channel - The channel for the communication. + */ + setCommDeviceStereoMode(type: YacaFilterEnum, mode: YacaStereoMode, channel?: number) { + if (!YaCAClientModule.isCommTypeValid(type)) { + return + } + + const protocol: YacaProtocol = { + comm_type: type, + output_mode: mode, + } + + if (typeof channel !== 'undefined') { + protocol.channel = channel + } + + this.sendWebsocket({ + base: { request_type: 'INGAME' }, + comm_device_settings: protocol, + }) + } + + /** + * Set the player speaking state and start the lip animation. + * + * @param ped - The ped to sync the lips with. + * @param playerId - The player ID to sync the lips with. + * @param isTalking - The talking state of the player. + */ + syncLipsPlayer(ped: number, playerId: number, isTalking: boolean) { + const animationData = localLipSyncAnimations[cache.game][isTalking ? 'true' : 'false'] + + SetPlayerTalkingOverride(playerId, isTalking) + if (this.isFiveM) { + PlayFacialAnim(ped, animationData.name, animationData.dict) + } else if (this.isRedM) { + playRdrFacialAnim(ped, animationData.name, animationData.dict) + } + } + + /** + * Handles the talk and mute state from teamspeak, displays it in UI and syncs lip to other players. + * + * @param {YacaResponse} payload - The response from teamspeak. + */ + handleTalkState(payload: YacaResponse) { + const messageState = payload.message === '1' + const isPlayerMuted = this.isMicrophoneMuted || this.isMicrophoneDisabled || this.isSoundMuted || this.isSoundDisabled + + const isTalking = !isPlayerMuted && messageState + if (this.isTalking !== isTalking) { + this.isTalking = isTalking + + this.syncLipsPlayer(cache.ped, cache.serverId, isTalking) + LocalPlayer.state.set(LIP_SYNC_STATE_NAME, isTalking, true) + + emit('yaca:external:isTalking', isTalking) + + // SaltyChat bridge + if (this.saltyChatBridge) { + emit('SaltyChat_TalkStateChanged', isTalking) + } + } + } + + /** + * Handles the sound state from teamspeak. + * + * @param payload - The response from teamspeak. + */ + handleSoundState(payload: YacaResponse) { + const soundStates: YacaSoundStateMessage = JSON.parse(payload.message) + + if (this.isMicrophoneMuted !== soundStates.microphoneMuted) { + this.isMicrophoneMuted = soundStates.microphoneMuted + emit('yaca:external:microphoneMuteStateChanged', soundStates.microphoneMuted) + emit('yaca:external:muteStateChanged', soundStates.microphoneMuted) // Deprecated in favor of microphoneMuteStateChanged + + // SaltyChat bridge + if (this.saltyChatBridge) { + emit('SaltyChat_MicStateChanged', soundStates.microphoneMuted) + } + } + + if (this.isMicrophoneDisabled !== soundStates.microphoneDisabled) { + this.isMicrophoneDisabled = soundStates.microphoneDisabled + emit('yaca:external:microphoneDisabledStateChanged', soundStates.microphoneDisabled) + + // SaltyChat bridge + if (this.saltyChatBridge) { + emit('SaltyChat_MicEnabledChanged', soundStates.microphoneDisabled) + } + } + + if (this.isSoundMuted !== soundStates.soundMuted) { + this.isSoundMuted = soundStates.soundMuted + emit('yaca:external:soundMuteStateChanged', soundStates.soundMuted) + + // SaltyChat bridge + if (this.saltyChatBridge) { + emit('SaltyChat_SoundStateChanged', soundStates.soundMuted) + } + } + + if (this.isSoundDisabled !== soundStates.soundDisabled) { + this.isSoundDisabled = soundStates.soundDisabled + emit('yaca:external:soundDisabledStateChanged', soundStates.soundDisabled) + + // SaltyChat bridge + if (this.saltyChatBridge) { + emit('SaltyChat_SoundEnabledChanged', soundStates.soundDisabled) + } + } + } + + /** + * Handles the talk state of other players. + * + * @param payload - The response from teamspeak. + */ + handleOtherTalkState(payload: YacaResponse) { + if (!this.sharedConfig.useLocalLipSync) { + return + } + + let talkData: { clientId: number; isTalking: boolean } + + try { + talkData = JSON.parse(payload.message) + } catch { + console.error('[YaCA-Websocket]: Error while parsing other talk state message') + return + } + + const player = this.getPlayerByClientId(talkData.clientId) + + if (!player || !player.remoteID) { + return + } + + const playerId = GetPlayerFromServerId(player.remoteID) + + if (playerId === -1) { + return + } + + SetPlayerTalkingOverride(playerId, talkData.isTalking) + } + + /** + * Handles the moved channel event. + * + * @param newChannel - The new channel the player is in. + */ + handleMovedChannel(newChannel: string) { + if (newChannel !== 'INGAME_CHANNEL' && newChannel !== 'EXCLUDED_CHANNEL') { + console.error('[YaCA-Websocket]: Unknown channel type: ', newChannel) + return + } + + if (newChannel === 'INGAME_CHANNEL') { + this.setCurrentPluginState(YacaPluginStates.IN_INGAME_CHANNEL) + } else { + this.setCurrentPluginState(YacaPluginStates.IN_EXCLUDED_CHANNEL) + } + + emit('yaca:external:channelChanged', newChannel) + } + + /** + * Checks if the vehicle has an opening. + * + * @param vehicle - The vehicle to check. + */ + checkIfVehicleHasOpening(vehicle: number | false) { + if (!vehicle) { + return true + } + + if (this.mufflingVehicleWhitelistHash.has(GetEntityModel(vehicle))) { + return true + } + + return vehicleHasOpening(vehicle) + } + + /** + * Get the muffle intensity for the nearby player. + * + * @param {number} playerPed - The player ped. + * @param {number} nearbyPlayerPed - The nearby player ped. + * @param {number} playerVehicle - The vehicle the player is in. + * @param {number} ownCurrentRoom - The current room the client is in. + * @param {boolean} ownVehicleHasOpening - The opening state ot the vehicle the client is in. + * @param {boolean} nearbyUsesMegaphone - The state if the nearby player uses a megaphone. + */ + getMuffleIntensity( + playerPed: number, + nearbyPlayerPed: number, + playerVehicle: number | false, + ownCurrentRoom: number, + ownVehicleHasOpening: boolean, + nearbyUsesMegaphone = false, + ) { + if (ownCurrentRoom !== GetRoomKeyFromEntity(nearbyPlayerPed) && !HasEntityClearLosToEntity(playerPed, nearbyPlayerPed, 17)) { + return this.sharedConfig.mufflingSettings.intensities.differentRoom + } + + const vehicleMuffling = this.sharedConfig.mufflingSettings.vehicleMuffling.enabled + if (this.isRedM || !vehicleMuffling) { + return 0 + } + + const nearbyPlayerVehicle = GetVehiclePedIsIn(nearbyPlayerPed, false) + const ownVehicleId = playerVehicle || 0 + + if (ownVehicleId === nearbyPlayerVehicle) { + return 0 + } + + if (nearbyUsesMegaphone) { + if (ownVehicleHasOpening) { + return 0 + } + + return this.sharedConfig.mufflingSettings.intensities.megaPhoneInCar + } + + const nearbyPlayerVehicleHasOpening = this.checkIfVehicleHasOpening(nearbyPlayerVehicle) + + if (!ownVehicleHasOpening && !nearbyPlayerVehicleHasOpening) { + return this.sharedConfig.mufflingSettings.intensities.bothCarsClosed + } + + if (!ownVehicleHasOpening || !nearbyPlayerVehicleHasOpening) { + return this.sharedConfig.mufflingSettings.intensities.oneCarClosed + } + + return 0 + } + + /** + * Handles the phone speaker emit. + * + * @param playersToPhoneSpeaker - The players to send the phone speaker to. + * @param playersOnPhoneSpeaker - The players who are on phone speaker. + */ + handlePhoneSpeakerEmit(playersToPhoneSpeaker: Set, playersOnPhoneSpeaker: Set): void { + if (this.useWhisper) { + if ( + (this.phoneModule.phoneSpeakerActive && this.phoneModule.inCallWith.size) || + ((!this.phoneModule.phoneSpeakerActive || !this.phoneModule.inCallWith.size) && this.currentlySendingPhoneSpeakerSender.size) + ) { + const playersToNotReceivePhoneSpeaker = [...this.currentlySendingPhoneSpeakerSender].filter((playerId) => !playersToPhoneSpeaker.has(playerId)) + const playersNeedsReceivePhoneSpeaker = [...playersToPhoneSpeaker].filter((playerId) => !this.currentlySendingPhoneSpeakerSender.has(playerId)) + + this.currentlySendingPhoneSpeakerSender = new Set(playersToPhoneSpeaker) + + if (playersNeedsReceivePhoneSpeaker.length || playersToNotReceivePhoneSpeaker.length) { + emitNet('server:yaca:phoneSpeakerEmitWhisper', playersNeedsReceivePhoneSpeaker, playersToNotReceivePhoneSpeaker) + } + } + } + + for (const playerId of this.currentlyPhoneSpeakerApplied) { + if (playersOnPhoneSpeaker.has(playerId)) { + continue + } + + this.currentlyPhoneSpeakerApplied.delete(playerId) + const player = this.getPlayerByID(playerId) + + if (!player) { + continue + } + + this.setPlayersCommType( + player, + YacaFilterEnum.PHONE_SPEAKER, + false, + undefined, + this.sharedConfig.maxPhoneSpeakerRange, + CommDeviceMode.RECEIVER, + CommDeviceMode.SENDER, + ) + } + } + + /** + * Handles around phone emit. + * + * @param playerToHearOnPhone - The players to hear on the phone. + */ + handlePhoneEmit(playerToHearOnPhone: Set) { + if (!this.sharedConfig.phoneHearPlayersNearby) return + + if (this.sharedConfig.phoneHearPlayersNearby === 'PHONE_SPEAKER') { + if ( + !( + (this.phoneModule.phoneSpeakerActive && this.phoneModule.inCallWith.size) || + ((!this.phoneModule.phoneSpeakerActive || !this.phoneModule.inCallWith.size) && this.phoneHearNearbyPlayer.size) + ) + ) { + return + } + } else { + if (!(this.phoneModule.inCallWith.size || (!this.phoneModule.inCallWith.size && this.phoneHearNearbyPlayer.size))) { + return + } + } + + const playersToNotHear = [...this.phoneHearNearbyPlayer].filter((playerId) => !playerToHearOnPhone.has(playerId)) + const playersToHear = [...playerToHearOnPhone].filter((playerId) => !this.phoneHearNearbyPlayer.has(playerId)) + + this.phoneHearNearbyPlayer = new Set(playerToHearOnPhone) + + if (playersToHear.length || playersToNotHear.length) { + emitNet('server:yaca:phoneEmit', playersToHear, playersToNotHear) + } + } + + /** + * Handles the voice range adjustment using the mouse wheel. + */ + handleVoiceRangeViaMouseWheel() { + if (this.isFiveM) { + HudWeaponWheelIgnoreSelection() + } + + let newValue = 0 + const currentVoiceRange = this.getVoiceRange() + + if (IsControlPressed(0, 242)) { + newValue = Math.max(1, currentVoiceRange - 1) + } else if (IsControlPressed(0, 241)) { + newValue = Math.min(this.sharedConfig.voiceRange.ranges[this.sharedConfig.voiceRange.ranges.length - 1], currentVoiceRange + 1) + + if (this.maxVoiceRange !== -1 && newValue > this.maxVoiceRange) { + newValue = this.maxVoiceRange + } + } + + if (newValue <= 0 || currentVoiceRange === newValue) return + + this.setVoiceRange(newValue) + } + + /** + * Calculate the players in streaming range and send them to the voice plugin. + */ + // skipcq: JS-R1005 + calcPlayers() { + const localData = this.getPlayerByID(cache.serverId) + if (!localData) return + + const players = new Map() + const playersToPhoneSpeaker = new Set() + const playersOnPhoneSpeaker = new Set() + const playerToHearOnPhone = new Set() + + let localPlayerPed = cache.ped + let localPlayerVehicle = cache.vehicle + + if (this.spectatingPlayer) { + const remotePlayerId = GetPlayerFromServerId(this.spectatingPlayer) + + if (remotePlayerId !== -1) { + const remotePlayerPed = GetPlayerPed(remotePlayerId) + if (remotePlayerPed !== 0) { + localPlayerPed = remotePlayerPed + const remotePlayerVehicle = GetVehiclePedIsIn(remotePlayerPed, false) + if (remotePlayerVehicle !== 0) { + localPlayerVehicle = remotePlayerVehicle + } else { + localPlayerVehicle = false + } + } + } + } + + const localPos = GetEntityCoords(localPlayerPed, false) + const currentRoom = GetRoomKeyFromEntity(localPlayerPed) + const hasVehicleOpening = this.isFiveM ? this.checkIfVehicleHasOpening(localPlayerVehicle) : true + const phoneSpeakerActive = this.phoneModule.phoneSpeakerActive && this.phoneModule.inCallWith.size + + for (const player of GetActivePlayers()) { + // Get the remote ID of the player and check if it is the local player or the server. + const remoteId = GetPlayerServerId(player) + const playerPed = GetPlayerPed(player) + // Check if the player is the local player and if the player is still in streaming range and the ped could be found. + if (remoteId === 0 || remoteId === cache.serverId || playerPed <= 0) continue + + // Get the player data and check if the player is initialized and has a client ID set. + const voiceSetting = this.getPlayerByID(remoteId) + if (!voiceSetting || !voiceSetting.clientId) continue + + // Get the player state and the voice range of the player. + const playerState = Player(remoteId).state + const range = playerState[VOICE_RANGE_STATE_NAME] ?? this.defaultVoiceRange + + // Get the muffle intensity for the player. + const muffleIntensity = this.getMuffleIntensity( + localPlayerPed, + playerPed, + localPlayerVehicle, + currentRoom, + hasVehicleOpening, + playerState[MEGAPHONE_STATE_NAME] !== null, + ) + + // Get the player position, the distance to the player, the player direction and if the player is underwater. + const playerPos = GetEntityCoords(playerPed, false) + const distanceToPlayer = calculateDistanceVec3(localPos, playerPos) + const playerDirection = GetEntityForwardVector(playerPed) + // @ts-expect-error Type error in the native + const isUnderwater = IsPedSwimmingUnderWater(playerPed) === 1 + + if (!playersOnPhoneSpeaker.has(remoteId)) { + players.set(remoteId, { + client_id: voiceSetting.clientId, + position: convertNumberArrayToXYZ(playerPos), + direction: convertNumberArrayToXYZ(playerDirection), + range, + is_underwater: isUnderwater, + muffle_intensity: muffleIntensity, + is_muted: voiceSetting.forceMuted ?? false, + }) + } + + // Who can be heard on the phone. + if (this.sharedConfig.phoneHearPlayersNearby && !localData.mutedOnPhone && !voiceSetting.forceMuted && distanceToPlayer <= range) { + if (this.sharedConfig.phoneHearPlayersNearby === 'PHONE_SPEAKER' && phoneSpeakerActive) { + playerToHearOnPhone.add(remoteId) + } else if (this.sharedConfig.phoneHearPlayersNearby === true && this.phoneModule.inCallWith.size) { + playerToHearOnPhone.add(remoteId) + } + } + + // Check if the player is in phone speaker range. + if (distanceToPlayer > this.sharedConfig.maxPhoneSpeakerRange) continue + + // Phone speaker handling - user who enabled it. + if (this.useWhisper && phoneSpeakerActive) playersToPhoneSpeaker.add(remoteId) + + // If no phone speaker is active, skip the rest. + if (!voiceSetting.phoneCallMemberIds) continue + + // Add all players which are in the call to the players list and give them the phone speaker effect. + for (const phoneCallMemberId of voiceSetting.phoneCallMemberIds) { + const phoneCallMember = this.getPlayerByID(phoneCallMemberId) + if (!phoneCallMember || !phoneCallMember.clientId || phoneCallMember.mutedOnPhone || phoneCallMember.forceMuted) continue + + players.delete(phoneCallMemberId) + players.set(phoneCallMemberId, { + client_id: phoneCallMember.clientId, + position: convertNumberArrayToXYZ(playerPos), + direction: convertNumberArrayToXYZ(playerDirection), + range: this.sharedConfig.maxPhoneSpeakerRange, + is_underwater: isUnderwater, + muffle_intensity: muffleIntensity, + is_muted: false, + }) + + playersOnPhoneSpeaker.add(phoneCallMemberId) + + if (this.currentlyPhoneSpeakerApplied.has(phoneCallMemberId)) continue + + this.setPlayersCommType( + phoneCallMember, + YacaFilterEnum.PHONE_SPEAKER, + true, + undefined, + this.sharedConfig.maxPhoneSpeakerRange, + CommDeviceMode.RECEIVER, + CommDeviceMode.SENDER, + ) + + this.currentlyPhoneSpeakerApplied.add(phoneCallMemberId) + } + } + + this.handlePhoneSpeakerEmit(playersToPhoneSpeaker, playersOnPhoneSpeaker) + this.handlePhoneEmit(playerToHearOnPhone) + + // Send the collected data to the voice plugin. + this.sendWebsocket({ + base: { request_type: 'INGAME' }, + player: { + player_direction: getCamDirection(), + player_position: convertNumberArrayToXYZ(localPos), + player_range: LocalPlayer.state[VOICE_RANGE_STATE_NAME] ?? this.defaultVoiceRange, + // @ts-expect-error Type error in the native + player_is_underwater: IsPedSwimmingUnderWater(localPlayerPed) === 1, + player_is_muted: localData.forceMuted ?? false, + players_list: Array.from(players.values()), + }, + }) + } +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/megaphone.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/megaphone.ts new file mode 100644 index 000000000..d4c53bdad --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/megaphone.ts @@ -0,0 +1,218 @@ +import { locale, MEGAPHONE_STATE_NAME } from '@yaca-voice/common' +import { CommDeviceMode, YacaFilterEnum } from '@yaca-voice/types' +import { cache, joaat, onCache, registerRdrKeyBind } from '../utils' +import type { YaCAClientModule } from './main' + +/** + * The megaphone module for the client. + */ +export class YaCAClientMegaphoneModule { + clientModule: YaCAClientModule + + canUseMegaphone = false + lastMegaphoneState = false + + megaphoneVehicleWhitelistHashes = new Set() + + /** + * Creates an instance of the megaphone module. + * + * @param clientModule - The client module. + */ + constructor(clientModule: YaCAClientModule) { + this.clientModule = clientModule + + this.registerEvents() + if (this.clientModule.isFiveM) { + this.registerKeybinds() + + for (const vehicleModel of this.clientModule.sharedConfig.megaphone.allowedVehicleModels) { + this.megaphoneVehicleWhitelistHashes.add(joaat(vehicleModel)) + } + } else if (this.clientModule.isRedM) { + this.registerRdrKeybinds() + } + this.registerExports() + this.registerStateBagHandlers() + } + + registerEvents() { + /** + * Handles the "client:yaca:setLastMegaphoneState" server event. + * + * @param {boolean} state - The state of the megaphone. + */ + onNet('client:yaca:setLastMegaphoneState', (state: boolean) => { + this.lastMegaphoneState = state + }) + + if (this.clientModule.isFiveM && this.clientModule.sharedConfig.megaphone.automaticVehicleDetection) { + /** + * Checks if the player can use the megaphone when they enter a vehicle. + * If they can, it sets the `canUseMegaphone` property to `true`. + * If they can't, it sets the `canUseMegaphone` property to `false`. + * If the player is not in a vehicle, it sets the `canUseMegaphone` property to `false` and emits the "server:yaca:playerLeftVehicle" event. + */ + onCache('seat', (seat) => { + if (seat === false || seat > 0 || !cache.vehicle) { + this.canUseMegaphone = false + emitNet('server:yaca:playerLeftVehicle') + return + } + + const vehicleClass = GetVehicleClass(cache.vehicle) + const vehicleModel = GetEntityModel(cache.vehicle) + + this.canUseMegaphone = + this.clientModule.sharedConfig.megaphone.allowedVehicleClasses.includes(vehicleClass) || + this.megaphoneVehicleWhitelistHashes.has(vehicleModel) + }) + } + } + + /** + * Registers the command and key mapping for the megaphone. + * This is only available in FiveM. + */ + registerKeybinds() { + if (this.clientModule.sharedConfig.keyBinds.megaphone === false) { + return + } + + /** + * Registers the command and key mapping for the megaphone. + */ + RegisterCommand( + '+yaca:megaphone', + () => { + this.useMegaphone(true) + }, + false, + ) + RegisterCommand( + '-yaca:megaphone', + () => { + this.useMegaphone(false) + }, + false, + ) + RegisterKeyMapping('+yaca:megaphone', locale('use_megaphone'), 'keyboard', this.clientModule.sharedConfig.keyBinds.megaphone) + } + + /** + * Registers the keybindings for the megaphone. + * This is only available in RedM. + */ + registerRdrKeybinds() { + if (this.clientModule.sharedConfig.keyBinds.megaphone === false) { + return + } + + /** + * Registers the command and key mapping for the megaphone. + */ + registerRdrKeyBind(this.clientModule.sharedConfig.keyBinds.megaphone, () => { + this.useMegaphone(!this.lastMegaphoneState) + }) + } + + registerExports() { + /** + * Gets the `canUseMegaphone` property. + * + * @returns {boolean} - The `canUseMegaphone` property. + */ + exports('getCanUseMegaphone', () => { + return this.canUseMegaphone + }) + + /** + * Sets the `canUseMegaphone` property. + * + * @param {boolean} state - The state to set the `canUseMegaphone` property to. + */ + exports('setCanUseMegaphone', (state: boolean) => { + this.canUseMegaphone = state + + if (!state && this.lastMegaphoneState) { + emitNet('server:yaca:playerLeftVehicle') + } + }) + + /** + * Toggles the use of the megaphone. + * + * @param {boolean} [state=false] - The state of the megaphone. Defaults to false if not provided. + */ + exports('useMegaphone', (state = false) => { + this.useMegaphone(state) + }) + } + + registerStateBagHandlers() { + /** + * Handles the megaphone state bag change. + */ + AddStateBagChangeHandler(MEGAPHONE_STATE_NAME, '', (bagName: string, _: string, value: number | null, __: number, replicated: boolean) => { + if (replicated) { + return + } + + const playerId = GetPlayerFromStateBagName(bagName) + if (playerId === 0) { + return + } + + const playerSource = GetPlayerServerId(playerId) + if (playerSource === 0) { + return + } + + if (playerSource === cache.serverId) { + this.clientModule.setPlayersCommType( + [], + YacaFilterEnum.MEGAPHONE, + typeof value === 'number', + undefined, + value, + CommDeviceMode.SENDER, + CommDeviceMode.RECEIVER, + ) + } else { + const player = this.clientModule.getPlayerByID(playerSource) + if (!player) { + return + } + + this.clientModule.setPlayersCommType( + player, + YacaFilterEnum.MEGAPHONE, + typeof value === 'number', + undefined, + value, + CommDeviceMode.RECEIVER, + CommDeviceMode.SENDER, + ) + } + }) + } + + /** + * Toggles the use of the megaphone. + * + * @param {boolean} [state=false] - The state of the megaphone. Defaults to false if not provided. + */ + useMegaphone(state = false) { + if ( + (!cache.vehicle && this.clientModule.sharedConfig.megaphone.automaticVehicleDetection) || + !this.canUseMegaphone || + state === this.lastMegaphoneState + ) { + return + } + + this.lastMegaphoneState = !this.lastMegaphoneState + emitNet('server:yaca:useMegaphone', state) + emit('yaca:external:megaphoneState', state) + } +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/phone.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/phone.ts new file mode 100644 index 000000000..8e7b27e4b --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/phone.ts @@ -0,0 +1,288 @@ +import { GLOBAL_ERROR_LEVEL_STATE_NAME, PHONE_SPEAKER_STATE_NAME } from '@yaca-voice/common' +import { CommDeviceMode, YacaFilterEnum, type YacaPlayerData } from '@yaca-voice/types' +import { cache } from '../utils' +import type { YaCAClientModule } from './main' + +/** + * The phone module for the client. + */ +export class YaCAClientPhoneModule { + clientModule: YaCAClientModule + + inCallWith = new Set() + phoneSpeakerActive = false + + /** + * Creates an instance of the phone module. + * + * @param clientModule - The client module. + */ + constructor(clientModule: YaCAClientModule) { + this.clientModule = clientModule + + this.registerEvents() + this.registerExports() + this.registerStateBagHandlers() + } + + registerEvents() { + /** + * Handles the "client:yaca:phone" server event. + * + * @param {number | number[]} targetIDs - The ID of the target. + * @param {boolean} state - The state of the phone. + */ + onNet('client:yaca:phone', (targetIDs: number | number[], state: boolean, filter: YacaFilterEnum = YacaFilterEnum.PHONE) => { + if (!Array.isArray(targetIDs)) { + targetIDs = [targetIDs] + } + + this.enablePhoneCall(targetIDs, state, filter) + }) + + /** + * Handles the "client:yaca:phoneHearAround" server event. + * + * @param {number[]} targetClientIds - The IDs of the targets. + * @param {boolean} state - The state of the phone hear around. + */ + onNet('client:yaca:phoneHearAround', (targetClientIds: number[], state: boolean) => { + if (!targetClientIds.length) return + + const commTargets = Array.from(targetClientIds).map((clientId) => ({ clientId })) + + this.clientModule.setPlayersCommType( + commTargets, + YacaFilterEnum.PHONE, + state, + undefined, + undefined, + CommDeviceMode.TRANSCEIVER, + CommDeviceMode.TRANSCEIVER, + GlobalState[PHONE_SPEAKER_STATE_NAME] ?? undefined, + ) + }) + + /** + * Handles the "client:yaca:phoneMute" server event. + * + * @param {number} targetID - The ID of the target. + * @param {boolean} state - The state of the phone mute. + * @param {boolean} onCallStop - The state of the call. + */ + onNet('client:yaca:phoneMute', (targetID: number, state: boolean, onCallStop = false) => { + const target = this.clientModule.getPlayerByID(targetID) + if (!target) { + return + } + + target.mutedOnPhone = state + + if (onCallStop) { + return + } + + if (this.clientModule.useWhisper && target.remoteID === cache.serverId) { + this.clientModule.setPlayersCommType([], YacaFilterEnum.PHONE, !state, undefined, undefined, CommDeviceMode.SENDER) + } else if (!this.clientModule.useWhisper && this.inCallWith.has(targetID)) { + this.clientModule.setPlayersCommType( + target, + YacaFilterEnum.PHONE, + state, + undefined, + undefined, + CommDeviceMode.TRANSCEIVER, + CommDeviceMode.TRANSCEIVER, + ) + } + }) + + /** + * Handles the "client:yaca:phoneSpeaker" server event. + * + * @param {number | number[]} playerIDs - The IDs of the players to be added or removed from the phone speaker. + * @param {boolean} state - The state indicating whether to add or remove the players. + */ + onNet('client:yaca:playersToPhoneSpeakerEmitWhisper', (playerIDs: number | number[], state: boolean) => { + if (!this.clientModule.useWhisper) return + + if (!Array.isArray(playerIDs)) { + playerIDs = [playerIDs] + } + + const targets = new Set() + for (const playerID of playerIDs) { + const player = this.clientModule.getPlayerByID(playerID) + if (!player) { + continue + } + + targets.add(player) + } + + if (targets.size < 1) { + return + } + + this.clientModule.setPlayersCommType( + Array.from(targets), + YacaFilterEnum.PHONE_SPEAKER, + state, + undefined, + undefined, + CommDeviceMode.SENDER, + CommDeviceMode.RECEIVER, + ) + }) + } + + registerExports() { + /** + * Exports the "isInCall" function. + * This function returns whether the player is in a phone call. + * + * @returns {boolean} - Whether the player is in a phone call. + */ + exports('isInCall', () => this.inCallWith.size > 0) + } + + registerStateBagHandlers() { + /** + * Handles the "yaca:phone" state bag change. + */ + AddStateBagChangeHandler(PHONE_SPEAKER_STATE_NAME, '', (bagName: string, _: string, value: number | number[] | null) => { + const playerId = GetPlayerFromStateBagName(bagName) + if (playerId === 0) { + return + } + + const playerSource = GetPlayerServerId(playerId) + if (playerSource === 0) { + return + } + + if (playerSource === cache.serverId) { + this.phoneSpeakerActive = value !== null + } + + this.removePhoneSpeakerFromEntity(playerSource) + if (value !== null) { + this.clientModule.setPlayerVariable(playerSource, 'phoneCallMemberIds', Array.isArray(value) ? value : [value]) + } + }) + } + + /** + * Removes the phone speaker effect from a player entity. + * + * @param {number} player - The player entity from which the phone speaker effect is to be removed. + */ + removePhoneSpeakerFromEntity(player: number) { + const entityData = this.clientModule.getPlayerByID(player) + if (!entityData?.phoneCallMemberIds) { + return + } + + const playersToSet = [] + for (const phoneCallMemberId of entityData.phoneCallMemberIds) { + const phoneCallMember = this.clientModule.getPlayerByID(phoneCallMemberId) + if (!phoneCallMember) { + continue + } + + playersToSet.push(phoneCallMember) + } + + this.clientModule.setPlayersCommType( + playersToSet, + YacaFilterEnum.PHONE_SPEAKER, + false, + undefined, + undefined, + CommDeviceMode.RECEIVER, + CommDeviceMode.SENDER, + ) + + entityData.phoneCallMemberIds = undefined + } + + /** + * Handles the disconnection of a player from a phone call. + * + * @param {number} targetID - The ID of the target. + */ + handleDisconnect(targetID: number) { + this.inCallWith.delete(targetID) + } + + /** + * Reestablishes a phone call with a target, when a player has restarted the voice plugin. + * + * @param {number | number[]} targetIDs - The IDs of the targets. + */ + reestablishCalls(targetIDs: number | number[]) { + if (!this.inCallWith.size) { + return + } + + if (!Array.isArray(targetIDs)) { + targetIDs = [targetIDs] + } + + if (!targetIDs.length) { + return + } + + const targetsToReestablish = [] + for (const targetId of targetIDs) { + if (this.inCallWith.has(targetId)) { + targetsToReestablish.push(targetId) + } + } + + if (targetsToReestablish.length) { + this.enablePhoneCall(targetsToReestablish, true, YacaFilterEnum.PHONE) + } + } + + /** + * Enables or disables a phone call. + * + * @param {number[]} targetIDs - The IDs of the targets. + * @param {boolean} state - The state of the phone call. + * @param {YacaFilterEnum} filter - The filter to use. + */ + enablePhoneCall(targetIDs: number[], state: boolean, filter: YacaFilterEnum = YacaFilterEnum.PHONE) { + if (!targetIDs.length) { + return + } + + const commTargets = [] + for (const targetID of targetIDs) { + const target = this.clientModule.getPlayerByID(targetID) + if (!target) { + if (!state) this.inCallWith.delete(targetID) + continue + } + + if (state) { + this.inCallWith.add(targetID) + } else { + this.inCallWith.delete(targetID) + } + + commTargets.push(target) + } + + this.clientModule.setPlayersCommType( + commTargets, + filter, + state, + undefined, + undefined, + state || (!state && this.inCallWith.size) ? CommDeviceMode.TRANSCEIVER : undefined, + CommDeviceMode.TRANSCEIVER, + GlobalState[GLOBAL_ERROR_LEVEL_STATE_NAME] ?? undefined, + ) + } +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/radio.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/radio.ts new file mode 100644 index 000000000..e63893b34 --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/radio.ts @@ -0,0 +1,1124 @@ +import { clamp, GLOBAL_ERROR_LEVEL_STATE_NAME, locale } from '@yaca-voice/common' +import { + CommDeviceMode, + type radioMode, + YacaFilterEnum, + YacaNotificationType, + type YacaPlayerData, + type YacaRadioSettings, + YacaStereoMode, +} from '@yaca-voice/types' +import { cache, calculateDistanceVec3, createProp, registerRdrKeyBind, requestAnimDict } from '../utils' +import type { YaCAClientModule } from './main' + +/** + * The radio module for the client. + */ +export class YaCAClientRadioModule { + clientModule: YaCAClientModule + + radioEnabled = false + radioInitialized = false + + talkingInChannels = new Set() + radioChannelSettings = new Map() + playersWithShortRange = new Map() + playersInRadioChannel = new Map>() + radioTowerCalculation = new Map() + + radioMode: radioMode = 'None' + activeRadioChannel = 1 + secondaryRadioChannel = 2 + + radioOnCooldown = false + currentRadioProp: number | null + + defaultRadioSettings: YacaRadioSettings = { + frequency: '0', + muted: false, + volume: 1, + stereo: YacaStereoMode.STEREO, + } + + /** + * Creates an instance of the radio module. + * + * @param clientModule - The client module. + */ + constructor(clientModule: YaCAClientModule) { + this.clientModule = clientModule + + this.radioMode = this.clientModule.sharedConfig.radioSettings.mode + + this.registerExports() + this.registerEvents() + + if (this.clientModule.isFiveM) { + this.registerKeybinds() + } else { + this.registerRdrKeybinds() + } + } + + /** + * Registers the exports for the radio module. + */ + registerExports() { + /** + * Enables or disables the radio system. + * + * @param {boolean} state - The state of the radio system. + */ + exports('enableRadio', (state: boolean) => this.enableRadio(state)) + + /** + * Returns the state of the radio system. + * + * @returns {boolean} The state of the radio system. + */ + exports('isRadioEnabled', () => this.radioEnabled) + + /** + * Changes the radio frequency of the active channel. + * + * @param {string} frequency - The frequency to set. + */ + exports('changeRadioFrequency', (frequency: string) => this.changeRadioFrequencyRaw(frequency)) + + /** + * Changes the radio frequency. + * + * @param {number} channel - The channel number. + * @param {string} frequency - The frequency to set. + */ + exports('changeRadioFrequencyRaw', (channel: number, frequency: string) => this.changeRadioFrequencyRaw(frequency, channel)) + + /** + * Returns the radio frequency of a channel. + * + * @param {number} channel - The channel number. + * @returns {string} The frequency of the channel. + */ + exports('getRadioFrequency', (channel: number) => this.getRadioFrequency(channel)) + + /** + * Mutes the active radio channel. + */ + exports('muteRadioChannel', (state?: boolean) => this.muteRadioChannel(state)) + + /** + * Exports the `muteRadioChannelRaw` function to the plugin. + * This function mutes a radio channel. + * + * @param {number} channel - The channel number. + */ + exports('muteRadioChannelRaw', (channel: number, state?: boolean) => this.muteRadioChannelRaw(channel, state)) + + /** + * Returns the mute state of a radio channel. + */ + exports('isRadioChannelMuted', (channel: number = this.activeRadioChannel) => this.isRadioChannelMuted(channel)) + + /** + * Exports the `setActiveRadioChannel` function to the plugin. + * This function changes the active radio channel. + * + * @param {number} channel - The new radio channel. + */ + exports('setActiveRadioChannel', (channel: number) => this.setActiveRadioChannel(channel)) + + /** + * Exports the `getActiveRadioChannel` function to the plugin. + * This function returns the active radio channel. + * + * @returns {number} The active radio channel. + */ + exports('getActiveRadioChannel', () => this.activeRadioChannel) + + /** + * Exports the `setSecondaryRadioChannel` function to the plugin. + * This function changes the secondary radio channel. + * + * @param {number} channel - The new radio channel. + */ + exports('setSecondaryRadioChannel', (channel: number) => this.setSecondaryRadioChannel(channel)) + + /** + * Exports the `getActiveRadioChannel` function to the plugin. + * This function returns the active radio channel. + * + * @returns {number} The active radio channel. + */ + exports('getSecondaryRadioChannel', () => this.secondaryRadioChannel) + + /** + * Exports the `changeRadioChannelVolume` function to the plugin. + * This function changes the volume of the active radio channel. + * + * @param {boolean} higher - Whether to increase the volume. + */ + exports('changeRadioChannelVolume', (higher: boolean) => this.changeRadioChannelVolume(higher)) + + /** + * Exports the `changeRadioChannelVolumeRaw` function to the plugin. + * This function changes the volume of a radio channel. + * + * @param {number} channel - The channel number. + * @param {number} volume - The volume to set. + */ + exports('changeRadioChannelVolumeRaw', (channel: number, volume: number) => this.changeRadioChannelVolumeRaw(volume, channel)) + + /** + * Returns the volume of a radio channel. + * + * @param {number} channel - The channel number. + * @returns {number} The volume of the channel. + */ + exports('getRadioChannelVolume', (channel: number) => this.getRadioChannelVolume(channel)) + + /** + * Exports the `changeRadioChannelStereo` function to the plugin. + * This function changes the stereo mode for the active radio channel. + */ + exports('changeRadioChannelStereo', () => this.changeRadioChannelStereo()) + + /** + * Exports the `changeRadioChannelStereoRaw` function to the plugin. + * This function changes the stereo mode for a radio channel. + * + * @param {number} channel - The channel number. + * @param {YacaStereoMode} stereo - The stereo mode to set. + */ + exports('changeRadioChannelStereoRaw', (channel: number, stereo: YacaStereoMode) => this.changeRadioChannelStereoRaw(stereo, channel)) + + /** + * Returns the stereo mode of a radio channel. + * + * @param {number} channel - The channel number. + * @returns {YacaStereoMode} The stereo mode of the channel. + */ + exports('getRadioChannelStereo', (channel: number) => this.getRadioChannelStereo(channel)) + + /** + * Exports the `radioTalkingStart` function to the plugin. + * This function starts the radio talking state. + * + * @param {boolean} state - The state of the radio talking. + * @param {number} channel - The radio channel. + */ + exports('radioTalkingStart', (state: boolean, channel: number) => this.radioTalkingStart(state, channel)) + + /** + * Sets the radio mode. + * + * @param {radioMode} mode - The radio mode to set. + */ + exports('setRadioMode', (mode: radioMode) => { + this.radioMode = mode + }) + + /** + * Returns the radio mode. + * + * @returns {radioMode} The current radio mode. + */ + exports('getRadioMode', () => this.radioMode) + } + + /** + * Registers the events for the radio module. + */ + registerEvents() { + /** + * Handles the "client:yaca:setRadioFreq" server event. + * + * @param {number} channel - The channel number. + * @param {string} frequency - The frequency to set. + */ + onNet('client:yaca:setRadioFreq', (channel: number, frequency: string) => { + this.setRadioFrequency(channel, frequency) + }) + + /** + * Handles the "client:yaca:radioTalking" server event. + * + * @param {number} target - The ID of the target. + * @param {string} frequency - The frequency of the radio. + * @param {boolean} state - The state of the radio talking. + * @param {object[]} infos - The information about the radio. + * @param {boolean} infos.shortRange - The state of the short range. + */ + onNet( + 'client:yaca:radioTalking', + ( + target: number, + frequency: string, + state: boolean, + infos: { shortRange: boolean }[], + senderDistanceToTower = -1, + senderPosition: [number, number, number] = [0, 0, 0], + ) => { + const channel = this.findRadioChannelByFrequency(frequency) + if (!channel) { + return + } + + const ownDistanceToTowerOrSender = this.getDistanceToTowerOrSender(senderPosition) + + if (state) { + if (this.radioMode !== 'None' && ownDistanceToTowerOrSender > this.clientModule.sharedConfig.radioSettings.maxDistance) return + if (this.radioMode === 'Tower' && senderDistanceToTower > this.clientModule.sharedConfig.radioSettings.maxDistance) return + } + + const player = this.clientModule.getPlayerByID(target) + if (!player) { + return + } + + const info = infos[cache.serverId] + + if (!info?.shortRange || (info?.shortRange && GetPlayerFromServerId(target) !== -1)) { + const errorLevel = this.getErrorLevelFromDistance(ownDistanceToTowerOrSender, senderDistanceToTower) + + this.clientModule.setPlayersCommType( + player, + YacaFilterEnum.RADIO, + state, + channel, + undefined, + CommDeviceMode.RECEIVER, + CommDeviceMode.SENDER, + errorLevel, + ) + } + + if (state) { + this.playersInRadioChannel.get(channel)?.add(target) + if (info?.shortRange) { + this.playersWithShortRange.set(target, frequency) + } + + emit('yaca:external:isRadioReceiving', true, channel) + this.clientModule.saltyChatBridge?.handleRadioReceivingStateChange(true, channel) + } else { + this.playersInRadioChannel.get(channel)?.delete(target) + if (info?.shortRange) { + this.playersWithShortRange.delete(target) + } + + const inRadio = this.playersInRadioChannel.get(channel)?.size || 0 + const state = inRadio > 0 + emit('yaca:external:isRadioReceiving', state, channel) + this.clientModule.saltyChatBridge?.handleRadioReceivingStateChange(state, channel) + } + }, + ) + + /** + * Handles the "client:yaca:radioTalking" server event. + * + * @param {number} target - The ID of the target. + * @param {string} frequency - The frequency of the radio. + * @param {boolean} state - The state of the radio talking. + * @param {object[]} infos - The information about the radio. + * @param {boolean} infos.shortRange - The state of the short range. + * @param {boolean} self - The state of the player. + */ + onNet( + 'client:yaca:radioTalkingWhisper', + (targets: number[], frequency: string, state: boolean, senderPosition: [number, number, number] = [0, 0, 0]) => { + const channel = this.findRadioChannelByFrequency(frequency) + if (!channel) { + return + } + + const ownDistanceToTowerOrSender = this.getDistanceToTowerOrSender(senderPosition) + + if (state && this.radioMode !== 'None' && ownDistanceToTowerOrSender > this.clientModule.sharedConfig.radioSettings.maxDistance) targets = [] + this.radioTalkingStateToPluginWithWhisper(state, targets, channel) + }, + ) + + /** + * Handles the "client:yaca:setRadioMuteState" server event. + * + * @param {number} channel - The channel number. + * @param {boolean} state - The state of the radio mute. + */ + onNet('client:yaca:setRadioMuteState', (channel: number, state: boolean) => { + const channelSettings = this.radioChannelSettings.get(channel) + + if (!channelSettings) { + return + } + + channelSettings.muted = state + emit('yaca:external:setRadioMuteState', channel, state) + this.disableRadioFromPlayerInChannel(channel) + this.updateRadioChannelData(channel) + }) + + /** + * Handles the "client:yaca:leaveRadioChannel" server event. + * + * @param {number | number[]} client_ids - The IDs of the clients. + * @param {string} frequency - The frequency of the radio. + */ + onNet('client:yaca:leaveRadioChannel', (client_ids: number | number[], frequency: string) => { + if (!Array.isArray(client_ids)) { + client_ids = [client_ids] + } + + const channel = this.findRadioChannelByFrequency(frequency) + if (!channel) { + return + } + + const playerData = this.clientModule.getPlayerByID(cache.serverId) + if (!playerData || !playerData.clientId) { + return + } + + if (client_ids.includes(playerData.clientId)) { + this.setRadioFrequency(channel, '0') + } + + this.clientModule.sendWebsocket({ + base: { request_type: 'INGAME' }, + comm_device_left: { + comm_type: YacaFilterEnum.RADIO, + client_ids, + channel, + }, + }) + }) + } + + /** + * Registers the command and key mapping for the radio talking. + */ + registerKeybinds() { + if (this.clientModule.sharedConfig.keyBinds.primaryRadioTransmit !== false) { + /** + * Registers the command and key mapping for the radio talking. + */ + RegisterCommand( + '+yaca:radioTalking', + () => { + this.radioTalkingStart(true, this.activeRadioChannel) + }, + false, + ) + RegisterCommand( + '-yaca:radioTalking', + () => { + this.radioTalkingStart(false, this.activeRadioChannel) + }, + false, + ) + RegisterKeyMapping('+yaca:radioTalking', locale('use_radio'), 'keyboard', this.clientModule.sharedConfig.keyBinds.primaryRadioTransmit) + } + + if (this.clientModule.sharedConfig.keyBinds.secondaryRadioTransmit !== false) { + /** + * Registers the command and key mapping for the secondary radio talking. + */ + RegisterCommand( + '+yaca:secondaryRadioTalking', + () => { + this.radioTalkingStart(true, this.secondaryRadioChannel) + }, + false, + ) + RegisterCommand( + '-yaca:secondaryRadioTalking', + () => { + this.radioTalkingStart(false, this.secondaryRadioChannel) + }, + false, + ) + RegisterKeyMapping( + '+yaca:secondaryRadioTalking', + locale('use_secondary_radio'), + 'keyboard', + this.clientModule.sharedConfig.keyBinds.secondaryRadioTransmit, + ) + } + } + + /** + * Registers the keybindings for the radio talking. + * This is only available in RedM. + */ + registerRdrKeybinds() { + if (this.clientModule.sharedConfig.keyBinds.primaryRadioTransmit !== false) { + registerRdrKeyBind( + this.clientModule.sharedConfig.keyBinds.primaryRadioTransmit, + () => { + this.radioTalkingStart(true, this.activeRadioChannel) + }, + () => { + this.radioTalkingStart(false, this.activeRadioChannel) + }, + ) + } + + if (this.clientModule.sharedConfig.keyBinds.secondaryRadioTransmit !== false) { + registerRdrKeyBind( + this.clientModule.sharedConfig.keyBinds.secondaryRadioTransmit, + () => { + this.radioTalkingStart(true, this.secondaryRadioChannel) + }, + () => { + this.radioTalkingStart(false, this.secondaryRadioChannel) + }, + ) + } + } + + /** + * Calculates the error level based on the distance to the tower or sender. + * + * @param ownDistanceToTower - The distance to the tower. + * @param senderDistanceToTower - The distance to the tower for the sender. + */ + getErrorLevelFromDistance(ownDistanceToTower: number, senderDistanceToTower: number) { + let errorLevel: number + + const globalErrorLevel = GlobalState[GLOBAL_ERROR_LEVEL_STATE_NAME] || 0 + + if (this.radioMode === 'Tower') { + const ownSignalStrength = this.calculateSignalStrength(ownDistanceToTower) + const senderSignalStrength = this.calculateSignalStrength(senderDistanceToTower) + + errorLevel = Math.max(ownSignalStrength, senderSignalStrength, globalErrorLevel) + } else if (this.radioMode === 'Direct') { + const signaleStrength = this.calculateSignalStrength(ownDistanceToTower) + + errorLevel = Math.max(signaleStrength, globalErrorLevel) + } else { + errorLevel = globalErrorLevel + } + + return errorLevel + } + + /** + * Get the distance to the tower or sender. + * + * @param senderPosition - The position of the sender. + */ + getDistanceToTowerOrSender(senderPosition: [number, number, number]) { + let ownDistanceToTower = Number.MAX_VALUE + + if (this.radioMode === 'Tower') { + ownDistanceToTower = this.getNearestRadioTower() + } else if (this.radioMode === 'Direct') { + ownDistanceToTower = calculateDistanceVec3(GetEntityCoords(cache.ped, false), senderPosition) + } + + return ownDistanceToTower + } + + /** + * Enable or disable the radio system. + * + * @param {boolean} state - The state of the radio system. + */ + enableRadio(state: boolean) { + if (!this.clientModule.isPluginInitialized()) { + return + } + + if (this.radioEnabled !== state) { + this.radioEnabled = state + emitNet('server:yaca:enableRadio', state) + + if (!state) { + for (let i = 1; i <= this.clientModule.sharedConfig.radioSettings.channelCount; i++) { + this.disableRadioFromPlayerInChannel(i) + } + } + + if (state && !this.radioInitialized) { + this.radioInitialized = true + this.initRadioSettings() + this.updateRadioChannelData(this.activeRadioChannel) + } + + emit('yaca:external:isRadioEnabled', state) + } + } + + /** + * Calculate the signal strength based on the distance. + * + * @param distance - The distance to the radio tower. + * @param maxDistance - The maximum distance to the radio tower. + * + * @returns {number} The signal strength. + */ + calculateSignalStrength(distance: number, maxDistance: number = this.clientModule.sharedConfig.radioSettings.maxDistance): number { + const ratio = distance / maxDistance + return clamp(Math.log10(1 + ratio * 8.5) / Math.log10(10), 0, 1) + } + + /** + * Finds the nearest tower to the local player. + * Iterates through all towers and calculates the distance to the local player's position. + * Keeps track of the nearest tower and returns its distance. + * + * @returns {number | null} The distance to the nearest tower, or null if no tower is found. + */ + getNearestRadioTower() { + let nearestTowerDistance = Number.MAX_VALUE + + const playerPos = GetEntityCoords(cache.ped, false) + + for (const coords of this.clientModule.towerConfig.towerPositions) { + const distance = calculateDistanceVec3(playerPos, coords) + + if (!nearestTowerDistance || distance < nearestTowerDistance) { + nearestTowerDistance = distance + } + } + + return nearestTowerDistance + } + + /** + * Change the radio frequency. + * + * @param {number} channel - The channel number. + * @param {string} frequency - The frequency to set. + */ + changeRadioFrequencyRaw(frequency: string, channel: number = this.activeRadioChannel) { + if (!this.clientModule.isPluginInitialized()) { + return + } + + emitNet('server:yaca:changeRadioFrequency', channel, frequency) + } + + /** + * Get the radio frequency of a channel. + * + * @param channel - The channel number. + * @returns {string} The frequency of the channel. + */ + getRadioFrequency(channel: number = this.activeRadioChannel): string { + const channelData = this.radioChannelSettings.get(channel) + + if (!channelData) { + return '0' + } + + return channelData.frequency + } + + /** + * Mute the active radio channel. + */ + muteRadioChannel(state?: boolean) { + this.muteRadioChannelRaw(this.activeRadioChannel, state) + } + + /** + * Mute a radio channel. + * + * @param {number} channel - The channel to mute. Defaults to the current active channel. + */ + muteRadioChannelRaw(channel: number = this.activeRadioChannel, state?: boolean) { + if (!this.clientModule.isPluginInitialized() || !this.radioEnabled) { + return + } + + const channelSettings = this.radioChannelSettings.get(channel) + + if (!channelSettings) { + return + } + + if (channelSettings.frequency === '0') { + return + } + + emitNet('server:yaca:muteRadioChannel', channel, state) + } + + /** + * Check if a radio channel is muted. + * + * @param channel - The channel number. Defaults to the active channel. + * @returns {boolean} Whether the channel is muted. If the channel does not exist, it will return true. + */ + isRadioChannelMuted(channel: number = this.activeRadioChannel): boolean { + const channelData = this.radioChannelSettings.get(channel) + + if (!channelData) { + return true + } + + return channelData.muted + } + + /** + * Change the active radio channel. + * + * @param {number} channel - The new radio channel. + * @returns {boolean} Whether the channel was changed. + */ + setActiveRadioChannel(channel: number): boolean { + if (!this.clientModule.isPluginInitialized() || !this.radioEnabled) { + return false + } + + emit('yaca:external:changedActiveRadioChannel', channel) + this.activeRadioChannel = channel + this.updateRadioChannelData(this.activeRadioChannel) + + return true + } + + /** + * Change the active radio channel. + * + * @param {number} channel - The new radio channel. + * @returns {boolean} Whether the channel was changed. + */ + setSecondaryRadioChannel(channel: number): boolean { + if (!this.clientModule.isPluginInitialized() || !this.radioEnabled) { + return false + } + + if (this.secondaryRadioChannel === channel) { + this.secondaryRadioChannel = -1 + this.clientModule.notification(locale('secondary_radio_channel_disabled'), YacaNotificationType.INFO) + } else { + this.secondaryRadioChannel = channel + this.clientModule.notification(locale('secondary_radio_channel_enabled', channel), YacaNotificationType.INFO) + } + + emit('yaca:external:changedSecondaryRadioChannel', this.secondaryRadioChannel) + + return true + } + + /** + * Change the volume of the active radio channel. + * + * @param {boolean} higher - Whether to increase the volume. + * @returns {boolean} Whether the volume was changed. + */ + changeRadioChannelVolume(higher: boolean): boolean { + const channel = this.activeRadioChannel + const radioSettings = this.radioChannelSettings.get(channel) + + if (!radioSettings) { + return false + } + + const oldVolume = radioSettings.volume + return this.changeRadioChannelVolumeRaw(oldVolume + (higher ? 0.17 : -0.17), channel) + } + + /** + * Change the volume of a radio channel. + * + * @param {number} channel - The channel number. Defaults to the active channel. + * @param {number} volume - The volume to set. + * @returns {boolean} Whether the volume was changed. + */ + changeRadioChannelVolumeRaw(volume: number, channel: number = this.activeRadioChannel): boolean { + if (!this.clientModule.isPluginInitialized() || !this.radioEnabled) { + return false + } + + const channelSettings = this.radioChannelSettings.get(channel) + if (!channelSettings) { + return false + } + + const oldVolume = channelSettings.volume + channelSettings.volume = clamp(volume, 0, 1) + + // Prevent event emit spams, if nothing changed + if (oldVolume === channelSettings.volume) { + return true + } + + if (channelSettings.volume === 0 || (oldVolume === 0 && channelSettings.volume > 0)) { + emitNet('server:yaca:muteRadioChannel', channel, channelSettings.volume === 0) + } + + // Prevent duplicate update, cuz mute has its own update + if (channelSettings.volume > 0) { + emit('yaca:external:setRadioVolume', channel, channelSettings.volume) + this.updateRadioChannelData(channel) + } + + // Send update to voice plugin + this.clientModule.setCommDeviceVolume(YacaFilterEnum.RADIO, channelSettings.volume, channel) + return true + } + + /** + * Get the volume of a radio channel. + * + * @param channel - The channel number. Defaults to the active channel. + * @returns {number} The volume of the channel. If the channel does not exist, it will return 0. + */ + getRadioChannelVolume(channel: number = this.activeRadioChannel): number { + const channelData = this.radioChannelSettings.get(channel) + + if (!channelData) { + return 0 + } + + return channelData.volume + } + + /** + * Change the stereo mode for the active radio channel. + * + * @param channel - The channel number. Defaults to the active channel. + * @returns {boolean} Whether the stereo mode was changed. + */ + changeRadioChannelStereo(channel: number = this.activeRadioChannel): boolean { + const channelSettings = this.radioChannelSettings.get(channel) + + if (!channelSettings) { + return false + } + + switch (channelSettings.stereo) { + case YacaStereoMode.STEREO: + if (this.changeRadioChannelStereoRaw(YacaStereoMode.MONO_LEFT, channel)) { + this.clientModule.notification(locale('changed_stereo_mode', channel, locale('left_ear')), YacaNotificationType.INFO) + return true + } + break + case YacaStereoMode.MONO_LEFT: + if (this.changeRadioChannelStereoRaw(YacaStereoMode.MONO_RIGHT, channel)) { + this.clientModule.notification(locale('changed_stereo_mode', channel, locale('right_ear')), YacaNotificationType.INFO) + return true + } + break + default: + if (this.changeRadioChannelStereoRaw(YacaStereoMode.STEREO, channel)) { + this.clientModule.notification(locale('changed_stereo_mode', channel, locale('both_ears')), YacaNotificationType.INFO) + return true + } + break + } + + return false + } + + /** + * Change the stereo mode for a radio channel. + * + * @param channel - The channel number. Defaults to the active channel. + * @param stereo - The stereo mode to set. + * @returns {boolean} Whether the stereo mode was changed. + */ + changeRadioChannelStereoRaw(stereo: YacaStereoMode, channel: number = this.activeRadioChannel): boolean { + if (!this.clientModule.isPluginInitialized() || !this.radioEnabled) { + return false + } + + const channelSettings = this.radioChannelSettings.get(channel) + if (!channelSettings) { + return false + } + + channelSettings.stereo = stereo + this.clientModule.setCommDeviceStereoMode(YacaFilterEnum.RADIO, stereo, channel) + + emit('yaca:external:setRadioChannelStereo', channel, stereo.toString()) + + return true + } + + /** + * Get the stereo mode of a radio channel. + * + * @param channel - The channel number. Defaults to the active channel. + * @returns {string} The stereo mode of the channel. + */ + getRadioChannelStereo(channel: number = this.activeRadioChannel): string { + const channelData = this.radioChannelSettings.get(channel) + + if (!channelData) { + return YacaStereoMode.STEREO.toString() + } + + return channelData.stereo.toString() + } + + /** + * Set volume & stereo mode for all radio channels on first start and reconnect. + */ + initRadioSettings() { + for (let i = 1; i <= this.clientModule.sharedConfig.radioSettings.channelCount; i++) { + if (!this.radioChannelSettings.has(i)) { + this.radioChannelSettings.set(i, { + ...this.defaultRadioSettings, + }) + } + if (!this.playersInRadioChannel.has(i)) { + this.playersInRadioChannel.set(i, new Set()) + } + + const { volume, stereo, frequency } = this.radioChannelSettings.get(i) ?? this.defaultRadioSettings + + this.clientModule.setCommDeviceStereoMode(YacaFilterEnum.RADIO, stereo, i) + this.clientModule.setCommDeviceVolume(YacaFilterEnum.RADIO, volume, i) + + if (frequency !== '0') { + emitNet('server:yaca:changeRadioFrequency', i, frequency) + } + } + } + + /** + * Sends an event to the plugin when a player starts or stops talking on the radio. + * + * @param {boolean} state - The state of the player talking on the radio. + * @param {number} channel - The channel number. + */ + radioTalkingStateToPlugin(state: boolean, channel: number) { + const player = this.clientModule.getPlayerByID(cache.serverId) + + if (!player) { + return + } + + this.clientModule.setPlayersCommType(player, YacaFilterEnum.RADIO, state, channel) + } + + /** + * Sends an event to the plugin when a player starts or stops talking on the radio with whisper. + * + * @param state - The state of the player talking on the radio. + * @param targets - The IDs of the targets. + * @param channel - The channel number. + */ + radioTalkingStateToPluginWithWhisper(state: boolean, targets: number[], channel: number) { + const comDeviceTargets = [] + + for (const target of targets) { + const player = this.clientModule.getPlayerByID(target) + if (!player) continue + + comDeviceTargets.push(player) + } + + this.clientModule.setPlayersCommType(comDeviceTargets, YacaFilterEnum.RADIO, state, channel, undefined, CommDeviceMode.SENDER, CommDeviceMode.RECEIVER) + } + + /** + * Finds a radio channel by a given frequency. + * + * @param {string} frequency - The frequency to search for. + * @returns {number | null} The channel number if found, null otherwise. + */ + findRadioChannelByFrequency(frequency: string): number | null { + for (const [channel, data] of this.radioChannelSettings) { + if (data.frequency === frequency) { + return channel + } + } + + return null + } + + /** + * Set the radio frequency. + * + * @param channel - The channel number. + * @param frequency - The frequency to set. + */ + setRadioFrequency(channel: number, frequency: string) { + const channelSettings = this.radioChannelSettings.get(channel) + if (!channelSettings) { + return + } + + if (channelSettings.frequency !== frequency) { + this.disableRadioFromPlayerInChannel(channel) + } + + channelSettings.frequency = frequency + emit('yaca:external:setRadioFrequency', channel, frequency) + + // SaltyChat bridge + if (this.clientModule.saltyChatBridge) { + const saltyFrequency = channelSettings.frequency === '0' ? '' : channelSettings.frequency + emit('SaltyChat_RadioChannelChanged', saltyFrequency, channel === 1) + } + } + + /** + * Disable radio effect for all players in the given channel. + * + * @param {number} channel - The channel number. + */ + disableRadioFromPlayerInChannel(channel: number) { + const players = this.playersInRadioChannel.get(channel) + if (!players || !players.size) { + return + } + + const targets: YacaPlayerData[] = [] + for (const playerId of players) { + const player = this.clientModule.getPlayerByID(playerId) + if (!player || !player.remoteID) { + continue + } + + targets.push(player) + players.delete(player.remoteID) + } + + if (targets.length) { + this.clientModule.setPlayersCommType(targets, YacaFilterEnum.RADIO, false, channel, undefined, CommDeviceMode.RECEIVER, CommDeviceMode.SENDER) + } + } + + /** + * Starts the radio talking state. + * + * @param {boolean} state - The state of the radio talking. + * @param {number} channel - The radio channel. + */ + async radioTalkingStart(state: boolean, channel: number) { + if (channel === -1) return + + if (!state) { + if (this.talkingInChannels.has(channel)) { + this.talkingInChannels.delete(channel) + if (this.radioTowerCalculation.has(channel)) { + clearInterval(this.radioTowerCalculation.get(channel) as CitizenTimer) + this.radioTowerCalculation.delete(channel) + } + + this.clientModule.saltyChatBridge?.handleRadioTalkingStateChange(false, channel) + + if (!this.clientModule.useWhisper) { + this.radioTalkingStateToPlugin(false, channel) + } + + emitNet('server:yaca:radioTalking', false, channel, -1) + emit('yaca:external:isRadioTalking', false, channel) + + StopAnimTask( + cache.ped, + this.clientModule.sharedConfig.radioSettings.animation.dictionary, + this.clientModule.sharedConfig.radioSettings.animation.name, + 4, + ) + RemoveAnimDict(this.clientModule.sharedConfig.radioSettings.animation.dictionary) + + if (this.currentRadioProp !== null) { + if (DoesEntityExist(this.currentRadioProp)) { + DeleteEntity(this.currentRadioProp) + } + + if (this.clientModule.sharedConfig.radioSettings.propWhileTalking.prop !== false) { + SetModelAsNoLongerNeeded(this.clientModule.sharedConfig.radioSettings.propWhileTalking.prop) + } + + this.currentRadioProp = null + } + } + + return + } + + if (this.clientModule.sharedConfig.radioAntiSpamCooldown) { + if (this.radioOnCooldown) { + return + } + + this.radioOnCooldown = true + + setTimeout(() => { + this.radioOnCooldown = false + }, this.clientModule.sharedConfig.radioAntiSpamCooldown) + } + + const channelSettings = this.radioChannelSettings.get(channel) + if (!this.radioEnabled || channelSettings?.frequency === '0' || this.talkingInChannels.has(channel)) { + return + } + + this.talkingInChannels.add(channel) + if (!this.clientModule.useWhisper) { + this.radioTalkingStateToPlugin(true, channel) + } + + if (this.clientModule.sharedConfig.radioSettings.propWhileTalking.prop !== false) { + const prop = await createProp( + this.clientModule.sharedConfig.radioSettings.propWhileTalking.prop, + this.clientModule.sharedConfig.radioSettings.propWhileTalking.boneId, + this.clientModule.sharedConfig.radioSettings.propWhileTalking.position, + this.clientModule.sharedConfig.radioSettings.propWhileTalking.rotation, + ) + + this.currentRadioProp = prop ?? null + } + + const animDict = await requestAnimDict(this.clientModule.sharedConfig.radioSettings.animation.dictionary) + if (animDict) { + TaskPlayAnim( + cache.ped, + animDict, + this.clientModule.sharedConfig.radioSettings.animation.name, + 3, + -4, + -1, + this.clientModule.sharedConfig.radioSettings.animation.flag, + 0.0, + false, + false, + false, + ) + } + + this.clientModule.saltyChatBridge?.handleRadioTalkingStateChange(true, channel) + + this.sendRadioRequestToServer(channel) + if (!this.radioTowerCalculation.has(channel)) { + this.radioTowerCalculation.set( + channel, + setInterval(() => { + this.sendRadioRequestToServer(channel) + }, 1000), + ) + } + + emit('yaca:external:isRadioTalking', true, channel) + } + + /** + * Sends a radio request to the server and calculates the distance to the nearest radio tower. + * + * @param channel - The radio channel to send the request to. + */ + sendRadioRequestToServer(channel: number) { + const distanceToTower = this.getNearestRadioTower() ?? -1 + emitNet('server:yaca:radioTalking', true, channel, distanceToTower) + } + + /** + * Updates the data of the specified radio channel if certain conditions are met. + * + * @param {number} channel - The number of the radio channel to update. + */ + updateRadioChannelData(channel: number) { + if (channel !== this.activeRadioChannel || GetResourceState('yaca-ui') !== 'started') return + + exports['yaca-ui'].setRadioChannelData(this.radioChannelSettings.get(channel)) + } +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/tsconfig.json b/resources/[voice]/yaca-voice/apps/yaca-client/tsconfig.json new file mode 100644 index 000000000..1a891b77f --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-client/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@yaca-voice/typescript-config/fivem.json", + "compilerOptions": { + "baseUrl": "./", + "rootDir": "./src", + "types": ["@citizenfx/client"] + }, + "include": ["./"] +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/build.js b/resources/[voice]/yaca-voice/apps/yaca-server/build.js new file mode 100644 index 000000000..644a07336 --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-server/build.js @@ -0,0 +1,23 @@ +import { build } from 'esbuild' + +const production = process.argv.includes('--mode=production') + +build({ + entryPoints: ['src/index.ts'], + outfile: './dist/server.js', + bundle: true, + loader: { + '.ts': 'ts', + '.js': 'js', + }, + write: true, + platform: 'node', + target: 'node16', + sourcemap: production ? false : 'inline', + dropLabels: production ? ['DEV'] : undefined, +}) + .then(() => { + console.log('Server built successfully') + }) + // skipcq: JS-0263 + .catch(() => process.exit(1)) diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/package.json b/resources/[voice]/yaca-voice/apps/yaca-server/package.json new file mode 100644 index 000000000..b7b01a29e --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-server/package.json @@ -0,0 +1,25 @@ +{ + "name": "yaca-server", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "node build.js --mode=production", + "dev": "node build.js", + "typecheck": "tsc --project tsconfig.json" + }, + "dependencies": { + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "@citizenfx/server": "latest", + "@types/luxon": "^3.4.2", + "@types/node": "^20.16.10", + "@yaca-voice/common": "workspace:*", + "@yaca-voice/types": "workspace:*", + "@yaca-voice/typescript-config": "workspace:*" + }, + "engines": { + "node": ">=16.9.1" + } +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/pnpm-lock.yaml b/resources/[voice]/yaca-voice/apps/yaca-server/pnpm-lock.yaml new file mode 100644 index 000000000..b8135cad3 --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-server/pnpm-lock.yaml @@ -0,0 +1,1136 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@overextended/ox_lib': + specifier: ^3.23.1 + version: 3.24.0 + node-fetch: + specifier: ^3.3.2 + version: 3.3.2 + devDependencies: + '@citizenfx/client': + specifier: latest + version: 2.0.9235-1 + '@eslint/js': + specifier: ^8.57.0 + version: 8.57.0 + '@types/luxon': + specifier: ^3.4.2 + version: 3.4.2 + '@types/node': + specifier: ^20.14.10 + version: 20.14.14 + esbuild: + specifier: ^0.20.2 + version: 0.20.2 + eslint: + specifier: ^8.57.0 + version: 8.57.0 + +packages: + + '@citizenfx/client@2.0.9235-1': + resolution: {integrity: sha512-e1FM4tX0eeY1Y1HuBj8D5JSlDqGmRW9epY24Odib+nk8L7IKRDBzxJBbktXfFsEqF10OJue9v7yx5ACot9CZGQ==} + + '@esbuild/aix-ppc64@0.20.2': + resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.20.2': + resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.20.2': + resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.20.2': + resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.20.2': + resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.20.2': + resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.20.2': + resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.20.2': + resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.20.2': + resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.20.2': + resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.20.2': + resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.20.2': + resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.20.2': + resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.20.2': + resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.20.2': + resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.20.2': + resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.20.2': + resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.20.2': + resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.20.2': + resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.20.2': + resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.20.2': + resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.20.2': + resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.20.2': + resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.4.0': + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.11.0': + resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.0': + resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@humanwhocodes/config-array@0.11.14': + resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@nativewrappers/client@1.7.33': + resolution: {integrity: sha512-phuBBGdDPxZiZyw5CaFs1XWfvllnEtwATMdLaNucwMofVg/O/FjlP1bTUq4SOm4qhSZ4Zdo351ijHzBSIbZs6g==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@overextended/ox_lib@3.24.0': + resolution: {integrity: sha512-diI+hvmGfDBoYzuFeKrVLrdXff1yYIgARNP22i8oQqrzlBig46HJMXm7YRmwsE+8Z/w6TwQv9UL9j/vzVGpnnQ==} + + '@types/luxon@3.4.2': + resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} + + '@types/node@20.14.14': + resolution: {integrity: sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==} + + '@ungap/structured-clone@1.2.0': + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + debug@4.3.6: + resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + esbuild@0.20.2: + resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} + engines: {node: '>=12'} + hasBin: true + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@8.57.0: + resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-printf@1.6.9: + resolution: {integrity: sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg==} + engines: {node: '>=10.0'} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + ignore@5.3.1: + resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} + engines: {node: '>= 4'} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + typescript@5.5.4: + resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@citizenfx/client@2.0.9235-1': {} + + '@esbuild/aix-ppc64@0.20.2': + optional: true + + '@esbuild/android-arm64@0.20.2': + optional: true + + '@esbuild/android-arm@0.20.2': + optional: true + + '@esbuild/android-x64@0.20.2': + optional: true + + '@esbuild/darwin-arm64@0.20.2': + optional: true + + '@esbuild/darwin-x64@0.20.2': + optional: true + + '@esbuild/freebsd-arm64@0.20.2': + optional: true + + '@esbuild/freebsd-x64@0.20.2': + optional: true + + '@esbuild/linux-arm64@0.20.2': + optional: true + + '@esbuild/linux-arm@0.20.2': + optional: true + + '@esbuild/linux-ia32@0.20.2': + optional: true + + '@esbuild/linux-loong64@0.20.2': + optional: true + + '@esbuild/linux-mips64el@0.20.2': + optional: true + + '@esbuild/linux-ppc64@0.20.2': + optional: true + + '@esbuild/linux-riscv64@0.20.2': + optional: true + + '@esbuild/linux-s390x@0.20.2': + optional: true + + '@esbuild/linux-x64@0.20.2': + optional: true + + '@esbuild/netbsd-x64@0.20.2': + optional: true + + '@esbuild/openbsd-x64@0.20.2': + optional: true + + '@esbuild/sunos-x64@0.20.2': + optional: true + + '@esbuild/win32-arm64@0.20.2': + optional: true + + '@esbuild/win32-ia32@0.20.2': + optional: true + + '@esbuild/win32-x64@0.20.2': + optional: true + + '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': + dependencies: + eslint: 8.57.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.11.0': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.3.6 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.0': {} + + '@humanwhocodes/config-array@0.11.14': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.3.6 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@nativewrappers/client@1.7.33': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@overextended/ox_lib@3.24.0': + dependencies: + '@nativewrappers/client': 1.7.33 + csstype: 3.1.3 + fast-printf: 1.6.9 + typescript: 5.5.4 + + '@types/luxon@3.4.2': {} + + '@types/node@20.14.14': + dependencies: + undici-types: 5.26.5 + + '@ungap/structured-clone@1.2.0': {} + + acorn-jsx@5.3.2(acorn@8.12.1): + dependencies: + acorn: 8.12.1 + + acorn@8.12.1: {} + + ajv@6.12.6: + 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-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + balanced-match@1.0.2: {} + + boolean@3.2.0: {} + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + callsites@3.1.0: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.1.3: {} + + data-uri-to-buffer@4.0.1: {} + + debug@4.3.6: + dependencies: + ms: 2.1.2 + + deep-is@0.1.4: {} + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + esbuild@0.20.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.20.2 + '@esbuild/android-arm': 0.20.2 + '@esbuild/android-arm64': 0.20.2 + '@esbuild/android-x64': 0.20.2 + '@esbuild/darwin-arm64': 0.20.2 + '@esbuild/darwin-x64': 0.20.2 + '@esbuild/freebsd-arm64': 0.20.2 + '@esbuild/freebsd-x64': 0.20.2 + '@esbuild/linux-arm': 0.20.2 + '@esbuild/linux-arm64': 0.20.2 + '@esbuild/linux-ia32': 0.20.2 + '@esbuild/linux-loong64': 0.20.2 + '@esbuild/linux-mips64el': 0.20.2 + '@esbuild/linux-ppc64': 0.20.2 + '@esbuild/linux-riscv64': 0.20.2 + '@esbuild/linux-s390x': 0.20.2 + '@esbuild/linux-x64': 0.20.2 + '@esbuild/netbsd-x64': 0.20.2 + '@esbuild/openbsd-x64': 0.20.2 + '@esbuild/sunos-x64': 0.20.2 + '@esbuild/win32-arm64': 0.20.2 + '@esbuild/win32-ia32': 0.20.2 + '@esbuild/win32-x64': 0.20.2 + + escape-string-regexp@4.0.0: {} + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint@8.57.0: + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/regexpp': 4.11.0 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.0 + '@humanwhocodes/config-array': 0.11.14 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.6 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.1 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.12.1 + acorn-jsx: 5.3.2(acorn@8.12.1) + eslint-visitor-keys: 3.4.3 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-printf@1.6.9: + dependencies: + boolean: 3.2.0 + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.3.1: {} + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + fs.realpath@1.0.0: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + ignore@5.3.1: {} + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-path-inside@3.0.3: {} + + isexe@2.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + ms@2.1.2: {} + + natural-compare@1.4.0: {} + + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + prelude-ls@1.2.1: {} + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + resolve-from@4.0.0: {} + + reusify@1.0.4: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + text-table@0.2.0: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + typescript@5.5.4: {} + + undici-types@5.26.5: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + web-streams-polyfill@3.3.3: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrappy@1.0.2: {} + + yocto-queue@0.1.0: {} diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/src/bridge/saltychat.ts b/resources/[voice]/yaca-voice/apps/yaca-server/src/bridge/saltychat.ts new file mode 100644 index 000000000..7c057f07b --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-server/src/bridge/saltychat.ts @@ -0,0 +1,165 @@ +import { saltyChatExport } from '@yaca-voice/common' +import { cache } from '../utils' +import type { YaCAServerModule } from '../yaca' + +/** + * The SaltyChat bridge for the server. + */ +export class YaCAServerSaltyChatBridge { + serverModule: YaCAServerModule + + private callMap = new Map>() + + /** + * Creates an instance of the SaltyChat bridge. + * + * @param {YaCAServerModule} serverModule - The server module. + */ + constructor(serverModule: YaCAServerModule) { + this.serverModule = serverModule + + this.registerSaltyChatEvents() + + console.log('[YaCA] SaltyChat bridge loaded') + + on('onResourceStop', (resourceName: string) => { + if (cache.resource !== resourceName) { + return + } + + emit('onServerResourceStop', 'saltychat') + }) + } + + /** + * Register SaltyChat events. + */ + registerSaltyChatEvents() { + saltyChatExport('GetPlayerAlive', (netId: number) => { + this.serverModule.getPlayerAliveStatus(netId) + }) + + saltyChatExport('SetPlayerAlive', (netId: number, isAlive: boolean) => { + this.serverModule.changePlayerAliveStatus(netId, isAlive) + }) + + saltyChatExport('GetPlayerVoiceRange', (netId: number) => { + this.serverModule.getPlayerVoiceRange(netId) + }) + + saltyChatExport('SetPlayerVoiceRange', (netId: number, voiceRange: number) => { + this.serverModule.changeVoiceRange(netId, voiceRange) + }) + + saltyChatExport('AddPlayerToCall', (callIdentifier: string, playerHandle: number) => this.addPlayerToCall(callIdentifier, playerHandle)) + + saltyChatExport('AddPlayersToCall', (callIdentifier: string, playerHandles: number[]) => this.addPlayerToCall(callIdentifier, playerHandles)) + + saltyChatExport('RemovePlayerFromCall', (callIdentifier: string, playerHandle: number) => this.removePlayerFromCall(callIdentifier, playerHandle)) + + saltyChatExport('RemovePlayersFromCall', (callIdentifier: string, playerHandles: number[]) => this.removePlayerFromCall(callIdentifier, playerHandles)) + + saltyChatExport('SetPhoneSpeaker', (playerHandle: number, toggle: boolean) => { + this.serverModule.phoneModule.enablePhoneSpeaker(playerHandle, toggle) + }) + + saltyChatExport('SetPlayerRadioSpeaker', () => { + console.warn('SetPlayerRadioSpeaker is not implemented in YaCA') + }) + + saltyChatExport('GetPlayersInRadioChannel', (radioChannelName: string) => this.serverModule.radioModule.getPlayersInRadioFrequency(radioChannelName)) + + saltyChatExport('SetPlayerRadioChannel', (netId: number, radioChannelName: string, primary = true) => { + const channel = primary ? 1 : 2 + const newRadioChannelName = radioChannelName === '' ? '0' : radioChannelName + + this.serverModule.radioModule.changeRadioFrequency(netId, channel, newRadioChannelName) + }) + + saltyChatExport('RemovePlayerRadioChannel', (netId: number, primary: boolean) => { + const channel = primary ? 1 : 2 + this.serverModule.radioModule.changeRadioFrequency(netId, channel, '0') + }) + + saltyChatExport('SetRadioTowers', () => { + console.warn('SetRadioTowers is not implemented in YaCA') + }) + + saltyChatExport('EstablishCall', (callerId: number, targetId: number) => { + this.serverModule.phoneModule.callPlayer(callerId, targetId, true) + }) + + saltyChatExport('EndCall', (callerId: number, targetId: number) => { + this.serverModule.phoneModule.callPlayer(callerId, targetId, false) + }) + } + + /** + * Add a player to a call. + * + * @param callIdentifier - The call identifier. + * @param playerHandle - The player handles. + */ + addPlayerToCall(callIdentifier: string, playerHandle: number | number[]) { + if (!Array.isArray(playerHandle)) { + playerHandle = [playerHandle] + } + + const currentlyInCall = this.callMap.get(callIdentifier) ?? new Set() + const newInCall = new Set() + + for (const player of playerHandle) { + if (!currentlyInCall.has(player)) { + currentlyInCall.add(player) + newInCall.add(player) + } + } + + this.callMap.set(callIdentifier, currentlyInCall) + + for (const player of currentlyInCall) { + for (const otherPlayer of newInCall) { + if (player !== otherPlayer) { + this.serverModule.phoneModule.callPlayer(player, otherPlayer, true) + } + } + } + } + + /** + * Remove a player from a call. + * + * @param callIdentifier - The call identifier. + * @param playerHandle - The player handles. + */ + removePlayerFromCall(callIdentifier: string, playerHandle: number | number[]) { + if (!Array.isArray(playerHandle)) { + playerHandle = [playerHandle] + } + + const beforeInCall = this.callMap.get(callIdentifier) + if (!beforeInCall) { + return + } + + const nowInCall = new Set(beforeInCall) + + const removedFromCall = new Set() + for (const player of playerHandle) { + if (beforeInCall.has(player)) { + nowInCall.delete(player) + removedFromCall.add(player) + } + } + + this.callMap.set(callIdentifier, nowInCall) + + for (const player of removedFromCall) { + for (const otherPlayer of beforeInCall) { + if (player !== otherPlayer) { + this.serverModule.phoneModule.callPlayer(player, otherPlayer, false) + } + } + } + } +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/src/index.ts b/resources/[voice]/yaca-voice/apps/yaca-server/src/index.ts new file mode 100644 index 000000000..5794f3376 --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-server/src/index.ts @@ -0,0 +1,5 @@ +/// + +import { YaCAServerModule } from 'src/yaca' + +new YaCAServerModule() diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/cache.ts b/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/cache.ts new file mode 100644 index 000000000..80aeb2e02 --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/cache.ts @@ -0,0 +1,8 @@ +import type { ServerCache } from '@yaca-voice/types' + +/** + * Cached values for the server. + */ +export const cache: ServerCache = { + resource: GetCurrentResourceName(), +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/events.ts b/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/events.ts new file mode 100644 index 000000000..e7585c8b2 --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/events.ts @@ -0,0 +1,23 @@ +/** + * Send a event to one or multiple clients. + * + * @param eventName - The name of the event. + * @param targetIds - The target ids. + * @param args - The arguments to send. + */ +export const triggerClientEvent = (eventName: string, targetIds: number[] | number, ...args: unknown[]) => { + if (!Array.isArray(targetIds)) { + targetIds = [targetIds] + } + + if (targetIds.length < 1) { + return + } + + // @ts-expect-error - msgpack_pack is not typed but available in the global scope. + const dataSerialized = msgpack_pack(args) + + for (const targetId of targetIds) { + TriggerClientEventInternal(eventName, targetId.toString(), dataSerialized, dataSerialized.length) + } +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/generator.ts b/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/generator.ts new file mode 100644 index 000000000..e31c08f6e --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/generator.ts @@ -0,0 +1,34 @@ +import { randomUUID } from 'node:crypto' + +/** + * Generate a random name and insert it into the database. + * + * @param src The ID of the player. + * @param nameSet The set of names to check against. + * @param namePattern The pattern to use for the name. + */ +export function generateRandomName(src: number, nameSet: Set, namePattern: string): string | undefined { + let name: string | undefined + + const playerName = GetPlayerName(src.toString()) + + for (let i = 0; i < 10; i++) { + let generatedName = namePattern + generatedName = generatedName.replace('{serverid}', src.toString()) + generatedName = generatedName.replace('{playername}', playerName) + generatedName = generatedName.replace('{guid}', randomUUID().replace(/-/g, '')) + generatedName = generatedName.slice(0, 30) + + if (!nameSet.has(generatedName)) { + name = generatedName + nameSet.add(generatedName) + break + } + } + + if (!name) { + console.error(`YaCA: Couldn't generate a random name for player ${playerName} (ID: ${src}).`) + } + + return name +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/index.ts b/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/index.ts new file mode 100644 index 000000000..4ae2ca2a3 --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/index.ts @@ -0,0 +1,4 @@ +export * from './cache' +export * from './events' +export * from './generator' +export * from './versioncheck' diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/versioncheck.ts b/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/versioncheck.ts new file mode 100644 index 000000000..41f7b30b7 --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/versioncheck.ts @@ -0,0 +1,57 @@ +import fetch from 'node-fetch' +import { cache } from './cache' + +/** + * Checks the version of the resource against the latest release on GitHub. + * If the resource is outdated, a message will be printed to the console. + */ +export const checkVersion = async () => { + const currentVersion = GetResourceMetadata(cache.resource, 'version', 0) + + if (!currentVersion) { + console.error('[YaCA] Version check failed, no version found in resource manifest.') + return + } + + const parsedVersion = currentVersion.match(/\d+\.\d+\.\d+/g) + + if (!parsedVersion) { + console.error('[YaCA] Version check failed, version in resource manifest is not in the correct format.') + return + } + + const response = await fetch('https://api.github.com/repos/yaca-systems/fivem-yaca-typescript/releases/latest') + if (response.status !== 200) { + console.error('[YaCA] Version check failed, unable to fetch latest release.') + return + } + + const data = (await response.json()) as { tag_name: string; html_url: string } + + const latestVersion = data.tag_name + if (!latestVersion && latestVersion === currentVersion) { + console.log('[YaCA] You are running the latest version of YaCA.') + return + } + + const parsedLatestVersion = latestVersion.match(/\d+\.\d+\.\d+/g) + if (!parsedLatestVersion) { + console.error('[YaCA] Version check failed, latest release is not in the correct format.') + return + } + + for (let i = 0; i < parsedVersion.length; i++) { + const current = Number.parseInt(parsedVersion[i]) + const latest = Number.parseInt(parsedLatestVersion[i]) + + if (current !== latest) { + if (current < latest) { + console.error( + `[YaCA] You are running an outdated version of YaCA. (current: ${currentVersion}, latest: ${latestVersion}) \r\n ${data.html_url}`, + ) + } else { + break + } + } + } +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/index.ts b/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/index.ts new file mode 100644 index 000000000..42a2b56f6 --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/index.ts @@ -0,0 +1,4 @@ +export * from './main' +export * from './megaphone' +export * from './phone' +export * from './radio' diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/main.ts b/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/main.ts new file mode 100644 index 000000000..0f25a8cf5 --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/main.ts @@ -0,0 +1,394 @@ +import { GLOBAL_ERROR_LEVEL_STATE_NAME, getGlobalErrorLevel, initLocale, loadConfig, setGlobalErrorLevel, VOICE_RANGE_STATE_NAME } from '@yaca-voice/common' +import { + type DataObject, + defaultServerConfig, + defaultSharedConfig, + defaultTowerConfig, + type ServerCache, + type YacaServerConfig, + type YacaSharedConfig, + type YacaTowerConfig, +} from '@yaca-voice/types' +import { YaCAServerSaltyChatBridge } from '../bridge/saltychat' +import { checkVersion, generateRandomName } from '../utils' +import { triggerClientEvent } from '../utils/events' +import { YaCAServerMegaphoneModule } from './megaphone' +import { YaCAServerPhoneModle } from './phone' +import { YaCAServerRadioModule } from './radio' + +/** + * The player data type for YaCA. + */ +export type YaCAPlayer = { + voiceSettings: { + voiceFirstConnect: boolean + forceMuted: boolean + ingameName: string + mutedOnPhone: boolean + inCallWith: Set + emittedPhoneSpeaker: Map> + } + radioSettings: { + activated: boolean + hasLong: boolean + frequencies: Record + } + voicePlugin?: { + playerId: number + clientId: number + forceMuted: boolean + mutedOnPhone: boolean + } +} + +/** + * The main server module for YaCA. + */ +export class YaCAServerModule { + cache: ServerCache + + nameSet: Set = new Set() + players: Map = new Map() + + defaultVoiceRange: number + + serverConfig: YacaServerConfig + sharedConfig: YacaSharedConfig + towerConfig: YacaTowerConfig + + phoneModule: YaCAServerPhoneModle + radioModule: YaCAServerRadioModule + megaphoneModule: YaCAServerMegaphoneModule + + saltChatBridge?: YaCAServerSaltyChatBridge + + /** + * Creates an instance of the server module. + */ + constructor() { + console.log('~g~ --> YaCA: Server loaded') + + this.serverConfig = loadConfig('config/server.json5', defaultServerConfig) + this.sharedConfig = loadConfig('config/shared.json5', defaultSharedConfig) + this.towerConfig = loadConfig('config/tower.json5', defaultTowerConfig) + + initLocale(this.sharedConfig.locale) + + if (this.sharedConfig.voiceRange.ranges[this.sharedConfig.voiceRange.defaultIndex]) { + this.defaultVoiceRange = this.sharedConfig.voiceRange.ranges[this.sharedConfig.voiceRange.defaultIndex] + } else { + this.defaultVoiceRange = 1 + this.sharedConfig.voiceRange.ranges = [1] + + console.error('[YaCA] Default voice range is not set correctly in the config.') + } + + this.phoneModule = new YaCAServerPhoneModle(this) + this.radioModule = new YaCAServerRadioModule(this) + this.megaphoneModule = new YaCAServerMegaphoneModule(this) + + this.registerExports() + this.registerEvents() + + if (this.sharedConfig.saltyChatBridge) { + this.saltChatBridge = new YaCAServerSaltyChatBridge(this) + } + + if (this.sharedConfig.versionCheck) { + checkVersion().then() + } + + GlobalState.set(GLOBAL_ERROR_LEVEL_STATE_NAME, 0, true) + } + + /** + * Get the player data for a specific player. + */ + getPlayer(playerId: number): YaCAPlayer | undefined { + return this.players.get(playerId) + } + + /** + * Initialize the player on first connect. + * + * @param {number} src - The source-id of the player to initialize. + */ + connectToVoice(src: number) { + const name = generateRandomName(src, this.nameSet, this.serverConfig.userNamePattern) + if (!name) { + DropPlayer(src.toString(), '[YaCA] Failed to generate a random name.') + return + } + + const playerState = Player(src).state + playerState.set(VOICE_RANGE_STATE_NAME, this.defaultVoiceRange, true) + + this.players.set(src, { + voiceSettings: { + voiceFirstConnect: false, + forceMuted: false, + ingameName: name, + mutedOnPhone: false, + inCallWith: new Set(), + emittedPhoneSpeaker: new Map>(), + }, + radioSettings: { + activated: false, + hasLong: true, + frequencies: {}, + }, + }) + + this.connect(src) + } + + /** + * Register all exports for the YaCA module. + */ + registerExports() { + exports('connectToVoice', (src: number) => this.connectToVoice(src)) + /** + * Get the alive status of a player. + * + * @param {number} playerId - The ID of the player to get the alive status for. + * @returns {boolean} - The alive status of the player. + */ + exports('getPlayerAliveStatus', (playerId: number) => this.getPlayerAliveStatus(playerId)) + + /** + * Set the alive status of a player. + * + * @param {number} playerId - The ID of the player to set the alive status for. + * @param {boolean} state - The new alive status. + */ + exports('setPlayerAliveStatus', (playerId: number, state: boolean) => this.changePlayerAliveStatus(playerId, state)) + + /** + * Get the voice range of a player. + * + * @param {number} playerId - The ID of the player to get the voice range for. + * @returns {number} - The voice range of the player. + */ + exports('getPlayerVoiceRange', (playerId: number) => this.getPlayerVoiceRange(playerId)) + + /** + * Set the voice range of a player. + * + * @param {number} playerId - The ID of the player to set the voice range for. + */ + exports('setPlayerVoiceRange', (playerId: number, range: number) => this.changeVoiceRange(playerId, range)) + + /** + * Set the global error level. + * + * @param {number} errorLevel - The new error level. Between 0 and 1. + */ + exports('setGlobalErrorLevel', (errorLevel: number) => setGlobalErrorLevel(errorLevel)) + + /** + * Get the global error level. + * + * @returns {number} - The global error level. + */ + exports('getGlobalErrorLevel', () => getGlobalErrorLevel()) + } + + /** + * Register all events for the YaCA module. + */ + registerEvents() { + // FiveM: player dropped + on('playerDropped', (_reason: string) => { + this.handlePlayerDisconnect(source) + }) + + // YaCA: connect to voice when NUI is ready + onNet('server:yaca:nuiReady', () => { + if (!this.sharedConfig.autoConnectOnJoin) return + this.connectToVoice(source) + }) + + // YaCA:successful voice connection and client-id sync + onNet('server:yaca:addPlayer', (clientId: number) => { + this.addNewPlayer(source, clientId) + }) + + // YaCa: voice restart + onNet('server:yaca:wsReady', () => { + this.playerReconnect(source) + }) + + // TxAdmin: spectate stop event + onNet('txsv:req:spectate:end', () => { + emitNet('client:yaca:txadmin:stopspectate', source) + }) + } + + /** + * Handle various cases if player disconnects. + * + * @param {number} src - The source-id of the player who disconnected. + */ + handlePlayerDisconnect(src: number) { + const player = this.players.get(src) + if (!player) { + return + } + + this.nameSet.delete(player.voiceSettings?.ingameName) + + const allFrequencies = this.radioModule.radioFrequencyMap + for (const [key, value] of allFrequencies) { + value.delete(src) + if (!value.size) { + this.radioModule.radioFrequencyMap.delete(key) + } + } + + for (const [targetId, emitterTargets] of player.voiceSettings.emittedPhoneSpeaker) { + const target = this.players.get(targetId) + if (!target || !target.voicePlugin) { + continue + } + + triggerClientEvent('client:yaca:phoneHearAround', Array.from(emitterTargets), [target.voicePlugin.clientId], false) + } + + emitNet('client:yaca:disconnect', -1, src) + } + + /** + * Syncs player alive status and mute him if he is dead or whatever. + * + * @param {number} src - The source-id of the player to sync. + * @param {boolean} alive - The new alive status. + */ + changePlayerAliveStatus(src: number, alive: boolean) { + const player = this.players.get(src) + if (!player) { + return + } + + player.voiceSettings.forceMuted = !alive + emitNet('client:yaca:muteTarget', -1, src, !alive) + + if (player.voicePlugin) { + player.voicePlugin.forceMuted = !alive + } + } + + /** + * Get the alive status of a player. + * + * @param playerId - The ID of the player to get the alive status for. + */ + getPlayerAliveStatus(playerId: number) { + return this.players.get(playerId)?.voiceSettings.forceMuted ?? false + } + + /** + * Used if a player reconnects to the server. + * + * @param {number} src - The source-id of the player to reconnect. + */ + playerReconnect(src: number) { + const player = this.players.get(src) + if (!player) { + return + } + + if (!player.voiceSettings.voiceFirstConnect) { + return + } + + this.connect(src) + } + + /** + * Change the voice range of a player. + * + * @param {number} src - The source-id of the player to change the voice range for. + * @param {number} range - The new voice range. Defaults to the default voice range if not provided. + */ + changeVoiceRange(src: number, range?: number) { + const playerState = Player(src).state + + playerState.set(VOICE_RANGE_STATE_NAME, range ?? this.defaultVoiceRange, true) + emitNet('client:yaca:changeVoiceRange', src, range) + } + + /** + * Get the voice range of a player. + * + * @param playerId - The ID of the player to get the voice range for. + */ + getPlayerVoiceRange(playerId: number) { + const playerState = Player(playerId).state + return playerState[VOICE_RANGE_STATE_NAME] ?? this.defaultVoiceRange + } + + /** + * Sends initial data needed to connect to teamspeak plugin. + * + * @param {number} src - The source-id of the player to connect + */ + connect(src: number) { + const player = this.players.get(src) + if (!player) { + console.error(`YaCA: Missing player data for ${src}.`) + return + } + + player.voiceSettings.voiceFirstConnect = true + + const initObject: DataObject = { + suid: this.serverConfig.uniqueServerId, + chid: this.serverConfig.ingameChannelId, + deChid: this.serverConfig.defaultChannelId, + channelPassword: this.serverConfig.ingameChannelPassword, + ingameName: player.voiceSettings.ingameName, + useWhisper: this.serverConfig.useWhisper, + excludeChannels: this.serverConfig.excludeChannels, + } + emitNet('client:yaca:init', src, initObject) + } + + /** + * Add new player to all other players on connect or reconnect, so they know about some variables. + * + * @param src - The source-id of the player to add. + * @param {number} clientId - The client ID of the player. + */ + addNewPlayer(src: number, clientId: number) { + const player = this.players.get(src) + if (!player || !clientId) { + return + } + + player.voicePlugin = { + playerId: src, + clientId, + forceMuted: player.voiceSettings.forceMuted, + mutedOnPhone: player.voiceSettings.mutedOnPhone, + } + + emitNet('client:yaca:addPlayers', -1, player.voicePlugin) + + const allPlayersData = [] + for (const playerSource of getPlayers()) { + const intPlayerSource = Number.parseInt(playerSource) + const playerServer = this.players.get(intPlayerSource) + if (!playerServer) { + continue + } + + if (!playerServer.voicePlugin || intPlayerSource === src) { + continue + } + + allPlayersData.push(playerServer.voicePlugin) + } + + emitNet('client:yaca:addPlayers', src, allPlayersData) + } +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/megaphone.ts b/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/megaphone.ts new file mode 100644 index 000000000..2f8423d5f --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/megaphone.ts @@ -0,0 +1,86 @@ +import { MEGAPHONE_STATE_NAME } from '@yaca-voice/common' +import type { YacaSharedConfig } from '@yaca-voice/types' +import type { YaCAServerModule } from './main' + +/** + * The server-side megaphone module. + */ +export class YaCAServerMegaphoneModule { + private serverModule: YaCAServerModule + private sharedConfig: YacaSharedConfig + + /** + * Creates an instance of the megaphone module. + * + * @param serverModule - The server module. + */ + constructor(serverModule: YaCAServerModule) { + this.serverModule = serverModule + this.sharedConfig = serverModule.sharedConfig + + this.registerEvents() + } + + /** + * Register server events. + */ + registerEvents() { + /** + * Changes megaphone state by player + * + * @param {boolean} state - The state of the megaphone effect. + */ + onNet('server:yaca:useMegaphone', (state: boolean) => { + this.playerUseMegaphone(source, state) + }) + + /** + * Handles the "server:yaca:playerLeftVehicle" event. + */ + onNet('server:yaca:playerLeftVehicle', () => { + this.changeMegaphoneState(source, false, true) + }) + } + + /** + * Apply the megaphone effect on a specific player via client event. + * + * @param {number} src - The source-id of the player to apply the megaphone effect to. + * @param {boolean} state - The state of the megaphone effect. + */ + playerUseMegaphone(src: number, state: boolean) { + const player = this.serverModule.getPlayer(src) + if (!player) { + return + } + + const playerState = Player(src).state + + if ((!state && !playerState[MEGAPHONE_STATE_NAME]) || (state && playerState[MEGAPHONE_STATE_NAME])) { + return + } + + this.changeMegaphoneState(src, state) + emit('yaca:external:changeMegaphoneState', src, state) + } + + /** + * Apply the megaphone effect on a specific player. + * + * @param {number} src - The source-id of the player to apply the megaphone effect to. + * @param {boolean} state - The state of the megaphone effect. + * @param {boolean} [forced=false] - Whether the change is forced. Defaults to false if not provided. + */ + changeMegaphoneState(src: number, state: boolean, forced = false) { + const playerState = Player(src).state + + if (!state && playerState[MEGAPHONE_STATE_NAME]) { + playerState.set(MEGAPHONE_STATE_NAME, null, true) + if (forced) { + emitNet('client:yaca:setLastMegaphoneState', src, false) + } + } else if (state && !playerState[MEGAPHONE_STATE_NAME]) { + playerState.set(MEGAPHONE_STATE_NAME, this.sharedConfig.megaphone.range, true) + } + } +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/phone.ts b/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/phone.ts new file mode 100644 index 000000000..62c923edf --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/phone.ts @@ -0,0 +1,278 @@ +import { PHONE_SPEAKER_STATE_NAME } from '@yaca-voice/common' +import { YacaFilterEnum } from '@yaca-voice/types' +import { triggerClientEvent } from '../utils/events' +import type { YaCAServerModule } from './main' + +/** + * The phone module for the server. + */ +export class YaCAServerPhoneModle { + private serverModule: YaCAServerModule + + /** + * Creates an instance of the phone module. + * + * @param {YaCAServerModule} serverModule - The server module. + */ + constructor(serverModule: YaCAServerModule) { + this.serverModule = serverModule + + this.registerEvents() + this.registerExports() + } + + /** + * Register server events. + */ + registerEvents() { + /** + * Handles the "server:yaca:phoneSpeakerEmitWhisper" event. + * + * @param {number[]} enableForTargets - The IDs of the players to enable the phone speaker for. + * @param {number[]} disableForTargets - The IDs of the players to disable the phone speaker for. + */ + onNet('server:yaca:phoneSpeakerEmitWhisper', (enableForTargets?: number[], disableForTargets?: number[]) => { + const player = this.serverModule.players.get(source) + if (!player) { + return + } + + const targets = new Set() + + for (const callTarget of player.voiceSettings.inCallWith) { + const target = this.serverModule.players.get(callTarget) + if (!target) { + continue + } + + targets.add(callTarget) + } + + if (targets.size && enableForTargets?.length) { + triggerClientEvent('client:yaca:playersToPhoneSpeakerEmitWhisper', Array.from(targets), enableForTargets, true) + } + + if (targets.size && disableForTargets?.length) { + triggerClientEvent('client:yaca:playersToPhoneSpeakerEmitWhisper', Array.from(targets), disableForTargets, false) + } + }) + + /** + * Handles the "server:yaca:phoneEmit" event. + * + * @param {number[]} enableForTargets - The IDs of the players to enable the phone speaker for. + * @param {number[]} disableForTargets - The IDs of the players to disable the phone speaker for. + */ + onNet('server:yaca:phoneEmit', (enableForTargets?: number[], disableForTargets?: number[]) => { + if (!this.serverModule.sharedConfig.phoneHearPlayersNearby) return + + const player = this.serverModule.players.get(source) + if (!player) { + return + } + + const enableReceive = new Set() + const disableReceive = new Set() + + if (enableForTargets?.length) { + for (const callTarget of player.voiceSettings.inCallWith) { + const target = this.serverModule.players.get(callTarget) + if (!target) continue + + enableReceive.add(callTarget) + + for (const targetID of enableForTargets) { + const map = player.voiceSettings.emittedPhoneSpeaker + const set = map.get(targetID) ?? new Set() + set.add(callTarget) + map.set(targetID, set) + } + } + } + + if (disableForTargets?.length) { + for (const targetID of disableForTargets) { + const emittedFor = player.voiceSettings.emittedPhoneSpeaker.get(targetID) + if (!emittedFor) continue + + for (const emittedTarget of emittedFor) { + const target = this.serverModule.players.get(emittedTarget) + if (!target) continue + + disableReceive.add(emittedTarget) + } + + player.voiceSettings.emittedPhoneSpeaker.delete(targetID) + } + } + + if (enableReceive.size && enableForTargets?.length) { + const enableForTargetsData = new Set() + + for (const enableTarget of enableForTargets) { + const target = this.serverModule.players.get(enableTarget) + if (!target || !target.voicePlugin) continue + + enableForTargetsData.add(target.voicePlugin.clientId) + } + + triggerClientEvent('client:yaca:phoneHearAround', Array.from(enableReceive), Array.from(enableForTargetsData), true) + } + + if (disableReceive.size && disableForTargets?.length) { + const disableForTargetsData = new Set() + + for (const disableTarget of disableForTargets) { + const target = this.serverModule.players.get(disableTarget) + if (!target || !target.voicePlugin) continue + + disableForTargetsData.add(target.voicePlugin.clientId) + } + + triggerClientEvent('client:yaca:phoneHearAround', Array.from(disableReceive), Array.from(disableForTargetsData), false) + } + }) + } + + registerExports() { + /** + * Creates a phone call between two players. + * + * @param {number} src - The player who is making the call. + * @param {number} target - The player who is being called. + * @param {boolean} state - The state of the call. + */ + exports('callPlayer', (src: number, target: number, state: boolean) => this.callPlayer(src, target, state)) + + /** + * Creates a phone call between two players with the old effect. + * + * @param {number} src - The player who is making the call. + * @param {number} target - The player who is being called. + * @param {boolean} state - The state of the call. + */ + exports('callPlayerOldEffect', (src: number, target: number, state: boolean) => this.callPlayer(src, target, state, YacaFilterEnum.PHONE_HISTORICAL)) + + /** + * Mute a player during a phone call. + * + * @param {number} src - The source-id of the player to mute. + * @param {boolean} state - The mute state. + */ + exports('muteOnPhone', (src: number, state: boolean) => this.muteOnPhone(src, state)) + + /** + * Enable or disable the phone speaker for a player. + * + * @param {number} src - The source-id of the player to enable the phone speaker for. + * @param {boolean} state - The state of the phone speaker. + */ + exports('enablePhoneSpeaker', (src: number, state: boolean) => this.enablePhoneSpeaker(src, state)) + + /** + * Is player in a phone call. + * + * @param {number} src - The source-id of the player to check. + */ + exports('isPlayerInCall', (src: number): [boolean, number[]] => { + const player = this.serverModule.players.get(src) + if (!player) { + return [false, []] + } + + return [player.voiceSettings.inCallWith.size > 0, [...player.voiceSettings.inCallWith]] + }) + } + + /** + * Call another player. + * + * @param {number} src - The player who is making the call. + * @param {number} target - The player who is being called. + * @param {boolean} state - The state of the call. + * @param {YacaFilterEnum} filter - The filter to use for the call. Defaults to PHONE if not provided. + */ + callPlayer(src: number, target: number, state: boolean, filter: YacaFilterEnum = YacaFilterEnum.PHONE) { + const player = this.serverModule.getPlayer(src) + const targetPlayer = this.serverModule.getPlayer(target) + if (!player || !targetPlayer) { + return + } + + emitNet('client:yaca:phone', target, src, state, filter) + emitNet('client:yaca:phone', src, target, state, filter) + + const playerState = Player(src).state + const targetState = Player(target).state + + if (state) { + player.voiceSettings.inCallWith.add(target) + targetPlayer.voiceSettings.inCallWith.add(src) + + if (playerState[PHONE_SPEAKER_STATE_NAME]) { + this.enablePhoneSpeaker(src, true) + } + + if (targetState[PHONE_SPEAKER_STATE_NAME]) { + this.enablePhoneSpeaker(target, true) + } + } else { + this.muteOnPhone(src, false, true) + this.muteOnPhone(target, false, true) + + player.voiceSettings.inCallWith.delete(target) + targetPlayer.voiceSettings.inCallWith.delete(src) + + if (playerState[PHONE_SPEAKER_STATE_NAME]) { + this.enablePhoneSpeaker(src, false) + } + + if (targetState[PHONE_SPEAKER_STATE_NAME]) { + this.enablePhoneSpeaker(target, false) + } + } + + emit('yaca:external:phoneCall', src, target, state, filter) + } + + /** + * Mute a player during a phone call. + * + * @param {number} src - The source-id of the player to mute. + * @param {boolean} state - The mute state. + * @param {boolean} [onCallStop=false] - Whether the call has stopped. Defaults to false if not provided. + */ + muteOnPhone(src: number, state: boolean, onCallStop = false) { + const player = this.serverModule.getPlayer(src) + if (!player) { + return + } + + player.voiceSettings.mutedOnPhone = state + emitNet('client:yaca:phoneMute', -1, src, state, onCallStop) + emit('yaca:external:phoneMute', src, state) + } + + /** + * Enable or disable the phone speaker for a player. + * + * @param {number} src - The source-id of the player to enable the phone speaker for. + * @param {boolean} state - The state of the phone speaker. + */ + enablePhoneSpeaker(src: number, state: boolean) { + const player = this.serverModule.getPlayer(src) + if (!player) { + return + } + + const playerState = Player(src).state + + if (state && player.voiceSettings.inCallWith.size) { + playerState.set(PHONE_SPEAKER_STATE_NAME, Array.from(player.voiceSettings.inCallWith), true) + emit('yaca:external:phoneSpeaker', src, true) + } else { + playerState.set(PHONE_SPEAKER_STATE_NAME, null, true) + emit('yaca:external:phoneSpeaker', src, false) + } + } +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/radio.ts b/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/radio.ts new file mode 100644 index 000000000..cb3679a7b --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/radio.ts @@ -0,0 +1,379 @@ +import { locale } from '@yaca-voice/common' +import { YacaNotificationType, type YacaServerConfig, type YacaSharedConfig } from '@yaca-voice/types' +import { triggerClientEvent } from '../utils/events' +import type { YaCAServerModule } from './main' + +/** + * The server-side radio module. + */ +export class YaCAServerRadioModule { + private serverModule: YaCAServerModule + private sharedConfig: YacaSharedConfig + private serverConfig: YacaServerConfig + + radioFrequencyMap = new Map>() + + /** + * Creates an instance of the radio module. + * + * @param {YaCAServerModule} serverModule - The server module. + */ + constructor(serverModule: YaCAServerModule) { + this.serverModule = serverModule + this.sharedConfig = serverModule.sharedConfig + this.serverConfig = serverModule.serverConfig + + this.registerEvents() + this.registerExports() + } + + /** + * Register server events. + */ + registerEvents() { + /** + * Handles the "server:yaca:enableRadio" server event. + * + * @param {boolean} state - The state of the radio. + */ + onNet('server:yaca:enableRadio', (state: boolean) => { + this.enableRadio(source, state) + }) + + /** + * Handles the "server:yaca:changeRadioFrequency" server event. + * + * @param {number} channel - The channel to change the frequency of. + * @param {string} frequency - The new frequency. + */ + onNet('server:yaca:changeRadioFrequency', (channel: number, frequency: string) => { + this.changeRadioFrequency(source, channel, frequency) + }) + + /** + * Handles the "server:yaca:muteRadioChannel" server event. + * + * @param {number} channel - The channel to mute. + */ + onNet('server:yaca:muteRadioChannel', (channel: number, state?: boolean) => { + this.radioChannelMute(source, channel, state) + }) + + /** + * Handles the "server:yaca:radioTalking" server event. + * + * @param {boolean} state - The state of the radio. + * @param {number} channel - The channel to change the talking state for. + * @param {number} distanceToTower - The distance to the tower. + */ + onNet('server:yaca:radioTalking', (state: boolean, channel: number, distanceToTower = -1) => { + this.radioTalkingState(source, state, channel, distanceToTower) + }) + } + + /** + * Register server exports. + */ + registerExports() { + /** + * Get all players in a radio frequency. + * + * @param {string} frequency - The frequency to get the players for. + * @returns {number[]} - The players in the radio frequency. + */ + exports('getPlayersInRadioFrequency', (frequency: string) => this.getPlayersInRadioFrequency(frequency)) + + /** + * Set the radio channel for a player. + * + * @param {number} src - The player to set the radio channel for. + * @param {number} channel - The channel to set. + * @param {string} frequency - The frequency to set. + */ + exports('setPlayerRadioChannel', (src: number, channel: number, frequency: string) => this.changeRadioFrequency(src, channel, frequency)) + + /** + * Get if a player has long range radio. + * + * @param {number} src - The player to set the long range radio for. + */ + exports('getPlayerHasLongRange', (src: number) => this.getPlayerHasLongRange(src)) + + /** + * Set if a player has long range radio. + * + * @param {number} src - The player to set the long range radio for. + * @param {boolean} state - The new state of the long range radio. + */ + exports('setPlayerHasLongRange', (src: number, state: boolean) => this.setPlayerHasLongRange(src, state)) + } + + /** + * Get all players in a radio frequency. + * + * @param frequency - The frequency to get the players for. + */ + getPlayersInRadioFrequency(frequency: string) { + const allPlayersInChannel = this.radioFrequencyMap.get(frequency) + const playersArray: number[] = [] + + if (!allPlayersInChannel) { + return playersArray + } + + for (const [key] of allPlayersInChannel) { + const target = this.serverModule.getPlayer(key) + if (!target) { + continue + } + playersArray.push(key) + } + return playersArray + } + + /** + * Gets if a player has long range radio. + * + * @param src - The player to get the long range radio for. + */ + getPlayerHasLongRange(src: number) { + const player = this.serverModule.getPlayer(src) + if (!player) { + return false + } + + return player.radioSettings.hasLong + } + + /** + * Sets if a player has long range radio. + * + * @param src - The player to set the long range radio for. + * @param state - The new state of the long range radio. + */ + setPlayerHasLongRange(src: number, state: boolean) { + const player = this.serverModule.getPlayer(src) + if (!player) { + return + } + + player.radioSettings.hasLong = state + } + + /** + * Enable or disable the radio for a player. + * + * @param {number} src - The player to enable or disable the radio for. + * @param {boolean} state - The new state of the radio. + */ + enableRadio(src: number, state: boolean) { + const player = this.serverModule.getPlayer(src) + if (!player) { + return + } + + player.radioSettings.activated = state + + emit('yaca:export:enabledRadio', src, state) + } + + /** + * Change the radio frequency for a player. + * + * @param {number} src - The player to change the radio frequency for. + * @param {number} channel - The channel to change the frequency of. + * @param {string} frequency - The new frequency. + */ + changeRadioFrequency(src: number, channel: number, frequency: string) { + const player = this.serverModule.getPlayer(src) + if (!player) { + return + } + + if (!player.radioSettings.activated) { + emitNet('client:yaca:notification', src, locale('radio_not_activated'), YacaNotificationType.ERROR) + return + } + + if (Number.isNaN(channel) || channel < 1 || channel > this.sharedConfig.radioSettings.channelCount) { + emitNet('client:yaca:notification', src, locale('radio_channel_invalid'), YacaNotificationType.ERROR) + return + } + + const oldFrequency = player.radioSettings.frequencies[channel] + + // Leave the old frequency if the new one is 0 + if (frequency === '0') { + this.leaveRadioFrequency(src, channel, oldFrequency) + return + } + + // Leave the old frequency if it's different from the new one + if (oldFrequency !== frequency) { + this.leaveRadioFrequency(src, channel, oldFrequency) + } + + // Add player to channel map, so we know who is in which channel + if (!this.radioFrequencyMap.has(frequency)) { + this.radioFrequencyMap.set(frequency, new Map()) + } + this.radioFrequencyMap.get(frequency)?.set(src, { muted: false }) + + player.radioSettings.frequencies[channel] = frequency + + emitNet('client:yaca:setRadioFreq', src, channel, frequency) + emit('yaca:external:changedRadioFrequency', src, channel, frequency) + + /* + * TODO: Add radio effect to player in new frequency + * const newPlayers = this.getPlayersInRadioFrequency(frequency); + * if (newPlayers.length) alt.emitClientRaw(newPlayers, "client:yaca:setRadioEffectInFrequency", frequency, player.id); + */ + } + + /** + * Make a player leave a radio frequency. + * + * @param {number} src - The player to leave the radio frequency. + * @param {number} channel - The channel to leave. + * @param {string} frequency - The frequency to leave. + */ + leaveRadioFrequency(src: number, channel: number, frequency: string) { + const player = this.serverModule.getPlayer(src) + if (!player) { + return + } + + const allPlayersInChannel = this.radioFrequencyMap.get(frequency) + if (!allPlayersInChannel) { + return + } + + player.radioSettings.frequencies[channel] = '0' + + const playersArray = [] + const allTargets = [] + for (const [key] of allPlayersInChannel) { + const target = this.serverModule.getPlayer(key) + if (!target) { + continue + } + + playersArray.push(key) + + if (key === src) { + continue + } + + allTargets.push(key) + } + + if (this.serverConfig.useWhisper) { + emitNet('client:yaca:radioTalking', src, allTargets, frequency, false, null, true) + } else if (player.voicePlugin) { + triggerClientEvent('client:yaca:leaveRadioChannel', playersArray, player.voicePlugin.clientId, frequency) + } + + allPlayersInChannel.delete(src) + if (!allPlayersInChannel.size) { + this.radioFrequencyMap.delete(frequency) + } + } + + /** + * Mute a radio channel for a player. + * + * @param {number} src - The player to mute the radio channel for. + * @param {number} channel - The channel to mute. + */ + radioChannelMute(src: number, channel: number, state?: boolean) { + const player = this.serverModule.getPlayer(src) + if (!player) { + return + } + + const radioFrequency = player.radioSettings.frequencies[channel] + const foundPlayer = this.radioFrequencyMap.get(radioFrequency)?.get(src) + if (!foundPlayer) { + return + } + + foundPlayer.muted = typeof state !== 'undefined' ? state : !foundPlayer.muted + emitNet('client:yaca:setRadioMuteState', src, channel, foundPlayer.muted) + emit('yaca:external:changedRadioMuteState', src, channel, foundPlayer.muted) + } + + /** + * Change the talking state of a player on the radio. + * + * @param {number} src - The player to change the talking state for. + * @param {boolean} state - The new talking state. + * @param {number} channel - The channel to change the talking state for. + * @param {number} distanceToTower - The distance to the tower. + */ + radioTalkingState(src: number, state: boolean, channel: number, distanceToTower: number) { + const player = this.serverModule.getPlayer(src) + if (!player || !player.radioSettings.activated) { + return + } + + const radioFrequency = player.radioSettings.frequencies[channel] + if (!radioFrequency || radioFrequency === '0') { + return + } + + const getPlayers = this.radioFrequencyMap.get(radioFrequency) + if (!getPlayers) { + return + } + + let targets: number[] = [] + const targetsToSender: number[] = [] + const radioInfos: Record = {} + + for (const [key, values] of getPlayers) { + if (values.muted) { + if (key === src) { + targets = [] + break + } + continue + } + + if (key === src) { + continue + } + + const target = this.serverModule.getPlayer(key) + if (!target || !target.radioSettings.activated) { + continue + } + + const shortRange = !player.radioSettings.hasLong && !target.radioSettings.hasLong + if ((player.radioSettings.hasLong && target.radioSettings.hasLong) || shortRange) { + targets.push(key) + + radioInfos[key] = { + shortRange, + } + + targetsToSender.push(key) + } + } + + triggerClientEvent( + 'client:yaca:radioTalking', + targets, + src, + radioFrequency, + state, + radioInfos, + distanceToTower, + GetEntityCoords(GetPlayerPed(src.toString())), + ) + + if (this.serverConfig.useWhisper) { + emitNet('client:yaca:radioTalkingWhisper', src, targetsToSender, radioFrequency, state, GetEntityCoords(GetPlayerPed(src.toString()))) + } + } +} diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/tsconfig.json b/resources/[voice]/yaca-voice/apps/yaca-server/tsconfig.json new file mode 100644 index 000000000..f620556a3 --- /dev/null +++ b/resources/[voice]/yaca-voice/apps/yaca-server/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@yaca-voice/typescript-config/fivem.json", + "compilerOptions": { + "baseUrl": "./", + "rootDir": "./src", + "types": ["@types/node", "@citizenfx/server"] + }, + "include": ["./"] +} diff --git a/resources/[voice]/yaca-voice/assets/yaca-voice/config/server.json5 b/resources/[voice]/yaca-voice/assets/yaca-voice/config/server.json5 new file mode 100644 index 000000000..c849cc5f2 --- /dev/null +++ b/resources/[voice]/yaca-voice/assets/yaca-voice/config/server.json5 @@ -0,0 +1,17 @@ +{ + "uniqueServerId": "", // The unique Server Identifier of the Teamspeak-Server + "ingameChannelId": 3, // The ID of the Ingame Channel + "ingameChannelPassword": "", // The Password used to join the Ingame Channel + "defaultChannelId": 1, // The ID of the Channel where a players should be moved to when leaving Ingame + "useWhisper": false, // If you want to use the Whisper functions of TeamSpeak, if set to false it mutes and unmutes the players + "excludeChannels": [], // The channels that should be able to join while being Ingame without instantly being moved back into the Ingame channel + + /* The pattern that is used to generate the username. + * + * Following placeholders will be replaced: + * - {serverid} with the Ingame-ID of the player + * - {playername} with the steam/fivem name of the player + * - {guid} with a string containing random letters and digits. + */ + "userNamePattern": "[{serverid}] {guid}" +} diff --git a/resources/[voice]/yaca-voice/assets/yaca-voice/config/shared.json5 b/resources/[voice]/yaca-voice/assets/yaca-voice/config/shared.json5 new file mode 100644 index 000000000..53351558e --- /dev/null +++ b/resources/[voice]/yaca-voice/assets/yaca-voice/config/shared.json5 @@ -0,0 +1,174 @@ +{ + // Enable the version check to get notified about new versions. + "versionCheck": true, + // Enable manual or automatic connecting to the voice channel when joining the server. + "autoConnectOnJoin": true, + // The build type of the plugin. 0 = release, 1 = develop (develop allows using all yaca plugin version) + "buildType": 0, + // The locale that should be used. + "locale": "en", + // The time before the teamspeak client is being unmuted after joining the ingame channel. + "unmuteDelay": 400, + // The range in which you can hear the phone speaker when active. + "maxPhoneSpeakerRange": 5, + /* Choose if players near other players that are in a call, should be heard by the players on the opposite side of the call. + * + * Following options are available: + * - false: Disable the feature completely + * - true: Enable the feature always + * - "PHONE_SPEAKER": Enable the feature only when the player on the phone has the phone speaker enabled. + */ + "phoneHearPlayersNearby": false, + // Choose which notifications should be enabled. + "notifications": { + // Enable notifications from ox_lib + "oxLib": false, + // Enable notifications from okokNotify + "okoknotify": false, + // Enable notifications from GTA (FiveM only) + "gta": true, + // Enable notifications from Rdr 2 (RedM only) + "redm": false, + // Enable the option to implement a custom notification + "own": false + }, + // Set the default key binds for the plugin, which can be then changed by the player. + "keyBinds": { + // The key to increase the voice range + "increaseVoiceRange": "ADD", + // The key to decrease the voice range + "decreaseVoiceRange": "SUBTRACT", + // The key to transmit on the primary radio + "primaryRadioTransmit": "N", + // The key to transmit on the secondary radio + "secondaryRadioTransmit": "CAPITAL", + // The key to use the megaphone + "megaphone": "B", + // The key to hold additional to change voice range via mousewheel by 1 meter, false to disable that feature + "voiceRangeWithMouseWheel": "LCONTROL" + }, + "radioSettings": { + // Customize radio animation + "animation": { + "dictionary": "random@arrests", + "name": "generic_radio_chatter", + "flag": 49 + }, + "propWhileTalking": { + // The prop that should be shown while talking on the radio. + "prop": false, + // The bone that the prop should be attached to. + "boneId": 28422, + // The position of the prop. + "position": [ + 0.0, + 0.0, + 0.0 + ], + // The rotation of the prop. + "rotation": [ + 0.0, + 0.0, + 0.0 + ] + }, + // The maximum amount of radio channels that can be used. + "channelCount": 9, + /* Choose the mode of the radio system. + * + * Following options are available: + * - "Tower": The radio system is based on towers, which means that the range is limited by the distance to the next tower. + * - "Direct": The radio system is based on the distance between the players. + * - "None": The radio always works no matter the distance. + */ + "mode": "Tower", + // The maximum distance between two players or to the tower to be able to hear each other. + "maxDistance": 5000 + }, + "voiceRange": { + // The default index which should be used for the voice range when a player joins the server. + "defaultIndex": 1, + // The ranges that should be available for the players. + "ranges": [ + 1, + 3, + 8, + 15, + 20, + 25, + 30, + 40 + ], + // Choose if a notification should be sent when the voice range is changed. + "sendNotification": false, + "markerColor": { + // Choose if the marker should be enabled. + "enabled": true, + // The color of the marker, r = red, g = green, b = blue, a = alpha + "r": 0, + "g": 255, + "b": 0, + "a": 50, + // The duration the marker should be shown. + "duration": 1000, + "type": 1, + "rotate": true + } + }, + "megaphone": { + // The range of the megaphone. + "range": 30, + // Choose if the plugin should automatically detect if the player should be able to use the megaphone in the vehicle. (FiveM only) + "automaticVehicleDetection": true, + // The allowed vehicle classes for the megaphone. (FiveM only) + "allowedVehicleClasses": [ + 18, + 19 + ], + // The allowed vehicle models for the megaphone. (FiveM only) + "allowedVehicleModels": [ + "polmav" + ] + }, + // Choose if the saltyChatBridge should be enabled. + "saltyChatBridge": false, + "mufflingSettings": { + // If set to -1, the player voice range is used, all values >= 0 sets the muffling range before it gets completely cut off + "mufflingRange": -1, + "vehicleMuffling": { + // If set to true, the vehicle muffling feature is enabled. (FiveM only) + "enabled": true, + // Whitelist of vehicles that should be not be affected by the vehicle muffling. (FiveM only) + "vehicleWhitelist": [ + "gauntlet6", + "draugur", + "bodhi2", + "vagrant", + "outlaw", + "trophytruck", + "ratel", + "drifttampa", + "sm722", + "tornado4", + "swinger", + "locust", + "hotring" + ], + }, + // The intensities of the muffling. (0 = no muffling, 10 = full muffling) + "intensities": { + // The intensity when the players are in different rooms. + "differentRoom": 10, + // The intensity when both cars are closed. (FiveM only) + "bothCarsClosed": 10, + // The intensity when one car is closed. (FiveM only) + "oneCarClosed": 6, + // The intensity of muffling of the megaphone of a player in a different car. (FiveM only) + "megaPhoneInCar": 6 + } + }, + // Cooldown in milliseconds which the player has to wait to use the radio again, defaults to false which disables the feature. + "radioAntiSpamCooldown": false, + // When set to true the plugin syncs the talk state via the plugin, instead of the default way via statebags. This imitates the way how saltychat syncs the talk state, but has some drawbacks. + "useLocalLipSync": false +} diff --git a/resources/[voice]/yaca-voice/assets/yaca-voice/config/towers.json5 b/resources/[voice]/yaca-voice/assets/yaca-voice/config/towers.json5 new file mode 100644 index 000000000..70831c2d3 --- /dev/null +++ b/resources/[voice]/yaca-voice/assets/yaca-voice/config/towers.json5 @@ -0,0 +1,519 @@ +{ + "towerPositions": [ + [ + 2572, + 5397, + 56 + ], + [ + 2663, + 4972, + 56 + ], + [ + 2892, + 3911, + 56 + ], + [ + 2720, + 3304, + 64 + ], + [ + 2388, + 2949, + 64 + ], + [ + 1830, + 2368, + 64 + ], + [ + 1650, + 1316, + 102 + ], + [ + 1363, + 680, + 102 + ], + [ + 918, + 230, + 92 + ], + [ + 567, + 303, + 58 + ], + [ + -47, + -666, + 74 + ], + [ + -585, + -902, + 53 + ], + [ + 2572, + 5397, + 56 + ], + [ + 2338, + 5940, + 77 + ], + [ + 1916, + 6244, + 65 + ], + [ + 1591, + 6371, + 42 + ], + [ + 953, + 6504, + 42 + ], + [ + 76, + 6606, + 42 + ], + [ + 408, + 6587, + 42 + ], + [ + -338, + -579, + 48 + ], + [ + -293, + -632, + 47 + ], + [ + -269, + -962, + 143 + ], + [ + 98, + -870, + 136 + ], + [ + -214, + -744, + 219 + ], + [ + -166, + -590, + 199 + ], + [ + 124, + -654, + 261 + ], + [ + 149, + -769, + 261 + ], + [ + 580, + 89, + 117 + ], + [ + 423, + 15, + 151 + ], + [ + 424, + 18, + 151 + ], + [ + 551, + -28, + 93 + ], + [ + 305, + -284, + 68 + ], + [ + 299, + -313, + 68 + ], + [ + 1240, + -1090, + 44 + ], + [ + -418, + -2804, + 14 + ], + [ + 802, + -2996, + 27 + ], + [ + 253, + -3145, + 39 + ], + [ + 207, + -3145, + 39 + ], + [ + 207, + -3307, + 39 + ], + [ + 247, + -3307, + 39 + ], + [ + 484, + -2178, + 40 + ], + [ + 548, + -2219, + 67 + ], + [ + -701, + 58, + 68 + ], + [ + -696, + 208, + 139 + ], + [ + -769, + 255, + 134 + ], + [ + -150, + -150, + 96 + ], + [ + -202, + -327, + 65 + ], + [ + -1913, + -3031, + 22 + ], + [ + -1918, + -3028, + 22 + ], + [ + -1039, + -2385, + 27 + ], + [ + -1042, + -2390, + 27 + ], + [ + -1583, + -3216, + 28 + ], + [ + -1590, + -3212, + 28 + ], + [ + -1308, + -2626, + 36 + ], + [ + -1311, + -2624, + 36 + ], + [ + -984, + -2778, + 48 + ], + [ + -991, + -2774, + 48 + ], + [ + -556, + -119, + 50 + ], + [ + -619, + -106, + 51 + ], + [ + -1167, + -575, + 40 + ], + [ + -1152, + -443, + 42 + ], + [ + -1156, + -498, + 49 + ], + [ + -1290, + -445, + 106 + ], + [ + -928, + -383, + 135 + ], + [ + -902, + -443, + 170 + ], + [ + -770, + -786, + 83 + ], + [ + -824, + -719, + 120 + ], + [ + -598, + -917, + 35 + ], + [ + -678, + -717, + 54 + ], + [ + -669, + -804, + 31 + ], + [ + -1463, + -526, + 83 + ], + [ + -1525, + -596, + 66 + ], + [ + -1375, + -465, + 83 + ], + [ + -1711, + 478, + 127 + ], + [ + -2311, + 335, + 187 + ], + [ + -2214, + 342, + 198 + ], + [ + -2234, + 187, + 193 + ], + [ + 202, + 1204, + 230 + ], + [ + 217, + 1140, + 230 + ], + [ + 668, + 590, + 136 + ], + [ + 722, + 562, + 134 + ], + [ + 838, + 510, + 138 + ], + [ + 773, + 575, + 138 + ], + [ + 735, + 231, + 145 + ], + [ + 450, + 5566, + 795 + ], + [ + -449, + 6019, + 35 + ], + [ + -142, + 6286, + 39 + ], + [ + -368, + 6105, + 38 + ], + [ + 2792, + 5996, + 355 + ], + [ + 2796, + 5992, + 354 + ], + [ + 3460, + 3653, + 51 + ], + [ + 3459, + 3659, + 51 + ], + [ + 3615, + 3642, + 51 + ], + [ + 3614, + 3636, + 51 + ], + [ + -2180, + 3252, + 54 + ], + [ + -2124, + 3219, + 54 + ], + [ + -2050, + 3178, + 54 + ], + [ + 1858, + 3694, + 37 + ], + [ + 1695, + 3614, + 37 + ], + [ + 1692, + 2532, + 60 + ], + [ + 1692, + 2647, + 60 + ], + [ + 1824, + 2574, + 60 + ], + [ + 1407, + 2117, + 104 + ] + ] +} \ No newline at end of file diff --git a/resources/[voice]/yaca-voice/assets/yaca-voice/locales/de.json b/resources/[voice]/yaca-voice/assets/yaca-voice/locales/de.json new file mode 100644 index 000000000..8bac3117b --- /dev/null +++ b/resources/[voice]/yaca-voice/assets/yaca-voice/locales/de.json @@ -0,0 +1,33 @@ +{ + "connect_error": "Fehler beim Verbinden zum Teamspeak-Server, bitte verbinde dich erneut!", + "plugin_not_initialized": "Das Voice-Plugin wurde noch nicht initialisiert!", + + "outdated_plugin": "Dein Voice-Plugin ist veraltet! Bitte aktualisiere auf die Version %s!", + "wrong_ts_server": "Du bist auf dem falschen Teamspeak-Server!", + "move_error": "Fehler beim Verschieben auf den Teamspeak-Server!", + "max_players_reached": "Ihre Lizenz hat die maximale Spieleranzahl erreicht. Bitte erweitern Sie Ihre Lizenz.", + "license_server_timed_out": "Die Verbindung zum Lizenzserver wurde beim Überprüfen der Lizenz unterbrochen. Bitte warte einen Moment.", + "unknown_error": "Unbekannter Fehlercode: %s", + + "changed_stereo_mode": "Kanal %s ist jetzt auf %s zu hören.", + "left_ear": "dem linken Ohr", + "right_ear": "dem rechten Ohr", + "both_ears": "beiden Ohren", + "secondary_radio_channel_disabled": "Der sekundäre Funkkanal wurde deaktiviert.", + "secondary_radio_channel_enabled": "Kanal %s ist nun der Sekundäre Funkkanal.", + + "use_megaphone": "Megaphon benutzen", + "use_radio": "Im primären Funkkanal senden", + "use_secondary_radio": "Im sekundären Funkkanal senden", + + "use_salty_primary_radio": "Primäres Funkgerät benutzen", + "use_salty_secondary_radio": "Sekundäres Funkgerät benutzen", + + "change_voice_range_increase": "Sprachreichweite erhöhen", + "change_voice_range_decrease": "Sprachreichweite verringern", + "voice_range_changed": "Sprachreichweite auf %s Meter geändert.", + "change_voice_range_via_mousewheel": "Sprachreichweite je 1 Meter ändern", + + "radio_not_activated": "Das Funkgerät ist nicht aktiviert!", + "radio_channel_invalid": "Ungültiger Funkkanal!" +} diff --git a/resources/[voice]/yaca-voice/assets/yaca-voice/locales/en.json b/resources/[voice]/yaca-voice/assets/yaca-voice/locales/en.json new file mode 100644 index 000000000..c0318f949 --- /dev/null +++ b/resources/[voice]/yaca-voice/assets/yaca-voice/locales/en.json @@ -0,0 +1,33 @@ +{ + "connect_error": "Error while connecting to voice server, please reconnect!", + "plugin_not_initialized": "Plugin not initialized!", + + "outdated_plugin": "Your plugin is outdated, please update to version %s!", + "wrong_ts_server": "You are connected to the wrong teamspeak server!", + "move_error": "Error while moving to the channel!", + "max_players_reached": "Your license reached the maximum player count. Please upgrade your license", + "license_server_timed_out": "The connection to the license server timed out, while verifying the license. Please wait a moment.", + "unknown_error": "Unknown error code: %s", + + "changed_stereo_mode": "Channel %s is now only heard in %s.", + "left_ear": "the left ear", + "right_ear": "the right ear", + "both_ears": "both ears", + "secondary_radio_channel_disabled": "The secondary radio channel has been disabled.", + "secondary_radio_channel_enabled": "Channel %s is now the secondary radio channel.", + + "use_megaphone": "Use megaphone", + "use_radio": "Use radio", + "use_secondary_radio": "Use secondary radio", + + "use_salty_primary_radio": "Use primary radio", + "use_salty_secondary_radio": "Use secondary radio", + + "change_voice_range_increase": "Increase voice range", + "change_voice_range_decrease": "Decrease voice range", + "voice_range_changed": "Voice range changed to %s meters.", + "change_voice_range_via_mousewheel": "Change voice range by 1 meter", + + "radio_not_activated": "Your radio is not activated!", + "radio_channel_invalid": "The radio channel is invalid!" +} diff --git a/resources/[voice]/yaca-voice/assets/yaca-voice/web/index.html b/resources/[voice]/yaca-voice/assets/yaca-voice/web/index.html new file mode 100644 index 000000000..a8d251332 --- /dev/null +++ b/resources/[voice]/yaca-voice/assets/yaca-voice/web/index.html @@ -0,0 +1,13 @@ + + + + + YACA WebSocket + + + + + + + + \ No newline at end of file diff --git a/resources/[voice]/yaca-voice/assets/yaca-voice/web/script.js b/resources/[voice]/yaca-voice/assets/yaca-voice/web/script.js new file mode 100644 index 000000000..3ca63e39c --- /dev/null +++ b/resources/[voice]/yaca-voice/assets/yaca-voice/web/script.js @@ -0,0 +1,85 @@ +let webSocket = null + +/** + * Connect to the YaCA voice plugin + */ +function connect() { + console.log('[YaCA-Websocket] Trying to Connect to YaCA WebSocket...') + + try { + webSocket = new window.WebSocket('ws://127.0.0.1:30125/') + } catch { + connect() + } + + webSocket.onmessage = (event) => { + if (!event) return + sendNuiData('YACA_OnMessage', event.data) + } + + webSocket.onopen = (event) => { + if (!event) return + sendNuiData('YACA_OnConnected') + } + + webSocket.onclose = (event) => { + if (!event) return + + sendNuiData('YACA_OnDisconnected', { + code: event.code, + reason: event.reason, + }) + + setTimeout(() => { + connect() + }, 1000) + } +} + +/** + * Send a command to the YaCA voice plugin + * + * @param command - The command to send as a object + */ +function runCommand(command) { + if (!webSocket || webSocket.readyState !== WebSocket.OPEN) { + return + } + + webSocket.send(JSON.stringify(command)) +} + +/** + * Send a NUI message to the client + * + * @param event - The name of the callback + * @param data - The data to send + */ +function sendNuiData(event, data = {}) { + // skipcq: JS-0125 + fetch(`https://${GetParentResourceName()}/${event}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: JSON.stringify(data), + }).catch((error) => console.error('[YaCA-Websocket] Error sending NUI Message:', error)) +} + +$(() => { + window.addEventListener('DOMContentLoaded', () => { + sendNuiData('YACA_OnNuiReady') + }) + + window.addEventListener('message', (event) => { + if (event.data.action === 'connect') { + connect() + } else if (event.data.action === 'command') { + runCommand(event.data.data) + } else if (event.data.action === 'close') { + if (webSocket) webSocket.close() + } else { + console.error('[YaCA-Websocket] Unknown message:', event.data) + } + }) +}) diff --git a/resources/[voice]/yaca-voice/biome.json b/resources/[voice]/yaca-voice/biome.json new file mode 100644 index 000000000..e7f1273fc --- /dev/null +++ b/resources/[voice]/yaca-voice/biome.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.0.6/schema.json", + "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true, "defaultBranch": "main" }, + "files": { "ignoreUnknown": false, "includes": ["**", "!**/resource", "!**/dist"] }, + "formatter": { + "enabled": true, + "useEditorconfig": true, + "formatWithErrors": false, + "indentStyle": "space", + "indentWidth": 4, + "lineWidth": 160, + "attributePosition": "auto", + "bracketSpacing": true, + "includes": ["**", "!**/dist", "!**/pnpm-lock.yaml"] + }, + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "noParameterAssign": "off" + } + }, + "includes": ["**", "!**/dist"] + }, + "javascript": { + "formatter": { + "jsxQuoteStyle": "double", + "quoteProperties": "asNeeded", + "trailingCommas": "all", + "semicolons": "asNeeded", + "arrowParentheses": "always", + "bracketSameLine": false, + "quoteStyle": "single", + "attributePosition": "auto", + "bracketSpacing": true + }, + "globals": [] + } +} diff --git a/resources/[voice]/yaca-voice/package.json b/resources/[voice]/yaca-voice/package.json new file mode 100644 index 000000000..ba4080e3c --- /dev/null +++ b/resources/[voice]/yaca-voice/package.json @@ -0,0 +1,27 @@ +{ + "name": "yaca-voice", + "type": "module", + "description": "YACA Voice Integration for FiveM & RedM", + "author": "MineMalox & LuftigerLuca", + "repository": { + "type": "git", + "url": "https://github.com/yaca-systems/fivem-yaca-typescript" + }, + "scripts": { + "preinstall": "npx only-allow pnpm", + "typecheck": "turbo typecheck --env-mode=loose", + "format": "pnpm biome format --write", + "lint": "pnpm biome lint --write", + "check": "pnpm biome check --formatter-enabled=true --linter-enabled=true --write", + "build": "turbo build --env-mode=loose", + "dev": "turbo dev --env-mode=loose", + "create-resource": "turbo create-resource --env-mode=loose && node scripts/create-resource.js" + }, + "devDependencies": { + "@biomejs/biome": "2.0.6", + "esbuild": "^0.24.2", + "turbo": "^2.3.4", + "typescript": "^5.7.3" + }, + "packageManager": "pnpm@9.15.9+sha512.68046141893c66fad01c079231128e9afb89ef87e2691d69e4d40eee228988295fd4682181bae55b58418c3a253bde65a505ec7c5f9403ece5cc3cd37dcf2531" +} diff --git a/resources/[voice]/yaca-voice/packages/common/package.json b/resources/[voice]/yaca-voice/packages/common/package.json new file mode 100644 index 000000000..731c48bd7 --- /dev/null +++ b/resources/[voice]/yaca-voice/packages/common/package.json @@ -0,0 +1,23 @@ +{ + "name": "@yaca-voice/common", + "private": true, + "version": "0.0.0", + "main": "./src/index.ts", + "types": "/src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "typecheck": "tsc --project tsconfig.json" + }, + "dependencies": { + "fast-printf": "^1.6.9", + "json5": "^2.2.3" + }, + "devDependencies": { + "@citizenfx/client": "latest", + "@citizenfx/server": "latest", + "@types/node": "^22.7.4", + "@yaca-voice/typescript-config": "workspace:*" + } +} diff --git a/resources/[voice]/yaca-voice/packages/common/pnpm-lock.yaml b/resources/[voice]/yaca-voice/packages/common/pnpm-lock.yaml new file mode 100644 index 000000000..e2fad36a3 --- /dev/null +++ b/resources/[voice]/yaca-voice/packages/common/pnpm-lock.yaml @@ -0,0 +1,30 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + fast-printf: + specifier: ^1.6.9 + version: 1.6.9 + +packages: + + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + + fast-printf@1.6.9: + resolution: {integrity: sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg==} + engines: {node: '>=10.0'} + +snapshots: + + boolean@3.2.0: {} + + fast-printf@1.6.9: + dependencies: + boolean: 3.2.0 diff --git a/resources/[voice]/yaca-voice/packages/common/src/bridge.ts b/resources/[voice]/yaca-voice/packages/common/src/bridge.ts new file mode 100644 index 000000000..568785226 --- /dev/null +++ b/resources/[voice]/yaca-voice/packages/common/src/bridge.ts @@ -0,0 +1,11 @@ +/** + * Add an export in the saltychat namespace. + * + * @param method - the export method name + * @param cb - the callback to execute + */ +export function saltyChatExport(method: string, cb: (...args: never[]) => void) { + on(`__cfx_export_saltychat_${method}`, (setCb: (...args: unknown[]) => void) => { + setCb(cb) + }) +} diff --git a/resources/[voice]/yaca-voice/packages/common/src/config.ts b/resources/[voice]/yaca-voice/packages/common/src/config.ts new file mode 100644 index 000000000..e92a609d8 --- /dev/null +++ b/resources/[voice]/yaca-voice/packages/common/src/config.ts @@ -0,0 +1,71 @@ +import JSON5 from 'json5' + +/** + * Merge the default object with the parsed object and validate the parsed object. + * This function will log warnings for missing and unknown keys in the parsed object. + * If a key is missing in the parsed object, the default value will be used. + * If a key is unknown in the parsed object, it will be ignored. + * + * @param defaultObj - The default object. + * @param parsedObj - The parsed object. + * @param path - The path to the current object. + */ +function mergeAndValidate(defaultObj: T, parsedObj: T, path: string[] = []): T { + const result: T = { ...defaultObj } + + for (const key in defaultObj) { + if (Object.prototype.hasOwnProperty.call(defaultObj, key) === false) { + continue + } + + const currentPath = [...path, key].join('.') + + if (!(key in parsedObj)) { + console.warn( + `[YaCA] Missing config value for key '${currentPath}' setting to default value: ${defaultObj[key]}\nMissing config values can cause unexpected behavior of the script.`, + ) + } else if ( + typeof defaultObj[key] === 'object' && + defaultObj[key] !== null && + !Array.isArray(defaultObj[key]) && + typeof parsedObj[key] === 'object' && + parsedObj[key] !== null && + !Array.isArray(parsedObj[key]) + ) { + // Recursive merge for nested objects + result[key] = mergeAndValidate(defaultObj[key], parsedObj[key], [...path, key]) + } else { + result[key] = parsedObj[key] + } + } + + for (const key of Object.keys(parsedObj)) { + const currentPath = [...path, key].join('.') + + if (!(key in defaultObj)) { + console.warn(`[YaCA] Unknown config key '${currentPath}' found in config file. This key will be ignored and can be removed.`) + } + } + + return result +} + +/** + * Load a config file from the resource and merge it with the default values. + * + * @param filePath - The path to the config file. + * @param defaultValues - The default values to set when the config file is missing values. + * + * @returns The loaded config. + */ +export function loadConfig(filePath: string, defaultValues: T): T { + const fileData = LoadResourceFile(GetCurrentResourceName(), filePath) + + if (!fileData) { + return defaultValues + } + + const parsedData = JSON5.parse(fileData) as T + + return mergeAndValidate(defaultValues, parsedData) +} diff --git a/resources/[voice]/yaca-voice/packages/common/src/constants.ts b/resources/[voice]/yaca-voice/packages/common/src/constants.ts new file mode 100644 index 000000000..6640a676e --- /dev/null +++ b/resources/[voice]/yaca-voice/packages/common/src/constants.ts @@ -0,0 +1,5 @@ +export const MEGAPHONE_STATE_NAME = 'yacaMegaphone' +export const PHONE_SPEAKER_STATE_NAME = 'yacaPhoneSpeaker' +export const LIP_SYNC_STATE_NAME = 'yacaLipSync' +export const VOICE_RANGE_STATE_NAME = 'yacaVoiceRange' +export const GLOBAL_ERROR_LEVEL_STATE_NAME = 'yacaGlobalErrorLevel' diff --git a/resources/[voice]/yaca-voice/packages/common/src/errorlevel.ts b/resources/[voice]/yaca-voice/packages/common/src/errorlevel.ts new file mode 100644 index 000000000..e0be45643 --- /dev/null +++ b/resources/[voice]/yaca-voice/packages/common/src/errorlevel.ts @@ -0,0 +1,20 @@ +import { GLOBAL_ERROR_LEVEL_STATE_NAME } from './constants' +import { clamp } from './index' + +/** + * Set the global error level. + * + * @param errorLevel The new error level. Between 0 and 1. + */ +export const setGlobalErrorLevel = (errorLevel: number) => { + GlobalState.set(GLOBAL_ERROR_LEVEL_STATE_NAME, clamp(errorLevel, 0, 1), true) +} + +/** + * Get the global error level. + * + * @returns The global error level. + */ +export const getGlobalErrorLevel = () => { + return GlobalState[GLOBAL_ERROR_LEVEL_STATE_NAME] ?? 0 +} diff --git a/resources/[voice]/yaca-voice/packages/common/src/index.ts b/resources/[voice]/yaca-voice/packages/common/src/index.ts new file mode 100644 index 000000000..55489d198 --- /dev/null +++ b/resources/[voice]/yaca-voice/packages/common/src/index.ts @@ -0,0 +1,58 @@ +export * from './bridge' +export * from './config' +export * from './constants' +export * from './errorlevel' +export * from './locale' + +/** + * Sleeps for a given amount of time. + * + * @param ms - The amount of time to sleep in milliseconds. + */ +export function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms, null)) +} + +/** + * Clamps a value between a minimum and maximum value. + * + * @param {number} value - The value to be clamped. + * @param {number} [min=0] - The minimum value. Defaults to 0 if not provided. + * @param {number} [max=1] - The maximum value. Defaults to 1 if not provided. + */ +export function clamp(value: number, min = 0, max = 1) { + return Math.max(min, Math.min(max, value)) +} + +/** + * Creates a promise that will be resolved once any value is returned by the function (including null). + * @param cb Function to call. + * @param errMessage Error message to throw if the function never resolves. + * @param {number?} timeout Error out after `~x` ms. Defaults to 1000, unless set to `false`. + */ +export async function waitFor(cb: () => T, errMessage?: string, timeout?: number | false): Promise { + let value = await cb() + + if (value !== undefined) return value + + if (timeout || timeout == null) { + if (typeof timeout !== 'number') timeout = 1000 + } + + const start = GetGameTimer() + let id: number + + return new Promise((resolve, reject) => { + id = setTick(async () => { + const elapsed = timeout && GetGameTimer() - start + + if (elapsed && elapsed > (timeout as number)) { + return reject(`${errMessage || 'failed to resolve callback'} (waited ${elapsed}ms)`) + } + + value = await cb() + + if (value !== undefined) resolve(value) + }) + }).finally(() => clearTick(id)) +} diff --git a/resources/[voice]/yaca-voice/packages/common/src/locale.ts b/resources/[voice]/yaca-voice/packages/common/src/locale.ts new file mode 100644 index 000000000..bf94a37aa --- /dev/null +++ b/resources/[voice]/yaca-voice/packages/common/src/locale.ts @@ -0,0 +1,91 @@ +import { printf } from 'fast-printf' + +const resourceName = GetCurrentResourceName() +const dict: Record = {} + +/** + * Flattens a dictionary. + * + * @param source - The source dictionary to flatten. + * @param target - The target dictionary to flatten to. + * @param prefix - The prefix to use. + */ +function flattenDict(source: Record, target: Record, prefix?: string) { + for (const [key, value] of Object.entries(source)) { + const fullKey = prefix ? `${prefix}.${key}` : key + + if (typeof value === 'object') flattenDict(value, target, fullKey) + else target[fullKey] = String(value) + } + + return target +} + +/** + * Get the localized string for a key. + * + * @param str - The key to get the localized string for. + * @param args - The arguments to use for string interpolation. + */ +export const locale = (str: string, ...args: (string | number | boolean)[]): string => { + const localeStr = dict[str] + + if (localeStr) { + if (args.length > 0) { + return printf(localeStr, ...args) + } + + return localeStr + } + + return str +} + +/** + * Get all the locales. + */ +export const getLocales = () => dict + +/** + * Initialize the locale. + * + * @param configLocale - The locale to use. Defaults to 'en'. If not found, falls back to 'en'. + */ +export const initLocale = (configLocale: string) => { + const lang = configLocale || 'en' + let locales: typeof dict = JSON.parse(LoadResourceFile(resourceName, `locales/${lang}.json`)) + + if (!locales) { + console.warn(`could not load 'locales/${lang}.json'`) + + if (lang !== 'en') { + locales = JSON.parse(LoadResourceFile(resourceName, 'locales/en.json')) + + if (!locales) { + console.warn("could not load 'locales/en.json'") + } + } + + if (!locales) return + } + + const flattened = flattenDict(locales, {}) + + for (const [k, v] of Object.entries(flattened)) { + const regExp = new RegExp(/\$\{([^}]+)}/g) + const matches = v.match(regExp) + if (matches) { + for (const match of matches) { + if (!match) break + const variable = match.substring(2, match.length - 1) as keyof typeof locales + const locale: string = flattened[variable] + + if (locale) { + flattened[k] = v.replace(match, locale) + } + } + } + + dict[k] = v + } +} diff --git a/resources/[voice]/yaca-voice/packages/common/tsconfig.json b/resources/[voice]/yaca-voice/packages/common/tsconfig.json new file mode 100644 index 000000000..6ae6bb6e7 --- /dev/null +++ b/resources/[voice]/yaca-voice/packages/common/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@yaca-voice/typescript-config/fivem.json", + "compilerOptions": { + "baseUrl": "./", + "rootDir": "./src" + }, + "include": ["./"] +} diff --git a/resources/[voice]/yaca-voice/packages/tsconfig/fivem.json b/resources/[voice]/yaca-voice/packages/tsconfig/fivem.json new file mode 100644 index 000000000..e55ced65d --- /dev/null +++ b/resources/[voice]/yaca-voice/packages/tsconfig/fivem.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "noImplicitAny": true, + "strictNullChecks": true, + "module": "es2020", + "target": "es2021", + "lib": ["es2021", "esnext.disposable"], + "types": ["@types/node", "@citizenfx/client", "@citizenfx/server"], + "resolveJsonModule": true, + "esModuleInterop": true, + "allowUnreachableCode": false, + "strictFunctionTypes": true, + "moduleResolution": "bundler", + "noImplicitThis": true, + "noUnusedLocals": true, + "skipLibCheck": true, + "declaration": true, + "noEmit": true, + "removeComments": true + } +} diff --git a/resources/[voice]/yaca-voice/packages/tsconfig/package.json b/resources/[voice]/yaca-voice/packages/tsconfig/package.json new file mode 100644 index 000000000..f586b4604 --- /dev/null +++ b/resources/[voice]/yaca-voice/packages/tsconfig/package.json @@ -0,0 +1,3 @@ +{ + "name": "@yaca-voice/typescript-config" +} diff --git a/resources/[voice]/yaca-voice/packages/types/package.json b/resources/[voice]/yaca-voice/packages/types/package.json new file mode 100644 index 000000000..f17b200c8 --- /dev/null +++ b/resources/[voice]/yaca-voice/packages/types/package.json @@ -0,0 +1,10 @@ +{ + "name": "@yaca-voice/types", + "private": true, + "version": "0.0.0", + "main": "./src/index.ts", + "types": "/src/index.ts", + "exports": { + ".": "./src/index.ts" + } +} diff --git a/resources/[voice]/yaca-voice/packages/types/src/config.ts b/resources/[voice]/yaca-voice/packages/types/src/config.ts new file mode 100644 index 000000000..0d4b82522 --- /dev/null +++ b/resources/[voice]/yaca-voice/packages/types/src/config.ts @@ -0,0 +1,305 @@ +import { YacaBuildType } from './enums' + +export type radioMode = 'None' | 'Direct' | 'Tower' + +export interface YacaSharedConfig { + versionCheck: boolean + autoConnectOnJoin: boolean + buildType: YacaBuildType + locale: string + unmuteDelay: number + maxPhoneSpeakerRange: number + phoneHearPlayersNearby: false | 'PHONE_SPEAKER' | true + notifications: { + oxLib: boolean + okoknotify: boolean + gta: boolean + redm: boolean + own: boolean + } + keyBinds: { + increaseVoiceRange: string | false + decreaseVoiceRange: string | false + primaryRadioTransmit: string | false + secondaryRadioTransmit: string | false + megaphone: string | false + voiceRangeWithMouseWheel: string | false + } + radioSettings: { + animation: { + dictionary: string + name: string + flag: number + } + propWhileTalking: { + prop: string | false + boneId: number + position: [number, number, number] + rotation: [number, number, number] + } + channelCount: number + mode: radioMode + maxDistance: number + } + voiceRange: { + defaultIndex: number + ranges: number[] + sendNotification: boolean + markerColor: { + enabled: boolean + r: number + g: number + b: number + a: number + duration: number + type: number + rotate: boolean + } + } + megaphone: { + range: number + automaticVehicleDetection: boolean + allowedVehicleClasses: number[] + allowedVehicleModels: string[] + } + saltyChatBridge: boolean + mufflingSettings: { + mufflingRange: number + vehicleMuffling: { + enabled: boolean + vehicleWhitelist: string[] + } + intensities: { + differentRoom: number + bothCarsClosed: number + oneCarClosed: number + megaPhoneInCar: number + } + } + radioAntiSpamCooldown: number | false + useLocalLipSync: boolean +} + +export const defaultSharedConfig: YacaSharedConfig = { + versionCheck: true, + autoConnectOnJoin: true, + buildType: YacaBuildType.RELEASE, + locale: 'en', + unmuteDelay: 400, + maxPhoneSpeakerRange: 5, + phoneHearPlayersNearby: false, + notifications: { + oxLib: false, + okoknotify: false, + gta: true, + redm: false, + own: false, + }, + keyBinds: { + increaseVoiceRange: 'ADD', + decreaseVoiceRange: 'SUBTRACT', + primaryRadioTransmit: 'N', + secondaryRadioTransmit: 'CAPITAL', + megaphone: 'B', + voiceRangeWithMouseWheel: 'LCONTROL', + }, + radioSettings: { + animation: { + dictionary: 'random@arrests', + name: 'generic_radio_chatter', + flag: 49, + }, + propWhileTalking: { + prop: false, + boneId: 28422, + position: [0.0, 0.0, 0.0], + rotation: [0.0, 0.0, 0.0], + }, + channelCount: 9, + mode: 'None', + maxDistance: 1000, + }, + voiceRange: { + defaultIndex: 1, + ranges: [1, 3, 8, 15, 20, 25, 30, 40], + sendNotification: true, + markerColor: { + enabled: true, + r: 0, + g: 255, + b: 0, + a: 50, + duration: 1000, + type: 1, + rotate: true, + }, + }, + megaphone: { + range: 30, + automaticVehicleDetection: true, + allowedVehicleClasses: [18, 19], + allowedVehicleModels: ['polmav'], + }, + saltyChatBridge: false, + mufflingSettings: { + mufflingRange: -1, + vehicleMuffling: { + enabled: true, + vehicleWhitelist: [ + 'gauntlet6', + 'draugur', + 'bodhi2', + 'vagrant', + 'outlaw', + 'trophytruck', + 'ratel', + 'drifttampa', + 'sm722', + 'tornado4', + 'swinger', + 'locust', + 'hotring', + ], + }, + intensities: { + differentRoom: 10, + bothCarsClosed: 10, + oneCarClosed: 6, + megaPhoneInCar: 6, + }, + }, + radioAntiSpamCooldown: false, + useLocalLipSync: false, +} + +export interface YacaServerConfig { + uniqueServerId: string + ingameChannelId: number + ingameChannelPassword: string + defaultChannelId: number + useWhisper: boolean + excludeChannels: number[] + userNamePattern: string +} + +export const defaultServerConfig: YacaServerConfig = { + uniqueServerId: '', + ingameChannelId: 3, + ingameChannelPassword: '', + defaultChannelId: 1, + useWhisper: false, + excludeChannels: [], + userNamePattern: '[{serverid}] {guid}', +} + +export interface YacaTowerConfig { + towerPositions: [number, number, number][] +} + +export const defaultTowerConfig: YacaTowerConfig = { + towerPositions: [ + [2572, 5397, 56], + [2663, 4972, 56], + [2892, 3911, 56], + [2720, 3304, 64], + [2388, 2949, 64], + [1830, 2368, 64], + [1650, 1316, 102], + [1363, 680, 102], + [918, 230, 92], + [567, 303, 58], + [-47, -666, 74], + [-585, -902, 53], + [2572, 5397, 56], + [2338, 5940, 77], + [1916, 6244, 65], + [1591, 6371, 42], + [953, 6504, 42], + [76, 6606, 42], + [408, 6587, 42], + [-338, -579, 48], + [-293, -632, 47], + [-269, -962, 143], + [98, -870, 136], + [-214, -744, 219], + [-166, -590, 199], + [124, -654, 261], + [149, -769, 261], + [580, 89, 117], + [423, 15, 151], + [424, 18, 151], + [551, -28, 93], + [305, -284, 68], + [299, -313, 68], + [1240, -1090, 44], + [-418, -2804, 14], + [802, -2996, 27], + [253, -3145, 39], + [207, -3145, 39], + [207, -3307, 39], + [247, -3307, 39], + [484, -2178, 40], + [548, -2219, 67], + [-701, 58, 68], + [-696, 208, 139], + [-769, 255, 134], + [-150, -150, 96], + [-202, -327, 65], + [-1913, -3031, 22], + [-1918, -3028, 22], + [-1039, -2385, 27], + [-1042, -2390, 27], + [-1583, -3216, 28], + [-1590, -3212, 28], + [-1308, -2626, 36], + [-1311, -2624, 36], + [-984, -2778, 48], + [-991, -2774, 48], + [-556, -119, 50], + [-619, -106, 51], + [-1167, -575, 40], + [-1152, -443, 42], + [-1156, -498, 49], + [-1290, -445, 106], + [-928, -383, 135], + [-902, -443, 170], + [-770, -786, 83], + [-824, -719, 120], + [-598, -917, 35], + [-678, -717, 54], + [-669, -804, 31], + [-1463, -526, 83], + [-1525, -596, 66], + [-1375, -465, 83], + [-1711, 478, 127], + [-2311, 335, 187], + [-2214, 342, 198], + [-2234, 187, 193], + [202, 1204, 230], + [217, 1140, 230], + [668, 590, 136], + [722, 562, 134], + [838, 510, 138], + [773, 575, 138], + [735, 231, 145], + [450, 5566, 795], + [-449, 6019, 35], + [-142, 6286, 39], + [-368, 6105, 38], + [2792, 5996, 355], + [2796, 5992, 354], + [3460, 3653, 51], + [3459, 3659, 51], + [3615, 3642, 51], + [3614, 3636, 51], + [-2180, 3252, 54], + [-2124, 3219, 54], + [-2050, 3178, 54], + [1858, 3694, 37], + [1695, 3614, 37], + [1692, 2532, 60], + [1692, 2647, 60], + [1824, 2574, 60], + [1407, 2117, 104], + ], +} diff --git a/resources/[voice]/yaca-voice/packages/types/src/enums.ts b/resources/[voice]/yaca-voice/packages/types/src/enums.ts new file mode 100644 index 000000000..58fc84942 --- /dev/null +++ b/resources/[voice]/yaca-voice/packages/types/src/enums.ts @@ -0,0 +1,40 @@ +export enum YacaFilterEnum { + RADIO = 'RADIO', + MEGAPHONE = 'MEGAPHONE', + PHONE = 'PHONE', + PHONE_SPEAKER = 'PHONE_SPEAKER', + INTERCOM = 'INTERCOM', + PHONE_HISTORICAL = 'PHONE_HISTORICAL', +} + +export enum YacaNotificationType { + ERROR = 'error', + INFO = 'inform', + SUCCESS = 'success', +} + +export enum YacaStereoMode { + MONO_LEFT = 'MONO_LEFT', + MONO_RIGHT = 'MONO_RIGHT', + STEREO = 'STEREO', +} + +export enum YacaBuildType { + RELEASE = 0, + DEVELOP = 1, +} + +export enum CommDeviceMode { + SENDER = 0, + RECEIVER = 1, + TRANSCEIVER = 2, +} + +export enum YacaPluginStates { + NOT_CONNECTED = 'NOT_CONNECTED', + CONNECTED = 'CONNECTED', + OUTDATED_VERSION = 'OUTDATED_VERSION', + WRONG_TS_SERVER = 'WRONG_TS_SERVER', + IN_INGAME_CHANNEL = 'IN_INGAME_CHANNEL', + IN_EXCLUDED_CHANNEL = 'IN_EXCLUDED_CHANNEL', +} diff --git a/resources/[voice]/yaca-voice/packages/types/src/index.ts b/resources/[voice]/yaca-voice/packages/types/src/index.ts new file mode 100644 index 000000000..be1daf1dd --- /dev/null +++ b/resources/[voice]/yaca-voice/packages/types/src/index.ts @@ -0,0 +1,3 @@ +export * from './config' +export * from './enums' +export * from './types' diff --git a/resources/[voice]/yaca-voice/packages/types/src/types.ts b/resources/[voice]/yaca-voice/packages/types/src/types.ts new file mode 100644 index 000000000..1ec218395 --- /dev/null +++ b/resources/[voice]/yaca-voice/packages/types/src/types.ts @@ -0,0 +1,98 @@ +import type { CommDeviceMode, YacaFilterEnum, YacaStereoMode } from './enums' + +export type YacaResponseCode = + | 'SOUND_STATE' + | 'MUTE_STATE' // Deprecated in favor of SOUND_STATE + | 'TALK_STATE' + | 'OK' + | 'WRONG_TS_SERVER' + | 'MOVE_ERROR' + | 'OUTDATED_VERSION' + | 'WAIT_GAME_INIT' + | 'HEARTBEAT' + | 'MAX_PLAYER_COUNT_REACHED' + | 'LICENSE_SERVER_TIMED_OUT' + | 'MOVED_CHANNEL' + | 'OTHER_TALK_STATE' + +export interface YacaResponse { + code: YacaResponseCode + requestType: string + message: string +} + +export interface YacaSoundStateMessage { + microphoneMuted: boolean + microphoneDisabled: boolean + soundMuted: boolean + soundDisabled: boolean +} + +export interface YacaPlayerData { + remoteID: number + clientId: number + forceMuted: boolean + mutedOnPhone: boolean + phoneCallMemberIds?: number[] +} + +export interface DataObject { + clientId?: number + playerId?: number + forceMuted?: boolean + mutedOnPhone?: boolean + suid?: string + chid?: number + deChid?: number + channelPassword?: string + ingameName?: string + useWhisper?: boolean + excludeChannels?: number[] +} + +export interface YacaClient { + client_id?: number + mode?: CommDeviceMode + errorLevel?: number +} + +export interface YacaProtocol { + comm_type: YacaFilterEnum + output_mode?: YacaStereoMode + members?: YacaClient[] + on?: boolean + volume?: number + channel?: number + range?: number +} + +export interface YacaRadioSettings { + frequency: string + muted: boolean + volume: number + stereo: YacaStereoMode +} + +export type ClientCache = { + serverId: number + playerId: number + resource: string + ped: number + vehicle: number | false + seat: number | false + game: 'fivem' | 'redm' +} + +export type ServerCache = { + resource: string +} + +export type YacaPluginPlayerData = { + client_id: number + position: { x: number; y: number; z: number } + direction: { x: number; y: number; z: number } + range: number + is_underwater: boolean + muffle_intensity: number + is_muted: boolean +} diff --git a/resources/[voice]/yaca-voice/pnpm-lock.yaml b/resources/[voice]/yaca-voice/pnpm-lock.yaml new file mode 100644 index 000000000..a63f1142c --- /dev/null +++ b/resources/[voice]/yaca-voice/pnpm-lock.yaml @@ -0,0 +1,622 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@biomejs/biome': + specifier: 2.0.6 + version: 2.0.6 + esbuild: + specifier: ^0.24.2 + version: 0.24.2 + turbo: + specifier: ^2.3.4 + version: 2.5.3 + typescript: + specifier: ^5.7.3 + version: 5.8.3 + + apps/yaca-client: + dependencies: + eventemitter2: + specifier: ^6.4.9 + version: 6.4.9 + devDependencies: + '@citizenfx/client': + specifier: latest + version: 2.0.15015-1 + '@types/luxon': + specifier: ^3.4.2 + version: 3.4.2 + '@types/node': + specifier: ^20.16.10 + version: 20.16.10 + '@yaca-voice/common': + specifier: workspace:* + version: link:../../packages/common + '@yaca-voice/types': + specifier: workspace:* + version: link:../../packages/types + '@yaca-voice/typescript-config': + specifier: workspace:* + version: link:../../packages/tsconfig + + apps/yaca-server: + dependencies: + node-fetch: + specifier: ^3.3.2 + version: 3.3.2 + devDependencies: + '@citizenfx/server': + specifier: latest + version: 2.0.14862-1 + '@types/luxon': + specifier: ^3.4.2 + version: 3.4.2 + '@types/node': + specifier: ^20.16.10 + version: 20.16.10 + '@yaca-voice/common': + specifier: workspace:* + version: link:../../packages/common + '@yaca-voice/types': + specifier: workspace:* + version: link:../../packages/types + '@yaca-voice/typescript-config': + specifier: workspace:* + version: link:../../packages/tsconfig + + packages/common: + dependencies: + fast-printf: + specifier: ^1.6.9 + version: 1.6.9 + json5: + specifier: ^2.2.3 + version: 2.2.3 + devDependencies: + '@citizenfx/client': + specifier: latest + version: 2.0.15015-1 + '@citizenfx/server': + specifier: latest + version: 2.0.14862-1 + '@types/node': + specifier: ^22.7.4 + version: 22.7.4 + '@yaca-voice/typescript-config': + specifier: workspace:* + version: link:../tsconfig + + packages/tsconfig: {} + + packages/types: {} + +packages: + + '@biomejs/biome@2.0.6': + resolution: {integrity: sha512-RRP+9cdh5qwe2t0gORwXaa27oTOiQRQvrFf49x2PA1tnpsyU7FIHX4ZOFMtBC4QNtyWsN7Dqkf5EDbg4X+9iqA==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.0.6': + resolution: {integrity: sha512-AzdiNNjNzsE6LfqWyBvcL29uWoIuZUkndu+wwlXW13EKcBHbbKjNQEZIJKYDc6IL+p7bmWGx3v9ZtcRyIoIz5A==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.0.6': + resolution: {integrity: sha512-wJjjP4E7bO4WJmiQaLnsdXMa516dbtC6542qeRkyJg0MqMXP0fvs4gdsHhZ7p9XWTAmGIjZHFKXdsjBvKGIJJQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.0.6': + resolution: {integrity: sha512-CVPEMlin3bW49sBqLBg2x016Pws7eUXA27XYDFlEtponD0luYjg2zQaMJ2nOqlkKG9fqzzkamdYxHdMDc2gZFw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@2.0.6': + resolution: {integrity: sha512-ZSVf6TYo5rNMUHIW1tww+rs/krol7U5A1Is/yzWyHVZguuB0lBnIodqyFuwCNqG9aJGyk7xIMS8HG0qGUPz0SA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@2.0.6': + resolution: {integrity: sha512-mKHE/e954hR/hSnAcJSjkf4xGqZc/53Kh39HVW1EgO5iFi0JutTN07TSjEMg616julRtfSNJi0KNyxvc30Y4rQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@2.0.6': + resolution: {integrity: sha512-geM1MkHTV1Kh2Cs/Xzot9BOF3WBacihw6bkEmxkz4nSga8B9/hWy5BDiOG3gHDGIBa8WxT0nzsJs2f/hPqQIQw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@2.0.6': + resolution: {integrity: sha512-290V4oSFoKaprKE1zkYVsDfAdn0An5DowZ+GIABgjoq1ndhvNxkJcpxPsiYtT7slbVe3xmlT0ncdfOsN7KruzA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.0.6': + resolution: {integrity: sha512-bfM1Bce0d69Ao7pjTjUS+AWSZ02+5UHdiAP85Th8e9yV5xzw6JrHXbL5YWlcEKQ84FIZMdDc7ncuti1wd2sdbw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@citizenfx/client@2.0.15015-1': + resolution: {integrity: sha512-gm0/fWM0/Wn5Hny+cnSRsBc73Opz1PPKS4LKCraIE477tQ0uGNVACoeTQqxRJz54Lu09KeA4cCfocVmefVAqUA==} + + '@citizenfx/server@2.0.14862-1': + resolution: {integrity: sha512-I6XnxIGBhskPe9S+q1OQLDqs6TYw1RhX06K6jUg+n0hGITYools6VBIIhdPe2uUlHA+76qJajQlV9ONeR3mDig==} + + '@esbuild/aix-ppc64@0.24.2': + resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.24.2': + resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.24.2': + resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.24.2': + resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.24.2': + resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.24.2': + resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.24.2': + resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.24.2': + resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.24.2': + resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.24.2': + resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.24.2': + resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.24.2': + resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.24.2': + resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.24.2': + resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.24.2': + resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.24.2': + resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.24.2': + resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.24.2': + resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.24.2': + resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.24.2': + resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.24.2': + resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.24.2': + resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.24.2': + resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.24.2': + resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.24.2': + resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@types/luxon@3.4.2': + resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} + + '@types/node@20.16.10': + resolution: {integrity: sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==} + + '@types/node@22.7.4': + resolution: {integrity: sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==} + + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + esbuild@0.24.2: + resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} + engines: {node: '>=18'} + hasBin: true + + eventemitter2@6.4.9: + resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==} + + fast-printf@1.6.9: + resolution: {integrity: sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg==} + engines: {node: '>=10.0'} + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + turbo-darwin-64@2.5.3: + resolution: {integrity: sha512-YSItEVBUIvAGPUDpAB9etEmSqZI3T6BHrkBkeSErvICXn3dfqXUfeLx35LfptLDEbrzFUdwYFNmt8QXOwe9yaw==} + cpu: [x64] + os: [darwin] + + turbo-darwin-arm64@2.5.3: + resolution: {integrity: sha512-5PefrwHd42UiZX7YA9m1LPW6x9YJBDErXmsegCkVp+GjmWrADfEOxpFrGQNonH3ZMj77WZB2PVE5Aw3gA+IOhg==} + cpu: [arm64] + os: [darwin] + + turbo-linux-64@2.5.3: + resolution: {integrity: sha512-M9xigFgawn5ofTmRzvjjLj3Lqc05O8VHKuOlWNUlnHPUltFquyEeSkpQNkE/vpPdOR14AzxqHbhhxtfS4qvb1w==} + cpu: [x64] + os: [linux] + + turbo-linux-arm64@2.5.3: + resolution: {integrity: sha512-auJRbYZ8SGJVqvzTikpg1bsRAsiI9Tk0/SDkA5Xgg0GdiHDH/BOzv1ZjDE2mjmlrO/obr19Dw+39OlMhwLffrw==} + cpu: [arm64] + os: [linux] + + turbo-windows-64@2.5.3: + resolution: {integrity: sha512-arLQYohuHtIEKkmQSCU9vtrKUg+/1TTstWB9VYRSsz+khvg81eX6LYHtXJfH/dK7Ho6ck+JaEh5G+QrE1jEmCQ==} + cpu: [x64] + os: [win32] + + turbo-windows-arm64@2.5.3: + resolution: {integrity: sha512-3JPn66HAynJ0gtr6H+hjY4VHpu1RPKcEwGATvGUTmLmYSYBQieVlnGDRMMoYN066YfyPqnNGCfhYbXfH92Cm0g==} + cpu: [arm64] + os: [win32] + + turbo@2.5.3: + resolution: {integrity: sha512-iHuaNcq5GZZnr3XDZNuu2LSyCzAOPwDuo5Qt+q64DfsTP1i3T2bKfxJhni2ZQxsvAoxRbuUK5QetJki4qc5aYA==} + hasBin: true + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + +snapshots: + + '@biomejs/biome@2.0.6': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.0.6 + '@biomejs/cli-darwin-x64': 2.0.6 + '@biomejs/cli-linux-arm64': 2.0.6 + '@biomejs/cli-linux-arm64-musl': 2.0.6 + '@biomejs/cli-linux-x64': 2.0.6 + '@biomejs/cli-linux-x64-musl': 2.0.6 + '@biomejs/cli-win32-arm64': 2.0.6 + '@biomejs/cli-win32-x64': 2.0.6 + + '@biomejs/cli-darwin-arm64@2.0.6': + optional: true + + '@biomejs/cli-darwin-x64@2.0.6': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.0.6': + optional: true + + '@biomejs/cli-linux-arm64@2.0.6': + optional: true + + '@biomejs/cli-linux-x64-musl@2.0.6': + optional: true + + '@biomejs/cli-linux-x64@2.0.6': + optional: true + + '@biomejs/cli-win32-arm64@2.0.6': + optional: true + + '@biomejs/cli-win32-x64@2.0.6': + optional: true + + '@citizenfx/client@2.0.15015-1': {} + + '@citizenfx/server@2.0.14862-1': {} + + '@esbuild/aix-ppc64@0.24.2': + optional: true + + '@esbuild/android-arm64@0.24.2': + optional: true + + '@esbuild/android-arm@0.24.2': + optional: true + + '@esbuild/android-x64@0.24.2': + optional: true + + '@esbuild/darwin-arm64@0.24.2': + optional: true + + '@esbuild/darwin-x64@0.24.2': + optional: true + + '@esbuild/freebsd-arm64@0.24.2': + optional: true + + '@esbuild/freebsd-x64@0.24.2': + optional: true + + '@esbuild/linux-arm64@0.24.2': + optional: true + + '@esbuild/linux-arm@0.24.2': + optional: true + + '@esbuild/linux-ia32@0.24.2': + optional: true + + '@esbuild/linux-loong64@0.24.2': + optional: true + + '@esbuild/linux-mips64el@0.24.2': + optional: true + + '@esbuild/linux-ppc64@0.24.2': + optional: true + + '@esbuild/linux-riscv64@0.24.2': + optional: true + + '@esbuild/linux-s390x@0.24.2': + optional: true + + '@esbuild/linux-x64@0.24.2': + optional: true + + '@esbuild/netbsd-arm64@0.24.2': + optional: true + + '@esbuild/netbsd-x64@0.24.2': + optional: true + + '@esbuild/openbsd-arm64@0.24.2': + optional: true + + '@esbuild/openbsd-x64@0.24.2': + optional: true + + '@esbuild/sunos-x64@0.24.2': + optional: true + + '@esbuild/win32-arm64@0.24.2': + optional: true + + '@esbuild/win32-ia32@0.24.2': + optional: true + + '@esbuild/win32-x64@0.24.2': + optional: true + + '@types/luxon@3.4.2': {} + + '@types/node@20.16.10': + dependencies: + undici-types: 6.19.8 + + '@types/node@22.7.4': + dependencies: + undici-types: 6.19.8 + + boolean@3.2.0: {} + + data-uri-to-buffer@4.0.1: {} + + esbuild@0.24.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.24.2 + '@esbuild/android-arm': 0.24.2 + '@esbuild/android-arm64': 0.24.2 + '@esbuild/android-x64': 0.24.2 + '@esbuild/darwin-arm64': 0.24.2 + '@esbuild/darwin-x64': 0.24.2 + '@esbuild/freebsd-arm64': 0.24.2 + '@esbuild/freebsd-x64': 0.24.2 + '@esbuild/linux-arm': 0.24.2 + '@esbuild/linux-arm64': 0.24.2 + '@esbuild/linux-ia32': 0.24.2 + '@esbuild/linux-loong64': 0.24.2 + '@esbuild/linux-mips64el': 0.24.2 + '@esbuild/linux-ppc64': 0.24.2 + '@esbuild/linux-riscv64': 0.24.2 + '@esbuild/linux-s390x': 0.24.2 + '@esbuild/linux-x64': 0.24.2 + '@esbuild/netbsd-arm64': 0.24.2 + '@esbuild/netbsd-x64': 0.24.2 + '@esbuild/openbsd-arm64': 0.24.2 + '@esbuild/openbsd-x64': 0.24.2 + '@esbuild/sunos-x64': 0.24.2 + '@esbuild/win32-arm64': 0.24.2 + '@esbuild/win32-ia32': 0.24.2 + '@esbuild/win32-x64': 0.24.2 + + eventemitter2@6.4.9: {} + + fast-printf@1.6.9: + dependencies: + boolean: 3.2.0 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + json5@2.2.3: {} + + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + turbo-darwin-64@2.5.3: + optional: true + + turbo-darwin-arm64@2.5.3: + optional: true + + turbo-linux-64@2.5.3: + optional: true + + turbo-linux-arm64@2.5.3: + optional: true + + turbo-windows-64@2.5.3: + optional: true + + turbo-windows-arm64@2.5.3: + optional: true + + turbo@2.5.3: + optionalDependencies: + turbo-darwin-64: 2.5.3 + turbo-darwin-arm64: 2.5.3 + turbo-linux-64: 2.5.3 + turbo-linux-arm64: 2.5.3 + turbo-windows-64: 2.5.3 + turbo-windows-arm64: 2.5.3 + + typescript@5.8.3: {} + + undici-types@6.19.8: {} + + web-streams-polyfill@3.3.3: {} diff --git a/resources/[voice]/yaca-voice/pnpm-workspace.yaml b/resources/[voice]/yaca-voice/pnpm-workspace.yaml new file mode 100644 index 000000000..3ff5faaaf --- /dev/null +++ b/resources/[voice]/yaca-voice/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "apps/*" + - "packages/*" diff --git a/resources/[voice]/yaca-voice/scripts/create-resource.js b/resources/[voice]/yaca-voice/scripts/create-resource.js new file mode 100644 index 000000000..a76f7a72f --- /dev/null +++ b/resources/[voice]/yaca-voice/scripts/create-resource.js @@ -0,0 +1,67 @@ +import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' + +console.log('Building resource...') + +if (existsSync('resource')) { + console.log('Removing existing resource directory...') + rmSync('resource', { recursive: true }) +} + +mkdirSync('resource') +mkdirSync('resource/yaca-voice') + +cpSync('assets/yaca-voice', 'resource/yaca-voice', { recursive: true }) + +mkdirSync('resource/yaca-voice/dist') + +copyFileSync('apps/yaca-client/dist/client.js', 'resource/yaca-voice/dist/client.js') +copyFileSync('apps/yaca-server/dist/server.js', 'resource/yaca-voice/dist/server.js') + +const packageJson = JSON.parse(readFileSync('package.json', { encoding: 'utf8' })) + +writeFileSync( + 'resource/yaca-voice/fxmanifest.lua', + `fx_version 'cerulean' +games { 'gta5', 'rdr3' } +rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.' + +name '${packageJson.name}' +author '${packageJson.author}' +version '${packageJson.version}' +repository '${packageJson.repository.url}' +description '${packageJson.description}' + +dependencies { + '/server:7290', + '/onesync', +} + +ui_page 'web/index.html' + +files { + 'web/index.html', + 'web/script.js', + 'config/shared.json5', + 'config/towers.json5', + 'locales/*.json', +} + +client_script 'dist/client.js' +server_script 'dist/server.js' + +provide 'saltychat' + +`, +) + +if (existsSync('config/yaca-voice/shared.json5')) { + copyFileSync('config/yaca-voice/shared.json5', 'resource/yaca-voice/config/shared.json5') +} + +if (existsSync('config/yaca-voice/server.json5')) { + copyFileSync('config/yaca-voice/server.json5', 'resource/yaca-voice/config/server.json5') +} + +copyFileSync('README.md', 'resource/yaca-voice/README.md') + +console.log('Resource built successfully!') diff --git a/resources/[voice]/yaca-voice/turbo.json b/resources/[voice]/yaca-voice/turbo.json new file mode 100644 index 000000000..12c5511cc --- /dev/null +++ b/resources/[voice]/yaca-voice/turbo.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://turbo.build/schema.json", + "tasks": { + "build": { + "dependsOn": ["typecheck", "^build"] + }, + "typecheck": { + "cache": false + }, + "dev": { + "persistent": true, + "cache": false + }, + "create-resource": { + "dependsOn": ["build"], + "cache": false + } + } +} From f495c860bf7618356178c85b1bfa84ea2802f6ce Mon Sep 17 00:00:00 2001 From: Miho931 <98314142+Miho931@users.noreply.github.com> Date: Mon, 30 Jun 2025 22:15:13 +0200 Subject: [PATCH 2/3] del --- resources/[voice]/yaca-voice/.deepsource.toml | 19 - resources/[voice]/yaca-voice/README.md | 723 ------- .../yaca-voice/apps/yaca-client/build.js | 25 - .../yaca-voice/apps/yaca-client/package.json | 22 - .../apps/yaca-client/pnpm-lock.yaml | 1088 ----------- .../apps/yaca-client/src/bridge/saltychat.ts | 181 -- .../yaca-voice/apps/yaca-client/src/index.ts | 8 - .../apps/yaca-client/src/utils/cache.ts | 79 - .../apps/yaca-client/src/utils/index.ts | 40 - .../apps/yaca-client/src/utils/props.ts | 49 - .../apps/yaca-client/src/utils/redm.ts | 62 - .../apps/yaca-client/src/utils/streaming.ts | 58 - .../apps/yaca-client/src/utils/vectors.ts | 38 - .../apps/yaca-client/src/utils/vehicle.ts | 93 - .../apps/yaca-client/src/utils/websocket.ts | 87 - .../apps/yaca-client/src/yaca/data.ts | 97 - .../apps/yaca-client/src/yaca/index.ts | 6 - .../apps/yaca-client/src/yaca/intercom.ts | 59 - .../apps/yaca-client/src/yaca/main.ts | 1733 ----------------- .../apps/yaca-client/src/yaca/megaphone.ts | 218 --- .../apps/yaca-client/src/yaca/phone.ts | 288 --- .../apps/yaca-client/src/yaca/radio.ts | 1124 ----------- .../yaca-voice/apps/yaca-client/tsconfig.json | 9 - .../yaca-voice/apps/yaca-server/build.js | 23 - .../yaca-voice/apps/yaca-server/package.json | 25 - .../apps/yaca-server/pnpm-lock.yaml | 1136 ----------- .../apps/yaca-server/src/bridge/saltychat.ts | 165 -- .../yaca-voice/apps/yaca-server/src/index.ts | 5 - .../apps/yaca-server/src/utils/cache.ts | 8 - .../apps/yaca-server/src/utils/events.ts | 23 - .../apps/yaca-server/src/utils/generator.ts | 34 - .../apps/yaca-server/src/utils/index.ts | 4 - .../yaca-server/src/utils/versioncheck.ts | 57 - .../apps/yaca-server/src/yaca/index.ts | 4 - .../apps/yaca-server/src/yaca/main.ts | 394 ---- .../apps/yaca-server/src/yaca/megaphone.ts | 86 - .../apps/yaca-server/src/yaca/phone.ts | 278 --- .../apps/yaca-server/src/yaca/radio.ts | 379 ---- .../yaca-voice/apps/yaca-server/tsconfig.json | 9 - .../assets/yaca-voice/config/server.json5 | 17 - .../assets/yaca-voice/config/shared.json5 | 174 -- .../assets/yaca-voice/config/towers.json5 | 519 ----- .../assets/yaca-voice/locales/de.json | 33 - .../assets/yaca-voice/locales/en.json | 33 - .../assets/yaca-voice/web/index.html | 13 - .../assets/yaca-voice/web/script.js | 85 - resources/[voice]/yaca-voice/biome.json | 47 - resources/[voice]/yaca-voice/package.json | 27 - .../yaca-voice/packages/common/package.json | 23 - .../yaca-voice/packages/common/pnpm-lock.yaml | 30 - .../yaca-voice/packages/common/src/bridge.ts | 11 - .../yaca-voice/packages/common/src/config.ts | 71 - .../packages/common/src/constants.ts | 5 - .../packages/common/src/errorlevel.ts | 20 - .../yaca-voice/packages/common/src/index.ts | 58 - .../yaca-voice/packages/common/src/locale.ts | 91 - .../yaca-voice/packages/common/tsconfig.json | 8 - .../yaca-voice/packages/tsconfig/fivem.json | 21 - .../yaca-voice/packages/tsconfig/package.json | 3 - .../yaca-voice/packages/types/package.json | 10 - .../yaca-voice/packages/types/src/config.ts | 305 --- .../yaca-voice/packages/types/src/enums.ts | 40 - .../yaca-voice/packages/types/src/index.ts | 3 - .../yaca-voice/packages/types/src/types.ts | 98 - resources/[voice]/yaca-voice/pnpm-lock.yaml | 622 ------ .../[voice]/yaca-voice/pnpm-workspace.yaml | 3 - .../yaca-voice/scripts/create-resource.js | 67 - resources/[voice]/yaca-voice/turbo.json | 19 - 68 files changed, 11192 deletions(-) delete mode 100644 resources/[voice]/yaca-voice/.deepsource.toml delete mode 100644 resources/[voice]/yaca-voice/README.md delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/build.js delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/package.json delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/pnpm-lock.yaml delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/bridge/saltychat.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/index.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/utils/cache.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/utils/index.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/utils/props.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/utils/redm.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/utils/streaming.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/utils/vectors.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/utils/vehicle.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/utils/websocket.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/data.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/index.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/intercom.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/main.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/megaphone.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/phone.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/radio.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-client/tsconfig.json delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/build.js delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/package.json delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/pnpm-lock.yaml delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/src/bridge/saltychat.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/src/index.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/src/utils/cache.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/src/utils/events.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/src/utils/generator.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/src/utils/index.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/src/utils/versioncheck.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/index.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/main.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/megaphone.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/phone.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/radio.ts delete mode 100644 resources/[voice]/yaca-voice/apps/yaca-server/tsconfig.json delete mode 100644 resources/[voice]/yaca-voice/assets/yaca-voice/config/server.json5 delete mode 100644 resources/[voice]/yaca-voice/assets/yaca-voice/config/shared.json5 delete mode 100644 resources/[voice]/yaca-voice/assets/yaca-voice/config/towers.json5 delete mode 100644 resources/[voice]/yaca-voice/assets/yaca-voice/locales/de.json delete mode 100644 resources/[voice]/yaca-voice/assets/yaca-voice/locales/en.json delete mode 100644 resources/[voice]/yaca-voice/assets/yaca-voice/web/index.html delete mode 100644 resources/[voice]/yaca-voice/assets/yaca-voice/web/script.js delete mode 100644 resources/[voice]/yaca-voice/biome.json delete mode 100644 resources/[voice]/yaca-voice/package.json delete mode 100644 resources/[voice]/yaca-voice/packages/common/package.json delete mode 100644 resources/[voice]/yaca-voice/packages/common/pnpm-lock.yaml delete mode 100644 resources/[voice]/yaca-voice/packages/common/src/bridge.ts delete mode 100644 resources/[voice]/yaca-voice/packages/common/src/config.ts delete mode 100644 resources/[voice]/yaca-voice/packages/common/src/constants.ts delete mode 100644 resources/[voice]/yaca-voice/packages/common/src/errorlevel.ts delete mode 100644 resources/[voice]/yaca-voice/packages/common/src/index.ts delete mode 100644 resources/[voice]/yaca-voice/packages/common/src/locale.ts delete mode 100644 resources/[voice]/yaca-voice/packages/common/tsconfig.json delete mode 100644 resources/[voice]/yaca-voice/packages/tsconfig/fivem.json delete mode 100644 resources/[voice]/yaca-voice/packages/tsconfig/package.json delete mode 100644 resources/[voice]/yaca-voice/packages/types/package.json delete mode 100644 resources/[voice]/yaca-voice/packages/types/src/config.ts delete mode 100644 resources/[voice]/yaca-voice/packages/types/src/enums.ts delete mode 100644 resources/[voice]/yaca-voice/packages/types/src/index.ts delete mode 100644 resources/[voice]/yaca-voice/packages/types/src/types.ts delete mode 100644 resources/[voice]/yaca-voice/pnpm-lock.yaml delete mode 100644 resources/[voice]/yaca-voice/pnpm-workspace.yaml delete mode 100644 resources/[voice]/yaca-voice/scripts/create-resource.js delete mode 100644 resources/[voice]/yaca-voice/turbo.json diff --git a/resources/[voice]/yaca-voice/.deepsource.toml b/resources/[voice]/yaca-voice/.deepsource.toml deleted file mode 100644 index 6ac43a34b..000000000 --- a/resources/[voice]/yaca-voice/.deepsource.toml +++ /dev/null @@ -1,19 +0,0 @@ -version = 1 - -exclude_patterns = [ - "resources/**", - "dist/**" -] - -[[analyzers]] -name = "secrets" - -[[analyzers]] -name = "javascript" - - [analyzers.meta] - environment = [ - "jquery", - "nodejs", - "browser" - ] \ No newline at end of file diff --git a/resources/[voice]/yaca-voice/README.md b/resources/[voice]/yaca-voice/README.md deleted file mode 100644 index dff0df44d..000000000 --- a/resources/[voice]/yaca-voice/README.md +++ /dev/null @@ -1,723 +0,0 @@ -# [yaca.systems](https://yaca.systems/) for [FiveM](https://fivem.net/) & [RedM](https://redm.net/) - -This is a example implementation for [FiveM](https://fivem.net/) & [RedM](https://redm.net/). -Feel free to report bugs via issues or contribute via pull requests. - -Join our [Discord](http://discord.yaca.systems/) to get help or make suggestions and start -using [yaca.systems](https://yaca.systems/) today! - -# Setup Steps - -Before you start, make sure you have OneSync enabled and your server artifacts are up to date. - -1. Download and install the latest [release](https://github.com/yaca-systems/fivem-yaca-typescript/releases) of this - resource. -2. Add `start yaca-voice` into your `server.cfg`. -3. Open `config/server.json5` and adjust the variables to your needs. -4. Open `config/shared.json5` and adjust the variables to your needs. - -# Exports - -
-Client - -### General - -#### `getVoiceRange(): int` - -Get the current voice range of the player as `int`. - -#### `getVoiceRanges(): int[]` - -Get all voice ranges as `int[]`. - -#### `changeVoiceRange(increase: boolean): void` - -Change the voice range of the player to the next range. - -#### `setVoiceRange(range: number): void` - -Set the voice range of the player. - -#### `setVoiceRangeChangeAllowedState(state: boolean): void` - -Enable or disable the possibility to change the voice range. - -| Parameter | Type | Description | -|-----------|-----------|------------------------------------------------| -| state | `boolean` | `true` to allow the voice range change, `false` to disable | - -#### `getVoiceRangeChangeAllowedState(): boolean` - -Get the voice range change allowed state of the player as `boolean`. - -#### `setMaxVoiceRange(range: number): void` - -Set the maximum allowed voice range of the player in meters to limit the voice range temporarily. - -| Parameter | Type | Description | -|-----------|-----------|------------------------------------------------| -| range | `number` | `-1` to disable the limit, or a number in meters to set the limit | - -#### `getMaxVoiceRange(): number` - -Get the maximum allowed voice range of the player in meters. - -#### `getMicrophoneMuteState(): boolean` - -Get the microphone mute state of the player as `boolean`. - -#### `getMicrophoneDisabledState(): boolean` - -Get the microphone disabled state of the player as `boolean`. - -#### `getSoundMuteState(): boolean` - -Get the sound mute state of the player as `boolean`. - -#### `getSoundDisabledState(): boolean` - -Get the sound disabled state of the player as `boolean`. - -#### `getPluginState(): string` - -Get the current plugin state as `string`. - -The state can be one of the following: - -- `"NOT_CONNECTED"`: The plugin is not connected -- `"CONNECTED`: The plugin is connected -- `"OUTDATED_VERSION"`: The plugin is not the version set in the dashboard -- `"WRONG_TS_SERVER"`: The user is connected to the wrong Teamspeak server -- `"IN_INGAME_CHANNEL"`: The user is in the ingame channel -- `"IN_EXCLUDED_CHANNEL"`: The user is in an excluded channel - -#### `getGlobalErrorLevel(): number` - -Get the global error level as `number`. - -#### `setSpectatingPlayer(playerId: number | false)` - -Set the player to spectate. - -| Parameter | Type | Description | -|-----------|-------------------|-------------------| -| playerId | `number \| false` | the player to set | - -#### `getSpectatingPlayer(): number` - -Get the player the user is spectating as `number`. - -#### `setVoiceRangeMarkerColor(red: number, green: number, blue: number, alpha: number)` - -Set the voice range marker color. - -#### `getVoiceRangeMarkerColor(): [number, number, number, number]` - -Get the voice range marker color as `[red, green, blue, alpha]`. - -#### `resetVoiceRangeMarkerColor()` - -Reset the voice range marker color to the default color defined in the config. - -### Radio - -#### `enableRadio(state: boolean)` - -Enables or disables the radio system. - -| Parameter | Type | Description | -|-----------|-----------|------------------------------------------------| -| state | `boolean` | `true` to enable the radio, `false` to disable | - -#### `isRadioEnabled(): boolean` - -Returns whether the radio system is enabled as `boolean`. - -#### `changeRadioFrequency(frequency: string)` - -Changes the radio frequency of the active channel. - -| Parameter | Type | Description | -|-----------|----------|--------------------------------------------| -| frequency | `string` | The frequency to set the active channel to | - -#### `changeRadioFrequencyRaw(channel: number, frequency: string)` - -Changes the radio frequency. - -| Parameter | Type | Description | -|-----------|----------|---------------------------------------------------------------------------------------| -| channel? | `number` | the channel number. Defaults to the current active channel when no channel is passed. | -| frequency | `string` | the frequency to set the channel to | - -#### `getRadioFrequency(channel: number): string` - -Returns the frequency of a radio channel as `string`. - -| Parameter | Type | Description | -|-----------|----------|---------------------------------------------------------------------------------------| -| channel? | `number` | the channel number. Defaults to the current active channel when no channel is passed. | - -#### `muteRadioChannel(state?: boolean)` - -Mutes the current active radio channel. - -| Parameter | Type | Description | -|-----------|-----------|-----------------------------------------------------------------------------| -| state? | `boolean` | `true` to mute the channel, `false` to unmute. Defaults to switch if not defined | - -#### `muteRadioChannelRaw(channel: number, state?: boolean)` - -Mutes a radio channel. - -| Parameter | Type | Description | -|-----------|----------|----------------------------------------------------------------------------------------| -| channel? | `number` | the channel to mute. Defaults to the current active channel when no channel is passed. | -| state? | `boolean` | `true` to mute the channel, `false` to unmute. Defaults to switch if not defined | - -#### `isRadioChannelMuted(channel: number): boolean` - -Returns whether a radio channel is muted as `boolean`. - -| Parameter | Type | Description | -|-----------|----------|--------------------| -| channel | `number` | the channel number | - -#### `setActiveRadioChannel(channel: number): bool` - -Changes the active radio channel. Returns whether the operation was successful as `bool`. - -| Parameter | Type | Description | -|-----------|----------|-----------------------| -| channel | `number` | the new radio channel | - -#### `getActiveRadioChannel(): number` - -Returns the active radio channel as `number`. - -#### `setSecondaryRadioChannel(channel: number): bool` - -Changes the secondary radio channel. Returns whether the operation was successful as `bool`. - -| Parameter | Type | Description | -|-----------|----------|-----------------------| -| channel | `number` | the new radio channel | - -#### `getSecondaryRadioChannel(): number` - -Returns the secondary radio channel as `number`. - -#### `changeRadioChannelVolume(higher: boolean): bool` - -Changes the volume of the active radio channel. Returns whether the operation was successful as `bool`. - -| Parameter | Type | Description | -|-----------|-----------|--------------------------------| -| higher | `boolean` | whether to increase the volume | - -#### `changeRadioChannelVolumeRaw(channel: number, volume: number): bool` - -Changes the volume of a radio channel. Returns whether the operation was successful as `bool`. - -| Parameter | Type | Description | -|-----------|----------|--------------------| -| channel | `number` | the channel number | -| volume | `number` | the volume to set | - -#### `getRadioChannelVolume(channel: number): number` - -Returns the volume of a radio channel as `number`. - -| Parameter | Type | Description | -|-----------|----------|--------------------| -| channel | `number` | the channel number | - -#### `changeRadioChannelStereo(): bool` - -Changes the stereo mode of the active radio channel. Returns whether the operation was successful as `bool`. - -#### `changeRadioChannelStereoRaw(channel: number, stereo: string): bool` - -Changes the stereo mode of a radio channel. Returns whether the operation was successful as `bool`. - -| Parameter | Type | Description | -|-----------|----------|---------------------------------------------------------------| -| channel | `number` | the channel number | -| stereo | `string` | the stereo mode (`"MONO_LEFT"`, `"MONO_RIGHT"` or `"STEREO"`) | - -#### `getRadioChannelStereo(channel: number): string` - -Returns the stereo mode of a radio channel as `string`. - -| Parameter | Type | Description | -|-----------|----------|--------------------| -| channel | `number` | the channel number | - -#### `radioTalkingStart(state: boolean, channel: number)` - -Starts or stops talking on the radio. - -| Parameter | Type | Description | -|-----------|-----------|------------------------------------------| -| state | `boolean` | `true` to start talking, `false` to stop | -| channel | `number` | the channel to talk on | - -#### `setRadioMode(mode: string)` - -Sets the radio mode. - -| Parameter | Type | Description | -|-----------|-----------------------------------------------------------------------------| -| mode | `string` | the radio mode to set. Can be either `None`, `Direct` or `Tower` | - -#### `getRadioMode(): string` - -Returns the radio mode as `string`. - -### Phone - -#### `isInCall(): boolean` - -Returns whether the player is in a phone call as a `boolean`. - -### Megaphone - -#### `getCanUseMegaphone(): boolean` - -Returns whether the player can use the megaphone as a `boolean`. - -#### `setCanUseMegaphone(state: boolean)` - -Sets whether the player can use the megaphone. - -| Parameter | Type | Description | -|-----------|-----------|---------------------------------------------------------| -| state | `boolean` | `true` to allow using of megaphone, `false` to disallow | - -### `useMegaphone(state: boolean)` - -Starts or stops using the megaphone. - -| Parameter | Type | Description | -|-----------|-----------|----------------------------------------| -| state | `boolean` | `true` to start using, `false` to stop | - -
- -
-Server - -### General - -#### `connectToVoice(source: number)` -Connects a player to the voice system. - -| Parameter | Type | Description | -|-----------|----------|-------------------| -| source | `number` | the player source | - -#### `getPlayerAliveStatus(source: number): bool` - -Get the alive status of a player as `bool`. - -| Parameter | Type | Description | -|-----------|----------|-------------------| -| source | `number` | the player source | - -#### `setPlayerAliveStatus(source: number, state: bool)` - -Set the alive status of a player. - -| Parameter | Type | Description | -|-----------|-----------|---------------------| -| source | `number` | the player source | -| state | `boolean` | the new alive state | - -#### `getPlayerVoiceRange(source: number): number` - -Get the voice range of a player as `number`. - -| Parameter | Type | Description | -|-----------|----------|-------------------| -| source | `number` | the player source | - -#### `setPlayerVoiceRange(source: number, range: number)` - -Set the voice range of a player. - -| Parameter | Type | Description | -|-----------|----------|---------------------------------------------------------------------------| -| source | `number` | the player source | -| range | `number` | The new voice range. Defaults to the default voice range if not provided. | - -### Radio - -#### `getPlayersInRadioFrequency(frequency: string): int[]` - -Returns all players in a radio frequency as `int[]`. - -| Parameter | Type | Description | -|-----------|----------|----------------------| -| frequency | `string` | the frequency to get | - -#### `setPlayerRadioChannel(source: number, channel: number, frequency: string)` - -Sets the radio channel of a player. - -| Parameter | Type | Description | -|-----------|----------|----------------------| -| source | `number` | the player source | -| channel | `number` | the channel to set | -| frequency | `string` | the frequency to set | - -#### `getPlayerHasLongRange(source: number): bool` - -Returns whether a player has long range enabled as `bool`. - -| Parameter | Type | Description | -|-----------|----------|-------------------| -| source | `number` | the player source | - -#### `setPlayerHasLongRange(source: number, state: bool)` - -Sets the long range state of a player. - -| Parameter | Type | Description | -|-----------|-----------|----------------------| -| source | `number` | the player source | -| state | `boolean` | the long range state | - -### Phone - -#### `callPlayer(source: number, target: number, state: bool)` - -Creates a phone call between two players. - -| Parameter | Type | Description | -|-----------|-----------|--------------------------| -| source | `number` | the player source | -| target | `number` | the target player source | -| state | `boolean` | the state of the call | - -#### `callPlayerOldEffect(source: number, target: number, state: bool)` - -Creates a phone call between two players with the old effect. - -| Parameter | Type | Description | -|-----------|-----------|--------------------------| -| source | `number` | the player source | -| target | `number` | the target player source | -| state | `boolean` | the state of the call | - -#### `muteOnPhone(source: number, state: bool)` - -Mutes the player when using the phone. - -| Parameter | Type | Description | -|-----------|-----------|-------------------| -| source | `number` | the player source | -| state | `boolean` | the mute state | - -#### `enablePhoneSpeaker(source: number, state: bool)` - -Enable or disable the phone speaker for a player. - -| Parameter | Type | Description | -|-----------|-----------|-------------------------| -| source | `number` | the player source | -| state | `boolean` | the phone speaker state | - -#### `isPlayerInCall(source: number): [bool, number[]]` - -Returns whether a player is in a phone call as `[bool, number[]]`. - -| Parameter | Type | Description | -|-----------|----------|-------------------| -| source | `number` | the player source | - -#### `setGlobalErrorLevel(level: number)` - -Sets the global error level. - -| Parameter | Type | Description | -|-----------|----------|-----------------| -| level | `number` | the error level | - -#### `getGlobalErrorLevel(): number` - -Returns the global error level as `number`. - -
- -# Events - -
-Client - -### yaca:external:pluginInitialized - -The event is triggered when the plugin is initialized. - -| Parameter | Type | Description | -|-----------|-------|----------------------------------------------| -| clientId | `int` | the client id of the local user in teamspeak | - -### yaca:external:pluginStateChanged - -The event is triggered when the plugin state changes. - -| Parameter | Type | Description | -|-----------|----------|----------------------------------------------| -| state | `string` | the current plugin state, as explained below | - -The state can be one of the following: - -- `"NOT_CONNECTED"`: The plugin is not connected -- `"CONNECTED`: The plugin is connected -- `"OUTDATED_VERSION"`: The plugin is not the version set in the dashboard -- `"WRONG_TS_SERVER"`: The user is connected to the wrong Teamspeak server -- `"IN_INGAME_CHANNEL"`: The user is in the ingame channel -- `"IN_EXCLUDED_CHANNEL"`: The user is in an excluded channel - -### yaca:external:voiceRangeUpdate - -This event is triggered when the voice range of a player is updated. - -| Parameter | Type | Description | -|------------|-------|---------------------------| -| range | `int` | the newly set voice range | -| rangeIndex | `int` | the index of the range | - -### yaca:external:muteStateChanged - -DEPRECATED: Use `yaca:external:microphoneMuteStateChanged` instead. -The event is triggered when the mute state of a player changes. - -| Parameter | Type | Description | -|-----------|-----------|--------------------| -| state | `boolean` | the new mute state | - -### yaca:external:microphoneMuteStateChanged - -The event is triggered when the microphone mute state of a player changes. - -| Parameter | Type | Description | -|-----------|-----------|--------------------| -| state | `boolean` | the new mute state | - -### yaca:external:microphoneDisabledStateChanged - -The event is triggered when the microphone disabled state of a player changes. - -| Parameter | Type | Description | -|-----------|-----------|--------------------| -| state | `boolean` | the new mute state | - -### yaca:external:soundMuteStateChanged - -The event is triggered when the sound mute state of a player changes. - -| Parameter | Type | Description | -|-----------|-----------|--------------------| -| state | `boolean` | the new mute state | - -### yaca:external:soundDisabledStateChanged - -The event is triggered when the sound disabled state of a player changes. - -| Parameter | Type | Description | -|-----------|-----------|--------------------| -| state | `boolean` | the new mute state | - -### yaca:external:isTalking - -The event is triggered when a player starts or stops talking. - -| Parameter | Type | Description | -|-----------|-----------|-----------------------| -| state | `boolean` | the new talking state | - -### yaca:external:megaphoneState - -The event is triggered when the megaphone state of a player changes. - -| Parameter | Type | Description | -|-----------|-----------|-------------------------| -| state | `boolean` | the new megaphone state | - -### yaca:external:setRadioMuteState - -The event is triggered when the radio mute state of a player changes. - -| Parameter | Type | Description | -|-----------|-----------|---------------------------------------------| -| channel | `number` | the channel where the mute state is changed | -| state | `boolean` | the new mute state | - -### yaca:external:isRadioEnabled - -The event is triggered when the radio state of a player changes. - -| Parameter | Type | Description | -|-----------|-----------|----------------------------------------------------------------------| -| state | `boolean` | `true` when the radio is enabled, `false` when the radio is disabled | - -### yaca:external:changedActiveRadioChannel - -The event is triggered when the active radio channel of a player changes. - -| Parameter | Type | Description | -|-----------|----------|------------------------------| -| channel | `number` | the new active radio channel | - -### yaca:external:changedSecondaryRadioChannel - -The event is triggered when the secondary radio channel of a player changes. - -| Parameter | Type | Description | -|-----------|----------|---------------------------------------------------| -| channel | `number` | the new active radio channel, or `-1` if disabled | - -### yaca:external:setRadioVolume - -The event is triggered when the radio volume of a player changes. - -| Parameter | Type | Description | -|-----------|----------|-----------------------| -| channel | `number` | the channel to change | -| volume | `number` | the new volume to set | - -### yaca:external:setRadioChannelStereo - -The event is triggered when the stereo mode of a radio channel changes. - -| Parameter | Type | Description | -|-----------|----------|-----------------------------------------------------------------------------------------------| -| channel | `number` | the channel to change | -| stereo | `string` | `"MONO_LEFT"` for the left ear, `"MONO_RIGHT"` for the right ear and `"STEREO"` for both ears | - -### yaca:external:setRadioFrequency - -The event is triggered when the radio frequency of a player changes. - -| Parameter | Type | Description | -|-----------|----------|----------------------| -| channel | `number` | the channel to set | -| frequency | `string` | the frequency to set | - -### yaca:external:isRadioTalking - -The event is triggered when a player starts or stops talking on the radio. - -| Parameter | Type | Description | -|-----------|-----------|--------------------------------------------| -| state | `boolean` | the new talking state | -| channel | `number` | the channel where the player is talking at | - -### yaca:external:isRadioReceiving - -The event is triggered when a player starts or stops receiving on the radio. - -| Parameter | Type | Description | -|-----------|-----------|------------------------------------------------| -| state | `boolean` | the new receiver state | -| channel | `number` | the channel from which the player is receiving | - -### yaca:external:notification - -The event is triggered when a notification should be shown. - -| Parameter | Type | Description | -|-----------|----------|--------------------------------------------------------------| -| message | `string` | the message to show | -| type | `string` | the type of the message (`"inform"`, `"error"`, `"success"`) | - -Example for custom notification: - -```lua -AddEventHandler('yaca:external:notification', function (message, type) - -- Call your Notifications System here. -end) -``` - -### yaca:external:channelChanged - -The event is triggered when the player changes the channel to the ingame or excluded channel. - -| Parameter | Type | Description | -|-------------|----------|------------------------------------------------------------------------------------------------------------------| -| channelType | `string` | `INGAME_CHANNEL` when moving into the ingame channel and `EXCLUDED_CHANNEL` when moving into a excluded channel. | - -
- -
-Server - -### yaca:external:changeMegaphoneState - -The event is triggered when the megaphone state of a player changes. - -| Parametr | Type | Description | -|----------|-----------|-------------------------| -| source | `int` | the player source | -| state | `boolean` | the new megaphone state | - -### yaca:external:phoneCall - -The event is triggered when a phone call is started or ended. - -| Parameter | Type | Description | -|-----------|------------------|---------------------------------------------------------------------------------| -| source | `int` | the player source | -| target | `int` | the target player source | -| state | `boolean` | the new phone call state | -| filter | `YacaFilterEnum` | the used filter for the phone call, can be either `PHONE` or `PHONE_HISTORICAL` | - -### yaca:external:phoneSpeaker - -The event is triggered when the phone speaker state of a player changes. - -| Parameter | Type | Description | -|-----------|-----------|-----------------------------| -| source | `int` | the player source | -| state | `boolean` | the new phone speaker state | - -### yaca:external:changedRadioFrequency - -The event is triggered when the radio frequency of a player changes. - -| Parameter | Type | Description | -|-----------|----------|-----------------------------------------| -| source | `int` | the player source | -| channel | `int` | the channel where the frequency was set | -| frequency | `string` | the frequency to set | - -### yaca:external:changedRadioMuteState - -The event is triggered when the radio mute state of a player changes. - -| Parameter | Type | Description | -|-----------|-----------|----------------------------------------------| -| source | `int` | the player source | -| channel | `int` | the channel where the mute state was changed | -| state | `boolean` | the new mute state | - -
- -# Developers - -If you want to contribute to this project, feel free to do so. We are happy about every contribution. If you have any -questions, feel free to ask in our [Discord](http://discord.yaca.systems/). - -## Building the resource - -To build the resource, you need to have [Node.js](https://nodejs.org/) installed. After that, you can run the following -commands to build the resource: - -```bash -pnpm install -pnpm run build -``` - -The built resource will be located in the `resource` folder, which you can then use in your FiveM server. diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/build.js b/resources/[voice]/yaca-voice/apps/yaca-client/build.js deleted file mode 100644 index 175b4f4e2..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-client/build.js +++ /dev/null @@ -1,25 +0,0 @@ -import { build } from 'esbuild' - -const production = process.argv.includes('--mode=production') - -build({ - entryPoints: ['src/index.ts'], - outfile: './dist/client.js', - bundle: true, - loader: { - '.ts': 'ts', - '.js': 'js', - }, - write: true, - platform: 'browser', - target: 'es2021', - format: 'iife', - minify: production, - sourcemap: production ? false : 'inline', - dropLabels: production ? ['DEV'] : undefined, -}) - .then(() => { - console.log('Client built successfully') - }) - // skipcq: JS-0263 - .catch(() => process.exit(1)) diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/package.json b/resources/[voice]/yaca-voice/apps/yaca-client/package.json deleted file mode 100644 index da6c9f1c8..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-client/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "yaca-client", - "version": "0.0.0", - "private": true, - "type": "module", - "scripts": { - "build": "node build.js --mode=production", - "dev": "node build.js", - "typecheck": "tsc --project tsconfig.json" - }, - "dependencies": { - "eventemitter2": "^6.4.9" - }, - "devDependencies": { - "@citizenfx/client": "latest", - "@types/luxon": "^3.4.2", - "@types/node": "^20.16.10", - "@yaca-voice/common": "workspace:*", - "@yaca-voice/types": "workspace:*", - "@yaca-voice/typescript-config": "workspace:*" - } -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/pnpm-lock.yaml b/resources/[voice]/yaca-voice/apps/yaca-client/pnpm-lock.yaml deleted file mode 100644 index 8015206fc..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-client/pnpm-lock.yaml +++ /dev/null @@ -1,1088 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@overextended/ox_lib': - specifier: ^3.23.1 - version: 3.24.0 - devDependencies: - '@citizenfx/server': - specifier: latest - version: 2.0.9235-1 - '@eslint/js': - specifier: ^8.57.0 - version: 8.57.0 - '@types/luxon': - specifier: ^3.4.2 - version: 3.4.2 - '@types/node': - specifier: ^20.14.10 - version: 20.14.14 - esbuild: - specifier: ^0.20.2 - version: 0.20.2 - eslint: - specifier: ^8.57.0 - version: 8.57.0 - -packages: - - '@citizenfx/server@2.0.9235-1': - resolution: {integrity: sha512-x5RgQKm+nIgmABzxQpOVvrZOTTsSWDKOkT87gbjnILW3zOWokq3LU7Ag+Ki1HvPXAe43laJdy9wUUej27KhsRQ==} - - '@esbuild/aix-ppc64@0.20.2': - resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.20.2': - resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.20.2': - resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.20.2': - resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.20.2': - resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.20.2': - resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.20.2': - resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.20.2': - resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.20.2': - resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.20.2': - resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.20.2': - resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.20.2': - resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.20.2': - resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.20.2': - resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.20.2': - resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.20.2': - resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.20.2': - resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-x64@0.20.2': - resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-x64@0.20.2': - resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - - '@esbuild/sunos-x64@0.20.2': - resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.20.2': - resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.20.2': - resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.20.2': - resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - - '@eslint-community/eslint-utils@4.4.0': - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.11.0': - resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/eslintrc@2.1.4': - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - '@eslint/js@8.57.0': - resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - '@humanwhocodes/config-array@0.11.14': - resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/object-schema@2.0.3': - resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} - deprecated: Use @eslint/object-schema instead - - '@nativewrappers/client@1.7.33': - resolution: {integrity: sha512-phuBBGdDPxZiZyw5CaFs1XWfvllnEtwATMdLaNucwMofVg/O/FjlP1bTUq4SOm4qhSZ4Zdo351ijHzBSIbZs6g==} - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@overextended/ox_lib@3.24.0': - resolution: {integrity: sha512-diI+hvmGfDBoYzuFeKrVLrdXff1yYIgARNP22i8oQqrzlBig46HJMXm7YRmwsE+8Z/w6TwQv9UL9j/vzVGpnnQ==} - - '@types/luxon@3.4.2': - resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} - - '@types/node@20.14.14': - resolution: {integrity: sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==} - - '@ungap/structured-clone@1.2.0': - resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@8.12.1: - resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} - engines: {node: '>=0.4.0'} - hasBin: true - - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - boolean@3.2.0: - resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} - - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} - - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - - debug@4.3.6: - resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - - esbuild@0.20.2: - resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} - engines: {node: '>=12'} - hasBin: true - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint@8.57.0: - resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - hasBin: true - - espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - - fast-printf@1.6.9: - resolution: {integrity: sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg==} - engines: {node: '>=10.0'} - - fastq@1.17.1: - resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} - - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} - - flatted@3.3.1: - resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} - - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported - - globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} - - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - ignore@5.3.1: - resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} - engines: {node: '>= 4'} - - import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - - typescript@5.5.4: - resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} - engines: {node: '>=14.17'} - hasBin: true - - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - -snapshots: - - '@citizenfx/server@2.0.9235-1': {} - - '@esbuild/aix-ppc64@0.20.2': - optional: true - - '@esbuild/android-arm64@0.20.2': - optional: true - - '@esbuild/android-arm@0.20.2': - optional: true - - '@esbuild/android-x64@0.20.2': - optional: true - - '@esbuild/darwin-arm64@0.20.2': - optional: true - - '@esbuild/darwin-x64@0.20.2': - optional: true - - '@esbuild/freebsd-arm64@0.20.2': - optional: true - - '@esbuild/freebsd-x64@0.20.2': - optional: true - - '@esbuild/linux-arm64@0.20.2': - optional: true - - '@esbuild/linux-arm@0.20.2': - optional: true - - '@esbuild/linux-ia32@0.20.2': - optional: true - - '@esbuild/linux-loong64@0.20.2': - optional: true - - '@esbuild/linux-mips64el@0.20.2': - optional: true - - '@esbuild/linux-ppc64@0.20.2': - optional: true - - '@esbuild/linux-riscv64@0.20.2': - optional: true - - '@esbuild/linux-s390x@0.20.2': - optional: true - - '@esbuild/linux-x64@0.20.2': - optional: true - - '@esbuild/netbsd-x64@0.20.2': - optional: true - - '@esbuild/openbsd-x64@0.20.2': - optional: true - - '@esbuild/sunos-x64@0.20.2': - optional: true - - '@esbuild/win32-arm64@0.20.2': - optional: true - - '@esbuild/win32-ia32@0.20.2': - optional: true - - '@esbuild/win32-x64@0.20.2': - optional: true - - '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': - dependencies: - eslint: 8.57.0 - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.11.0': {} - - '@eslint/eslintrc@2.1.4': - dependencies: - ajv: 6.12.6 - debug: 4.3.6 - espree: 9.6.1 - globals: 13.24.0 - ignore: 5.3.1 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@8.57.0': {} - - '@humanwhocodes/config-array@0.11.14': - dependencies: - '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.6 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/object-schema@2.0.3': {} - - '@nativewrappers/client@1.7.33': {} - - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.17.1 - - '@overextended/ox_lib@3.24.0': - dependencies: - '@nativewrappers/client': 1.7.33 - csstype: 3.1.3 - fast-printf: 1.6.9 - typescript: 5.5.4 - - '@types/luxon@3.4.2': {} - - '@types/node@20.14.14': - dependencies: - undici-types: 5.26.5 - - '@ungap/structured-clone@1.2.0': {} - - acorn-jsx@5.3.2(acorn@8.12.1): - dependencies: - acorn: 8.12.1 - - acorn@8.12.1: {} - - ajv@6.12.6: - 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-regex@5.0.1: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - argparse@2.0.1: {} - - balanced-match@1.0.2: {} - - boolean@3.2.0: {} - - brace-expansion@1.1.11: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - callsites@3.1.0: {} - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - concat-map@0.0.1: {} - - cross-spawn@7.0.3: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - csstype@3.1.3: {} - - debug@4.3.6: - dependencies: - ms: 2.1.2 - - deep-is@0.1.4: {} - - doctrine@3.0.0: - dependencies: - esutils: 2.0.3 - - esbuild@0.20.2: - optionalDependencies: - '@esbuild/aix-ppc64': 0.20.2 - '@esbuild/android-arm': 0.20.2 - '@esbuild/android-arm64': 0.20.2 - '@esbuild/android-x64': 0.20.2 - '@esbuild/darwin-arm64': 0.20.2 - '@esbuild/darwin-x64': 0.20.2 - '@esbuild/freebsd-arm64': 0.20.2 - '@esbuild/freebsd-x64': 0.20.2 - '@esbuild/linux-arm': 0.20.2 - '@esbuild/linux-arm64': 0.20.2 - '@esbuild/linux-ia32': 0.20.2 - '@esbuild/linux-loong64': 0.20.2 - '@esbuild/linux-mips64el': 0.20.2 - '@esbuild/linux-ppc64': 0.20.2 - '@esbuild/linux-riscv64': 0.20.2 - '@esbuild/linux-s390x': 0.20.2 - '@esbuild/linux-x64': 0.20.2 - '@esbuild/netbsd-x64': 0.20.2 - '@esbuild/openbsd-x64': 0.20.2 - '@esbuild/sunos-x64': 0.20.2 - '@esbuild/win32-arm64': 0.20.2 - '@esbuild/win32-ia32': 0.20.2 - '@esbuild/win32-x64': 0.20.2 - - escape-string-regexp@4.0.0: {} - - eslint-scope@7.2.2: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint@8.57.0: - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@eslint-community/regexpp': 4.11.0 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.0 - '@humanwhocodes/config-array': 0.11.14 - '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.2.0 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.6 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - esquery: 1.6.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - find-up: 5.0.0 - glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 - ignore: 5.3.1 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.0 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color - - espree@9.6.1: - dependencies: - acorn: 8.12.1 - acorn-jsx: 5.3.2(acorn@8.12.1) - eslint-visitor-keys: 3.4.3 - - esquery@1.6.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - - esutils@2.0.3: {} - - fast-deep-equal@3.1.3: {} - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - fast-printf@1.6.9: - dependencies: - boolean: 3.2.0 - - fastq@1.17.1: - dependencies: - reusify: 1.0.4 - - file-entry-cache@6.0.1: - dependencies: - flat-cache: 3.2.0 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - flat-cache@3.2.0: - dependencies: - flatted: 3.3.1 - keyv: 4.5.4 - rimraf: 3.0.2 - - flatted@3.3.1: {} - - fs.realpath@1.0.0: {} - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - - globals@13.24.0: - dependencies: - type-fest: 0.20.2 - - graphemer@1.4.0: {} - - has-flag@4.0.0: {} - - ignore@5.3.1: {} - - import-fresh@3.3.0: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - imurmurhash@0.1.4: {} - - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.4: {} - - is-extglob@2.1.1: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-path-inside@3.0.3: {} - - isexe@2.0.0: {} - - js-yaml@4.1.0: - dependencies: - argparse: 2.0.1 - - json-buffer@3.0.1: {} - - json-schema-traverse@0.4.1: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - lodash.merge@4.6.2: {} - - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.11 - - ms@2.1.2: {} - - natural-compare@1.4.0: {} - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - path-exists@4.0.0: {} - - path-is-absolute@1.0.1: {} - - path-key@3.1.1: {} - - prelude-ls@1.2.1: {} - - punycode@2.3.1: {} - - queue-microtask@1.2.3: {} - - resolve-from@4.0.0: {} - - reusify@1.0.4: {} - - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-json-comments@3.1.1: {} - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - text-table@0.2.0: {} - - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - type-fest@0.20.2: {} - - typescript@5.5.4: {} - - undici-types@5.26.5: {} - - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - word-wrap@1.2.5: {} - - wrappy@1.0.2: {} - - yocto-queue@0.1.0: {} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/bridge/saltychat.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/bridge/saltychat.ts deleted file mode 100644 index b93198f51..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-client/src/bridge/saltychat.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { saltyChatExport, sleep } from '@yaca-voice/common' -import { YacaPluginStates } from '@yaca-voice/types' -import { cache } from '../utils' -import type { YaCAClientModule } from '../yaca' - -/** - * The SaltyChat bridge for the client. - */ -export class YaCAClientSaltyChatBridge { - private clientModule: YaCAClientModule - - private currentPluginState = -1 - - private isPrimarySending = false - private isSecondarySending = false - - private isPrimaryReceiving = false - private isSecondaryReceiving = false - - /** - * Creates an instance of the SaltyChat bridge. - * - * @param {YaCAClientModule} clientModule - The client module. - */ - constructor(clientModule: YaCAClientModule) { - this.clientModule = clientModule - - this.registerSaltyChatExports() - this.enableRadio().then() - - console.log('[YaCA] SaltyChat bridge loaded') - - on('onResourceStop', (resourceName: string) => { - if (cache.resource !== resourceName) { - return - } - - emit('onClientResourceStop', 'saltychat') - }) - } - - /** - * Enables the radio on bridge load. - */ - async enableRadio() { - while (!this.clientModule.isPluginInitialized(true)) { - await sleep(1000) - } - - this.clientModule.radioModule.enableRadio(true) - } - - /** - * Register SaltyChat exports. - */ - registerSaltyChatExports() { - saltyChatExport('GetVoiceRange', () => this.clientModule.getVoiceRange()) - - saltyChatExport('GetRadioChannel', (primary: boolean) => { - const channel = primary ? 1 : 2 - - const currentFrequency = this.clientModule.radioModule.getRadioFrequency(channel) - - if (currentFrequency === '0') { - return '' - } - - return currentFrequency - }) - - saltyChatExport('GetRadioVolume', () => { - return this.clientModule.radioModule.getRadioChannelVolume(1) - }) - - saltyChatExport('GetRadioSpeaker', () => { - console.warn('GetRadioSpeaker is not implemented in YaCA') - return false - }) - - saltyChatExport('GetMicClick', () => { - console.warn('GetMicClick is not implemented in YaCA') - return false - }) - - saltyChatExport('SetRadioChannel', (radioChannelName: string, primary: boolean) => { - const channel = primary ? 1 : 2 - const newRadioChannelName = radioChannelName === '' ? '0' : radioChannelName - - this.clientModule.radioModule.changeRadioFrequencyRaw(newRadioChannelName, channel) - }) - - saltyChatExport('SetRadioVolume', (volume: number) => { - this.clientModule.radioModule.changeRadioChannelVolumeRaw(volume, 1) - this.clientModule.radioModule.changeRadioChannelVolumeRaw(volume, 2) - }) - - saltyChatExport('SetRadioSpeaker', () => { - console.warn('SetRadioSpeaker is not implemented in YaCA') - }) - - saltyChatExport('SetMicClick', () => { - console.warn('SetMicClick is not implemented in YaCA') - }) - - saltyChatExport('GetPluginState', () => { - return this.currentPluginState - }) - } - - /** - * Handles the plugin state change. - * - * @param response - The last response code. - */ - handleChangePluginState(response: YacaPluginStates) { - let state = 0 - - switch (response) { - case YacaPluginStates.IN_EXCLUDED_CHANNEL: - state = 3 - break - case YacaPluginStates.IN_INGAME_CHANNEL: - state = 2 - break - case YacaPluginStates.CONNECTED: - state = 1 - break - case YacaPluginStates.WRONG_TS_SERVER: - case YacaPluginStates.OUTDATED_VERSION: - state = 0 - break - case YacaPluginStates.NOT_CONNECTED: - state = -1 - break - default: - return - } - - emit('SaltyChat_PluginStateChanged', state) - this.currentPluginState = state - } - - /** - * Sends the radio talking state. - */ - sendRadioTalkingState() { - emit('SaltyChat_RadioTrafficStateChanged', this.isPrimaryReceiving, this.isPrimarySending, this.isSecondaryReceiving, this.isSecondarySending) - } - - /** - * Handle radio talking state change. - * - * @param state - The state of the radio talking. - * @param channel - The radio channel. - */ - handleRadioTalkingStateChange(state: boolean, channel: number) { - if (channel === 1) { - this.isPrimarySending = state - } else { - this.isSecondarySending = state - } - - this.sendRadioTalkingState() - } - - /** - * Handle radio receiving state change. - * - * @param state - The state of the radio receiving. - * @param channel - The radio channel. - */ - handleRadioReceivingStateChange(state: boolean, channel: number) { - if (channel === 1) { - this.isPrimaryReceiving = state - } else { - this.isSecondaryReceiving = state - } - - this.sendRadioTalkingState() - } -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/index.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/index.ts deleted file mode 100644 index 57efb413e..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-client/src/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/// - -import { initCache } from './utils' -import { YaCAClientModule } from './yaca' - -initCache() - -new YaCAClientModule() diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/cache.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/cache.ts deleted file mode 100644 index e71135166..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/cache.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { ClientCache } from '@yaca-voice/types' - -const playerId = PlayerId() - -/** - * Cached values for the client. - */ -const cache: ClientCache = new Proxy( - { - playerId, - serverId: GetPlayerServerId(playerId), - ped: PlayerPedId(), - vehicle: false, - seat: false, - resource: GetCurrentResourceName(), - game: GetGameName() as 'fivem' | 'redm', - }, - { - set(target: ClientCache, key: keyof ClientCache, value: never) { - if (target[key] === value) return true - - target[key] = value - emit(`yaca:cache:${key}`, value) - return true - }, - get(target: ClientCache, key: keyof ClientCache) { - return target[key] - }, - }, -) - -/** - * Initializes the cache and starts updating it. - */ -function initCache() { - /** - * This function will update the cache every 100ms. - */ - const updateCache = () => { - const ped = PlayerPedId() - cache.ped = ped - - const vehicle = GetVehiclePedIsIn(ped, false) - - if (vehicle > 0) { - if (vehicle !== cache.vehicle) { - cache.seat = false - } - - cache.vehicle = vehicle - - if (!cache.seat || GetPedInVehicleSeat(vehicle, cache.seat) !== ped) { - for (let i = -1; i < GetVehicleMaxNumberOfPassengers(vehicle) - 1; i++) { - if (GetPedInVehicleSeat(vehicle, i) === ped) { - cache.seat = i - break - } - } - } - } else { - cache.vehicle = false - cache.seat = false - } - } - - setInterval(updateCache, 100) -} - -/** - * Listen for cache updates. - * - * @param key - The cache key to listen for. - * @param cb - The callback to execute when the cache updates. - */ -export const onCache = (key: keyof ClientCache, cb: (value: T) => void) => { - on(`yaca:cache:${key}`, cb) -} - -export { initCache, cache } diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/index.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/index.ts deleted file mode 100644 index 55058e291..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { cache } from './cache' - -export * from './cache' -export * from './props' -export * from './redm' -export * from './streaming' -export * from './vectors' -export * from './vehicle' -export * from './websocket' - -/** - * Rounds a float to a specified number of decimal places. - * Defaults to 2 decimal places if not provided. - * - * @param {number} num - The number to round. - * @param {number} decimalPlaces - The number of decimal places to round to. - * - * @returns {number} The rounded number. - */ -export function roundFloat(num: number, decimalPlaces = 17): number { - return Number.parseFloat(num.toFixed(decimalPlaces)) -} - -/** - * Convert camera rotation to direction vector. - * - * @returns {x: number, y: number, z: number} The direction vector. - */ -export function getCamDirection(): { x: number; y: number; z: number } { - const rotVector = GetGameplayCamRot(0) - const num = rotVector[2] * 0.0174532924 - const num2 = rotVector[0] * 0.0174532924 - const num3 = Math.abs(Math.cos(num2)) - - return { - x: roundFloat(-Math.sin(num) * num3), - y: roundFloat(Math.cos(num) * num3), - z: roundFloat(GetEntityForwardVector(cache.ped)[2]), - } -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/props.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/props.ts deleted file mode 100644 index 760b5e610..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/props.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { cache } from './cache' -import { requestModel } from './streaming' - -export const joaat = (input: string, ignore_casing = true) => { - input = !ignore_casing ? input.toLowerCase() : input - const length = input.length - - let hash: number - let i: number - - for (hash = i = 0; i < length; i++) { - hash += input.charCodeAt(i) - hash += hash << 10 - hash ^= hash >>> 6 - } - - hash += hash << 3 - hash ^= hash >>> 11 - hash += hash << 15 - - return hash >>> 0 -} - -/** - * Create a prop and attach it to the player. - * - * @param model - The model of the prop. - * @param boneId - The bone id to attach the prop to. - * @param offset - The offset of the prop. - * @param rotation - The rotation of the prop. - */ -export const createProp = async ( - model: string | number, - boneId: number, - offset: [number, number, number] = [0.0, 0.0, 0.0], - rotation: [number, number, number] = [0.0, 0.0, 0.0], -) => { - const modelHash = await requestModel(model) - if (!modelHash) return - - const [x, y, z] = GetEntityCoords(cache.ped, true) - const [ox, oy, oz] = offset - const [rx, ry, rz] = rotation - const object = CreateObject(modelHash, x, y, z, true, true, false) - SetEntityCollision(object, false, false) - AttachEntityToEntity(object, cache.ped, GetPedBoneIndex(cache.ped, boneId), ox, oy, oz, rx, ry, rz, true, false, false, true, 2, true) - - return object -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/redm.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/redm.ts deleted file mode 100644 index fc5398cba..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/redm.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { REDM_KEY_TO_HASH } from '../yaca' -import { requestAnimDict } from './streaming' - -/** - * Play a facial animation on a ped. - * - * @param ped - The ped to play the facial animation on. - * @param animName - The animation name to use. - * @param animDict - The animation dictionary to use. - */ -export const playRdrFacialAnim = async (ped: number, animName: string, animDict: string) => { - const loadedAnimDict = await requestAnimDict(animDict) - if (!loadedAnimDict) return - - SetFacialIdleAnimOverride(ped, animName, loadedAnimDict) -} - -/** - * Display a notification in RDR. - * - * @param text - The text to display. - * @param duration - The duration to display the notification for. - */ -export const displayRdrNotification = (text: string, duration: number) => { - // @ts-expect-error VarString is a redm native - const str = VarString(10, 'LITERAL_STRING', text) - - const struct1 = new DataView(new ArrayBuffer(96)) - struct1.setUint32(0, duration, true) - - const struct2 = new DataView(new ArrayBuffer(8 + 8)) - struct2.setBigUint64(8, BigInt(str), true) - - Citizen.invokeNative('0x049D5C615BD38BAD', struct1, struct2, 1) -} - -/** - * Register a keybind for RDR. - * - * @param key - The key to bind. - * @param onPressed - The function to call when the key is pressed. - * @param onReleased - The function to call when the key is released. - */ -export const registerRdrKeyBind = (key: string, onPressed?: () => void, onReleased?: () => void) => { - const keyHash = REDM_KEY_TO_HASH[key] - - if (!keyHash) { - console.error(`[YaCA] No key hash available for ${key}, please choose another keybind`) - return - } - - setTick(() => { - DisableControlAction(0, keyHash, true) - if (onPressed && IsDisabledControlJustPressed(0, keyHash)) { - onPressed() - } - - if (onReleased && IsDisabledControlJustReleased(0, keyHash)) { - onReleased() - } - }) -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/streaming.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/streaming.ts deleted file mode 100644 index 0b8eff2c5..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/streaming.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { waitFor } from '@yaca-voice/common' -import { joaat } from './props' - -/** - * Request an asset and wait for it to load. - * - * @param request - The function to request the asset - * @param hasLoaded - The function to check if the asset has loaded - * @param assetType - The type of the asset - * @param asset - The asset to request - * @param timeout - The timeout in ms - */ -async function streamingRequest( - request: (asset: T) => unknown, - hasLoaded: (asset: T) => boolean, - assetType: string, - asset: T, - timeout = 30000, -) { - if (hasLoaded(asset)) return asset - - request(asset) - - return waitFor( - () => { - if (hasLoaded(asset)) return asset - }, - `failed to load ${assetType} '${asset}' - this may be caused by\n- too many loaded assets\n- oversized, invalid, or corrupted assets`, - timeout, - ) -} - -/** - * Request a animation dictionary. - * - * @param animDict - The animation dictionary to request. - * @returns A promise that resolves to the animation dictionary once it is loaded. - * @throws Will throw an error if the animation dictionary is not valid or if the animation dictionary fails to load within the timeout. - */ -export const requestAnimDict = (animDict: string) => { - if (!DoesAnimDictExist(animDict)) throw new Error(`attempted to load invalid animDict '${animDict}'`) - - return streamingRequest(RequestAnimDict, HasAnimDictLoaded, 'animDict', animDict) -} - -/** - * Loads a model by its name or hash key. - * - * @param modelName - The name or hash key of the model to load. - * @returns A promise that resolves to the model hash key once the model is loaded. - * @throws Will throw an error if the model is not valid or if the model fails to load within the timeout. - */ -export const requestModel = (modelName: string | number) => { - if (typeof modelName !== 'number') modelName = joaat(modelName) - if (!IsModelValid(modelName)) throw new Error(`attempted to load invalid model '${modelName}'`) - - return streamingRequest(RequestModel, HasModelLoaded, 'model', modelName) -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/vectors.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/vectors.ts deleted file mode 100644 index 9f6ab4ff0..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/vectors.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { roundFloat } from './index' - -/** - * Calculate the distance between two points in 3D space - * - * @param firstPoint - The first point - * @param secondPoint - The second point - */ -export function calculateDistanceVec3(firstPoint: number[], secondPoint: number[]) { - return Math.sqrt((firstPoint[0] - secondPoint[0]) ** 2 + (firstPoint[1] - secondPoint[1]) ** 2 + (firstPoint[2] - secondPoint[2]) ** 2) -} - -/** - * Calculate the distance between two points in 2D space - * - * @param firstPoint - The first point - * @param secondPoint - The second point - */ -export function calculateDistanceVec2(firstPoint: number[], secondPoint: number[]) { - return Math.sqrt((firstPoint[0] - secondPoint[0]) ** 2 + (firstPoint[1] - secondPoint[1]) ** 2) -} - -/** - * Convert an array of numbers to an object with x, y, and z properties - * - * @param array - The array to convert - */ -export function convertNumberArrayToXYZ(array: number[]): { - x: number - y: number - z: number -} { - return { - x: roundFloat(array[0]), - y: roundFloat(array[1]), - z: roundFloat(array[2]), - } -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/vehicle.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/vehicle.ts deleted file mode 100644 index 98533d211..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/vehicle.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Checks if the vehicle has a window. - * - * @param vehicle - The vehicle. - * @param windowId - The window ID to check. - * @returns {boolean} - Whether the vehicle has a window. - */ -export function hasWindow(vehicle: number, windowId: number): boolean { - switch (windowId) { - case 0: - return GetEntityBoneIndexByName(vehicle, 'window_lf') !== -1 - case 1: - return GetEntityBoneIndexByName(vehicle, 'window_rf') !== -1 - case 2: - return GetEntityBoneIndexByName(vehicle, 'window_lr') !== -1 - case 3: - return GetEntityBoneIndexByName(vehicle, 'window_rr') !== -1 - default: - return false - } -} - -/** - * Checks if the vehicle has a door. - * - * @param vehicle - The vehicle. - * @param doorId - The door ID to check. - * @returns {boolean} - Whether the vehicle has a door. - */ -export function hasDoor(vehicle: number, doorId: number): boolean { - switch (doorId) { - case 0: - return GetEntityBoneIndexByName(vehicle, 'door_dside_f') !== -1 - case 1: - return GetEntityBoneIndexByName(vehicle, 'door_pside_f') !== -1 - case 2: - return GetEntityBoneIndexByName(vehicle, 'door_dside_r') !== -1 - case 3: - return GetEntityBoneIndexByName(vehicle, 'door_pside_r') !== -1 - case 4: - return GetEntityBoneIndexByName(vehicle, 'bonnet') !== -1 - case 5: - return GetEntityBoneIndexByName(vehicle, 'boot') !== -1 - default: - return false - } -} - -/** - * Checks if the vehicle has an opening. - * - * @param vehicle - The vehicle. - * @returns {boolean} - Whether the vehicle has an opening. - */ -export function vehicleHasOpening(vehicle: number): boolean { - const doors = [] - for (let i = 0; i < 6; i++) { - if (i === 4 || !hasDoor(vehicle, i)) continue - doors.push(i) - } - - if (doors.length === 0) return true - for (const door of doors) { - const doorAngle = GetVehicleDoorAngleRatio(vehicle, door) - if (doorAngle > 0) { - return true - } - - if (IsVehicleDoorDamaged(vehicle, door)) { - return true - } - } - - if (!AreAllVehicleWindowsIntact(vehicle)) { - return true - } - - for (let i = 0; i < 8 /* max windows */; i++) { - const hasWindows = hasWindow(vehicle, i) - if (hasWindows && !IsVehicleWindowIntact(vehicle, i)) { - return true - } - } - - if (IsVehicleAConvertible(vehicle, false)) { - const roofState = GetConvertibleRoofState(vehicle) - if (roofState !== 0) { - return true - } - } - - return false -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/websocket.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/websocket.ts deleted file mode 100644 index 87ac1c48d..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-client/src/utils/websocket.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { sleep } from '@yaca-voice/common' -import EventEmitter2 from 'eventemitter2' - -/** - * The WebSocket class handles the communication between the nui and the client. - */ -export class WebSocket extends EventEmitter2 { - public readyState = 0 - nuiReady = false - initialized = false - - /** - * Creates an instance of the WebSocket class. - */ - constructor() { - super() - - RegisterNuiCallbackType('YACA_OnMessage') - RegisterNuiCallbackType('YACA_OnConnected') - RegisterNuiCallbackType('YACA_OnDisconnected') - - on('__cfx_nui:YACA_OnMessage', (data: object, cb: (data: unknown) => void) => { - this.emit('message', data) - cb({}) - }) - - on('__cfx_nui:YACA_OnConnected', (_: unknown, cb: (data: unknown) => void) => { - this.readyState = 1 - this.emit('open') - cb({}) - }) - - on('__cfx_nui:YACA_OnDisconnected', (data: { code: number; reason: string }, cb: (data: unknown) => void) => { - this.readyState = 3 - this.emit('close', data.code, data.reason) - cb({}) - }) - } - - /** - * Sends the message to the nui that the websocket should connect. - */ - async start() { - while (!this.nuiReady) { - await sleep(100) - } - - SendNuiMessage( - JSON.stringify({ - action: 'connect', - }), - ) - } - - /** - * Sends the message to the nui that the websocket should disconnect. - * - * @param data - The data to send. - */ - send(data: object) { - if (this.readyState !== 1) { - return - } - - SendNuiMessage( - JSON.stringify({ - action: 'command', - data, - }), - ) - } - - /** - * Sends the message to the nui that the websocket should disconnect. - */ - close() { - if (this.readyState === 3) { - return - } - - SendNuiMessage( - JSON.stringify({ - action: 'close', - }), - ) - } -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/data.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/data.ts deleted file mode 100644 index 99e46b357..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/data.ts +++ /dev/null @@ -1,97 +0,0 @@ -const localLipSyncAnimations: Record<'fivem' | 'redm', Record> = { - fivem: { - true: { - name: 'mic_chatter', - dict: 'mp_facial', - }, - false: { - name: 'mood_normal_1', - dict: 'facials@gen_male@variations@normal', - }, - }, - redm: { - true: { - name: 'mood_talking_normal', - dict: 'face_human@gen_male@base', - }, - false: { - name: 'mood_normal', - dict: 'face_human@gen_male@base', - }, - }, -} - -const REDM_KEY_TO_HASH: Record = { - // Letters - A: 0x7065027d, - B: 0x4cc0e2fe, - C: 0x9959a6f0, - D: 0xb4e465b4, - E: 0xcefd9220, - F: 0xb2f377e8, - G: 0x760a9c6f, - H: 0x24978a28, - I: 0xc1989f95, - J: 0xf3830d8e, - K: null, - L: 0x80f28e95, - M: 0xe31c6a41, - N: 0x4bc9dabb, // (Push to Talk) - O: 0xf1301666, - P: 0xd82e0bd2, - Q: 0xde794e3e, - R: 0xe30cd707, - S: 0xd27782e3, - T: null, - U: 0xd8f73058, - V: 0x7f8d09b8, - W: 0x8fd015d8, - X: 0x8cc9cd42, - Y: null, - Z: 0x26e9dc00, - - // Symbol Keys - RIGHTBRACKET: 0xa5bdcd3c, - LEFTBRACKET: 0x430593aa, - - // Mouse buttons - MOUSE1: 0x07ce1e61, - MOUSE2: 0xf84fa74f, - MOUSE3: 0xcee12b50, - MWUP: 0x3076e97c, - - // Modifier Keys - CTRL: 0xdb096b85, - TAB: 0xb238fe0b, - SHIFT: 0x8ffc75d6, - SPACEBAR: 0xd9d0e1c0, - ENTER: 0xc7b5340a, - BACKSPACE: 0x156f7119, - LALT: 0x8aaa0ad4, - DEL: 0x4af4d473, - PGUP: 0x446258b6, - PGDN: 0x3c3dd371, - - // Function Keys - F1: 0xa8e3f467, - F4: 0x1f6d95e5, - F6: 0x3c0a40f2, - - // Number Keys - '1': 0xe6f612e4, - '2': 0x1ce6d9eb, - '3': 0x4f49cc4c, - '4': 0x8f9f9e58, - '5': 0xab62e997, - '6': 0xa1fde2a6, - '7': 0xb03a913b, - '8': 0x42385422, - - // Arrow Keys - DOWN: 0x05ca7c52, - UP: 0x6319db71, - LEFT: 0xa65ebab4, - RIGHT: 0xdeb34313, -} - -export { localLipSyncAnimations, REDM_KEY_TO_HASH } diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/index.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/index.ts deleted file mode 100644 index 0e576c9aa..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './data' -export * from './intercom' -export * from './main' -export * from './megaphone' -export * from './phone' -export * from './radio' diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/intercom.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/intercom.ts deleted file mode 100644 index 516ea7e3a..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/intercom.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { CommDeviceMode, YacaFilterEnum, type YacaPlayerData } from '@yaca-voice/types' -import type { YaCAClientModule } from './main' - -/** - * The intercom module for the client. - */ -export class YaCAClientIntercomModule { - clientModule: YaCAClientModule - - /** - * Creates an instance of the intercom module. - * - * @param clientModule - The client module. - */ - constructor(clientModule: YaCAClientModule) { - this.clientModule = clientModule - - this.registerEvents() - } - - /** - * Register the intercom events. - */ - registerEvents() { - /** - * Handles the "client:yaca:addRemovePlayerIntercomFilter" server event. - * - * @param {number[] | number} playerIDs - The IDs of the players to be added or removed from the intercom filter. - * @param {boolean} state - The state indicating whether to add or remove the players. - */ - onNet('client:yaca:addRemovePlayerIntercomFilter', (playerIDs: number | number[], state: boolean) => { - if (!Array.isArray(playerIDs)) { - playerIDs = [playerIDs] - } - - const playersToAddRemove: Set = new Set() - for (const playerID of playerIDs) { - const player = this.clientModule.getPlayerByID(playerID) - if (!player) { - continue - } - playersToAddRemove.add(player) - } - - if (playersToAddRemove.size < 1) { - return - } - this.clientModule.setPlayersCommType( - Array.from(playersToAddRemove), - YacaFilterEnum.INTERCOM, - state, - undefined, - undefined, - CommDeviceMode.TRANSCEIVER, - CommDeviceMode.TRANSCEIVER, - ) - }) - } -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/main.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/main.ts deleted file mode 100644 index fbc0a93a7..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/main.ts +++ /dev/null @@ -1,1733 +0,0 @@ -import { - clamp, - GLOBAL_ERROR_LEVEL_STATE_NAME, - initLocale, - LIP_SYNC_STATE_NAME, - loadConfig, - locale, - MEGAPHONE_STATE_NAME, - VOICE_RANGE_STATE_NAME, -} from '@yaca-voice/common' -import { - CommDeviceMode, - type DataObject, - defaultSharedConfig, - defaultTowerConfig, - type YacaClient, - YacaFilterEnum, - YacaNotificationType, - type YacaPlayerData, - type YacaPluginPlayerData, - YacaPluginStates, - type YacaProtocol, - type YacaResponse, - type YacaSharedConfig, - type YacaSoundStateMessage, - type YacaStereoMode, - type YacaTowerConfig, -} from '@yaca-voice/types' -import { YaCAClientSaltyChatBridge } from '../bridge/saltychat' -import { - cache, - calculateDistanceVec3, - convertNumberArrayToXYZ, - displayRdrNotification, - getCamDirection, - joaat, - playRdrFacialAnim, - registerRdrKeyBind, - vehicleHasOpening, - WebSocket, -} from '../utils' -import { localLipSyncAnimations } from './data' -import { YaCAClientIntercomModule } from './intercom' -import { YaCAClientMegaphoneModule } from './megaphone' -import { YaCAClientPhoneModule } from './phone' -import { YaCAClientRadioModule } from './radio' - -/** - * The YaCA client module. - * This module is responsible for handling the client side of the voice plugin. - * It also handles the websocket connection to the voice plugin. - */ -export class YaCAClientModule { - websocket: WebSocket - - sharedConfig: YacaSharedConfig - towerConfig: YacaTowerConfig - - mufflingVehicleWhitelistHash = new Set() - allPlayers = new Map() - firstConnect = true - - radioModule: YaCAClientRadioModule - phoneModule: YaCAClientPhoneModule - megaphoneModule: YaCAClientMegaphoneModule - intercomModule: YaCAClientIntercomModule - - saltyChatBridge?: YaCAClientSaltyChatBridge - - canChangeVoiceRange = true - defaultVoiceRange = 1 - maxVoiceRange = -1 - rangeIndex: number - rangeInterval: CitizenTimer | null = null - visualVoiceRangeTimeout: CitizenTimer | null = null - visualVoiceRangeTick: CitizenTimer | null = null - voiceRangeViaMouseWheelTick: CitizenTimer | null = null - - isTalking = false - useWhisper = false - spectatingPlayer: number | false = false - notificationTimeout: Map = new Map() - - isMicrophoneMuted = false - isMicrophoneDisabled = false - isSoundMuted = false - isSoundDisabled = false - - currentlyPhoneSpeakerApplied = new Set() - currentlySendingPhoneSpeakerSender = new Set() - phoneHearNearbyPlayer = new Set() - - isFiveM = cache.game === 'fivem' - isRedM = cache.game === 'redm' - - private currentPluginState: YacaPluginStates - - /** - * Sets the current plugin state and emits an event. - * - * @param state - The new plugin state. - */ - setCurrentPluginState(state: YacaPluginStates) { - if (this.currentPluginState === state) { - return - } - - this.currentPluginState = state - emit('yaca:external:pluginStateChanged', state) - - this.saltyChatBridge?.handleChangePluginState(state) - } - - /** - * Sends a radar notification. - * - * @param {string} message - The message to be sent in the notification. - * @param {YacaNotificationType} type - The type of the notification, e.g. error, inform, success. - */ - notification(message: string, type: YacaNotificationType) { - if (this.sharedConfig.notifications.oxLib) { - emit('ox_lib:notify', { - id: 'yaca', - title: 'YaCA', - description: message, - type, - }) - } - - if (this.sharedConfig.notifications.okoknotify && GetResourceState('okokNotify') === 'started') { - const okType = type === YacaNotificationType.INFO ? 'info' : type - exports.okokNotify.Alert('YaCA', message, 2000, okType) - } - - if (this.sharedConfig.notifications.gta) { - if (this.isFiveM) { - BeginTextCommandThefeedPost('STRING') - AddTextComponentSubstringPlayerName(`YaCA: ${message}`) - if (type === YacaNotificationType.ERROR) { - ThefeedSetNextPostBackgroundColor(6) - } - EndTextCommandThefeedPostTicker(false, false) - } else { - console.warn('[YaCA] GTA notification is only available in FiveM.') - } - } - - if (this.sharedConfig.notifications.redm) { - if (this.isRedM) { - displayRdrNotification(`YaCA: ${message}`, 2000) - } else { - console.warn('[YaCA] RedM notification is only available in RedM.') - } - } - - if (this.sharedConfig.notifications.own) { - emit('yaca:external:notification', message, type) - } - } - - constructor() { - this.sharedConfig = loadConfig('config/shared.json5', defaultSharedConfig) - this.towerConfig = loadConfig('config/tower.json5', defaultTowerConfig) - initLocale(this.sharedConfig.locale) - - this.rangeIndex = this.sharedConfig.voiceRange.defaultIndex - if (this.sharedConfig.voiceRange.ranges[this.rangeIndex]) { - this.defaultVoiceRange = this.sharedConfig.voiceRange.ranges[this.rangeIndex] - } else { - this.defaultVoiceRange = 1 - this.rangeIndex = 0 - this.sharedConfig.voiceRange.ranges = [1] - - console.error('[YaCA] Default voice range is not set correctly in the config.') - } - - if (this.isFiveM) { - for (const vehicleModel of this.sharedConfig.mufflingSettings.vehicleMuffling.vehicleWhitelist) { - this.mufflingVehicleWhitelistHash.add(joaat(vehicleModel)) - } - } - - this.websocket = new WebSocket() - this.setCurrentPluginState(YacaPluginStates.NOT_CONNECTED) - - /** - * Register the NUI callback types. - */ - RegisterNuiCallbackType('YACA_OnNuiReady') - on('__cfx_nui:YACA_OnNuiReady', (_: unknown, cb: (data: unknown) => void) => { - this.websocket.nuiReady = true - - if (this.sharedConfig.autoConnectOnJoin) { - setTimeout(() => { - emitNet('server:yaca:nuiReady') - }, 5000) - } - - // skipcq: JS-0255 - cb({}) - }) - - this.registerExports() - this.registerEvents() - if (this.isFiveM) { - this.registerKeybindings() - } else if (this.isRedM) { - this.registerRdrKeybindings() - } - - this.intercomModule = new YaCAClientIntercomModule(this) - this.megaphoneModule = new YaCAClientMegaphoneModule(this) - this.phoneModule = new YaCAClientPhoneModule(this) - this.radioModule = new YaCAClientRadioModule(this) - - if (!this.sharedConfig.useLocalLipSync) { - /** - * Add a state bag change handler for the lip sync state bag. - * Which is used to override the talking state of the player. - */ - AddStateBagChangeHandler(LIP_SYNC_STATE_NAME, '', (bagName: string, _: string, value: boolean, __: number) => { - const playerId = GetPlayerFromStateBagName(bagName) - if (playerId === 0) { - return - } - - SetPlayerTalkingOverride(playerId, value) - }) - - /** - * Add a state bag change handler for the global error level state bag. - * Which is used to override the global error level. - */ - AddStateBagChangeHandler(GLOBAL_ERROR_LEVEL_STATE_NAME, '', (_bagName: string, _key: string, _value: number, __: number) => { - setImmediate(() => { - this.phoneModule.enablePhoneCall(Array.from(this.phoneModule.inCallWith), true) - }) - }) - } - - if (this.sharedConfig.saltyChatBridge) { - this.radioModule.secondaryRadioChannel = 2 - this.saltyChatBridge = new YaCAClientSaltyChatBridge(this) - } - - console.log('[Client] YaCA Client loaded.') - } - - registerExports() { - /** - * Get the current voice range. - * - * @returns {number} The current voice range. - */ - exports('getVoiceRange', () => this.getVoiceRange()) - - /** - * Get all voice ranges. - * - * @returns {number[]} All available voice ranges. - */ - exports('getVoiceRanges', () => this.sharedConfig.voiceRange.ranges) - - /** - * Change the voice range to the next range. - * - * @param {boolean} increase - If the voice range should be increased or decreased. - */ - exports('changeVoiceRange', (increase = true) => { - this.changeVoiceRange(increase) - }) - - /** - * Set the voice range to the given value. - * - * @param {number} range - The voice range to set - */ - exports('setVoiceRange', (range: number) => { - this.setVoiceRange(range) - }) - - /** - * Enable or disable the voice range change function. - * - * @param {boolean} enable - If the voice range change function should be enabled or disabled. - */ - exports('setVoiceRangeChangeAllowedState', (enable: boolean) => { - this.canChangeVoiceRange = enable - }) - - /** - * Get the voice range change allowed state. - * - * @returns {boolean} The voice range change allowed state. - */ - exports('getVoiceRangeChangeAllowedState', () => this.canChangeVoiceRange) - - /** - * Set the maximum voice range. - * - * @param {number} maxVoiceRange - The maximum voice range, -1 to disable. - */ - exports('setMaxVoiceRange', (maxVoiceRange: number) => { - this.maxVoiceRange = maxVoiceRange - }) - - /** - * Get the maximum allowed voice range. - * - * @returns {number} The maximum voice range. - */ - exports('getMaxVoiceRange', () => this.maxVoiceRange) - - /** - * Get microphone mute state. - * - * @returns {boolean} The microphone mute state. - */ - exports('getMicrophoneMuteState', () => this.isMicrophoneMuted) - - /** - * Get microphone disabled state. - * - * @returns {boolean} The microphone disabled state. - */ - exports('getMicrophoneDisabledState', () => this.isMicrophoneDisabled) - - /** - * Get sound mute state. - * - * @returns {boolean} - */ - exports('getSoundMuteState', () => this.isSoundMuted) - - /** - * Get sound disabled state. - * - * @returns {boolean} - */ - exports('getSoundDisabledState', () => this.isSoundDisabled) - - /** - * Get the plugin state. - * - * @returns {YacaPluginStates} The current plugin state. - */ - exports('getPluginState', () => this.currentPluginState ?? YacaPluginStates.NOT_CONNECTED) - - /** - * Get the global error level. - * - * @returns {number} The global error level. - */ - exports('getGlobalErrorLevel', () => GlobalState[GLOBAL_ERROR_LEVEL_STATE_NAME] ?? 0) - - /** - * Set the player that should be spectated. - * - * @param {number | false} player - The player to be spectated. - */ - exports('setSpectatingPlayer', (player: number | false) => { - this.spectatingPlayer = player - }) - - /** - * Get the player that is currently spectated. - * - * @returns {number | false} The player that is currently spectated. False if no player is spectated. - */ - exports('getSpectatingPlayer', () => this.spectatingPlayer) - - /** - * Set the voice range marker color. - * - * @param {number} r - The red component of the color. - * @param {number} g - The green component of the color. - * @param {number} b - The blue component of the color. - * @param {number} a - The alpha component of the color. - */ - exports('setVoiceRangeMarkerColor', (r: number, g: number, b: number, a: number) => { - if (typeof r !== 'number' || typeof g !== 'number' || typeof b !== 'number' || typeof a !== 'number') { - console.error('[YaCA] Invalid color value in setVoiceRangeMarkerColor') - return - } - - this.sharedConfig.voiceRange.markerColor.r = r - this.sharedConfig.voiceRange.markerColor.g = g - this.sharedConfig.voiceRange.markerColor.b = b - this.sharedConfig.voiceRange.markerColor.a = a - }) - - /** - * Get the voice range marker color. - * - * @returns {number[]} The voice range marker color as an array of [r, g, b, a]. - */ - exports('getVoiceRangeMarkerColor', () => { - const { r, g, b, a } = this.sharedConfig.voiceRange.markerColor - return [r, g, b, a] - }) - - /** - * Reset the voice range marker color to the default value. - */ - exports('resetVoiceRangeMarkerColor', () => { - const defaultColor = defaultSharedConfig.voiceRange.markerColor - this.sharedConfig.voiceRange.markerColor.r = defaultColor.r - this.sharedConfig.voiceRange.markerColor.g = defaultColor.g - this.sharedConfig.voiceRange.markerColor.b = defaultColor.b - this.sharedConfig.voiceRange.markerColor.a = defaultColor.a - }) - } - - /** - * Registers the keybindings for the plugin. - * This is only available in FiveM. - */ - registerKeybindings() { - if (this.sharedConfig.keyBinds.increaseVoiceRange !== false) { - /** - * Registers the "yaca:increaseVoiceRange" command and keybinding. - * This command is used to change the voice range. - */ - RegisterCommand( - 'yaca:increaseVoiceRange', - () => { - this.changeVoiceRange(true) - }, - false, - ) - RegisterKeyMapping('yaca:increaseVoiceRange', locale('change_voice_range_increase'), 'keyboard', this.sharedConfig.keyBinds.increaseVoiceRange) - } - - if (this.sharedConfig.keyBinds.decreaseVoiceRange !== false) { - /** - * Registers the "yaca:decreaseVoiceRange" command and keybinding. - * This command is used to change the voice range. - */ - RegisterCommand( - 'yaca:decreaseVoiceRange', - () => { - this.changeVoiceRange(false) - }, - false, - ) - RegisterKeyMapping('yaca:decreaseVoiceRange', locale('change_voice_range_decrease'), 'keyboard', this.sharedConfig.keyBinds.decreaseVoiceRange) - } - - if (this.sharedConfig.keyBinds.voiceRangeWithMouseWheel !== false) { - /** - * Registers the "+yaca:changeVoiceRangeWithMousewheel" command and keybinding. - * This command is used to change the voice range. - */ - RegisterCommand( - '+yaca:changeVoiceRangeWithMousewheel', - () => { - this.voiceRangeViaMouseWheelTick = setInterval(() => { - this.handleVoiceRangeViaMouseWheel() - }) - }, - false, - ) - - RegisterCommand( - '-yaca:changeVoiceRangeWithMousewheel', - () => { - if (this.voiceRangeViaMouseWheelTick) { - clearInterval(this.voiceRangeViaMouseWheelTick) - this.voiceRangeViaMouseWheelTick = null - } - }, - false, - ) - - RegisterKeyMapping( - '+yaca:changeVoiceRangeWithMousewheel', - locale('change_voice_range_via_mousewheel'), - 'keyboard', - this.sharedConfig.keyBinds.voiceRangeWithMouseWheel, - ) - } - } - - /** - * Registers the keybindings for RedM. - * This is only available in RedM. - */ - registerRdrKeybindings() { - if (this.sharedConfig.keyBinds.increaseVoiceRange !== false) { - /** - * Registers the keybinding for changing the voice Range. - */ - registerRdrKeyBind(this.sharedConfig.keyBinds.increaseVoiceRange, () => { - this.changeVoiceRange() - }) - } - - if (this.sharedConfig.keyBinds.decreaseVoiceRange !== false) { - /** - * Registers the keybinding for changing the voice Range. - */ - registerRdrKeyBind(this.sharedConfig.keyBinds.decreaseVoiceRange, () => { - this.changeVoiceRange(false) - }) - } - - if (this.sharedConfig.keyBinds.voiceRangeWithMouseWheel !== false) { - /** - * Registers the "+yaca:changeVoiceRangeWithScroll" command and keybinding. - * This command is used to change the voice range. - */ - - registerRdrKeyBind( - this.sharedConfig.keyBinds.voiceRangeWithMouseWheel, - () => { - this.voiceRangeViaMouseWheelTick = setInterval(() => { - this.handleVoiceRangeViaMouseWheel() - }) - }, - () => { - if (this.voiceRangeViaMouseWheelTick) { - clearInterval(this.voiceRangeViaMouseWheelTick) - this.voiceRangeViaMouseWheelTick = null - } - }, - ) - } - } - - /** - * Registers the events for the plugin. - */ - registerEvents() { - /** - * Handles the "onPlayerJoining" server event. - * - * @param {number} target - The ID of the target. - */ - onNet('onPlayerJoining', (target: number) => { - const player = this.getPlayerByID(target) - if (!player) { - return - } - - const frequency = this.radioModule?.playersWithShortRange.get(target) - if (frequency) { - const channel = this.radioModule?.findRadioChannelByFrequency(frequency) - if (channel) { - this.setPlayersCommType( - player, - YacaFilterEnum.RADIO, - true, - channel, - undefined, - CommDeviceMode.RECEIVER, - CommDeviceMode.SENDER, - GlobalState[GLOBAL_ERROR_LEVEL_STATE_NAME] ?? undefined, - ) - this.saltyChatBridge?.handleRadioReceivingStateChange(true, channel) - } - } - }) - - /** - * Handles the "onPlayerDropped" server event. - * - * @param {number} target - The ID of the target. - */ - onNet('onPlayerDropped', (target: number) => { - const player = this.getPlayerByID(target) - if (!player) { - return - } - - this.phoneModule.removePhoneSpeakerFromEntity(target) - - const frequency = this.radioModule?.playersWithShortRange.get(target) - if (frequency) { - const channel = this.radioModule?.findRadioChannelByFrequency(frequency) - if (channel) { - this.setPlayersCommType( - player, - YacaFilterEnum.RADIO, - false, - channel, - undefined, - CommDeviceMode.RECEIVER, - CommDeviceMode.SENDER, - GlobalState[GLOBAL_ERROR_LEVEL_STATE_NAME] ?? undefined, - ) - - if (this.saltyChatBridge) { - const inRadio = this.radioModule?.playersInRadioChannel.get(channel) - if (inRadio) { - const inRadioArray = [...inRadio].filter((id) => id !== target) - const state = inRadioArray.length > 0 - this.saltyChatBridge.handleRadioReceivingStateChange(state, channel) - } - } - } - } - }) - - /** - * Handles the "onResourceStop" event. - * - * @param {string} resourceName - The name of the resource that has started. - */ - on('onResourceStop', (resourceName: string) => { - if (cache.resource !== resourceName) { - return - } - - if (this.websocket.initialized) { - this.websocket.close() - } - }) - - /** - * Handles the "client:yaca:init" server event. - * - * @param {DataObject} dataObj - The data object to be initialized. - */ - onNet('client:yaca:init', async (dataObj: DataObject) => { - if (this.rangeInterval) { - clearInterval(this.rangeInterval) - this.rangeInterval = null - } - - if (!this.websocket.initialized) { - this.websocket.initialized = true - - this.websocket.on('message', (msg: string) => { - this.handleResponse(msg) - }) - - this.websocket.on('close', (code: number, reason: string) => { - this.setCurrentPluginState(YacaPluginStates.NOT_CONNECTED) - - console.error('[YACA-Websocket]: client disconnected', code, reason) - }) - - this.websocket.on('open', () => { - this.setCurrentPluginState(YacaPluginStates.CONNECTED) - - if (this.firstConnect) { - this.initRequest(dataObj) - this.firstConnect = false - } else { - emitNet('server:yaca:wsReady') - } - - console.log('[YACA-Websocket]: Successfully connected to the voice plugin') - }) - - await this.websocket.start() - } - - if (this.firstConnect) { - return - } - - this.initRequest(dataObj) - }) - - /** - * Handles the "client:yaca:disconnect" server event. - * - * @param {number} remoteId - The remote ID of the player to be disconnected. - * - */ - onNet('client:yaca:disconnect', (remoteId: number) => { - this.phoneModule.handleDisconnect(remoteId) - this.allPlayers.delete(remoteId) - }) - - /** - * Handles the "client:yaca:addPlayers" server event. - * - * @param {DataObject | DataObject[]} dataObjects - The data object or objects to be added. - */ - onNet('client:yaca:addPlayers', (dataObjects: DataObject | DataObject[]) => { - if (!Array.isArray(dataObjects)) { - dataObjects = [dataObjects] - } - - const newPlayers: number[] = [] - for (const dataObj of dataObjects) { - if (!dataObj || typeof dataObj.clientId === 'undefined' || typeof dataObj.playerId === 'undefined') { - continue - } - - const currentData = this.getPlayerByID(dataObj.playerId) - - this.allPlayers.set(dataObj.playerId, { - remoteID: dataObj.playerId, - clientId: dataObj.clientId, - forceMuted: dataObj.forceMuted || false, - phoneCallMemberIds: currentData?.phoneCallMemberIds || undefined, - mutedOnPhone: dataObj.mutedOnPhone || false, - }) - - newPlayers.push(dataObj.playerId) - } - - this.phoneModule.reestablishCalls(newPlayers) - }) - - /** - * Handles the "client:yaca:muteTarget" server event. - * - * @param {number} target - The target to be muted. - * @param {boolean} muted - The mute status. - */ - onNet('client:yaca:muteTarget', (target: number, muted: boolean) => { - const player = this.getPlayerByID(target) - if (!player) return - - player.forceMuted = muted - }) - - /** - * Handles the "client:yaca:changeOwnVoiceRange" server event. - * - * @param {number} range - The new voice range. - */ - onNet('client:yaca:changeVoiceRange', (range: number) => { - emit('yaca:external:voiceRangeUpdate', range, this.rangeIndex) - // SaltyChat bridge - if (this.saltyChatBridge) { - emit('SaltyChat_VoiceRangeChanged', range.toFixed(1), this.rangeIndex, this.sharedConfig.voiceRange.ranges.length) - } - }) - - /** - * Handles the "client:yaca:notification" server event. - * - * @param {string} message - The message to be sent in the notification. - * @param {YacaNotificationType} type - The type of the notification, e.g. error, inform, success. - */ - onNet('client:yaca:notification', (message: string, type: YacaNotificationType) => { - this.notification(message, type) - }) - - /** - * Handles the "txcl:spectate:start" server event. - * - * @param {number} targetServerId - The ID of the target server that is spectated. - */ - onNet('txcl:spectate:start', (targetServerId: number) => { - this.spectatingPlayer = targetServerId - }) - - /** - * Handles the "txcl:spectate:stop" server event. - */ - onNet('client:yaca:txadmin:stopspectate', () => { - this.spectatingPlayer = false - }) - } - - /** - * Get the player by remote ID. - * - * @param remoteId The remote ID of the player. - */ - getPlayerByID(remoteId: number) { - return this.allPlayers.get(remoteId) - } - - /** - * Get the player by client ID. - * - * @param clientId The client ID (TeamSpeak) of the player. - * @returns The player data. - */ - getPlayerByClientId(clientId: number) { - for (const player of this.allPlayers.values()) { - if (player.clientId === clientId) { - return player - } - } - - return null - } - - /** - * Initializes the plugin. - * - * @param {DataObject} dataObj - The data object to initialize the plugin with. - */ - initRequest(dataObj: DataObject) { - if ( - !dataObj || - !dataObj.suid || - typeof dataObj.chid !== 'number' || - !dataObj.deChid || - !dataObj.ingameName || - typeof dataObj.channelPassword === 'undefined' - ) { - console.log('[YACA-Websocket]: Error while initializing plugin') - this.notification(locale('connect_error'), YacaNotificationType.ERROR) - return - } - - this.sendWebsocket({ - base: { request_type: 'INIT' }, - server_guid: dataObj.suid, - ingame_name: dataObj.ingameName, - ingame_channel: dataObj.chid, - default_channel: dataObj.deChid, - ingame_channel_password: dataObj.channelPassword, - excluded_channels: dataObj.excludeChannels, - muffling_range: this.sharedConfig.mufflingSettings.mufflingRange, - build_type: this.sharedConfig.buildType, - unmute_delay: this.sharedConfig.unmuteDelay, - operation_mode: dataObj.useWhisper ? 1 : 0, - }) - - this.useWhisper = dataObj.useWhisper ?? false - } - - /** - * Checks if the plugin is initialized. - * - * @returns {boolean} Returns true if the plugin is initialized, false otherwise. - */ - isPluginInitialized(silent = false): boolean { - const initialized = Boolean(this.getPlayerByID(cache.serverId)) - - if (!initialized && !silent) { - this.notification(locale('plugin_not_initialized'), YacaNotificationType.ERROR) - } - - return initialized - } - - /** - * Sends a message to the voice plugin via websocket. - * - * @param {object} msg - The message to be sent. - */ - sendWebsocket(msg: object) { - if (!this.websocket) { - console.error('[Voice-Websocket]: No websocket created') - return - } - - this.websocket.send(msg) - } - - /** - * Handles messages from the voice plugin. - * - * @param {string} payload - The response from the voice plugin. - */ - handleResponse(payload: string) { - if (!payload) { - return - } - - let parsedPayload: YacaResponse - - try { - parsedPayload = JSON.parse(payload) - } catch (e) { - console.error('[YaCA-Websocket]: Error while parsing message: ', e) - return - } - - switch (parsedPayload.code) { - case 'OK': - if (parsedPayload.requestType === 'JOIN') { - const clientId = Number.parseInt(parsedPayload.message) - emitNet('server:yaca:addPlayer', clientId) - - if (this.rangeInterval) { - clearInterval(this.rangeInterval) - this.rangeInterval = null - } - - this.rangeInterval = setInterval(this.calcPlayers.bind(this), 250) - - // Set radio settings on reconnect only, else on first opening - if (this.radioModule.radioInitialized) { - this.radioModule.initRadioSettings() - } - - emit('yaca:external:pluginInitialized', clientId) - return - } - - return - case 'TALK_STATE': - this.handleTalkState(parsedPayload) - return - case 'SOUND_STATE': - this.handleSoundState(parsedPayload) - return - case 'OTHER_TALK_STATE': - this.handleOtherTalkState(parsedPayload) - return - case 'MOVED_CHANNEL': - this.handleMovedChannel(parsedPayload.message) - return - case 'WRONG_TS_SERVER': { - this.setCurrentPluginState(YacaPluginStates.WRONG_TS_SERVER) - - const currentTimeout = this.notificationTimeout.get('WRONG_TS_SERVER') - if (currentTimeout && currentTimeout > Date.now()) { - return - } - - this.notificationTimeout.set('WRONG_TS_SERVER', Date.now() + 10000) - this.notification(locale('wrong_ts_server') ?? 'You are connected to the wrong teamspeak server!', YacaNotificationType.ERROR) - return - } - case 'OUTDATED_VERSION': - this.setCurrentPluginState(YacaPluginStates.OUTDATED_VERSION) - this.notification( - locale('outdated_plugin', parsedPayload.message) ?? `Your plugin is outdated, please update to version ${parsedPayload.message}!`, - YacaNotificationType.ERROR, - ) - return - case 'MAX_PLAYER_COUNT_REACHED': - this.notification( - locale('max_players_reached') ?? 'Your license reached the maximum player count. Please upgrade your license.', - YacaNotificationType.ERROR, - ) - return - case 'LICENSE_SERVER_TIMED_OUT': - this.notification( - locale('license_server_timed_out') ?? 'The connection to the license server timed out, while verifying the license. Please wait a moment.', - YacaNotificationType.ERROR, - ) - return - case 'MOVE_ERROR': - this.notification(locale('move_error') ?? 'You are not connected to the teamspeak server!', YacaNotificationType.ERROR) - return - case 'WAIT_GAME_INIT': - case 'HEARTBEAT': - case 'MUTE_STATE': - return - default: - console.log(`[YaCA-Websocket]: Unknown error code: ${parsedPayload.code}`) - this.notification(locale('unknown_error', parsedPayload.code) ?? `Unknown error code: ${parsedPayload.code}`, YacaNotificationType.ERROR) - return - } - } - - /** - * Sets a variable for a player. - * - * @param {string} player - The player for whom the variable is to be set. - * @param {string} variable - The name of the variable. - * @param {*} value - The value to be set for the variable. - */ - setPlayerVariable(player: number, variable: string, value: unknown) { - const currentData = this.getPlayerByID(player) - if (!currentData) return - - // @ts-expect-error Object cannot be undefined - currentData[variable] = value - } - - /** - * Get the current voice range. - * - * @returns {number} The current voice range. - */ - getVoiceRange(): number { - return LocalPlayer.state[VOICE_RANGE_STATE_NAME] ?? this.defaultVoiceRange - } - - /** - * Changes the voice range to the next range. - * - * @param {boolean} increase - If the voice range should be increased or decreased. - */ - changeVoiceRange(increase = true) { - if (!this.canChangeVoiceRange) return - - const currentVoiceRange = this.getVoiceRange() - if (increase) { - const newIndex = this.sharedConfig.voiceRange.ranges.findIndex( - (range) => ((this.maxVoiceRange !== -1 && range <= this.maxVoiceRange) || this.maxVoiceRange === -1) && range > currentVoiceRange, - ) - this.rangeIndex = newIndex !== -1 ? newIndex : 0 - } else { - const newIndex = this.sharedConfig.voiceRange.ranges - .slice() - .reverse() - .findIndex((range) => range < currentVoiceRange) - this.rangeIndex = newIndex !== -1 ? this.sharedConfig.voiceRange.ranges.length - 1 - newIndex : this.sharedConfig.voiceRange.ranges.length - 1 - - // If maxrange is defined and the range is higher than maxrange, set the range to maxrange - if (this.maxVoiceRange !== -1 && this.sharedConfig.voiceRange.ranges[this.rangeIndex] > this.maxVoiceRange) { - const newIndex = this.sharedConfig.voiceRange.ranges - .slice() - .reverse() - .findIndex((range) => range <= this.maxVoiceRange) - this.rangeIndex = newIndex !== -1 ? this.sharedConfig.voiceRange.ranges.length - 1 - newIndex : this.sharedConfig.voiceRange.ranges.length - 1 - } - } - - const voiceRange = this.sharedConfig.voiceRange.ranges[this.rangeIndex] ?? 1 - this.changeVoiceRangeInternal(voiceRange) - } - - /** - * Set the voice range to the given value. - * - * @param voiceRange - The voice range to set - */ - setVoiceRange(voiceRange: number) { - this.rangeIndex = -1 - this.changeVoiceRangeInternal(voiceRange) - } - - /** - * Internal function to change the voice range. - * - * @param voiceRange - The voice range to set - * @private - */ - private changeVoiceRangeInternal(voiceRange: number) { - if (!this.canChangeVoiceRange) return - if (this.maxVoiceRange !== -1 && voiceRange > this.maxVoiceRange) return - - this.showRangeVisual(voiceRange) - - LocalPlayer.state.set(VOICE_RANGE_STATE_NAME, voiceRange, true) - - emit('yaca:external:voiceRangeUpdate', voiceRange, this.rangeIndex) - // SaltyChat bridge - if (this.saltyChatBridge) { - emit('SaltyChat_VoiceRangeChanged', voiceRange.toFixed(1), this.rangeIndex, this.sharedConfig.voiceRange.ranges.length) - } - } - - /** - * Shows the voice range visuals. - * - * @param newVoiceRange - The new voice range - */ - showRangeVisual(newVoiceRange: number) { - if (this.visualVoiceRangeTimeout) { - clearTimeout(this.visualVoiceRangeTimeout) - this.visualVoiceRangeTimeout = null - } - - if (this.visualVoiceRangeTick) { - clearInterval(this.visualVoiceRangeTick) - this.visualVoiceRangeTick = null - } - - if (this.sharedConfig.voiceRange.sendNotification) { - this.notification(locale('voice_range_changed', newVoiceRange), YacaNotificationType.INFO) - } - - if (this.sharedConfig.voiceRange.markerColor.enabled) { - const red = this.sharedConfig.voiceRange.markerColor.r - const green = this.sharedConfig.voiceRange.markerColor.g - const blue = this.sharedConfig.voiceRange.markerColor.b - const alpha = this.sharedConfig.voiceRange.markerColor.a - const duration = this.sharedConfig.voiceRange.markerColor.duration - - this.visualVoiceRangeTimeout = setTimeout(() => { - if (this.visualVoiceRangeTick) { - clearInterval(this.visualVoiceRangeTick) - this.visualVoiceRangeTick = null - } - - this.visualVoiceRangeTimeout = null - }, duration) - - if (!this.isFiveM && this.sharedConfig.voiceRange.markerColor.type < 1000) { - this.sharedConfig.voiceRange.markerColor.type = 0x94fdae17 - console.warn('[YaCA] Marker type is not supported in RedM. Using default marker type.') - } - - this.visualVoiceRangeTick = setInterval(() => { - const entity = cache.vehicle || cache.ped - const pos = GetEntityCoords(entity, false) - const posZ = cache.vehicle ? pos[2] - 0.6 : pos[2] - 0.98 - - DrawMarker( - this.sharedConfig.voiceRange.markerColor.type, - pos[0], - pos[1], - posZ, - 0, - 0, - 0, - 0, - 0, - 0, - newVoiceRange * 2, - newVoiceRange * 2, - 1, - red, - green, - blue, - alpha, - false, - true, - 2, - this.sharedConfig.voiceRange.markerColor.rotate, - // @ts-expect-error Type error in the native - null, - null, - false, - ) - }) - } - } - - /** - * Checks if the communication type is valid. - * - * @param {string} type - The type of communication to be validated. - * @returns {boolean} Returns true if the type is valid, false otherwise. - */ - static isCommTypeValid(type: string): boolean { - const valid = type in YacaFilterEnum - if (!valid) { - console.error(`[YaCA-Websocket]: Invalid comm type: ${type}`) - } - - return valid - } - - /** - * Set the communication type for the given players. - * - * @param {YacaPlayerData | YacaPlayerData[]} players - The player or players for whom the communication type is to be set. - * @param {YacaFilterEnum} type - The type of communication. - * @param {boolean} state - The state of the communication. - * @param {number} channel - The channel for the communication. Optional. - * @param {number} range - The range for the communication. Optional. - * @param {CommDeviceMode} ownMode - The mode for the player. Optional. - * @param {CommDeviceMode} otherPlayersMode - The mode for the other players. Optional. - * @param {number} errorLevel - The error level for the communication. Optional. - */ - setPlayersCommType( - players: { clientId: number } | { clientId: number }[], - type: YacaFilterEnum, - state: boolean, - channel?: number | null, - range?: number | null, - ownMode?: CommDeviceMode, - otherPlayersMode?: CommDeviceMode, - errorLevel?: number | null, - ) { - if (!Array.isArray(players)) { - players = [players] - } - - const clientIds: YacaClient[] = [] - if (typeof ownMode !== 'undefined') { - clientIds.push({ - client_id: this.getPlayerByID(cache.serverId)?.clientId, - mode: ownMode, - }) - } - - for (const player of players) { - if (!player) { - continue - } - - const clientProtocol: YacaClient = { - client_id: player.clientId, - mode: otherPlayersMode, - } - - if (typeof errorLevel !== 'undefined' && errorLevel !== null) { - clientProtocol.errorLevel = errorLevel - } - - clientIds.push(clientProtocol) - } - - const protocol: YacaProtocol = { - on: state, - comm_type: type, - members: clientIds, - } - - if (typeof channel !== 'undefined' && channel !== null) { - protocol.channel = channel - } - if (typeof range !== 'undefined' && range !== null) { - protocol.range = range - } - - this.sendWebsocket({ - base: { request_type: 'INGAME' }, - comm_device: protocol, - }) - } - - /** - * Update the volume for a specific communication type. - * - * @param {string} type - The type of communication. - * @param {number} volume - The volume to be set. - * @param {number} channel - The channel for the communication. - */ - setCommDeviceVolume(type: YacaFilterEnum, volume: number, channel?: number) { - if (!YaCAClientModule.isCommTypeValid(type)) { - return - } - - const protocol: YacaProtocol = { - comm_type: type, - volume: clamp(volume, 0, 1), - } - - if (typeof channel !== 'undefined') { - protocol.channel = channel - } - - this.sendWebsocket({ - base: { request_type: 'INGAME' }, - comm_device_settings: protocol, - }) - } - - /** - * Update the stereo mode for a specific communication type. - * - * @param {YacaFilterEnum} type - The type of communication. - * @param {YacaStereoMode} mode - The stereo mode to be set. - * @param {number} channel - The channel for the communication. - */ - setCommDeviceStereoMode(type: YacaFilterEnum, mode: YacaStereoMode, channel?: number) { - if (!YaCAClientModule.isCommTypeValid(type)) { - return - } - - const protocol: YacaProtocol = { - comm_type: type, - output_mode: mode, - } - - if (typeof channel !== 'undefined') { - protocol.channel = channel - } - - this.sendWebsocket({ - base: { request_type: 'INGAME' }, - comm_device_settings: protocol, - }) - } - - /** - * Set the player speaking state and start the lip animation. - * - * @param ped - The ped to sync the lips with. - * @param playerId - The player ID to sync the lips with. - * @param isTalking - The talking state of the player. - */ - syncLipsPlayer(ped: number, playerId: number, isTalking: boolean) { - const animationData = localLipSyncAnimations[cache.game][isTalking ? 'true' : 'false'] - - SetPlayerTalkingOverride(playerId, isTalking) - if (this.isFiveM) { - PlayFacialAnim(ped, animationData.name, animationData.dict) - } else if (this.isRedM) { - playRdrFacialAnim(ped, animationData.name, animationData.dict) - } - } - - /** - * Handles the talk and mute state from teamspeak, displays it in UI and syncs lip to other players. - * - * @param {YacaResponse} payload - The response from teamspeak. - */ - handleTalkState(payload: YacaResponse) { - const messageState = payload.message === '1' - const isPlayerMuted = this.isMicrophoneMuted || this.isMicrophoneDisabled || this.isSoundMuted || this.isSoundDisabled - - const isTalking = !isPlayerMuted && messageState - if (this.isTalking !== isTalking) { - this.isTalking = isTalking - - this.syncLipsPlayer(cache.ped, cache.serverId, isTalking) - LocalPlayer.state.set(LIP_SYNC_STATE_NAME, isTalking, true) - - emit('yaca:external:isTalking', isTalking) - - // SaltyChat bridge - if (this.saltyChatBridge) { - emit('SaltyChat_TalkStateChanged', isTalking) - } - } - } - - /** - * Handles the sound state from teamspeak. - * - * @param payload - The response from teamspeak. - */ - handleSoundState(payload: YacaResponse) { - const soundStates: YacaSoundStateMessage = JSON.parse(payload.message) - - if (this.isMicrophoneMuted !== soundStates.microphoneMuted) { - this.isMicrophoneMuted = soundStates.microphoneMuted - emit('yaca:external:microphoneMuteStateChanged', soundStates.microphoneMuted) - emit('yaca:external:muteStateChanged', soundStates.microphoneMuted) // Deprecated in favor of microphoneMuteStateChanged - - // SaltyChat bridge - if (this.saltyChatBridge) { - emit('SaltyChat_MicStateChanged', soundStates.microphoneMuted) - } - } - - if (this.isMicrophoneDisabled !== soundStates.microphoneDisabled) { - this.isMicrophoneDisabled = soundStates.microphoneDisabled - emit('yaca:external:microphoneDisabledStateChanged', soundStates.microphoneDisabled) - - // SaltyChat bridge - if (this.saltyChatBridge) { - emit('SaltyChat_MicEnabledChanged', soundStates.microphoneDisabled) - } - } - - if (this.isSoundMuted !== soundStates.soundMuted) { - this.isSoundMuted = soundStates.soundMuted - emit('yaca:external:soundMuteStateChanged', soundStates.soundMuted) - - // SaltyChat bridge - if (this.saltyChatBridge) { - emit('SaltyChat_SoundStateChanged', soundStates.soundMuted) - } - } - - if (this.isSoundDisabled !== soundStates.soundDisabled) { - this.isSoundDisabled = soundStates.soundDisabled - emit('yaca:external:soundDisabledStateChanged', soundStates.soundDisabled) - - // SaltyChat bridge - if (this.saltyChatBridge) { - emit('SaltyChat_SoundEnabledChanged', soundStates.soundDisabled) - } - } - } - - /** - * Handles the talk state of other players. - * - * @param payload - The response from teamspeak. - */ - handleOtherTalkState(payload: YacaResponse) { - if (!this.sharedConfig.useLocalLipSync) { - return - } - - let talkData: { clientId: number; isTalking: boolean } - - try { - talkData = JSON.parse(payload.message) - } catch { - console.error('[YaCA-Websocket]: Error while parsing other talk state message') - return - } - - const player = this.getPlayerByClientId(talkData.clientId) - - if (!player || !player.remoteID) { - return - } - - const playerId = GetPlayerFromServerId(player.remoteID) - - if (playerId === -1) { - return - } - - SetPlayerTalkingOverride(playerId, talkData.isTalking) - } - - /** - * Handles the moved channel event. - * - * @param newChannel - The new channel the player is in. - */ - handleMovedChannel(newChannel: string) { - if (newChannel !== 'INGAME_CHANNEL' && newChannel !== 'EXCLUDED_CHANNEL') { - console.error('[YaCA-Websocket]: Unknown channel type: ', newChannel) - return - } - - if (newChannel === 'INGAME_CHANNEL') { - this.setCurrentPluginState(YacaPluginStates.IN_INGAME_CHANNEL) - } else { - this.setCurrentPluginState(YacaPluginStates.IN_EXCLUDED_CHANNEL) - } - - emit('yaca:external:channelChanged', newChannel) - } - - /** - * Checks if the vehicle has an opening. - * - * @param vehicle - The vehicle to check. - */ - checkIfVehicleHasOpening(vehicle: number | false) { - if (!vehicle) { - return true - } - - if (this.mufflingVehicleWhitelistHash.has(GetEntityModel(vehicle))) { - return true - } - - return vehicleHasOpening(vehicle) - } - - /** - * Get the muffle intensity for the nearby player. - * - * @param {number} playerPed - The player ped. - * @param {number} nearbyPlayerPed - The nearby player ped. - * @param {number} playerVehicle - The vehicle the player is in. - * @param {number} ownCurrentRoom - The current room the client is in. - * @param {boolean} ownVehicleHasOpening - The opening state ot the vehicle the client is in. - * @param {boolean} nearbyUsesMegaphone - The state if the nearby player uses a megaphone. - */ - getMuffleIntensity( - playerPed: number, - nearbyPlayerPed: number, - playerVehicle: number | false, - ownCurrentRoom: number, - ownVehicleHasOpening: boolean, - nearbyUsesMegaphone = false, - ) { - if (ownCurrentRoom !== GetRoomKeyFromEntity(nearbyPlayerPed) && !HasEntityClearLosToEntity(playerPed, nearbyPlayerPed, 17)) { - return this.sharedConfig.mufflingSettings.intensities.differentRoom - } - - const vehicleMuffling = this.sharedConfig.mufflingSettings.vehicleMuffling.enabled - if (this.isRedM || !vehicleMuffling) { - return 0 - } - - const nearbyPlayerVehicle = GetVehiclePedIsIn(nearbyPlayerPed, false) - const ownVehicleId = playerVehicle || 0 - - if (ownVehicleId === nearbyPlayerVehicle) { - return 0 - } - - if (nearbyUsesMegaphone) { - if (ownVehicleHasOpening) { - return 0 - } - - return this.sharedConfig.mufflingSettings.intensities.megaPhoneInCar - } - - const nearbyPlayerVehicleHasOpening = this.checkIfVehicleHasOpening(nearbyPlayerVehicle) - - if (!ownVehicleHasOpening && !nearbyPlayerVehicleHasOpening) { - return this.sharedConfig.mufflingSettings.intensities.bothCarsClosed - } - - if (!ownVehicleHasOpening || !nearbyPlayerVehicleHasOpening) { - return this.sharedConfig.mufflingSettings.intensities.oneCarClosed - } - - return 0 - } - - /** - * Handles the phone speaker emit. - * - * @param playersToPhoneSpeaker - The players to send the phone speaker to. - * @param playersOnPhoneSpeaker - The players who are on phone speaker. - */ - handlePhoneSpeakerEmit(playersToPhoneSpeaker: Set, playersOnPhoneSpeaker: Set): void { - if (this.useWhisper) { - if ( - (this.phoneModule.phoneSpeakerActive && this.phoneModule.inCallWith.size) || - ((!this.phoneModule.phoneSpeakerActive || !this.phoneModule.inCallWith.size) && this.currentlySendingPhoneSpeakerSender.size) - ) { - const playersToNotReceivePhoneSpeaker = [...this.currentlySendingPhoneSpeakerSender].filter((playerId) => !playersToPhoneSpeaker.has(playerId)) - const playersNeedsReceivePhoneSpeaker = [...playersToPhoneSpeaker].filter((playerId) => !this.currentlySendingPhoneSpeakerSender.has(playerId)) - - this.currentlySendingPhoneSpeakerSender = new Set(playersToPhoneSpeaker) - - if (playersNeedsReceivePhoneSpeaker.length || playersToNotReceivePhoneSpeaker.length) { - emitNet('server:yaca:phoneSpeakerEmitWhisper', playersNeedsReceivePhoneSpeaker, playersToNotReceivePhoneSpeaker) - } - } - } - - for (const playerId of this.currentlyPhoneSpeakerApplied) { - if (playersOnPhoneSpeaker.has(playerId)) { - continue - } - - this.currentlyPhoneSpeakerApplied.delete(playerId) - const player = this.getPlayerByID(playerId) - - if (!player) { - continue - } - - this.setPlayersCommType( - player, - YacaFilterEnum.PHONE_SPEAKER, - false, - undefined, - this.sharedConfig.maxPhoneSpeakerRange, - CommDeviceMode.RECEIVER, - CommDeviceMode.SENDER, - ) - } - } - - /** - * Handles around phone emit. - * - * @param playerToHearOnPhone - The players to hear on the phone. - */ - handlePhoneEmit(playerToHearOnPhone: Set) { - if (!this.sharedConfig.phoneHearPlayersNearby) return - - if (this.sharedConfig.phoneHearPlayersNearby === 'PHONE_SPEAKER') { - if ( - !( - (this.phoneModule.phoneSpeakerActive && this.phoneModule.inCallWith.size) || - ((!this.phoneModule.phoneSpeakerActive || !this.phoneModule.inCallWith.size) && this.phoneHearNearbyPlayer.size) - ) - ) { - return - } - } else { - if (!(this.phoneModule.inCallWith.size || (!this.phoneModule.inCallWith.size && this.phoneHearNearbyPlayer.size))) { - return - } - } - - const playersToNotHear = [...this.phoneHearNearbyPlayer].filter((playerId) => !playerToHearOnPhone.has(playerId)) - const playersToHear = [...playerToHearOnPhone].filter((playerId) => !this.phoneHearNearbyPlayer.has(playerId)) - - this.phoneHearNearbyPlayer = new Set(playerToHearOnPhone) - - if (playersToHear.length || playersToNotHear.length) { - emitNet('server:yaca:phoneEmit', playersToHear, playersToNotHear) - } - } - - /** - * Handles the voice range adjustment using the mouse wheel. - */ - handleVoiceRangeViaMouseWheel() { - if (this.isFiveM) { - HudWeaponWheelIgnoreSelection() - } - - let newValue = 0 - const currentVoiceRange = this.getVoiceRange() - - if (IsControlPressed(0, 242)) { - newValue = Math.max(1, currentVoiceRange - 1) - } else if (IsControlPressed(0, 241)) { - newValue = Math.min(this.sharedConfig.voiceRange.ranges[this.sharedConfig.voiceRange.ranges.length - 1], currentVoiceRange + 1) - - if (this.maxVoiceRange !== -1 && newValue > this.maxVoiceRange) { - newValue = this.maxVoiceRange - } - } - - if (newValue <= 0 || currentVoiceRange === newValue) return - - this.setVoiceRange(newValue) - } - - /** - * Calculate the players in streaming range and send them to the voice plugin. - */ - // skipcq: JS-R1005 - calcPlayers() { - const localData = this.getPlayerByID(cache.serverId) - if (!localData) return - - const players = new Map() - const playersToPhoneSpeaker = new Set() - const playersOnPhoneSpeaker = new Set() - const playerToHearOnPhone = new Set() - - let localPlayerPed = cache.ped - let localPlayerVehicle = cache.vehicle - - if (this.spectatingPlayer) { - const remotePlayerId = GetPlayerFromServerId(this.spectatingPlayer) - - if (remotePlayerId !== -1) { - const remotePlayerPed = GetPlayerPed(remotePlayerId) - if (remotePlayerPed !== 0) { - localPlayerPed = remotePlayerPed - const remotePlayerVehicle = GetVehiclePedIsIn(remotePlayerPed, false) - if (remotePlayerVehicle !== 0) { - localPlayerVehicle = remotePlayerVehicle - } else { - localPlayerVehicle = false - } - } - } - } - - const localPos = GetEntityCoords(localPlayerPed, false) - const currentRoom = GetRoomKeyFromEntity(localPlayerPed) - const hasVehicleOpening = this.isFiveM ? this.checkIfVehicleHasOpening(localPlayerVehicle) : true - const phoneSpeakerActive = this.phoneModule.phoneSpeakerActive && this.phoneModule.inCallWith.size - - for (const player of GetActivePlayers()) { - // Get the remote ID of the player and check if it is the local player or the server. - const remoteId = GetPlayerServerId(player) - const playerPed = GetPlayerPed(player) - // Check if the player is the local player and if the player is still in streaming range and the ped could be found. - if (remoteId === 0 || remoteId === cache.serverId || playerPed <= 0) continue - - // Get the player data and check if the player is initialized and has a client ID set. - const voiceSetting = this.getPlayerByID(remoteId) - if (!voiceSetting || !voiceSetting.clientId) continue - - // Get the player state and the voice range of the player. - const playerState = Player(remoteId).state - const range = playerState[VOICE_RANGE_STATE_NAME] ?? this.defaultVoiceRange - - // Get the muffle intensity for the player. - const muffleIntensity = this.getMuffleIntensity( - localPlayerPed, - playerPed, - localPlayerVehicle, - currentRoom, - hasVehicleOpening, - playerState[MEGAPHONE_STATE_NAME] !== null, - ) - - // Get the player position, the distance to the player, the player direction and if the player is underwater. - const playerPos = GetEntityCoords(playerPed, false) - const distanceToPlayer = calculateDistanceVec3(localPos, playerPos) - const playerDirection = GetEntityForwardVector(playerPed) - // @ts-expect-error Type error in the native - const isUnderwater = IsPedSwimmingUnderWater(playerPed) === 1 - - if (!playersOnPhoneSpeaker.has(remoteId)) { - players.set(remoteId, { - client_id: voiceSetting.clientId, - position: convertNumberArrayToXYZ(playerPos), - direction: convertNumberArrayToXYZ(playerDirection), - range, - is_underwater: isUnderwater, - muffle_intensity: muffleIntensity, - is_muted: voiceSetting.forceMuted ?? false, - }) - } - - // Who can be heard on the phone. - if (this.sharedConfig.phoneHearPlayersNearby && !localData.mutedOnPhone && !voiceSetting.forceMuted && distanceToPlayer <= range) { - if (this.sharedConfig.phoneHearPlayersNearby === 'PHONE_SPEAKER' && phoneSpeakerActive) { - playerToHearOnPhone.add(remoteId) - } else if (this.sharedConfig.phoneHearPlayersNearby === true && this.phoneModule.inCallWith.size) { - playerToHearOnPhone.add(remoteId) - } - } - - // Check if the player is in phone speaker range. - if (distanceToPlayer > this.sharedConfig.maxPhoneSpeakerRange) continue - - // Phone speaker handling - user who enabled it. - if (this.useWhisper && phoneSpeakerActive) playersToPhoneSpeaker.add(remoteId) - - // If no phone speaker is active, skip the rest. - if (!voiceSetting.phoneCallMemberIds) continue - - // Add all players which are in the call to the players list and give them the phone speaker effect. - for (const phoneCallMemberId of voiceSetting.phoneCallMemberIds) { - const phoneCallMember = this.getPlayerByID(phoneCallMemberId) - if (!phoneCallMember || !phoneCallMember.clientId || phoneCallMember.mutedOnPhone || phoneCallMember.forceMuted) continue - - players.delete(phoneCallMemberId) - players.set(phoneCallMemberId, { - client_id: phoneCallMember.clientId, - position: convertNumberArrayToXYZ(playerPos), - direction: convertNumberArrayToXYZ(playerDirection), - range: this.sharedConfig.maxPhoneSpeakerRange, - is_underwater: isUnderwater, - muffle_intensity: muffleIntensity, - is_muted: false, - }) - - playersOnPhoneSpeaker.add(phoneCallMemberId) - - if (this.currentlyPhoneSpeakerApplied.has(phoneCallMemberId)) continue - - this.setPlayersCommType( - phoneCallMember, - YacaFilterEnum.PHONE_SPEAKER, - true, - undefined, - this.sharedConfig.maxPhoneSpeakerRange, - CommDeviceMode.RECEIVER, - CommDeviceMode.SENDER, - ) - - this.currentlyPhoneSpeakerApplied.add(phoneCallMemberId) - } - } - - this.handlePhoneSpeakerEmit(playersToPhoneSpeaker, playersOnPhoneSpeaker) - this.handlePhoneEmit(playerToHearOnPhone) - - // Send the collected data to the voice plugin. - this.sendWebsocket({ - base: { request_type: 'INGAME' }, - player: { - player_direction: getCamDirection(), - player_position: convertNumberArrayToXYZ(localPos), - player_range: LocalPlayer.state[VOICE_RANGE_STATE_NAME] ?? this.defaultVoiceRange, - // @ts-expect-error Type error in the native - player_is_underwater: IsPedSwimmingUnderWater(localPlayerPed) === 1, - player_is_muted: localData.forceMuted ?? false, - players_list: Array.from(players.values()), - }, - }) - } -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/megaphone.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/megaphone.ts deleted file mode 100644 index d4c53bdad..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/megaphone.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { locale, MEGAPHONE_STATE_NAME } from '@yaca-voice/common' -import { CommDeviceMode, YacaFilterEnum } from '@yaca-voice/types' -import { cache, joaat, onCache, registerRdrKeyBind } from '../utils' -import type { YaCAClientModule } from './main' - -/** - * The megaphone module for the client. - */ -export class YaCAClientMegaphoneModule { - clientModule: YaCAClientModule - - canUseMegaphone = false - lastMegaphoneState = false - - megaphoneVehicleWhitelistHashes = new Set() - - /** - * Creates an instance of the megaphone module. - * - * @param clientModule - The client module. - */ - constructor(clientModule: YaCAClientModule) { - this.clientModule = clientModule - - this.registerEvents() - if (this.clientModule.isFiveM) { - this.registerKeybinds() - - for (const vehicleModel of this.clientModule.sharedConfig.megaphone.allowedVehicleModels) { - this.megaphoneVehicleWhitelistHashes.add(joaat(vehicleModel)) - } - } else if (this.clientModule.isRedM) { - this.registerRdrKeybinds() - } - this.registerExports() - this.registerStateBagHandlers() - } - - registerEvents() { - /** - * Handles the "client:yaca:setLastMegaphoneState" server event. - * - * @param {boolean} state - The state of the megaphone. - */ - onNet('client:yaca:setLastMegaphoneState', (state: boolean) => { - this.lastMegaphoneState = state - }) - - if (this.clientModule.isFiveM && this.clientModule.sharedConfig.megaphone.automaticVehicleDetection) { - /** - * Checks if the player can use the megaphone when they enter a vehicle. - * If they can, it sets the `canUseMegaphone` property to `true`. - * If they can't, it sets the `canUseMegaphone` property to `false`. - * If the player is not in a vehicle, it sets the `canUseMegaphone` property to `false` and emits the "server:yaca:playerLeftVehicle" event. - */ - onCache('seat', (seat) => { - if (seat === false || seat > 0 || !cache.vehicle) { - this.canUseMegaphone = false - emitNet('server:yaca:playerLeftVehicle') - return - } - - const vehicleClass = GetVehicleClass(cache.vehicle) - const vehicleModel = GetEntityModel(cache.vehicle) - - this.canUseMegaphone = - this.clientModule.sharedConfig.megaphone.allowedVehicleClasses.includes(vehicleClass) || - this.megaphoneVehicleWhitelistHashes.has(vehicleModel) - }) - } - } - - /** - * Registers the command and key mapping for the megaphone. - * This is only available in FiveM. - */ - registerKeybinds() { - if (this.clientModule.sharedConfig.keyBinds.megaphone === false) { - return - } - - /** - * Registers the command and key mapping for the megaphone. - */ - RegisterCommand( - '+yaca:megaphone', - () => { - this.useMegaphone(true) - }, - false, - ) - RegisterCommand( - '-yaca:megaphone', - () => { - this.useMegaphone(false) - }, - false, - ) - RegisterKeyMapping('+yaca:megaphone', locale('use_megaphone'), 'keyboard', this.clientModule.sharedConfig.keyBinds.megaphone) - } - - /** - * Registers the keybindings for the megaphone. - * This is only available in RedM. - */ - registerRdrKeybinds() { - if (this.clientModule.sharedConfig.keyBinds.megaphone === false) { - return - } - - /** - * Registers the command and key mapping for the megaphone. - */ - registerRdrKeyBind(this.clientModule.sharedConfig.keyBinds.megaphone, () => { - this.useMegaphone(!this.lastMegaphoneState) - }) - } - - registerExports() { - /** - * Gets the `canUseMegaphone` property. - * - * @returns {boolean} - The `canUseMegaphone` property. - */ - exports('getCanUseMegaphone', () => { - return this.canUseMegaphone - }) - - /** - * Sets the `canUseMegaphone` property. - * - * @param {boolean} state - The state to set the `canUseMegaphone` property to. - */ - exports('setCanUseMegaphone', (state: boolean) => { - this.canUseMegaphone = state - - if (!state && this.lastMegaphoneState) { - emitNet('server:yaca:playerLeftVehicle') - } - }) - - /** - * Toggles the use of the megaphone. - * - * @param {boolean} [state=false] - The state of the megaphone. Defaults to false if not provided. - */ - exports('useMegaphone', (state = false) => { - this.useMegaphone(state) - }) - } - - registerStateBagHandlers() { - /** - * Handles the megaphone state bag change. - */ - AddStateBagChangeHandler(MEGAPHONE_STATE_NAME, '', (bagName: string, _: string, value: number | null, __: number, replicated: boolean) => { - if (replicated) { - return - } - - const playerId = GetPlayerFromStateBagName(bagName) - if (playerId === 0) { - return - } - - const playerSource = GetPlayerServerId(playerId) - if (playerSource === 0) { - return - } - - if (playerSource === cache.serverId) { - this.clientModule.setPlayersCommType( - [], - YacaFilterEnum.MEGAPHONE, - typeof value === 'number', - undefined, - value, - CommDeviceMode.SENDER, - CommDeviceMode.RECEIVER, - ) - } else { - const player = this.clientModule.getPlayerByID(playerSource) - if (!player) { - return - } - - this.clientModule.setPlayersCommType( - player, - YacaFilterEnum.MEGAPHONE, - typeof value === 'number', - undefined, - value, - CommDeviceMode.RECEIVER, - CommDeviceMode.SENDER, - ) - } - }) - } - - /** - * Toggles the use of the megaphone. - * - * @param {boolean} [state=false] - The state of the megaphone. Defaults to false if not provided. - */ - useMegaphone(state = false) { - if ( - (!cache.vehicle && this.clientModule.sharedConfig.megaphone.automaticVehicleDetection) || - !this.canUseMegaphone || - state === this.lastMegaphoneState - ) { - return - } - - this.lastMegaphoneState = !this.lastMegaphoneState - emitNet('server:yaca:useMegaphone', state) - emit('yaca:external:megaphoneState', state) - } -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/phone.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/phone.ts deleted file mode 100644 index 8e7b27e4b..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/phone.ts +++ /dev/null @@ -1,288 +0,0 @@ -import { GLOBAL_ERROR_LEVEL_STATE_NAME, PHONE_SPEAKER_STATE_NAME } from '@yaca-voice/common' -import { CommDeviceMode, YacaFilterEnum, type YacaPlayerData } from '@yaca-voice/types' -import { cache } from '../utils' -import type { YaCAClientModule } from './main' - -/** - * The phone module for the client. - */ -export class YaCAClientPhoneModule { - clientModule: YaCAClientModule - - inCallWith = new Set() - phoneSpeakerActive = false - - /** - * Creates an instance of the phone module. - * - * @param clientModule - The client module. - */ - constructor(clientModule: YaCAClientModule) { - this.clientModule = clientModule - - this.registerEvents() - this.registerExports() - this.registerStateBagHandlers() - } - - registerEvents() { - /** - * Handles the "client:yaca:phone" server event. - * - * @param {number | number[]} targetIDs - The ID of the target. - * @param {boolean} state - The state of the phone. - */ - onNet('client:yaca:phone', (targetIDs: number | number[], state: boolean, filter: YacaFilterEnum = YacaFilterEnum.PHONE) => { - if (!Array.isArray(targetIDs)) { - targetIDs = [targetIDs] - } - - this.enablePhoneCall(targetIDs, state, filter) - }) - - /** - * Handles the "client:yaca:phoneHearAround" server event. - * - * @param {number[]} targetClientIds - The IDs of the targets. - * @param {boolean} state - The state of the phone hear around. - */ - onNet('client:yaca:phoneHearAround', (targetClientIds: number[], state: boolean) => { - if (!targetClientIds.length) return - - const commTargets = Array.from(targetClientIds).map((clientId) => ({ clientId })) - - this.clientModule.setPlayersCommType( - commTargets, - YacaFilterEnum.PHONE, - state, - undefined, - undefined, - CommDeviceMode.TRANSCEIVER, - CommDeviceMode.TRANSCEIVER, - GlobalState[PHONE_SPEAKER_STATE_NAME] ?? undefined, - ) - }) - - /** - * Handles the "client:yaca:phoneMute" server event. - * - * @param {number} targetID - The ID of the target. - * @param {boolean} state - The state of the phone mute. - * @param {boolean} onCallStop - The state of the call. - */ - onNet('client:yaca:phoneMute', (targetID: number, state: boolean, onCallStop = false) => { - const target = this.clientModule.getPlayerByID(targetID) - if (!target) { - return - } - - target.mutedOnPhone = state - - if (onCallStop) { - return - } - - if (this.clientModule.useWhisper && target.remoteID === cache.serverId) { - this.clientModule.setPlayersCommType([], YacaFilterEnum.PHONE, !state, undefined, undefined, CommDeviceMode.SENDER) - } else if (!this.clientModule.useWhisper && this.inCallWith.has(targetID)) { - this.clientModule.setPlayersCommType( - target, - YacaFilterEnum.PHONE, - state, - undefined, - undefined, - CommDeviceMode.TRANSCEIVER, - CommDeviceMode.TRANSCEIVER, - ) - } - }) - - /** - * Handles the "client:yaca:phoneSpeaker" server event. - * - * @param {number | number[]} playerIDs - The IDs of the players to be added or removed from the phone speaker. - * @param {boolean} state - The state indicating whether to add or remove the players. - */ - onNet('client:yaca:playersToPhoneSpeakerEmitWhisper', (playerIDs: number | number[], state: boolean) => { - if (!this.clientModule.useWhisper) return - - if (!Array.isArray(playerIDs)) { - playerIDs = [playerIDs] - } - - const targets = new Set() - for (const playerID of playerIDs) { - const player = this.clientModule.getPlayerByID(playerID) - if (!player) { - continue - } - - targets.add(player) - } - - if (targets.size < 1) { - return - } - - this.clientModule.setPlayersCommType( - Array.from(targets), - YacaFilterEnum.PHONE_SPEAKER, - state, - undefined, - undefined, - CommDeviceMode.SENDER, - CommDeviceMode.RECEIVER, - ) - }) - } - - registerExports() { - /** - * Exports the "isInCall" function. - * This function returns whether the player is in a phone call. - * - * @returns {boolean} - Whether the player is in a phone call. - */ - exports('isInCall', () => this.inCallWith.size > 0) - } - - registerStateBagHandlers() { - /** - * Handles the "yaca:phone" state bag change. - */ - AddStateBagChangeHandler(PHONE_SPEAKER_STATE_NAME, '', (bagName: string, _: string, value: number | number[] | null) => { - const playerId = GetPlayerFromStateBagName(bagName) - if (playerId === 0) { - return - } - - const playerSource = GetPlayerServerId(playerId) - if (playerSource === 0) { - return - } - - if (playerSource === cache.serverId) { - this.phoneSpeakerActive = value !== null - } - - this.removePhoneSpeakerFromEntity(playerSource) - if (value !== null) { - this.clientModule.setPlayerVariable(playerSource, 'phoneCallMemberIds', Array.isArray(value) ? value : [value]) - } - }) - } - - /** - * Removes the phone speaker effect from a player entity. - * - * @param {number} player - The player entity from which the phone speaker effect is to be removed. - */ - removePhoneSpeakerFromEntity(player: number) { - const entityData = this.clientModule.getPlayerByID(player) - if (!entityData?.phoneCallMemberIds) { - return - } - - const playersToSet = [] - for (const phoneCallMemberId of entityData.phoneCallMemberIds) { - const phoneCallMember = this.clientModule.getPlayerByID(phoneCallMemberId) - if (!phoneCallMember) { - continue - } - - playersToSet.push(phoneCallMember) - } - - this.clientModule.setPlayersCommType( - playersToSet, - YacaFilterEnum.PHONE_SPEAKER, - false, - undefined, - undefined, - CommDeviceMode.RECEIVER, - CommDeviceMode.SENDER, - ) - - entityData.phoneCallMemberIds = undefined - } - - /** - * Handles the disconnection of a player from a phone call. - * - * @param {number} targetID - The ID of the target. - */ - handleDisconnect(targetID: number) { - this.inCallWith.delete(targetID) - } - - /** - * Reestablishes a phone call with a target, when a player has restarted the voice plugin. - * - * @param {number | number[]} targetIDs - The IDs of the targets. - */ - reestablishCalls(targetIDs: number | number[]) { - if (!this.inCallWith.size) { - return - } - - if (!Array.isArray(targetIDs)) { - targetIDs = [targetIDs] - } - - if (!targetIDs.length) { - return - } - - const targetsToReestablish = [] - for (const targetId of targetIDs) { - if (this.inCallWith.has(targetId)) { - targetsToReestablish.push(targetId) - } - } - - if (targetsToReestablish.length) { - this.enablePhoneCall(targetsToReestablish, true, YacaFilterEnum.PHONE) - } - } - - /** - * Enables or disables a phone call. - * - * @param {number[]} targetIDs - The IDs of the targets. - * @param {boolean} state - The state of the phone call. - * @param {YacaFilterEnum} filter - The filter to use. - */ - enablePhoneCall(targetIDs: number[], state: boolean, filter: YacaFilterEnum = YacaFilterEnum.PHONE) { - if (!targetIDs.length) { - return - } - - const commTargets = [] - for (const targetID of targetIDs) { - const target = this.clientModule.getPlayerByID(targetID) - if (!target) { - if (!state) this.inCallWith.delete(targetID) - continue - } - - if (state) { - this.inCallWith.add(targetID) - } else { - this.inCallWith.delete(targetID) - } - - commTargets.push(target) - } - - this.clientModule.setPlayersCommType( - commTargets, - filter, - state, - undefined, - undefined, - state || (!state && this.inCallWith.size) ? CommDeviceMode.TRANSCEIVER : undefined, - CommDeviceMode.TRANSCEIVER, - GlobalState[GLOBAL_ERROR_LEVEL_STATE_NAME] ?? undefined, - ) - } -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/radio.ts b/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/radio.ts deleted file mode 100644 index e63893b34..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-client/src/yaca/radio.ts +++ /dev/null @@ -1,1124 +0,0 @@ -import { clamp, GLOBAL_ERROR_LEVEL_STATE_NAME, locale } from '@yaca-voice/common' -import { - CommDeviceMode, - type radioMode, - YacaFilterEnum, - YacaNotificationType, - type YacaPlayerData, - type YacaRadioSettings, - YacaStereoMode, -} from '@yaca-voice/types' -import { cache, calculateDistanceVec3, createProp, registerRdrKeyBind, requestAnimDict } from '../utils' -import type { YaCAClientModule } from './main' - -/** - * The radio module for the client. - */ -export class YaCAClientRadioModule { - clientModule: YaCAClientModule - - radioEnabled = false - radioInitialized = false - - talkingInChannels = new Set() - radioChannelSettings = new Map() - playersWithShortRange = new Map() - playersInRadioChannel = new Map>() - radioTowerCalculation = new Map() - - radioMode: radioMode = 'None' - activeRadioChannel = 1 - secondaryRadioChannel = 2 - - radioOnCooldown = false - currentRadioProp: number | null - - defaultRadioSettings: YacaRadioSettings = { - frequency: '0', - muted: false, - volume: 1, - stereo: YacaStereoMode.STEREO, - } - - /** - * Creates an instance of the radio module. - * - * @param clientModule - The client module. - */ - constructor(clientModule: YaCAClientModule) { - this.clientModule = clientModule - - this.radioMode = this.clientModule.sharedConfig.radioSettings.mode - - this.registerExports() - this.registerEvents() - - if (this.clientModule.isFiveM) { - this.registerKeybinds() - } else { - this.registerRdrKeybinds() - } - } - - /** - * Registers the exports for the radio module. - */ - registerExports() { - /** - * Enables or disables the radio system. - * - * @param {boolean} state - The state of the radio system. - */ - exports('enableRadio', (state: boolean) => this.enableRadio(state)) - - /** - * Returns the state of the radio system. - * - * @returns {boolean} The state of the radio system. - */ - exports('isRadioEnabled', () => this.radioEnabled) - - /** - * Changes the radio frequency of the active channel. - * - * @param {string} frequency - The frequency to set. - */ - exports('changeRadioFrequency', (frequency: string) => this.changeRadioFrequencyRaw(frequency)) - - /** - * Changes the radio frequency. - * - * @param {number} channel - The channel number. - * @param {string} frequency - The frequency to set. - */ - exports('changeRadioFrequencyRaw', (channel: number, frequency: string) => this.changeRadioFrequencyRaw(frequency, channel)) - - /** - * Returns the radio frequency of a channel. - * - * @param {number} channel - The channel number. - * @returns {string} The frequency of the channel. - */ - exports('getRadioFrequency', (channel: number) => this.getRadioFrequency(channel)) - - /** - * Mutes the active radio channel. - */ - exports('muteRadioChannel', (state?: boolean) => this.muteRadioChannel(state)) - - /** - * Exports the `muteRadioChannelRaw` function to the plugin. - * This function mutes a radio channel. - * - * @param {number} channel - The channel number. - */ - exports('muteRadioChannelRaw', (channel: number, state?: boolean) => this.muteRadioChannelRaw(channel, state)) - - /** - * Returns the mute state of a radio channel. - */ - exports('isRadioChannelMuted', (channel: number = this.activeRadioChannel) => this.isRadioChannelMuted(channel)) - - /** - * Exports the `setActiveRadioChannel` function to the plugin. - * This function changes the active radio channel. - * - * @param {number} channel - The new radio channel. - */ - exports('setActiveRadioChannel', (channel: number) => this.setActiveRadioChannel(channel)) - - /** - * Exports the `getActiveRadioChannel` function to the plugin. - * This function returns the active radio channel. - * - * @returns {number} The active radio channel. - */ - exports('getActiveRadioChannel', () => this.activeRadioChannel) - - /** - * Exports the `setSecondaryRadioChannel` function to the plugin. - * This function changes the secondary radio channel. - * - * @param {number} channel - The new radio channel. - */ - exports('setSecondaryRadioChannel', (channel: number) => this.setSecondaryRadioChannel(channel)) - - /** - * Exports the `getActiveRadioChannel` function to the plugin. - * This function returns the active radio channel. - * - * @returns {number} The active radio channel. - */ - exports('getSecondaryRadioChannel', () => this.secondaryRadioChannel) - - /** - * Exports the `changeRadioChannelVolume` function to the plugin. - * This function changes the volume of the active radio channel. - * - * @param {boolean} higher - Whether to increase the volume. - */ - exports('changeRadioChannelVolume', (higher: boolean) => this.changeRadioChannelVolume(higher)) - - /** - * Exports the `changeRadioChannelVolumeRaw` function to the plugin. - * This function changes the volume of a radio channel. - * - * @param {number} channel - The channel number. - * @param {number} volume - The volume to set. - */ - exports('changeRadioChannelVolumeRaw', (channel: number, volume: number) => this.changeRadioChannelVolumeRaw(volume, channel)) - - /** - * Returns the volume of a radio channel. - * - * @param {number} channel - The channel number. - * @returns {number} The volume of the channel. - */ - exports('getRadioChannelVolume', (channel: number) => this.getRadioChannelVolume(channel)) - - /** - * Exports the `changeRadioChannelStereo` function to the plugin. - * This function changes the stereo mode for the active radio channel. - */ - exports('changeRadioChannelStereo', () => this.changeRadioChannelStereo()) - - /** - * Exports the `changeRadioChannelStereoRaw` function to the plugin. - * This function changes the stereo mode for a radio channel. - * - * @param {number} channel - The channel number. - * @param {YacaStereoMode} stereo - The stereo mode to set. - */ - exports('changeRadioChannelStereoRaw', (channel: number, stereo: YacaStereoMode) => this.changeRadioChannelStereoRaw(stereo, channel)) - - /** - * Returns the stereo mode of a radio channel. - * - * @param {number} channel - The channel number. - * @returns {YacaStereoMode} The stereo mode of the channel. - */ - exports('getRadioChannelStereo', (channel: number) => this.getRadioChannelStereo(channel)) - - /** - * Exports the `radioTalkingStart` function to the plugin. - * This function starts the radio talking state. - * - * @param {boolean} state - The state of the radio talking. - * @param {number} channel - The radio channel. - */ - exports('radioTalkingStart', (state: boolean, channel: number) => this.radioTalkingStart(state, channel)) - - /** - * Sets the radio mode. - * - * @param {radioMode} mode - The radio mode to set. - */ - exports('setRadioMode', (mode: radioMode) => { - this.radioMode = mode - }) - - /** - * Returns the radio mode. - * - * @returns {radioMode} The current radio mode. - */ - exports('getRadioMode', () => this.radioMode) - } - - /** - * Registers the events for the radio module. - */ - registerEvents() { - /** - * Handles the "client:yaca:setRadioFreq" server event. - * - * @param {number} channel - The channel number. - * @param {string} frequency - The frequency to set. - */ - onNet('client:yaca:setRadioFreq', (channel: number, frequency: string) => { - this.setRadioFrequency(channel, frequency) - }) - - /** - * Handles the "client:yaca:radioTalking" server event. - * - * @param {number} target - The ID of the target. - * @param {string} frequency - The frequency of the radio. - * @param {boolean} state - The state of the radio talking. - * @param {object[]} infos - The information about the radio. - * @param {boolean} infos.shortRange - The state of the short range. - */ - onNet( - 'client:yaca:radioTalking', - ( - target: number, - frequency: string, - state: boolean, - infos: { shortRange: boolean }[], - senderDistanceToTower = -1, - senderPosition: [number, number, number] = [0, 0, 0], - ) => { - const channel = this.findRadioChannelByFrequency(frequency) - if (!channel) { - return - } - - const ownDistanceToTowerOrSender = this.getDistanceToTowerOrSender(senderPosition) - - if (state) { - if (this.radioMode !== 'None' && ownDistanceToTowerOrSender > this.clientModule.sharedConfig.radioSettings.maxDistance) return - if (this.radioMode === 'Tower' && senderDistanceToTower > this.clientModule.sharedConfig.radioSettings.maxDistance) return - } - - const player = this.clientModule.getPlayerByID(target) - if (!player) { - return - } - - const info = infos[cache.serverId] - - if (!info?.shortRange || (info?.shortRange && GetPlayerFromServerId(target) !== -1)) { - const errorLevel = this.getErrorLevelFromDistance(ownDistanceToTowerOrSender, senderDistanceToTower) - - this.clientModule.setPlayersCommType( - player, - YacaFilterEnum.RADIO, - state, - channel, - undefined, - CommDeviceMode.RECEIVER, - CommDeviceMode.SENDER, - errorLevel, - ) - } - - if (state) { - this.playersInRadioChannel.get(channel)?.add(target) - if (info?.shortRange) { - this.playersWithShortRange.set(target, frequency) - } - - emit('yaca:external:isRadioReceiving', true, channel) - this.clientModule.saltyChatBridge?.handleRadioReceivingStateChange(true, channel) - } else { - this.playersInRadioChannel.get(channel)?.delete(target) - if (info?.shortRange) { - this.playersWithShortRange.delete(target) - } - - const inRadio = this.playersInRadioChannel.get(channel)?.size || 0 - const state = inRadio > 0 - emit('yaca:external:isRadioReceiving', state, channel) - this.clientModule.saltyChatBridge?.handleRadioReceivingStateChange(state, channel) - } - }, - ) - - /** - * Handles the "client:yaca:radioTalking" server event. - * - * @param {number} target - The ID of the target. - * @param {string} frequency - The frequency of the radio. - * @param {boolean} state - The state of the radio talking. - * @param {object[]} infos - The information about the radio. - * @param {boolean} infos.shortRange - The state of the short range. - * @param {boolean} self - The state of the player. - */ - onNet( - 'client:yaca:radioTalkingWhisper', - (targets: number[], frequency: string, state: boolean, senderPosition: [number, number, number] = [0, 0, 0]) => { - const channel = this.findRadioChannelByFrequency(frequency) - if (!channel) { - return - } - - const ownDistanceToTowerOrSender = this.getDistanceToTowerOrSender(senderPosition) - - if (state && this.radioMode !== 'None' && ownDistanceToTowerOrSender > this.clientModule.sharedConfig.radioSettings.maxDistance) targets = [] - this.radioTalkingStateToPluginWithWhisper(state, targets, channel) - }, - ) - - /** - * Handles the "client:yaca:setRadioMuteState" server event. - * - * @param {number} channel - The channel number. - * @param {boolean} state - The state of the radio mute. - */ - onNet('client:yaca:setRadioMuteState', (channel: number, state: boolean) => { - const channelSettings = this.radioChannelSettings.get(channel) - - if (!channelSettings) { - return - } - - channelSettings.muted = state - emit('yaca:external:setRadioMuteState', channel, state) - this.disableRadioFromPlayerInChannel(channel) - this.updateRadioChannelData(channel) - }) - - /** - * Handles the "client:yaca:leaveRadioChannel" server event. - * - * @param {number | number[]} client_ids - The IDs of the clients. - * @param {string} frequency - The frequency of the radio. - */ - onNet('client:yaca:leaveRadioChannel', (client_ids: number | number[], frequency: string) => { - if (!Array.isArray(client_ids)) { - client_ids = [client_ids] - } - - const channel = this.findRadioChannelByFrequency(frequency) - if (!channel) { - return - } - - const playerData = this.clientModule.getPlayerByID(cache.serverId) - if (!playerData || !playerData.clientId) { - return - } - - if (client_ids.includes(playerData.clientId)) { - this.setRadioFrequency(channel, '0') - } - - this.clientModule.sendWebsocket({ - base: { request_type: 'INGAME' }, - comm_device_left: { - comm_type: YacaFilterEnum.RADIO, - client_ids, - channel, - }, - }) - }) - } - - /** - * Registers the command and key mapping for the radio talking. - */ - registerKeybinds() { - if (this.clientModule.sharedConfig.keyBinds.primaryRadioTransmit !== false) { - /** - * Registers the command and key mapping for the radio talking. - */ - RegisterCommand( - '+yaca:radioTalking', - () => { - this.radioTalkingStart(true, this.activeRadioChannel) - }, - false, - ) - RegisterCommand( - '-yaca:radioTalking', - () => { - this.radioTalkingStart(false, this.activeRadioChannel) - }, - false, - ) - RegisterKeyMapping('+yaca:radioTalking', locale('use_radio'), 'keyboard', this.clientModule.sharedConfig.keyBinds.primaryRadioTransmit) - } - - if (this.clientModule.sharedConfig.keyBinds.secondaryRadioTransmit !== false) { - /** - * Registers the command and key mapping for the secondary radio talking. - */ - RegisterCommand( - '+yaca:secondaryRadioTalking', - () => { - this.radioTalkingStart(true, this.secondaryRadioChannel) - }, - false, - ) - RegisterCommand( - '-yaca:secondaryRadioTalking', - () => { - this.radioTalkingStart(false, this.secondaryRadioChannel) - }, - false, - ) - RegisterKeyMapping( - '+yaca:secondaryRadioTalking', - locale('use_secondary_radio'), - 'keyboard', - this.clientModule.sharedConfig.keyBinds.secondaryRadioTransmit, - ) - } - } - - /** - * Registers the keybindings for the radio talking. - * This is only available in RedM. - */ - registerRdrKeybinds() { - if (this.clientModule.sharedConfig.keyBinds.primaryRadioTransmit !== false) { - registerRdrKeyBind( - this.clientModule.sharedConfig.keyBinds.primaryRadioTransmit, - () => { - this.radioTalkingStart(true, this.activeRadioChannel) - }, - () => { - this.radioTalkingStart(false, this.activeRadioChannel) - }, - ) - } - - if (this.clientModule.sharedConfig.keyBinds.secondaryRadioTransmit !== false) { - registerRdrKeyBind( - this.clientModule.sharedConfig.keyBinds.secondaryRadioTransmit, - () => { - this.radioTalkingStart(true, this.secondaryRadioChannel) - }, - () => { - this.radioTalkingStart(false, this.secondaryRadioChannel) - }, - ) - } - } - - /** - * Calculates the error level based on the distance to the tower or sender. - * - * @param ownDistanceToTower - The distance to the tower. - * @param senderDistanceToTower - The distance to the tower for the sender. - */ - getErrorLevelFromDistance(ownDistanceToTower: number, senderDistanceToTower: number) { - let errorLevel: number - - const globalErrorLevel = GlobalState[GLOBAL_ERROR_LEVEL_STATE_NAME] || 0 - - if (this.radioMode === 'Tower') { - const ownSignalStrength = this.calculateSignalStrength(ownDistanceToTower) - const senderSignalStrength = this.calculateSignalStrength(senderDistanceToTower) - - errorLevel = Math.max(ownSignalStrength, senderSignalStrength, globalErrorLevel) - } else if (this.radioMode === 'Direct') { - const signaleStrength = this.calculateSignalStrength(ownDistanceToTower) - - errorLevel = Math.max(signaleStrength, globalErrorLevel) - } else { - errorLevel = globalErrorLevel - } - - return errorLevel - } - - /** - * Get the distance to the tower or sender. - * - * @param senderPosition - The position of the sender. - */ - getDistanceToTowerOrSender(senderPosition: [number, number, number]) { - let ownDistanceToTower = Number.MAX_VALUE - - if (this.radioMode === 'Tower') { - ownDistanceToTower = this.getNearestRadioTower() - } else if (this.radioMode === 'Direct') { - ownDistanceToTower = calculateDistanceVec3(GetEntityCoords(cache.ped, false), senderPosition) - } - - return ownDistanceToTower - } - - /** - * Enable or disable the radio system. - * - * @param {boolean} state - The state of the radio system. - */ - enableRadio(state: boolean) { - if (!this.clientModule.isPluginInitialized()) { - return - } - - if (this.radioEnabled !== state) { - this.radioEnabled = state - emitNet('server:yaca:enableRadio', state) - - if (!state) { - for (let i = 1; i <= this.clientModule.sharedConfig.radioSettings.channelCount; i++) { - this.disableRadioFromPlayerInChannel(i) - } - } - - if (state && !this.radioInitialized) { - this.radioInitialized = true - this.initRadioSettings() - this.updateRadioChannelData(this.activeRadioChannel) - } - - emit('yaca:external:isRadioEnabled', state) - } - } - - /** - * Calculate the signal strength based on the distance. - * - * @param distance - The distance to the radio tower. - * @param maxDistance - The maximum distance to the radio tower. - * - * @returns {number} The signal strength. - */ - calculateSignalStrength(distance: number, maxDistance: number = this.clientModule.sharedConfig.radioSettings.maxDistance): number { - const ratio = distance / maxDistance - return clamp(Math.log10(1 + ratio * 8.5) / Math.log10(10), 0, 1) - } - - /** - * Finds the nearest tower to the local player. - * Iterates through all towers and calculates the distance to the local player's position. - * Keeps track of the nearest tower and returns its distance. - * - * @returns {number | null} The distance to the nearest tower, or null if no tower is found. - */ - getNearestRadioTower() { - let nearestTowerDistance = Number.MAX_VALUE - - const playerPos = GetEntityCoords(cache.ped, false) - - for (const coords of this.clientModule.towerConfig.towerPositions) { - const distance = calculateDistanceVec3(playerPos, coords) - - if (!nearestTowerDistance || distance < nearestTowerDistance) { - nearestTowerDistance = distance - } - } - - return nearestTowerDistance - } - - /** - * Change the radio frequency. - * - * @param {number} channel - The channel number. - * @param {string} frequency - The frequency to set. - */ - changeRadioFrequencyRaw(frequency: string, channel: number = this.activeRadioChannel) { - if (!this.clientModule.isPluginInitialized()) { - return - } - - emitNet('server:yaca:changeRadioFrequency', channel, frequency) - } - - /** - * Get the radio frequency of a channel. - * - * @param channel - The channel number. - * @returns {string} The frequency of the channel. - */ - getRadioFrequency(channel: number = this.activeRadioChannel): string { - const channelData = this.radioChannelSettings.get(channel) - - if (!channelData) { - return '0' - } - - return channelData.frequency - } - - /** - * Mute the active radio channel. - */ - muteRadioChannel(state?: boolean) { - this.muteRadioChannelRaw(this.activeRadioChannel, state) - } - - /** - * Mute a radio channel. - * - * @param {number} channel - The channel to mute. Defaults to the current active channel. - */ - muteRadioChannelRaw(channel: number = this.activeRadioChannel, state?: boolean) { - if (!this.clientModule.isPluginInitialized() || !this.radioEnabled) { - return - } - - const channelSettings = this.radioChannelSettings.get(channel) - - if (!channelSettings) { - return - } - - if (channelSettings.frequency === '0') { - return - } - - emitNet('server:yaca:muteRadioChannel', channel, state) - } - - /** - * Check if a radio channel is muted. - * - * @param channel - The channel number. Defaults to the active channel. - * @returns {boolean} Whether the channel is muted. If the channel does not exist, it will return true. - */ - isRadioChannelMuted(channel: number = this.activeRadioChannel): boolean { - const channelData = this.radioChannelSettings.get(channel) - - if (!channelData) { - return true - } - - return channelData.muted - } - - /** - * Change the active radio channel. - * - * @param {number} channel - The new radio channel. - * @returns {boolean} Whether the channel was changed. - */ - setActiveRadioChannel(channel: number): boolean { - if (!this.clientModule.isPluginInitialized() || !this.radioEnabled) { - return false - } - - emit('yaca:external:changedActiveRadioChannel', channel) - this.activeRadioChannel = channel - this.updateRadioChannelData(this.activeRadioChannel) - - return true - } - - /** - * Change the active radio channel. - * - * @param {number} channel - The new radio channel. - * @returns {boolean} Whether the channel was changed. - */ - setSecondaryRadioChannel(channel: number): boolean { - if (!this.clientModule.isPluginInitialized() || !this.radioEnabled) { - return false - } - - if (this.secondaryRadioChannel === channel) { - this.secondaryRadioChannel = -1 - this.clientModule.notification(locale('secondary_radio_channel_disabled'), YacaNotificationType.INFO) - } else { - this.secondaryRadioChannel = channel - this.clientModule.notification(locale('secondary_radio_channel_enabled', channel), YacaNotificationType.INFO) - } - - emit('yaca:external:changedSecondaryRadioChannel', this.secondaryRadioChannel) - - return true - } - - /** - * Change the volume of the active radio channel. - * - * @param {boolean} higher - Whether to increase the volume. - * @returns {boolean} Whether the volume was changed. - */ - changeRadioChannelVolume(higher: boolean): boolean { - const channel = this.activeRadioChannel - const radioSettings = this.radioChannelSettings.get(channel) - - if (!radioSettings) { - return false - } - - const oldVolume = radioSettings.volume - return this.changeRadioChannelVolumeRaw(oldVolume + (higher ? 0.17 : -0.17), channel) - } - - /** - * Change the volume of a radio channel. - * - * @param {number} channel - The channel number. Defaults to the active channel. - * @param {number} volume - The volume to set. - * @returns {boolean} Whether the volume was changed. - */ - changeRadioChannelVolumeRaw(volume: number, channel: number = this.activeRadioChannel): boolean { - if (!this.clientModule.isPluginInitialized() || !this.radioEnabled) { - return false - } - - const channelSettings = this.radioChannelSettings.get(channel) - if (!channelSettings) { - return false - } - - const oldVolume = channelSettings.volume - channelSettings.volume = clamp(volume, 0, 1) - - // Prevent event emit spams, if nothing changed - if (oldVolume === channelSettings.volume) { - return true - } - - if (channelSettings.volume === 0 || (oldVolume === 0 && channelSettings.volume > 0)) { - emitNet('server:yaca:muteRadioChannel', channel, channelSettings.volume === 0) - } - - // Prevent duplicate update, cuz mute has its own update - if (channelSettings.volume > 0) { - emit('yaca:external:setRadioVolume', channel, channelSettings.volume) - this.updateRadioChannelData(channel) - } - - // Send update to voice plugin - this.clientModule.setCommDeviceVolume(YacaFilterEnum.RADIO, channelSettings.volume, channel) - return true - } - - /** - * Get the volume of a radio channel. - * - * @param channel - The channel number. Defaults to the active channel. - * @returns {number} The volume of the channel. If the channel does not exist, it will return 0. - */ - getRadioChannelVolume(channel: number = this.activeRadioChannel): number { - const channelData = this.radioChannelSettings.get(channel) - - if (!channelData) { - return 0 - } - - return channelData.volume - } - - /** - * Change the stereo mode for the active radio channel. - * - * @param channel - The channel number. Defaults to the active channel. - * @returns {boolean} Whether the stereo mode was changed. - */ - changeRadioChannelStereo(channel: number = this.activeRadioChannel): boolean { - const channelSettings = this.radioChannelSettings.get(channel) - - if (!channelSettings) { - return false - } - - switch (channelSettings.stereo) { - case YacaStereoMode.STEREO: - if (this.changeRadioChannelStereoRaw(YacaStereoMode.MONO_LEFT, channel)) { - this.clientModule.notification(locale('changed_stereo_mode', channel, locale('left_ear')), YacaNotificationType.INFO) - return true - } - break - case YacaStereoMode.MONO_LEFT: - if (this.changeRadioChannelStereoRaw(YacaStereoMode.MONO_RIGHT, channel)) { - this.clientModule.notification(locale('changed_stereo_mode', channel, locale('right_ear')), YacaNotificationType.INFO) - return true - } - break - default: - if (this.changeRadioChannelStereoRaw(YacaStereoMode.STEREO, channel)) { - this.clientModule.notification(locale('changed_stereo_mode', channel, locale('both_ears')), YacaNotificationType.INFO) - return true - } - break - } - - return false - } - - /** - * Change the stereo mode for a radio channel. - * - * @param channel - The channel number. Defaults to the active channel. - * @param stereo - The stereo mode to set. - * @returns {boolean} Whether the stereo mode was changed. - */ - changeRadioChannelStereoRaw(stereo: YacaStereoMode, channel: number = this.activeRadioChannel): boolean { - if (!this.clientModule.isPluginInitialized() || !this.radioEnabled) { - return false - } - - const channelSettings = this.radioChannelSettings.get(channel) - if (!channelSettings) { - return false - } - - channelSettings.stereo = stereo - this.clientModule.setCommDeviceStereoMode(YacaFilterEnum.RADIO, stereo, channel) - - emit('yaca:external:setRadioChannelStereo', channel, stereo.toString()) - - return true - } - - /** - * Get the stereo mode of a radio channel. - * - * @param channel - The channel number. Defaults to the active channel. - * @returns {string} The stereo mode of the channel. - */ - getRadioChannelStereo(channel: number = this.activeRadioChannel): string { - const channelData = this.radioChannelSettings.get(channel) - - if (!channelData) { - return YacaStereoMode.STEREO.toString() - } - - return channelData.stereo.toString() - } - - /** - * Set volume & stereo mode for all radio channels on first start and reconnect. - */ - initRadioSettings() { - for (let i = 1; i <= this.clientModule.sharedConfig.radioSettings.channelCount; i++) { - if (!this.radioChannelSettings.has(i)) { - this.radioChannelSettings.set(i, { - ...this.defaultRadioSettings, - }) - } - if (!this.playersInRadioChannel.has(i)) { - this.playersInRadioChannel.set(i, new Set()) - } - - const { volume, stereo, frequency } = this.radioChannelSettings.get(i) ?? this.defaultRadioSettings - - this.clientModule.setCommDeviceStereoMode(YacaFilterEnum.RADIO, stereo, i) - this.clientModule.setCommDeviceVolume(YacaFilterEnum.RADIO, volume, i) - - if (frequency !== '0') { - emitNet('server:yaca:changeRadioFrequency', i, frequency) - } - } - } - - /** - * Sends an event to the plugin when a player starts or stops talking on the radio. - * - * @param {boolean} state - The state of the player talking on the radio. - * @param {number} channel - The channel number. - */ - radioTalkingStateToPlugin(state: boolean, channel: number) { - const player = this.clientModule.getPlayerByID(cache.serverId) - - if (!player) { - return - } - - this.clientModule.setPlayersCommType(player, YacaFilterEnum.RADIO, state, channel) - } - - /** - * Sends an event to the plugin when a player starts or stops talking on the radio with whisper. - * - * @param state - The state of the player talking on the radio. - * @param targets - The IDs of the targets. - * @param channel - The channel number. - */ - radioTalkingStateToPluginWithWhisper(state: boolean, targets: number[], channel: number) { - const comDeviceTargets = [] - - for (const target of targets) { - const player = this.clientModule.getPlayerByID(target) - if (!player) continue - - comDeviceTargets.push(player) - } - - this.clientModule.setPlayersCommType(comDeviceTargets, YacaFilterEnum.RADIO, state, channel, undefined, CommDeviceMode.SENDER, CommDeviceMode.RECEIVER) - } - - /** - * Finds a radio channel by a given frequency. - * - * @param {string} frequency - The frequency to search for. - * @returns {number | null} The channel number if found, null otherwise. - */ - findRadioChannelByFrequency(frequency: string): number | null { - for (const [channel, data] of this.radioChannelSettings) { - if (data.frequency === frequency) { - return channel - } - } - - return null - } - - /** - * Set the radio frequency. - * - * @param channel - The channel number. - * @param frequency - The frequency to set. - */ - setRadioFrequency(channel: number, frequency: string) { - const channelSettings = this.radioChannelSettings.get(channel) - if (!channelSettings) { - return - } - - if (channelSettings.frequency !== frequency) { - this.disableRadioFromPlayerInChannel(channel) - } - - channelSettings.frequency = frequency - emit('yaca:external:setRadioFrequency', channel, frequency) - - // SaltyChat bridge - if (this.clientModule.saltyChatBridge) { - const saltyFrequency = channelSettings.frequency === '0' ? '' : channelSettings.frequency - emit('SaltyChat_RadioChannelChanged', saltyFrequency, channel === 1) - } - } - - /** - * Disable radio effect for all players in the given channel. - * - * @param {number} channel - The channel number. - */ - disableRadioFromPlayerInChannel(channel: number) { - const players = this.playersInRadioChannel.get(channel) - if (!players || !players.size) { - return - } - - const targets: YacaPlayerData[] = [] - for (const playerId of players) { - const player = this.clientModule.getPlayerByID(playerId) - if (!player || !player.remoteID) { - continue - } - - targets.push(player) - players.delete(player.remoteID) - } - - if (targets.length) { - this.clientModule.setPlayersCommType(targets, YacaFilterEnum.RADIO, false, channel, undefined, CommDeviceMode.RECEIVER, CommDeviceMode.SENDER) - } - } - - /** - * Starts the radio talking state. - * - * @param {boolean} state - The state of the radio talking. - * @param {number} channel - The radio channel. - */ - async radioTalkingStart(state: boolean, channel: number) { - if (channel === -1) return - - if (!state) { - if (this.talkingInChannels.has(channel)) { - this.talkingInChannels.delete(channel) - if (this.radioTowerCalculation.has(channel)) { - clearInterval(this.radioTowerCalculation.get(channel) as CitizenTimer) - this.radioTowerCalculation.delete(channel) - } - - this.clientModule.saltyChatBridge?.handleRadioTalkingStateChange(false, channel) - - if (!this.clientModule.useWhisper) { - this.radioTalkingStateToPlugin(false, channel) - } - - emitNet('server:yaca:radioTalking', false, channel, -1) - emit('yaca:external:isRadioTalking', false, channel) - - StopAnimTask( - cache.ped, - this.clientModule.sharedConfig.radioSettings.animation.dictionary, - this.clientModule.sharedConfig.radioSettings.animation.name, - 4, - ) - RemoveAnimDict(this.clientModule.sharedConfig.radioSettings.animation.dictionary) - - if (this.currentRadioProp !== null) { - if (DoesEntityExist(this.currentRadioProp)) { - DeleteEntity(this.currentRadioProp) - } - - if (this.clientModule.sharedConfig.radioSettings.propWhileTalking.prop !== false) { - SetModelAsNoLongerNeeded(this.clientModule.sharedConfig.radioSettings.propWhileTalking.prop) - } - - this.currentRadioProp = null - } - } - - return - } - - if (this.clientModule.sharedConfig.radioAntiSpamCooldown) { - if (this.radioOnCooldown) { - return - } - - this.radioOnCooldown = true - - setTimeout(() => { - this.radioOnCooldown = false - }, this.clientModule.sharedConfig.radioAntiSpamCooldown) - } - - const channelSettings = this.radioChannelSettings.get(channel) - if (!this.radioEnabled || channelSettings?.frequency === '0' || this.talkingInChannels.has(channel)) { - return - } - - this.talkingInChannels.add(channel) - if (!this.clientModule.useWhisper) { - this.radioTalkingStateToPlugin(true, channel) - } - - if (this.clientModule.sharedConfig.radioSettings.propWhileTalking.prop !== false) { - const prop = await createProp( - this.clientModule.sharedConfig.radioSettings.propWhileTalking.prop, - this.clientModule.sharedConfig.radioSettings.propWhileTalking.boneId, - this.clientModule.sharedConfig.radioSettings.propWhileTalking.position, - this.clientModule.sharedConfig.radioSettings.propWhileTalking.rotation, - ) - - this.currentRadioProp = prop ?? null - } - - const animDict = await requestAnimDict(this.clientModule.sharedConfig.radioSettings.animation.dictionary) - if (animDict) { - TaskPlayAnim( - cache.ped, - animDict, - this.clientModule.sharedConfig.radioSettings.animation.name, - 3, - -4, - -1, - this.clientModule.sharedConfig.radioSettings.animation.flag, - 0.0, - false, - false, - false, - ) - } - - this.clientModule.saltyChatBridge?.handleRadioTalkingStateChange(true, channel) - - this.sendRadioRequestToServer(channel) - if (!this.radioTowerCalculation.has(channel)) { - this.radioTowerCalculation.set( - channel, - setInterval(() => { - this.sendRadioRequestToServer(channel) - }, 1000), - ) - } - - emit('yaca:external:isRadioTalking', true, channel) - } - - /** - * Sends a radio request to the server and calculates the distance to the nearest radio tower. - * - * @param channel - The radio channel to send the request to. - */ - sendRadioRequestToServer(channel: number) { - const distanceToTower = this.getNearestRadioTower() ?? -1 - emitNet('server:yaca:radioTalking', true, channel, distanceToTower) - } - - /** - * Updates the data of the specified radio channel if certain conditions are met. - * - * @param {number} channel - The number of the radio channel to update. - */ - updateRadioChannelData(channel: number) { - if (channel !== this.activeRadioChannel || GetResourceState('yaca-ui') !== 'started') return - - exports['yaca-ui'].setRadioChannelData(this.radioChannelSettings.get(channel)) - } -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-client/tsconfig.json b/resources/[voice]/yaca-voice/apps/yaca-client/tsconfig.json deleted file mode 100644 index 1a891b77f..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-client/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "@yaca-voice/typescript-config/fivem.json", - "compilerOptions": { - "baseUrl": "./", - "rootDir": "./src", - "types": ["@citizenfx/client"] - }, - "include": ["./"] -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/build.js b/resources/[voice]/yaca-voice/apps/yaca-server/build.js deleted file mode 100644 index 644a07336..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-server/build.js +++ /dev/null @@ -1,23 +0,0 @@ -import { build } from 'esbuild' - -const production = process.argv.includes('--mode=production') - -build({ - entryPoints: ['src/index.ts'], - outfile: './dist/server.js', - bundle: true, - loader: { - '.ts': 'ts', - '.js': 'js', - }, - write: true, - platform: 'node', - target: 'node16', - sourcemap: production ? false : 'inline', - dropLabels: production ? ['DEV'] : undefined, -}) - .then(() => { - console.log('Server built successfully') - }) - // skipcq: JS-0263 - .catch(() => process.exit(1)) diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/package.json b/resources/[voice]/yaca-voice/apps/yaca-server/package.json deleted file mode 100644 index b7b01a29e..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-server/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "yaca-server", - "version": "0.0.0", - "private": true, - "type": "module", - "scripts": { - "build": "node build.js --mode=production", - "dev": "node build.js", - "typecheck": "tsc --project tsconfig.json" - }, - "dependencies": { - "node-fetch": "^3.3.2" - }, - "devDependencies": { - "@citizenfx/server": "latest", - "@types/luxon": "^3.4.2", - "@types/node": "^20.16.10", - "@yaca-voice/common": "workspace:*", - "@yaca-voice/types": "workspace:*", - "@yaca-voice/typescript-config": "workspace:*" - }, - "engines": { - "node": ">=16.9.1" - } -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/pnpm-lock.yaml b/resources/[voice]/yaca-voice/apps/yaca-server/pnpm-lock.yaml deleted file mode 100644 index b8135cad3..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-server/pnpm-lock.yaml +++ /dev/null @@ -1,1136 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@overextended/ox_lib': - specifier: ^3.23.1 - version: 3.24.0 - node-fetch: - specifier: ^3.3.2 - version: 3.3.2 - devDependencies: - '@citizenfx/client': - specifier: latest - version: 2.0.9235-1 - '@eslint/js': - specifier: ^8.57.0 - version: 8.57.0 - '@types/luxon': - specifier: ^3.4.2 - version: 3.4.2 - '@types/node': - specifier: ^20.14.10 - version: 20.14.14 - esbuild: - specifier: ^0.20.2 - version: 0.20.2 - eslint: - specifier: ^8.57.0 - version: 8.57.0 - -packages: - - '@citizenfx/client@2.0.9235-1': - resolution: {integrity: sha512-e1FM4tX0eeY1Y1HuBj8D5JSlDqGmRW9epY24Odib+nk8L7IKRDBzxJBbktXfFsEqF10OJue9v7yx5ACot9CZGQ==} - - '@esbuild/aix-ppc64@0.20.2': - resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.20.2': - resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.20.2': - resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.20.2': - resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.20.2': - resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.20.2': - resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.20.2': - resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.20.2': - resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.20.2': - resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.20.2': - resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.20.2': - resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.20.2': - resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.20.2': - resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.20.2': - resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.20.2': - resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.20.2': - resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.20.2': - resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-x64@0.20.2': - resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-x64@0.20.2': - resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - - '@esbuild/sunos-x64@0.20.2': - resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.20.2': - resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.20.2': - resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.20.2': - resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - - '@eslint-community/eslint-utils@4.4.0': - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.11.0': - resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/eslintrc@2.1.4': - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - '@eslint/js@8.57.0': - resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - '@humanwhocodes/config-array@0.11.14': - resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/object-schema@2.0.3': - resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} - deprecated: Use @eslint/object-schema instead - - '@nativewrappers/client@1.7.33': - resolution: {integrity: sha512-phuBBGdDPxZiZyw5CaFs1XWfvllnEtwATMdLaNucwMofVg/O/FjlP1bTUq4SOm4qhSZ4Zdo351ijHzBSIbZs6g==} - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@overextended/ox_lib@3.24.0': - resolution: {integrity: sha512-diI+hvmGfDBoYzuFeKrVLrdXff1yYIgARNP22i8oQqrzlBig46HJMXm7YRmwsE+8Z/w6TwQv9UL9j/vzVGpnnQ==} - - '@types/luxon@3.4.2': - resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} - - '@types/node@20.14.14': - resolution: {integrity: sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==} - - '@ungap/structured-clone@1.2.0': - resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@8.12.1: - resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} - engines: {node: '>=0.4.0'} - hasBin: true - - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - boolean@3.2.0: - resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} - - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} - - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - - data-uri-to-buffer@4.0.1: - resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} - engines: {node: '>= 12'} - - debug@4.3.6: - resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - - esbuild@0.20.2: - resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} - engines: {node: '>=12'} - hasBin: true - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint@8.57.0: - resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - hasBin: true - - espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - - fast-printf@1.6.9: - resolution: {integrity: sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg==} - engines: {node: '>=10.0'} - - fastq@1.17.1: - resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} - - fetch-blob@3.2.0: - resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} - engines: {node: ^12.20 || >= 14.13} - - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} - - flatted@3.3.1: - resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} - - formdata-polyfill@4.0.10: - resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} - engines: {node: '>=12.20.0'} - - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported - - globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} - - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - ignore@5.3.1: - resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} - engines: {node: '>= 4'} - - import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - - node-fetch@3.3.2: - resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - - typescript@5.5.4: - resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} - engines: {node: '>=14.17'} - hasBin: true - - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - web-streams-polyfill@3.3.3: - resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} - engines: {node: '>= 8'} - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - -snapshots: - - '@citizenfx/client@2.0.9235-1': {} - - '@esbuild/aix-ppc64@0.20.2': - optional: true - - '@esbuild/android-arm64@0.20.2': - optional: true - - '@esbuild/android-arm@0.20.2': - optional: true - - '@esbuild/android-x64@0.20.2': - optional: true - - '@esbuild/darwin-arm64@0.20.2': - optional: true - - '@esbuild/darwin-x64@0.20.2': - optional: true - - '@esbuild/freebsd-arm64@0.20.2': - optional: true - - '@esbuild/freebsd-x64@0.20.2': - optional: true - - '@esbuild/linux-arm64@0.20.2': - optional: true - - '@esbuild/linux-arm@0.20.2': - optional: true - - '@esbuild/linux-ia32@0.20.2': - optional: true - - '@esbuild/linux-loong64@0.20.2': - optional: true - - '@esbuild/linux-mips64el@0.20.2': - optional: true - - '@esbuild/linux-ppc64@0.20.2': - optional: true - - '@esbuild/linux-riscv64@0.20.2': - optional: true - - '@esbuild/linux-s390x@0.20.2': - optional: true - - '@esbuild/linux-x64@0.20.2': - optional: true - - '@esbuild/netbsd-x64@0.20.2': - optional: true - - '@esbuild/openbsd-x64@0.20.2': - optional: true - - '@esbuild/sunos-x64@0.20.2': - optional: true - - '@esbuild/win32-arm64@0.20.2': - optional: true - - '@esbuild/win32-ia32@0.20.2': - optional: true - - '@esbuild/win32-x64@0.20.2': - optional: true - - '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': - dependencies: - eslint: 8.57.0 - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.11.0': {} - - '@eslint/eslintrc@2.1.4': - dependencies: - ajv: 6.12.6 - debug: 4.3.6 - espree: 9.6.1 - globals: 13.24.0 - ignore: 5.3.1 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@8.57.0': {} - - '@humanwhocodes/config-array@0.11.14': - dependencies: - '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.6 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/object-schema@2.0.3': {} - - '@nativewrappers/client@1.7.33': {} - - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.17.1 - - '@overextended/ox_lib@3.24.0': - dependencies: - '@nativewrappers/client': 1.7.33 - csstype: 3.1.3 - fast-printf: 1.6.9 - typescript: 5.5.4 - - '@types/luxon@3.4.2': {} - - '@types/node@20.14.14': - dependencies: - undici-types: 5.26.5 - - '@ungap/structured-clone@1.2.0': {} - - acorn-jsx@5.3.2(acorn@8.12.1): - dependencies: - acorn: 8.12.1 - - acorn@8.12.1: {} - - ajv@6.12.6: - 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-regex@5.0.1: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - argparse@2.0.1: {} - - balanced-match@1.0.2: {} - - boolean@3.2.0: {} - - brace-expansion@1.1.11: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - callsites@3.1.0: {} - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - concat-map@0.0.1: {} - - cross-spawn@7.0.3: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - csstype@3.1.3: {} - - data-uri-to-buffer@4.0.1: {} - - debug@4.3.6: - dependencies: - ms: 2.1.2 - - deep-is@0.1.4: {} - - doctrine@3.0.0: - dependencies: - esutils: 2.0.3 - - esbuild@0.20.2: - optionalDependencies: - '@esbuild/aix-ppc64': 0.20.2 - '@esbuild/android-arm': 0.20.2 - '@esbuild/android-arm64': 0.20.2 - '@esbuild/android-x64': 0.20.2 - '@esbuild/darwin-arm64': 0.20.2 - '@esbuild/darwin-x64': 0.20.2 - '@esbuild/freebsd-arm64': 0.20.2 - '@esbuild/freebsd-x64': 0.20.2 - '@esbuild/linux-arm': 0.20.2 - '@esbuild/linux-arm64': 0.20.2 - '@esbuild/linux-ia32': 0.20.2 - '@esbuild/linux-loong64': 0.20.2 - '@esbuild/linux-mips64el': 0.20.2 - '@esbuild/linux-ppc64': 0.20.2 - '@esbuild/linux-riscv64': 0.20.2 - '@esbuild/linux-s390x': 0.20.2 - '@esbuild/linux-x64': 0.20.2 - '@esbuild/netbsd-x64': 0.20.2 - '@esbuild/openbsd-x64': 0.20.2 - '@esbuild/sunos-x64': 0.20.2 - '@esbuild/win32-arm64': 0.20.2 - '@esbuild/win32-ia32': 0.20.2 - '@esbuild/win32-x64': 0.20.2 - - escape-string-regexp@4.0.0: {} - - eslint-scope@7.2.2: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint@8.57.0: - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@eslint-community/regexpp': 4.11.0 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.0 - '@humanwhocodes/config-array': 0.11.14 - '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.2.0 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.6 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - esquery: 1.6.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - find-up: 5.0.0 - glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 - ignore: 5.3.1 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.0 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color - - espree@9.6.1: - dependencies: - acorn: 8.12.1 - acorn-jsx: 5.3.2(acorn@8.12.1) - eslint-visitor-keys: 3.4.3 - - esquery@1.6.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - - esutils@2.0.3: {} - - fast-deep-equal@3.1.3: {} - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - fast-printf@1.6.9: - dependencies: - boolean: 3.2.0 - - fastq@1.17.1: - dependencies: - reusify: 1.0.4 - - fetch-blob@3.2.0: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 3.3.3 - - file-entry-cache@6.0.1: - dependencies: - flat-cache: 3.2.0 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - flat-cache@3.2.0: - dependencies: - flatted: 3.3.1 - keyv: 4.5.4 - rimraf: 3.0.2 - - flatted@3.3.1: {} - - formdata-polyfill@4.0.10: - dependencies: - fetch-blob: 3.2.0 - - fs.realpath@1.0.0: {} - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - - globals@13.24.0: - dependencies: - type-fest: 0.20.2 - - graphemer@1.4.0: {} - - has-flag@4.0.0: {} - - ignore@5.3.1: {} - - import-fresh@3.3.0: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - imurmurhash@0.1.4: {} - - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.4: {} - - is-extglob@2.1.1: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-path-inside@3.0.3: {} - - isexe@2.0.0: {} - - js-yaml@4.1.0: - dependencies: - argparse: 2.0.1 - - json-buffer@3.0.1: {} - - json-schema-traverse@0.4.1: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - lodash.merge@4.6.2: {} - - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.11 - - ms@2.1.2: {} - - natural-compare@1.4.0: {} - - node-domexception@1.0.0: {} - - node-fetch@3.3.2: - dependencies: - data-uri-to-buffer: 4.0.1 - fetch-blob: 3.2.0 - formdata-polyfill: 4.0.10 - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - path-exists@4.0.0: {} - - path-is-absolute@1.0.1: {} - - path-key@3.1.1: {} - - prelude-ls@1.2.1: {} - - punycode@2.3.1: {} - - queue-microtask@1.2.3: {} - - resolve-from@4.0.0: {} - - reusify@1.0.4: {} - - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-json-comments@3.1.1: {} - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - text-table@0.2.0: {} - - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - type-fest@0.20.2: {} - - typescript@5.5.4: {} - - undici-types@5.26.5: {} - - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - - web-streams-polyfill@3.3.3: {} - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - word-wrap@1.2.5: {} - - wrappy@1.0.2: {} - - yocto-queue@0.1.0: {} diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/src/bridge/saltychat.ts b/resources/[voice]/yaca-voice/apps/yaca-server/src/bridge/saltychat.ts deleted file mode 100644 index 7c057f07b..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-server/src/bridge/saltychat.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { saltyChatExport } from '@yaca-voice/common' -import { cache } from '../utils' -import type { YaCAServerModule } from '../yaca' - -/** - * The SaltyChat bridge for the server. - */ -export class YaCAServerSaltyChatBridge { - serverModule: YaCAServerModule - - private callMap = new Map>() - - /** - * Creates an instance of the SaltyChat bridge. - * - * @param {YaCAServerModule} serverModule - The server module. - */ - constructor(serverModule: YaCAServerModule) { - this.serverModule = serverModule - - this.registerSaltyChatEvents() - - console.log('[YaCA] SaltyChat bridge loaded') - - on('onResourceStop', (resourceName: string) => { - if (cache.resource !== resourceName) { - return - } - - emit('onServerResourceStop', 'saltychat') - }) - } - - /** - * Register SaltyChat events. - */ - registerSaltyChatEvents() { - saltyChatExport('GetPlayerAlive', (netId: number) => { - this.serverModule.getPlayerAliveStatus(netId) - }) - - saltyChatExport('SetPlayerAlive', (netId: number, isAlive: boolean) => { - this.serverModule.changePlayerAliveStatus(netId, isAlive) - }) - - saltyChatExport('GetPlayerVoiceRange', (netId: number) => { - this.serverModule.getPlayerVoiceRange(netId) - }) - - saltyChatExport('SetPlayerVoiceRange', (netId: number, voiceRange: number) => { - this.serverModule.changeVoiceRange(netId, voiceRange) - }) - - saltyChatExport('AddPlayerToCall', (callIdentifier: string, playerHandle: number) => this.addPlayerToCall(callIdentifier, playerHandle)) - - saltyChatExport('AddPlayersToCall', (callIdentifier: string, playerHandles: number[]) => this.addPlayerToCall(callIdentifier, playerHandles)) - - saltyChatExport('RemovePlayerFromCall', (callIdentifier: string, playerHandle: number) => this.removePlayerFromCall(callIdentifier, playerHandle)) - - saltyChatExport('RemovePlayersFromCall', (callIdentifier: string, playerHandles: number[]) => this.removePlayerFromCall(callIdentifier, playerHandles)) - - saltyChatExport('SetPhoneSpeaker', (playerHandle: number, toggle: boolean) => { - this.serverModule.phoneModule.enablePhoneSpeaker(playerHandle, toggle) - }) - - saltyChatExport('SetPlayerRadioSpeaker', () => { - console.warn('SetPlayerRadioSpeaker is not implemented in YaCA') - }) - - saltyChatExport('GetPlayersInRadioChannel', (radioChannelName: string) => this.serverModule.radioModule.getPlayersInRadioFrequency(radioChannelName)) - - saltyChatExport('SetPlayerRadioChannel', (netId: number, radioChannelName: string, primary = true) => { - const channel = primary ? 1 : 2 - const newRadioChannelName = radioChannelName === '' ? '0' : radioChannelName - - this.serverModule.radioModule.changeRadioFrequency(netId, channel, newRadioChannelName) - }) - - saltyChatExport('RemovePlayerRadioChannel', (netId: number, primary: boolean) => { - const channel = primary ? 1 : 2 - this.serverModule.radioModule.changeRadioFrequency(netId, channel, '0') - }) - - saltyChatExport('SetRadioTowers', () => { - console.warn('SetRadioTowers is not implemented in YaCA') - }) - - saltyChatExport('EstablishCall', (callerId: number, targetId: number) => { - this.serverModule.phoneModule.callPlayer(callerId, targetId, true) - }) - - saltyChatExport('EndCall', (callerId: number, targetId: number) => { - this.serverModule.phoneModule.callPlayer(callerId, targetId, false) - }) - } - - /** - * Add a player to a call. - * - * @param callIdentifier - The call identifier. - * @param playerHandle - The player handles. - */ - addPlayerToCall(callIdentifier: string, playerHandle: number | number[]) { - if (!Array.isArray(playerHandle)) { - playerHandle = [playerHandle] - } - - const currentlyInCall = this.callMap.get(callIdentifier) ?? new Set() - const newInCall = new Set() - - for (const player of playerHandle) { - if (!currentlyInCall.has(player)) { - currentlyInCall.add(player) - newInCall.add(player) - } - } - - this.callMap.set(callIdentifier, currentlyInCall) - - for (const player of currentlyInCall) { - for (const otherPlayer of newInCall) { - if (player !== otherPlayer) { - this.serverModule.phoneModule.callPlayer(player, otherPlayer, true) - } - } - } - } - - /** - * Remove a player from a call. - * - * @param callIdentifier - The call identifier. - * @param playerHandle - The player handles. - */ - removePlayerFromCall(callIdentifier: string, playerHandle: number | number[]) { - if (!Array.isArray(playerHandle)) { - playerHandle = [playerHandle] - } - - const beforeInCall = this.callMap.get(callIdentifier) - if (!beforeInCall) { - return - } - - const nowInCall = new Set(beforeInCall) - - const removedFromCall = new Set() - for (const player of playerHandle) { - if (beforeInCall.has(player)) { - nowInCall.delete(player) - removedFromCall.add(player) - } - } - - this.callMap.set(callIdentifier, nowInCall) - - for (const player of removedFromCall) { - for (const otherPlayer of beforeInCall) { - if (player !== otherPlayer) { - this.serverModule.phoneModule.callPlayer(player, otherPlayer, false) - } - } - } - } -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/src/index.ts b/resources/[voice]/yaca-voice/apps/yaca-server/src/index.ts deleted file mode 100644 index 5794f3376..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-server/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -/// - -import { YaCAServerModule } from 'src/yaca' - -new YaCAServerModule() diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/cache.ts b/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/cache.ts deleted file mode 100644 index 80aeb2e02..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/cache.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { ServerCache } from '@yaca-voice/types' - -/** - * Cached values for the server. - */ -export const cache: ServerCache = { - resource: GetCurrentResourceName(), -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/events.ts b/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/events.ts deleted file mode 100644 index e7585c8b2..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/events.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Send a event to one or multiple clients. - * - * @param eventName - The name of the event. - * @param targetIds - The target ids. - * @param args - The arguments to send. - */ -export const triggerClientEvent = (eventName: string, targetIds: number[] | number, ...args: unknown[]) => { - if (!Array.isArray(targetIds)) { - targetIds = [targetIds] - } - - if (targetIds.length < 1) { - return - } - - // @ts-expect-error - msgpack_pack is not typed but available in the global scope. - const dataSerialized = msgpack_pack(args) - - for (const targetId of targetIds) { - TriggerClientEventInternal(eventName, targetId.toString(), dataSerialized, dataSerialized.length) - } -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/generator.ts b/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/generator.ts deleted file mode 100644 index e31c08f6e..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/generator.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { randomUUID } from 'node:crypto' - -/** - * Generate a random name and insert it into the database. - * - * @param src The ID of the player. - * @param nameSet The set of names to check against. - * @param namePattern The pattern to use for the name. - */ -export function generateRandomName(src: number, nameSet: Set, namePattern: string): string | undefined { - let name: string | undefined - - const playerName = GetPlayerName(src.toString()) - - for (let i = 0; i < 10; i++) { - let generatedName = namePattern - generatedName = generatedName.replace('{serverid}', src.toString()) - generatedName = generatedName.replace('{playername}', playerName) - generatedName = generatedName.replace('{guid}', randomUUID().replace(/-/g, '')) - generatedName = generatedName.slice(0, 30) - - if (!nameSet.has(generatedName)) { - name = generatedName - nameSet.add(generatedName) - break - } - } - - if (!name) { - console.error(`YaCA: Couldn't generate a random name for player ${playerName} (ID: ${src}).`) - } - - return name -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/index.ts b/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/index.ts deleted file mode 100644 index 4ae2ca2a3..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './cache' -export * from './events' -export * from './generator' -export * from './versioncheck' diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/versioncheck.ts b/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/versioncheck.ts deleted file mode 100644 index 41f7b30b7..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-server/src/utils/versioncheck.ts +++ /dev/null @@ -1,57 +0,0 @@ -import fetch from 'node-fetch' -import { cache } from './cache' - -/** - * Checks the version of the resource against the latest release on GitHub. - * If the resource is outdated, a message will be printed to the console. - */ -export const checkVersion = async () => { - const currentVersion = GetResourceMetadata(cache.resource, 'version', 0) - - if (!currentVersion) { - console.error('[YaCA] Version check failed, no version found in resource manifest.') - return - } - - const parsedVersion = currentVersion.match(/\d+\.\d+\.\d+/g) - - if (!parsedVersion) { - console.error('[YaCA] Version check failed, version in resource manifest is not in the correct format.') - return - } - - const response = await fetch('https://api.github.com/repos/yaca-systems/fivem-yaca-typescript/releases/latest') - if (response.status !== 200) { - console.error('[YaCA] Version check failed, unable to fetch latest release.') - return - } - - const data = (await response.json()) as { tag_name: string; html_url: string } - - const latestVersion = data.tag_name - if (!latestVersion && latestVersion === currentVersion) { - console.log('[YaCA] You are running the latest version of YaCA.') - return - } - - const parsedLatestVersion = latestVersion.match(/\d+\.\d+\.\d+/g) - if (!parsedLatestVersion) { - console.error('[YaCA] Version check failed, latest release is not in the correct format.') - return - } - - for (let i = 0; i < parsedVersion.length; i++) { - const current = Number.parseInt(parsedVersion[i]) - const latest = Number.parseInt(parsedLatestVersion[i]) - - if (current !== latest) { - if (current < latest) { - console.error( - `[YaCA] You are running an outdated version of YaCA. (current: ${currentVersion}, latest: ${latestVersion}) \r\n ${data.html_url}`, - ) - } else { - break - } - } - } -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/index.ts b/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/index.ts deleted file mode 100644 index 42a2b56f6..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './main' -export * from './megaphone' -export * from './phone' -export * from './radio' diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/main.ts b/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/main.ts deleted file mode 100644 index 0f25a8cf5..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/main.ts +++ /dev/null @@ -1,394 +0,0 @@ -import { GLOBAL_ERROR_LEVEL_STATE_NAME, getGlobalErrorLevel, initLocale, loadConfig, setGlobalErrorLevel, VOICE_RANGE_STATE_NAME } from '@yaca-voice/common' -import { - type DataObject, - defaultServerConfig, - defaultSharedConfig, - defaultTowerConfig, - type ServerCache, - type YacaServerConfig, - type YacaSharedConfig, - type YacaTowerConfig, -} from '@yaca-voice/types' -import { YaCAServerSaltyChatBridge } from '../bridge/saltychat' -import { checkVersion, generateRandomName } from '../utils' -import { triggerClientEvent } from '../utils/events' -import { YaCAServerMegaphoneModule } from './megaphone' -import { YaCAServerPhoneModle } from './phone' -import { YaCAServerRadioModule } from './radio' - -/** - * The player data type for YaCA. - */ -export type YaCAPlayer = { - voiceSettings: { - voiceFirstConnect: boolean - forceMuted: boolean - ingameName: string - mutedOnPhone: boolean - inCallWith: Set - emittedPhoneSpeaker: Map> - } - radioSettings: { - activated: boolean - hasLong: boolean - frequencies: Record - } - voicePlugin?: { - playerId: number - clientId: number - forceMuted: boolean - mutedOnPhone: boolean - } -} - -/** - * The main server module for YaCA. - */ -export class YaCAServerModule { - cache: ServerCache - - nameSet: Set = new Set() - players: Map = new Map() - - defaultVoiceRange: number - - serverConfig: YacaServerConfig - sharedConfig: YacaSharedConfig - towerConfig: YacaTowerConfig - - phoneModule: YaCAServerPhoneModle - radioModule: YaCAServerRadioModule - megaphoneModule: YaCAServerMegaphoneModule - - saltChatBridge?: YaCAServerSaltyChatBridge - - /** - * Creates an instance of the server module. - */ - constructor() { - console.log('~g~ --> YaCA: Server loaded') - - this.serverConfig = loadConfig('config/server.json5', defaultServerConfig) - this.sharedConfig = loadConfig('config/shared.json5', defaultSharedConfig) - this.towerConfig = loadConfig('config/tower.json5', defaultTowerConfig) - - initLocale(this.sharedConfig.locale) - - if (this.sharedConfig.voiceRange.ranges[this.sharedConfig.voiceRange.defaultIndex]) { - this.defaultVoiceRange = this.sharedConfig.voiceRange.ranges[this.sharedConfig.voiceRange.defaultIndex] - } else { - this.defaultVoiceRange = 1 - this.sharedConfig.voiceRange.ranges = [1] - - console.error('[YaCA] Default voice range is not set correctly in the config.') - } - - this.phoneModule = new YaCAServerPhoneModle(this) - this.radioModule = new YaCAServerRadioModule(this) - this.megaphoneModule = new YaCAServerMegaphoneModule(this) - - this.registerExports() - this.registerEvents() - - if (this.sharedConfig.saltyChatBridge) { - this.saltChatBridge = new YaCAServerSaltyChatBridge(this) - } - - if (this.sharedConfig.versionCheck) { - checkVersion().then() - } - - GlobalState.set(GLOBAL_ERROR_LEVEL_STATE_NAME, 0, true) - } - - /** - * Get the player data for a specific player. - */ - getPlayer(playerId: number): YaCAPlayer | undefined { - return this.players.get(playerId) - } - - /** - * Initialize the player on first connect. - * - * @param {number} src - The source-id of the player to initialize. - */ - connectToVoice(src: number) { - const name = generateRandomName(src, this.nameSet, this.serverConfig.userNamePattern) - if (!name) { - DropPlayer(src.toString(), '[YaCA] Failed to generate a random name.') - return - } - - const playerState = Player(src).state - playerState.set(VOICE_RANGE_STATE_NAME, this.defaultVoiceRange, true) - - this.players.set(src, { - voiceSettings: { - voiceFirstConnect: false, - forceMuted: false, - ingameName: name, - mutedOnPhone: false, - inCallWith: new Set(), - emittedPhoneSpeaker: new Map>(), - }, - radioSettings: { - activated: false, - hasLong: true, - frequencies: {}, - }, - }) - - this.connect(src) - } - - /** - * Register all exports for the YaCA module. - */ - registerExports() { - exports('connectToVoice', (src: number) => this.connectToVoice(src)) - /** - * Get the alive status of a player. - * - * @param {number} playerId - The ID of the player to get the alive status for. - * @returns {boolean} - The alive status of the player. - */ - exports('getPlayerAliveStatus', (playerId: number) => this.getPlayerAliveStatus(playerId)) - - /** - * Set the alive status of a player. - * - * @param {number} playerId - The ID of the player to set the alive status for. - * @param {boolean} state - The new alive status. - */ - exports('setPlayerAliveStatus', (playerId: number, state: boolean) => this.changePlayerAliveStatus(playerId, state)) - - /** - * Get the voice range of a player. - * - * @param {number} playerId - The ID of the player to get the voice range for. - * @returns {number} - The voice range of the player. - */ - exports('getPlayerVoiceRange', (playerId: number) => this.getPlayerVoiceRange(playerId)) - - /** - * Set the voice range of a player. - * - * @param {number} playerId - The ID of the player to set the voice range for. - */ - exports('setPlayerVoiceRange', (playerId: number, range: number) => this.changeVoiceRange(playerId, range)) - - /** - * Set the global error level. - * - * @param {number} errorLevel - The new error level. Between 0 and 1. - */ - exports('setGlobalErrorLevel', (errorLevel: number) => setGlobalErrorLevel(errorLevel)) - - /** - * Get the global error level. - * - * @returns {number} - The global error level. - */ - exports('getGlobalErrorLevel', () => getGlobalErrorLevel()) - } - - /** - * Register all events for the YaCA module. - */ - registerEvents() { - // FiveM: player dropped - on('playerDropped', (_reason: string) => { - this.handlePlayerDisconnect(source) - }) - - // YaCA: connect to voice when NUI is ready - onNet('server:yaca:nuiReady', () => { - if (!this.sharedConfig.autoConnectOnJoin) return - this.connectToVoice(source) - }) - - // YaCA:successful voice connection and client-id sync - onNet('server:yaca:addPlayer', (clientId: number) => { - this.addNewPlayer(source, clientId) - }) - - // YaCa: voice restart - onNet('server:yaca:wsReady', () => { - this.playerReconnect(source) - }) - - // TxAdmin: spectate stop event - onNet('txsv:req:spectate:end', () => { - emitNet('client:yaca:txadmin:stopspectate', source) - }) - } - - /** - * Handle various cases if player disconnects. - * - * @param {number} src - The source-id of the player who disconnected. - */ - handlePlayerDisconnect(src: number) { - const player = this.players.get(src) - if (!player) { - return - } - - this.nameSet.delete(player.voiceSettings?.ingameName) - - const allFrequencies = this.radioModule.radioFrequencyMap - for (const [key, value] of allFrequencies) { - value.delete(src) - if (!value.size) { - this.radioModule.radioFrequencyMap.delete(key) - } - } - - for (const [targetId, emitterTargets] of player.voiceSettings.emittedPhoneSpeaker) { - const target = this.players.get(targetId) - if (!target || !target.voicePlugin) { - continue - } - - triggerClientEvent('client:yaca:phoneHearAround', Array.from(emitterTargets), [target.voicePlugin.clientId], false) - } - - emitNet('client:yaca:disconnect', -1, src) - } - - /** - * Syncs player alive status and mute him if he is dead or whatever. - * - * @param {number} src - The source-id of the player to sync. - * @param {boolean} alive - The new alive status. - */ - changePlayerAliveStatus(src: number, alive: boolean) { - const player = this.players.get(src) - if (!player) { - return - } - - player.voiceSettings.forceMuted = !alive - emitNet('client:yaca:muteTarget', -1, src, !alive) - - if (player.voicePlugin) { - player.voicePlugin.forceMuted = !alive - } - } - - /** - * Get the alive status of a player. - * - * @param playerId - The ID of the player to get the alive status for. - */ - getPlayerAliveStatus(playerId: number) { - return this.players.get(playerId)?.voiceSettings.forceMuted ?? false - } - - /** - * Used if a player reconnects to the server. - * - * @param {number} src - The source-id of the player to reconnect. - */ - playerReconnect(src: number) { - const player = this.players.get(src) - if (!player) { - return - } - - if (!player.voiceSettings.voiceFirstConnect) { - return - } - - this.connect(src) - } - - /** - * Change the voice range of a player. - * - * @param {number} src - The source-id of the player to change the voice range for. - * @param {number} range - The new voice range. Defaults to the default voice range if not provided. - */ - changeVoiceRange(src: number, range?: number) { - const playerState = Player(src).state - - playerState.set(VOICE_RANGE_STATE_NAME, range ?? this.defaultVoiceRange, true) - emitNet('client:yaca:changeVoiceRange', src, range) - } - - /** - * Get the voice range of a player. - * - * @param playerId - The ID of the player to get the voice range for. - */ - getPlayerVoiceRange(playerId: number) { - const playerState = Player(playerId).state - return playerState[VOICE_RANGE_STATE_NAME] ?? this.defaultVoiceRange - } - - /** - * Sends initial data needed to connect to teamspeak plugin. - * - * @param {number} src - The source-id of the player to connect - */ - connect(src: number) { - const player = this.players.get(src) - if (!player) { - console.error(`YaCA: Missing player data for ${src}.`) - return - } - - player.voiceSettings.voiceFirstConnect = true - - const initObject: DataObject = { - suid: this.serverConfig.uniqueServerId, - chid: this.serverConfig.ingameChannelId, - deChid: this.serverConfig.defaultChannelId, - channelPassword: this.serverConfig.ingameChannelPassword, - ingameName: player.voiceSettings.ingameName, - useWhisper: this.serverConfig.useWhisper, - excludeChannels: this.serverConfig.excludeChannels, - } - emitNet('client:yaca:init', src, initObject) - } - - /** - * Add new player to all other players on connect or reconnect, so they know about some variables. - * - * @param src - The source-id of the player to add. - * @param {number} clientId - The client ID of the player. - */ - addNewPlayer(src: number, clientId: number) { - const player = this.players.get(src) - if (!player || !clientId) { - return - } - - player.voicePlugin = { - playerId: src, - clientId, - forceMuted: player.voiceSettings.forceMuted, - mutedOnPhone: player.voiceSettings.mutedOnPhone, - } - - emitNet('client:yaca:addPlayers', -1, player.voicePlugin) - - const allPlayersData = [] - for (const playerSource of getPlayers()) { - const intPlayerSource = Number.parseInt(playerSource) - const playerServer = this.players.get(intPlayerSource) - if (!playerServer) { - continue - } - - if (!playerServer.voicePlugin || intPlayerSource === src) { - continue - } - - allPlayersData.push(playerServer.voicePlugin) - } - - emitNet('client:yaca:addPlayers', src, allPlayersData) - } -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/megaphone.ts b/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/megaphone.ts deleted file mode 100644 index 2f8423d5f..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/megaphone.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { MEGAPHONE_STATE_NAME } from '@yaca-voice/common' -import type { YacaSharedConfig } from '@yaca-voice/types' -import type { YaCAServerModule } from './main' - -/** - * The server-side megaphone module. - */ -export class YaCAServerMegaphoneModule { - private serverModule: YaCAServerModule - private sharedConfig: YacaSharedConfig - - /** - * Creates an instance of the megaphone module. - * - * @param serverModule - The server module. - */ - constructor(serverModule: YaCAServerModule) { - this.serverModule = serverModule - this.sharedConfig = serverModule.sharedConfig - - this.registerEvents() - } - - /** - * Register server events. - */ - registerEvents() { - /** - * Changes megaphone state by player - * - * @param {boolean} state - The state of the megaphone effect. - */ - onNet('server:yaca:useMegaphone', (state: boolean) => { - this.playerUseMegaphone(source, state) - }) - - /** - * Handles the "server:yaca:playerLeftVehicle" event. - */ - onNet('server:yaca:playerLeftVehicle', () => { - this.changeMegaphoneState(source, false, true) - }) - } - - /** - * Apply the megaphone effect on a specific player via client event. - * - * @param {number} src - The source-id of the player to apply the megaphone effect to. - * @param {boolean} state - The state of the megaphone effect. - */ - playerUseMegaphone(src: number, state: boolean) { - const player = this.serverModule.getPlayer(src) - if (!player) { - return - } - - const playerState = Player(src).state - - if ((!state && !playerState[MEGAPHONE_STATE_NAME]) || (state && playerState[MEGAPHONE_STATE_NAME])) { - return - } - - this.changeMegaphoneState(src, state) - emit('yaca:external:changeMegaphoneState', src, state) - } - - /** - * Apply the megaphone effect on a specific player. - * - * @param {number} src - The source-id of the player to apply the megaphone effect to. - * @param {boolean} state - The state of the megaphone effect. - * @param {boolean} [forced=false] - Whether the change is forced. Defaults to false if not provided. - */ - changeMegaphoneState(src: number, state: boolean, forced = false) { - const playerState = Player(src).state - - if (!state && playerState[MEGAPHONE_STATE_NAME]) { - playerState.set(MEGAPHONE_STATE_NAME, null, true) - if (forced) { - emitNet('client:yaca:setLastMegaphoneState', src, false) - } - } else if (state && !playerState[MEGAPHONE_STATE_NAME]) { - playerState.set(MEGAPHONE_STATE_NAME, this.sharedConfig.megaphone.range, true) - } - } -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/phone.ts b/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/phone.ts deleted file mode 100644 index 62c923edf..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/phone.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { PHONE_SPEAKER_STATE_NAME } from '@yaca-voice/common' -import { YacaFilterEnum } from '@yaca-voice/types' -import { triggerClientEvent } from '../utils/events' -import type { YaCAServerModule } from './main' - -/** - * The phone module for the server. - */ -export class YaCAServerPhoneModle { - private serverModule: YaCAServerModule - - /** - * Creates an instance of the phone module. - * - * @param {YaCAServerModule} serverModule - The server module. - */ - constructor(serverModule: YaCAServerModule) { - this.serverModule = serverModule - - this.registerEvents() - this.registerExports() - } - - /** - * Register server events. - */ - registerEvents() { - /** - * Handles the "server:yaca:phoneSpeakerEmitWhisper" event. - * - * @param {number[]} enableForTargets - The IDs of the players to enable the phone speaker for. - * @param {number[]} disableForTargets - The IDs of the players to disable the phone speaker for. - */ - onNet('server:yaca:phoneSpeakerEmitWhisper', (enableForTargets?: number[], disableForTargets?: number[]) => { - const player = this.serverModule.players.get(source) - if (!player) { - return - } - - const targets = new Set() - - for (const callTarget of player.voiceSettings.inCallWith) { - const target = this.serverModule.players.get(callTarget) - if (!target) { - continue - } - - targets.add(callTarget) - } - - if (targets.size && enableForTargets?.length) { - triggerClientEvent('client:yaca:playersToPhoneSpeakerEmitWhisper', Array.from(targets), enableForTargets, true) - } - - if (targets.size && disableForTargets?.length) { - triggerClientEvent('client:yaca:playersToPhoneSpeakerEmitWhisper', Array.from(targets), disableForTargets, false) - } - }) - - /** - * Handles the "server:yaca:phoneEmit" event. - * - * @param {number[]} enableForTargets - The IDs of the players to enable the phone speaker for. - * @param {number[]} disableForTargets - The IDs of the players to disable the phone speaker for. - */ - onNet('server:yaca:phoneEmit', (enableForTargets?: number[], disableForTargets?: number[]) => { - if (!this.serverModule.sharedConfig.phoneHearPlayersNearby) return - - const player = this.serverModule.players.get(source) - if (!player) { - return - } - - const enableReceive = new Set() - const disableReceive = new Set() - - if (enableForTargets?.length) { - for (const callTarget of player.voiceSettings.inCallWith) { - const target = this.serverModule.players.get(callTarget) - if (!target) continue - - enableReceive.add(callTarget) - - for (const targetID of enableForTargets) { - const map = player.voiceSettings.emittedPhoneSpeaker - const set = map.get(targetID) ?? new Set() - set.add(callTarget) - map.set(targetID, set) - } - } - } - - if (disableForTargets?.length) { - for (const targetID of disableForTargets) { - const emittedFor = player.voiceSettings.emittedPhoneSpeaker.get(targetID) - if (!emittedFor) continue - - for (const emittedTarget of emittedFor) { - const target = this.serverModule.players.get(emittedTarget) - if (!target) continue - - disableReceive.add(emittedTarget) - } - - player.voiceSettings.emittedPhoneSpeaker.delete(targetID) - } - } - - if (enableReceive.size && enableForTargets?.length) { - const enableForTargetsData = new Set() - - for (const enableTarget of enableForTargets) { - const target = this.serverModule.players.get(enableTarget) - if (!target || !target.voicePlugin) continue - - enableForTargetsData.add(target.voicePlugin.clientId) - } - - triggerClientEvent('client:yaca:phoneHearAround', Array.from(enableReceive), Array.from(enableForTargetsData), true) - } - - if (disableReceive.size && disableForTargets?.length) { - const disableForTargetsData = new Set() - - for (const disableTarget of disableForTargets) { - const target = this.serverModule.players.get(disableTarget) - if (!target || !target.voicePlugin) continue - - disableForTargetsData.add(target.voicePlugin.clientId) - } - - triggerClientEvent('client:yaca:phoneHearAround', Array.from(disableReceive), Array.from(disableForTargetsData), false) - } - }) - } - - registerExports() { - /** - * Creates a phone call between two players. - * - * @param {number} src - The player who is making the call. - * @param {number} target - The player who is being called. - * @param {boolean} state - The state of the call. - */ - exports('callPlayer', (src: number, target: number, state: boolean) => this.callPlayer(src, target, state)) - - /** - * Creates a phone call between two players with the old effect. - * - * @param {number} src - The player who is making the call. - * @param {number} target - The player who is being called. - * @param {boolean} state - The state of the call. - */ - exports('callPlayerOldEffect', (src: number, target: number, state: boolean) => this.callPlayer(src, target, state, YacaFilterEnum.PHONE_HISTORICAL)) - - /** - * Mute a player during a phone call. - * - * @param {number} src - The source-id of the player to mute. - * @param {boolean} state - The mute state. - */ - exports('muteOnPhone', (src: number, state: boolean) => this.muteOnPhone(src, state)) - - /** - * Enable or disable the phone speaker for a player. - * - * @param {number} src - The source-id of the player to enable the phone speaker for. - * @param {boolean} state - The state of the phone speaker. - */ - exports('enablePhoneSpeaker', (src: number, state: boolean) => this.enablePhoneSpeaker(src, state)) - - /** - * Is player in a phone call. - * - * @param {number} src - The source-id of the player to check. - */ - exports('isPlayerInCall', (src: number): [boolean, number[]] => { - const player = this.serverModule.players.get(src) - if (!player) { - return [false, []] - } - - return [player.voiceSettings.inCallWith.size > 0, [...player.voiceSettings.inCallWith]] - }) - } - - /** - * Call another player. - * - * @param {number} src - The player who is making the call. - * @param {number} target - The player who is being called. - * @param {boolean} state - The state of the call. - * @param {YacaFilterEnum} filter - The filter to use for the call. Defaults to PHONE if not provided. - */ - callPlayer(src: number, target: number, state: boolean, filter: YacaFilterEnum = YacaFilterEnum.PHONE) { - const player = this.serverModule.getPlayer(src) - const targetPlayer = this.serverModule.getPlayer(target) - if (!player || !targetPlayer) { - return - } - - emitNet('client:yaca:phone', target, src, state, filter) - emitNet('client:yaca:phone', src, target, state, filter) - - const playerState = Player(src).state - const targetState = Player(target).state - - if (state) { - player.voiceSettings.inCallWith.add(target) - targetPlayer.voiceSettings.inCallWith.add(src) - - if (playerState[PHONE_SPEAKER_STATE_NAME]) { - this.enablePhoneSpeaker(src, true) - } - - if (targetState[PHONE_SPEAKER_STATE_NAME]) { - this.enablePhoneSpeaker(target, true) - } - } else { - this.muteOnPhone(src, false, true) - this.muteOnPhone(target, false, true) - - player.voiceSettings.inCallWith.delete(target) - targetPlayer.voiceSettings.inCallWith.delete(src) - - if (playerState[PHONE_SPEAKER_STATE_NAME]) { - this.enablePhoneSpeaker(src, false) - } - - if (targetState[PHONE_SPEAKER_STATE_NAME]) { - this.enablePhoneSpeaker(target, false) - } - } - - emit('yaca:external:phoneCall', src, target, state, filter) - } - - /** - * Mute a player during a phone call. - * - * @param {number} src - The source-id of the player to mute. - * @param {boolean} state - The mute state. - * @param {boolean} [onCallStop=false] - Whether the call has stopped. Defaults to false if not provided. - */ - muteOnPhone(src: number, state: boolean, onCallStop = false) { - const player = this.serverModule.getPlayer(src) - if (!player) { - return - } - - player.voiceSettings.mutedOnPhone = state - emitNet('client:yaca:phoneMute', -1, src, state, onCallStop) - emit('yaca:external:phoneMute', src, state) - } - - /** - * Enable or disable the phone speaker for a player. - * - * @param {number} src - The source-id of the player to enable the phone speaker for. - * @param {boolean} state - The state of the phone speaker. - */ - enablePhoneSpeaker(src: number, state: boolean) { - const player = this.serverModule.getPlayer(src) - if (!player) { - return - } - - const playerState = Player(src).state - - if (state && player.voiceSettings.inCallWith.size) { - playerState.set(PHONE_SPEAKER_STATE_NAME, Array.from(player.voiceSettings.inCallWith), true) - emit('yaca:external:phoneSpeaker', src, true) - } else { - playerState.set(PHONE_SPEAKER_STATE_NAME, null, true) - emit('yaca:external:phoneSpeaker', src, false) - } - } -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/radio.ts b/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/radio.ts deleted file mode 100644 index cb3679a7b..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-server/src/yaca/radio.ts +++ /dev/null @@ -1,379 +0,0 @@ -import { locale } from '@yaca-voice/common' -import { YacaNotificationType, type YacaServerConfig, type YacaSharedConfig } from '@yaca-voice/types' -import { triggerClientEvent } from '../utils/events' -import type { YaCAServerModule } from './main' - -/** - * The server-side radio module. - */ -export class YaCAServerRadioModule { - private serverModule: YaCAServerModule - private sharedConfig: YacaSharedConfig - private serverConfig: YacaServerConfig - - radioFrequencyMap = new Map>() - - /** - * Creates an instance of the radio module. - * - * @param {YaCAServerModule} serverModule - The server module. - */ - constructor(serverModule: YaCAServerModule) { - this.serverModule = serverModule - this.sharedConfig = serverModule.sharedConfig - this.serverConfig = serverModule.serverConfig - - this.registerEvents() - this.registerExports() - } - - /** - * Register server events. - */ - registerEvents() { - /** - * Handles the "server:yaca:enableRadio" server event. - * - * @param {boolean} state - The state of the radio. - */ - onNet('server:yaca:enableRadio', (state: boolean) => { - this.enableRadio(source, state) - }) - - /** - * Handles the "server:yaca:changeRadioFrequency" server event. - * - * @param {number} channel - The channel to change the frequency of. - * @param {string} frequency - The new frequency. - */ - onNet('server:yaca:changeRadioFrequency', (channel: number, frequency: string) => { - this.changeRadioFrequency(source, channel, frequency) - }) - - /** - * Handles the "server:yaca:muteRadioChannel" server event. - * - * @param {number} channel - The channel to mute. - */ - onNet('server:yaca:muteRadioChannel', (channel: number, state?: boolean) => { - this.radioChannelMute(source, channel, state) - }) - - /** - * Handles the "server:yaca:radioTalking" server event. - * - * @param {boolean} state - The state of the radio. - * @param {number} channel - The channel to change the talking state for. - * @param {number} distanceToTower - The distance to the tower. - */ - onNet('server:yaca:radioTalking', (state: boolean, channel: number, distanceToTower = -1) => { - this.radioTalkingState(source, state, channel, distanceToTower) - }) - } - - /** - * Register server exports. - */ - registerExports() { - /** - * Get all players in a radio frequency. - * - * @param {string} frequency - The frequency to get the players for. - * @returns {number[]} - The players in the radio frequency. - */ - exports('getPlayersInRadioFrequency', (frequency: string) => this.getPlayersInRadioFrequency(frequency)) - - /** - * Set the radio channel for a player. - * - * @param {number} src - The player to set the radio channel for. - * @param {number} channel - The channel to set. - * @param {string} frequency - The frequency to set. - */ - exports('setPlayerRadioChannel', (src: number, channel: number, frequency: string) => this.changeRadioFrequency(src, channel, frequency)) - - /** - * Get if a player has long range radio. - * - * @param {number} src - The player to set the long range radio for. - */ - exports('getPlayerHasLongRange', (src: number) => this.getPlayerHasLongRange(src)) - - /** - * Set if a player has long range radio. - * - * @param {number} src - The player to set the long range radio for. - * @param {boolean} state - The new state of the long range radio. - */ - exports('setPlayerHasLongRange', (src: number, state: boolean) => this.setPlayerHasLongRange(src, state)) - } - - /** - * Get all players in a radio frequency. - * - * @param frequency - The frequency to get the players for. - */ - getPlayersInRadioFrequency(frequency: string) { - const allPlayersInChannel = this.radioFrequencyMap.get(frequency) - const playersArray: number[] = [] - - if (!allPlayersInChannel) { - return playersArray - } - - for (const [key] of allPlayersInChannel) { - const target = this.serverModule.getPlayer(key) - if (!target) { - continue - } - playersArray.push(key) - } - return playersArray - } - - /** - * Gets if a player has long range radio. - * - * @param src - The player to get the long range radio for. - */ - getPlayerHasLongRange(src: number) { - const player = this.serverModule.getPlayer(src) - if (!player) { - return false - } - - return player.radioSettings.hasLong - } - - /** - * Sets if a player has long range radio. - * - * @param src - The player to set the long range radio for. - * @param state - The new state of the long range radio. - */ - setPlayerHasLongRange(src: number, state: boolean) { - const player = this.serverModule.getPlayer(src) - if (!player) { - return - } - - player.radioSettings.hasLong = state - } - - /** - * Enable or disable the radio for a player. - * - * @param {number} src - The player to enable or disable the radio for. - * @param {boolean} state - The new state of the radio. - */ - enableRadio(src: number, state: boolean) { - const player = this.serverModule.getPlayer(src) - if (!player) { - return - } - - player.radioSettings.activated = state - - emit('yaca:export:enabledRadio', src, state) - } - - /** - * Change the radio frequency for a player. - * - * @param {number} src - The player to change the radio frequency for. - * @param {number} channel - The channel to change the frequency of. - * @param {string} frequency - The new frequency. - */ - changeRadioFrequency(src: number, channel: number, frequency: string) { - const player = this.serverModule.getPlayer(src) - if (!player) { - return - } - - if (!player.radioSettings.activated) { - emitNet('client:yaca:notification', src, locale('radio_not_activated'), YacaNotificationType.ERROR) - return - } - - if (Number.isNaN(channel) || channel < 1 || channel > this.sharedConfig.radioSettings.channelCount) { - emitNet('client:yaca:notification', src, locale('radio_channel_invalid'), YacaNotificationType.ERROR) - return - } - - const oldFrequency = player.radioSettings.frequencies[channel] - - // Leave the old frequency if the new one is 0 - if (frequency === '0') { - this.leaveRadioFrequency(src, channel, oldFrequency) - return - } - - // Leave the old frequency if it's different from the new one - if (oldFrequency !== frequency) { - this.leaveRadioFrequency(src, channel, oldFrequency) - } - - // Add player to channel map, so we know who is in which channel - if (!this.radioFrequencyMap.has(frequency)) { - this.radioFrequencyMap.set(frequency, new Map()) - } - this.radioFrequencyMap.get(frequency)?.set(src, { muted: false }) - - player.radioSettings.frequencies[channel] = frequency - - emitNet('client:yaca:setRadioFreq', src, channel, frequency) - emit('yaca:external:changedRadioFrequency', src, channel, frequency) - - /* - * TODO: Add radio effect to player in new frequency - * const newPlayers = this.getPlayersInRadioFrequency(frequency); - * if (newPlayers.length) alt.emitClientRaw(newPlayers, "client:yaca:setRadioEffectInFrequency", frequency, player.id); - */ - } - - /** - * Make a player leave a radio frequency. - * - * @param {number} src - The player to leave the radio frequency. - * @param {number} channel - The channel to leave. - * @param {string} frequency - The frequency to leave. - */ - leaveRadioFrequency(src: number, channel: number, frequency: string) { - const player = this.serverModule.getPlayer(src) - if (!player) { - return - } - - const allPlayersInChannel = this.radioFrequencyMap.get(frequency) - if (!allPlayersInChannel) { - return - } - - player.radioSettings.frequencies[channel] = '0' - - const playersArray = [] - const allTargets = [] - for (const [key] of allPlayersInChannel) { - const target = this.serverModule.getPlayer(key) - if (!target) { - continue - } - - playersArray.push(key) - - if (key === src) { - continue - } - - allTargets.push(key) - } - - if (this.serverConfig.useWhisper) { - emitNet('client:yaca:radioTalking', src, allTargets, frequency, false, null, true) - } else if (player.voicePlugin) { - triggerClientEvent('client:yaca:leaveRadioChannel', playersArray, player.voicePlugin.clientId, frequency) - } - - allPlayersInChannel.delete(src) - if (!allPlayersInChannel.size) { - this.radioFrequencyMap.delete(frequency) - } - } - - /** - * Mute a radio channel for a player. - * - * @param {number} src - The player to mute the radio channel for. - * @param {number} channel - The channel to mute. - */ - radioChannelMute(src: number, channel: number, state?: boolean) { - const player = this.serverModule.getPlayer(src) - if (!player) { - return - } - - const radioFrequency = player.radioSettings.frequencies[channel] - const foundPlayer = this.radioFrequencyMap.get(radioFrequency)?.get(src) - if (!foundPlayer) { - return - } - - foundPlayer.muted = typeof state !== 'undefined' ? state : !foundPlayer.muted - emitNet('client:yaca:setRadioMuteState', src, channel, foundPlayer.muted) - emit('yaca:external:changedRadioMuteState', src, channel, foundPlayer.muted) - } - - /** - * Change the talking state of a player on the radio. - * - * @param {number} src - The player to change the talking state for. - * @param {boolean} state - The new talking state. - * @param {number} channel - The channel to change the talking state for. - * @param {number} distanceToTower - The distance to the tower. - */ - radioTalkingState(src: number, state: boolean, channel: number, distanceToTower: number) { - const player = this.serverModule.getPlayer(src) - if (!player || !player.radioSettings.activated) { - return - } - - const radioFrequency = player.radioSettings.frequencies[channel] - if (!radioFrequency || radioFrequency === '0') { - return - } - - const getPlayers = this.radioFrequencyMap.get(radioFrequency) - if (!getPlayers) { - return - } - - let targets: number[] = [] - const targetsToSender: number[] = [] - const radioInfos: Record = {} - - for (const [key, values] of getPlayers) { - if (values.muted) { - if (key === src) { - targets = [] - break - } - continue - } - - if (key === src) { - continue - } - - const target = this.serverModule.getPlayer(key) - if (!target || !target.radioSettings.activated) { - continue - } - - const shortRange = !player.radioSettings.hasLong && !target.radioSettings.hasLong - if ((player.radioSettings.hasLong && target.radioSettings.hasLong) || shortRange) { - targets.push(key) - - radioInfos[key] = { - shortRange, - } - - targetsToSender.push(key) - } - } - - triggerClientEvent( - 'client:yaca:radioTalking', - targets, - src, - radioFrequency, - state, - radioInfos, - distanceToTower, - GetEntityCoords(GetPlayerPed(src.toString())), - ) - - if (this.serverConfig.useWhisper) { - emitNet('client:yaca:radioTalkingWhisper', src, targetsToSender, radioFrequency, state, GetEntityCoords(GetPlayerPed(src.toString()))) - } - } -} diff --git a/resources/[voice]/yaca-voice/apps/yaca-server/tsconfig.json b/resources/[voice]/yaca-voice/apps/yaca-server/tsconfig.json deleted file mode 100644 index f620556a3..000000000 --- a/resources/[voice]/yaca-voice/apps/yaca-server/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "@yaca-voice/typescript-config/fivem.json", - "compilerOptions": { - "baseUrl": "./", - "rootDir": "./src", - "types": ["@types/node", "@citizenfx/server"] - }, - "include": ["./"] -} diff --git a/resources/[voice]/yaca-voice/assets/yaca-voice/config/server.json5 b/resources/[voice]/yaca-voice/assets/yaca-voice/config/server.json5 deleted file mode 100644 index c849cc5f2..000000000 --- a/resources/[voice]/yaca-voice/assets/yaca-voice/config/server.json5 +++ /dev/null @@ -1,17 +0,0 @@ -{ - "uniqueServerId": "", // The unique Server Identifier of the Teamspeak-Server - "ingameChannelId": 3, // The ID of the Ingame Channel - "ingameChannelPassword": "", // The Password used to join the Ingame Channel - "defaultChannelId": 1, // The ID of the Channel where a players should be moved to when leaving Ingame - "useWhisper": false, // If you want to use the Whisper functions of TeamSpeak, if set to false it mutes and unmutes the players - "excludeChannels": [], // The channels that should be able to join while being Ingame without instantly being moved back into the Ingame channel - - /* The pattern that is used to generate the username. - * - * Following placeholders will be replaced: - * - {serverid} with the Ingame-ID of the player - * - {playername} with the steam/fivem name of the player - * - {guid} with a string containing random letters and digits. - */ - "userNamePattern": "[{serverid}] {guid}" -} diff --git a/resources/[voice]/yaca-voice/assets/yaca-voice/config/shared.json5 b/resources/[voice]/yaca-voice/assets/yaca-voice/config/shared.json5 deleted file mode 100644 index 53351558e..000000000 --- a/resources/[voice]/yaca-voice/assets/yaca-voice/config/shared.json5 +++ /dev/null @@ -1,174 +0,0 @@ -{ - // Enable the version check to get notified about new versions. - "versionCheck": true, - // Enable manual or automatic connecting to the voice channel when joining the server. - "autoConnectOnJoin": true, - // The build type of the plugin. 0 = release, 1 = develop (develop allows using all yaca plugin version) - "buildType": 0, - // The locale that should be used. - "locale": "en", - // The time before the teamspeak client is being unmuted after joining the ingame channel. - "unmuteDelay": 400, - // The range in which you can hear the phone speaker when active. - "maxPhoneSpeakerRange": 5, - /* Choose if players near other players that are in a call, should be heard by the players on the opposite side of the call. - * - * Following options are available: - * - false: Disable the feature completely - * - true: Enable the feature always - * - "PHONE_SPEAKER": Enable the feature only when the player on the phone has the phone speaker enabled. - */ - "phoneHearPlayersNearby": false, - // Choose which notifications should be enabled. - "notifications": { - // Enable notifications from ox_lib - "oxLib": false, - // Enable notifications from okokNotify - "okoknotify": false, - // Enable notifications from GTA (FiveM only) - "gta": true, - // Enable notifications from Rdr 2 (RedM only) - "redm": false, - // Enable the option to implement a custom notification - "own": false - }, - // Set the default key binds for the plugin, which can be then changed by the player. - "keyBinds": { - // The key to increase the voice range - "increaseVoiceRange": "ADD", - // The key to decrease the voice range - "decreaseVoiceRange": "SUBTRACT", - // The key to transmit on the primary radio - "primaryRadioTransmit": "N", - // The key to transmit on the secondary radio - "secondaryRadioTransmit": "CAPITAL", - // The key to use the megaphone - "megaphone": "B", - // The key to hold additional to change voice range via mousewheel by 1 meter, false to disable that feature - "voiceRangeWithMouseWheel": "LCONTROL" - }, - "radioSettings": { - // Customize radio animation - "animation": { - "dictionary": "random@arrests", - "name": "generic_radio_chatter", - "flag": 49 - }, - "propWhileTalking": { - // The prop that should be shown while talking on the radio. - "prop": false, - // The bone that the prop should be attached to. - "boneId": 28422, - // The position of the prop. - "position": [ - 0.0, - 0.0, - 0.0 - ], - // The rotation of the prop. - "rotation": [ - 0.0, - 0.0, - 0.0 - ] - }, - // The maximum amount of radio channels that can be used. - "channelCount": 9, - /* Choose the mode of the radio system. - * - * Following options are available: - * - "Tower": The radio system is based on towers, which means that the range is limited by the distance to the next tower. - * - "Direct": The radio system is based on the distance between the players. - * - "None": The radio always works no matter the distance. - */ - "mode": "Tower", - // The maximum distance between two players or to the tower to be able to hear each other. - "maxDistance": 5000 - }, - "voiceRange": { - // The default index which should be used for the voice range when a player joins the server. - "defaultIndex": 1, - // The ranges that should be available for the players. - "ranges": [ - 1, - 3, - 8, - 15, - 20, - 25, - 30, - 40 - ], - // Choose if a notification should be sent when the voice range is changed. - "sendNotification": false, - "markerColor": { - // Choose if the marker should be enabled. - "enabled": true, - // The color of the marker, r = red, g = green, b = blue, a = alpha - "r": 0, - "g": 255, - "b": 0, - "a": 50, - // The duration the marker should be shown. - "duration": 1000, - "type": 1, - "rotate": true - } - }, - "megaphone": { - // The range of the megaphone. - "range": 30, - // Choose if the plugin should automatically detect if the player should be able to use the megaphone in the vehicle. (FiveM only) - "automaticVehicleDetection": true, - // The allowed vehicle classes for the megaphone. (FiveM only) - "allowedVehicleClasses": [ - 18, - 19 - ], - // The allowed vehicle models for the megaphone. (FiveM only) - "allowedVehicleModels": [ - "polmav" - ] - }, - // Choose if the saltyChatBridge should be enabled. - "saltyChatBridge": false, - "mufflingSettings": { - // If set to -1, the player voice range is used, all values >= 0 sets the muffling range before it gets completely cut off - "mufflingRange": -1, - "vehicleMuffling": { - // If set to true, the vehicle muffling feature is enabled. (FiveM only) - "enabled": true, - // Whitelist of vehicles that should be not be affected by the vehicle muffling. (FiveM only) - "vehicleWhitelist": [ - "gauntlet6", - "draugur", - "bodhi2", - "vagrant", - "outlaw", - "trophytruck", - "ratel", - "drifttampa", - "sm722", - "tornado4", - "swinger", - "locust", - "hotring" - ], - }, - // The intensities of the muffling. (0 = no muffling, 10 = full muffling) - "intensities": { - // The intensity when the players are in different rooms. - "differentRoom": 10, - // The intensity when both cars are closed. (FiveM only) - "bothCarsClosed": 10, - // The intensity when one car is closed. (FiveM only) - "oneCarClosed": 6, - // The intensity of muffling of the megaphone of a player in a different car. (FiveM only) - "megaPhoneInCar": 6 - } - }, - // Cooldown in milliseconds which the player has to wait to use the radio again, defaults to false which disables the feature. - "radioAntiSpamCooldown": false, - // When set to true the plugin syncs the talk state via the plugin, instead of the default way via statebags. This imitates the way how saltychat syncs the talk state, but has some drawbacks. - "useLocalLipSync": false -} diff --git a/resources/[voice]/yaca-voice/assets/yaca-voice/config/towers.json5 b/resources/[voice]/yaca-voice/assets/yaca-voice/config/towers.json5 deleted file mode 100644 index 70831c2d3..000000000 --- a/resources/[voice]/yaca-voice/assets/yaca-voice/config/towers.json5 +++ /dev/null @@ -1,519 +0,0 @@ -{ - "towerPositions": [ - [ - 2572, - 5397, - 56 - ], - [ - 2663, - 4972, - 56 - ], - [ - 2892, - 3911, - 56 - ], - [ - 2720, - 3304, - 64 - ], - [ - 2388, - 2949, - 64 - ], - [ - 1830, - 2368, - 64 - ], - [ - 1650, - 1316, - 102 - ], - [ - 1363, - 680, - 102 - ], - [ - 918, - 230, - 92 - ], - [ - 567, - 303, - 58 - ], - [ - -47, - -666, - 74 - ], - [ - -585, - -902, - 53 - ], - [ - 2572, - 5397, - 56 - ], - [ - 2338, - 5940, - 77 - ], - [ - 1916, - 6244, - 65 - ], - [ - 1591, - 6371, - 42 - ], - [ - 953, - 6504, - 42 - ], - [ - 76, - 6606, - 42 - ], - [ - 408, - 6587, - 42 - ], - [ - -338, - -579, - 48 - ], - [ - -293, - -632, - 47 - ], - [ - -269, - -962, - 143 - ], - [ - 98, - -870, - 136 - ], - [ - -214, - -744, - 219 - ], - [ - -166, - -590, - 199 - ], - [ - 124, - -654, - 261 - ], - [ - 149, - -769, - 261 - ], - [ - 580, - 89, - 117 - ], - [ - 423, - 15, - 151 - ], - [ - 424, - 18, - 151 - ], - [ - 551, - -28, - 93 - ], - [ - 305, - -284, - 68 - ], - [ - 299, - -313, - 68 - ], - [ - 1240, - -1090, - 44 - ], - [ - -418, - -2804, - 14 - ], - [ - 802, - -2996, - 27 - ], - [ - 253, - -3145, - 39 - ], - [ - 207, - -3145, - 39 - ], - [ - 207, - -3307, - 39 - ], - [ - 247, - -3307, - 39 - ], - [ - 484, - -2178, - 40 - ], - [ - 548, - -2219, - 67 - ], - [ - -701, - 58, - 68 - ], - [ - -696, - 208, - 139 - ], - [ - -769, - 255, - 134 - ], - [ - -150, - -150, - 96 - ], - [ - -202, - -327, - 65 - ], - [ - -1913, - -3031, - 22 - ], - [ - -1918, - -3028, - 22 - ], - [ - -1039, - -2385, - 27 - ], - [ - -1042, - -2390, - 27 - ], - [ - -1583, - -3216, - 28 - ], - [ - -1590, - -3212, - 28 - ], - [ - -1308, - -2626, - 36 - ], - [ - -1311, - -2624, - 36 - ], - [ - -984, - -2778, - 48 - ], - [ - -991, - -2774, - 48 - ], - [ - -556, - -119, - 50 - ], - [ - -619, - -106, - 51 - ], - [ - -1167, - -575, - 40 - ], - [ - -1152, - -443, - 42 - ], - [ - -1156, - -498, - 49 - ], - [ - -1290, - -445, - 106 - ], - [ - -928, - -383, - 135 - ], - [ - -902, - -443, - 170 - ], - [ - -770, - -786, - 83 - ], - [ - -824, - -719, - 120 - ], - [ - -598, - -917, - 35 - ], - [ - -678, - -717, - 54 - ], - [ - -669, - -804, - 31 - ], - [ - -1463, - -526, - 83 - ], - [ - -1525, - -596, - 66 - ], - [ - -1375, - -465, - 83 - ], - [ - -1711, - 478, - 127 - ], - [ - -2311, - 335, - 187 - ], - [ - -2214, - 342, - 198 - ], - [ - -2234, - 187, - 193 - ], - [ - 202, - 1204, - 230 - ], - [ - 217, - 1140, - 230 - ], - [ - 668, - 590, - 136 - ], - [ - 722, - 562, - 134 - ], - [ - 838, - 510, - 138 - ], - [ - 773, - 575, - 138 - ], - [ - 735, - 231, - 145 - ], - [ - 450, - 5566, - 795 - ], - [ - -449, - 6019, - 35 - ], - [ - -142, - 6286, - 39 - ], - [ - -368, - 6105, - 38 - ], - [ - 2792, - 5996, - 355 - ], - [ - 2796, - 5992, - 354 - ], - [ - 3460, - 3653, - 51 - ], - [ - 3459, - 3659, - 51 - ], - [ - 3615, - 3642, - 51 - ], - [ - 3614, - 3636, - 51 - ], - [ - -2180, - 3252, - 54 - ], - [ - -2124, - 3219, - 54 - ], - [ - -2050, - 3178, - 54 - ], - [ - 1858, - 3694, - 37 - ], - [ - 1695, - 3614, - 37 - ], - [ - 1692, - 2532, - 60 - ], - [ - 1692, - 2647, - 60 - ], - [ - 1824, - 2574, - 60 - ], - [ - 1407, - 2117, - 104 - ] - ] -} \ No newline at end of file diff --git a/resources/[voice]/yaca-voice/assets/yaca-voice/locales/de.json b/resources/[voice]/yaca-voice/assets/yaca-voice/locales/de.json deleted file mode 100644 index 8bac3117b..000000000 --- a/resources/[voice]/yaca-voice/assets/yaca-voice/locales/de.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "connect_error": "Fehler beim Verbinden zum Teamspeak-Server, bitte verbinde dich erneut!", - "plugin_not_initialized": "Das Voice-Plugin wurde noch nicht initialisiert!", - - "outdated_plugin": "Dein Voice-Plugin ist veraltet! Bitte aktualisiere auf die Version %s!", - "wrong_ts_server": "Du bist auf dem falschen Teamspeak-Server!", - "move_error": "Fehler beim Verschieben auf den Teamspeak-Server!", - "max_players_reached": "Ihre Lizenz hat die maximale Spieleranzahl erreicht. Bitte erweitern Sie Ihre Lizenz.", - "license_server_timed_out": "Die Verbindung zum Lizenzserver wurde beim Überprüfen der Lizenz unterbrochen. Bitte warte einen Moment.", - "unknown_error": "Unbekannter Fehlercode: %s", - - "changed_stereo_mode": "Kanal %s ist jetzt auf %s zu hören.", - "left_ear": "dem linken Ohr", - "right_ear": "dem rechten Ohr", - "both_ears": "beiden Ohren", - "secondary_radio_channel_disabled": "Der sekundäre Funkkanal wurde deaktiviert.", - "secondary_radio_channel_enabled": "Kanal %s ist nun der Sekundäre Funkkanal.", - - "use_megaphone": "Megaphon benutzen", - "use_radio": "Im primären Funkkanal senden", - "use_secondary_radio": "Im sekundären Funkkanal senden", - - "use_salty_primary_radio": "Primäres Funkgerät benutzen", - "use_salty_secondary_radio": "Sekundäres Funkgerät benutzen", - - "change_voice_range_increase": "Sprachreichweite erhöhen", - "change_voice_range_decrease": "Sprachreichweite verringern", - "voice_range_changed": "Sprachreichweite auf %s Meter geändert.", - "change_voice_range_via_mousewheel": "Sprachreichweite je 1 Meter ändern", - - "radio_not_activated": "Das Funkgerät ist nicht aktiviert!", - "radio_channel_invalid": "Ungültiger Funkkanal!" -} diff --git a/resources/[voice]/yaca-voice/assets/yaca-voice/locales/en.json b/resources/[voice]/yaca-voice/assets/yaca-voice/locales/en.json deleted file mode 100644 index c0318f949..000000000 --- a/resources/[voice]/yaca-voice/assets/yaca-voice/locales/en.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "connect_error": "Error while connecting to voice server, please reconnect!", - "plugin_not_initialized": "Plugin not initialized!", - - "outdated_plugin": "Your plugin is outdated, please update to version %s!", - "wrong_ts_server": "You are connected to the wrong teamspeak server!", - "move_error": "Error while moving to the channel!", - "max_players_reached": "Your license reached the maximum player count. Please upgrade your license", - "license_server_timed_out": "The connection to the license server timed out, while verifying the license. Please wait a moment.", - "unknown_error": "Unknown error code: %s", - - "changed_stereo_mode": "Channel %s is now only heard in %s.", - "left_ear": "the left ear", - "right_ear": "the right ear", - "both_ears": "both ears", - "secondary_radio_channel_disabled": "The secondary radio channel has been disabled.", - "secondary_radio_channel_enabled": "Channel %s is now the secondary radio channel.", - - "use_megaphone": "Use megaphone", - "use_radio": "Use radio", - "use_secondary_radio": "Use secondary radio", - - "use_salty_primary_radio": "Use primary radio", - "use_salty_secondary_radio": "Use secondary radio", - - "change_voice_range_increase": "Increase voice range", - "change_voice_range_decrease": "Decrease voice range", - "voice_range_changed": "Voice range changed to %s meters.", - "change_voice_range_via_mousewheel": "Change voice range by 1 meter", - - "radio_not_activated": "Your radio is not activated!", - "radio_channel_invalid": "The radio channel is invalid!" -} diff --git a/resources/[voice]/yaca-voice/assets/yaca-voice/web/index.html b/resources/[voice]/yaca-voice/assets/yaca-voice/web/index.html deleted file mode 100644 index a8d251332..000000000 --- a/resources/[voice]/yaca-voice/assets/yaca-voice/web/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - YACA WebSocket - - - - - - - - \ No newline at end of file diff --git a/resources/[voice]/yaca-voice/assets/yaca-voice/web/script.js b/resources/[voice]/yaca-voice/assets/yaca-voice/web/script.js deleted file mode 100644 index 3ca63e39c..000000000 --- a/resources/[voice]/yaca-voice/assets/yaca-voice/web/script.js +++ /dev/null @@ -1,85 +0,0 @@ -let webSocket = null - -/** - * Connect to the YaCA voice plugin - */ -function connect() { - console.log('[YaCA-Websocket] Trying to Connect to YaCA WebSocket...') - - try { - webSocket = new window.WebSocket('ws://127.0.0.1:30125/') - } catch { - connect() - } - - webSocket.onmessage = (event) => { - if (!event) return - sendNuiData('YACA_OnMessage', event.data) - } - - webSocket.onopen = (event) => { - if (!event) return - sendNuiData('YACA_OnConnected') - } - - webSocket.onclose = (event) => { - if (!event) return - - sendNuiData('YACA_OnDisconnected', { - code: event.code, - reason: event.reason, - }) - - setTimeout(() => { - connect() - }, 1000) - } -} - -/** - * Send a command to the YaCA voice plugin - * - * @param command - The command to send as a object - */ -function runCommand(command) { - if (!webSocket || webSocket.readyState !== WebSocket.OPEN) { - return - } - - webSocket.send(JSON.stringify(command)) -} - -/** - * Send a NUI message to the client - * - * @param event - The name of the callback - * @param data - The data to send - */ -function sendNuiData(event, data = {}) { - // skipcq: JS-0125 - fetch(`https://${GetParentResourceName()}/${event}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - }, - body: JSON.stringify(data), - }).catch((error) => console.error('[YaCA-Websocket] Error sending NUI Message:', error)) -} - -$(() => { - window.addEventListener('DOMContentLoaded', () => { - sendNuiData('YACA_OnNuiReady') - }) - - window.addEventListener('message', (event) => { - if (event.data.action === 'connect') { - connect() - } else if (event.data.action === 'command') { - runCommand(event.data.data) - } else if (event.data.action === 'close') { - if (webSocket) webSocket.close() - } else { - console.error('[YaCA-Websocket] Unknown message:', event.data) - } - }) -}) diff --git a/resources/[voice]/yaca-voice/biome.json b/resources/[voice]/yaca-voice/biome.json deleted file mode 100644 index e7f1273fc..000000000 --- a/resources/[voice]/yaca-voice/biome.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/2.0.6/schema.json", - "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true, "defaultBranch": "main" }, - "files": { "ignoreUnknown": false, "includes": ["**", "!**/resource", "!**/dist"] }, - "formatter": { - "enabled": true, - "useEditorconfig": true, - "formatWithErrors": false, - "indentStyle": "space", - "indentWidth": 4, - "lineWidth": 160, - "attributePosition": "auto", - "bracketSpacing": true, - "includes": ["**", "!**/dist", "!**/pnpm-lock.yaml"] - }, - "assist": { - "actions": { - "source": { - "organizeImports": "on" - } - } - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true, - "style": { - "noParameterAssign": "off" - } - }, - "includes": ["**", "!**/dist"] - }, - "javascript": { - "formatter": { - "jsxQuoteStyle": "double", - "quoteProperties": "asNeeded", - "trailingCommas": "all", - "semicolons": "asNeeded", - "arrowParentheses": "always", - "bracketSameLine": false, - "quoteStyle": "single", - "attributePosition": "auto", - "bracketSpacing": true - }, - "globals": [] - } -} diff --git a/resources/[voice]/yaca-voice/package.json b/resources/[voice]/yaca-voice/package.json deleted file mode 100644 index ba4080e3c..000000000 --- a/resources/[voice]/yaca-voice/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "yaca-voice", - "type": "module", - "description": "YACA Voice Integration for FiveM & RedM", - "author": "MineMalox & LuftigerLuca", - "repository": { - "type": "git", - "url": "https://github.com/yaca-systems/fivem-yaca-typescript" - }, - "scripts": { - "preinstall": "npx only-allow pnpm", - "typecheck": "turbo typecheck --env-mode=loose", - "format": "pnpm biome format --write", - "lint": "pnpm biome lint --write", - "check": "pnpm biome check --formatter-enabled=true --linter-enabled=true --write", - "build": "turbo build --env-mode=loose", - "dev": "turbo dev --env-mode=loose", - "create-resource": "turbo create-resource --env-mode=loose && node scripts/create-resource.js" - }, - "devDependencies": { - "@biomejs/biome": "2.0.6", - "esbuild": "^0.24.2", - "turbo": "^2.3.4", - "typescript": "^5.7.3" - }, - "packageManager": "pnpm@9.15.9+sha512.68046141893c66fad01c079231128e9afb89ef87e2691d69e4d40eee228988295fd4682181bae55b58418c3a253bde65a505ec7c5f9403ece5cc3cd37dcf2531" -} diff --git a/resources/[voice]/yaca-voice/packages/common/package.json b/resources/[voice]/yaca-voice/packages/common/package.json deleted file mode 100644 index 731c48bd7..000000000 --- a/resources/[voice]/yaca-voice/packages/common/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@yaca-voice/common", - "private": true, - "version": "0.0.0", - "main": "./src/index.ts", - "types": "/src/index.ts", - "exports": { - ".": "./src/index.ts" - }, - "scripts": { - "typecheck": "tsc --project tsconfig.json" - }, - "dependencies": { - "fast-printf": "^1.6.9", - "json5": "^2.2.3" - }, - "devDependencies": { - "@citizenfx/client": "latest", - "@citizenfx/server": "latest", - "@types/node": "^22.7.4", - "@yaca-voice/typescript-config": "workspace:*" - } -} diff --git a/resources/[voice]/yaca-voice/packages/common/pnpm-lock.yaml b/resources/[voice]/yaca-voice/packages/common/pnpm-lock.yaml deleted file mode 100644 index e2fad36a3..000000000 --- a/resources/[voice]/yaca-voice/packages/common/pnpm-lock.yaml +++ /dev/null @@ -1,30 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - fast-printf: - specifier: ^1.6.9 - version: 1.6.9 - -packages: - - boolean@3.2.0: - resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} - - fast-printf@1.6.9: - resolution: {integrity: sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg==} - engines: {node: '>=10.0'} - -snapshots: - - boolean@3.2.0: {} - - fast-printf@1.6.9: - dependencies: - boolean: 3.2.0 diff --git a/resources/[voice]/yaca-voice/packages/common/src/bridge.ts b/resources/[voice]/yaca-voice/packages/common/src/bridge.ts deleted file mode 100644 index 568785226..000000000 --- a/resources/[voice]/yaca-voice/packages/common/src/bridge.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Add an export in the saltychat namespace. - * - * @param method - the export method name - * @param cb - the callback to execute - */ -export function saltyChatExport(method: string, cb: (...args: never[]) => void) { - on(`__cfx_export_saltychat_${method}`, (setCb: (...args: unknown[]) => void) => { - setCb(cb) - }) -} diff --git a/resources/[voice]/yaca-voice/packages/common/src/config.ts b/resources/[voice]/yaca-voice/packages/common/src/config.ts deleted file mode 100644 index e92a609d8..000000000 --- a/resources/[voice]/yaca-voice/packages/common/src/config.ts +++ /dev/null @@ -1,71 +0,0 @@ -import JSON5 from 'json5' - -/** - * Merge the default object with the parsed object and validate the parsed object. - * This function will log warnings for missing and unknown keys in the parsed object. - * If a key is missing in the parsed object, the default value will be used. - * If a key is unknown in the parsed object, it will be ignored. - * - * @param defaultObj - The default object. - * @param parsedObj - The parsed object. - * @param path - The path to the current object. - */ -function mergeAndValidate(defaultObj: T, parsedObj: T, path: string[] = []): T { - const result: T = { ...defaultObj } - - for (const key in defaultObj) { - if (Object.prototype.hasOwnProperty.call(defaultObj, key) === false) { - continue - } - - const currentPath = [...path, key].join('.') - - if (!(key in parsedObj)) { - console.warn( - `[YaCA] Missing config value for key '${currentPath}' setting to default value: ${defaultObj[key]}\nMissing config values can cause unexpected behavior of the script.`, - ) - } else if ( - typeof defaultObj[key] === 'object' && - defaultObj[key] !== null && - !Array.isArray(defaultObj[key]) && - typeof parsedObj[key] === 'object' && - parsedObj[key] !== null && - !Array.isArray(parsedObj[key]) - ) { - // Recursive merge for nested objects - result[key] = mergeAndValidate(defaultObj[key], parsedObj[key], [...path, key]) - } else { - result[key] = parsedObj[key] - } - } - - for (const key of Object.keys(parsedObj)) { - const currentPath = [...path, key].join('.') - - if (!(key in defaultObj)) { - console.warn(`[YaCA] Unknown config key '${currentPath}' found in config file. This key will be ignored and can be removed.`) - } - } - - return result -} - -/** - * Load a config file from the resource and merge it with the default values. - * - * @param filePath - The path to the config file. - * @param defaultValues - The default values to set when the config file is missing values. - * - * @returns The loaded config. - */ -export function loadConfig(filePath: string, defaultValues: T): T { - const fileData = LoadResourceFile(GetCurrentResourceName(), filePath) - - if (!fileData) { - return defaultValues - } - - const parsedData = JSON5.parse(fileData) as T - - return mergeAndValidate(defaultValues, parsedData) -} diff --git a/resources/[voice]/yaca-voice/packages/common/src/constants.ts b/resources/[voice]/yaca-voice/packages/common/src/constants.ts deleted file mode 100644 index 6640a676e..000000000 --- a/resources/[voice]/yaca-voice/packages/common/src/constants.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const MEGAPHONE_STATE_NAME = 'yacaMegaphone' -export const PHONE_SPEAKER_STATE_NAME = 'yacaPhoneSpeaker' -export const LIP_SYNC_STATE_NAME = 'yacaLipSync' -export const VOICE_RANGE_STATE_NAME = 'yacaVoiceRange' -export const GLOBAL_ERROR_LEVEL_STATE_NAME = 'yacaGlobalErrorLevel' diff --git a/resources/[voice]/yaca-voice/packages/common/src/errorlevel.ts b/resources/[voice]/yaca-voice/packages/common/src/errorlevel.ts deleted file mode 100644 index e0be45643..000000000 --- a/resources/[voice]/yaca-voice/packages/common/src/errorlevel.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { GLOBAL_ERROR_LEVEL_STATE_NAME } from './constants' -import { clamp } from './index' - -/** - * Set the global error level. - * - * @param errorLevel The new error level. Between 0 and 1. - */ -export const setGlobalErrorLevel = (errorLevel: number) => { - GlobalState.set(GLOBAL_ERROR_LEVEL_STATE_NAME, clamp(errorLevel, 0, 1), true) -} - -/** - * Get the global error level. - * - * @returns The global error level. - */ -export const getGlobalErrorLevel = () => { - return GlobalState[GLOBAL_ERROR_LEVEL_STATE_NAME] ?? 0 -} diff --git a/resources/[voice]/yaca-voice/packages/common/src/index.ts b/resources/[voice]/yaca-voice/packages/common/src/index.ts deleted file mode 100644 index 55489d198..000000000 --- a/resources/[voice]/yaca-voice/packages/common/src/index.ts +++ /dev/null @@ -1,58 +0,0 @@ -export * from './bridge' -export * from './config' -export * from './constants' -export * from './errorlevel' -export * from './locale' - -/** - * Sleeps for a given amount of time. - * - * @param ms - The amount of time to sleep in milliseconds. - */ -export function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms, null)) -} - -/** - * Clamps a value between a minimum and maximum value. - * - * @param {number} value - The value to be clamped. - * @param {number} [min=0] - The minimum value. Defaults to 0 if not provided. - * @param {number} [max=1] - The maximum value. Defaults to 1 if not provided. - */ -export function clamp(value: number, min = 0, max = 1) { - return Math.max(min, Math.min(max, value)) -} - -/** - * Creates a promise that will be resolved once any value is returned by the function (including null). - * @param cb Function to call. - * @param errMessage Error message to throw if the function never resolves. - * @param {number?} timeout Error out after `~x` ms. Defaults to 1000, unless set to `false`. - */ -export async function waitFor(cb: () => T, errMessage?: string, timeout?: number | false): Promise { - let value = await cb() - - if (value !== undefined) return value - - if (timeout || timeout == null) { - if (typeof timeout !== 'number') timeout = 1000 - } - - const start = GetGameTimer() - let id: number - - return new Promise((resolve, reject) => { - id = setTick(async () => { - const elapsed = timeout && GetGameTimer() - start - - if (elapsed && elapsed > (timeout as number)) { - return reject(`${errMessage || 'failed to resolve callback'} (waited ${elapsed}ms)`) - } - - value = await cb() - - if (value !== undefined) resolve(value) - }) - }).finally(() => clearTick(id)) -} diff --git a/resources/[voice]/yaca-voice/packages/common/src/locale.ts b/resources/[voice]/yaca-voice/packages/common/src/locale.ts deleted file mode 100644 index bf94a37aa..000000000 --- a/resources/[voice]/yaca-voice/packages/common/src/locale.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { printf } from 'fast-printf' - -const resourceName = GetCurrentResourceName() -const dict: Record = {} - -/** - * Flattens a dictionary. - * - * @param source - The source dictionary to flatten. - * @param target - The target dictionary to flatten to. - * @param prefix - The prefix to use. - */ -function flattenDict(source: Record, target: Record, prefix?: string) { - for (const [key, value] of Object.entries(source)) { - const fullKey = prefix ? `${prefix}.${key}` : key - - if (typeof value === 'object') flattenDict(value, target, fullKey) - else target[fullKey] = String(value) - } - - return target -} - -/** - * Get the localized string for a key. - * - * @param str - The key to get the localized string for. - * @param args - The arguments to use for string interpolation. - */ -export const locale = (str: string, ...args: (string | number | boolean)[]): string => { - const localeStr = dict[str] - - if (localeStr) { - if (args.length > 0) { - return printf(localeStr, ...args) - } - - return localeStr - } - - return str -} - -/** - * Get all the locales. - */ -export const getLocales = () => dict - -/** - * Initialize the locale. - * - * @param configLocale - The locale to use. Defaults to 'en'. If not found, falls back to 'en'. - */ -export const initLocale = (configLocale: string) => { - const lang = configLocale || 'en' - let locales: typeof dict = JSON.parse(LoadResourceFile(resourceName, `locales/${lang}.json`)) - - if (!locales) { - console.warn(`could not load 'locales/${lang}.json'`) - - if (lang !== 'en') { - locales = JSON.parse(LoadResourceFile(resourceName, 'locales/en.json')) - - if (!locales) { - console.warn("could not load 'locales/en.json'") - } - } - - if (!locales) return - } - - const flattened = flattenDict(locales, {}) - - for (const [k, v] of Object.entries(flattened)) { - const regExp = new RegExp(/\$\{([^}]+)}/g) - const matches = v.match(regExp) - if (matches) { - for (const match of matches) { - if (!match) break - const variable = match.substring(2, match.length - 1) as keyof typeof locales - const locale: string = flattened[variable] - - if (locale) { - flattened[k] = v.replace(match, locale) - } - } - } - - dict[k] = v - } -} diff --git a/resources/[voice]/yaca-voice/packages/common/tsconfig.json b/resources/[voice]/yaca-voice/packages/common/tsconfig.json deleted file mode 100644 index 6ae6bb6e7..000000000 --- a/resources/[voice]/yaca-voice/packages/common/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "@yaca-voice/typescript-config/fivem.json", - "compilerOptions": { - "baseUrl": "./", - "rootDir": "./src" - }, - "include": ["./"] -} diff --git a/resources/[voice]/yaca-voice/packages/tsconfig/fivem.json b/resources/[voice]/yaca-voice/packages/tsconfig/fivem.json deleted file mode 100644 index e55ced65d..000000000 --- a/resources/[voice]/yaca-voice/packages/tsconfig/fivem.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "noImplicitAny": true, - "strictNullChecks": true, - "module": "es2020", - "target": "es2021", - "lib": ["es2021", "esnext.disposable"], - "types": ["@types/node", "@citizenfx/client", "@citizenfx/server"], - "resolveJsonModule": true, - "esModuleInterop": true, - "allowUnreachableCode": false, - "strictFunctionTypes": true, - "moduleResolution": "bundler", - "noImplicitThis": true, - "noUnusedLocals": true, - "skipLibCheck": true, - "declaration": true, - "noEmit": true, - "removeComments": true - } -} diff --git a/resources/[voice]/yaca-voice/packages/tsconfig/package.json b/resources/[voice]/yaca-voice/packages/tsconfig/package.json deleted file mode 100644 index f586b4604..000000000 --- a/resources/[voice]/yaca-voice/packages/tsconfig/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "@yaca-voice/typescript-config" -} diff --git a/resources/[voice]/yaca-voice/packages/types/package.json b/resources/[voice]/yaca-voice/packages/types/package.json deleted file mode 100644 index f17b200c8..000000000 --- a/resources/[voice]/yaca-voice/packages/types/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "@yaca-voice/types", - "private": true, - "version": "0.0.0", - "main": "./src/index.ts", - "types": "/src/index.ts", - "exports": { - ".": "./src/index.ts" - } -} diff --git a/resources/[voice]/yaca-voice/packages/types/src/config.ts b/resources/[voice]/yaca-voice/packages/types/src/config.ts deleted file mode 100644 index 0d4b82522..000000000 --- a/resources/[voice]/yaca-voice/packages/types/src/config.ts +++ /dev/null @@ -1,305 +0,0 @@ -import { YacaBuildType } from './enums' - -export type radioMode = 'None' | 'Direct' | 'Tower' - -export interface YacaSharedConfig { - versionCheck: boolean - autoConnectOnJoin: boolean - buildType: YacaBuildType - locale: string - unmuteDelay: number - maxPhoneSpeakerRange: number - phoneHearPlayersNearby: false | 'PHONE_SPEAKER' | true - notifications: { - oxLib: boolean - okoknotify: boolean - gta: boolean - redm: boolean - own: boolean - } - keyBinds: { - increaseVoiceRange: string | false - decreaseVoiceRange: string | false - primaryRadioTransmit: string | false - secondaryRadioTransmit: string | false - megaphone: string | false - voiceRangeWithMouseWheel: string | false - } - radioSettings: { - animation: { - dictionary: string - name: string - flag: number - } - propWhileTalking: { - prop: string | false - boneId: number - position: [number, number, number] - rotation: [number, number, number] - } - channelCount: number - mode: radioMode - maxDistance: number - } - voiceRange: { - defaultIndex: number - ranges: number[] - sendNotification: boolean - markerColor: { - enabled: boolean - r: number - g: number - b: number - a: number - duration: number - type: number - rotate: boolean - } - } - megaphone: { - range: number - automaticVehicleDetection: boolean - allowedVehicleClasses: number[] - allowedVehicleModels: string[] - } - saltyChatBridge: boolean - mufflingSettings: { - mufflingRange: number - vehicleMuffling: { - enabled: boolean - vehicleWhitelist: string[] - } - intensities: { - differentRoom: number - bothCarsClosed: number - oneCarClosed: number - megaPhoneInCar: number - } - } - radioAntiSpamCooldown: number | false - useLocalLipSync: boolean -} - -export const defaultSharedConfig: YacaSharedConfig = { - versionCheck: true, - autoConnectOnJoin: true, - buildType: YacaBuildType.RELEASE, - locale: 'en', - unmuteDelay: 400, - maxPhoneSpeakerRange: 5, - phoneHearPlayersNearby: false, - notifications: { - oxLib: false, - okoknotify: false, - gta: true, - redm: false, - own: false, - }, - keyBinds: { - increaseVoiceRange: 'ADD', - decreaseVoiceRange: 'SUBTRACT', - primaryRadioTransmit: 'N', - secondaryRadioTransmit: 'CAPITAL', - megaphone: 'B', - voiceRangeWithMouseWheel: 'LCONTROL', - }, - radioSettings: { - animation: { - dictionary: 'random@arrests', - name: 'generic_radio_chatter', - flag: 49, - }, - propWhileTalking: { - prop: false, - boneId: 28422, - position: [0.0, 0.0, 0.0], - rotation: [0.0, 0.0, 0.0], - }, - channelCount: 9, - mode: 'None', - maxDistance: 1000, - }, - voiceRange: { - defaultIndex: 1, - ranges: [1, 3, 8, 15, 20, 25, 30, 40], - sendNotification: true, - markerColor: { - enabled: true, - r: 0, - g: 255, - b: 0, - a: 50, - duration: 1000, - type: 1, - rotate: true, - }, - }, - megaphone: { - range: 30, - automaticVehicleDetection: true, - allowedVehicleClasses: [18, 19], - allowedVehicleModels: ['polmav'], - }, - saltyChatBridge: false, - mufflingSettings: { - mufflingRange: -1, - vehicleMuffling: { - enabled: true, - vehicleWhitelist: [ - 'gauntlet6', - 'draugur', - 'bodhi2', - 'vagrant', - 'outlaw', - 'trophytruck', - 'ratel', - 'drifttampa', - 'sm722', - 'tornado4', - 'swinger', - 'locust', - 'hotring', - ], - }, - intensities: { - differentRoom: 10, - bothCarsClosed: 10, - oneCarClosed: 6, - megaPhoneInCar: 6, - }, - }, - radioAntiSpamCooldown: false, - useLocalLipSync: false, -} - -export interface YacaServerConfig { - uniqueServerId: string - ingameChannelId: number - ingameChannelPassword: string - defaultChannelId: number - useWhisper: boolean - excludeChannels: number[] - userNamePattern: string -} - -export const defaultServerConfig: YacaServerConfig = { - uniqueServerId: '', - ingameChannelId: 3, - ingameChannelPassword: '', - defaultChannelId: 1, - useWhisper: false, - excludeChannels: [], - userNamePattern: '[{serverid}] {guid}', -} - -export interface YacaTowerConfig { - towerPositions: [number, number, number][] -} - -export const defaultTowerConfig: YacaTowerConfig = { - towerPositions: [ - [2572, 5397, 56], - [2663, 4972, 56], - [2892, 3911, 56], - [2720, 3304, 64], - [2388, 2949, 64], - [1830, 2368, 64], - [1650, 1316, 102], - [1363, 680, 102], - [918, 230, 92], - [567, 303, 58], - [-47, -666, 74], - [-585, -902, 53], - [2572, 5397, 56], - [2338, 5940, 77], - [1916, 6244, 65], - [1591, 6371, 42], - [953, 6504, 42], - [76, 6606, 42], - [408, 6587, 42], - [-338, -579, 48], - [-293, -632, 47], - [-269, -962, 143], - [98, -870, 136], - [-214, -744, 219], - [-166, -590, 199], - [124, -654, 261], - [149, -769, 261], - [580, 89, 117], - [423, 15, 151], - [424, 18, 151], - [551, -28, 93], - [305, -284, 68], - [299, -313, 68], - [1240, -1090, 44], - [-418, -2804, 14], - [802, -2996, 27], - [253, -3145, 39], - [207, -3145, 39], - [207, -3307, 39], - [247, -3307, 39], - [484, -2178, 40], - [548, -2219, 67], - [-701, 58, 68], - [-696, 208, 139], - [-769, 255, 134], - [-150, -150, 96], - [-202, -327, 65], - [-1913, -3031, 22], - [-1918, -3028, 22], - [-1039, -2385, 27], - [-1042, -2390, 27], - [-1583, -3216, 28], - [-1590, -3212, 28], - [-1308, -2626, 36], - [-1311, -2624, 36], - [-984, -2778, 48], - [-991, -2774, 48], - [-556, -119, 50], - [-619, -106, 51], - [-1167, -575, 40], - [-1152, -443, 42], - [-1156, -498, 49], - [-1290, -445, 106], - [-928, -383, 135], - [-902, -443, 170], - [-770, -786, 83], - [-824, -719, 120], - [-598, -917, 35], - [-678, -717, 54], - [-669, -804, 31], - [-1463, -526, 83], - [-1525, -596, 66], - [-1375, -465, 83], - [-1711, 478, 127], - [-2311, 335, 187], - [-2214, 342, 198], - [-2234, 187, 193], - [202, 1204, 230], - [217, 1140, 230], - [668, 590, 136], - [722, 562, 134], - [838, 510, 138], - [773, 575, 138], - [735, 231, 145], - [450, 5566, 795], - [-449, 6019, 35], - [-142, 6286, 39], - [-368, 6105, 38], - [2792, 5996, 355], - [2796, 5992, 354], - [3460, 3653, 51], - [3459, 3659, 51], - [3615, 3642, 51], - [3614, 3636, 51], - [-2180, 3252, 54], - [-2124, 3219, 54], - [-2050, 3178, 54], - [1858, 3694, 37], - [1695, 3614, 37], - [1692, 2532, 60], - [1692, 2647, 60], - [1824, 2574, 60], - [1407, 2117, 104], - ], -} diff --git a/resources/[voice]/yaca-voice/packages/types/src/enums.ts b/resources/[voice]/yaca-voice/packages/types/src/enums.ts deleted file mode 100644 index 58fc84942..000000000 --- a/resources/[voice]/yaca-voice/packages/types/src/enums.ts +++ /dev/null @@ -1,40 +0,0 @@ -export enum YacaFilterEnum { - RADIO = 'RADIO', - MEGAPHONE = 'MEGAPHONE', - PHONE = 'PHONE', - PHONE_SPEAKER = 'PHONE_SPEAKER', - INTERCOM = 'INTERCOM', - PHONE_HISTORICAL = 'PHONE_HISTORICAL', -} - -export enum YacaNotificationType { - ERROR = 'error', - INFO = 'inform', - SUCCESS = 'success', -} - -export enum YacaStereoMode { - MONO_LEFT = 'MONO_LEFT', - MONO_RIGHT = 'MONO_RIGHT', - STEREO = 'STEREO', -} - -export enum YacaBuildType { - RELEASE = 0, - DEVELOP = 1, -} - -export enum CommDeviceMode { - SENDER = 0, - RECEIVER = 1, - TRANSCEIVER = 2, -} - -export enum YacaPluginStates { - NOT_CONNECTED = 'NOT_CONNECTED', - CONNECTED = 'CONNECTED', - OUTDATED_VERSION = 'OUTDATED_VERSION', - WRONG_TS_SERVER = 'WRONG_TS_SERVER', - IN_INGAME_CHANNEL = 'IN_INGAME_CHANNEL', - IN_EXCLUDED_CHANNEL = 'IN_EXCLUDED_CHANNEL', -} diff --git a/resources/[voice]/yaca-voice/packages/types/src/index.ts b/resources/[voice]/yaca-voice/packages/types/src/index.ts deleted file mode 100644 index be1daf1dd..000000000 --- a/resources/[voice]/yaca-voice/packages/types/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './config' -export * from './enums' -export * from './types' diff --git a/resources/[voice]/yaca-voice/packages/types/src/types.ts b/resources/[voice]/yaca-voice/packages/types/src/types.ts deleted file mode 100644 index 1ec218395..000000000 --- a/resources/[voice]/yaca-voice/packages/types/src/types.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { CommDeviceMode, YacaFilterEnum, YacaStereoMode } from './enums' - -export type YacaResponseCode = - | 'SOUND_STATE' - | 'MUTE_STATE' // Deprecated in favor of SOUND_STATE - | 'TALK_STATE' - | 'OK' - | 'WRONG_TS_SERVER' - | 'MOVE_ERROR' - | 'OUTDATED_VERSION' - | 'WAIT_GAME_INIT' - | 'HEARTBEAT' - | 'MAX_PLAYER_COUNT_REACHED' - | 'LICENSE_SERVER_TIMED_OUT' - | 'MOVED_CHANNEL' - | 'OTHER_TALK_STATE' - -export interface YacaResponse { - code: YacaResponseCode - requestType: string - message: string -} - -export interface YacaSoundStateMessage { - microphoneMuted: boolean - microphoneDisabled: boolean - soundMuted: boolean - soundDisabled: boolean -} - -export interface YacaPlayerData { - remoteID: number - clientId: number - forceMuted: boolean - mutedOnPhone: boolean - phoneCallMemberIds?: number[] -} - -export interface DataObject { - clientId?: number - playerId?: number - forceMuted?: boolean - mutedOnPhone?: boolean - suid?: string - chid?: number - deChid?: number - channelPassword?: string - ingameName?: string - useWhisper?: boolean - excludeChannels?: number[] -} - -export interface YacaClient { - client_id?: number - mode?: CommDeviceMode - errorLevel?: number -} - -export interface YacaProtocol { - comm_type: YacaFilterEnum - output_mode?: YacaStereoMode - members?: YacaClient[] - on?: boolean - volume?: number - channel?: number - range?: number -} - -export interface YacaRadioSettings { - frequency: string - muted: boolean - volume: number - stereo: YacaStereoMode -} - -export type ClientCache = { - serverId: number - playerId: number - resource: string - ped: number - vehicle: number | false - seat: number | false - game: 'fivem' | 'redm' -} - -export type ServerCache = { - resource: string -} - -export type YacaPluginPlayerData = { - client_id: number - position: { x: number; y: number; z: number } - direction: { x: number; y: number; z: number } - range: number - is_underwater: boolean - muffle_intensity: number - is_muted: boolean -} diff --git a/resources/[voice]/yaca-voice/pnpm-lock.yaml b/resources/[voice]/yaca-voice/pnpm-lock.yaml deleted file mode 100644 index a63f1142c..000000000 --- a/resources/[voice]/yaca-voice/pnpm-lock.yaml +++ /dev/null @@ -1,622 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - devDependencies: - '@biomejs/biome': - specifier: 2.0.6 - version: 2.0.6 - esbuild: - specifier: ^0.24.2 - version: 0.24.2 - turbo: - specifier: ^2.3.4 - version: 2.5.3 - typescript: - specifier: ^5.7.3 - version: 5.8.3 - - apps/yaca-client: - dependencies: - eventemitter2: - specifier: ^6.4.9 - version: 6.4.9 - devDependencies: - '@citizenfx/client': - specifier: latest - version: 2.0.15015-1 - '@types/luxon': - specifier: ^3.4.2 - version: 3.4.2 - '@types/node': - specifier: ^20.16.10 - version: 20.16.10 - '@yaca-voice/common': - specifier: workspace:* - version: link:../../packages/common - '@yaca-voice/types': - specifier: workspace:* - version: link:../../packages/types - '@yaca-voice/typescript-config': - specifier: workspace:* - version: link:../../packages/tsconfig - - apps/yaca-server: - dependencies: - node-fetch: - specifier: ^3.3.2 - version: 3.3.2 - devDependencies: - '@citizenfx/server': - specifier: latest - version: 2.0.14862-1 - '@types/luxon': - specifier: ^3.4.2 - version: 3.4.2 - '@types/node': - specifier: ^20.16.10 - version: 20.16.10 - '@yaca-voice/common': - specifier: workspace:* - version: link:../../packages/common - '@yaca-voice/types': - specifier: workspace:* - version: link:../../packages/types - '@yaca-voice/typescript-config': - specifier: workspace:* - version: link:../../packages/tsconfig - - packages/common: - dependencies: - fast-printf: - specifier: ^1.6.9 - version: 1.6.9 - json5: - specifier: ^2.2.3 - version: 2.2.3 - devDependencies: - '@citizenfx/client': - specifier: latest - version: 2.0.15015-1 - '@citizenfx/server': - specifier: latest - version: 2.0.14862-1 - '@types/node': - specifier: ^22.7.4 - version: 22.7.4 - '@yaca-voice/typescript-config': - specifier: workspace:* - version: link:../tsconfig - - packages/tsconfig: {} - - packages/types: {} - -packages: - - '@biomejs/biome@2.0.6': - resolution: {integrity: sha512-RRP+9cdh5qwe2t0gORwXaa27oTOiQRQvrFf49x2PA1tnpsyU7FIHX4ZOFMtBC4QNtyWsN7Dqkf5EDbg4X+9iqA==} - engines: {node: '>=14.21.3'} - hasBin: true - - '@biomejs/cli-darwin-arm64@2.0.6': - resolution: {integrity: sha512-AzdiNNjNzsE6LfqWyBvcL29uWoIuZUkndu+wwlXW13EKcBHbbKjNQEZIJKYDc6IL+p7bmWGx3v9ZtcRyIoIz5A==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [darwin] - - '@biomejs/cli-darwin-x64@2.0.6': - resolution: {integrity: sha512-wJjjP4E7bO4WJmiQaLnsdXMa516dbtC6542qeRkyJg0MqMXP0fvs4gdsHhZ7p9XWTAmGIjZHFKXdsjBvKGIJJQ==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [darwin] - - '@biomejs/cli-linux-arm64-musl@2.0.6': - resolution: {integrity: sha512-CVPEMlin3bW49sBqLBg2x016Pws7eUXA27XYDFlEtponD0luYjg2zQaMJ2nOqlkKG9fqzzkamdYxHdMDc2gZFw==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - - '@biomejs/cli-linux-arm64@2.0.6': - resolution: {integrity: sha512-ZSVf6TYo5rNMUHIW1tww+rs/krol7U5A1Is/yzWyHVZguuB0lBnIodqyFuwCNqG9aJGyk7xIMS8HG0qGUPz0SA==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - - '@biomejs/cli-linux-x64-musl@2.0.6': - resolution: {integrity: sha512-mKHE/e954hR/hSnAcJSjkf4xGqZc/53Kh39HVW1EgO5iFi0JutTN07TSjEMg616julRtfSNJi0KNyxvc30Y4rQ==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - - '@biomejs/cli-linux-x64@2.0.6': - resolution: {integrity: sha512-geM1MkHTV1Kh2Cs/Xzot9BOF3WBacihw6bkEmxkz4nSga8B9/hWy5BDiOG3gHDGIBa8WxT0nzsJs2f/hPqQIQw==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - - '@biomejs/cli-win32-arm64@2.0.6': - resolution: {integrity: sha512-290V4oSFoKaprKE1zkYVsDfAdn0An5DowZ+GIABgjoq1ndhvNxkJcpxPsiYtT7slbVe3xmlT0ncdfOsN7KruzA==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [win32] - - '@biomejs/cli-win32-x64@2.0.6': - resolution: {integrity: sha512-bfM1Bce0d69Ao7pjTjUS+AWSZ02+5UHdiAP85Th8e9yV5xzw6JrHXbL5YWlcEKQ84FIZMdDc7ncuti1wd2sdbw==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [win32] - - '@citizenfx/client@2.0.15015-1': - resolution: {integrity: sha512-gm0/fWM0/Wn5Hny+cnSRsBc73Opz1PPKS4LKCraIE477tQ0uGNVACoeTQqxRJz54Lu09KeA4cCfocVmefVAqUA==} - - '@citizenfx/server@2.0.14862-1': - resolution: {integrity: sha512-I6XnxIGBhskPe9S+q1OQLDqs6TYw1RhX06K6jUg+n0hGITYools6VBIIhdPe2uUlHA+76qJajQlV9ONeR3mDig==} - - '@esbuild/aix-ppc64@0.24.2': - resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.24.2': - resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.24.2': - resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.24.2': - resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.24.2': - resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.24.2': - resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.24.2': - resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.24.2': - resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.24.2': - resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.24.2': - resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.24.2': - resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.24.2': - resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.24.2': - resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.24.2': - resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.24.2': - resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.24.2': - resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.24.2': - resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.24.2': - resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.24.2': - resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.24.2': - resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.24.2': - resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/sunos-x64@0.24.2': - resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.24.2': - resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.24.2': - resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.24.2': - resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@types/luxon@3.4.2': - resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} - - '@types/node@20.16.10': - resolution: {integrity: sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==} - - '@types/node@22.7.4': - resolution: {integrity: sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==} - - boolean@3.2.0: - resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - - data-uri-to-buffer@4.0.1: - resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} - engines: {node: '>= 12'} - - esbuild@0.24.2: - resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} - engines: {node: '>=18'} - hasBin: true - - eventemitter2@6.4.9: - resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==} - - fast-printf@1.6.9: - resolution: {integrity: sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg==} - engines: {node: '>=10.0'} - - fetch-blob@3.2.0: - resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} - engines: {node: ^12.20 || >= 14.13} - - formdata-polyfill@4.0.10: - resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} - engines: {node: '>=12.20.0'} - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - - node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - deprecated: Use your platform's native DOMException instead - - node-fetch@3.3.2: - resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - turbo-darwin-64@2.5.3: - resolution: {integrity: sha512-YSItEVBUIvAGPUDpAB9etEmSqZI3T6BHrkBkeSErvICXn3dfqXUfeLx35LfptLDEbrzFUdwYFNmt8QXOwe9yaw==} - cpu: [x64] - os: [darwin] - - turbo-darwin-arm64@2.5.3: - resolution: {integrity: sha512-5PefrwHd42UiZX7YA9m1LPW6x9YJBDErXmsegCkVp+GjmWrADfEOxpFrGQNonH3ZMj77WZB2PVE5Aw3gA+IOhg==} - cpu: [arm64] - os: [darwin] - - turbo-linux-64@2.5.3: - resolution: {integrity: sha512-M9xigFgawn5ofTmRzvjjLj3Lqc05O8VHKuOlWNUlnHPUltFquyEeSkpQNkE/vpPdOR14AzxqHbhhxtfS4qvb1w==} - cpu: [x64] - os: [linux] - - turbo-linux-arm64@2.5.3: - resolution: {integrity: sha512-auJRbYZ8SGJVqvzTikpg1bsRAsiI9Tk0/SDkA5Xgg0GdiHDH/BOzv1ZjDE2mjmlrO/obr19Dw+39OlMhwLffrw==} - cpu: [arm64] - os: [linux] - - turbo-windows-64@2.5.3: - resolution: {integrity: sha512-arLQYohuHtIEKkmQSCU9vtrKUg+/1TTstWB9VYRSsz+khvg81eX6LYHtXJfH/dK7Ho6ck+JaEh5G+QrE1jEmCQ==} - cpu: [x64] - os: [win32] - - turbo-windows-arm64@2.5.3: - resolution: {integrity: sha512-3JPn66HAynJ0gtr6H+hjY4VHpu1RPKcEwGATvGUTmLmYSYBQieVlnGDRMMoYN066YfyPqnNGCfhYbXfH92Cm0g==} - cpu: [arm64] - os: [win32] - - turbo@2.5.3: - resolution: {integrity: sha512-iHuaNcq5GZZnr3XDZNuu2LSyCzAOPwDuo5Qt+q64DfsTP1i3T2bKfxJhni2ZQxsvAoxRbuUK5QetJki4qc5aYA==} - hasBin: true - - typescript@5.8.3: - resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} - engines: {node: '>=14.17'} - hasBin: true - - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} - - web-streams-polyfill@3.3.3: - resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} - engines: {node: '>= 8'} - -snapshots: - - '@biomejs/biome@2.0.6': - optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.0.6 - '@biomejs/cli-darwin-x64': 2.0.6 - '@biomejs/cli-linux-arm64': 2.0.6 - '@biomejs/cli-linux-arm64-musl': 2.0.6 - '@biomejs/cli-linux-x64': 2.0.6 - '@biomejs/cli-linux-x64-musl': 2.0.6 - '@biomejs/cli-win32-arm64': 2.0.6 - '@biomejs/cli-win32-x64': 2.0.6 - - '@biomejs/cli-darwin-arm64@2.0.6': - optional: true - - '@biomejs/cli-darwin-x64@2.0.6': - optional: true - - '@biomejs/cli-linux-arm64-musl@2.0.6': - optional: true - - '@biomejs/cli-linux-arm64@2.0.6': - optional: true - - '@biomejs/cli-linux-x64-musl@2.0.6': - optional: true - - '@biomejs/cli-linux-x64@2.0.6': - optional: true - - '@biomejs/cli-win32-arm64@2.0.6': - optional: true - - '@biomejs/cli-win32-x64@2.0.6': - optional: true - - '@citizenfx/client@2.0.15015-1': {} - - '@citizenfx/server@2.0.14862-1': {} - - '@esbuild/aix-ppc64@0.24.2': - optional: true - - '@esbuild/android-arm64@0.24.2': - optional: true - - '@esbuild/android-arm@0.24.2': - optional: true - - '@esbuild/android-x64@0.24.2': - optional: true - - '@esbuild/darwin-arm64@0.24.2': - optional: true - - '@esbuild/darwin-x64@0.24.2': - optional: true - - '@esbuild/freebsd-arm64@0.24.2': - optional: true - - '@esbuild/freebsd-x64@0.24.2': - optional: true - - '@esbuild/linux-arm64@0.24.2': - optional: true - - '@esbuild/linux-arm@0.24.2': - optional: true - - '@esbuild/linux-ia32@0.24.2': - optional: true - - '@esbuild/linux-loong64@0.24.2': - optional: true - - '@esbuild/linux-mips64el@0.24.2': - optional: true - - '@esbuild/linux-ppc64@0.24.2': - optional: true - - '@esbuild/linux-riscv64@0.24.2': - optional: true - - '@esbuild/linux-s390x@0.24.2': - optional: true - - '@esbuild/linux-x64@0.24.2': - optional: true - - '@esbuild/netbsd-arm64@0.24.2': - optional: true - - '@esbuild/netbsd-x64@0.24.2': - optional: true - - '@esbuild/openbsd-arm64@0.24.2': - optional: true - - '@esbuild/openbsd-x64@0.24.2': - optional: true - - '@esbuild/sunos-x64@0.24.2': - optional: true - - '@esbuild/win32-arm64@0.24.2': - optional: true - - '@esbuild/win32-ia32@0.24.2': - optional: true - - '@esbuild/win32-x64@0.24.2': - optional: true - - '@types/luxon@3.4.2': {} - - '@types/node@20.16.10': - dependencies: - undici-types: 6.19.8 - - '@types/node@22.7.4': - dependencies: - undici-types: 6.19.8 - - boolean@3.2.0: {} - - data-uri-to-buffer@4.0.1: {} - - esbuild@0.24.2: - optionalDependencies: - '@esbuild/aix-ppc64': 0.24.2 - '@esbuild/android-arm': 0.24.2 - '@esbuild/android-arm64': 0.24.2 - '@esbuild/android-x64': 0.24.2 - '@esbuild/darwin-arm64': 0.24.2 - '@esbuild/darwin-x64': 0.24.2 - '@esbuild/freebsd-arm64': 0.24.2 - '@esbuild/freebsd-x64': 0.24.2 - '@esbuild/linux-arm': 0.24.2 - '@esbuild/linux-arm64': 0.24.2 - '@esbuild/linux-ia32': 0.24.2 - '@esbuild/linux-loong64': 0.24.2 - '@esbuild/linux-mips64el': 0.24.2 - '@esbuild/linux-ppc64': 0.24.2 - '@esbuild/linux-riscv64': 0.24.2 - '@esbuild/linux-s390x': 0.24.2 - '@esbuild/linux-x64': 0.24.2 - '@esbuild/netbsd-arm64': 0.24.2 - '@esbuild/netbsd-x64': 0.24.2 - '@esbuild/openbsd-arm64': 0.24.2 - '@esbuild/openbsd-x64': 0.24.2 - '@esbuild/sunos-x64': 0.24.2 - '@esbuild/win32-arm64': 0.24.2 - '@esbuild/win32-ia32': 0.24.2 - '@esbuild/win32-x64': 0.24.2 - - eventemitter2@6.4.9: {} - - fast-printf@1.6.9: - dependencies: - boolean: 3.2.0 - - fetch-blob@3.2.0: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 3.3.3 - - formdata-polyfill@4.0.10: - dependencies: - fetch-blob: 3.2.0 - - json5@2.2.3: {} - - node-domexception@1.0.0: {} - - node-fetch@3.3.2: - dependencies: - data-uri-to-buffer: 4.0.1 - fetch-blob: 3.2.0 - formdata-polyfill: 4.0.10 - - turbo-darwin-64@2.5.3: - optional: true - - turbo-darwin-arm64@2.5.3: - optional: true - - turbo-linux-64@2.5.3: - optional: true - - turbo-linux-arm64@2.5.3: - optional: true - - turbo-windows-64@2.5.3: - optional: true - - turbo-windows-arm64@2.5.3: - optional: true - - turbo@2.5.3: - optionalDependencies: - turbo-darwin-64: 2.5.3 - turbo-darwin-arm64: 2.5.3 - turbo-linux-64: 2.5.3 - turbo-linux-arm64: 2.5.3 - turbo-windows-64: 2.5.3 - turbo-windows-arm64: 2.5.3 - - typescript@5.8.3: {} - - undici-types@6.19.8: {} - - web-streams-polyfill@3.3.3: {} diff --git a/resources/[voice]/yaca-voice/pnpm-workspace.yaml b/resources/[voice]/yaca-voice/pnpm-workspace.yaml deleted file mode 100644 index 3ff5faaaf..000000000 --- a/resources/[voice]/yaca-voice/pnpm-workspace.yaml +++ /dev/null @@ -1,3 +0,0 @@ -packages: - - "apps/*" - - "packages/*" diff --git a/resources/[voice]/yaca-voice/scripts/create-resource.js b/resources/[voice]/yaca-voice/scripts/create-resource.js deleted file mode 100644 index a76f7a72f..000000000 --- a/resources/[voice]/yaca-voice/scripts/create-resource.js +++ /dev/null @@ -1,67 +0,0 @@ -import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' - -console.log('Building resource...') - -if (existsSync('resource')) { - console.log('Removing existing resource directory...') - rmSync('resource', { recursive: true }) -} - -mkdirSync('resource') -mkdirSync('resource/yaca-voice') - -cpSync('assets/yaca-voice', 'resource/yaca-voice', { recursive: true }) - -mkdirSync('resource/yaca-voice/dist') - -copyFileSync('apps/yaca-client/dist/client.js', 'resource/yaca-voice/dist/client.js') -copyFileSync('apps/yaca-server/dist/server.js', 'resource/yaca-voice/dist/server.js') - -const packageJson = JSON.parse(readFileSync('package.json', { encoding: 'utf8' })) - -writeFileSync( - 'resource/yaca-voice/fxmanifest.lua', - `fx_version 'cerulean' -games { 'gta5', 'rdr3' } -rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.' - -name '${packageJson.name}' -author '${packageJson.author}' -version '${packageJson.version}' -repository '${packageJson.repository.url}' -description '${packageJson.description}' - -dependencies { - '/server:7290', - '/onesync', -} - -ui_page 'web/index.html' - -files { - 'web/index.html', - 'web/script.js', - 'config/shared.json5', - 'config/towers.json5', - 'locales/*.json', -} - -client_script 'dist/client.js' -server_script 'dist/server.js' - -provide 'saltychat' - -`, -) - -if (existsSync('config/yaca-voice/shared.json5')) { - copyFileSync('config/yaca-voice/shared.json5', 'resource/yaca-voice/config/shared.json5') -} - -if (existsSync('config/yaca-voice/server.json5')) { - copyFileSync('config/yaca-voice/server.json5', 'resource/yaca-voice/config/server.json5') -} - -copyFileSync('README.md', 'resource/yaca-voice/README.md') - -console.log('Resource built successfully!') diff --git a/resources/[voice]/yaca-voice/turbo.json b/resources/[voice]/yaca-voice/turbo.json deleted file mode 100644 index 12c5511cc..000000000 --- a/resources/[voice]/yaca-voice/turbo.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "https://turbo.build/schema.json", - "tasks": { - "build": { - "dependsOn": ["typecheck", "^build"] - }, - "typecheck": { - "cache": false - }, - "dev": { - "persistent": true, - "cache": false - }, - "create-resource": { - "dependsOn": ["build"], - "cache": false - } - } -} From d0f7a5597237002176ce8ff2000817ce2e944c61 Mon Sep 17 00:00:00 2001 From: Miho931 <98314142+Miho931@users.noreply.github.com> Date: Mon, 30 Jun 2025 22:18:20 +0200 Subject: [PATCH 3/3] ADD Yaca (Richtige Datein) --- resources/[voice]/yaca-voice/README.md | 723 ++ .../[voice]/yaca-voice/config/server.json5 | 17 + .../[voice]/yaca-voice/config/shared.json5 | 174 + .../[voice]/yaca-voice/config/towers.json5 | 519 + resources/[voice]/yaca-voice/dist/client.js | 28 + resources/[voice]/yaca-voice/dist/server.js | 9040 +++++++++++++++++ resources/[voice]/yaca-voice/fxmanifest.lua | 30 + resources/[voice]/yaca-voice/locales/de.json | 33 + resources/[voice]/yaca-voice/locales/en.json | 33 + resources/[voice]/yaca-voice/web/index.html | 13 + resources/[voice]/yaca-voice/web/script.js | 85 + 11 files changed, 10695 insertions(+) create mode 100644 resources/[voice]/yaca-voice/README.md create mode 100644 resources/[voice]/yaca-voice/config/server.json5 create mode 100644 resources/[voice]/yaca-voice/config/shared.json5 create mode 100644 resources/[voice]/yaca-voice/config/towers.json5 create mode 100644 resources/[voice]/yaca-voice/dist/client.js create mode 100644 resources/[voice]/yaca-voice/dist/server.js create mode 100644 resources/[voice]/yaca-voice/fxmanifest.lua create mode 100644 resources/[voice]/yaca-voice/locales/de.json create mode 100644 resources/[voice]/yaca-voice/locales/en.json create mode 100644 resources/[voice]/yaca-voice/web/index.html create mode 100644 resources/[voice]/yaca-voice/web/script.js diff --git a/resources/[voice]/yaca-voice/README.md b/resources/[voice]/yaca-voice/README.md new file mode 100644 index 000000000..dff0df44d --- /dev/null +++ b/resources/[voice]/yaca-voice/README.md @@ -0,0 +1,723 @@ +# [yaca.systems](https://yaca.systems/) for [FiveM](https://fivem.net/) & [RedM](https://redm.net/) + +This is a example implementation for [FiveM](https://fivem.net/) & [RedM](https://redm.net/). +Feel free to report bugs via issues or contribute via pull requests. + +Join our [Discord](http://discord.yaca.systems/) to get help or make suggestions and start +using [yaca.systems](https://yaca.systems/) today! + +# Setup Steps + +Before you start, make sure you have OneSync enabled and your server artifacts are up to date. + +1. Download and install the latest [release](https://github.com/yaca-systems/fivem-yaca-typescript/releases) of this + resource. +2. Add `start yaca-voice` into your `server.cfg`. +3. Open `config/server.json5` and adjust the variables to your needs. +4. Open `config/shared.json5` and adjust the variables to your needs. + +# Exports + +
+Client + +### General + +#### `getVoiceRange(): int` + +Get the current voice range of the player as `int`. + +#### `getVoiceRanges(): int[]` + +Get all voice ranges as `int[]`. + +#### `changeVoiceRange(increase: boolean): void` + +Change the voice range of the player to the next range. + +#### `setVoiceRange(range: number): void` + +Set the voice range of the player. + +#### `setVoiceRangeChangeAllowedState(state: boolean): void` + +Enable or disable the possibility to change the voice range. + +| Parameter | Type | Description | +|-----------|-----------|------------------------------------------------| +| state | `boolean` | `true` to allow the voice range change, `false` to disable | + +#### `getVoiceRangeChangeAllowedState(): boolean` + +Get the voice range change allowed state of the player as `boolean`. + +#### `setMaxVoiceRange(range: number): void` + +Set the maximum allowed voice range of the player in meters to limit the voice range temporarily. + +| Parameter | Type | Description | +|-----------|-----------|------------------------------------------------| +| range | `number` | `-1` to disable the limit, or a number in meters to set the limit | + +#### `getMaxVoiceRange(): number` + +Get the maximum allowed voice range of the player in meters. + +#### `getMicrophoneMuteState(): boolean` + +Get the microphone mute state of the player as `boolean`. + +#### `getMicrophoneDisabledState(): boolean` + +Get the microphone disabled state of the player as `boolean`. + +#### `getSoundMuteState(): boolean` + +Get the sound mute state of the player as `boolean`. + +#### `getSoundDisabledState(): boolean` + +Get the sound disabled state of the player as `boolean`. + +#### `getPluginState(): string` + +Get the current plugin state as `string`. + +The state can be one of the following: + +- `"NOT_CONNECTED"`: The plugin is not connected +- `"CONNECTED`: The plugin is connected +- `"OUTDATED_VERSION"`: The plugin is not the version set in the dashboard +- `"WRONG_TS_SERVER"`: The user is connected to the wrong Teamspeak server +- `"IN_INGAME_CHANNEL"`: The user is in the ingame channel +- `"IN_EXCLUDED_CHANNEL"`: The user is in an excluded channel + +#### `getGlobalErrorLevel(): number` + +Get the global error level as `number`. + +#### `setSpectatingPlayer(playerId: number | false)` + +Set the player to spectate. + +| Parameter | Type | Description | +|-----------|-------------------|-------------------| +| playerId | `number \| false` | the player to set | + +#### `getSpectatingPlayer(): number` + +Get the player the user is spectating as `number`. + +#### `setVoiceRangeMarkerColor(red: number, green: number, blue: number, alpha: number)` + +Set the voice range marker color. + +#### `getVoiceRangeMarkerColor(): [number, number, number, number]` + +Get the voice range marker color as `[red, green, blue, alpha]`. + +#### `resetVoiceRangeMarkerColor()` + +Reset the voice range marker color to the default color defined in the config. + +### Radio + +#### `enableRadio(state: boolean)` + +Enables or disables the radio system. + +| Parameter | Type | Description | +|-----------|-----------|------------------------------------------------| +| state | `boolean` | `true` to enable the radio, `false` to disable | + +#### `isRadioEnabled(): boolean` + +Returns whether the radio system is enabled as `boolean`. + +#### `changeRadioFrequency(frequency: string)` + +Changes the radio frequency of the active channel. + +| Parameter | Type | Description | +|-----------|----------|--------------------------------------------| +| frequency | `string` | The frequency to set the active channel to | + +#### `changeRadioFrequencyRaw(channel: number, frequency: string)` + +Changes the radio frequency. + +| Parameter | Type | Description | +|-----------|----------|---------------------------------------------------------------------------------------| +| channel? | `number` | the channel number. Defaults to the current active channel when no channel is passed. | +| frequency | `string` | the frequency to set the channel to | + +#### `getRadioFrequency(channel: number): string` + +Returns the frequency of a radio channel as `string`. + +| Parameter | Type | Description | +|-----------|----------|---------------------------------------------------------------------------------------| +| channel? | `number` | the channel number. Defaults to the current active channel when no channel is passed. | + +#### `muteRadioChannel(state?: boolean)` + +Mutes the current active radio channel. + +| Parameter | Type | Description | +|-----------|-----------|-----------------------------------------------------------------------------| +| state? | `boolean` | `true` to mute the channel, `false` to unmute. Defaults to switch if not defined | + +#### `muteRadioChannelRaw(channel: number, state?: boolean)` + +Mutes a radio channel. + +| Parameter | Type | Description | +|-----------|----------|----------------------------------------------------------------------------------------| +| channel? | `number` | the channel to mute. Defaults to the current active channel when no channel is passed. | +| state? | `boolean` | `true` to mute the channel, `false` to unmute. Defaults to switch if not defined | + +#### `isRadioChannelMuted(channel: number): boolean` + +Returns whether a radio channel is muted as `boolean`. + +| Parameter | Type | Description | +|-----------|----------|--------------------| +| channel | `number` | the channel number | + +#### `setActiveRadioChannel(channel: number): bool` + +Changes the active radio channel. Returns whether the operation was successful as `bool`. + +| Parameter | Type | Description | +|-----------|----------|-----------------------| +| channel | `number` | the new radio channel | + +#### `getActiveRadioChannel(): number` + +Returns the active radio channel as `number`. + +#### `setSecondaryRadioChannel(channel: number): bool` + +Changes the secondary radio channel. Returns whether the operation was successful as `bool`. + +| Parameter | Type | Description | +|-----------|----------|-----------------------| +| channel | `number` | the new radio channel | + +#### `getSecondaryRadioChannel(): number` + +Returns the secondary radio channel as `number`. + +#### `changeRadioChannelVolume(higher: boolean): bool` + +Changes the volume of the active radio channel. Returns whether the operation was successful as `bool`. + +| Parameter | Type | Description | +|-----------|-----------|--------------------------------| +| higher | `boolean` | whether to increase the volume | + +#### `changeRadioChannelVolumeRaw(channel: number, volume: number): bool` + +Changes the volume of a radio channel. Returns whether the operation was successful as `bool`. + +| Parameter | Type | Description | +|-----------|----------|--------------------| +| channel | `number` | the channel number | +| volume | `number` | the volume to set | + +#### `getRadioChannelVolume(channel: number): number` + +Returns the volume of a radio channel as `number`. + +| Parameter | Type | Description | +|-----------|----------|--------------------| +| channel | `number` | the channel number | + +#### `changeRadioChannelStereo(): bool` + +Changes the stereo mode of the active radio channel. Returns whether the operation was successful as `bool`. + +#### `changeRadioChannelStereoRaw(channel: number, stereo: string): bool` + +Changes the stereo mode of a radio channel. Returns whether the operation was successful as `bool`. + +| Parameter | Type | Description | +|-----------|----------|---------------------------------------------------------------| +| channel | `number` | the channel number | +| stereo | `string` | the stereo mode (`"MONO_LEFT"`, `"MONO_RIGHT"` or `"STEREO"`) | + +#### `getRadioChannelStereo(channel: number): string` + +Returns the stereo mode of a radio channel as `string`. + +| Parameter | Type | Description | +|-----------|----------|--------------------| +| channel | `number` | the channel number | + +#### `radioTalkingStart(state: boolean, channel: number)` + +Starts or stops talking on the radio. + +| Parameter | Type | Description | +|-----------|-----------|------------------------------------------| +| state | `boolean` | `true` to start talking, `false` to stop | +| channel | `number` | the channel to talk on | + +#### `setRadioMode(mode: string)` + +Sets the radio mode. + +| Parameter | Type | Description | +|-----------|-----------------------------------------------------------------------------| +| mode | `string` | the radio mode to set. Can be either `None`, `Direct` or `Tower` | + +#### `getRadioMode(): string` + +Returns the radio mode as `string`. + +### Phone + +#### `isInCall(): boolean` + +Returns whether the player is in a phone call as a `boolean`. + +### Megaphone + +#### `getCanUseMegaphone(): boolean` + +Returns whether the player can use the megaphone as a `boolean`. + +#### `setCanUseMegaphone(state: boolean)` + +Sets whether the player can use the megaphone. + +| Parameter | Type | Description | +|-----------|-----------|---------------------------------------------------------| +| state | `boolean` | `true` to allow using of megaphone, `false` to disallow | + +### `useMegaphone(state: boolean)` + +Starts or stops using the megaphone. + +| Parameter | Type | Description | +|-----------|-----------|----------------------------------------| +| state | `boolean` | `true` to start using, `false` to stop | + +
+ +
+Server + +### General + +#### `connectToVoice(source: number)` +Connects a player to the voice system. + +| Parameter | Type | Description | +|-----------|----------|-------------------| +| source | `number` | the player source | + +#### `getPlayerAliveStatus(source: number): bool` + +Get the alive status of a player as `bool`. + +| Parameter | Type | Description | +|-----------|----------|-------------------| +| source | `number` | the player source | + +#### `setPlayerAliveStatus(source: number, state: bool)` + +Set the alive status of a player. + +| Parameter | Type | Description | +|-----------|-----------|---------------------| +| source | `number` | the player source | +| state | `boolean` | the new alive state | + +#### `getPlayerVoiceRange(source: number): number` + +Get the voice range of a player as `number`. + +| Parameter | Type | Description | +|-----------|----------|-------------------| +| source | `number` | the player source | + +#### `setPlayerVoiceRange(source: number, range: number)` + +Set the voice range of a player. + +| Parameter | Type | Description | +|-----------|----------|---------------------------------------------------------------------------| +| source | `number` | the player source | +| range | `number` | The new voice range. Defaults to the default voice range if not provided. | + +### Radio + +#### `getPlayersInRadioFrequency(frequency: string): int[]` + +Returns all players in a radio frequency as `int[]`. + +| Parameter | Type | Description | +|-----------|----------|----------------------| +| frequency | `string` | the frequency to get | + +#### `setPlayerRadioChannel(source: number, channel: number, frequency: string)` + +Sets the radio channel of a player. + +| Parameter | Type | Description | +|-----------|----------|----------------------| +| source | `number` | the player source | +| channel | `number` | the channel to set | +| frequency | `string` | the frequency to set | + +#### `getPlayerHasLongRange(source: number): bool` + +Returns whether a player has long range enabled as `bool`. + +| Parameter | Type | Description | +|-----------|----------|-------------------| +| source | `number` | the player source | + +#### `setPlayerHasLongRange(source: number, state: bool)` + +Sets the long range state of a player. + +| Parameter | Type | Description | +|-----------|-----------|----------------------| +| source | `number` | the player source | +| state | `boolean` | the long range state | + +### Phone + +#### `callPlayer(source: number, target: number, state: bool)` + +Creates a phone call between two players. + +| Parameter | Type | Description | +|-----------|-----------|--------------------------| +| source | `number` | the player source | +| target | `number` | the target player source | +| state | `boolean` | the state of the call | + +#### `callPlayerOldEffect(source: number, target: number, state: bool)` + +Creates a phone call between two players with the old effect. + +| Parameter | Type | Description | +|-----------|-----------|--------------------------| +| source | `number` | the player source | +| target | `number` | the target player source | +| state | `boolean` | the state of the call | + +#### `muteOnPhone(source: number, state: bool)` + +Mutes the player when using the phone. + +| Parameter | Type | Description | +|-----------|-----------|-------------------| +| source | `number` | the player source | +| state | `boolean` | the mute state | + +#### `enablePhoneSpeaker(source: number, state: bool)` + +Enable or disable the phone speaker for a player. + +| Parameter | Type | Description | +|-----------|-----------|-------------------------| +| source | `number` | the player source | +| state | `boolean` | the phone speaker state | + +#### `isPlayerInCall(source: number): [bool, number[]]` + +Returns whether a player is in a phone call as `[bool, number[]]`. + +| Parameter | Type | Description | +|-----------|----------|-------------------| +| source | `number` | the player source | + +#### `setGlobalErrorLevel(level: number)` + +Sets the global error level. + +| Parameter | Type | Description | +|-----------|----------|-----------------| +| level | `number` | the error level | + +#### `getGlobalErrorLevel(): number` + +Returns the global error level as `number`. + +
+ +# Events + +
+Client + +### yaca:external:pluginInitialized + +The event is triggered when the plugin is initialized. + +| Parameter | Type | Description | +|-----------|-------|----------------------------------------------| +| clientId | `int` | the client id of the local user in teamspeak | + +### yaca:external:pluginStateChanged + +The event is triggered when the plugin state changes. + +| Parameter | Type | Description | +|-----------|----------|----------------------------------------------| +| state | `string` | the current plugin state, as explained below | + +The state can be one of the following: + +- `"NOT_CONNECTED"`: The plugin is not connected +- `"CONNECTED`: The plugin is connected +- `"OUTDATED_VERSION"`: The plugin is not the version set in the dashboard +- `"WRONG_TS_SERVER"`: The user is connected to the wrong Teamspeak server +- `"IN_INGAME_CHANNEL"`: The user is in the ingame channel +- `"IN_EXCLUDED_CHANNEL"`: The user is in an excluded channel + +### yaca:external:voiceRangeUpdate + +This event is triggered when the voice range of a player is updated. + +| Parameter | Type | Description | +|------------|-------|---------------------------| +| range | `int` | the newly set voice range | +| rangeIndex | `int` | the index of the range | + +### yaca:external:muteStateChanged + +DEPRECATED: Use `yaca:external:microphoneMuteStateChanged` instead. +The event is triggered when the mute state of a player changes. + +| Parameter | Type | Description | +|-----------|-----------|--------------------| +| state | `boolean` | the new mute state | + +### yaca:external:microphoneMuteStateChanged + +The event is triggered when the microphone mute state of a player changes. + +| Parameter | Type | Description | +|-----------|-----------|--------------------| +| state | `boolean` | the new mute state | + +### yaca:external:microphoneDisabledStateChanged + +The event is triggered when the microphone disabled state of a player changes. + +| Parameter | Type | Description | +|-----------|-----------|--------------------| +| state | `boolean` | the new mute state | + +### yaca:external:soundMuteStateChanged + +The event is triggered when the sound mute state of a player changes. + +| Parameter | Type | Description | +|-----------|-----------|--------------------| +| state | `boolean` | the new mute state | + +### yaca:external:soundDisabledStateChanged + +The event is triggered when the sound disabled state of a player changes. + +| Parameter | Type | Description | +|-----------|-----------|--------------------| +| state | `boolean` | the new mute state | + +### yaca:external:isTalking + +The event is triggered when a player starts or stops talking. + +| Parameter | Type | Description | +|-----------|-----------|-----------------------| +| state | `boolean` | the new talking state | + +### yaca:external:megaphoneState + +The event is triggered when the megaphone state of a player changes. + +| Parameter | Type | Description | +|-----------|-----------|-------------------------| +| state | `boolean` | the new megaphone state | + +### yaca:external:setRadioMuteState + +The event is triggered when the radio mute state of a player changes. + +| Parameter | Type | Description | +|-----------|-----------|---------------------------------------------| +| channel | `number` | the channel where the mute state is changed | +| state | `boolean` | the new mute state | + +### yaca:external:isRadioEnabled + +The event is triggered when the radio state of a player changes. + +| Parameter | Type | Description | +|-----------|-----------|----------------------------------------------------------------------| +| state | `boolean` | `true` when the radio is enabled, `false` when the radio is disabled | + +### yaca:external:changedActiveRadioChannel + +The event is triggered when the active radio channel of a player changes. + +| Parameter | Type | Description | +|-----------|----------|------------------------------| +| channel | `number` | the new active radio channel | + +### yaca:external:changedSecondaryRadioChannel + +The event is triggered when the secondary radio channel of a player changes. + +| Parameter | Type | Description | +|-----------|----------|---------------------------------------------------| +| channel | `number` | the new active radio channel, or `-1` if disabled | + +### yaca:external:setRadioVolume + +The event is triggered when the radio volume of a player changes. + +| Parameter | Type | Description | +|-----------|----------|-----------------------| +| channel | `number` | the channel to change | +| volume | `number` | the new volume to set | + +### yaca:external:setRadioChannelStereo + +The event is triggered when the stereo mode of a radio channel changes. + +| Parameter | Type | Description | +|-----------|----------|-----------------------------------------------------------------------------------------------| +| channel | `number` | the channel to change | +| stereo | `string` | `"MONO_LEFT"` for the left ear, `"MONO_RIGHT"` for the right ear and `"STEREO"` for both ears | + +### yaca:external:setRadioFrequency + +The event is triggered when the radio frequency of a player changes. + +| Parameter | Type | Description | +|-----------|----------|----------------------| +| channel | `number` | the channel to set | +| frequency | `string` | the frequency to set | + +### yaca:external:isRadioTalking + +The event is triggered when a player starts or stops talking on the radio. + +| Parameter | Type | Description | +|-----------|-----------|--------------------------------------------| +| state | `boolean` | the new talking state | +| channel | `number` | the channel where the player is talking at | + +### yaca:external:isRadioReceiving + +The event is triggered when a player starts or stops receiving on the radio. + +| Parameter | Type | Description | +|-----------|-----------|------------------------------------------------| +| state | `boolean` | the new receiver state | +| channel | `number` | the channel from which the player is receiving | + +### yaca:external:notification + +The event is triggered when a notification should be shown. + +| Parameter | Type | Description | +|-----------|----------|--------------------------------------------------------------| +| message | `string` | the message to show | +| type | `string` | the type of the message (`"inform"`, `"error"`, `"success"`) | + +Example for custom notification: + +```lua +AddEventHandler('yaca:external:notification', function (message, type) + -- Call your Notifications System here. +end) +``` + +### yaca:external:channelChanged + +The event is triggered when the player changes the channel to the ingame or excluded channel. + +| Parameter | Type | Description | +|-------------|----------|------------------------------------------------------------------------------------------------------------------| +| channelType | `string` | `INGAME_CHANNEL` when moving into the ingame channel and `EXCLUDED_CHANNEL` when moving into a excluded channel. | + +
+ +
+Server + +### yaca:external:changeMegaphoneState + +The event is triggered when the megaphone state of a player changes. + +| Parametr | Type | Description | +|----------|-----------|-------------------------| +| source | `int` | the player source | +| state | `boolean` | the new megaphone state | + +### yaca:external:phoneCall + +The event is triggered when a phone call is started or ended. + +| Parameter | Type | Description | +|-----------|------------------|---------------------------------------------------------------------------------| +| source | `int` | the player source | +| target | `int` | the target player source | +| state | `boolean` | the new phone call state | +| filter | `YacaFilterEnum` | the used filter for the phone call, can be either `PHONE` or `PHONE_HISTORICAL` | + +### yaca:external:phoneSpeaker + +The event is triggered when the phone speaker state of a player changes. + +| Parameter | Type | Description | +|-----------|-----------|-----------------------------| +| source | `int` | the player source | +| state | `boolean` | the new phone speaker state | + +### yaca:external:changedRadioFrequency + +The event is triggered when the radio frequency of a player changes. + +| Parameter | Type | Description | +|-----------|----------|-----------------------------------------| +| source | `int` | the player source | +| channel | `int` | the channel where the frequency was set | +| frequency | `string` | the frequency to set | + +### yaca:external:changedRadioMuteState + +The event is triggered when the radio mute state of a player changes. + +| Parameter | Type | Description | +|-----------|-----------|----------------------------------------------| +| source | `int` | the player source | +| channel | `int` | the channel where the mute state was changed | +| state | `boolean` | the new mute state | + +
+ +# Developers + +If you want to contribute to this project, feel free to do so. We are happy about every contribution. If you have any +questions, feel free to ask in our [Discord](http://discord.yaca.systems/). + +## Building the resource + +To build the resource, you need to have [Node.js](https://nodejs.org/) installed. After that, you can run the following +commands to build the resource: + +```bash +pnpm install +pnpm run build +``` + +The built resource will be located in the `resource` folder, which you can then use in your FiveM server. diff --git a/resources/[voice]/yaca-voice/config/server.json5 b/resources/[voice]/yaca-voice/config/server.json5 new file mode 100644 index 000000000..b5e5dd8a8 --- /dev/null +++ b/resources/[voice]/yaca-voice/config/server.json5 @@ -0,0 +1,17 @@ +{ + "uniqueServerId": "FAqZTlphJBka2Y0gZr/KrZyXzQY=", // The unique Server Identifier of the Teamspeak-Server + "ingameChannelId": 5, // The ID of the Ingame Channel + "ingameChannelPassword": "SvqvZYGhX43CrIkt", // The Password used to join the Ingame Channel + "defaultChannelId": 9, // The ID of the Channel where a players should be moved to when leaving Ingame + "useWhisper": false, // If you want to use the Whisper functions of TeamSpeak, if set to false it mutes and unmutes the players + "excludeChannels": [], // The channels that should be able to join while being Ingame without instantly being moved back into the Ingame channel + + /* The pattern that is used to generate the username. + * + * Following placeholders will be replaced: + * - {serverid} with the Ingame-ID of the player + * - {playername} with the steam/fivem name of the player + * - {guid} with a string containing random letters and digits. + */ + "userNamePattern": "{serverid}" +} diff --git a/resources/[voice]/yaca-voice/config/shared.json5 b/resources/[voice]/yaca-voice/config/shared.json5 new file mode 100644 index 000000000..99ed237b0 --- /dev/null +++ b/resources/[voice]/yaca-voice/config/shared.json5 @@ -0,0 +1,174 @@ +{ + // Enable the version check to get notified about new versions. + "versionCheck": true, + // Enable manual or automatic connecting to the voice channel when joining the server. + "autoConnectOnJoin": true, + // The build type of the plugin. 0 = release, 1 = develop (develop allows using all yaca plugin version) + "buildType": 0, + // The locale that should be used. + "locale": "de", + // The time before the teamspeak client is being unmuted after joining the ingame channel. + "unmuteDelay": 400, + // The range in which you can hear the phone speaker when active. + "maxPhoneSpeakerRange": 5, + /* Choose if players near other players that are in a call, should be heard by the players on the opposite side of the call. + * + * Following options are available: + * - false: Disable the feature completely + * - true: Enable the feature always + * - "PHONE_SPEAKER": Enable the feature only when the player on the phone has the phone speaker enabled. + */ + "phoneHearPlayersNearby": false, + // Choose which notifications should be enabled. + "notifications": { + // Enable notifications from ox_lib + "oxLib": false, + // Enable notifications from okokNotify + "okoknotify": false, + // Enable notifications from GTA (FiveM only) + "gta": true, + // Enable notifications from Rdr 2 (RedM only) + "redm": false, + // Enable the option to implement a custom notification + "own": false + }, + // Set the default key binds for the plugin, which can be then changed by the player. + "keyBinds": { + // The key to increase the voice range + "increaseVoiceRange": "ADD", + // The key to decrease the voice range + "decreaseVoiceRange": "SUBTRACT", + // The key to transmit on the primary radio + "primaryRadioTransmit": "N", + // The key to transmit on the secondary radio + "secondaryRadioTransmit": "CAPITAL", + // The key to use the megaphone + "megaphone": "B", + // The key to hold additional to change voice range via mousewheel by 1 meter, false to disable that feature + "voiceRangeWithMouseWheel": "LCONTROL" + }, + "radioSettings": { + // Customize radio animation + "animation": { + "dictionary": "random@arrests", + "name": "generic_radio_chatter", + "flag": 49 + }, + "propWhileTalking": { + // The prop that should be shown while talking on the radio. + "prop": false, + // The bone that the prop should be attached to. + "boneId": 28422, + // The position of the prop. + "position": [ + 0.0, + 0.0, + 0.0 + ], + // The rotation of the prop. + "rotation": [ + 0.0, + 0.0, + 0.0 + ] + }, + // The maximum amount of radio channels that can be used. + "channelCount": 9, + /* Choose the mode of the radio system. + * + * Following options are available: + * - "Tower": The radio system is based on towers, which means that the range is limited by the distance to the next tower. + * - "Direct": The radio system is based on the distance between the players. + * - "None": The radio always works no matter the distance. + */ + "mode": "Tower", + // The maximum distance between two players or to the tower to be able to hear each other. + "maxDistance": 5000 + }, + "voiceRange": { + // The default index which should be used for the voice range when a player joins the server. + "defaultIndex": 1, + // The ranges that should be available for the players. + "ranges": [ + 1, + 3, + 8, + 15, + 20, + 25, + 30, + 40 + ], + // Choose if a notification should be sent when the voice range is changed. + "sendNotification": false, + "markerColor": { + // Choose if the marker should be enabled. + "enabled": true, + // The color of the marker, r = red, g = green, b = blue, a = alpha + "r": 0, + "g": 255, + "b": 0, + "a": 50, + // The duration the marker should be shown. + "duration": 1000, + "type": 1, + "rotate": true + } + }, + "megaphone": { + // The range of the megaphone. + "range": 30, + // Choose if the plugin should automatically detect if the player should be able to use the megaphone in the vehicle. (FiveM only) + "automaticVehicleDetection": true, + // The allowed vehicle classes for the megaphone. (FiveM only) + "allowedVehicleClasses": [ + 18, + 19 + ], + // The allowed vehicle models for the megaphone. (FiveM only) + "allowedVehicleModels": [ + "polmav" + ] + }, + // Choose if the saltyChatBridge should be enabled. + "saltyChatBridge": false, + "mufflingSettings": { + // If set to -1, the player voice range is used, all values >= 0 sets the muffling range before it gets completely cut off + "mufflingRange": -1, + "vehicleMuffling": { + // If set to true, the vehicle muffling feature is enabled. (FiveM only) + "enabled": true, + // Whitelist of vehicles that should be not be affected by the vehicle muffling. (FiveM only) + "vehicleWhitelist": [ + "gauntlet6", + "draugur", + "bodhi2", + "vagrant", + "outlaw", + "trophytruck", + "ratel", + "drifttampa", + "sm722", + "tornado4", + "swinger", + "locust", + "hotring" + ], + }, + // The intensities of the muffling. (0 = no muffling, 10 = full muffling) + "intensities": { + // The intensity when the players are in different rooms. + "differentRoom": 10, + // The intensity when both cars are closed. (FiveM only) + "bothCarsClosed": 10, + // The intensity when one car is closed. (FiveM only) + "oneCarClosed": 6, + // The intensity of muffling of the megaphone of a player in a different car. (FiveM only) + "megaPhoneInCar": 6 + } + }, + // Cooldown in milliseconds which the player has to wait to use the radio again, defaults to false which disables the feature. + "radioAntiSpamCooldown": false, + // When set to true the plugin syncs the talk state via the plugin, instead of the default way via statebags. This imitates the way how saltychat syncs the talk state, but has some drawbacks. + "useLocalLipSync": false +} diff --git a/resources/[voice]/yaca-voice/config/towers.json5 b/resources/[voice]/yaca-voice/config/towers.json5 new file mode 100644 index 000000000..70831c2d3 --- /dev/null +++ b/resources/[voice]/yaca-voice/config/towers.json5 @@ -0,0 +1,519 @@ +{ + "towerPositions": [ + [ + 2572, + 5397, + 56 + ], + [ + 2663, + 4972, + 56 + ], + [ + 2892, + 3911, + 56 + ], + [ + 2720, + 3304, + 64 + ], + [ + 2388, + 2949, + 64 + ], + [ + 1830, + 2368, + 64 + ], + [ + 1650, + 1316, + 102 + ], + [ + 1363, + 680, + 102 + ], + [ + 918, + 230, + 92 + ], + [ + 567, + 303, + 58 + ], + [ + -47, + -666, + 74 + ], + [ + -585, + -902, + 53 + ], + [ + 2572, + 5397, + 56 + ], + [ + 2338, + 5940, + 77 + ], + [ + 1916, + 6244, + 65 + ], + [ + 1591, + 6371, + 42 + ], + [ + 953, + 6504, + 42 + ], + [ + 76, + 6606, + 42 + ], + [ + 408, + 6587, + 42 + ], + [ + -338, + -579, + 48 + ], + [ + -293, + -632, + 47 + ], + [ + -269, + -962, + 143 + ], + [ + 98, + -870, + 136 + ], + [ + -214, + -744, + 219 + ], + [ + -166, + -590, + 199 + ], + [ + 124, + -654, + 261 + ], + [ + 149, + -769, + 261 + ], + [ + 580, + 89, + 117 + ], + [ + 423, + 15, + 151 + ], + [ + 424, + 18, + 151 + ], + [ + 551, + -28, + 93 + ], + [ + 305, + -284, + 68 + ], + [ + 299, + -313, + 68 + ], + [ + 1240, + -1090, + 44 + ], + [ + -418, + -2804, + 14 + ], + [ + 802, + -2996, + 27 + ], + [ + 253, + -3145, + 39 + ], + [ + 207, + -3145, + 39 + ], + [ + 207, + -3307, + 39 + ], + [ + 247, + -3307, + 39 + ], + [ + 484, + -2178, + 40 + ], + [ + 548, + -2219, + 67 + ], + [ + -701, + 58, + 68 + ], + [ + -696, + 208, + 139 + ], + [ + -769, + 255, + 134 + ], + [ + -150, + -150, + 96 + ], + [ + -202, + -327, + 65 + ], + [ + -1913, + -3031, + 22 + ], + [ + -1918, + -3028, + 22 + ], + [ + -1039, + -2385, + 27 + ], + [ + -1042, + -2390, + 27 + ], + [ + -1583, + -3216, + 28 + ], + [ + -1590, + -3212, + 28 + ], + [ + -1308, + -2626, + 36 + ], + [ + -1311, + -2624, + 36 + ], + [ + -984, + -2778, + 48 + ], + [ + -991, + -2774, + 48 + ], + [ + -556, + -119, + 50 + ], + [ + -619, + -106, + 51 + ], + [ + -1167, + -575, + 40 + ], + [ + -1152, + -443, + 42 + ], + [ + -1156, + -498, + 49 + ], + [ + -1290, + -445, + 106 + ], + [ + -928, + -383, + 135 + ], + [ + -902, + -443, + 170 + ], + [ + -770, + -786, + 83 + ], + [ + -824, + -719, + 120 + ], + [ + -598, + -917, + 35 + ], + [ + -678, + -717, + 54 + ], + [ + -669, + -804, + 31 + ], + [ + -1463, + -526, + 83 + ], + [ + -1525, + -596, + 66 + ], + [ + -1375, + -465, + 83 + ], + [ + -1711, + 478, + 127 + ], + [ + -2311, + 335, + 187 + ], + [ + -2214, + 342, + 198 + ], + [ + -2234, + 187, + 193 + ], + [ + 202, + 1204, + 230 + ], + [ + 217, + 1140, + 230 + ], + [ + 668, + 590, + 136 + ], + [ + 722, + 562, + 134 + ], + [ + 838, + 510, + 138 + ], + [ + 773, + 575, + 138 + ], + [ + 735, + 231, + 145 + ], + [ + 450, + 5566, + 795 + ], + [ + -449, + 6019, + 35 + ], + [ + -142, + 6286, + 39 + ], + [ + -368, + 6105, + 38 + ], + [ + 2792, + 5996, + 355 + ], + [ + 2796, + 5992, + 354 + ], + [ + 3460, + 3653, + 51 + ], + [ + 3459, + 3659, + 51 + ], + [ + 3615, + 3642, + 51 + ], + [ + 3614, + 3636, + 51 + ], + [ + -2180, + 3252, + 54 + ], + [ + -2124, + 3219, + 54 + ], + [ + -2050, + 3178, + 54 + ], + [ + 1858, + 3694, + 37 + ], + [ + 1695, + 3614, + 37 + ], + [ + 1692, + 2532, + 60 + ], + [ + 1692, + 2647, + 60 + ], + [ + 1824, + 2574, + 60 + ], + [ + 1407, + 2117, + 104 + ] + ] +} \ No newline at end of file diff --git a/resources/[voice]/yaca-voice/dist/client.js b/resources/[voice]/yaca-voice/dist/client.js new file mode 100644 index 000000000..5bf6b1092 --- /dev/null +++ b/resources/[voice]/yaca-voice/dist/client.js @@ -0,0 +1,28 @@ +(()=>{var xn=Object.create;var Nt=Object.defineProperty;var Pn=Object.getOwnPropertyDescriptor;var Tn=Object.getOwnPropertyNames;var Nn=Object.getPrototypeOf,In=Object.prototype.hasOwnProperty;var Se=(o,e)=>()=>(e||o((e={exports:{}}).exports,e),e.exports);var kn=(o,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let u of Tn(e))!In.call(o,u)&&u!==t&&Nt(o,u,{get:()=>e[u],enumerable:!(i=Pn(e,u))||i.enumerable});return o};var vt=(o,e,t)=>(t=o!=null?xn(Nn(o)):{},kn(e||!o||!o.__esModule?Nt(t,"default",{value:o,enumerable:!0}):t,o));var Ot=Se((Rt,St)=>{(function(o,e){typeof Rt=="object"&&typeof St<"u"?St.exports=e():typeof define=="function"&&define.amd?define(e):o.JSON5=e()})(Rt,function(){"use strict";function o(a,h){return h={exports:{}},a(h,h.exports),h.exports}var e=o(function(a){var h=a.exports=typeof window<"u"&&window.Math==Math?window:typeof self<"u"&&self.Math==Math?self:Function("return this")();typeof __g=="number"&&(__g=h)}),t=o(function(a){var h=a.exports={version:"2.6.5"};typeof __e=="number"&&(__e=h)}),i=t.version,u=function(a){return typeof a=="object"?a!==null:typeof a=="function"},d=function(a){if(!u(a))throw TypeError(a+" is not an object!");return a},y=function(a){try{return!!a()}catch{return!0}},_=!y(function(){return Object.defineProperty({},"a",{get:function(){return 7}}).a!=7}),b=e.document,A=u(b)&&u(b.createElement),v=function(a){return A?b.createElement(a):{}},re=!_&&!y(function(){return Object.defineProperty(v("div"),"a",{get:function(){return 7}}).a!=7}),J=function(a,h){if(!u(a))return a;var F,E;if(h&&typeof(F=a.toString)=="function"&&!u(E=F.call(a))||typeof(F=a.valueOf)=="function"&&!u(E=F.call(a))||!h&&typeof(F=a.toString)=="function"&&!u(E=F.call(a)))return E;throw TypeError("Can't convert object to primitive value")},z=Object.defineProperty,Q=_?Object.defineProperty:function(h,F,E){if(d(h),F=J(F,!0),d(E),re)try{return z(h,F,E)}catch{}if("get"in E||"set"in E)throw TypeError("Accessors not supported!");return"value"in E&&(h[F]=E.value),h},le={f:Q},Ee=function(a,h){return{enumerable:!(a&1),configurable:!(a&2),writable:!(a&4),value:h}},ce=_?function(a,h,F){return le.f(a,h,Ee(1,F))}:function(a,h,F){return a[h]=F,a},Be={}.hasOwnProperty,Ce=function(a,h){return Be.call(a,h)},Oe=0,_e=Math.random(),Me=function(a){return"Symbol(".concat(a===void 0?"":a,")_",(++Oe+_e).toString(36))},O=!1,ge=o(function(a){var h="__core-js_shared__",F=e[h]||(e[h]={});(a.exports=function(E,B){return F[E]||(F[E]=B!==void 0?B:{})})("versions",[]).push({version:t.version,mode:O?"pure":"global",copyright:"\xA9 2019 Denis Pushkarev (zloirock.ru)"})}),Le=ge("native-function-to-string",Function.toString),Ue=o(function(a){var h=Me("src"),F="toString",E=(""+Le).split(F);t.inspectSource=function(B){return Le.call(B)},(a.exports=function(B,T,I,ue){var $=typeof I=="function";$&&(Ce(I,"name")||ce(I,"name",T)),B[T]!==I&&($&&(Ce(I,h)||ce(I,h,B[T]?""+B[T]:E.join(String(T)))),B===e?B[T]=I:ue?B[T]?B[T]=I:ce(B,T,I):(delete B[T],ce(B,T,I)))})(Function.prototype,F,function(){return typeof this=="function"&&this[h]||Le.call(this)})}),Ze=function(a){if(typeof a!="function")throw TypeError(a+" is not a function!");return a},Qe=function(a,h,F){if(Ze(a),h===void 0)return a;switch(F){case 1:return function(E){return a.call(h,E)};case 2:return function(E,B){return a.call(h,E,B)};case 3:return function(E,B,T){return a.call(h,E,B,T)}}return function(){return a.apply(h,arguments)}},M="prototype",ee=function(a,h,F){var E=a&ee.F,B=a&ee.G,T=a&ee.S,I=a&ee.P,ue=a&ee.B,$=B?e:T?e[h]||(e[h]={}):(e[h]||{})[M],Te=B?t:t[h]||(t[h]={}),We=Te[M]||(Te[M]={}),de,me,ae,Ge;B&&(F=h);for(de in F)me=!E&&$&&$[de]!==void 0,ae=(me?$:F)[de],Ge=ue&&me?Qe(ae,e):I&&typeof ae=="function"?Qe(Function.call,ae):ae,$&&Ue($,de,ae,a&ee.U),Te[de]!=ae&&ce(Te,de,Ge),I&&We[de]!=ae&&(We[de]=ae)};e.core=t,ee.F=1,ee.G=2,ee.S=4,ee.P=8,ee.B=16,ee.W=32,ee.U=64,ee.R=128;var Ae=ee,Et=Math.ceil,n=Math.floor,r=function(a){return isNaN(a=+a)?0:(a>0?n:Et)(a)},s=function(a){if(a==null)throw TypeError("Can't call method on "+a);return a},l=function(a){return function(h,F){var E=String(s(h)),B=r(F),T=E.length,I,ue;return B<0||B>=T?a?"":void 0:(I=E.charCodeAt(B),I<55296||I>56319||B+1===T||(ue=E.charCodeAt(B+1))<56320||ue>57343?a?E.charAt(B):I:a?E.slice(B,B+2):(I-55296<<10)+(ue-56320)+65536)}},c=l(!1);Ae(Ae.P,"String",{codePointAt:function(h){return c(this,h)}});var f=t.String.codePointAt,C=Math.max,p=Math.min,g=function(a,h){return a=r(a),a<0?C(a+h,0):p(a,h)},m=String.fromCharCode,R=String.fromCodePoint;Ae(Ae.S+Ae.F*(!!R&&R.length!=1),"String",{fromCodePoint:function(h){for(var F=arguments,E=[],B=arguments.length,T=0,I;B>T;){if(I=+F[T++],g(I,1114111)!==I)throw RangeError(I+" is not a valid code point");E.push(I<65536?m(I):m(((I-=65536)>>10)+55296,I%1024+56320))}return E.join("")}});var X=t.String.fromCodePoint,k=/[\u1680\u2000-\u200A\u202F\u205F\u3000]/,V=/[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312E\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEA\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF19]|\uD806[\uDCA0-\uDCDF\uDCFF\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE83\uDE86-\uDE89\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50\uDF93-\uDF9F\uDFE0\uDFE1]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00-\uDD1E\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]/,j=/[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u08D4-\u08E1\u08E3-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u09FC\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0AF9-\u0AFF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58-\u0C5A\u0C60-\u0C63\u0C66-\u0C6F\u0C80-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D00-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D54-\u0D57\u0D5F-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19D9\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABD\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1CD0-\u1CD2\u1CD4-\u1CF9\u1D00-\u1DF9\u1DFB-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u2E2F\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099\u309A\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312E\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEA\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA827\uA840-\uA873\uA880-\uA8C5\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA8FD\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE2F\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDDFD\uDE80-\uDE9C\uDEA0-\uDED0\uDEE0\uDF00-\uDF1F\uDF2D-\uDF4A\uDF50-\uDF7A\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00-\uDE03\uDE05\uDE06\uDE0C-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE38-\uDE3A\uDE3F\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE6\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC00-\uDC46\uDC66-\uDC6F\uDC7F-\uDCBA\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD00-\uDD34\uDD36-\uDD3F\uDD50-\uDD73\uDD76\uDD80-\uDDC4\uDDCA-\uDDCC\uDDD0-\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE37\uDE3E\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEEA\uDEF0-\uDEF9\uDF00-\uDF03\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3C-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF50\uDF57\uDF5D-\uDF63\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC00-\uDC4A\uDC50-\uDC59\uDC80-\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDB5\uDDB8-\uDDC0\uDDD8-\uDDDD\uDE00-\uDE40\uDE44\uDE50-\uDE59\uDE80-\uDEB7\uDEC0-\uDEC9\uDF00-\uDF19\uDF1D-\uDF2B\uDF30-\uDF39]|\uD806[\uDCA0-\uDCE9\uDCFF\uDE00-\uDE3E\uDE47\uDE50-\uDE83\uDE86-\uDE99\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC36\uDC38-\uDC40\uDC50-\uDC59\uDC72-\uDC8F\uDC92-\uDCA7\uDCA9-\uDCB6\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD36\uDD3A\uDD3C\uDD3D\uDD3F-\uDD47\uDD50-\uDD59]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDED0-\uDEED\uDEF0-\uDEF4\uDF00-\uDF36\uDF40-\uDF43\uDF50-\uDF59\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50-\uDF7E\uDF8F-\uDF9F\uDFE0\uDFE1]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00-\uDD1E\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99\uDC9D\uDC9E]|\uD834[\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD838[\uDC00-\uDC06\uDC08-\uDC18\uDC1B-\uDC21\uDC23\uDC24\uDC26-\uDC2A]|\uD83A[\uDC00-\uDCC4\uDCD0-\uDCD6\uDD00-\uDD4A\uDD50-\uDD59]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uDB40[\uDD00-\uDDEF]/,K={Space_Separator:k,ID_Start:V,ID_Continue:j},G={isSpaceSeparator:function(h){return typeof h=="string"&&K.Space_Separator.test(h)},isIdStartChar:function(h){return typeof h=="string"&&(h>="a"&&h<="z"||h>="A"&&h<="Z"||h==="$"||h==="_"||K.ID_Start.test(h))},isIdContinueChar:function(h){return typeof h=="string"&&(h>="a"&&h<="z"||h>="A"&&h<="Z"||h>="0"&&h<="9"||h==="$"||h==="_"||h==="\u200C"||h==="\u200D"||K.ID_Continue.test(h))},isDigit:function(h){return typeof h=="string"&&/[0-9]/.test(h)},isHexDigit:function(h){return typeof h=="string"&&/[0-9A-Fa-f]/.test(h)}},we,U,se,oe,q,he,Z,At,Ke,mn=function(h,F){we=String(h),U="start",se=[],oe=0,q=1,he=0,Z=void 0,At=void 0,Ke=void 0;do Z=pn(),yn[U]();while(Z.type!=="eof");return typeof F=="function"?yt({"":Ke},"",F):Ke};function yt(a,h,F){var E=a[h];if(E!=null&&typeof E=="object")if(Array.isArray(E))for(var B=0;B0;){var F=Fe();if(!G.isHexDigit(F))throw H(D());a+=D()}return String.fromCodePoint(parseInt(a,16))}var yn={start:function(){if(Z.type==="eof")throw Pe();bt()},beforePropertyName:function(){switch(Z.type){case"identifier":case"string":At=Z.value,U="afterPropertyName";return;case"punctuator":et();return;case"eof":throw Pe()}},afterPropertyName:function(){if(Z.type==="eof")throw Pe();U="beforePropertyValue"},beforePropertyValue:function(){if(Z.type==="eof")throw Pe();bt()},beforeArrayValue:function(){if(Z.type==="eof")throw Pe();if(Z.type==="punctuator"&&Z.value==="]"){et();return}bt()},afterPropertyValue:function(){if(Z.type==="eof")throw Pe();switch(Z.value){case",":U="beforePropertyName";return;case"}":et()}},afterArrayValue:function(){if(Z.type==="eof")throw Pe();switch(Z.value){case",":U="beforeArrayValue";return;case"]":et()}},end:function(){}};function bt(){var a;switch(Z.type){case"punctuator":switch(Z.value){case"{":a={};break;case"[":a=[];break}break;case"null":case"boolean":case"numeric":case"string":a=Z.value;break}if(Ke===void 0)Ke=a;else{var h=se[se.length-1];Array.isArray(h)?h.push(a):Object.defineProperty(h,At,{value:a,writable:!0,enumerable:!0,configurable:!0})}if(a!==null&&typeof a=="object")se.push(a),Array.isArray(a)?U="beforeArrayValue":U="beforePropertyName";else{var F=se[se.length-1];F==null?U="end":Array.isArray(F)?U="afterArrayValue":U="afterPropertyValue"}}function et(){se.pop();var a=se[se.length-1];a==null?U="end":Array.isArray(a)?U="afterArrayValue":U="afterPropertyValue"}function H(a){return tt(a===void 0?"JSON5: invalid end of input at "+q+":"+he:"JSON5: invalid character '"+Tt(a)+"' at "+q+":"+he)}function Pe(){return tt("JSON5: invalid end of input at "+q+":"+he)}function Pt(){return he-=5,tt("JSON5: invalid identifier character at "+q+":"+he)}function Fn(a){console.warn("JSON5: '"+Tt(a)+"' in strings is not valid ECMAScript; consider escaping")}function Tt(a){var h={"'":"\\'",'"':'\\"',"\\":"\\\\","\b":"\\b","\f":"\\f","\n":"\\n","\r":"\\r"," ":"\\t","\v":"\\v","\0":"\\0","\u2028":"\\u2028","\u2029":"\\u2029"};if(h[a])return h[a];if(a<" "){var F=a.charCodeAt(0).toString(16);return"\\x"+("00"+F).substring(F.length)}return a}function tt(a){var h=new SyntaxError(a);return h.lineNumber=q,h.columnNumber=he,h}var bn=function(h,F,E){var B=[],T="",I,ue,$="",Te;if(F!=null&&typeof F=="object"&&!Array.isArray(F)&&(E=F.space,Te=F.quote,F=F.replacer),typeof F=="function")ue=F;else if(Array.isArray(F)){I=[];for(var We=0,de=F;We0&&(E=Math.min(10,Math.floor(E)),$=" ".substr(0,E)):typeof E=="string"&&($=E.substr(0,10)),Ge("",{"":h});function Ge(L,ne){var x=ne[L];switch(x!=null&&(typeof x.toJSON5=="function"?x=x.toJSON5(L):typeof x.toJSON=="function"&&(x=x.toJSON(L))),ue&&(x=ue.call(ne,L,x)),x instanceof Number?x=Number(x):x instanceof String?x=String(x):x instanceof Boolean&&(x=x.valueOf()),x){case null:return"null";case!0:return"true";case!1:return"false"}if(typeof x=="string")return nt(x,!1);if(typeof x=="number")return String(x);if(typeof x=="object")return Array.isArray(x)?Mn(x):Bn(x)}function nt(L){for(var ne={"'":.1,'"':.2},x={"'":"\\'",'"':'\\"',"\\":"\\\\","\b":"\\b","\f":"\\f","\n":"\\n","\r":"\\r"," ":"\\t","\v":"\\v","\0":"\\0","\u2028":"\\u2028","\u2029":"\\u2029"},te="",fe=0;fe=0)throw TypeError("Converting circular structure to JSON5");B.push(L);var ne=T;T=T+$;for(var x=I||Object.keys(L),te=[],fe=0,ie=x;fe=0)throw TypeError("Converting circular structure to JSON5");B.push(L);var ne=T;T=T+$;for(var x=[],te=0;te{"use strict";Object.defineProperty(ut,"__esModule",{value:!0});ut.boolean=void 0;var Vn=function(o){switch(Object.prototype.toString.call(o)){case"[object String]":return["true","t","yes","y","on","1"].includes(o.trim().toLowerCase());case"[object Number]":return o.valueOf()===1;case"[object Boolean]":return o.valueOf();default:return!1}};ut.boolean=Vn});var Yt=Se(at=>{"use strict";Object.defineProperty(at,"__esModule",{value:!0});at.isBooleanable=void 0;var On=function(o){switch(Object.prototype.toString.call(o)){case"[object String]":return["true","t","yes","y","on","1","false","f","no","n","off","0"].includes(o.trim().toLowerCase());case"[object Number]":return[0,1].includes(o.valueOf());case"[object Boolean]":return!0;default:return!1}};at.isBooleanable=On});var Ht=Se(He=>{"use strict";Object.defineProperty(He,"__esModule",{value:!0});He.isBooleanable=He.boolean=void 0;var Ln=Gt();Object.defineProperty(He,"boolean",{enumerable:!0,get:function(){return Ln.boolean}});var Wn=Yt();Object.defineProperty(He,"isBooleanable",{enumerable:!0,get:function(){return Wn.isBooleanable}})});var jt=Se(ot=>{"use strict";Object.defineProperty(ot,"__esModule",{value:!0});ot.tokenize=void 0;var Gn=/(?:%(?([+0-]|-\+))?(?\d+)?(?\d+\$)?(?\.\d+)?(?[%BCESb-iosux]))|(\\%)/g,Yn=o=>{let e,t=[],i=0,u=0,d=null;for(;(e=Gn.exec(o))!==null;){e.index>u&&(d={literal:o.slice(u,e.index),type:"literal"},t.push(d));let y=e[0];u=e.index+y.length,y==="\\%"||y==="%%"?d&&d.type==="literal"?d.literal+="%":(d={literal:"%",type:"literal"},t.push(d)):e.groups&&(d={conversion:e.groups.conversion,flag:e.groups.flag||null,placeholder:y,position:e.groups.position?Number.parseInt(e.groups.position,10)-1:i++,precision:e.groups.precision?Number.parseInt(e.groups.precision.slice(1),10):null,type:"placeholder",width:e.groups.width?Number.parseInt(e.groups.width,10):null},t.push(d))}return u<=o.length-1&&(d&&d.type==="literal"?d.literal+=o.slice(u):t.push({literal:o.slice(u),type:"literal"})),t};ot.tokenize=Yn});var qt=Se(st=>{"use strict";Object.defineProperty(st,"__esModule",{value:!0});st.createPrintf=void 0;var zt=Ht(),Hn=jt(),jn=(o,e)=>e.placeholder,zn=o=>{var e;let t=(d,y,_)=>_==="-"?d.padEnd(y," "):_==="-+"?((Number(d)>=0?"+":"")+d).padEnd(y," "):_==="+"?((Number(d)>=0?"+":"")+d).padStart(y," "):_==="0"?d.padStart(y,"0"):d.padStart(y," "),i=(e=o?.formatUnboundExpression)!==null&&e!==void 0?e:jn,u={};return(d,...y)=>{let _=u[d];_||(_=u[d]=Hn.tokenize(d));let b="";for(let A of _)if(A.type==="literal")b+=A.literal;else{let v=y[A.position];if(v===void 0)b+=i(d,A,y);else if(A.conversion==="b")b+=zt.boolean(v)?"true":"false";else if(A.conversion==="B")b+=zt.boolean(v)?"TRUE":"FALSE";else if(A.conversion==="c")b+=v;else if(A.conversion==="C")b+=String(v).toUpperCase();else if(A.conversion==="i"||A.conversion==="d")v=String(Math.trunc(v)),A.width!==null&&(v=t(v,A.width,A.flag)),b+=v;else if(A.conversion==="e")b+=Number(v).toExponential();else if(A.conversion==="E")b+=Number(v).toExponential().toUpperCase();else if(A.conversion==="f")A.precision!==null&&(v=Number(v).toFixed(A.precision)),A.width!==null&&(v=t(String(v),A.width,A.flag)),b+=v;else if(A.conversion==="o")b+=(Number.parseInt(String(v),10)>>>0).toString(8);else if(A.conversion==="s")A.width!==null&&(v=t(String(v),A.width,A.flag)),b+=v;else if(A.conversion==="S")A.width!==null&&(v=t(String(v),A.width,A.flag)),b+=String(v).toUpperCase();else if(A.conversion==="u")b+=Number.parseInt(String(v),10)>>>0;else if(A.conversion==="x")v=(Number.parseInt(String(v),10)>>>0).toString(16),A.width!==null&&(v=t(String(v),A.width,A.flag)),b+=v;else throw new Error("Unknown format specifier.")}return b}};st.createPrintf=zn});var Kt=Se(je=>{"use strict";Object.defineProperty(je,"__esModule",{value:!0});je.printf=je.createPrintf=void 0;var Ut=qt();Object.defineProperty(je,"createPrintf",{enumerable:!0,get:function(){return Ut.createPrintf}});je.printf=Ut.createPrintf()});var Cn=Se((fn,Dn)=>{(function(o){var e=Object.hasOwnProperty,t=Array.isArray?Array.isArray:function(r){return Object.prototype.toString.call(r)==="[object Array]"},i=10,u=typeof process=="object"&&typeof process.nextTick=="function",d=typeof Symbol=="function",y=typeof Reflect=="object",_=typeof setImmediate=="function",b=_?setImmediate:setTimeout,A=d?y&&typeof Reflect.ownKeys=="function"?Reflect.ownKeys:function(n){var r=Object.getOwnPropertyNames(n);return r.push.apply(r,Object.getOwnPropertySymbols(n)),r}:Object.keys;function v(){this._events={},this._conf&&re.call(this,this._conf)}function re(n){n&&(this._conf=n,n.delimiter&&(this.delimiter=n.delimiter),n.maxListeners!==o&&(this._maxListeners=n.maxListeners),n.wildcard&&(this.wildcard=n.wildcard),n.newListener&&(this._newListener=n.newListener),n.removeListener&&(this._removeListener=n.removeListener),n.verboseMemoryLeak&&(this.verboseMemoryLeak=n.verboseMemoryLeak),n.ignoreErrors&&(this.ignoreErrors=n.ignoreErrors),this.wildcard&&(this.listenerTree={}))}function J(n,r){var s="(node) warning: possible EventEmitter memory leak detected. "+n+" listeners added. Use emitter.setMaxListeners() to increase limit.";if(this.verboseMemoryLeak&&(s+=" Event name: "+r+"."),typeof process<"u"&&process.emitWarning){var l=new Error(s);l.name="MaxListenersExceededWarning",l.emitter=this,l.count=n,process.emitWarning(l)}else console.error(s),console.trace&&console.trace()}var z=function(n,r,s){var l=arguments.length;switch(l){case 0:return[];case 1:return[n];case 2:return[n,r];case 3:return[n,r,s];default:for(var c=new Array(l);l--;)c[l]=arguments[l];return c}};function Q(n,r){for(var s={},l,c=n.length,f=r?r.length:0,C=0;C0;)n=f[g],C.call(p,n,s[n]);this._listeners={},this._listenersCount=0,m()}}});function Ee(n,r,s,l){var c=Object.assign({},r);if(!n)return c;if(typeof n!="object")throw TypeError("options must be an object");var f=Object.keys(n),C=f.length,p,g,m;function R(k){throw Error('Invalid "'+p+'" option value'+(k?". Reason: "+k:""))}for(var X=0;X0;)if(p===n[g])return f;C(r)}}var Ce=Be(["function"]),Oe=Be(["object","function"]);function _e(n,r,s){var l,c,f=0,C,p=new n(function(g,m,R){s=Ee(s,{timeout:0,overload:!1},{timeout:function(j,K){return j*=1,(typeof j!="number"||j<0||!Number.isFinite(j))&&K("timeout must be a positive number"),j}}),l=!s.overload&&typeof n.prototype.cancel=="function"&&typeof R=="function";function X(){c&&(c=null),f&&(clearTimeout(f),f=0)}var k=function(j){X(),g(j)},V=function(j){X(),m(j)};l?r(k,V,R):(c=[function(j){V(j||Error("canceled"))}],r(k,V,function(j){if(C)throw Error("Unable to subscribe on cancel event asynchronously");if(typeof j!="function")throw TypeError("onCancel callback must be a function");c.push(j)}),C=!0),s.timeout>0&&(f=setTimeout(function(){var j=Error("timeout");j.code="ETIMEDOUT",f=0,p.cancel(j),m(j)},s.timeout))});return l||(p.cancel=function(g){if(c){for(var m=c.length,R=1;R0;)V=oe[p],V!=="_listeners"&&(q=O(n,r,s[V],l+1,c),q&&(k?k.push.apply(k,q):k=q));return k}else if(U==="**"){for(we=l+1===c||l+2===c&&se==="*",we&&s._listeners&&(k=O(n,r,s,c,c)),oe=A(s),p=oe.length;p-- >0;)V=oe[p],V!=="_listeners"&&(V==="*"||V==="**"?(s[V]._listeners&&!we&&(q=O(n,r,s[V],c,c),q&&(k?k.push.apply(k,q):k=q)),q=O(n,r,s[V],l,c)):V===se?q=O(n,r,s[V],l+2,c):q=O(n,r,s[V],l,c),q&&(k?k.push.apply(k,q):k=q));return k}else s[U]&&(k=O(n,r,s[U],l+1,c));if(j=s["*"],j&&O(n,r,j,l+1,c),K=s["**"],K)if(l0;)V=oe[p],V!=="_listeners"&&(V===se?O(n,r,K[V],l+2,c):V===U?O(n,r,K[V],l+1,c):(G={},G[V]=K[V],O(n,r,{"**":G},l+1,c)));else K._listeners?O(n,r,K,c,c):K["*"]&&K["*"]._listeners&&O(n,r,K["*"],c,c);return k}function ge(n,r,s){var l=0,c=0,f,C=this.delimiter,p=C.length,g;if(typeof n=="string")if((f=n.indexOf(C))!==-1){g=new Array(5);do g[l++]=n.slice(c,f),c=f+p;while((f=n.indexOf(C,c))!==-1);g[l++]=n.slice(c)}else g=[n],l=1;else g=n,l=n.length;if(l>1){for(f=0;f+10&&m._listeners.length>this._maxListeners&&(m._listeners.warned=!0,J.call(this,m._listeners.length,R))):m._listeners=r,!0;return!0}function Le(n,r,s,l){for(var c=A(n),f=c.length,C,p,g,m=n._listeners,R;f-- >0;)p=c[f],C=n[p],p==="_listeners"?g=s:g=s?s.concat(p):[p],R=l||typeof p=="symbol",m&&r.push(R?g:g.join(this.delimiter)),typeof C=="object"&&Le.call(this,C,r,g,R);return r}function Ue(n){for(var r=A(n),s=r.length,l,c,f;s-- >0;)c=r[s],l=n[c],l&&(f=!0,c!=="_listeners"&&!Ue(l)&&delete n[c]);return f}function Ze(n,r,s){this.emitter=n,this.event=r,this.listener=s}Ze.prototype.off=function(){return this.emitter.off(this.event,this.listener),this};function Qe(n,r,s){if(s===!0)c=!0;else if(s===!1)l=!0;else{if(!s||typeof s!="object")throw TypeError("options should be an object or true");var l=s.async,c=s.promisify,f=s.nextTick,C=s.objectify}if(l||f||c){var p=r,g=r._origin||r;if(f&&!u)throw Error("process.nextTick is not supported");c===o&&(c=r.constructor.name==="AsyncFunction"),r=function(){var m=arguments,R=this,X=this.event;return c?f?Promise.resolve():new Promise(function(k){b(k)}).then(function(){return R.event=X,p.apply(R,m)}):(f?process.nextTick:b)(function(){R.event=X,p.apply(R,m)})},r._async=!0,r._origin=g}return[r,C?new Ze(this,n,r):this]}function M(n){this._events={},this._newListener=!1,this._removeListener=!1,this.verboseMemoryLeak=!1,re.call(this,n)}M.EventEmitter2=M,M.prototype.listenTo=function(n,r,s){if(typeof n!="object")throw TypeError("target musts be an object");var l=this;s=Ee(s,{on:o,off:o,reducers:o},{on:Ce,off:Ce,reducers:Oe});function c(f){if(typeof f!="object")throw TypeError("events must be an object");var C=s.reducers,p=Me.call(l,n),g;p===-1?g=new le(l,n,s):g=l._observers[p];for(var m=A(f),R=m.length,X,k=typeof C=="function",V=0;V0;)c=s[l],(!n||c._target===n)&&(c.unsubscribe(r),f=!0);return f},M.prototype.delimiter=".",M.prototype.setMaxListeners=function(n){n!==o&&(this._maxListeners=n,this._conf||(this._conf={}),this._conf.maxListeners=n)},M.prototype.getMaxListeners=function(){return this._maxListeners},M.prototype.event="",M.prototype.once=function(n,r,s){return this._once(n,r,!1,s)},M.prototype.prependOnceListener=function(n,r,s){return this._once(n,r,!0,s)},M.prototype._once=function(n,r,s,l){return this._many(n,1,r,s,l)},M.prototype.many=function(n,r,s,l){return this._many(n,r,s,!1,l)},M.prototype.prependMany=function(n,r,s,l){return this._many(n,r,s,!0,l)},M.prototype._many=function(n,r,s,l,c){var f=this;if(typeof s!="function")throw new Error("many only accepts instances of Function");function C(){return--r===0&&f.off(n,C),s.apply(this,arguments)}return C._origin=s,this._on(n,C,l,c)},M.prototype.emit=function(){if(!this._events&&!this._all)return!1;this._events||v.call(this);var n=arguments[0],r,s=this.wildcard,l,c,f,C,p;if(n==="newListener"&&!this._newListener&&!this._events.newListener)return!1;if(s&&(r=n,n!=="newListener"&&n!=="removeListener"&&typeof n=="object")){if(c=n.length,d){for(f=0;f3)for(l=new Array(g-1),C=1;C3)for(c=new Array(m-1),p=1;p0&&this._events[n].length>this._maxListeners&&(this._events[n].warned=!0,J.call(this,this._events[n].length,n))):this._events[n]=r,c)},M.prototype.off=function(n,r){if(typeof r!="function")throw new Error("removeListener only takes instances of Function");var s,l=[];if(this.wildcard){var c=typeof n=="string"?n.split(this.delimiter):n.slice();if(l=O.call(this,null,c,this.listenerTree,0),!l)return this}else{if(!this._events[n])return this;s=this._events[n],l.push({_listeners:s})}for(var f=0;f0){for(l=this._all,r=0,s=l.length;r0;)l=r[s[f]],typeof l=="function"?c.push(l):c.push.apply(c,l);return c}else{if(this.wildcard){if(C=this.listenerTree,!C)return[];var p=[],g=typeof n=="string"?n.split(this.delimiter):n.slice();return O.call(this,p,g,C,0),p}return r?(l=r[n],l?typeof l=="function"?[l]:l:[]):[]}},M.prototype.eventNames=function(n){var r=this._events;return this.wildcard?Le.call(this,this.listenerTree,[],null,n):r?A(r):[]},M.prototype.listenerCount=function(n){return this.listeners(n).length},M.prototype.hasListeners=function(n){if(this.wildcard){var r=[],s=typeof n=="string"?n.split(this.delimiter):n.slice();return O.call(this,r,s,this.listenerTree,0),r.length>0}var l=this._events,c=this._all;return!!(c&&c.length||l&&(n===o?A(l).length:l[n]))},M.prototype.listenersAny=function(){return this._all?this._all:[]},M.prototype.waitFor=function(n,r){var s=this,l=typeof r;return l==="number"?r={timeout:r}:l==="function"&&(r={filter:r}),r=Ee(r,{timeout:0,filter:o,handleError:!1,Promise,overload:!1},{filter:Ce,Promise:ce}),_e(r.Promise,function(c,f,C){function p(){var g=r.filter;if(!(g&&!g.apply(s,arguments)))if(s.off(n,p),r.handleError){var m=arguments[0];m?f(m):c(z.apply(null,arguments).slice(1))}else c(z.apply(null,arguments))}C(function(){s.off(n,p)}),s._on(n,p,!1)},{timeout:r.timeout,overload:r.overload})};function ee(n,r,s){s=Ee(s,{Promise,timeout:0,overload:!1},{Promise:ce});var l=s.Promise;return _e(l,function(c,f,C){var p;if(typeof n.addEventListener=="function"){p=function(){c(z.apply(null,arguments))},C(function(){n.removeEventListener(r,p)}),n.addEventListener(r,p,{once:!0});return}var g=function(){m&&n.removeListener("error",m),c(z.apply(null,arguments))},m;r!=="error"&&(m=function(R){n.removeListener(r,g),f(R)},n.once("error",m)),C(function(){m&&n.removeListener("error",m),n.removeListener(r,g)}),n.once(r,g)},{timeout:s.timeout,overload:s.overload})}var Ae=M.prototype;if(Object.defineProperties(M,{defaultMaxListeners:{get:function(){return Ae._maxListeners},set:function(n){if(typeof n!="number"||n<0||Number.isNaN(n))throw TypeError("n must be a non-negative number");Ae._maxListeners=n},enumerable:!0},once:{value:ee,writable:!0,configurable:!0}}),Object.defineProperties(Ae,{_maxListeners:{value:i,writable:!0,configurable:!0},_observers:{value:null,writable:!0,configurable:!0}}),typeof define=="function"&&define.amd)define(function(){return M});else if(typeof fn=="object")Dn.exports=M;else{var Et=new Function("","return this")();Et.EventEmitter2=M}})()});var It=PlayerId(),S=new Proxy({playerId:It,serverId:GetPlayerServerId(It),ped:PlayerPedId(),vehicle:!1,seat:!1,resource:GetCurrentResourceName(),game:GetGameName()},{set(o,e,t){return o[e]===t||(o[e]=t,emit(`yaca:cache:${e}`,t)),!0},get(o,e){return o[e]}});function kt(){setInterval(()=>{let e=PlayerPedId();S.ped=e;let t=GetVehiclePedIsIn(e,!1);if(t>0){if(t!==S.vehicle&&(S.seat=!1),S.vehicle=t,!S.seat||GetPedInVehicleSeat(t,S.seat)!==e){for(let i=-1;i{on(`yaca:cache:${o}`,e)};function De(o,e){on(`__cfx_export_saltychat_${o}`,t=>{t(e)})}var Lt=vt(Ot());function Wt(o,e,t=[]){let i={...o};for(let u in o){if(Object.prototype.hasOwnProperty.call(o,u)===!1)continue;let d=[...t,u].join(".");u in e?typeof o[u]=="object"&&o[u]!==null&&!Array.isArray(o[u])&&typeof e[u]=="object"&&e[u]!==null&&!Array.isArray(e[u])?i[u]=Wt(o[u],e[u],[...t,u]):i[u]=e[u]:console.warn(`[YaCA] Missing config value for key '${d}' setting to default value: ${o[u]} +Missing config values can cause unexpected behavior of the script.`)}for(let u of Object.keys(e)){let d=[...t,u].join(".");u in o||console.warn(`[YaCA] Unknown config key '${d}' found in config file. This key will be ignored and can be removed.`)}return i}function Bt(o,e){let t=LoadResourceFile(GetCurrentResourceName(),o);if(!t)return e;let i=Lt.default.parse(t);return Wt(e,i)}var rt="yacaMegaphone",_t="yacaPhoneSpeaker",Mt="yacaLipSync",Je="yacaVoiceRange",pe="yacaGlobalErrorLevel";var Jt=vt(Kt()),$t=GetCurrentResourceName(),Xt={};function Zt(o,e,t){for(let[i,u]of Object.entries(o)){let d=t?`${t}.${i}`:i;typeof u=="object"?Zt(u,e,d):e[d]=String(u)}return e}var W=(o,...e)=>{let t=Xt[o];return t?e.length>0?(0,Jt.printf)(t,...e):t:o};var Qt=o=>{let e=o||"en",t=JSON.parse(LoadResourceFile($t,`locales/${e}.json`));if(!t&&(console.warn(`could not load 'locales/${e}.json'`),e!=="en"&&(t=JSON.parse(LoadResourceFile($t,"locales/en.json")),t||console.warn("could not load 'locales/en.json'")),!t))return;let i=Zt(t,{});for(let[u,d]of Object.entries(i)){let y=new RegExp(/\$\{([^}]+)}/g),_=d.match(y);if(_)for(let b of _){if(!b)break;let A=b.substring(2,b.length-1),v=i[A];v&&(i[u]=d.replace(b,v))}Xt[u]=d}};function lt(o){return new Promise(e=>setTimeout(e,o,null))}function Ye(o,e=0,t=1){return Math.max(e,Math.min(t,o))}async function en(o,e,t){let i=await o();if(i!==void 0)return i;(t||t==null)&&typeof t!="number"&&(t=1e3);let u=GetGameTimer(),d;return new Promise((y,_)=>{d=setTick(async()=>{let b=t&&GetGameTimer()-u;if(b&&b>t)return _(`${e||"failed to resolve callback"} (waited ${b}ms)`);i=await o(),i!==void 0&&y(i)})}).finally(()=>clearTick(d))}async function tn(o,e,t,i,u=3e4){return e(i)?i:(o(i),en(()=>{if(e(i))return i},`failed to load ${t} '${i}' - this may be caused by +- too many loaded assets +- oversized, invalid, or corrupted assets`,u))}var ct=o=>{if(!DoesAnimDictExist(o))throw new Error(`attempted to load invalid animDict '${o}'`);return tn(RequestAnimDict,HasAnimDictLoaded,"animDict",o)},nn=o=>{if(typeof o!="number"&&(o=ze(o)),!IsModelValid(o))throw new Error(`attempted to load invalid model '${o}'`);return tn(RequestModel,HasModelLoaded,"model",o)};var ze=(o,e=!0)=>{o=e?o:o.toLowerCase();let t=o.length,i,u;for(i=u=0;u>>6;return i+=i<<3,i^=i>>>11,i+=i<<15,i>>>0},rn=async(o,e,t=[0,0,0],i=[0,0,0])=>{let u=await nn(o);if(!u)return;let[d,y,_]=GetEntityCoords(S.ped,!0),[b,A,v]=t,[re,J,z]=i,Q=CreateObject(u,d,y,_,!0,!0,!1);return SetEntityCollision(Q,!1,!1),AttachEntityToEntity(Q,S.ped,GetPedBoneIndex(S.ped,e),b,A,v,re,J,z,!0,!1,!1,!0,2,!0),Q};var un={fivem:{true:{name:"mic_chatter",dict:"mp_facial"},false:{name:"mood_normal_1",dict:"facials@gen_male@variations@normal"}},redm:{true:{name:"mood_talking_normal",dict:"face_human@gen_male@base"},false:{name:"mood_normal",dict:"face_human@gen_male@base"}}},an={A:1885667965,B:1287709438,C:2572789488,D:3034867124,E:3472724512,F:3002300392,G:1980406895,H:613911080,I:3248005013,J:4085452174,K:null,L:2163379861,M:3810290241,N:1271519931,O:4046460518,P:3626896338,Q:3732491838,R:3809269511,S:3531047651,T:null,U:3640078424,V:2139949496,W:2412778968,X:2362035522,Y:null,Z:652860416,RIGHTBRACKET:2780679484,LEFTBRACKET:1124438954,MOUSE1:130948705,MOUSE2:4165969743,MOUSE3:3470863184,MWUP:813099388,CTRL:3674827653,TAB:2990079499,SHIFT:2415687126,SPACEBAR:3654345152,ENTER:3350541322,BACKSPACE:359624985,LALT:2326399700,DEL:1257559155,PGUP:1147295926,PGDN:1010684785,F1:2833511527,F4:527275493,F6:1007304946,1:3874886372,2:484891115,3:1330236492,4:2409602648,5:2875386263,6:2717770406,7:2956628283,8:1110987810,DOWN:97156178,UP:1662638961,LEFT:2791226036,RIGHT:3736290067};var ke=(y=>(y.RADIO="RADIO",y.MEGAPHONE="MEGAPHONE",y.PHONE="PHONE",y.PHONE_SPEAKER="PHONE_SPEAKER",y.INTERCOM="INTERCOM",y.PHONE_HISTORICAL="PHONE_HISTORICAL",y))(ke||{});var wt={versionCheck:!0,autoConnectOnJoin:!0,buildType:0,locale:"en",unmuteDelay:400,maxPhoneSpeakerRange:5,phoneHearPlayersNearby:!1,notifications:{oxLib:!1,okoknotify:!1,gta:!0,redm:!1,own:!1},keyBinds:{increaseVoiceRange:"ADD",decreaseVoiceRange:"SUBTRACT",primaryRadioTransmit:"N",secondaryRadioTransmit:"CAPITAL",megaphone:"B",voiceRangeWithMouseWheel:"LCONTROL"},radioSettings:{animation:{dictionary:"random@arrests",name:"generic_radio_chatter",flag:49},propWhileTalking:{prop:!1,boneId:28422,position:[0,0,0],rotation:[0,0,0]},channelCount:9,mode:"None",maxDistance:1e3},voiceRange:{defaultIndex:1,ranges:[1,3,8,15,20,25,30,40],sendNotification:!0,markerColor:{enabled:!0,r:0,g:255,b:0,a:50,duration:1e3,type:1,rotate:!0}},megaphone:{range:30,automaticVehicleDetection:!0,allowedVehicleClasses:[18,19],allowedVehicleModels:["polmav"]},saltyChatBridge:!1,mufflingSettings:{mufflingRange:-1,vehicleMuffling:{enabled:!0,vehicleWhitelist:["gauntlet6","draugur","bodhi2","vagrant","outlaw","trophytruck","ratel","drifttampa","sm722","tornado4","swinger","locust","hotring"]},intensities:{differentRoom:10,bothCarsClosed:10,oneCarClosed:6,megaPhoneInCar:6}},radioAntiSpamCooldown:!1,useLocalLipSync:!1};var sn={towerPositions:[[2572,5397,56],[2663,4972,56],[2892,3911,56],[2720,3304,64],[2388,2949,64],[1830,2368,64],[1650,1316,102],[1363,680,102],[918,230,92],[567,303,58],[-47,-666,74],[-585,-902,53],[2572,5397,56],[2338,5940,77],[1916,6244,65],[1591,6371,42],[953,6504,42],[76,6606,42],[408,6587,42],[-338,-579,48],[-293,-632,47],[-269,-962,143],[98,-870,136],[-214,-744,219],[-166,-590,199],[124,-654,261],[149,-769,261],[580,89,117],[423,15,151],[424,18,151],[551,-28,93],[305,-284,68],[299,-313,68],[1240,-1090,44],[-418,-2804,14],[802,-2996,27],[253,-3145,39],[207,-3145,39],[207,-3307,39],[247,-3307,39],[484,-2178,40],[548,-2219,67],[-701,58,68],[-696,208,139],[-769,255,134],[-150,-150,96],[-202,-327,65],[-1913,-3031,22],[-1918,-3028,22],[-1039,-2385,27],[-1042,-2390,27],[-1583,-3216,28],[-1590,-3212,28],[-1308,-2626,36],[-1311,-2624,36],[-984,-2778,48],[-991,-2774,48],[-556,-119,50],[-619,-106,51],[-1167,-575,40],[-1152,-443,42],[-1156,-498,49],[-1290,-445,106],[-928,-383,135],[-902,-443,170],[-770,-786,83],[-824,-719,120],[-598,-917,35],[-678,-717,54],[-669,-804,31],[-1463,-526,83],[-1525,-596,66],[-1375,-465,83],[-1711,478,127],[-2311,335,187],[-2214,342,198],[-2234,187,193],[202,1204,230],[217,1140,230],[668,590,136],[722,562,134],[838,510,138],[773,575,138],[735,231,145],[450,5566,795],[-449,6019,35],[-142,6286,39],[-368,6105,38],[2792,5996,355],[2796,5992,354],[3460,3653,51],[3459,3659,51],[3615,3642,51],[3614,3636,51],[-2180,3252,54],[-2124,3219,54],[-2050,3178,54],[1858,3694,37],[1695,3614,37],[1692,2532,60],[1692,2647,60],[1824,2574,60],[1407,2117,104]]};var ht=class{constructor(e){this.clientModule=e,this.registerEvents()}registerEvents(){onNet("client:yaca:addRemovePlayerIntercomFilter",(e,t)=>{Array.isArray(e)||(e=[e]);let i=new Set;for(let u of e){let d=this.clientModule.getPlayerByID(u);d&&i.add(d)}i.size<1||this.clientModule.setPlayersCommType(Array.from(i),"INTERCOM",t,void 0,void 0,2,2)})}};var ft=class{constructor(e){this.currentPluginState=-1;this.isPrimarySending=!1;this.isSecondarySending=!1;this.isPrimaryReceiving=!1;this.isSecondaryReceiving=!1;this.clientModule=e,this.registerSaltyChatExports(),this.enableRadio().then(),console.log("[YaCA] SaltyChat bridge loaded"),on("onResourceStop",t=>{S.resource===t&&emit("onClientResourceStop","saltychat")})}async enableRadio(){for(;!this.clientModule.isPluginInitialized(!0);)await lt(1e3);this.clientModule.radioModule.enableRadio(!0)}registerSaltyChatExports(){De("GetVoiceRange",()=>this.clientModule.getVoiceRange()),De("GetRadioChannel",e=>{let t=e?1:2,i=this.clientModule.radioModule.getRadioFrequency(t);return i==="0"?"":i}),De("GetRadioVolume",()=>this.clientModule.radioModule.getRadioChannelVolume(1)),De("GetRadioSpeaker",()=>(console.warn("GetRadioSpeaker is not implemented in YaCA"),!1)),De("GetMicClick",()=>(console.warn("GetMicClick is not implemented in YaCA"),!1)),De("SetRadioChannel",(e,t)=>{let i=t?1:2,u=e===""?"0":e;this.clientModule.radioModule.changeRadioFrequencyRaw(u,i)}),De("SetRadioVolume",e=>{this.clientModule.radioModule.changeRadioChannelVolumeRaw(e,1),this.clientModule.radioModule.changeRadioChannelVolumeRaw(e,2)}),De("SetRadioSpeaker",()=>{console.warn("SetRadioSpeaker is not implemented in YaCA")}),De("SetMicClick",()=>{console.warn("SetMicClick is not implemented in YaCA")}),De("GetPluginState",()=>this.currentPluginState)}handleChangePluginState(e){let t=0;switch(e){case"IN_EXCLUDED_CHANNEL":t=3;break;case"IN_INGAME_CHANNEL":t=2;break;case"CONNECTED":t=1;break;case"WRONG_TS_SERVER":case"OUTDATED_VERSION":t=0;break;case"NOT_CONNECTED":t=-1;break;default:return}emit("SaltyChat_PluginStateChanged",t),this.currentPluginState=t}sendRadioTalkingState(){emit("SaltyChat_RadioTrafficStateChanged",this.isPrimaryReceiving,this.isPrimarySending,this.isSecondaryReceiving,this.isSecondarySending)}handleRadioTalkingStateChange(e,t){t===1?this.isPrimarySending=e:this.isSecondarySending=e,this.sendRadioTalkingState()}handleRadioReceivingStateChange(e,t){t===1?this.isPrimaryReceiving=e:this.isSecondaryReceiving=e,this.sendRadioTalkingState()}};var Dt=class{constructor(e){this.canUseMegaphone=!1;this.lastMegaphoneState=!1;this.megaphoneVehicleWhitelistHashes=new Set;if(this.clientModule=e,this.registerEvents(),this.clientModule.isFiveM){this.registerKeybinds();for(let t of this.clientModule.sharedConfig.megaphone.allowedVehicleModels)this.megaphoneVehicleWhitelistHashes.add(ze(t))}else this.clientModule.isRedM&&this.registerRdrKeybinds();this.registerExports(),this.registerStateBagHandlers()}registerEvents(){onNet("client:yaca:setLastMegaphoneState",e=>{this.lastMegaphoneState=e}),this.clientModule.isFiveM&&this.clientModule.sharedConfig.megaphone.automaticVehicleDetection&&Vt("seat",e=>{if(e===!1||e>0||!S.vehicle){this.canUseMegaphone=!1,emitNet("server:yaca:playerLeftVehicle");return}let t=GetVehicleClass(S.vehicle),i=GetEntityModel(S.vehicle);this.canUseMegaphone=this.clientModule.sharedConfig.megaphone.allowedVehicleClasses.includes(t)||this.megaphoneVehicleWhitelistHashes.has(i)})}registerKeybinds(){this.clientModule.sharedConfig.keyBinds.megaphone!==!1&&(RegisterCommand("+yaca:megaphone",()=>{this.useMegaphone(!0)},!1),RegisterCommand("-yaca:megaphone",()=>{this.useMegaphone(!1)},!1),RegisterKeyMapping("+yaca:megaphone",W("use_megaphone"),"keyboard",this.clientModule.sharedConfig.keyBinds.megaphone))}registerRdrKeybinds(){this.clientModule.sharedConfig.keyBinds.megaphone!==!1&&ve(this.clientModule.sharedConfig.keyBinds.megaphone,()=>{this.useMegaphone(!this.lastMegaphoneState)})}registerExports(){exports("getCanUseMegaphone",()=>this.canUseMegaphone),exports("setCanUseMegaphone",e=>{this.canUseMegaphone=e,!e&&this.lastMegaphoneState&&emitNet("server:yaca:playerLeftVehicle")}),exports("useMegaphone",(e=!1)=>{this.useMegaphone(e)})}registerStateBagHandlers(){AddStateBagChangeHandler(rt,"",(e,t,i,u,d)=>{if(d)return;let y=GetPlayerFromStateBagName(e);if(y===0)return;let _=GetPlayerServerId(y);if(_!==0)if(_===S.serverId)this.clientModule.setPlayersCommType([],"MEGAPHONE",typeof i=="number",void 0,i,0,1);else{let b=this.clientModule.getPlayerByID(_);if(!b)return;this.clientModule.setPlayersCommType(b,"MEGAPHONE",typeof i=="number",void 0,i,1,0)}})}useMegaphone(e=!1){!S.vehicle&&this.clientModule.sharedConfig.megaphone.automaticVehicleDetection||!this.canUseMegaphone||e===this.lastMegaphoneState||(this.lastMegaphoneState=!this.lastMegaphoneState,emitNet("server:yaca:useMegaphone",e),emit("yaca:external:megaphoneState",e))}};var Ct=class{constructor(e){this.inCallWith=new Set;this.phoneSpeakerActive=!1;this.clientModule=e,this.registerEvents(),this.registerExports(),this.registerStateBagHandlers()}registerEvents(){onNet("client:yaca:phone",(e,t,i="PHONE")=>{Array.isArray(e)||(e=[e]),this.enablePhoneCall(e,t,i)}),onNet("client:yaca:phoneHearAround",(e,t)=>{if(!e.length)return;let i=Array.from(e).map(u=>({clientId:u}));this.clientModule.setPlayersCommType(i,"PHONE",t,void 0,void 0,2,2,GlobalState[_t]??void 0)}),onNet("client:yaca:phoneMute",(e,t,i=!1)=>{let u=this.clientModule.getPlayerByID(e);u&&(u.mutedOnPhone=t,!i&&(this.clientModule.useWhisper&&u.remoteID===S.serverId?this.clientModule.setPlayersCommType([],"PHONE",!t,void 0,void 0,0):!this.clientModule.useWhisper&&this.inCallWith.has(e)&&this.clientModule.setPlayersCommType(u,"PHONE",t,void 0,void 0,2,2)))}),onNet("client:yaca:playersToPhoneSpeakerEmitWhisper",(e,t)=>{if(!this.clientModule.useWhisper)return;Array.isArray(e)||(e=[e]);let i=new Set;for(let u of e){let d=this.clientModule.getPlayerByID(u);d&&i.add(d)}i.size<1||this.clientModule.setPlayersCommType(Array.from(i),"PHONE_SPEAKER",t,void 0,void 0,0,1)})}registerExports(){exports("isInCall",()=>this.inCallWith.size>0)}registerStateBagHandlers(){AddStateBagChangeHandler(_t,"",(e,t,i)=>{let u=GetPlayerFromStateBagName(e);if(u===0)return;let d=GetPlayerServerId(u);d!==0&&(d===S.serverId&&(this.phoneSpeakerActive=i!==null),this.removePhoneSpeakerFromEntity(d),i!==null&&this.clientModule.setPlayerVariable(d,"phoneCallMemberIds",Array.isArray(i)?i:[i]))})}removePhoneSpeakerFromEntity(e){let t=this.clientModule.getPlayerByID(e);if(!t?.phoneCallMemberIds)return;let i=[];for(let u of t.phoneCallMemberIds){let d=this.clientModule.getPlayerByID(u);d&&i.push(d)}this.clientModule.setPlayersCommType(i,"PHONE_SPEAKER",!1,void 0,void 0,1,0),t.phoneCallMemberIds=void 0}handleDisconnect(e){this.inCallWith.delete(e)}reestablishCalls(e){if(!this.inCallWith.size||(Array.isArray(e)||(e=[e]),!e.length))return;let t=[];for(let i of e)this.inCallWith.has(i)&&t.push(i);t.length&&this.enablePhoneCall(t,!0,"PHONE")}enablePhoneCall(e,t,i="PHONE"){if(!e.length)return;let u=[];for(let d of e){let y=this.clientModule.getPlayerByID(d);if(!y){t||this.inCallWith.delete(d);continue}t?this.inCallWith.add(d):this.inCallWith.delete(d),u.push(y)}this.clientModule.setPlayersCommType(u,i,t,void 0,void 0,t||!t&&this.inCallWith.size?2:void 0,2,GlobalState[pe]??void 0)}};var gt=class{constructor(e){this.radioEnabled=!1;this.radioInitialized=!1;this.talkingInChannels=new Set;this.radioChannelSettings=new Map;this.playersWithShortRange=new Map;this.playersInRadioChannel=new Map;this.radioTowerCalculation=new Map;this.radioMode="None";this.activeRadioChannel=1;this.secondaryRadioChannel=2;this.radioOnCooldown=!1;this.defaultRadioSettings={frequency:"0",muted:!1,volume:1,stereo:"STEREO"};this.clientModule=e,this.radioMode=this.clientModule.sharedConfig.radioSettings.mode,this.registerExports(),this.registerEvents(),this.clientModule.isFiveM?this.registerKeybinds():this.registerRdrKeybinds()}registerExports(){exports("enableRadio",e=>this.enableRadio(e)),exports("isRadioEnabled",()=>this.radioEnabled),exports("changeRadioFrequency",e=>this.changeRadioFrequencyRaw(e)),exports("changeRadioFrequencyRaw",(e,t)=>this.changeRadioFrequencyRaw(t,e)),exports("getRadioFrequency",e=>this.getRadioFrequency(e)),exports("muteRadioChannel",e=>this.muteRadioChannel(e)),exports("muteRadioChannelRaw",(e,t)=>this.muteRadioChannelRaw(e,t)),exports("isRadioChannelMuted",(e=this.activeRadioChannel)=>this.isRadioChannelMuted(e)),exports("setActiveRadioChannel",e=>this.setActiveRadioChannel(e)),exports("getActiveRadioChannel",()=>this.activeRadioChannel),exports("setSecondaryRadioChannel",e=>this.setSecondaryRadioChannel(e)),exports("getSecondaryRadioChannel",()=>this.secondaryRadioChannel),exports("changeRadioChannelVolume",e=>this.changeRadioChannelVolume(e)),exports("changeRadioChannelVolumeRaw",(e,t)=>this.changeRadioChannelVolumeRaw(t,e)),exports("getRadioChannelVolume",e=>this.getRadioChannelVolume(e)),exports("changeRadioChannelStereo",()=>this.changeRadioChannelStereo()),exports("changeRadioChannelStereoRaw",(e,t)=>this.changeRadioChannelStereoRaw(t,e)),exports("getRadioChannelStereo",e=>this.getRadioChannelStereo(e)),exports("radioTalkingStart",(e,t)=>this.radioTalkingStart(e,t)),exports("setRadioMode",e=>{this.radioMode=e}),exports("getRadioMode",()=>this.radioMode)}registerEvents(){onNet("client:yaca:setRadioFreq",(e,t)=>{this.setRadioFrequency(e,t)}),onNet("client:yaca:radioTalking",(e,t,i,u,d=-1,y=[0,0,0])=>{let _=this.findRadioChannelByFrequency(t);if(!_)return;let b=this.getDistanceToTowerOrSender(y);if(i&&(this.radioMode!=="None"&&b>this.clientModule.sharedConfig.radioSettings.maxDistance||this.radioMode==="Tower"&&d>this.clientModule.sharedConfig.radioSettings.maxDistance))return;let A=this.clientModule.getPlayerByID(e);if(!A)return;let v=u[S.serverId];if(!v?.shortRange||v?.shortRange&&GetPlayerFromServerId(e)!==-1){let re=this.getErrorLevelFromDistance(b,d);this.clientModule.setPlayersCommType(A,"RADIO",i,_,void 0,1,0,re)}if(i)this.playersInRadioChannel.get(_)?.add(e),v?.shortRange&&this.playersWithShortRange.set(e,t),emit("yaca:external:isRadioReceiving",!0,_),this.clientModule.saltyChatBridge?.handleRadioReceivingStateChange(!0,_);else{this.playersInRadioChannel.get(_)?.delete(e),v?.shortRange&&this.playersWithShortRange.delete(e);let J=(this.playersInRadioChannel.get(_)?.size||0)>0;emit("yaca:external:isRadioReceiving",J,_),this.clientModule.saltyChatBridge?.handleRadioReceivingStateChange(J,_)}}),onNet("client:yaca:radioTalkingWhisper",(e,t,i,u=[0,0,0])=>{let d=this.findRadioChannelByFrequency(t);if(!d)return;let y=this.getDistanceToTowerOrSender(u);i&&this.radioMode!=="None"&&y>this.clientModule.sharedConfig.radioSettings.maxDistance&&(e=[]),this.radioTalkingStateToPluginWithWhisper(i,e,d)}),onNet("client:yaca:setRadioMuteState",(e,t)=>{let i=this.radioChannelSettings.get(e);i&&(i.muted=t,emit("yaca:external:setRadioMuteState",e,t),this.disableRadioFromPlayerInChannel(e),this.updateRadioChannelData(e))}),onNet("client:yaca:leaveRadioChannel",(e,t)=>{Array.isArray(e)||(e=[e]);let i=this.findRadioChannelByFrequency(t);if(!i)return;let u=this.clientModule.getPlayerByID(S.serverId);!u||!u.clientId||(e.includes(u.clientId)&&this.setRadioFrequency(i,"0"),this.clientModule.sendWebsocket({base:{request_type:"INGAME"},comm_device_left:{comm_type:"RADIO",client_ids:e,channel:i}}))})}registerKeybinds(){this.clientModule.sharedConfig.keyBinds.primaryRadioTransmit!==!1&&(RegisterCommand("+yaca:radioTalking",()=>{this.radioTalkingStart(!0,this.activeRadioChannel)},!1),RegisterCommand("-yaca:radioTalking",()=>{this.radioTalkingStart(!1,this.activeRadioChannel)},!1),RegisterKeyMapping("+yaca:radioTalking",W("use_radio"),"keyboard",this.clientModule.sharedConfig.keyBinds.primaryRadioTransmit)),this.clientModule.sharedConfig.keyBinds.secondaryRadioTransmit!==!1&&(RegisterCommand("+yaca:secondaryRadioTalking",()=>{this.radioTalkingStart(!0,this.secondaryRadioChannel)},!1),RegisterCommand("-yaca:secondaryRadioTalking",()=>{this.radioTalkingStart(!1,this.secondaryRadioChannel)},!1),RegisterKeyMapping("+yaca:secondaryRadioTalking",W("use_secondary_radio"),"keyboard",this.clientModule.sharedConfig.keyBinds.secondaryRadioTransmit))}registerRdrKeybinds(){this.clientModule.sharedConfig.keyBinds.primaryRadioTransmit!==!1&&ve(this.clientModule.sharedConfig.keyBinds.primaryRadioTransmit,()=>{this.radioTalkingStart(!0,this.activeRadioChannel)},()=>{this.radioTalkingStart(!1,this.activeRadioChannel)}),this.clientModule.sharedConfig.keyBinds.secondaryRadioTransmit!==!1&&ve(this.clientModule.sharedConfig.keyBinds.secondaryRadioTransmit,()=>{this.radioTalkingStart(!0,this.secondaryRadioChannel)},()=>{this.radioTalkingStart(!1,this.secondaryRadioChannel)})}getErrorLevelFromDistance(e,t){let i,u=GlobalState[pe]||0;if(this.radioMode==="Tower"){let d=this.calculateSignalStrength(e),y=this.calculateSignalStrength(t);i=Math.max(d,y,u)}else if(this.radioMode==="Direct"){let d=this.calculateSignalStrength(e);i=Math.max(d,u)}else i=u;return i}getDistanceToTowerOrSender(e){let t=Number.MAX_VALUE;return this.radioMode==="Tower"?t=this.getNearestRadioTower():this.radioMode==="Direct"&&(t=Xe(GetEntityCoords(S.ped,!1),e)),t}enableRadio(e){if(this.clientModule.isPluginInitialized()&&this.radioEnabled!==e){if(this.radioEnabled=e,emitNet("server:yaca:enableRadio",e),!e)for(let t=1;t<=this.clientModule.sharedConfig.radioSettings.channelCount;t++)this.disableRadioFromPlayerInChannel(t);e&&!this.radioInitialized&&(this.radioInitialized=!0,this.initRadioSettings(),this.updateRadioChannelData(this.activeRadioChannel)),emit("yaca:external:isRadioEnabled",e)}}calculateSignalStrength(e,t=this.clientModule.sharedConfig.radioSettings.maxDistance){let i=e/t;return Ye(Math.log10(1+i*8.5)/Math.log10(10),0,1)}getNearestRadioTower(){let e=Number.MAX_VALUE,t=GetEntityCoords(S.ped,!1);for(let i of this.clientModule.towerConfig.towerPositions){let u=Xe(t,i);(!e||u0)&&emitNet("server:yaca:muteRadioChannel",t,i.volume===0),i.volume>0&&(emit("yaca:external:setRadioVolume",t,i.volume),this.updateRadioChannelData(t)),this.clientModule.setCommDeviceVolume("RADIO",i.volume,t)),!0}getRadioChannelVolume(e=this.activeRadioChannel){let t=this.radioChannelSettings.get(e);return t?t.volume:0}changeRadioChannelStereo(e=this.activeRadioChannel){let t=this.radioChannelSettings.get(e);if(!t)return!1;switch(t.stereo){case"STEREO":if(this.changeRadioChannelStereoRaw("MONO_LEFT",e))return this.clientModule.notification(W("changed_stereo_mode",e,W("left_ear")),"inform"),!0;break;case"MONO_LEFT":if(this.changeRadioChannelStereoRaw("MONO_RIGHT",e))return this.clientModule.notification(W("changed_stereo_mode",e,W("right_ear")),"inform"),!0;break;default:if(this.changeRadioChannelStereoRaw("STEREO",e))return this.clientModule.notification(W("changed_stereo_mode",e,W("both_ears")),"inform"),!0;break}return!1}changeRadioChannelStereoRaw(e,t=this.activeRadioChannel){if(!this.clientModule.isPluginInitialized()||!this.radioEnabled)return!1;let i=this.radioChannelSettings.get(t);return i?(i.stereo=e,this.clientModule.setCommDeviceStereoMode("RADIO",e,t),emit("yaca:external:setRadioChannelStereo",t,e.toString()),!0):!1}getRadioChannelStereo(e=this.activeRadioChannel){let t=this.radioChannelSettings.get(e);return t?t.stereo.toString():"STEREO".toString()}initRadioSettings(){for(let e=1;e<=this.clientModule.sharedConfig.radioSettings.channelCount;e++){this.radioChannelSettings.has(e)||this.radioChannelSettings.set(e,{...this.defaultRadioSettings}),this.playersInRadioChannel.has(e)||this.playersInRadioChannel.set(e,new Set);let{volume:t,stereo:i,frequency:u}=this.radioChannelSettings.get(e)??this.defaultRadioSettings;this.clientModule.setCommDeviceStereoMode("RADIO",i,e),this.clientModule.setCommDeviceVolume("RADIO",t,e),u!=="0"&&emitNet("server:yaca:changeRadioFrequency",e,u)}}radioTalkingStateToPlugin(e,t){let i=this.clientModule.getPlayerByID(S.serverId);i&&this.clientModule.setPlayersCommType(i,"RADIO",e,t)}radioTalkingStateToPluginWithWhisper(e,t,i){let u=[];for(let d of t){let y=this.clientModule.getPlayerByID(d);y&&u.push(y)}this.clientModule.setPlayersCommType(u,"RADIO",e,i,void 0,0,1)}findRadioChannelByFrequency(e){for(let[t,i]of this.radioChannelSettings)if(i.frequency===e)return t;return null}setRadioFrequency(e,t){let i=this.radioChannelSettings.get(e);if(i&&(i.frequency!==t&&this.disableRadioFromPlayerInChannel(e),i.frequency=t,emit("yaca:external:setRadioFrequency",e,t),this.clientModule.saltyChatBridge)){let u=i.frequency==="0"?"":i.frequency;emit("SaltyChat_RadioChannelChanged",u,e===1)}}disableRadioFromPlayerInChannel(e){let t=this.playersInRadioChannel.get(e);if(!t||!t.size)return;let i=[];for(let u of t){let d=this.clientModule.getPlayerByID(u);!d||!d.remoteID||(i.push(d),t.delete(d.remoteID))}i.length&&this.clientModule.setPlayersCommType(i,"RADIO",!1,e,void 0,1,0)}async radioTalkingStart(e,t){if(t===-1)return;if(!e){this.talkingInChannels.has(t)&&(this.talkingInChannels.delete(t),this.radioTowerCalculation.has(t)&&(clearInterval(this.radioTowerCalculation.get(t)),this.radioTowerCalculation.delete(t)),this.clientModule.saltyChatBridge?.handleRadioTalkingStateChange(!1,t),this.clientModule.useWhisper||this.radioTalkingStateToPlugin(!1,t),emitNet("server:yaca:radioTalking",!1,t,-1),emit("yaca:external:isRadioTalking",!1,t),StopAnimTask(S.ped,this.clientModule.sharedConfig.radioSettings.animation.dictionary,this.clientModule.sharedConfig.radioSettings.animation.name,4),RemoveAnimDict(this.clientModule.sharedConfig.radioSettings.animation.dictionary),this.currentRadioProp!==null&&(DoesEntityExist(this.currentRadioProp)&&DeleteEntity(this.currentRadioProp),this.clientModule.sharedConfig.radioSettings.propWhileTalking.prop!==!1&&SetModelAsNoLongerNeeded(this.clientModule.sharedConfig.radioSettings.propWhileTalking.prop),this.currentRadioProp=null));return}if(this.clientModule.sharedConfig.radioAntiSpamCooldown){if(this.radioOnCooldown)return;this.radioOnCooldown=!0,setTimeout(()=>{this.radioOnCooldown=!1},this.clientModule.sharedConfig.radioAntiSpamCooldown)}let i=this.radioChannelSettings.get(t);if(!this.radioEnabled||i?.frequency==="0"||this.talkingInChannels.has(t))return;if(this.talkingInChannels.add(t),this.clientModule.useWhisper||this.radioTalkingStateToPlugin(!0,t),this.clientModule.sharedConfig.radioSettings.propWhileTalking.prop!==!1){let d=await rn(this.clientModule.sharedConfig.radioSettings.propWhileTalking.prop,this.clientModule.sharedConfig.radioSettings.propWhileTalking.boneId,this.clientModule.sharedConfig.radioSettings.propWhileTalking.position,this.clientModule.sharedConfig.radioSettings.propWhileTalking.rotation);this.currentRadioProp=d??null}let u=await ct(this.clientModule.sharedConfig.radioSettings.animation.dictionary);u&&TaskPlayAnim(S.ped,u,this.clientModule.sharedConfig.radioSettings.animation.name,3,-4,-1,this.clientModule.sharedConfig.radioSettings.animation.flag,0,!1,!1,!1),this.clientModule.saltyChatBridge?.handleRadioTalkingStateChange(!0,t),this.sendRadioRequestToServer(t),this.radioTowerCalculation.has(t)||this.radioTowerCalculation.set(t,setInterval(()=>{this.sendRadioRequestToServer(t)},1e3)),emit("yaca:external:isRadioTalking",!0,t)}sendRadioRequestToServer(e){let t=this.getNearestRadioTower()??-1;emitNet("server:yaca:radioTalking",!0,e,t)}updateRadioChannelData(e){e!==this.activeRadioChannel||GetResourceState("yaca-ui")!=="started"||exports["yaca-ui"].setRadioChannelData(this.radioChannelSettings.get(e))}};var mt=class o{constructor(){this.mufflingVehicleWhitelistHash=new Set;this.allPlayers=new Map;this.firstConnect=!0;this.canChangeVoiceRange=!0;this.defaultVoiceRange=1;this.maxVoiceRange=-1;this.rangeInterval=null;this.visualVoiceRangeTimeout=null;this.visualVoiceRangeTick=null;this.voiceRangeViaMouseWheelTick=null;this.isTalking=!1;this.useWhisper=!1;this.spectatingPlayer=!1;this.notificationTimeout=new Map;this.isMicrophoneMuted=!1;this.isMicrophoneDisabled=!1;this.isSoundMuted=!1;this.isSoundDisabled=!1;this.currentlyPhoneSpeakerApplied=new Set;this.currentlySendingPhoneSpeakerSender=new Set;this.phoneHearNearbyPlayer=new Set;this.isFiveM=S.game==="fivem";this.isRedM=S.game==="redm";if(this.sharedConfig=Bt("config/shared.json5",wt),this.towerConfig=Bt("config/tower.json5",sn),Qt(this.sharedConfig.locale),this.rangeIndex=this.sharedConfig.voiceRange.defaultIndex,this.sharedConfig.voiceRange.ranges[this.rangeIndex]?this.defaultVoiceRange=this.sharedConfig.voiceRange.ranges[this.rangeIndex]:(this.defaultVoiceRange=1,this.rangeIndex=0,this.sharedConfig.voiceRange.ranges=[1],console.error("[YaCA] Default voice range is not set correctly in the config.")),this.isFiveM)for(let e of this.sharedConfig.mufflingSettings.vehicleMuffling.vehicleWhitelist)this.mufflingVehicleWhitelistHash.add(ze(e));this.websocket=new pt,this.setCurrentPluginState("NOT_CONNECTED"),RegisterNuiCallbackType("YACA_OnNuiReady"),on("__cfx_nui:YACA_OnNuiReady",(e,t)=>{this.websocket.nuiReady=!0,this.sharedConfig.autoConnectOnJoin&&setTimeout(()=>{emitNet("server:yaca:nuiReady")},5e3),t({})}),this.registerExports(),this.registerEvents(),this.isFiveM?this.registerKeybindings():this.isRedM&&this.registerRdrKeybindings(),this.intercomModule=new ht(this),this.megaphoneModule=new Dt(this),this.phoneModule=new Ct(this),this.radioModule=new gt(this),this.sharedConfig.useLocalLipSync||(AddStateBagChangeHandler(Mt,"",(e,t,i,u)=>{let d=GetPlayerFromStateBagName(e);d!==0&&SetPlayerTalkingOverride(d,i)}),AddStateBagChangeHandler(pe,"",(e,t,i,u)=>{setImmediate(()=>{this.phoneModule.enablePhoneCall(Array.from(this.phoneModule.inCallWith),!0)})})),this.sharedConfig.saltyChatBridge&&(this.radioModule.secondaryRadioChannel=2,this.saltyChatBridge=new ft(this)),console.log("[Client] YaCA Client loaded.")}setCurrentPluginState(e){this.currentPluginState!==e&&(this.currentPluginState=e,emit("yaca:external:pluginStateChanged",e),this.saltyChatBridge?.handleChangePluginState(e))}notification(e,t){if(this.sharedConfig.notifications.oxLib&&emit("ox_lib:notify",{id:"yaca",title:"YaCA",description:e,type:t}),this.sharedConfig.notifications.okoknotify&&GetResourceState("okokNotify")==="started"){let i=t==="inform"?"info":t;exports.okokNotify.Alert("YaCA",e,2e3,i)}this.sharedConfig.notifications.gta&&(this.isFiveM?(BeginTextCommandThefeedPost("STRING"),AddTextComponentSubstringPlayerName(`YaCA: ${e}`),t==="error"&&ThefeedSetNextPostBackgroundColor(6),EndTextCommandThefeedPostTicker(!1,!1)):console.warn("[YaCA] GTA notification is only available in FiveM.")),this.sharedConfig.notifications.redm&&(this.isRedM?cn(`YaCA: ${e}`,2e3):console.warn("[YaCA] RedM notification is only available in RedM.")),this.sharedConfig.notifications.own&&emit("yaca:external:notification",e,t)}registerExports(){exports("getVoiceRange",()=>this.getVoiceRange()),exports("getVoiceRanges",()=>this.sharedConfig.voiceRange.ranges),exports("changeVoiceRange",(e=!0)=>{this.changeVoiceRange(e)}),exports("setVoiceRange",e=>{this.setVoiceRange(e)}),exports("setVoiceRangeChangeAllowedState",e=>{this.canChangeVoiceRange=e}),exports("getVoiceRangeChangeAllowedState",()=>this.canChangeVoiceRange),exports("setMaxVoiceRange",e=>{this.maxVoiceRange=e}),exports("getMaxVoiceRange",()=>this.maxVoiceRange),exports("getMicrophoneMuteState",()=>this.isMicrophoneMuted),exports("getMicrophoneDisabledState",()=>this.isMicrophoneDisabled),exports("getSoundMuteState",()=>this.isSoundMuted),exports("getSoundDisabledState",()=>this.isSoundDisabled),exports("getPluginState",()=>this.currentPluginState??"NOT_CONNECTED"),exports("getGlobalErrorLevel",()=>GlobalState[pe]??0),exports("setSpectatingPlayer",e=>{this.spectatingPlayer=e}),exports("getSpectatingPlayer",()=>this.spectatingPlayer),exports("setVoiceRangeMarkerColor",(e,t,i,u)=>{if(typeof e!="number"||typeof t!="number"||typeof i!="number"||typeof u!="number"){console.error("[YaCA] Invalid color value in setVoiceRangeMarkerColor");return}this.sharedConfig.voiceRange.markerColor.r=e,this.sharedConfig.voiceRange.markerColor.g=t,this.sharedConfig.voiceRange.markerColor.b=i,this.sharedConfig.voiceRange.markerColor.a=u}),exports("getVoiceRangeMarkerColor",()=>{let{r:e,g:t,b:i,a:u}=this.sharedConfig.voiceRange.markerColor;return[e,t,i,u]}),exports("resetVoiceRangeMarkerColor",()=>{let e=wt.voiceRange.markerColor;this.sharedConfig.voiceRange.markerColor.r=e.r,this.sharedConfig.voiceRange.markerColor.g=e.g,this.sharedConfig.voiceRange.markerColor.b=e.b,this.sharedConfig.voiceRange.markerColor.a=e.a})}registerKeybindings(){this.sharedConfig.keyBinds.increaseVoiceRange!==!1&&(RegisterCommand("yaca:increaseVoiceRange",()=>{this.changeVoiceRange(!0)},!1),RegisterKeyMapping("yaca:increaseVoiceRange",W("change_voice_range_increase"),"keyboard",this.sharedConfig.keyBinds.increaseVoiceRange)),this.sharedConfig.keyBinds.decreaseVoiceRange!==!1&&(RegisterCommand("yaca:decreaseVoiceRange",()=>{this.changeVoiceRange(!1)},!1),RegisterKeyMapping("yaca:decreaseVoiceRange",W("change_voice_range_decrease"),"keyboard",this.sharedConfig.keyBinds.decreaseVoiceRange)),this.sharedConfig.keyBinds.voiceRangeWithMouseWheel!==!1&&(RegisterCommand("+yaca:changeVoiceRangeWithMousewheel",()=>{this.voiceRangeViaMouseWheelTick=setInterval(()=>{this.handleVoiceRangeViaMouseWheel()})},!1),RegisterCommand("-yaca:changeVoiceRangeWithMousewheel",()=>{this.voiceRangeViaMouseWheelTick&&(clearInterval(this.voiceRangeViaMouseWheelTick),this.voiceRangeViaMouseWheelTick=null)},!1),RegisterKeyMapping("+yaca:changeVoiceRangeWithMousewheel",W("change_voice_range_via_mousewheel"),"keyboard",this.sharedConfig.keyBinds.voiceRangeWithMouseWheel))}registerRdrKeybindings(){this.sharedConfig.keyBinds.increaseVoiceRange!==!1&&ve(this.sharedConfig.keyBinds.increaseVoiceRange,()=>{this.changeVoiceRange()}),this.sharedConfig.keyBinds.decreaseVoiceRange!==!1&&ve(this.sharedConfig.keyBinds.decreaseVoiceRange,()=>{this.changeVoiceRange(!1)}),this.sharedConfig.keyBinds.voiceRangeWithMouseWheel!==!1&&ve(this.sharedConfig.keyBinds.voiceRangeWithMouseWheel,()=>{this.voiceRangeViaMouseWheelTick=setInterval(()=>{this.handleVoiceRangeViaMouseWheel()})},()=>{this.voiceRangeViaMouseWheelTick&&(clearInterval(this.voiceRangeViaMouseWheelTick),this.voiceRangeViaMouseWheelTick=null)})}registerEvents(){onNet("onPlayerJoining",e=>{let t=this.getPlayerByID(e);if(!t)return;let i=this.radioModule?.playersWithShortRange.get(e);if(i){let u=this.radioModule?.findRadioChannelByFrequency(i);u&&(this.setPlayersCommType(t,"RADIO",!0,u,void 0,1,0,GlobalState[pe]??void 0),this.saltyChatBridge?.handleRadioReceivingStateChange(!0,u))}}),onNet("onPlayerDropped",e=>{let t=this.getPlayerByID(e);if(!t)return;this.phoneModule.removePhoneSpeakerFromEntity(e);let i=this.radioModule?.playersWithShortRange.get(e);if(i){let u=this.radioModule?.findRadioChannelByFrequency(i);if(u&&(this.setPlayersCommType(t,"RADIO",!1,u,void 0,1,0,GlobalState[pe]??void 0),this.saltyChatBridge)){let d=this.radioModule?.playersInRadioChannel.get(u);if(d){let _=[...d].filter(b=>b!==e).length>0;this.saltyChatBridge.handleRadioReceivingStateChange(_,u)}}}}),on("onResourceStop",e=>{S.resource===e&&this.websocket.initialized&&this.websocket.close()}),onNet("client:yaca:init",async e=>{this.rangeInterval&&(clearInterval(this.rangeInterval),this.rangeInterval=null),this.websocket.initialized||(this.websocket.initialized=!0,this.websocket.on("message",t=>{this.handleResponse(t)}),this.websocket.on("close",(t,i)=>{this.setCurrentPluginState("NOT_CONNECTED"),console.error("[YACA-Websocket]: client disconnected",t,i)}),this.websocket.on("open",()=>{this.setCurrentPluginState("CONNECTED"),this.firstConnect?(this.initRequest(e),this.firstConnect=!1):emitNet("server:yaca:wsReady"),console.log("[YACA-Websocket]: Successfully connected to the voice plugin")}),await this.websocket.start()),!this.firstConnect&&this.initRequest(e)}),onNet("client:yaca:disconnect",e=>{this.phoneModule.handleDisconnect(e),this.allPlayers.delete(e)}),onNet("client:yaca:addPlayers",e=>{Array.isArray(e)||(e=[e]);let t=[];for(let i of e){if(!i||typeof i.clientId>"u"||typeof i.playerId>"u")continue;let u=this.getPlayerByID(i.playerId);this.allPlayers.set(i.playerId,{remoteID:i.playerId,clientId:i.clientId,forceMuted:i.forceMuted||!1,phoneCallMemberIds:u?.phoneCallMemberIds||void 0,mutedOnPhone:i.mutedOnPhone||!1}),t.push(i.playerId)}this.phoneModule.reestablishCalls(t)}),onNet("client:yaca:muteTarget",(e,t)=>{let i=this.getPlayerByID(e);i&&(i.forceMuted=t)}),onNet("client:yaca:changeVoiceRange",e=>{emit("yaca:external:voiceRangeUpdate",e,this.rangeIndex),this.saltyChatBridge&&emit("SaltyChat_VoiceRangeChanged",e.toFixed(1),this.rangeIndex,this.sharedConfig.voiceRange.ranges.length)}),onNet("client:yaca:notification",(e,t)=>{this.notification(e,t)}),onNet("txcl:spectate:start",e=>{this.spectatingPlayer=e}),onNet("client:yaca:txadmin:stopspectate",()=>{this.spectatingPlayer=!1})}getPlayerByID(e){return this.allPlayers.get(e)}getPlayerByClientId(e){for(let t of this.allPlayers.values())if(t.clientId===e)return t;return null}initRequest(e){if(!e||!e.suid||typeof e.chid!="number"||!e.deChid||!e.ingameName||typeof e.channelPassword>"u"){console.log("[YACA-Websocket]: Error while initializing plugin"),this.notification(W("connect_error"),"error");return}this.sendWebsocket({base:{request_type:"INIT"},server_guid:e.suid,ingame_name:e.ingameName,ingame_channel:e.chid,default_channel:e.deChid,ingame_channel_password:e.channelPassword,excluded_channels:e.excludeChannels,muffling_range:this.sharedConfig.mufflingSettings.mufflingRange,build_type:this.sharedConfig.buildType,unmute_delay:this.sharedConfig.unmuteDelay,operation_mode:e.useWhisper?1:0}),this.useWhisper=e.useWhisper??!1}isPluginInitialized(e=!1){let t=!!this.getPlayerByID(S.serverId);return!t&&!e&&this.notification(W("plugin_not_initialized"),"error"),t}sendWebsocket(e){if(!this.websocket){console.error("[Voice-Websocket]: No websocket created");return}this.websocket.send(e)}handleResponse(e){if(!e)return;let t;try{t=JSON.parse(e)}catch(i){console.error("[YaCA-Websocket]: Error while parsing message: ",i);return}switch(t.code){case"OK":if(t.requestType==="JOIN"){let i=Number.parseInt(t.message);emitNet("server:yaca:addPlayer",i),this.rangeInterval&&(clearInterval(this.rangeInterval),this.rangeInterval=null),this.rangeInterval=setInterval(this.calcPlayers.bind(this),250),this.radioModule.radioInitialized&&this.radioModule.initRadioSettings(),emit("yaca:external:pluginInitialized",i);return}return;case"TALK_STATE":this.handleTalkState(t);return;case"SOUND_STATE":this.handleSoundState(t);return;case"OTHER_TALK_STATE":this.handleOtherTalkState(t);return;case"MOVED_CHANNEL":this.handleMovedChannel(t.message);return;case"WRONG_TS_SERVER":{this.setCurrentPluginState("WRONG_TS_SERVER");let i=this.notificationTimeout.get("WRONG_TS_SERVER");if(i&&i>Date.now())return;this.notificationTimeout.set("WRONG_TS_SERVER",Date.now()+1e4),this.notification(W("wrong_ts_server")??"You are connected to the wrong teamspeak server!","error");return}case"OUTDATED_VERSION":this.setCurrentPluginState("OUTDATED_VERSION"),this.notification(W("outdated_plugin",t.message)??`Your plugin is outdated, please update to version ${t.message}!`,"error");return;case"MAX_PLAYER_COUNT_REACHED":this.notification(W("max_players_reached")??"Your license reached the maximum player count. Please upgrade your license.","error");return;case"LICENSE_SERVER_TIMED_OUT":this.notification(W("license_server_timed_out")??"The connection to the license server timed out, while verifying the license. Please wait a moment.","error");return;case"MOVE_ERROR":this.notification(W("move_error")??"You are not connected to the teamspeak server!","error");return;case"WAIT_GAME_INIT":case"HEARTBEAT":case"MUTE_STATE":return;default:console.log(`[YaCA-Websocket]: Unknown error code: ${t.code}`),this.notification(W("unknown_error",t.code)??`Unknown error code: ${t.code}`,"error");return}}setPlayerVariable(e,t,i){let u=this.getPlayerByID(e);u&&(u[t]=i)}getVoiceRange(){return LocalPlayer.state[Je]??this.defaultVoiceRange}changeVoiceRange(e=!0){if(!this.canChangeVoiceRange)return;let t=this.getVoiceRange();if(e){let u=this.sharedConfig.voiceRange.ranges.findIndex(d=>(this.maxVoiceRange!==-1&&d<=this.maxVoiceRange||this.maxVoiceRange===-1)&&d>t);this.rangeIndex=u!==-1?u:0}else{let u=this.sharedConfig.voiceRange.ranges.slice().reverse().findIndex(d=>dthis.maxVoiceRange){let d=this.sharedConfig.voiceRange.ranges.slice().reverse().findIndex(y=>y<=this.maxVoiceRange);this.rangeIndex=d!==-1?this.sharedConfig.voiceRange.ranges.length-1-d:this.sharedConfig.voiceRange.ranges.length-1}}let i=this.sharedConfig.voiceRange.ranges[this.rangeIndex]??1;this.changeVoiceRangeInternal(i)}setVoiceRange(e){this.rangeIndex=-1,this.changeVoiceRangeInternal(e)}changeVoiceRangeInternal(e){this.canChangeVoiceRange&&(this.maxVoiceRange!==-1&&e>this.maxVoiceRange||(this.showRangeVisual(e),LocalPlayer.state.set(Je,e,!0),emit("yaca:external:voiceRangeUpdate",e,this.rangeIndex),this.saltyChatBridge&&emit("SaltyChat_VoiceRangeChanged",e.toFixed(1),this.rangeIndex,this.sharedConfig.voiceRange.ranges.length)))}showRangeVisual(e){if(this.visualVoiceRangeTimeout&&(clearTimeout(this.visualVoiceRangeTimeout),this.visualVoiceRangeTimeout=null),this.visualVoiceRangeTick&&(clearInterval(this.visualVoiceRangeTick),this.visualVoiceRangeTick=null),this.sharedConfig.voiceRange.sendNotification&&this.notification(W("voice_range_changed",e),"inform"),this.sharedConfig.voiceRange.markerColor.enabled){let t=this.sharedConfig.voiceRange.markerColor.r,i=this.sharedConfig.voiceRange.markerColor.g,u=this.sharedConfig.voiceRange.markerColor.b,d=this.sharedConfig.voiceRange.markerColor.a,y=this.sharedConfig.voiceRange.markerColor.duration;this.visualVoiceRangeTimeout=setTimeout(()=>{this.visualVoiceRangeTick&&(clearInterval(this.visualVoiceRangeTick),this.visualVoiceRangeTick=null),this.visualVoiceRangeTimeout=null},y),!this.isFiveM&&this.sharedConfig.voiceRange.markerColor.type<1e3&&(this.sharedConfig.voiceRange.markerColor.type=2499653143,console.warn("[YaCA] Marker type is not supported in RedM. Using default marker type.")),this.visualVoiceRangeTick=setInterval(()=>{let _=S.vehicle||S.ped,b=GetEntityCoords(_,!1),A=S.vehicle?b[2]-.6:b[2]-.98;DrawMarker(this.sharedConfig.voiceRange.markerColor.type,b[0],b[1],A,0,0,0,0,0,0,e*2,e*2,1,t,i,u,d,!1,!0,2,this.sharedConfig.voiceRange.markerColor.rotate,null,null,!1)})}}static isCommTypeValid(e){let t=e in ke;return t||console.error(`[YaCA-Websocket]: Invalid comm type: ${e}`),t}setPlayersCommType(e,t,i,u,d,y,_,b){Array.isArray(e)||(e=[e]);let A=[];typeof y<"u"&&A.push({client_id:this.getPlayerByID(S.serverId)?.clientId,mode:y});for(let re of e){if(!re)continue;let J={client_id:re.clientId,mode:_};typeof b<"u"&&b!==null&&(J.errorLevel=b),A.push(J)}let v={on:i,comm_type:t,members:A};typeof u<"u"&&u!==null&&(v.channel=u),typeof d<"u"&&d!==null&&(v.range=d),this.sendWebsocket({base:{request_type:"INGAME"},comm_device:v})}setCommDeviceVolume(e,t,i){if(!o.isCommTypeValid(e))return;let u={comm_type:e,volume:Ye(t,0,1)};typeof i<"u"&&(u.channel=i),this.sendWebsocket({base:{request_type:"INGAME"},comm_device_settings:u})}setCommDeviceStereoMode(e,t,i){if(!o.isCommTypeValid(e))return;let u={comm_type:e,output_mode:t};typeof i<"u"&&(u.channel=i),this.sendWebsocket({base:{request_type:"INGAME"},comm_device_settings:u})}syncLipsPlayer(e,t,i){let u=un[S.game][i?"true":"false"];SetPlayerTalkingOverride(t,i),this.isFiveM?PlayFacialAnim(e,u.name,u.dict):this.isRedM&&ln(e,u.name,u.dict)}handleTalkState(e){let t=e.message==="1",u=!(this.isMicrophoneMuted||this.isMicrophoneDisabled||this.isSoundMuted||this.isSoundDisabled)&&t;this.isTalking!==u&&(this.isTalking=u,this.syncLipsPlayer(S.ped,S.serverId,u),LocalPlayer.state.set(Mt,u,!0),emit("yaca:external:isTalking",u),this.saltyChatBridge&&emit("SaltyChat_TalkStateChanged",u))}handleSoundState(e){let t=JSON.parse(e.message);this.isMicrophoneMuted!==t.microphoneMuted&&(this.isMicrophoneMuted=t.microphoneMuted,emit("yaca:external:microphoneMuteStateChanged",t.microphoneMuted),emit("yaca:external:muteStateChanged",t.microphoneMuted),this.saltyChatBridge&&emit("SaltyChat_MicStateChanged",t.microphoneMuted)),this.isMicrophoneDisabled!==t.microphoneDisabled&&(this.isMicrophoneDisabled=t.microphoneDisabled,emit("yaca:external:microphoneDisabledStateChanged",t.microphoneDisabled),this.saltyChatBridge&&emit("SaltyChat_MicEnabledChanged",t.microphoneDisabled)),this.isSoundMuted!==t.soundMuted&&(this.isSoundMuted=t.soundMuted,emit("yaca:external:soundMuteStateChanged",t.soundMuted),this.saltyChatBridge&&emit("SaltyChat_SoundStateChanged",t.soundMuted)),this.isSoundDisabled!==t.soundDisabled&&(this.isSoundDisabled=t.soundDisabled,emit("yaca:external:soundDisabledStateChanged",t.soundDisabled),this.saltyChatBridge&&emit("SaltyChat_SoundEnabledChanged",t.soundDisabled))}handleOtherTalkState(e){if(!this.sharedConfig.useLocalLipSync)return;let t;try{t=JSON.parse(e.message)}catch{console.error("[YaCA-Websocket]: Error while parsing other talk state message");return}let i=this.getPlayerByClientId(t.clientId);if(!i||!i.remoteID)return;let u=GetPlayerFromServerId(i.remoteID);u!==-1&&SetPlayerTalkingOverride(u,t.isTalking)}handleMovedChannel(e){if(e!=="INGAME_CHANNEL"&&e!=="EXCLUDED_CHANNEL"){console.error("[YaCA-Websocket]: Unknown channel type: ",e);return}e==="INGAME_CHANNEL"?this.setCurrentPluginState("IN_INGAME_CHANNEL"):this.setCurrentPluginState("IN_EXCLUDED_CHANNEL"),emit("yaca:external:channelChanged",e)}checkIfVehicleHasOpening(e){return!e||this.mufflingVehicleWhitelistHash.has(GetEntityModel(e))?!0:hn(e)}getMuffleIntensity(e,t,i,u,d,y=!1){if(u!==GetRoomKeyFromEntity(t)&&!HasEntityClearLosToEntity(e,t,17))return this.sharedConfig.mufflingSettings.intensities.differentRoom;let _=this.sharedConfig.mufflingSettings.vehicleMuffling.enabled;if(this.isRedM||!_)return 0;let b=GetVehiclePedIsIn(t,!1);if((i||0)===b)return 0;if(y)return d?0:this.sharedConfig.mufflingSettings.intensities.megaPhoneInCar;let v=this.checkIfVehicleHasOpening(b);return!d&&!v?this.sharedConfig.mufflingSettings.intensities.bothCarsClosed:!d||!v?this.sharedConfig.mufflingSettings.intensities.oneCarClosed:0}handlePhoneSpeakerEmit(e,t){if(this.useWhisper&&(this.phoneModule.phoneSpeakerActive&&this.phoneModule.inCallWith.size||(!this.phoneModule.phoneSpeakerActive||!this.phoneModule.inCallWith.size)&&this.currentlySendingPhoneSpeakerSender.size)){let i=[...this.currentlySendingPhoneSpeakerSender].filter(d=>!e.has(d)),u=[...e].filter(d=>!this.currentlySendingPhoneSpeakerSender.has(d));this.currentlySendingPhoneSpeakerSender=new Set(e),(u.length||i.length)&&emitNet("server:yaca:phoneSpeakerEmitWhisper",u,i)}for(let i of this.currentlyPhoneSpeakerApplied){if(t.has(i))continue;this.currentlyPhoneSpeakerApplied.delete(i);let u=this.getPlayerByID(i);u&&this.setPlayersCommType(u,"PHONE_SPEAKER",!1,void 0,this.sharedConfig.maxPhoneSpeakerRange,1,0)}}handlePhoneEmit(e){if(!this.sharedConfig.phoneHearPlayersNearby)return;if(this.sharedConfig.phoneHearPlayersNearby==="PHONE_SPEAKER"){if(!(this.phoneModule.phoneSpeakerActive&&this.phoneModule.inCallWith.size||(!this.phoneModule.phoneSpeakerActive||!this.phoneModule.inCallWith.size)&&this.phoneHearNearbyPlayer.size))return}else if(!(this.phoneModule.inCallWith.size||!this.phoneModule.inCallWith.size&&this.phoneHearNearbyPlayer.size))return;let t=[...this.phoneHearNearbyPlayer].filter(u=>!e.has(u)),i=[...e].filter(u=>!this.phoneHearNearbyPlayer.has(u));this.phoneHearNearbyPlayer=new Set(e),(i.length||t.length)&&emitNet("server:yaca:phoneEmit",i,t)}handleVoiceRangeViaMouseWheel(){this.isFiveM&&HudWeaponWheelIgnoreSelection();let e=0,t=this.getVoiceRange();IsControlPressed(0,242)?e=Math.max(1,t-1):IsControlPressed(0,241)&&(e=Math.min(this.sharedConfig.voiceRange.ranges[this.sharedConfig.voiceRange.ranges.length-1],t+1),this.maxVoiceRange!==-1&&e>this.maxVoiceRange&&(e=this.maxVoiceRange)),!(e<=0||t===e)&&this.setVoiceRange(e)}calcPlayers(){let e=this.getPlayerByID(S.serverId);if(!e)return;let t=new Map,i=new Set,u=new Set,d=new Set,y=S.ped,_=S.vehicle;if(this.spectatingPlayer){let J=GetPlayerFromServerId(this.spectatingPlayer);if(J!==-1){let z=GetPlayerPed(J);if(z!==0){y=z;let Q=GetVehiclePedIsIn(z,!1);Q!==0?_=Q:_=!1}}}let b=GetEntityCoords(y,!1),A=GetRoomKeyFromEntity(y),v=this.isFiveM?this.checkIfVehicleHasOpening(_):!0,re=this.phoneModule.phoneSpeakerActive&&this.phoneModule.inCallWith.size;for(let J of GetActivePlayers()){let z=GetPlayerServerId(J),Q=GetPlayerPed(J);if(z===0||z===S.serverId||Q<=0)continue;let le=this.getPlayerByID(z);if(!le||!le.clientId)continue;let Ee=Player(z).state,ce=Ee[Je]??this.defaultVoiceRange,Be=this.getMuffleIntensity(y,Q,_,A,v,Ee[rt]!==null),Ce=GetEntityCoords(Q,!1),Oe=Xe(b,Ce),_e=GetEntityForwardVector(Q),Me=IsPedSwimmingUnderWater(Q)===1;if(u.has(z)||t.set(z,{client_id:le.clientId,position:qe(Ce),direction:qe(_e),range:ce,is_underwater:Me,muffle_intensity:Be,is_muted:le.forceMuted??!1}),this.sharedConfig.phoneHearPlayersNearby&&!e.mutedOnPhone&&!le.forceMuted&&Oe<=ce&&(this.sharedConfig.phoneHearPlayersNearby==="PHONE_SPEAKER"&&re||this.sharedConfig.phoneHearPlayersNearby===!0&&this.phoneModule.inCallWith.size)&&d.add(z),!(Oe>this.sharedConfig.maxPhoneSpeakerRange)&&(this.useWhisper&&re&&i.add(z),!!le.phoneCallMemberIds))for(let O of le.phoneCallMemberIds){let ge=this.getPlayerByID(O);!ge||!ge.clientId||ge.mutedOnPhone||ge.forceMuted||(t.delete(O),t.set(O,{client_id:ge.clientId,position:qe(Ce),direction:qe(_e),range:this.sharedConfig.maxPhoneSpeakerRange,is_underwater:Me,muffle_intensity:Be,is_muted:!1}),u.add(O),!this.currentlyPhoneSpeakerApplied.has(O)&&(this.setPlayersCommType(ge,"PHONE_SPEAKER",!0,void 0,this.sharedConfig.maxPhoneSpeakerRange,1,0),this.currentlyPhoneSpeakerApplied.add(O)))}}this.handlePhoneSpeakerEmit(i,u),this.handlePhoneEmit(d),this.sendWebsocket({base:{request_type:"INGAME"},player:{player_direction:dn(),player_position:qe(b),player_range:LocalPlayer.state[Je]??this.defaultVoiceRange,player_is_underwater:IsPedSwimmingUnderWater(y)===1,player_is_muted:e.forceMuted??!1,players_list:Array.from(t.values())}})}};var ln=async(o,e,t)=>{let i=await ct(t);i&&SetFacialIdleAnimOverride(o,e,i)},cn=(o,e)=>{let t=VarString(10,"LITERAL_STRING",o),i=new DataView(new ArrayBuffer(96));i.setUint32(0,e,!0);let u=new DataView(new ArrayBuffer(16));u.setBigUint64(8,BigInt(t),!0),Citizen.invokeNative("0x049D5C615BD38BAD",i,u,1)},ve=(o,e,t)=>{let i=an[o];if(!i){console.error(`[YaCA] No key hash available for ${o}, please choose another keybind`);return}setTick(()=>{DisableControlAction(0,i,!0),e&&IsDisabledControlJustPressed(0,i)&&e(),t&&IsDisabledControlJustReleased(0,i)&&t()})};function Xe(o,e){return Math.sqrt((o[0]-e[0])**2+(o[1]-e[1])**2+(o[2]-e[2])**2)}function qe(o){return{x:Ve(o[0]),y:Ve(o[1]),z:Ve(o[2])}}function Kn(o,e){switch(e){case 0:return GetEntityBoneIndexByName(o,"window_lf")!==-1;case 1:return GetEntityBoneIndexByName(o,"window_rf")!==-1;case 2:return GetEntityBoneIndexByName(o,"window_lr")!==-1;case 3:return GetEntityBoneIndexByName(o,"window_rr")!==-1;default:return!1}}function $n(o,e){switch(e){case 0:return GetEntityBoneIndexByName(o,"door_dside_f")!==-1;case 1:return GetEntityBoneIndexByName(o,"door_pside_f")!==-1;case 2:return GetEntityBoneIndexByName(o,"door_dside_r")!==-1;case 3:return GetEntityBoneIndexByName(o,"door_pside_r")!==-1;case 4:return GetEntityBoneIndexByName(o,"bonnet")!==-1;case 5:return GetEntityBoneIndexByName(o,"boot")!==-1;default:return!1}}function hn(o){let e=[];for(let t=0;t<6;t++)t===4||!$n(o,t)||e.push(t);if(e.length===0)return!0;for(let t of e)if(GetVehicleDoorAngleRatio(o,t)>0||IsVehicleDoorDamaged(o,t))return!0;if(!AreAllVehicleWindowsIntact(o))return!0;for(let t=0;t<8;t++)if(Kn(o,t)&&!IsVehicleWindowIntact(o,t))return!0;return!!(IsVehicleAConvertible(o,!1)&&GetConvertibleRoofState(o)!==0)}var gn=vt(Cn(),1),pt=class extends gn.default{constructor(){super();this.readyState=0;this.nuiReady=!1;this.initialized=!1;RegisterNuiCallbackType("YACA_OnMessage"),RegisterNuiCallbackType("YACA_OnConnected"),RegisterNuiCallbackType("YACA_OnDisconnected"),on("__cfx_nui:YACA_OnMessage",(t,i)=>{this.emit("message",t),i({})}),on("__cfx_nui:YACA_OnConnected",(t,i)=>{this.readyState=1,this.emit("open"),i({})}),on("__cfx_nui:YACA_OnDisconnected",(t,i)=>{this.readyState=3,this.emit("close",t.code,t.reason),i({})})}async start(){for(;!this.nuiReady;)await lt(100);SendNuiMessage(JSON.stringify({action:"connect"}))}send(t){this.readyState===1&&SendNuiMessage(JSON.stringify({action:"command",data:t}))}close(){this.readyState!==3&&SendNuiMessage(JSON.stringify({action:"close"}))}};function Ve(o,e=17){return Number.parseFloat(o.toFixed(e))}function dn(){let o=GetGameplayCamRot(0),e=o[2]*.0174532924,t=o[0]*.0174532924,i=Math.abs(Math.cos(t));return{x:Ve(-Math.sin(e)*i),y:Ve(Math.cos(e)*i),z:Ve(GetEntityForwardVector(S.ped)[2])}}kt();new mt;})(); +/*! Bundled license information: + +eventemitter2/lib/eventemitter2.js: + (*! + * EventEmitter2 + * https://github.com/hij1nx/EventEmitter2 + * + * Copyright (c) 2013 hij1nx + * Licensed under the MIT license. + *) +*/ diff --git a/resources/[voice]/yaca-voice/dist/server.js b/resources/[voice]/yaca-voice/dist/server.js new file mode 100644 index 000000000..1527bd8a0 --- /dev/null +++ b/resources/[voice]/yaca-voice/dist/server.js @@ -0,0 +1,9040 @@ +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __esm = (fn, res) => function __init() { + return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; +}; +var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); + +// ../../node_modules/.pnpm/json5@2.2.3/node_modules/json5/lib/unicode.js +var require_unicode = __commonJS({ + "../../node_modules/.pnpm/json5@2.2.3/node_modules/json5/lib/unicode.js"(exports2, module2) { + module2.exports.Space_Separator = /[\u1680\u2000-\u200A\u202F\u205F\u3000]/; + module2.exports.ID_Start = /[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312E\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEA\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF19]|\uD806[\uDCA0-\uDCDF\uDCFF\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE83\uDE86-\uDE89\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50\uDF93-\uDF9F\uDFE0\uDFE1]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00-\uDD1E\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]/; + module2.exports.ID_Continue = /[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u08D4-\u08E1\u08E3-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u09FC\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0AF9-\u0AFF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58-\u0C5A\u0C60-\u0C63\u0C66-\u0C6F\u0C80-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D00-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D54-\u0D57\u0D5F-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19D9\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABD\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1CD0-\u1CD2\u1CD4-\u1CF9\u1D00-\u1DF9\u1DFB-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u2E2F\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099\u309A\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312E\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEA\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA827\uA840-\uA873\uA880-\uA8C5\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA8FD\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE2F\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDDFD\uDE80-\uDE9C\uDEA0-\uDED0\uDEE0\uDF00-\uDF1F\uDF2D-\uDF4A\uDF50-\uDF7A\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00-\uDE03\uDE05\uDE06\uDE0C-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE38-\uDE3A\uDE3F\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE6\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC00-\uDC46\uDC66-\uDC6F\uDC7F-\uDCBA\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD00-\uDD34\uDD36-\uDD3F\uDD50-\uDD73\uDD76\uDD80-\uDDC4\uDDCA-\uDDCC\uDDD0-\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE37\uDE3E\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEEA\uDEF0-\uDEF9\uDF00-\uDF03\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3C-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF50\uDF57\uDF5D-\uDF63\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC00-\uDC4A\uDC50-\uDC59\uDC80-\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDB5\uDDB8-\uDDC0\uDDD8-\uDDDD\uDE00-\uDE40\uDE44\uDE50-\uDE59\uDE80-\uDEB7\uDEC0-\uDEC9\uDF00-\uDF19\uDF1D-\uDF2B\uDF30-\uDF39]|\uD806[\uDCA0-\uDCE9\uDCFF\uDE00-\uDE3E\uDE47\uDE50-\uDE83\uDE86-\uDE99\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC36\uDC38-\uDC40\uDC50-\uDC59\uDC72-\uDC8F\uDC92-\uDCA7\uDCA9-\uDCB6\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD36\uDD3A\uDD3C\uDD3D\uDD3F-\uDD47\uDD50-\uDD59]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDED0-\uDEED\uDEF0-\uDEF4\uDF00-\uDF36\uDF40-\uDF43\uDF50-\uDF59\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50-\uDF7E\uDF8F-\uDF9F\uDFE0\uDFE1]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00-\uDD1E\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99\uDC9D\uDC9E]|\uD834[\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD838[\uDC00-\uDC06\uDC08-\uDC18\uDC1B-\uDC21\uDC23\uDC24\uDC26-\uDC2A]|\uD83A[\uDC00-\uDCC4\uDCD0-\uDCD6\uDD00-\uDD4A\uDD50-\uDD59]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uDB40[\uDD00-\uDDEF]/; + } +}); + +// ../../node_modules/.pnpm/json5@2.2.3/node_modules/json5/lib/util.js +var require_util = __commonJS({ + "../../node_modules/.pnpm/json5@2.2.3/node_modules/json5/lib/util.js"(exports2, module2) { + var unicode = require_unicode(); + module2.exports = { + isSpaceSeparator(c) { + return typeof c === "string" && unicode.Space_Separator.test(c); + }, + isIdStartChar(c) { + return typeof c === "string" && (c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c === "$" || c === "_" || unicode.ID_Start.test(c)); + }, + isIdContinueChar(c) { + return typeof c === "string" && (c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c >= "0" && c <= "9" || c === "$" || c === "_" || c === "\u200C" || c === "\u200D" || unicode.ID_Continue.test(c)); + }, + isDigit(c) { + return typeof c === "string" && /[0-9]/.test(c); + }, + isHexDigit(c) { + return typeof c === "string" && /[0-9A-Fa-f]/.test(c); + } + }; + } +}); + +// ../../node_modules/.pnpm/json5@2.2.3/node_modules/json5/lib/parse.js +var require_parse = __commonJS({ + "../../node_modules/.pnpm/json5@2.2.3/node_modules/json5/lib/parse.js"(exports2, module2) { + var util = require_util(); + var source2; + var parseState; + var stack; + var pos; + var line; + var column; + var token; + var key; + var root; + module2.exports = function parse(text, reviver) { + source2 = String(text); + parseState = "start"; + stack = []; + pos = 0; + line = 1; + column = 0; + token = void 0; + key = void 0; + root = void 0; + do { + token = lex(); + parseStates[parseState](); + } while (token.type !== "eof"); + if (typeof reviver === "function") { + return internalize({ "": root }, "", reviver); + } + return root; + }; + function internalize(holder, name, reviver) { + const value = holder[name]; + if (value != null && typeof value === "object") { + if (Array.isArray(value)) { + for (let i2 = 0; i2 < value.length; i2++) { + const key2 = String(i2); + const replacement = internalize(value, key2, reviver); + if (replacement === void 0) { + delete value[key2]; + } else { + Object.defineProperty(value, key2, { + value: replacement, + writable: true, + enumerable: true, + configurable: true + }); + } + } + } else { + for (const key2 in value) { + const replacement = internalize(value, key2, reviver); + if (replacement === void 0) { + delete value[key2]; + } else { + Object.defineProperty(value, key2, { + value: replacement, + writable: true, + enumerable: true, + configurable: true + }); + } + } + } + } + return reviver.call(holder, name, value); + } + var lexState; + var buffer; + var doubleQuote; + var sign; + var c; + function lex() { + lexState = "default"; + buffer = ""; + doubleQuote = false; + sign = 1; + for (; ; ) { + c = peek(); + const token2 = lexStates[lexState](); + if (token2) { + return token2; + } + } + } + function peek() { + if (source2[pos]) { + return String.fromCodePoint(source2.codePointAt(pos)); + } + } + function read() { + const c2 = peek(); + if (c2 === "\n") { + line++; + column = 0; + } else if (c2) { + column += c2.length; + } else { + column++; + } + if (c2) { + pos += c2.length; + } + return c2; + } + var lexStates = { + default() { + switch (c) { + case " ": + case "\v": + case "\f": + case " ": + case "\xA0": + case "\uFEFF": + case "\n": + case "\r": + case "\u2028": + case "\u2029": + read(); + return; + case "/": + read(); + lexState = "comment"; + return; + case void 0: + read(); + return newToken("eof"); + } + if (util.isSpaceSeparator(c)) { + read(); + return; + } + return lexStates[parseState](); + }, + comment() { + switch (c) { + case "*": + read(); + lexState = "multiLineComment"; + return; + case "/": + read(); + lexState = "singleLineComment"; + return; + } + throw invalidChar(read()); + }, + multiLineComment() { + switch (c) { + case "*": + read(); + lexState = "multiLineCommentAsterisk"; + return; + case void 0: + throw invalidChar(read()); + } + read(); + }, + multiLineCommentAsterisk() { + switch (c) { + case "*": + read(); + return; + case "/": + read(); + lexState = "default"; + return; + case void 0: + throw invalidChar(read()); + } + read(); + lexState = "multiLineComment"; + }, + singleLineComment() { + switch (c) { + case "\n": + case "\r": + case "\u2028": + case "\u2029": + read(); + lexState = "default"; + return; + case void 0: + read(); + return newToken("eof"); + } + read(); + }, + value() { + switch (c) { + case "{": + case "[": + return newToken("punctuator", read()); + case "n": + read(); + literal("ull"); + return newToken("null", null); + case "t": + read(); + literal("rue"); + return newToken("boolean", true); + case "f": + read(); + literal("alse"); + return newToken("boolean", false); + case "-": + case "+": + if (read() === "-") { + sign = -1; + } + lexState = "sign"; + return; + case ".": + buffer = read(); + lexState = "decimalPointLeading"; + return; + case "0": + buffer = read(); + lexState = "zero"; + return; + case "1": + case "2": + case "3": + case "4": + case "5": + case "6": + case "7": + case "8": + case "9": + buffer = read(); + lexState = "decimalInteger"; + return; + case "I": + read(); + literal("nfinity"); + return newToken("numeric", Infinity); + case "N": + read(); + literal("aN"); + return newToken("numeric", NaN); + case '"': + case "'": + doubleQuote = read() === '"'; + buffer = ""; + lexState = "string"; + return; + } + throw invalidChar(read()); + }, + identifierNameStartEscape() { + if (c !== "u") { + throw invalidChar(read()); + } + read(); + const u = unicodeEscape(); + switch (u) { + case "$": + case "_": + break; + default: + if (!util.isIdStartChar(u)) { + throw invalidIdentifier(); + } + break; + } + buffer += u; + lexState = "identifierName"; + }, + identifierName() { + switch (c) { + case "$": + case "_": + case "\u200C": + case "\u200D": + buffer += read(); + return; + case "\\": + read(); + lexState = "identifierNameEscape"; + return; + } + if (util.isIdContinueChar(c)) { + buffer += read(); + return; + } + return newToken("identifier", buffer); + }, + identifierNameEscape() { + if (c !== "u") { + throw invalidChar(read()); + } + read(); + const u = unicodeEscape(); + switch (u) { + case "$": + case "_": + case "\u200C": + case "\u200D": + break; + default: + if (!util.isIdContinueChar(u)) { + throw invalidIdentifier(); + } + break; + } + buffer += u; + lexState = "identifierName"; + }, + sign() { + switch (c) { + case ".": + buffer = read(); + lexState = "decimalPointLeading"; + return; + case "0": + buffer = read(); + lexState = "zero"; + return; + case "1": + case "2": + case "3": + case "4": + case "5": + case "6": + case "7": + case "8": + case "9": + buffer = read(); + lexState = "decimalInteger"; + return; + case "I": + read(); + literal("nfinity"); + return newToken("numeric", sign * Infinity); + case "N": + read(); + literal("aN"); + return newToken("numeric", NaN); + } + throw invalidChar(read()); + }, + zero() { + switch (c) { + case ".": + buffer += read(); + lexState = "decimalPoint"; + return; + case "e": + case "E": + buffer += read(); + lexState = "decimalExponent"; + return; + case "x": + case "X": + buffer += read(); + lexState = "hexadecimal"; + return; + } + return newToken("numeric", sign * 0); + }, + decimalInteger() { + switch (c) { + case ".": + buffer += read(); + lexState = "decimalPoint"; + return; + case "e": + case "E": + buffer += read(); + lexState = "decimalExponent"; + return; + } + if (util.isDigit(c)) { + buffer += read(); + return; + } + return newToken("numeric", sign * Number(buffer)); + }, + decimalPointLeading() { + if (util.isDigit(c)) { + buffer += read(); + lexState = "decimalFraction"; + return; + } + throw invalidChar(read()); + }, + decimalPoint() { + switch (c) { + case "e": + case "E": + buffer += read(); + lexState = "decimalExponent"; + return; + } + if (util.isDigit(c)) { + buffer += read(); + lexState = "decimalFraction"; + return; + } + return newToken("numeric", sign * Number(buffer)); + }, + decimalFraction() { + switch (c) { + case "e": + case "E": + buffer += read(); + lexState = "decimalExponent"; + return; + } + if (util.isDigit(c)) { + buffer += read(); + return; + } + return newToken("numeric", sign * Number(buffer)); + }, + decimalExponent() { + switch (c) { + case "+": + case "-": + buffer += read(); + lexState = "decimalExponentSign"; + return; + } + if (util.isDigit(c)) { + buffer += read(); + lexState = "decimalExponentInteger"; + return; + } + throw invalidChar(read()); + }, + decimalExponentSign() { + if (util.isDigit(c)) { + buffer += read(); + lexState = "decimalExponentInteger"; + return; + } + throw invalidChar(read()); + }, + decimalExponentInteger() { + if (util.isDigit(c)) { + buffer += read(); + return; + } + return newToken("numeric", sign * Number(buffer)); + }, + hexadecimal() { + if (util.isHexDigit(c)) { + buffer += read(); + lexState = "hexadecimalInteger"; + return; + } + throw invalidChar(read()); + }, + hexadecimalInteger() { + if (util.isHexDigit(c)) { + buffer += read(); + return; + } + return newToken("numeric", sign * Number(buffer)); + }, + string() { + switch (c) { + case "\\": + read(); + buffer += escape(); + return; + case '"': + if (doubleQuote) { + read(); + return newToken("string", buffer); + } + buffer += read(); + return; + case "'": + if (!doubleQuote) { + read(); + return newToken("string", buffer); + } + buffer += read(); + return; + case "\n": + case "\r": + throw invalidChar(read()); + case "\u2028": + case "\u2029": + separatorChar(c); + break; + case void 0: + throw invalidChar(read()); + } + buffer += read(); + }, + start() { + switch (c) { + case "{": + case "[": + return newToken("punctuator", read()); + } + lexState = "value"; + }, + beforePropertyName() { + switch (c) { + case "$": + case "_": + buffer = read(); + lexState = "identifierName"; + return; + case "\\": + read(); + lexState = "identifierNameStartEscape"; + return; + case "}": + return newToken("punctuator", read()); + case '"': + case "'": + doubleQuote = read() === '"'; + lexState = "string"; + return; + } + if (util.isIdStartChar(c)) { + buffer += read(); + lexState = "identifierName"; + return; + } + throw invalidChar(read()); + }, + afterPropertyName() { + if (c === ":") { + return newToken("punctuator", read()); + } + throw invalidChar(read()); + }, + beforePropertyValue() { + lexState = "value"; + }, + afterPropertyValue() { + switch (c) { + case ",": + case "}": + return newToken("punctuator", read()); + } + throw invalidChar(read()); + }, + beforeArrayValue() { + if (c === "]") { + return newToken("punctuator", read()); + } + lexState = "value"; + }, + afterArrayValue() { + switch (c) { + case ",": + case "]": + return newToken("punctuator", read()); + } + throw invalidChar(read()); + }, + end() { + throw invalidChar(read()); + } + }; + function newToken(type, value) { + return { + type, + value, + line, + column + }; + } + function literal(s2) { + for (const c2 of s2) { + const p = peek(); + if (p !== c2) { + throw invalidChar(read()); + } + read(); + } + } + function escape() { + const c2 = peek(); + switch (c2) { + case "b": + read(); + return "\b"; + case "f": + read(); + return "\f"; + case "n": + read(); + return "\n"; + case "r": + read(); + return "\r"; + case "t": + read(); + return " "; + case "v": + read(); + return "\v"; + case "0": + read(); + if (util.isDigit(peek())) { + throw invalidChar(read()); + } + return "\0"; + case "x": + read(); + return hexEscape(); + case "u": + read(); + return unicodeEscape(); + case "\n": + case "\u2028": + case "\u2029": + read(); + return ""; + case "\r": + read(); + if (peek() === "\n") { + read(); + } + return ""; + case "1": + case "2": + case "3": + case "4": + case "5": + case "6": + case "7": + case "8": + case "9": + throw invalidChar(read()); + case void 0: + throw invalidChar(read()); + } + return read(); + } + function hexEscape() { + let buffer2 = ""; + let c2 = peek(); + if (!util.isHexDigit(c2)) { + throw invalidChar(read()); + } + buffer2 += read(); + c2 = peek(); + if (!util.isHexDigit(c2)) { + throw invalidChar(read()); + } + buffer2 += read(); + return String.fromCodePoint(parseInt(buffer2, 16)); + } + function unicodeEscape() { + let buffer2 = ""; + let count = 4; + while (count-- > 0) { + const c2 = peek(); + if (!util.isHexDigit(c2)) { + throw invalidChar(read()); + } + buffer2 += read(); + } + return String.fromCodePoint(parseInt(buffer2, 16)); + } + var parseStates = { + start() { + if (token.type === "eof") { + throw invalidEOF(); + } + push(); + }, + beforePropertyName() { + switch (token.type) { + case "identifier": + case "string": + key = token.value; + parseState = "afterPropertyName"; + return; + case "punctuator": + pop(); + return; + case "eof": + throw invalidEOF(); + } + }, + afterPropertyName() { + if (token.type === "eof") { + throw invalidEOF(); + } + parseState = "beforePropertyValue"; + }, + beforePropertyValue() { + if (token.type === "eof") { + throw invalidEOF(); + } + push(); + }, + beforeArrayValue() { + if (token.type === "eof") { + throw invalidEOF(); + } + if (token.type === "punctuator" && token.value === "]") { + pop(); + return; + } + push(); + }, + afterPropertyValue() { + if (token.type === "eof") { + throw invalidEOF(); + } + switch (token.value) { + case ",": + parseState = "beforePropertyName"; + return; + case "}": + pop(); + } + }, + afterArrayValue() { + if (token.type === "eof") { + throw invalidEOF(); + } + switch (token.value) { + case ",": + parseState = "beforeArrayValue"; + return; + case "]": + pop(); + } + }, + end() { + } + }; + function push() { + let value; + switch (token.type) { + case "punctuator": + switch (token.value) { + case "{": + value = {}; + break; + case "[": + value = []; + break; + } + break; + case "null": + case "boolean": + case "numeric": + case "string": + value = token.value; + break; + } + if (root === void 0) { + root = value; + } else { + const parent = stack[stack.length - 1]; + if (Array.isArray(parent)) { + parent.push(value); + } else { + Object.defineProperty(parent, key, { + value, + writable: true, + enumerable: true, + configurable: true + }); + } + } + if (value !== null && typeof value === "object") { + stack.push(value); + if (Array.isArray(value)) { + parseState = "beforeArrayValue"; + } else { + parseState = "beforePropertyName"; + } + } else { + const current = stack[stack.length - 1]; + if (current == null) { + parseState = "end"; + } else if (Array.isArray(current)) { + parseState = "afterArrayValue"; + } else { + parseState = "afterPropertyValue"; + } + } + } + function pop() { + stack.pop(); + const current = stack[stack.length - 1]; + if (current == null) { + parseState = "end"; + } else if (Array.isArray(current)) { + parseState = "afterArrayValue"; + } else { + parseState = "afterPropertyValue"; + } + } + function invalidChar(c2) { + if (c2 === void 0) { + return syntaxError(`JSON5: invalid end of input at ${line}:${column}`); + } + return syntaxError(`JSON5: invalid character '${formatChar(c2)}' at ${line}:${column}`); + } + function invalidEOF() { + return syntaxError(`JSON5: invalid end of input at ${line}:${column}`); + } + function invalidIdentifier() { + column -= 5; + return syntaxError(`JSON5: invalid identifier character at ${line}:${column}`); + } + function separatorChar(c2) { + console.warn(`JSON5: '${formatChar(c2)}' in strings is not valid ECMAScript; consider escaping`); + } + function formatChar(c2) { + const replacements = { + "'": "\\'", + '"': '\\"', + "\\": "\\\\", + "\b": "\\b", + "\f": "\\f", + "\n": "\\n", + "\r": "\\r", + " ": "\\t", + "\v": "\\v", + "\0": "\\0", + "\u2028": "\\u2028", + "\u2029": "\\u2029" + }; + if (replacements[c2]) { + return replacements[c2]; + } + if (c2 < " ") { + const hexString = c2.charCodeAt(0).toString(16); + return "\\x" + ("00" + hexString).substring(hexString.length); + } + return c2; + } + function syntaxError(message) { + const err = new SyntaxError(message); + err.lineNumber = line; + err.columnNumber = column; + return err; + } + } +}); + +// ../../node_modules/.pnpm/json5@2.2.3/node_modules/json5/lib/stringify.js +var require_stringify = __commonJS({ + "../../node_modules/.pnpm/json5@2.2.3/node_modules/json5/lib/stringify.js"(exports2, module2) { + var util = require_util(); + module2.exports = function stringify(value, replacer, space) { + const stack = []; + let indent = ""; + let propertyList; + let replacerFunc; + let gap = ""; + let quote; + if (replacer != null && typeof replacer === "object" && !Array.isArray(replacer)) { + space = replacer.space; + quote = replacer.quote; + replacer = replacer.replacer; + } + if (typeof replacer === "function") { + replacerFunc = replacer; + } else if (Array.isArray(replacer)) { + propertyList = []; + for (const v of replacer) { + let item; + if (typeof v === "string") { + item = v; + } else if (typeof v === "number" || v instanceof String || v instanceof Number) { + item = String(v); + } + if (item !== void 0 && propertyList.indexOf(item) < 0) { + propertyList.push(item); + } + } + } + if (space instanceof Number) { + space = Number(space); + } else if (space instanceof String) { + space = String(space); + } + if (typeof space === "number") { + if (space > 0) { + space = Math.min(10, Math.floor(space)); + gap = " ".substr(0, space); + } + } else if (typeof space === "string") { + gap = space.substr(0, 10); + } + return serializeProperty("", { "": value }); + function serializeProperty(key, holder) { + let value2 = holder[key]; + if (value2 != null) { + if (typeof value2.toJSON5 === "function") { + value2 = value2.toJSON5(key); + } else if (typeof value2.toJSON === "function") { + value2 = value2.toJSON(key); + } + } + if (replacerFunc) { + value2 = replacerFunc.call(holder, key, value2); + } + if (value2 instanceof Number) { + value2 = Number(value2); + } else if (value2 instanceof String) { + value2 = String(value2); + } else if (value2 instanceof Boolean) { + value2 = value2.valueOf(); + } + switch (value2) { + case null: + return "null"; + case true: + return "true"; + case false: + return "false"; + } + if (typeof value2 === "string") { + return quoteString(value2, false); + } + if (typeof value2 === "number") { + return String(value2); + } + if (typeof value2 === "object") { + return Array.isArray(value2) ? serializeArray(value2) : serializeObject(value2); + } + return void 0; + } + function quoteString(value2) { + const quotes = { + "'": 0.1, + '"': 0.2 + }; + const replacements = { + "'": "\\'", + '"': '\\"', + "\\": "\\\\", + "\b": "\\b", + "\f": "\\f", + "\n": "\\n", + "\r": "\\r", + " ": "\\t", + "\v": "\\v", + "\0": "\\0", + "\u2028": "\\u2028", + "\u2029": "\\u2029" + }; + let product = ""; + for (let i2 = 0; i2 < value2.length; i2++) { + const c = value2[i2]; + switch (c) { + case "'": + case '"': + quotes[c]++; + product += c; + continue; + case "\0": + if (util.isDigit(value2[i2 + 1])) { + product += "\\x00"; + continue; + } + } + if (replacements[c]) { + product += replacements[c]; + continue; + } + if (c < " ") { + let hexString = c.charCodeAt(0).toString(16); + product += "\\x" + ("00" + hexString).substring(hexString.length); + continue; + } + product += c; + } + const quoteChar = quote || Object.keys(quotes).reduce((a, b) => quotes[a] < quotes[b] ? a : b); + product = product.replace(new RegExp(quoteChar, "g"), replacements[quoteChar]); + return quoteChar + product + quoteChar; + } + function serializeObject(value2) { + if (stack.indexOf(value2) >= 0) { + throw TypeError("Converting circular structure to JSON5"); + } + stack.push(value2); + let stepback = indent; + indent = indent + gap; + let keys = propertyList || Object.keys(value2); + let partial = []; + for (const key of keys) { + const propertyString = serializeProperty(key, value2); + if (propertyString !== void 0) { + let member = serializeKey(key) + ":"; + if (gap !== "") { + member += " "; + } + member += propertyString; + partial.push(member); + } + } + let final; + if (partial.length === 0) { + final = "{}"; + } else { + let properties; + if (gap === "") { + properties = partial.join(","); + final = "{" + properties + "}"; + } else { + let separator = ",\n" + indent; + properties = partial.join(separator); + final = "{\n" + indent + properties + ",\n" + stepback + "}"; + } + } + stack.pop(); + indent = stepback; + return final; + } + function serializeKey(key) { + if (key.length === 0) { + return quoteString(key, true); + } + const firstChar = String.fromCodePoint(key.codePointAt(0)); + if (!util.isIdStartChar(firstChar)) { + return quoteString(key, true); + } + for (let i2 = firstChar.length; i2 < key.length; i2++) { + if (!util.isIdContinueChar(String.fromCodePoint(key.codePointAt(i2)))) { + return quoteString(key, true); + } + } + return key; + } + function serializeArray(value2) { + if (stack.indexOf(value2) >= 0) { + throw TypeError("Converting circular structure to JSON5"); + } + stack.push(value2); + let stepback = indent; + indent = indent + gap; + let partial = []; + for (let i2 = 0; i2 < value2.length; i2++) { + const propertyString = serializeProperty(String(i2), value2); + partial.push(propertyString !== void 0 ? propertyString : "null"); + } + let final; + if (partial.length === 0) { + final = "[]"; + } else { + if (gap === "") { + let properties = partial.join(","); + final = "[" + properties + "]"; + } else { + let separator = ",\n" + indent; + let properties = partial.join(separator); + final = "[\n" + indent + properties + ",\n" + stepback + "]"; + } + } + stack.pop(); + indent = stepback; + return final; + } + }; + } +}); + +// ../../node_modules/.pnpm/json5@2.2.3/node_modules/json5/lib/index.js +var require_lib = __commonJS({ + "../../node_modules/.pnpm/json5@2.2.3/node_modules/json5/lib/index.js"(exports2, module2) { + var parse = require_parse(); + var stringify = require_stringify(); + var JSON52 = { + parse, + stringify + }; + module2.exports = JSON52; + } +}); + +// ../../node_modules/.pnpm/boolean@3.2.0/node_modules/boolean/build/lib/boolean.js +var require_boolean = __commonJS({ + "../../node_modules/.pnpm/boolean@3.2.0/node_modules/boolean/build/lib/boolean.js"(exports2) { + "use strict"; + Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.boolean = void 0; + var boolean = function(value) { + switch (Object.prototype.toString.call(value)) { + case "[object String]": + return ["true", "t", "yes", "y", "on", "1"].includes(value.trim().toLowerCase()); + case "[object Number]": + return value.valueOf() === 1; + case "[object Boolean]": + return value.valueOf(); + default: + return false; + } + }; + exports2.boolean = boolean; + } +}); + +// ../../node_modules/.pnpm/boolean@3.2.0/node_modules/boolean/build/lib/isBooleanable.js +var require_isBooleanable = __commonJS({ + "../../node_modules/.pnpm/boolean@3.2.0/node_modules/boolean/build/lib/isBooleanable.js"(exports2) { + "use strict"; + Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.isBooleanable = void 0; + var isBooleanable = function(value) { + switch (Object.prototype.toString.call(value)) { + case "[object String]": + return [ + "true", + "t", + "yes", + "y", + "on", + "1", + "false", + "f", + "no", + "n", + "off", + "0" + ].includes(value.trim().toLowerCase()); + case "[object Number]": + return [0, 1].includes(value.valueOf()); + case "[object Boolean]": + return true; + default: + return false; + } + }; + exports2.isBooleanable = isBooleanable; + } +}); + +// ../../node_modules/.pnpm/boolean@3.2.0/node_modules/boolean/build/lib/index.js +var require_lib2 = __commonJS({ + "../../node_modules/.pnpm/boolean@3.2.0/node_modules/boolean/build/lib/index.js"(exports2) { + "use strict"; + Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.isBooleanable = exports2.boolean = void 0; + var boolean_1 = require_boolean(); + Object.defineProperty(exports2, "boolean", { enumerable: true, get: function() { + return boolean_1.boolean; + } }); + var isBooleanable_1 = require_isBooleanable(); + Object.defineProperty(exports2, "isBooleanable", { enumerable: true, get: function() { + return isBooleanable_1.isBooleanable; + } }); + } +}); + +// ../../node_modules/.pnpm/fast-printf@1.6.9/node_modules/fast-printf/dist/src/tokenize.js +var require_tokenize = __commonJS({ + "../../node_modules/.pnpm/fast-printf@1.6.9/node_modules/fast-printf/dist/src/tokenize.js"(exports2) { + "use strict"; + Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.tokenize = void 0; + var TokenRule = /(?:%(?([+0-]|-\+))?(?\d+)?(?\d+\$)?(?\.\d+)?(?[%BCESb-iosux]))|(\\%)/g; + var tokenize = (subject) => { + let matchResult; + const tokens = []; + let argumentIndex = 0; + let lastIndex = 0; + let lastToken = null; + while ((matchResult = TokenRule.exec(subject)) !== null) { + if (matchResult.index > lastIndex) { + lastToken = { + literal: subject.slice(lastIndex, matchResult.index), + type: "literal" + }; + tokens.push(lastToken); + } + const match = matchResult[0]; + lastIndex = matchResult.index + match.length; + if (match === "\\%" || match === "%%") { + if (lastToken && lastToken.type === "literal") { + lastToken.literal += "%"; + } else { + lastToken = { + literal: "%", + type: "literal" + }; + tokens.push(lastToken); + } + } else if (matchResult.groups) { + lastToken = { + conversion: matchResult.groups.conversion, + flag: matchResult.groups.flag || null, + placeholder: match, + position: matchResult.groups.position ? Number.parseInt(matchResult.groups.position, 10) - 1 : argumentIndex++, + precision: matchResult.groups.precision ? Number.parseInt(matchResult.groups.precision.slice(1), 10) : null, + type: "placeholder", + width: matchResult.groups.width ? Number.parseInt(matchResult.groups.width, 10) : null + }; + tokens.push(lastToken); + } + } + if (lastIndex <= subject.length - 1) { + if (lastToken && lastToken.type === "literal") { + lastToken.literal += subject.slice(lastIndex); + } else { + tokens.push({ + literal: subject.slice(lastIndex), + type: "literal" + }); + } + } + return tokens; + }; + exports2.tokenize = tokenize; + } +}); + +// ../../node_modules/.pnpm/fast-printf@1.6.9/node_modules/fast-printf/dist/src/createPrintf.js +var require_createPrintf = __commonJS({ + "../../node_modules/.pnpm/fast-printf@1.6.9/node_modules/fast-printf/dist/src/createPrintf.js"(exports2) { + "use strict"; + Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.createPrintf = void 0; + var boolean_1 = require_lib2(); + var tokenize_1 = require_tokenize(); + var formatDefaultUnboundExpression = (subject, token) => { + return token.placeholder; + }; + var createPrintf = (configuration) => { + var _a; + const padValue = (value, width, flag) => { + if (flag === "-") { + return value.padEnd(width, " "); + } else if (flag === "-+") { + return ((Number(value) >= 0 ? "+" : "") + value).padEnd(width, " "); + } else if (flag === "+") { + return ((Number(value) >= 0 ? "+" : "") + value).padStart(width, " "); + } else if (flag === "0") { + return value.padStart(width, "0"); + } else { + return value.padStart(width, " "); + } + }; + const formatUnboundExpression = (_a = configuration === null || configuration === void 0 ? void 0 : configuration.formatUnboundExpression) !== null && _a !== void 0 ? _a : formatDefaultUnboundExpression; + const cache2 = {}; + return (subject, ...boundValues) => { + let tokens = cache2[subject]; + if (!tokens) { + tokens = cache2[subject] = tokenize_1.tokenize(subject); + } + let result = ""; + for (const token of tokens) { + if (token.type === "literal") { + result += token.literal; + } else { + let boundValue = boundValues[token.position]; + if (boundValue === void 0) { + result += formatUnboundExpression(subject, token, boundValues); + } else if (token.conversion === "b") { + result += boolean_1.boolean(boundValue) ? "true" : "false"; + } else if (token.conversion === "B") { + result += boolean_1.boolean(boundValue) ? "TRUE" : "FALSE"; + } else if (token.conversion === "c") { + result += boundValue; + } else if (token.conversion === "C") { + result += String(boundValue).toUpperCase(); + } else if (token.conversion === "i" || token.conversion === "d") { + boundValue = String(Math.trunc(boundValue)); + if (token.width !== null) { + boundValue = padValue(boundValue, token.width, token.flag); + } + result += boundValue; + } else if (token.conversion === "e") { + result += Number(boundValue).toExponential(); + } else if (token.conversion === "E") { + result += Number(boundValue).toExponential().toUpperCase(); + } else if (token.conversion === "f") { + if (token.precision !== null) { + boundValue = Number(boundValue).toFixed(token.precision); + } + if (token.width !== null) { + boundValue = padValue(String(boundValue), token.width, token.flag); + } + result += boundValue; + } else if (token.conversion === "o") { + result += (Number.parseInt(String(boundValue), 10) >>> 0).toString(8); + } else if (token.conversion === "s") { + if (token.width !== null) { + boundValue = padValue(String(boundValue), token.width, token.flag); + } + result += boundValue; + } else if (token.conversion === "S") { + if (token.width !== null) { + boundValue = padValue(String(boundValue), token.width, token.flag); + } + result += String(boundValue).toUpperCase(); + } else if (token.conversion === "u") { + result += Number.parseInt(String(boundValue), 10) >>> 0; + } else if (token.conversion === "x") { + boundValue = (Number.parseInt(String(boundValue), 10) >>> 0).toString(16); + if (token.width !== null) { + boundValue = padValue(String(boundValue), token.width, token.flag); + } + result += boundValue; + } else { + throw new Error("Unknown format specifier."); + } + } + } + return result; + }; + }; + exports2.createPrintf = createPrintf; + } +}); + +// ../../node_modules/.pnpm/fast-printf@1.6.9/node_modules/fast-printf/dist/src/printf.js +var require_printf = __commonJS({ + "../../node_modules/.pnpm/fast-printf@1.6.9/node_modules/fast-printf/dist/src/printf.js"(exports2) { + "use strict"; + Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.printf = exports2.createPrintf = void 0; + var createPrintf_1 = require_createPrintf(); + Object.defineProperty(exports2, "createPrintf", { enumerable: true, get: function() { + return createPrintf_1.createPrintf; + } }); + exports2.printf = createPrintf_1.createPrintf(); + } +}); + +// ../../node_modules/.pnpm/web-streams-polyfill@3.3.3/node_modules/web-streams-polyfill/dist/ponyfill.es2018.js +var require_ponyfill_es2018 = __commonJS({ + "../../node_modules/.pnpm/web-streams-polyfill@3.3.3/node_modules/web-streams-polyfill/dist/ponyfill.es2018.js"(exports2, module2) { + (function(global2, factory) { + typeof exports2 === "object" && typeof module2 !== "undefined" ? factory(exports2) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global2 = typeof globalThis !== "undefined" ? globalThis : global2 || self, factory(global2.WebStreamsPolyfill = {})); + })(exports2, function(exports3) { + "use strict"; + function noop2() { + return void 0; + } + function typeIsObject(x2) { + return typeof x2 === "object" && x2 !== null || typeof x2 === "function"; + } + const rethrowAssertionErrorRejection = noop2; + function setFunctionName(fn, name) { + try { + Object.defineProperty(fn, "name", { + value: name, + configurable: true + }); + } catch (_a2) { + } + } + const originalPromise = Promise; + const originalPromiseThen = Promise.prototype.then; + const originalPromiseReject = Promise.reject.bind(originalPromise); + function newPromise(executor) { + return new originalPromise(executor); + } + function promiseResolvedWith(value) { + return newPromise((resolve) => resolve(value)); + } + function promiseRejectedWith(reason) { + return originalPromiseReject(reason); + } + function PerformPromiseThen(promise, onFulfilled, onRejected) { + return originalPromiseThen.call(promise, onFulfilled, onRejected); + } + function uponPromise(promise, onFulfilled, onRejected) { + PerformPromiseThen(PerformPromiseThen(promise, onFulfilled, onRejected), void 0, rethrowAssertionErrorRejection); + } + function uponFulfillment(promise, onFulfilled) { + uponPromise(promise, onFulfilled); + } + function uponRejection(promise, onRejected) { + uponPromise(promise, void 0, onRejected); + } + function transformPromiseWith(promise, fulfillmentHandler, rejectionHandler) { + return PerformPromiseThen(promise, fulfillmentHandler, rejectionHandler); + } + function setPromiseIsHandledToTrue(promise) { + PerformPromiseThen(promise, void 0, rethrowAssertionErrorRejection); + } + let _queueMicrotask = (callback) => { + if (typeof queueMicrotask === "function") { + _queueMicrotask = queueMicrotask; + } else { + const resolvedPromise = promiseResolvedWith(void 0); + _queueMicrotask = (cb) => PerformPromiseThen(resolvedPromise, cb); + } + return _queueMicrotask(callback); + }; + function reflectCall(F2, V, args) { + if (typeof F2 !== "function") { + throw new TypeError("Argument is not a function"); + } + return Function.prototype.apply.call(F2, V, args); + } + function promiseCall(F2, V, args) { + try { + return promiseResolvedWith(reflectCall(F2, V, args)); + } catch (value) { + return promiseRejectedWith(value); + } + } + const QUEUE_MAX_ARRAY_SIZE = 16384; + class SimpleQueue { + constructor() { + this._cursor = 0; + this._size = 0; + this._front = { + _elements: [], + _next: void 0 + }; + this._back = this._front; + this._cursor = 0; + this._size = 0; + } + get length() { + return this._size; + } + // For exception safety, this method is structured in order: + // 1. Read state + // 2. Calculate required state mutations + // 3. Perform state mutations + push(element) { + const oldBack = this._back; + let newBack = oldBack; + if (oldBack._elements.length === QUEUE_MAX_ARRAY_SIZE - 1) { + newBack = { + _elements: [], + _next: void 0 + }; + } + oldBack._elements.push(element); + if (newBack !== oldBack) { + this._back = newBack; + oldBack._next = newBack; + } + ++this._size; + } + // Like push(), shift() follows the read -> calculate -> mutate pattern for + // exception safety. + shift() { + const oldFront = this._front; + let newFront = oldFront; + const oldCursor = this._cursor; + let newCursor = oldCursor + 1; + const elements = oldFront._elements; + const element = elements[oldCursor]; + if (newCursor === QUEUE_MAX_ARRAY_SIZE) { + newFront = oldFront._next; + newCursor = 0; + } + --this._size; + this._cursor = newCursor; + if (oldFront !== newFront) { + this._front = newFront; + } + elements[oldCursor] = void 0; + return element; + } + // The tricky thing about forEach() is that it can be called + // re-entrantly. The queue may be mutated inside the callback. It is easy to + // see that push() within the callback has no negative effects since the end + // of the queue is checked for on every iteration. If shift() is called + // repeatedly within the callback then the next iteration may return an + // element that has been removed. In this case the callback will be called + // with undefined values until we either "catch up" with elements that still + // exist or reach the back of the queue. + forEach(callback) { + let i2 = this._cursor; + let node = this._front; + let elements = node._elements; + while (i2 !== elements.length || node._next !== void 0) { + if (i2 === elements.length) { + node = node._next; + elements = node._elements; + i2 = 0; + if (elements.length === 0) { + break; + } + } + callback(elements[i2]); + ++i2; + } + } + // Return the element that would be returned if shift() was called now, + // without modifying the queue. + peek() { + const front = this._front; + const cursor = this._cursor; + return front._elements[cursor]; + } + } + const AbortSteps = Symbol("[[AbortSteps]]"); + const ErrorSteps = Symbol("[[ErrorSteps]]"); + const CancelSteps = Symbol("[[CancelSteps]]"); + const PullSteps = Symbol("[[PullSteps]]"); + const ReleaseSteps = Symbol("[[ReleaseSteps]]"); + function ReadableStreamReaderGenericInitialize(reader, stream) { + reader._ownerReadableStream = stream; + stream._reader = reader; + if (stream._state === "readable") { + defaultReaderClosedPromiseInitialize(reader); + } else if (stream._state === "closed") { + defaultReaderClosedPromiseInitializeAsResolved(reader); + } else { + defaultReaderClosedPromiseInitializeAsRejected(reader, stream._storedError); + } + } + function ReadableStreamReaderGenericCancel(reader, reason) { + const stream = reader._ownerReadableStream; + return ReadableStreamCancel(stream, reason); + } + function ReadableStreamReaderGenericRelease(reader) { + const stream = reader._ownerReadableStream; + if (stream._state === "readable") { + defaultReaderClosedPromiseReject(reader, new TypeError(`Reader was released and can no longer be used to monitor the stream's closedness`)); + } else { + defaultReaderClosedPromiseResetToRejected(reader, new TypeError(`Reader was released and can no longer be used to monitor the stream's closedness`)); + } + stream._readableStreamController[ReleaseSteps](); + stream._reader = void 0; + reader._ownerReadableStream = void 0; + } + function readerLockException(name) { + return new TypeError("Cannot " + name + " a stream using a released reader"); + } + function defaultReaderClosedPromiseInitialize(reader) { + reader._closedPromise = newPromise((resolve, reject) => { + reader._closedPromise_resolve = resolve; + reader._closedPromise_reject = reject; + }); + } + function defaultReaderClosedPromiseInitializeAsRejected(reader, reason) { + defaultReaderClosedPromiseInitialize(reader); + defaultReaderClosedPromiseReject(reader, reason); + } + function defaultReaderClosedPromiseInitializeAsResolved(reader) { + defaultReaderClosedPromiseInitialize(reader); + defaultReaderClosedPromiseResolve(reader); + } + function defaultReaderClosedPromiseReject(reader, reason) { + if (reader._closedPromise_reject === void 0) { + return; + } + setPromiseIsHandledToTrue(reader._closedPromise); + reader._closedPromise_reject(reason); + reader._closedPromise_resolve = void 0; + reader._closedPromise_reject = void 0; + } + function defaultReaderClosedPromiseResetToRejected(reader, reason) { + defaultReaderClosedPromiseInitializeAsRejected(reader, reason); + } + function defaultReaderClosedPromiseResolve(reader) { + if (reader._closedPromise_resolve === void 0) { + return; + } + reader._closedPromise_resolve(void 0); + reader._closedPromise_resolve = void 0; + reader._closedPromise_reject = void 0; + } + const NumberIsFinite = Number.isFinite || function(x2) { + return typeof x2 === "number" && isFinite(x2); + }; + const MathTrunc = Math.trunc || function(v) { + return v < 0 ? Math.ceil(v) : Math.floor(v); + }; + function isDictionary(x2) { + return typeof x2 === "object" || typeof x2 === "function"; + } + function assertDictionary(obj, context) { + if (obj !== void 0 && !isDictionary(obj)) { + throw new TypeError(`${context} is not an object.`); + } + } + function assertFunction(x2, context) { + if (typeof x2 !== "function") { + throw new TypeError(`${context} is not a function.`); + } + } + function isObject(x2) { + return typeof x2 === "object" && x2 !== null || typeof x2 === "function"; + } + function assertObject(x2, context) { + if (!isObject(x2)) { + throw new TypeError(`${context} is not an object.`); + } + } + function assertRequiredArgument(x2, position, context) { + if (x2 === void 0) { + throw new TypeError(`Parameter ${position} is required in '${context}'.`); + } + } + function assertRequiredField(x2, field, context) { + if (x2 === void 0) { + throw new TypeError(`${field} is required in '${context}'.`); + } + } + function convertUnrestrictedDouble(value) { + return Number(value); + } + function censorNegativeZero(x2) { + return x2 === 0 ? 0 : x2; + } + function integerPart(x2) { + return censorNegativeZero(MathTrunc(x2)); + } + function convertUnsignedLongLongWithEnforceRange(value, context) { + const lowerBound = 0; + const upperBound = Number.MAX_SAFE_INTEGER; + let x2 = Number(value); + x2 = censorNegativeZero(x2); + if (!NumberIsFinite(x2)) { + throw new TypeError(`${context} is not a finite number`); + } + x2 = integerPart(x2); + if (x2 < lowerBound || x2 > upperBound) { + throw new TypeError(`${context} is outside the accepted range of ${lowerBound} to ${upperBound}, inclusive`); + } + if (!NumberIsFinite(x2) || x2 === 0) { + return 0; + } + return x2; + } + function assertReadableStream(x2, context) { + if (!IsReadableStream(x2)) { + throw new TypeError(`${context} is not a ReadableStream.`); + } + } + function AcquireReadableStreamDefaultReader(stream) { + return new ReadableStreamDefaultReader(stream); + } + function ReadableStreamAddReadRequest(stream, readRequest) { + stream._reader._readRequests.push(readRequest); + } + function ReadableStreamFulfillReadRequest(stream, chunk, done) { + const reader = stream._reader; + const readRequest = reader._readRequests.shift(); + if (done) { + readRequest._closeSteps(); + } else { + readRequest._chunkSteps(chunk); + } + } + function ReadableStreamGetNumReadRequests(stream) { + return stream._reader._readRequests.length; + } + function ReadableStreamHasDefaultReader(stream) { + const reader = stream._reader; + if (reader === void 0) { + return false; + } + if (!IsReadableStreamDefaultReader(reader)) { + return false; + } + return true; + } + class ReadableStreamDefaultReader { + constructor(stream) { + assertRequiredArgument(stream, 1, "ReadableStreamDefaultReader"); + assertReadableStream(stream, "First parameter"); + if (IsReadableStreamLocked(stream)) { + throw new TypeError("This stream has already been locked for exclusive reading by another reader"); + } + ReadableStreamReaderGenericInitialize(this, stream); + this._readRequests = new SimpleQueue(); + } + /** + * Returns a promise that will be fulfilled when the stream becomes closed, + * or rejected if the stream ever errors or the reader's lock is released before the stream finishes closing. + */ + get closed() { + if (!IsReadableStreamDefaultReader(this)) { + return promiseRejectedWith(defaultReaderBrandCheckException("closed")); + } + return this._closedPromise; + } + /** + * If the reader is active, behaves the same as {@link ReadableStream.cancel | stream.cancel(reason)}. + */ + cancel(reason = void 0) { + if (!IsReadableStreamDefaultReader(this)) { + return promiseRejectedWith(defaultReaderBrandCheckException("cancel")); + } + if (this._ownerReadableStream === void 0) { + return promiseRejectedWith(readerLockException("cancel")); + } + return ReadableStreamReaderGenericCancel(this, reason); + } + /** + * Returns a promise that allows access to the next chunk from the stream's internal queue, if available. + * + * If reading a chunk causes the queue to become empty, more data will be pulled from the underlying source. + */ + read() { + if (!IsReadableStreamDefaultReader(this)) { + return promiseRejectedWith(defaultReaderBrandCheckException("read")); + } + if (this._ownerReadableStream === void 0) { + return promiseRejectedWith(readerLockException("read from")); + } + let resolvePromise; + let rejectPromise; + const promise = newPromise((resolve, reject) => { + resolvePromise = resolve; + rejectPromise = reject; + }); + const readRequest = { + _chunkSteps: (chunk) => resolvePromise({ value: chunk, done: false }), + _closeSteps: () => resolvePromise({ value: void 0, done: true }), + _errorSteps: (e2) => rejectPromise(e2) + }; + ReadableStreamDefaultReaderRead(this, readRequest); + return promise; + } + /** + * Releases the reader's lock on the corresponding stream. After the lock is released, the reader is no longer active. + * If the associated stream is errored when the lock is released, the reader will appear errored in the same way + * from now on; otherwise, the reader will appear closed. + * + * A reader's lock cannot be released while it still has a pending read request, i.e., if a promise returned by + * the reader's {@link ReadableStreamDefaultReader.read | read()} method has not yet been settled. Attempting to + * do so will throw a `TypeError` and leave the reader locked to the stream. + */ + releaseLock() { + if (!IsReadableStreamDefaultReader(this)) { + throw defaultReaderBrandCheckException("releaseLock"); + } + if (this._ownerReadableStream === void 0) { + return; + } + ReadableStreamDefaultReaderRelease(this); + } + } + Object.defineProperties(ReadableStreamDefaultReader.prototype, { + cancel: { enumerable: true }, + read: { enumerable: true }, + releaseLock: { enumerable: true }, + closed: { enumerable: true } + }); + setFunctionName(ReadableStreamDefaultReader.prototype.cancel, "cancel"); + setFunctionName(ReadableStreamDefaultReader.prototype.read, "read"); + setFunctionName(ReadableStreamDefaultReader.prototype.releaseLock, "releaseLock"); + if (typeof Symbol.toStringTag === "symbol") { + Object.defineProperty(ReadableStreamDefaultReader.prototype, Symbol.toStringTag, { + value: "ReadableStreamDefaultReader", + configurable: true + }); + } + function IsReadableStreamDefaultReader(x2) { + if (!typeIsObject(x2)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x2, "_readRequests")) { + return false; + } + return x2 instanceof ReadableStreamDefaultReader; + } + function ReadableStreamDefaultReaderRead(reader, readRequest) { + const stream = reader._ownerReadableStream; + stream._disturbed = true; + if (stream._state === "closed") { + readRequest._closeSteps(); + } else if (stream._state === "errored") { + readRequest._errorSteps(stream._storedError); + } else { + stream._readableStreamController[PullSteps](readRequest); + } + } + function ReadableStreamDefaultReaderRelease(reader) { + ReadableStreamReaderGenericRelease(reader); + const e2 = new TypeError("Reader was released"); + ReadableStreamDefaultReaderErrorReadRequests(reader, e2); + } + function ReadableStreamDefaultReaderErrorReadRequests(reader, e2) { + const readRequests = reader._readRequests; + reader._readRequests = new SimpleQueue(); + readRequests.forEach((readRequest) => { + readRequest._errorSteps(e2); + }); + } + function defaultReaderBrandCheckException(name) { + return new TypeError(`ReadableStreamDefaultReader.prototype.${name} can only be used on a ReadableStreamDefaultReader`); + } + const AsyncIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf(async function* () { + }).prototype); + class ReadableStreamAsyncIteratorImpl { + constructor(reader, preventCancel) { + this._ongoingPromise = void 0; + this._isFinished = false; + this._reader = reader; + this._preventCancel = preventCancel; + } + next() { + const nextSteps = () => this._nextSteps(); + this._ongoingPromise = this._ongoingPromise ? transformPromiseWith(this._ongoingPromise, nextSteps, nextSteps) : nextSteps(); + return this._ongoingPromise; + } + return(value) { + const returnSteps = () => this._returnSteps(value); + return this._ongoingPromise ? transformPromiseWith(this._ongoingPromise, returnSteps, returnSteps) : returnSteps(); + } + _nextSteps() { + if (this._isFinished) { + return Promise.resolve({ value: void 0, done: true }); + } + const reader = this._reader; + let resolvePromise; + let rejectPromise; + const promise = newPromise((resolve, reject) => { + resolvePromise = resolve; + rejectPromise = reject; + }); + const readRequest = { + _chunkSteps: (chunk) => { + this._ongoingPromise = void 0; + _queueMicrotask(() => resolvePromise({ value: chunk, done: false })); + }, + _closeSteps: () => { + this._ongoingPromise = void 0; + this._isFinished = true; + ReadableStreamReaderGenericRelease(reader); + resolvePromise({ value: void 0, done: true }); + }, + _errorSteps: (reason) => { + this._ongoingPromise = void 0; + this._isFinished = true; + ReadableStreamReaderGenericRelease(reader); + rejectPromise(reason); + } + }; + ReadableStreamDefaultReaderRead(reader, readRequest); + return promise; + } + _returnSteps(value) { + if (this._isFinished) { + return Promise.resolve({ value, done: true }); + } + this._isFinished = true; + const reader = this._reader; + if (!this._preventCancel) { + const result = ReadableStreamReaderGenericCancel(reader, value); + ReadableStreamReaderGenericRelease(reader); + return transformPromiseWith(result, () => ({ value, done: true })); + } + ReadableStreamReaderGenericRelease(reader); + return promiseResolvedWith({ value, done: true }); + } + } + const ReadableStreamAsyncIteratorPrototype = { + next() { + if (!IsReadableStreamAsyncIterator(this)) { + return promiseRejectedWith(streamAsyncIteratorBrandCheckException("next")); + } + return this._asyncIteratorImpl.next(); + }, + return(value) { + if (!IsReadableStreamAsyncIterator(this)) { + return promiseRejectedWith(streamAsyncIteratorBrandCheckException("return")); + } + return this._asyncIteratorImpl.return(value); + } + }; + Object.setPrototypeOf(ReadableStreamAsyncIteratorPrototype, AsyncIteratorPrototype); + function AcquireReadableStreamAsyncIterator(stream, preventCancel) { + const reader = AcquireReadableStreamDefaultReader(stream); + const impl = new ReadableStreamAsyncIteratorImpl(reader, preventCancel); + const iterator = Object.create(ReadableStreamAsyncIteratorPrototype); + iterator._asyncIteratorImpl = impl; + return iterator; + } + function IsReadableStreamAsyncIterator(x2) { + if (!typeIsObject(x2)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x2, "_asyncIteratorImpl")) { + return false; + } + try { + return x2._asyncIteratorImpl instanceof ReadableStreamAsyncIteratorImpl; + } catch (_a2) { + return false; + } + } + function streamAsyncIteratorBrandCheckException(name) { + return new TypeError(`ReadableStreamAsyncIterator.${name} can only be used on a ReadableSteamAsyncIterator`); + } + const NumberIsNaN = Number.isNaN || function(x2) { + return x2 !== x2; + }; + var _a, _b, _c; + function CreateArrayFromList(elements) { + return elements.slice(); + } + function CopyDataBlockBytes(dest, destOffset, src, srcOffset, n) { + new Uint8Array(dest).set(new Uint8Array(src, srcOffset, n), destOffset); + } + let TransferArrayBuffer = (O) => { + if (typeof O.transfer === "function") { + TransferArrayBuffer = (buffer) => buffer.transfer(); + } else if (typeof structuredClone === "function") { + TransferArrayBuffer = (buffer) => structuredClone(buffer, { transfer: [buffer] }); + } else { + TransferArrayBuffer = (buffer) => buffer; + } + return TransferArrayBuffer(O); + }; + let IsDetachedBuffer = (O) => { + if (typeof O.detached === "boolean") { + IsDetachedBuffer = (buffer) => buffer.detached; + } else { + IsDetachedBuffer = (buffer) => buffer.byteLength === 0; + } + return IsDetachedBuffer(O); + }; + function ArrayBufferSlice(buffer, begin, end) { + if (buffer.slice) { + return buffer.slice(begin, end); + } + const length = end - begin; + const slice = new ArrayBuffer(length); + CopyDataBlockBytes(slice, 0, buffer, begin, length); + return slice; + } + function GetMethod(receiver, prop) { + const func = receiver[prop]; + if (func === void 0 || func === null) { + return void 0; + } + if (typeof func !== "function") { + throw new TypeError(`${String(prop)} is not a function`); + } + return func; + } + function CreateAsyncFromSyncIterator(syncIteratorRecord) { + const syncIterable = { + [Symbol.iterator]: () => syncIteratorRecord.iterator + }; + const asyncIterator = async function* () { + return yield* syncIterable; + }(); + const nextMethod = asyncIterator.next; + return { iterator: asyncIterator, nextMethod, done: false }; + } + const SymbolAsyncIterator = (_c = (_a = Symbol.asyncIterator) !== null && _a !== void 0 ? _a : (_b = Symbol.for) === null || _b === void 0 ? void 0 : _b.call(Symbol, "Symbol.asyncIterator")) !== null && _c !== void 0 ? _c : "@@asyncIterator"; + function GetIterator(obj, hint = "sync", method) { + if (method === void 0) { + if (hint === "async") { + method = GetMethod(obj, SymbolAsyncIterator); + if (method === void 0) { + const syncMethod = GetMethod(obj, Symbol.iterator); + const syncIteratorRecord = GetIterator(obj, "sync", syncMethod); + return CreateAsyncFromSyncIterator(syncIteratorRecord); + } + } else { + method = GetMethod(obj, Symbol.iterator); + } + } + if (method === void 0) { + throw new TypeError("The object is not iterable"); + } + const iterator = reflectCall(method, obj, []); + if (!typeIsObject(iterator)) { + throw new TypeError("The iterator method must return an object"); + } + const nextMethod = iterator.next; + return { iterator, nextMethod, done: false }; + } + function IteratorNext(iteratorRecord) { + const result = reflectCall(iteratorRecord.nextMethod, iteratorRecord.iterator, []); + if (!typeIsObject(result)) { + throw new TypeError("The iterator.next() method must return an object"); + } + return result; + } + function IteratorComplete(iterResult) { + return Boolean(iterResult.done); + } + function IteratorValue(iterResult) { + return iterResult.value; + } + function IsNonNegativeNumber(v) { + if (typeof v !== "number") { + return false; + } + if (NumberIsNaN(v)) { + return false; + } + if (v < 0) { + return false; + } + return true; + } + function CloneAsUint8Array(O) { + const buffer = ArrayBufferSlice(O.buffer, O.byteOffset, O.byteOffset + O.byteLength); + return new Uint8Array(buffer); + } + function DequeueValue(container) { + const pair = container._queue.shift(); + container._queueTotalSize -= pair.size; + if (container._queueTotalSize < 0) { + container._queueTotalSize = 0; + } + return pair.value; + } + function EnqueueValueWithSize(container, value, size) { + if (!IsNonNegativeNumber(size) || size === Infinity) { + throw new RangeError("Size must be a finite, non-NaN, non-negative number."); + } + container._queue.push({ value, size }); + container._queueTotalSize += size; + } + function PeekQueueValue(container) { + const pair = container._queue.peek(); + return pair.value; + } + function ResetQueue(container) { + container._queue = new SimpleQueue(); + container._queueTotalSize = 0; + } + function isDataViewConstructor(ctor) { + return ctor === DataView; + } + function isDataView(view) { + return isDataViewConstructor(view.constructor); + } + function arrayBufferViewElementSize(ctor) { + if (isDataViewConstructor(ctor)) { + return 1; + } + return ctor.BYTES_PER_ELEMENT; + } + class ReadableStreamBYOBRequest { + constructor() { + throw new TypeError("Illegal constructor"); + } + /** + * Returns the view for writing in to, or `null` if the BYOB request has already been responded to. + */ + get view() { + if (!IsReadableStreamBYOBRequest(this)) { + throw byobRequestBrandCheckException("view"); + } + return this._view; + } + respond(bytesWritten) { + if (!IsReadableStreamBYOBRequest(this)) { + throw byobRequestBrandCheckException("respond"); + } + assertRequiredArgument(bytesWritten, 1, "respond"); + bytesWritten = convertUnsignedLongLongWithEnforceRange(bytesWritten, "First parameter"); + if (this._associatedReadableByteStreamController === void 0) { + throw new TypeError("This BYOB request has been invalidated"); + } + if (IsDetachedBuffer(this._view.buffer)) { + throw new TypeError(`The BYOB request's buffer has been detached and so cannot be used as a response`); + } + ReadableByteStreamControllerRespond(this._associatedReadableByteStreamController, bytesWritten); + } + respondWithNewView(view) { + if (!IsReadableStreamBYOBRequest(this)) { + throw byobRequestBrandCheckException("respondWithNewView"); + } + assertRequiredArgument(view, 1, "respondWithNewView"); + if (!ArrayBuffer.isView(view)) { + throw new TypeError("You can only respond with array buffer views"); + } + if (this._associatedReadableByteStreamController === void 0) { + throw new TypeError("This BYOB request has been invalidated"); + } + if (IsDetachedBuffer(view.buffer)) { + throw new TypeError("The given view's buffer has been detached and so cannot be used as a response"); + } + ReadableByteStreamControllerRespondWithNewView(this._associatedReadableByteStreamController, view); + } + } + Object.defineProperties(ReadableStreamBYOBRequest.prototype, { + respond: { enumerable: true }, + respondWithNewView: { enumerable: true }, + view: { enumerable: true } + }); + setFunctionName(ReadableStreamBYOBRequest.prototype.respond, "respond"); + setFunctionName(ReadableStreamBYOBRequest.prototype.respondWithNewView, "respondWithNewView"); + if (typeof Symbol.toStringTag === "symbol") { + Object.defineProperty(ReadableStreamBYOBRequest.prototype, Symbol.toStringTag, { + value: "ReadableStreamBYOBRequest", + configurable: true + }); + } + class ReadableByteStreamController { + constructor() { + throw new TypeError("Illegal constructor"); + } + /** + * Returns the current BYOB pull request, or `null` if there isn't one. + */ + get byobRequest() { + if (!IsReadableByteStreamController(this)) { + throw byteStreamControllerBrandCheckException("byobRequest"); + } + return ReadableByteStreamControllerGetBYOBRequest(this); + } + /** + * Returns the desired size to fill the controlled stream's internal queue. It can be negative, if the queue is + * over-full. An underlying byte source ought to use this information to determine when and how to apply backpressure. + */ + get desiredSize() { + if (!IsReadableByteStreamController(this)) { + throw byteStreamControllerBrandCheckException("desiredSize"); + } + return ReadableByteStreamControllerGetDesiredSize(this); + } + /** + * Closes the controlled readable stream. Consumers will still be able to read any previously-enqueued chunks from + * the stream, but once those are read, the stream will become closed. + */ + close() { + if (!IsReadableByteStreamController(this)) { + throw byteStreamControllerBrandCheckException("close"); + } + if (this._closeRequested) { + throw new TypeError("The stream has already been closed; do not close it again!"); + } + const state = this._controlledReadableByteStream._state; + if (state !== "readable") { + throw new TypeError(`The stream (in ${state} state) is not in the readable state and cannot be closed`); + } + ReadableByteStreamControllerClose(this); + } + enqueue(chunk) { + if (!IsReadableByteStreamController(this)) { + throw byteStreamControllerBrandCheckException("enqueue"); + } + assertRequiredArgument(chunk, 1, "enqueue"); + if (!ArrayBuffer.isView(chunk)) { + throw new TypeError("chunk must be an array buffer view"); + } + if (chunk.byteLength === 0) { + throw new TypeError("chunk must have non-zero byteLength"); + } + if (chunk.buffer.byteLength === 0) { + throw new TypeError(`chunk's buffer must have non-zero byteLength`); + } + if (this._closeRequested) { + throw new TypeError("stream is closed or draining"); + } + const state = this._controlledReadableByteStream._state; + if (state !== "readable") { + throw new TypeError(`The stream (in ${state} state) is not in the readable state and cannot be enqueued to`); + } + ReadableByteStreamControllerEnqueue(this, chunk); + } + /** + * Errors the controlled readable stream, making all future interactions with it fail with the given error `e`. + */ + error(e2 = void 0) { + if (!IsReadableByteStreamController(this)) { + throw byteStreamControllerBrandCheckException("error"); + } + ReadableByteStreamControllerError(this, e2); + } + /** @internal */ + [CancelSteps](reason) { + ReadableByteStreamControllerClearPendingPullIntos(this); + ResetQueue(this); + const result = this._cancelAlgorithm(reason); + ReadableByteStreamControllerClearAlgorithms(this); + return result; + } + /** @internal */ + [PullSteps](readRequest) { + const stream = this._controlledReadableByteStream; + if (this._queueTotalSize > 0) { + ReadableByteStreamControllerFillReadRequestFromQueue(this, readRequest); + return; + } + const autoAllocateChunkSize = this._autoAllocateChunkSize; + if (autoAllocateChunkSize !== void 0) { + let buffer; + try { + buffer = new ArrayBuffer(autoAllocateChunkSize); + } catch (bufferE) { + readRequest._errorSteps(bufferE); + return; + } + const pullIntoDescriptor = { + buffer, + bufferByteLength: autoAllocateChunkSize, + byteOffset: 0, + byteLength: autoAllocateChunkSize, + bytesFilled: 0, + minimumFill: 1, + elementSize: 1, + viewConstructor: Uint8Array, + readerType: "default" + }; + this._pendingPullIntos.push(pullIntoDescriptor); + } + ReadableStreamAddReadRequest(stream, readRequest); + ReadableByteStreamControllerCallPullIfNeeded(this); + } + /** @internal */ + [ReleaseSteps]() { + if (this._pendingPullIntos.length > 0) { + const firstPullInto = this._pendingPullIntos.peek(); + firstPullInto.readerType = "none"; + this._pendingPullIntos = new SimpleQueue(); + this._pendingPullIntos.push(firstPullInto); + } + } + } + Object.defineProperties(ReadableByteStreamController.prototype, { + close: { enumerable: true }, + enqueue: { enumerable: true }, + error: { enumerable: true }, + byobRequest: { enumerable: true }, + desiredSize: { enumerable: true } + }); + setFunctionName(ReadableByteStreamController.prototype.close, "close"); + setFunctionName(ReadableByteStreamController.prototype.enqueue, "enqueue"); + setFunctionName(ReadableByteStreamController.prototype.error, "error"); + if (typeof Symbol.toStringTag === "symbol") { + Object.defineProperty(ReadableByteStreamController.prototype, Symbol.toStringTag, { + value: "ReadableByteStreamController", + configurable: true + }); + } + function IsReadableByteStreamController(x2) { + if (!typeIsObject(x2)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x2, "_controlledReadableByteStream")) { + return false; + } + return x2 instanceof ReadableByteStreamController; + } + function IsReadableStreamBYOBRequest(x2) { + if (!typeIsObject(x2)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x2, "_associatedReadableByteStreamController")) { + return false; + } + return x2 instanceof ReadableStreamBYOBRequest; + } + function ReadableByteStreamControllerCallPullIfNeeded(controller) { + const shouldPull = ReadableByteStreamControllerShouldCallPull(controller); + if (!shouldPull) { + return; + } + if (controller._pulling) { + controller._pullAgain = true; + return; + } + controller._pulling = true; + const pullPromise = controller._pullAlgorithm(); + uponPromise(pullPromise, () => { + controller._pulling = false; + if (controller._pullAgain) { + controller._pullAgain = false; + ReadableByteStreamControllerCallPullIfNeeded(controller); + } + return null; + }, (e2) => { + ReadableByteStreamControllerError(controller, e2); + return null; + }); + } + function ReadableByteStreamControllerClearPendingPullIntos(controller) { + ReadableByteStreamControllerInvalidateBYOBRequest(controller); + controller._pendingPullIntos = new SimpleQueue(); + } + function ReadableByteStreamControllerCommitPullIntoDescriptor(stream, pullIntoDescriptor) { + let done = false; + if (stream._state === "closed") { + done = true; + } + const filledView = ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor); + if (pullIntoDescriptor.readerType === "default") { + ReadableStreamFulfillReadRequest(stream, filledView, done); + } else { + ReadableStreamFulfillReadIntoRequest(stream, filledView, done); + } + } + function ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor) { + const bytesFilled = pullIntoDescriptor.bytesFilled; + const elementSize = pullIntoDescriptor.elementSize; + return new pullIntoDescriptor.viewConstructor(pullIntoDescriptor.buffer, pullIntoDescriptor.byteOffset, bytesFilled / elementSize); + } + function ReadableByteStreamControllerEnqueueChunkToQueue(controller, buffer, byteOffset, byteLength) { + controller._queue.push({ buffer, byteOffset, byteLength }); + controller._queueTotalSize += byteLength; + } + function ReadableByteStreamControllerEnqueueClonedChunkToQueue(controller, buffer, byteOffset, byteLength) { + let clonedChunk; + try { + clonedChunk = ArrayBufferSlice(buffer, byteOffset, byteOffset + byteLength); + } catch (cloneE) { + ReadableByteStreamControllerError(controller, cloneE); + throw cloneE; + } + ReadableByteStreamControllerEnqueueChunkToQueue(controller, clonedChunk, 0, byteLength); + } + function ReadableByteStreamControllerEnqueueDetachedPullIntoToQueue(controller, firstDescriptor) { + if (firstDescriptor.bytesFilled > 0) { + ReadableByteStreamControllerEnqueueClonedChunkToQueue(controller, firstDescriptor.buffer, firstDescriptor.byteOffset, firstDescriptor.bytesFilled); + } + ReadableByteStreamControllerShiftPendingPullInto(controller); + } + function ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor) { + const maxBytesToCopy = Math.min(controller._queueTotalSize, pullIntoDescriptor.byteLength - pullIntoDescriptor.bytesFilled); + const maxBytesFilled = pullIntoDescriptor.bytesFilled + maxBytesToCopy; + let totalBytesToCopyRemaining = maxBytesToCopy; + let ready = false; + const remainderBytes = maxBytesFilled % pullIntoDescriptor.elementSize; + const maxAlignedBytes = maxBytesFilled - remainderBytes; + if (maxAlignedBytes >= pullIntoDescriptor.minimumFill) { + totalBytesToCopyRemaining = maxAlignedBytes - pullIntoDescriptor.bytesFilled; + ready = true; + } + const queue = controller._queue; + while (totalBytesToCopyRemaining > 0) { + const headOfQueue = queue.peek(); + const bytesToCopy = Math.min(totalBytesToCopyRemaining, headOfQueue.byteLength); + const destStart = pullIntoDescriptor.byteOffset + pullIntoDescriptor.bytesFilled; + CopyDataBlockBytes(pullIntoDescriptor.buffer, destStart, headOfQueue.buffer, headOfQueue.byteOffset, bytesToCopy); + if (headOfQueue.byteLength === bytesToCopy) { + queue.shift(); + } else { + headOfQueue.byteOffset += bytesToCopy; + headOfQueue.byteLength -= bytesToCopy; + } + controller._queueTotalSize -= bytesToCopy; + ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, bytesToCopy, pullIntoDescriptor); + totalBytesToCopyRemaining -= bytesToCopy; + } + return ready; + } + function ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, size, pullIntoDescriptor) { + pullIntoDescriptor.bytesFilled += size; + } + function ReadableByteStreamControllerHandleQueueDrain(controller) { + if (controller._queueTotalSize === 0 && controller._closeRequested) { + ReadableByteStreamControllerClearAlgorithms(controller); + ReadableStreamClose(controller._controlledReadableByteStream); + } else { + ReadableByteStreamControllerCallPullIfNeeded(controller); + } + } + function ReadableByteStreamControllerInvalidateBYOBRequest(controller) { + if (controller._byobRequest === null) { + return; + } + controller._byobRequest._associatedReadableByteStreamController = void 0; + controller._byobRequest._view = null; + controller._byobRequest = null; + } + function ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller) { + while (controller._pendingPullIntos.length > 0) { + if (controller._queueTotalSize === 0) { + return; + } + const pullIntoDescriptor = controller._pendingPullIntos.peek(); + if (ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor)) { + ReadableByteStreamControllerShiftPendingPullInto(controller); + ReadableByteStreamControllerCommitPullIntoDescriptor(controller._controlledReadableByteStream, pullIntoDescriptor); + } + } + } + function ReadableByteStreamControllerProcessReadRequestsUsingQueue(controller) { + const reader = controller._controlledReadableByteStream._reader; + while (reader._readRequests.length > 0) { + if (controller._queueTotalSize === 0) { + return; + } + const readRequest = reader._readRequests.shift(); + ReadableByteStreamControllerFillReadRequestFromQueue(controller, readRequest); + } + } + function ReadableByteStreamControllerPullInto(controller, view, min, readIntoRequest) { + const stream = controller._controlledReadableByteStream; + const ctor = view.constructor; + const elementSize = arrayBufferViewElementSize(ctor); + const { byteOffset, byteLength } = view; + const minimumFill = min * elementSize; + let buffer; + try { + buffer = TransferArrayBuffer(view.buffer); + } catch (e2) { + readIntoRequest._errorSteps(e2); + return; + } + const pullIntoDescriptor = { + buffer, + bufferByteLength: buffer.byteLength, + byteOffset, + byteLength, + bytesFilled: 0, + minimumFill, + elementSize, + viewConstructor: ctor, + readerType: "byob" + }; + if (controller._pendingPullIntos.length > 0) { + controller._pendingPullIntos.push(pullIntoDescriptor); + ReadableStreamAddReadIntoRequest(stream, readIntoRequest); + return; + } + if (stream._state === "closed") { + const emptyView = new ctor(pullIntoDescriptor.buffer, pullIntoDescriptor.byteOffset, 0); + readIntoRequest._closeSteps(emptyView); + return; + } + if (controller._queueTotalSize > 0) { + if (ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor)) { + const filledView = ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor); + ReadableByteStreamControllerHandleQueueDrain(controller); + readIntoRequest._chunkSteps(filledView); + return; + } + if (controller._closeRequested) { + const e2 = new TypeError("Insufficient bytes to fill elements in the given buffer"); + ReadableByteStreamControllerError(controller, e2); + readIntoRequest._errorSteps(e2); + return; + } + } + controller._pendingPullIntos.push(pullIntoDescriptor); + ReadableStreamAddReadIntoRequest(stream, readIntoRequest); + ReadableByteStreamControllerCallPullIfNeeded(controller); + } + function ReadableByteStreamControllerRespondInClosedState(controller, firstDescriptor) { + if (firstDescriptor.readerType === "none") { + ReadableByteStreamControllerShiftPendingPullInto(controller); + } + const stream = controller._controlledReadableByteStream; + if (ReadableStreamHasBYOBReader(stream)) { + while (ReadableStreamGetNumReadIntoRequests(stream) > 0) { + const pullIntoDescriptor = ReadableByteStreamControllerShiftPendingPullInto(controller); + ReadableByteStreamControllerCommitPullIntoDescriptor(stream, pullIntoDescriptor); + } + } + } + function ReadableByteStreamControllerRespondInReadableState(controller, bytesWritten, pullIntoDescriptor) { + ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, bytesWritten, pullIntoDescriptor); + if (pullIntoDescriptor.readerType === "none") { + ReadableByteStreamControllerEnqueueDetachedPullIntoToQueue(controller, pullIntoDescriptor); + ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller); + return; + } + if (pullIntoDescriptor.bytesFilled < pullIntoDescriptor.minimumFill) { + return; + } + ReadableByteStreamControllerShiftPendingPullInto(controller); + const remainderSize = pullIntoDescriptor.bytesFilled % pullIntoDescriptor.elementSize; + if (remainderSize > 0) { + const end = pullIntoDescriptor.byteOffset + pullIntoDescriptor.bytesFilled; + ReadableByteStreamControllerEnqueueClonedChunkToQueue(controller, pullIntoDescriptor.buffer, end - remainderSize, remainderSize); + } + pullIntoDescriptor.bytesFilled -= remainderSize; + ReadableByteStreamControllerCommitPullIntoDescriptor(controller._controlledReadableByteStream, pullIntoDescriptor); + ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller); + } + function ReadableByteStreamControllerRespondInternal(controller, bytesWritten) { + const firstDescriptor = controller._pendingPullIntos.peek(); + ReadableByteStreamControllerInvalidateBYOBRequest(controller); + const state = controller._controlledReadableByteStream._state; + if (state === "closed") { + ReadableByteStreamControllerRespondInClosedState(controller, firstDescriptor); + } else { + ReadableByteStreamControllerRespondInReadableState(controller, bytesWritten, firstDescriptor); + } + ReadableByteStreamControllerCallPullIfNeeded(controller); + } + function ReadableByteStreamControllerShiftPendingPullInto(controller) { + const descriptor = controller._pendingPullIntos.shift(); + return descriptor; + } + function ReadableByteStreamControllerShouldCallPull(controller) { + const stream = controller._controlledReadableByteStream; + if (stream._state !== "readable") { + return false; + } + if (controller._closeRequested) { + return false; + } + if (!controller._started) { + return false; + } + if (ReadableStreamHasDefaultReader(stream) && ReadableStreamGetNumReadRequests(stream) > 0) { + return true; + } + if (ReadableStreamHasBYOBReader(stream) && ReadableStreamGetNumReadIntoRequests(stream) > 0) { + return true; + } + const desiredSize = ReadableByteStreamControllerGetDesiredSize(controller); + if (desiredSize > 0) { + return true; + } + return false; + } + function ReadableByteStreamControllerClearAlgorithms(controller) { + controller._pullAlgorithm = void 0; + controller._cancelAlgorithm = void 0; + } + function ReadableByteStreamControllerClose(controller) { + const stream = controller._controlledReadableByteStream; + if (controller._closeRequested || stream._state !== "readable") { + return; + } + if (controller._queueTotalSize > 0) { + controller._closeRequested = true; + return; + } + if (controller._pendingPullIntos.length > 0) { + const firstPendingPullInto = controller._pendingPullIntos.peek(); + if (firstPendingPullInto.bytesFilled % firstPendingPullInto.elementSize !== 0) { + const e2 = new TypeError("Insufficient bytes to fill elements in the given buffer"); + ReadableByteStreamControllerError(controller, e2); + throw e2; + } + } + ReadableByteStreamControllerClearAlgorithms(controller); + ReadableStreamClose(stream); + } + function ReadableByteStreamControllerEnqueue(controller, chunk) { + const stream = controller._controlledReadableByteStream; + if (controller._closeRequested || stream._state !== "readable") { + return; + } + const { buffer, byteOffset, byteLength } = chunk; + if (IsDetachedBuffer(buffer)) { + throw new TypeError("chunk's buffer is detached and so cannot be enqueued"); + } + const transferredBuffer = TransferArrayBuffer(buffer); + if (controller._pendingPullIntos.length > 0) { + const firstPendingPullInto = controller._pendingPullIntos.peek(); + if (IsDetachedBuffer(firstPendingPullInto.buffer)) { + throw new TypeError("The BYOB request's buffer has been detached and so cannot be filled with an enqueued chunk"); + } + ReadableByteStreamControllerInvalidateBYOBRequest(controller); + firstPendingPullInto.buffer = TransferArrayBuffer(firstPendingPullInto.buffer); + if (firstPendingPullInto.readerType === "none") { + ReadableByteStreamControllerEnqueueDetachedPullIntoToQueue(controller, firstPendingPullInto); + } + } + if (ReadableStreamHasDefaultReader(stream)) { + ReadableByteStreamControllerProcessReadRequestsUsingQueue(controller); + if (ReadableStreamGetNumReadRequests(stream) === 0) { + ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength); + } else { + if (controller._pendingPullIntos.length > 0) { + ReadableByteStreamControllerShiftPendingPullInto(controller); + } + const transferredView = new Uint8Array(transferredBuffer, byteOffset, byteLength); + ReadableStreamFulfillReadRequest(stream, transferredView, false); + } + } else if (ReadableStreamHasBYOBReader(stream)) { + ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength); + ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller); + } else { + ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength); + } + ReadableByteStreamControllerCallPullIfNeeded(controller); + } + function ReadableByteStreamControllerError(controller, e2) { + const stream = controller._controlledReadableByteStream; + if (stream._state !== "readable") { + return; + } + ReadableByteStreamControllerClearPendingPullIntos(controller); + ResetQueue(controller); + ReadableByteStreamControllerClearAlgorithms(controller); + ReadableStreamError(stream, e2); + } + function ReadableByteStreamControllerFillReadRequestFromQueue(controller, readRequest) { + const entry = controller._queue.shift(); + controller._queueTotalSize -= entry.byteLength; + ReadableByteStreamControllerHandleQueueDrain(controller); + const view = new Uint8Array(entry.buffer, entry.byteOffset, entry.byteLength); + readRequest._chunkSteps(view); + } + function ReadableByteStreamControllerGetBYOBRequest(controller) { + if (controller._byobRequest === null && controller._pendingPullIntos.length > 0) { + const firstDescriptor = controller._pendingPullIntos.peek(); + const view = new Uint8Array(firstDescriptor.buffer, firstDescriptor.byteOffset + firstDescriptor.bytesFilled, firstDescriptor.byteLength - firstDescriptor.bytesFilled); + const byobRequest = Object.create(ReadableStreamBYOBRequest.prototype); + SetUpReadableStreamBYOBRequest(byobRequest, controller, view); + controller._byobRequest = byobRequest; + } + return controller._byobRequest; + } + function ReadableByteStreamControllerGetDesiredSize(controller) { + const state = controller._controlledReadableByteStream._state; + if (state === "errored") { + return null; + } + if (state === "closed") { + return 0; + } + return controller._strategyHWM - controller._queueTotalSize; + } + function ReadableByteStreamControllerRespond(controller, bytesWritten) { + const firstDescriptor = controller._pendingPullIntos.peek(); + const state = controller._controlledReadableByteStream._state; + if (state === "closed") { + if (bytesWritten !== 0) { + throw new TypeError("bytesWritten must be 0 when calling respond() on a closed stream"); + } + } else { + if (bytesWritten === 0) { + throw new TypeError("bytesWritten must be greater than 0 when calling respond() on a readable stream"); + } + if (firstDescriptor.bytesFilled + bytesWritten > firstDescriptor.byteLength) { + throw new RangeError("bytesWritten out of range"); + } + } + firstDescriptor.buffer = TransferArrayBuffer(firstDescriptor.buffer); + ReadableByteStreamControllerRespondInternal(controller, bytesWritten); + } + function ReadableByteStreamControllerRespondWithNewView(controller, view) { + const firstDescriptor = controller._pendingPullIntos.peek(); + const state = controller._controlledReadableByteStream._state; + if (state === "closed") { + if (view.byteLength !== 0) { + throw new TypeError("The view's length must be 0 when calling respondWithNewView() on a closed stream"); + } + } else { + if (view.byteLength === 0) { + throw new TypeError("The view's length must be greater than 0 when calling respondWithNewView() on a readable stream"); + } + } + if (firstDescriptor.byteOffset + firstDescriptor.bytesFilled !== view.byteOffset) { + throw new RangeError("The region specified by view does not match byobRequest"); + } + if (firstDescriptor.bufferByteLength !== view.buffer.byteLength) { + throw new RangeError("The buffer of view has different capacity than byobRequest"); + } + if (firstDescriptor.bytesFilled + view.byteLength > firstDescriptor.byteLength) { + throw new RangeError("The region specified by view is larger than byobRequest"); + } + const viewByteLength = view.byteLength; + firstDescriptor.buffer = TransferArrayBuffer(view.buffer); + ReadableByteStreamControllerRespondInternal(controller, viewByteLength); + } + function SetUpReadableByteStreamController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, autoAllocateChunkSize) { + controller._controlledReadableByteStream = stream; + controller._pullAgain = false; + controller._pulling = false; + controller._byobRequest = null; + controller._queue = controller._queueTotalSize = void 0; + ResetQueue(controller); + controller._closeRequested = false; + controller._started = false; + controller._strategyHWM = highWaterMark; + controller._pullAlgorithm = pullAlgorithm; + controller._cancelAlgorithm = cancelAlgorithm; + controller._autoAllocateChunkSize = autoAllocateChunkSize; + controller._pendingPullIntos = new SimpleQueue(); + stream._readableStreamController = controller; + const startResult = startAlgorithm(); + uponPromise(promiseResolvedWith(startResult), () => { + controller._started = true; + ReadableByteStreamControllerCallPullIfNeeded(controller); + return null; + }, (r2) => { + ReadableByteStreamControllerError(controller, r2); + return null; + }); + } + function SetUpReadableByteStreamControllerFromUnderlyingSource(stream, underlyingByteSource, highWaterMark) { + const controller = Object.create(ReadableByteStreamController.prototype); + let startAlgorithm; + let pullAlgorithm; + let cancelAlgorithm; + if (underlyingByteSource.start !== void 0) { + startAlgorithm = () => underlyingByteSource.start(controller); + } else { + startAlgorithm = () => void 0; + } + if (underlyingByteSource.pull !== void 0) { + pullAlgorithm = () => underlyingByteSource.pull(controller); + } else { + pullAlgorithm = () => promiseResolvedWith(void 0); + } + if (underlyingByteSource.cancel !== void 0) { + cancelAlgorithm = (reason) => underlyingByteSource.cancel(reason); + } else { + cancelAlgorithm = () => promiseResolvedWith(void 0); + } + const autoAllocateChunkSize = underlyingByteSource.autoAllocateChunkSize; + if (autoAllocateChunkSize === 0) { + throw new TypeError("autoAllocateChunkSize must be greater than 0"); + } + SetUpReadableByteStreamController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, autoAllocateChunkSize); + } + function SetUpReadableStreamBYOBRequest(request, controller, view) { + request._associatedReadableByteStreamController = controller; + request._view = view; + } + function byobRequestBrandCheckException(name) { + return new TypeError(`ReadableStreamBYOBRequest.prototype.${name} can only be used on a ReadableStreamBYOBRequest`); + } + function byteStreamControllerBrandCheckException(name) { + return new TypeError(`ReadableByteStreamController.prototype.${name} can only be used on a ReadableByteStreamController`); + } + function convertReaderOptions(options, context) { + assertDictionary(options, context); + const mode = options === null || options === void 0 ? void 0 : options.mode; + return { + mode: mode === void 0 ? void 0 : convertReadableStreamReaderMode(mode, `${context} has member 'mode' that`) + }; + } + function convertReadableStreamReaderMode(mode, context) { + mode = `${mode}`; + if (mode !== "byob") { + throw new TypeError(`${context} '${mode}' is not a valid enumeration value for ReadableStreamReaderMode`); + } + return mode; + } + function convertByobReadOptions(options, context) { + var _a2; + assertDictionary(options, context); + const min = (_a2 = options === null || options === void 0 ? void 0 : options.min) !== null && _a2 !== void 0 ? _a2 : 1; + return { + min: convertUnsignedLongLongWithEnforceRange(min, `${context} has member 'min' that`) + }; + } + function AcquireReadableStreamBYOBReader(stream) { + return new ReadableStreamBYOBReader(stream); + } + function ReadableStreamAddReadIntoRequest(stream, readIntoRequest) { + stream._reader._readIntoRequests.push(readIntoRequest); + } + function ReadableStreamFulfillReadIntoRequest(stream, chunk, done) { + const reader = stream._reader; + const readIntoRequest = reader._readIntoRequests.shift(); + if (done) { + readIntoRequest._closeSteps(chunk); + } else { + readIntoRequest._chunkSteps(chunk); + } + } + function ReadableStreamGetNumReadIntoRequests(stream) { + return stream._reader._readIntoRequests.length; + } + function ReadableStreamHasBYOBReader(stream) { + const reader = stream._reader; + if (reader === void 0) { + return false; + } + if (!IsReadableStreamBYOBReader(reader)) { + return false; + } + return true; + } + class ReadableStreamBYOBReader { + constructor(stream) { + assertRequiredArgument(stream, 1, "ReadableStreamBYOBReader"); + assertReadableStream(stream, "First parameter"); + if (IsReadableStreamLocked(stream)) { + throw new TypeError("This stream has already been locked for exclusive reading by another reader"); + } + if (!IsReadableByteStreamController(stream._readableStreamController)) { + throw new TypeError("Cannot construct a ReadableStreamBYOBReader for a stream not constructed with a byte source"); + } + ReadableStreamReaderGenericInitialize(this, stream); + this._readIntoRequests = new SimpleQueue(); + } + /** + * Returns a promise that will be fulfilled when the stream becomes closed, or rejected if the stream ever errors or + * the reader's lock is released before the stream finishes closing. + */ + get closed() { + if (!IsReadableStreamBYOBReader(this)) { + return promiseRejectedWith(byobReaderBrandCheckException("closed")); + } + return this._closedPromise; + } + /** + * If the reader is active, behaves the same as {@link ReadableStream.cancel | stream.cancel(reason)}. + */ + cancel(reason = void 0) { + if (!IsReadableStreamBYOBReader(this)) { + return promiseRejectedWith(byobReaderBrandCheckException("cancel")); + } + if (this._ownerReadableStream === void 0) { + return promiseRejectedWith(readerLockException("cancel")); + } + return ReadableStreamReaderGenericCancel(this, reason); + } + read(view, rawOptions = {}) { + if (!IsReadableStreamBYOBReader(this)) { + return promiseRejectedWith(byobReaderBrandCheckException("read")); + } + if (!ArrayBuffer.isView(view)) { + return promiseRejectedWith(new TypeError("view must be an array buffer view")); + } + if (view.byteLength === 0) { + return promiseRejectedWith(new TypeError("view must have non-zero byteLength")); + } + if (view.buffer.byteLength === 0) { + return promiseRejectedWith(new TypeError(`view's buffer must have non-zero byteLength`)); + } + if (IsDetachedBuffer(view.buffer)) { + return promiseRejectedWith(new TypeError("view's buffer has been detached")); + } + let options; + try { + options = convertByobReadOptions(rawOptions, "options"); + } catch (e2) { + return promiseRejectedWith(e2); + } + const min = options.min; + if (min === 0) { + return promiseRejectedWith(new TypeError("options.min must be greater than 0")); + } + if (!isDataView(view)) { + if (min > view.length) { + return promiseRejectedWith(new RangeError("options.min must be less than or equal to view's length")); + } + } else if (min > view.byteLength) { + return promiseRejectedWith(new RangeError("options.min must be less than or equal to view's byteLength")); + } + if (this._ownerReadableStream === void 0) { + return promiseRejectedWith(readerLockException("read from")); + } + let resolvePromise; + let rejectPromise; + const promise = newPromise((resolve, reject) => { + resolvePromise = resolve; + rejectPromise = reject; + }); + const readIntoRequest = { + _chunkSteps: (chunk) => resolvePromise({ value: chunk, done: false }), + _closeSteps: (chunk) => resolvePromise({ value: chunk, done: true }), + _errorSteps: (e2) => rejectPromise(e2) + }; + ReadableStreamBYOBReaderRead(this, view, min, readIntoRequest); + return promise; + } + /** + * Releases the reader's lock on the corresponding stream. After the lock is released, the reader is no longer active. + * If the associated stream is errored when the lock is released, the reader will appear errored in the same way + * from now on; otherwise, the reader will appear closed. + * + * A reader's lock cannot be released while it still has a pending read request, i.e., if a promise returned by + * the reader's {@link ReadableStreamBYOBReader.read | read()} method has not yet been settled. Attempting to + * do so will throw a `TypeError` and leave the reader locked to the stream. + */ + releaseLock() { + if (!IsReadableStreamBYOBReader(this)) { + throw byobReaderBrandCheckException("releaseLock"); + } + if (this._ownerReadableStream === void 0) { + return; + } + ReadableStreamBYOBReaderRelease(this); + } + } + Object.defineProperties(ReadableStreamBYOBReader.prototype, { + cancel: { enumerable: true }, + read: { enumerable: true }, + releaseLock: { enumerable: true }, + closed: { enumerable: true } + }); + setFunctionName(ReadableStreamBYOBReader.prototype.cancel, "cancel"); + setFunctionName(ReadableStreamBYOBReader.prototype.read, "read"); + setFunctionName(ReadableStreamBYOBReader.prototype.releaseLock, "releaseLock"); + if (typeof Symbol.toStringTag === "symbol") { + Object.defineProperty(ReadableStreamBYOBReader.prototype, Symbol.toStringTag, { + value: "ReadableStreamBYOBReader", + configurable: true + }); + } + function IsReadableStreamBYOBReader(x2) { + if (!typeIsObject(x2)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x2, "_readIntoRequests")) { + return false; + } + return x2 instanceof ReadableStreamBYOBReader; + } + function ReadableStreamBYOBReaderRead(reader, view, min, readIntoRequest) { + const stream = reader._ownerReadableStream; + stream._disturbed = true; + if (stream._state === "errored") { + readIntoRequest._errorSteps(stream._storedError); + } else { + ReadableByteStreamControllerPullInto(stream._readableStreamController, view, min, readIntoRequest); + } + } + function ReadableStreamBYOBReaderRelease(reader) { + ReadableStreamReaderGenericRelease(reader); + const e2 = new TypeError("Reader was released"); + ReadableStreamBYOBReaderErrorReadIntoRequests(reader, e2); + } + function ReadableStreamBYOBReaderErrorReadIntoRequests(reader, e2) { + const readIntoRequests = reader._readIntoRequests; + reader._readIntoRequests = new SimpleQueue(); + readIntoRequests.forEach((readIntoRequest) => { + readIntoRequest._errorSteps(e2); + }); + } + function byobReaderBrandCheckException(name) { + return new TypeError(`ReadableStreamBYOBReader.prototype.${name} can only be used on a ReadableStreamBYOBReader`); + } + function ExtractHighWaterMark(strategy, defaultHWM) { + const { highWaterMark } = strategy; + if (highWaterMark === void 0) { + return defaultHWM; + } + if (NumberIsNaN(highWaterMark) || highWaterMark < 0) { + throw new RangeError("Invalid highWaterMark"); + } + return highWaterMark; + } + function ExtractSizeAlgorithm(strategy) { + const { size } = strategy; + if (!size) { + return () => 1; + } + return size; + } + function convertQueuingStrategy(init, context) { + assertDictionary(init, context); + const highWaterMark = init === null || init === void 0 ? void 0 : init.highWaterMark; + const size = init === null || init === void 0 ? void 0 : init.size; + return { + highWaterMark: highWaterMark === void 0 ? void 0 : convertUnrestrictedDouble(highWaterMark), + size: size === void 0 ? void 0 : convertQueuingStrategySize(size, `${context} has member 'size' that`) + }; + } + function convertQueuingStrategySize(fn, context) { + assertFunction(fn, context); + return (chunk) => convertUnrestrictedDouble(fn(chunk)); + } + function convertUnderlyingSink(original, context) { + assertDictionary(original, context); + const abort = original === null || original === void 0 ? void 0 : original.abort; + const close = original === null || original === void 0 ? void 0 : original.close; + const start = original === null || original === void 0 ? void 0 : original.start; + const type = original === null || original === void 0 ? void 0 : original.type; + const write = original === null || original === void 0 ? void 0 : original.write; + return { + abort: abort === void 0 ? void 0 : convertUnderlyingSinkAbortCallback(abort, original, `${context} has member 'abort' that`), + close: close === void 0 ? void 0 : convertUnderlyingSinkCloseCallback(close, original, `${context} has member 'close' that`), + start: start === void 0 ? void 0 : convertUnderlyingSinkStartCallback(start, original, `${context} has member 'start' that`), + write: write === void 0 ? void 0 : convertUnderlyingSinkWriteCallback(write, original, `${context} has member 'write' that`), + type + }; + } + function convertUnderlyingSinkAbortCallback(fn, original, context) { + assertFunction(fn, context); + return (reason) => promiseCall(fn, original, [reason]); + } + function convertUnderlyingSinkCloseCallback(fn, original, context) { + assertFunction(fn, context); + return () => promiseCall(fn, original, []); + } + function convertUnderlyingSinkStartCallback(fn, original, context) { + assertFunction(fn, context); + return (controller) => reflectCall(fn, original, [controller]); + } + function convertUnderlyingSinkWriteCallback(fn, original, context) { + assertFunction(fn, context); + return (chunk, controller) => promiseCall(fn, original, [chunk, controller]); + } + function assertWritableStream(x2, context) { + if (!IsWritableStream(x2)) { + throw new TypeError(`${context} is not a WritableStream.`); + } + } + function isAbortSignal2(value) { + if (typeof value !== "object" || value === null) { + return false; + } + try { + return typeof value.aborted === "boolean"; + } catch (_a2) { + return false; + } + } + const supportsAbortController = typeof AbortController === "function"; + function createAbortController() { + if (supportsAbortController) { + return new AbortController(); + } + return void 0; + } + class WritableStream { + constructor(rawUnderlyingSink = {}, rawStrategy = {}) { + if (rawUnderlyingSink === void 0) { + rawUnderlyingSink = null; + } else { + assertObject(rawUnderlyingSink, "First parameter"); + } + const strategy = convertQueuingStrategy(rawStrategy, "Second parameter"); + const underlyingSink = convertUnderlyingSink(rawUnderlyingSink, "First parameter"); + InitializeWritableStream(this); + const type = underlyingSink.type; + if (type !== void 0) { + throw new RangeError("Invalid type is specified"); + } + const sizeAlgorithm = ExtractSizeAlgorithm(strategy); + const highWaterMark = ExtractHighWaterMark(strategy, 1); + SetUpWritableStreamDefaultControllerFromUnderlyingSink(this, underlyingSink, highWaterMark, sizeAlgorithm); + } + /** + * Returns whether or not the writable stream is locked to a writer. + */ + get locked() { + if (!IsWritableStream(this)) { + throw streamBrandCheckException$2("locked"); + } + return IsWritableStreamLocked(this); + } + /** + * Aborts the stream, signaling that the producer can no longer successfully write to the stream and it is to be + * immediately moved to an errored state, with any queued-up writes discarded. This will also execute any abort + * mechanism of the underlying sink. + * + * The returned promise will fulfill if the stream shuts down successfully, or reject if the underlying sink signaled + * that there was an error doing so. Additionally, it will reject with a `TypeError` (without attempting to cancel + * the stream) if the stream is currently locked. + */ + abort(reason = void 0) { + if (!IsWritableStream(this)) { + return promiseRejectedWith(streamBrandCheckException$2("abort")); + } + if (IsWritableStreamLocked(this)) { + return promiseRejectedWith(new TypeError("Cannot abort a stream that already has a writer")); + } + return WritableStreamAbort(this, reason); + } + /** + * Closes the stream. The underlying sink will finish processing any previously-written chunks, before invoking its + * close behavior. During this time any further attempts to write will fail (without erroring the stream). + * + * The method returns a promise that will fulfill if all remaining chunks are successfully written and the stream + * successfully closes, or rejects if an error is encountered during this process. Additionally, it will reject with + * a `TypeError` (without attempting to cancel the stream) if the stream is currently locked. + */ + close() { + if (!IsWritableStream(this)) { + return promiseRejectedWith(streamBrandCheckException$2("close")); + } + if (IsWritableStreamLocked(this)) { + return promiseRejectedWith(new TypeError("Cannot close a stream that already has a writer")); + } + if (WritableStreamCloseQueuedOrInFlight(this)) { + return promiseRejectedWith(new TypeError("Cannot close an already-closing stream")); + } + return WritableStreamClose(this); + } + /** + * Creates a {@link WritableStreamDefaultWriter | writer} and locks the stream to the new writer. While the stream + * is locked, no other writer can be acquired until this one is released. + * + * This functionality is especially useful for creating abstractions that desire the ability to write to a stream + * without interruption or interleaving. By getting a writer for the stream, you can ensure nobody else can write at + * the same time, which would cause the resulting written data to be unpredictable and probably useless. + */ + getWriter() { + if (!IsWritableStream(this)) { + throw streamBrandCheckException$2("getWriter"); + } + return AcquireWritableStreamDefaultWriter(this); + } + } + Object.defineProperties(WritableStream.prototype, { + abort: { enumerable: true }, + close: { enumerable: true }, + getWriter: { enumerable: true }, + locked: { enumerable: true } + }); + setFunctionName(WritableStream.prototype.abort, "abort"); + setFunctionName(WritableStream.prototype.close, "close"); + setFunctionName(WritableStream.prototype.getWriter, "getWriter"); + if (typeof Symbol.toStringTag === "symbol") { + Object.defineProperty(WritableStream.prototype, Symbol.toStringTag, { + value: "WritableStream", + configurable: true + }); + } + function AcquireWritableStreamDefaultWriter(stream) { + return new WritableStreamDefaultWriter(stream); + } + function CreateWritableStream(startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, highWaterMark = 1, sizeAlgorithm = () => 1) { + const stream = Object.create(WritableStream.prototype); + InitializeWritableStream(stream); + const controller = Object.create(WritableStreamDefaultController.prototype); + SetUpWritableStreamDefaultController(stream, controller, startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, highWaterMark, sizeAlgorithm); + return stream; + } + function InitializeWritableStream(stream) { + stream._state = "writable"; + stream._storedError = void 0; + stream._writer = void 0; + stream._writableStreamController = void 0; + stream._writeRequests = new SimpleQueue(); + stream._inFlightWriteRequest = void 0; + stream._closeRequest = void 0; + stream._inFlightCloseRequest = void 0; + stream._pendingAbortRequest = void 0; + stream._backpressure = false; + } + function IsWritableStream(x2) { + if (!typeIsObject(x2)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x2, "_writableStreamController")) { + return false; + } + return x2 instanceof WritableStream; + } + function IsWritableStreamLocked(stream) { + if (stream._writer === void 0) { + return false; + } + return true; + } + function WritableStreamAbort(stream, reason) { + var _a2; + if (stream._state === "closed" || stream._state === "errored") { + return promiseResolvedWith(void 0); + } + stream._writableStreamController._abortReason = reason; + (_a2 = stream._writableStreamController._abortController) === null || _a2 === void 0 ? void 0 : _a2.abort(reason); + const state = stream._state; + if (state === "closed" || state === "errored") { + return promiseResolvedWith(void 0); + } + if (stream._pendingAbortRequest !== void 0) { + return stream._pendingAbortRequest._promise; + } + let wasAlreadyErroring = false; + if (state === "erroring") { + wasAlreadyErroring = true; + reason = void 0; + } + const promise = newPromise((resolve, reject) => { + stream._pendingAbortRequest = { + _promise: void 0, + _resolve: resolve, + _reject: reject, + _reason: reason, + _wasAlreadyErroring: wasAlreadyErroring + }; + }); + stream._pendingAbortRequest._promise = promise; + if (!wasAlreadyErroring) { + WritableStreamStartErroring(stream, reason); + } + return promise; + } + function WritableStreamClose(stream) { + const state = stream._state; + if (state === "closed" || state === "errored") { + return promiseRejectedWith(new TypeError(`The stream (in ${state} state) is not in the writable state and cannot be closed`)); + } + const promise = newPromise((resolve, reject) => { + const closeRequest = { + _resolve: resolve, + _reject: reject + }; + stream._closeRequest = closeRequest; + }); + const writer = stream._writer; + if (writer !== void 0 && stream._backpressure && state === "writable") { + defaultWriterReadyPromiseResolve(writer); + } + WritableStreamDefaultControllerClose(stream._writableStreamController); + return promise; + } + function WritableStreamAddWriteRequest(stream) { + const promise = newPromise((resolve, reject) => { + const writeRequest = { + _resolve: resolve, + _reject: reject + }; + stream._writeRequests.push(writeRequest); + }); + return promise; + } + function WritableStreamDealWithRejection(stream, error) { + const state = stream._state; + if (state === "writable") { + WritableStreamStartErroring(stream, error); + return; + } + WritableStreamFinishErroring(stream); + } + function WritableStreamStartErroring(stream, reason) { + const controller = stream._writableStreamController; + stream._state = "erroring"; + stream._storedError = reason; + const writer = stream._writer; + if (writer !== void 0) { + WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, reason); + } + if (!WritableStreamHasOperationMarkedInFlight(stream) && controller._started) { + WritableStreamFinishErroring(stream); + } + } + function WritableStreamFinishErroring(stream) { + stream._state = "errored"; + stream._writableStreamController[ErrorSteps](); + const storedError = stream._storedError; + stream._writeRequests.forEach((writeRequest) => { + writeRequest._reject(storedError); + }); + stream._writeRequests = new SimpleQueue(); + if (stream._pendingAbortRequest === void 0) { + WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream); + return; + } + const abortRequest = stream._pendingAbortRequest; + stream._pendingAbortRequest = void 0; + if (abortRequest._wasAlreadyErroring) { + abortRequest._reject(storedError); + WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream); + return; + } + const promise = stream._writableStreamController[AbortSteps](abortRequest._reason); + uponPromise(promise, () => { + abortRequest._resolve(); + WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream); + return null; + }, (reason) => { + abortRequest._reject(reason); + WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream); + return null; + }); + } + function WritableStreamFinishInFlightWrite(stream) { + stream._inFlightWriteRequest._resolve(void 0); + stream._inFlightWriteRequest = void 0; + } + function WritableStreamFinishInFlightWriteWithError(stream, error) { + stream._inFlightWriteRequest._reject(error); + stream._inFlightWriteRequest = void 0; + WritableStreamDealWithRejection(stream, error); + } + function WritableStreamFinishInFlightClose(stream) { + stream._inFlightCloseRequest._resolve(void 0); + stream._inFlightCloseRequest = void 0; + const state = stream._state; + if (state === "erroring") { + stream._storedError = void 0; + if (stream._pendingAbortRequest !== void 0) { + stream._pendingAbortRequest._resolve(); + stream._pendingAbortRequest = void 0; + } + } + stream._state = "closed"; + const writer = stream._writer; + if (writer !== void 0) { + defaultWriterClosedPromiseResolve(writer); + } + } + function WritableStreamFinishInFlightCloseWithError(stream, error) { + stream._inFlightCloseRequest._reject(error); + stream._inFlightCloseRequest = void 0; + if (stream._pendingAbortRequest !== void 0) { + stream._pendingAbortRequest._reject(error); + stream._pendingAbortRequest = void 0; + } + WritableStreamDealWithRejection(stream, error); + } + function WritableStreamCloseQueuedOrInFlight(stream) { + if (stream._closeRequest === void 0 && stream._inFlightCloseRequest === void 0) { + return false; + } + return true; + } + function WritableStreamHasOperationMarkedInFlight(stream) { + if (stream._inFlightWriteRequest === void 0 && stream._inFlightCloseRequest === void 0) { + return false; + } + return true; + } + function WritableStreamMarkCloseRequestInFlight(stream) { + stream._inFlightCloseRequest = stream._closeRequest; + stream._closeRequest = void 0; + } + function WritableStreamMarkFirstWriteRequestInFlight(stream) { + stream._inFlightWriteRequest = stream._writeRequests.shift(); + } + function WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream) { + if (stream._closeRequest !== void 0) { + stream._closeRequest._reject(stream._storedError); + stream._closeRequest = void 0; + } + const writer = stream._writer; + if (writer !== void 0) { + defaultWriterClosedPromiseReject(writer, stream._storedError); + } + } + function WritableStreamUpdateBackpressure(stream, backpressure) { + const writer = stream._writer; + if (writer !== void 0 && backpressure !== stream._backpressure) { + if (backpressure) { + defaultWriterReadyPromiseReset(writer); + } else { + defaultWriterReadyPromiseResolve(writer); + } + } + stream._backpressure = backpressure; + } + class WritableStreamDefaultWriter { + constructor(stream) { + assertRequiredArgument(stream, 1, "WritableStreamDefaultWriter"); + assertWritableStream(stream, "First parameter"); + if (IsWritableStreamLocked(stream)) { + throw new TypeError("This stream has already been locked for exclusive writing by another writer"); + } + this._ownerWritableStream = stream; + stream._writer = this; + const state = stream._state; + if (state === "writable") { + if (!WritableStreamCloseQueuedOrInFlight(stream) && stream._backpressure) { + defaultWriterReadyPromiseInitialize(this); + } else { + defaultWriterReadyPromiseInitializeAsResolved(this); + } + defaultWriterClosedPromiseInitialize(this); + } else if (state === "erroring") { + defaultWriterReadyPromiseInitializeAsRejected(this, stream._storedError); + defaultWriterClosedPromiseInitialize(this); + } else if (state === "closed") { + defaultWriterReadyPromiseInitializeAsResolved(this); + defaultWriterClosedPromiseInitializeAsResolved(this); + } else { + const storedError = stream._storedError; + defaultWriterReadyPromiseInitializeAsRejected(this, storedError); + defaultWriterClosedPromiseInitializeAsRejected(this, storedError); + } + } + /** + * Returns a promise that will be fulfilled when the stream becomes closed, or rejected if the stream ever errors or + * the writer’s lock is released before the stream finishes closing. + */ + get closed() { + if (!IsWritableStreamDefaultWriter(this)) { + return promiseRejectedWith(defaultWriterBrandCheckException("closed")); + } + return this._closedPromise; + } + /** + * Returns the desired size to fill the stream’s internal queue. It can be negative, if the queue is over-full. + * A producer can use this information to determine the right amount of data to write. + * + * It will be `null` if the stream cannot be successfully written to (due to either being errored, or having an abort + * queued up). It will return zero if the stream is closed. And the getter will throw an exception if invoked when + * the writer’s lock is released. + */ + get desiredSize() { + if (!IsWritableStreamDefaultWriter(this)) { + throw defaultWriterBrandCheckException("desiredSize"); + } + if (this._ownerWritableStream === void 0) { + throw defaultWriterLockException("desiredSize"); + } + return WritableStreamDefaultWriterGetDesiredSize(this); + } + /** + * Returns a promise that will be fulfilled when the desired size to fill the stream’s internal queue transitions + * from non-positive to positive, signaling that it is no longer applying backpressure. Once the desired size dips + * back to zero or below, the getter will return a new promise that stays pending until the next transition. + * + * If the stream becomes errored or aborted, or the writer’s lock is released, the returned promise will become + * rejected. + */ + get ready() { + if (!IsWritableStreamDefaultWriter(this)) { + return promiseRejectedWith(defaultWriterBrandCheckException("ready")); + } + return this._readyPromise; + } + /** + * If the reader is active, behaves the same as {@link WritableStream.abort | stream.abort(reason)}. + */ + abort(reason = void 0) { + if (!IsWritableStreamDefaultWriter(this)) { + return promiseRejectedWith(defaultWriterBrandCheckException("abort")); + } + if (this._ownerWritableStream === void 0) { + return promiseRejectedWith(defaultWriterLockException("abort")); + } + return WritableStreamDefaultWriterAbort(this, reason); + } + /** + * If the reader is active, behaves the same as {@link WritableStream.close | stream.close()}. + */ + close() { + if (!IsWritableStreamDefaultWriter(this)) { + return promiseRejectedWith(defaultWriterBrandCheckException("close")); + } + const stream = this._ownerWritableStream; + if (stream === void 0) { + return promiseRejectedWith(defaultWriterLockException("close")); + } + if (WritableStreamCloseQueuedOrInFlight(stream)) { + return promiseRejectedWith(new TypeError("Cannot close an already-closing stream")); + } + return WritableStreamDefaultWriterClose(this); + } + /** + * Releases the writer’s lock on the corresponding stream. After the lock is released, the writer is no longer active. + * If the associated stream is errored when the lock is released, the writer will appear errored in the same way from + * now on; otherwise, the writer will appear closed. + * + * Note that the lock can still be released even if some ongoing writes have not yet finished (i.e. even if the + * promises returned from previous calls to {@link WritableStreamDefaultWriter.write | write()} have not yet settled). + * It’s not necessary to hold the lock on the writer for the duration of the write; the lock instead simply prevents + * other producers from writing in an interleaved manner. + */ + releaseLock() { + if (!IsWritableStreamDefaultWriter(this)) { + throw defaultWriterBrandCheckException("releaseLock"); + } + const stream = this._ownerWritableStream; + if (stream === void 0) { + return; + } + WritableStreamDefaultWriterRelease(this); + } + write(chunk = void 0) { + if (!IsWritableStreamDefaultWriter(this)) { + return promiseRejectedWith(defaultWriterBrandCheckException("write")); + } + if (this._ownerWritableStream === void 0) { + return promiseRejectedWith(defaultWriterLockException("write to")); + } + return WritableStreamDefaultWriterWrite(this, chunk); + } + } + Object.defineProperties(WritableStreamDefaultWriter.prototype, { + abort: { enumerable: true }, + close: { enumerable: true }, + releaseLock: { enumerable: true }, + write: { enumerable: true }, + closed: { enumerable: true }, + desiredSize: { enumerable: true }, + ready: { enumerable: true } + }); + setFunctionName(WritableStreamDefaultWriter.prototype.abort, "abort"); + setFunctionName(WritableStreamDefaultWriter.prototype.close, "close"); + setFunctionName(WritableStreamDefaultWriter.prototype.releaseLock, "releaseLock"); + setFunctionName(WritableStreamDefaultWriter.prototype.write, "write"); + if (typeof Symbol.toStringTag === "symbol") { + Object.defineProperty(WritableStreamDefaultWriter.prototype, Symbol.toStringTag, { + value: "WritableStreamDefaultWriter", + configurable: true + }); + } + function IsWritableStreamDefaultWriter(x2) { + if (!typeIsObject(x2)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x2, "_ownerWritableStream")) { + return false; + } + return x2 instanceof WritableStreamDefaultWriter; + } + function WritableStreamDefaultWriterAbort(writer, reason) { + const stream = writer._ownerWritableStream; + return WritableStreamAbort(stream, reason); + } + function WritableStreamDefaultWriterClose(writer) { + const stream = writer._ownerWritableStream; + return WritableStreamClose(stream); + } + function WritableStreamDefaultWriterCloseWithErrorPropagation(writer) { + const stream = writer._ownerWritableStream; + const state = stream._state; + if (WritableStreamCloseQueuedOrInFlight(stream) || state === "closed") { + return promiseResolvedWith(void 0); + } + if (state === "errored") { + return promiseRejectedWith(stream._storedError); + } + return WritableStreamDefaultWriterClose(writer); + } + function WritableStreamDefaultWriterEnsureClosedPromiseRejected(writer, error) { + if (writer._closedPromiseState === "pending") { + defaultWriterClosedPromiseReject(writer, error); + } else { + defaultWriterClosedPromiseResetToRejected(writer, error); + } + } + function WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, error) { + if (writer._readyPromiseState === "pending") { + defaultWriterReadyPromiseReject(writer, error); + } else { + defaultWriterReadyPromiseResetToRejected(writer, error); + } + } + function WritableStreamDefaultWriterGetDesiredSize(writer) { + const stream = writer._ownerWritableStream; + const state = stream._state; + if (state === "errored" || state === "erroring") { + return null; + } + if (state === "closed") { + return 0; + } + return WritableStreamDefaultControllerGetDesiredSize(stream._writableStreamController); + } + function WritableStreamDefaultWriterRelease(writer) { + const stream = writer._ownerWritableStream; + const releasedError = new TypeError(`Writer was released and can no longer be used to monitor the stream's closedness`); + WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, releasedError); + WritableStreamDefaultWriterEnsureClosedPromiseRejected(writer, releasedError); + stream._writer = void 0; + writer._ownerWritableStream = void 0; + } + function WritableStreamDefaultWriterWrite(writer, chunk) { + const stream = writer._ownerWritableStream; + const controller = stream._writableStreamController; + const chunkSize = WritableStreamDefaultControllerGetChunkSize(controller, chunk); + if (stream !== writer._ownerWritableStream) { + return promiseRejectedWith(defaultWriterLockException("write to")); + } + const state = stream._state; + if (state === "errored") { + return promiseRejectedWith(stream._storedError); + } + if (WritableStreamCloseQueuedOrInFlight(stream) || state === "closed") { + return promiseRejectedWith(new TypeError("The stream is closing or closed and cannot be written to")); + } + if (state === "erroring") { + return promiseRejectedWith(stream._storedError); + } + const promise = WritableStreamAddWriteRequest(stream); + WritableStreamDefaultControllerWrite(controller, chunk, chunkSize); + return promise; + } + const closeSentinel = {}; + class WritableStreamDefaultController { + constructor() { + throw new TypeError("Illegal constructor"); + } + /** + * The reason which was passed to `WritableStream.abort(reason)` when the stream was aborted. + * + * @deprecated + * This property has been removed from the specification, see https://github.com/whatwg/streams/pull/1177. + * Use {@link WritableStreamDefaultController.signal}'s `reason` instead. + */ + get abortReason() { + if (!IsWritableStreamDefaultController(this)) { + throw defaultControllerBrandCheckException$2("abortReason"); + } + return this._abortReason; + } + /** + * An `AbortSignal` that can be used to abort the pending write or close operation when the stream is aborted. + */ + get signal() { + if (!IsWritableStreamDefaultController(this)) { + throw defaultControllerBrandCheckException$2("signal"); + } + if (this._abortController === void 0) { + throw new TypeError("WritableStreamDefaultController.prototype.signal is not supported"); + } + return this._abortController.signal; + } + /** + * Closes the controlled writable stream, making all future interactions with it fail with the given error `e`. + * + * This method is rarely used, since usually it suffices to return a rejected promise from one of the underlying + * sink's methods. However, it can be useful for suddenly shutting down a stream in response to an event outside the + * normal lifecycle of interactions with the underlying sink. + */ + error(e2 = void 0) { + if (!IsWritableStreamDefaultController(this)) { + throw defaultControllerBrandCheckException$2("error"); + } + const state = this._controlledWritableStream._state; + if (state !== "writable") { + return; + } + WritableStreamDefaultControllerError(this, e2); + } + /** @internal */ + [AbortSteps](reason) { + const result = this._abortAlgorithm(reason); + WritableStreamDefaultControllerClearAlgorithms(this); + return result; + } + /** @internal */ + [ErrorSteps]() { + ResetQueue(this); + } + } + Object.defineProperties(WritableStreamDefaultController.prototype, { + abortReason: { enumerable: true }, + signal: { enumerable: true }, + error: { enumerable: true } + }); + if (typeof Symbol.toStringTag === "symbol") { + Object.defineProperty(WritableStreamDefaultController.prototype, Symbol.toStringTag, { + value: "WritableStreamDefaultController", + configurable: true + }); + } + function IsWritableStreamDefaultController(x2) { + if (!typeIsObject(x2)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x2, "_controlledWritableStream")) { + return false; + } + return x2 instanceof WritableStreamDefaultController; + } + function SetUpWritableStreamDefaultController(stream, controller, startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, highWaterMark, sizeAlgorithm) { + controller._controlledWritableStream = stream; + stream._writableStreamController = controller; + controller._queue = void 0; + controller._queueTotalSize = void 0; + ResetQueue(controller); + controller._abortReason = void 0; + controller._abortController = createAbortController(); + controller._started = false; + controller._strategySizeAlgorithm = sizeAlgorithm; + controller._strategyHWM = highWaterMark; + controller._writeAlgorithm = writeAlgorithm; + controller._closeAlgorithm = closeAlgorithm; + controller._abortAlgorithm = abortAlgorithm; + const backpressure = WritableStreamDefaultControllerGetBackpressure(controller); + WritableStreamUpdateBackpressure(stream, backpressure); + const startResult = startAlgorithm(); + const startPromise = promiseResolvedWith(startResult); + uponPromise(startPromise, () => { + controller._started = true; + WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller); + return null; + }, (r2) => { + controller._started = true; + WritableStreamDealWithRejection(stream, r2); + return null; + }); + } + function SetUpWritableStreamDefaultControllerFromUnderlyingSink(stream, underlyingSink, highWaterMark, sizeAlgorithm) { + const controller = Object.create(WritableStreamDefaultController.prototype); + let startAlgorithm; + let writeAlgorithm; + let closeAlgorithm; + let abortAlgorithm; + if (underlyingSink.start !== void 0) { + startAlgorithm = () => underlyingSink.start(controller); + } else { + startAlgorithm = () => void 0; + } + if (underlyingSink.write !== void 0) { + writeAlgorithm = (chunk) => underlyingSink.write(chunk, controller); + } else { + writeAlgorithm = () => promiseResolvedWith(void 0); + } + if (underlyingSink.close !== void 0) { + closeAlgorithm = () => underlyingSink.close(); + } else { + closeAlgorithm = () => promiseResolvedWith(void 0); + } + if (underlyingSink.abort !== void 0) { + abortAlgorithm = (reason) => underlyingSink.abort(reason); + } else { + abortAlgorithm = () => promiseResolvedWith(void 0); + } + SetUpWritableStreamDefaultController(stream, controller, startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, highWaterMark, sizeAlgorithm); + } + function WritableStreamDefaultControllerClearAlgorithms(controller) { + controller._writeAlgorithm = void 0; + controller._closeAlgorithm = void 0; + controller._abortAlgorithm = void 0; + controller._strategySizeAlgorithm = void 0; + } + function WritableStreamDefaultControllerClose(controller) { + EnqueueValueWithSize(controller, closeSentinel, 0); + WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller); + } + function WritableStreamDefaultControllerGetChunkSize(controller, chunk) { + try { + return controller._strategySizeAlgorithm(chunk); + } catch (chunkSizeE) { + WritableStreamDefaultControllerErrorIfNeeded(controller, chunkSizeE); + return 1; + } + } + function WritableStreamDefaultControllerGetDesiredSize(controller) { + return controller._strategyHWM - controller._queueTotalSize; + } + function WritableStreamDefaultControllerWrite(controller, chunk, chunkSize) { + try { + EnqueueValueWithSize(controller, chunk, chunkSize); + } catch (enqueueE) { + WritableStreamDefaultControllerErrorIfNeeded(controller, enqueueE); + return; + } + const stream = controller._controlledWritableStream; + if (!WritableStreamCloseQueuedOrInFlight(stream) && stream._state === "writable") { + const backpressure = WritableStreamDefaultControllerGetBackpressure(controller); + WritableStreamUpdateBackpressure(stream, backpressure); + } + WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller); + } + function WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller) { + const stream = controller._controlledWritableStream; + if (!controller._started) { + return; + } + if (stream._inFlightWriteRequest !== void 0) { + return; + } + const state = stream._state; + if (state === "erroring") { + WritableStreamFinishErroring(stream); + return; + } + if (controller._queue.length === 0) { + return; + } + const value = PeekQueueValue(controller); + if (value === closeSentinel) { + WritableStreamDefaultControllerProcessClose(controller); + } else { + WritableStreamDefaultControllerProcessWrite(controller, value); + } + } + function WritableStreamDefaultControllerErrorIfNeeded(controller, error) { + if (controller._controlledWritableStream._state === "writable") { + WritableStreamDefaultControllerError(controller, error); + } + } + function WritableStreamDefaultControllerProcessClose(controller) { + const stream = controller._controlledWritableStream; + WritableStreamMarkCloseRequestInFlight(stream); + DequeueValue(controller); + const sinkClosePromise = controller._closeAlgorithm(); + WritableStreamDefaultControllerClearAlgorithms(controller); + uponPromise(sinkClosePromise, () => { + WritableStreamFinishInFlightClose(stream); + return null; + }, (reason) => { + WritableStreamFinishInFlightCloseWithError(stream, reason); + return null; + }); + } + function WritableStreamDefaultControllerProcessWrite(controller, chunk) { + const stream = controller._controlledWritableStream; + WritableStreamMarkFirstWriteRequestInFlight(stream); + const sinkWritePromise = controller._writeAlgorithm(chunk); + uponPromise(sinkWritePromise, () => { + WritableStreamFinishInFlightWrite(stream); + const state = stream._state; + DequeueValue(controller); + if (!WritableStreamCloseQueuedOrInFlight(stream) && state === "writable") { + const backpressure = WritableStreamDefaultControllerGetBackpressure(controller); + WritableStreamUpdateBackpressure(stream, backpressure); + } + WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller); + return null; + }, (reason) => { + if (stream._state === "writable") { + WritableStreamDefaultControllerClearAlgorithms(controller); + } + WritableStreamFinishInFlightWriteWithError(stream, reason); + return null; + }); + } + function WritableStreamDefaultControllerGetBackpressure(controller) { + const desiredSize = WritableStreamDefaultControllerGetDesiredSize(controller); + return desiredSize <= 0; + } + function WritableStreamDefaultControllerError(controller, error) { + const stream = controller._controlledWritableStream; + WritableStreamDefaultControllerClearAlgorithms(controller); + WritableStreamStartErroring(stream, error); + } + function streamBrandCheckException$2(name) { + return new TypeError(`WritableStream.prototype.${name} can only be used on a WritableStream`); + } + function defaultControllerBrandCheckException$2(name) { + return new TypeError(`WritableStreamDefaultController.prototype.${name} can only be used on a WritableStreamDefaultController`); + } + function defaultWriterBrandCheckException(name) { + return new TypeError(`WritableStreamDefaultWriter.prototype.${name} can only be used on a WritableStreamDefaultWriter`); + } + function defaultWriterLockException(name) { + return new TypeError("Cannot " + name + " a stream using a released writer"); + } + function defaultWriterClosedPromiseInitialize(writer) { + writer._closedPromise = newPromise((resolve, reject) => { + writer._closedPromise_resolve = resolve; + writer._closedPromise_reject = reject; + writer._closedPromiseState = "pending"; + }); + } + function defaultWriterClosedPromiseInitializeAsRejected(writer, reason) { + defaultWriterClosedPromiseInitialize(writer); + defaultWriterClosedPromiseReject(writer, reason); + } + function defaultWriterClosedPromiseInitializeAsResolved(writer) { + defaultWriterClosedPromiseInitialize(writer); + defaultWriterClosedPromiseResolve(writer); + } + function defaultWriterClosedPromiseReject(writer, reason) { + if (writer._closedPromise_reject === void 0) { + return; + } + setPromiseIsHandledToTrue(writer._closedPromise); + writer._closedPromise_reject(reason); + writer._closedPromise_resolve = void 0; + writer._closedPromise_reject = void 0; + writer._closedPromiseState = "rejected"; + } + function defaultWriterClosedPromiseResetToRejected(writer, reason) { + defaultWriterClosedPromiseInitializeAsRejected(writer, reason); + } + function defaultWriterClosedPromiseResolve(writer) { + if (writer._closedPromise_resolve === void 0) { + return; + } + writer._closedPromise_resolve(void 0); + writer._closedPromise_resolve = void 0; + writer._closedPromise_reject = void 0; + writer._closedPromiseState = "resolved"; + } + function defaultWriterReadyPromiseInitialize(writer) { + writer._readyPromise = newPromise((resolve, reject) => { + writer._readyPromise_resolve = resolve; + writer._readyPromise_reject = reject; + }); + writer._readyPromiseState = "pending"; + } + function defaultWriterReadyPromiseInitializeAsRejected(writer, reason) { + defaultWriterReadyPromiseInitialize(writer); + defaultWriterReadyPromiseReject(writer, reason); + } + function defaultWriterReadyPromiseInitializeAsResolved(writer) { + defaultWriterReadyPromiseInitialize(writer); + defaultWriterReadyPromiseResolve(writer); + } + function defaultWriterReadyPromiseReject(writer, reason) { + if (writer._readyPromise_reject === void 0) { + return; + } + setPromiseIsHandledToTrue(writer._readyPromise); + writer._readyPromise_reject(reason); + writer._readyPromise_resolve = void 0; + writer._readyPromise_reject = void 0; + writer._readyPromiseState = "rejected"; + } + function defaultWriterReadyPromiseReset(writer) { + defaultWriterReadyPromiseInitialize(writer); + } + function defaultWriterReadyPromiseResetToRejected(writer, reason) { + defaultWriterReadyPromiseInitializeAsRejected(writer, reason); + } + function defaultWriterReadyPromiseResolve(writer) { + if (writer._readyPromise_resolve === void 0) { + return; + } + writer._readyPromise_resolve(void 0); + writer._readyPromise_resolve = void 0; + writer._readyPromise_reject = void 0; + writer._readyPromiseState = "fulfilled"; + } + function getGlobals() { + if (typeof globalThis !== "undefined") { + return globalThis; + } else if (typeof self !== "undefined") { + return self; + } else if (typeof global !== "undefined") { + return global; + } + return void 0; + } + const globals = getGlobals(); + function isDOMExceptionConstructor(ctor) { + if (!(typeof ctor === "function" || typeof ctor === "object")) { + return false; + } + if (ctor.name !== "DOMException") { + return false; + } + try { + new ctor(); + return true; + } catch (_a2) { + return false; + } + } + function getFromGlobal() { + const ctor = globals === null || globals === void 0 ? void 0 : globals.DOMException; + return isDOMExceptionConstructor(ctor) ? ctor : void 0; + } + function createPolyfill() { + const ctor = function DOMException3(message, name) { + this.message = message || ""; + this.name = name || "Error"; + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + }; + setFunctionName(ctor, "DOMException"); + ctor.prototype = Object.create(Error.prototype); + Object.defineProperty(ctor.prototype, "constructor", { value: ctor, writable: true, configurable: true }); + return ctor; + } + const DOMException2 = getFromGlobal() || createPolyfill(); + function ReadableStreamPipeTo(source2, dest, preventClose, preventAbort, preventCancel, signal) { + const reader = AcquireReadableStreamDefaultReader(source2); + const writer = AcquireWritableStreamDefaultWriter(dest); + source2._disturbed = true; + let shuttingDown = false; + let currentWrite = promiseResolvedWith(void 0); + return newPromise((resolve, reject) => { + let abortAlgorithm; + if (signal !== void 0) { + abortAlgorithm = () => { + const error = signal.reason !== void 0 ? signal.reason : new DOMException2("Aborted", "AbortError"); + const actions = []; + if (!preventAbort) { + actions.push(() => { + if (dest._state === "writable") { + return WritableStreamAbort(dest, error); + } + return promiseResolvedWith(void 0); + }); + } + if (!preventCancel) { + actions.push(() => { + if (source2._state === "readable") { + return ReadableStreamCancel(source2, error); + } + return promiseResolvedWith(void 0); + }); + } + shutdownWithAction(() => Promise.all(actions.map((action) => action())), true, error); + }; + if (signal.aborted) { + abortAlgorithm(); + return; + } + signal.addEventListener("abort", abortAlgorithm); + } + function pipeLoop() { + return newPromise((resolveLoop, rejectLoop) => { + function next(done) { + if (done) { + resolveLoop(); + } else { + PerformPromiseThen(pipeStep(), next, rejectLoop); + } + } + next(false); + }); + } + function pipeStep() { + if (shuttingDown) { + return promiseResolvedWith(true); + } + return PerformPromiseThen(writer._readyPromise, () => { + return newPromise((resolveRead, rejectRead) => { + ReadableStreamDefaultReaderRead(reader, { + _chunkSteps: (chunk) => { + currentWrite = PerformPromiseThen(WritableStreamDefaultWriterWrite(writer, chunk), void 0, noop2); + resolveRead(false); + }, + _closeSteps: () => resolveRead(true), + _errorSteps: rejectRead + }); + }); + }); + } + isOrBecomesErrored(source2, reader._closedPromise, (storedError) => { + if (!preventAbort) { + shutdownWithAction(() => WritableStreamAbort(dest, storedError), true, storedError); + } else { + shutdown(true, storedError); + } + return null; + }); + isOrBecomesErrored(dest, writer._closedPromise, (storedError) => { + if (!preventCancel) { + shutdownWithAction(() => ReadableStreamCancel(source2, storedError), true, storedError); + } else { + shutdown(true, storedError); + } + return null; + }); + isOrBecomesClosed(source2, reader._closedPromise, () => { + if (!preventClose) { + shutdownWithAction(() => WritableStreamDefaultWriterCloseWithErrorPropagation(writer)); + } else { + shutdown(); + } + return null; + }); + if (WritableStreamCloseQueuedOrInFlight(dest) || dest._state === "closed") { + const destClosed = new TypeError("the destination writable stream closed before all data could be piped to it"); + if (!preventCancel) { + shutdownWithAction(() => ReadableStreamCancel(source2, destClosed), true, destClosed); + } else { + shutdown(true, destClosed); + } + } + setPromiseIsHandledToTrue(pipeLoop()); + function waitForWritesToFinish() { + const oldCurrentWrite = currentWrite; + return PerformPromiseThen(currentWrite, () => oldCurrentWrite !== currentWrite ? waitForWritesToFinish() : void 0); + } + function isOrBecomesErrored(stream, promise, action) { + if (stream._state === "errored") { + action(stream._storedError); + } else { + uponRejection(promise, action); + } + } + function isOrBecomesClosed(stream, promise, action) { + if (stream._state === "closed") { + action(); + } else { + uponFulfillment(promise, action); + } + } + function shutdownWithAction(action, originalIsError, originalError) { + if (shuttingDown) { + return; + } + shuttingDown = true; + if (dest._state === "writable" && !WritableStreamCloseQueuedOrInFlight(dest)) { + uponFulfillment(waitForWritesToFinish(), doTheRest); + } else { + doTheRest(); + } + function doTheRest() { + uponPromise(action(), () => finalize(originalIsError, originalError), (newError) => finalize(true, newError)); + return null; + } + } + function shutdown(isError, error) { + if (shuttingDown) { + return; + } + shuttingDown = true; + if (dest._state === "writable" && !WritableStreamCloseQueuedOrInFlight(dest)) { + uponFulfillment(waitForWritesToFinish(), () => finalize(isError, error)); + } else { + finalize(isError, error); + } + } + function finalize(isError, error) { + WritableStreamDefaultWriterRelease(writer); + ReadableStreamReaderGenericRelease(reader); + if (signal !== void 0) { + signal.removeEventListener("abort", abortAlgorithm); + } + if (isError) { + reject(error); + } else { + resolve(void 0); + } + return null; + } + }); + } + class ReadableStreamDefaultController { + constructor() { + throw new TypeError("Illegal constructor"); + } + /** + * Returns the desired size to fill the controlled stream's internal queue. It can be negative, if the queue is + * over-full. An underlying source ought to use this information to determine when and how to apply backpressure. + */ + get desiredSize() { + if (!IsReadableStreamDefaultController(this)) { + throw defaultControllerBrandCheckException$1("desiredSize"); + } + return ReadableStreamDefaultControllerGetDesiredSize(this); + } + /** + * Closes the controlled readable stream. Consumers will still be able to read any previously-enqueued chunks from + * the stream, but once those are read, the stream will become closed. + */ + close() { + if (!IsReadableStreamDefaultController(this)) { + throw defaultControllerBrandCheckException$1("close"); + } + if (!ReadableStreamDefaultControllerCanCloseOrEnqueue(this)) { + throw new TypeError("The stream is not in a state that permits close"); + } + ReadableStreamDefaultControllerClose(this); + } + enqueue(chunk = void 0) { + if (!IsReadableStreamDefaultController(this)) { + throw defaultControllerBrandCheckException$1("enqueue"); + } + if (!ReadableStreamDefaultControllerCanCloseOrEnqueue(this)) { + throw new TypeError("The stream is not in a state that permits enqueue"); + } + return ReadableStreamDefaultControllerEnqueue(this, chunk); + } + /** + * Errors the controlled readable stream, making all future interactions with it fail with the given error `e`. + */ + error(e2 = void 0) { + if (!IsReadableStreamDefaultController(this)) { + throw defaultControllerBrandCheckException$1("error"); + } + ReadableStreamDefaultControllerError(this, e2); + } + /** @internal */ + [CancelSteps](reason) { + ResetQueue(this); + const result = this._cancelAlgorithm(reason); + ReadableStreamDefaultControllerClearAlgorithms(this); + return result; + } + /** @internal */ + [PullSteps](readRequest) { + const stream = this._controlledReadableStream; + if (this._queue.length > 0) { + const chunk = DequeueValue(this); + if (this._closeRequested && this._queue.length === 0) { + ReadableStreamDefaultControllerClearAlgorithms(this); + ReadableStreamClose(stream); + } else { + ReadableStreamDefaultControllerCallPullIfNeeded(this); + } + readRequest._chunkSteps(chunk); + } else { + ReadableStreamAddReadRequest(stream, readRequest); + ReadableStreamDefaultControllerCallPullIfNeeded(this); + } + } + /** @internal */ + [ReleaseSteps]() { + } + } + Object.defineProperties(ReadableStreamDefaultController.prototype, { + close: { enumerable: true }, + enqueue: { enumerable: true }, + error: { enumerable: true }, + desiredSize: { enumerable: true } + }); + setFunctionName(ReadableStreamDefaultController.prototype.close, "close"); + setFunctionName(ReadableStreamDefaultController.prototype.enqueue, "enqueue"); + setFunctionName(ReadableStreamDefaultController.prototype.error, "error"); + if (typeof Symbol.toStringTag === "symbol") { + Object.defineProperty(ReadableStreamDefaultController.prototype, Symbol.toStringTag, { + value: "ReadableStreamDefaultController", + configurable: true + }); + } + function IsReadableStreamDefaultController(x2) { + if (!typeIsObject(x2)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x2, "_controlledReadableStream")) { + return false; + } + return x2 instanceof ReadableStreamDefaultController; + } + function ReadableStreamDefaultControllerCallPullIfNeeded(controller) { + const shouldPull = ReadableStreamDefaultControllerShouldCallPull(controller); + if (!shouldPull) { + return; + } + if (controller._pulling) { + controller._pullAgain = true; + return; + } + controller._pulling = true; + const pullPromise = controller._pullAlgorithm(); + uponPromise(pullPromise, () => { + controller._pulling = false; + if (controller._pullAgain) { + controller._pullAgain = false; + ReadableStreamDefaultControllerCallPullIfNeeded(controller); + } + return null; + }, (e2) => { + ReadableStreamDefaultControllerError(controller, e2); + return null; + }); + } + function ReadableStreamDefaultControllerShouldCallPull(controller) { + const stream = controller._controlledReadableStream; + if (!ReadableStreamDefaultControllerCanCloseOrEnqueue(controller)) { + return false; + } + if (!controller._started) { + return false; + } + if (IsReadableStreamLocked(stream) && ReadableStreamGetNumReadRequests(stream) > 0) { + return true; + } + const desiredSize = ReadableStreamDefaultControllerGetDesiredSize(controller); + if (desiredSize > 0) { + return true; + } + return false; + } + function ReadableStreamDefaultControllerClearAlgorithms(controller) { + controller._pullAlgorithm = void 0; + controller._cancelAlgorithm = void 0; + controller._strategySizeAlgorithm = void 0; + } + function ReadableStreamDefaultControllerClose(controller) { + if (!ReadableStreamDefaultControllerCanCloseOrEnqueue(controller)) { + return; + } + const stream = controller._controlledReadableStream; + controller._closeRequested = true; + if (controller._queue.length === 0) { + ReadableStreamDefaultControllerClearAlgorithms(controller); + ReadableStreamClose(stream); + } + } + function ReadableStreamDefaultControllerEnqueue(controller, chunk) { + if (!ReadableStreamDefaultControllerCanCloseOrEnqueue(controller)) { + return; + } + const stream = controller._controlledReadableStream; + if (IsReadableStreamLocked(stream) && ReadableStreamGetNumReadRequests(stream) > 0) { + ReadableStreamFulfillReadRequest(stream, chunk, false); + } else { + let chunkSize; + try { + chunkSize = controller._strategySizeAlgorithm(chunk); + } catch (chunkSizeE) { + ReadableStreamDefaultControllerError(controller, chunkSizeE); + throw chunkSizeE; + } + try { + EnqueueValueWithSize(controller, chunk, chunkSize); + } catch (enqueueE) { + ReadableStreamDefaultControllerError(controller, enqueueE); + throw enqueueE; + } + } + ReadableStreamDefaultControllerCallPullIfNeeded(controller); + } + function ReadableStreamDefaultControllerError(controller, e2) { + const stream = controller._controlledReadableStream; + if (stream._state !== "readable") { + return; + } + ResetQueue(controller); + ReadableStreamDefaultControllerClearAlgorithms(controller); + ReadableStreamError(stream, e2); + } + function ReadableStreamDefaultControllerGetDesiredSize(controller) { + const state = controller._controlledReadableStream._state; + if (state === "errored") { + return null; + } + if (state === "closed") { + return 0; + } + return controller._strategyHWM - controller._queueTotalSize; + } + function ReadableStreamDefaultControllerHasBackpressure(controller) { + if (ReadableStreamDefaultControllerShouldCallPull(controller)) { + return false; + } + return true; + } + function ReadableStreamDefaultControllerCanCloseOrEnqueue(controller) { + const state = controller._controlledReadableStream._state; + if (!controller._closeRequested && state === "readable") { + return true; + } + return false; + } + function SetUpReadableStreamDefaultController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, sizeAlgorithm) { + controller._controlledReadableStream = stream; + controller._queue = void 0; + controller._queueTotalSize = void 0; + ResetQueue(controller); + controller._started = false; + controller._closeRequested = false; + controller._pullAgain = false; + controller._pulling = false; + controller._strategySizeAlgorithm = sizeAlgorithm; + controller._strategyHWM = highWaterMark; + controller._pullAlgorithm = pullAlgorithm; + controller._cancelAlgorithm = cancelAlgorithm; + stream._readableStreamController = controller; + const startResult = startAlgorithm(); + uponPromise(promiseResolvedWith(startResult), () => { + controller._started = true; + ReadableStreamDefaultControllerCallPullIfNeeded(controller); + return null; + }, (r2) => { + ReadableStreamDefaultControllerError(controller, r2); + return null; + }); + } + function SetUpReadableStreamDefaultControllerFromUnderlyingSource(stream, underlyingSource, highWaterMark, sizeAlgorithm) { + const controller = Object.create(ReadableStreamDefaultController.prototype); + let startAlgorithm; + let pullAlgorithm; + let cancelAlgorithm; + if (underlyingSource.start !== void 0) { + startAlgorithm = () => underlyingSource.start(controller); + } else { + startAlgorithm = () => void 0; + } + if (underlyingSource.pull !== void 0) { + pullAlgorithm = () => underlyingSource.pull(controller); + } else { + pullAlgorithm = () => promiseResolvedWith(void 0); + } + if (underlyingSource.cancel !== void 0) { + cancelAlgorithm = (reason) => underlyingSource.cancel(reason); + } else { + cancelAlgorithm = () => promiseResolvedWith(void 0); + } + SetUpReadableStreamDefaultController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, sizeAlgorithm); + } + function defaultControllerBrandCheckException$1(name) { + return new TypeError(`ReadableStreamDefaultController.prototype.${name} can only be used on a ReadableStreamDefaultController`); + } + function ReadableStreamTee(stream, cloneForBranch2) { + if (IsReadableByteStreamController(stream._readableStreamController)) { + return ReadableByteStreamTee(stream); + } + return ReadableStreamDefaultTee(stream); + } + function ReadableStreamDefaultTee(stream, cloneForBranch2) { + const reader = AcquireReadableStreamDefaultReader(stream); + let reading = false; + let readAgain = false; + let canceled1 = false; + let canceled2 = false; + let reason1; + let reason2; + let branch1; + let branch2; + let resolveCancelPromise; + const cancelPromise = newPromise((resolve) => { + resolveCancelPromise = resolve; + }); + function pullAlgorithm() { + if (reading) { + readAgain = true; + return promiseResolvedWith(void 0); + } + reading = true; + const readRequest = { + _chunkSteps: (chunk) => { + _queueMicrotask(() => { + readAgain = false; + const chunk1 = chunk; + const chunk2 = chunk; + if (!canceled1) { + ReadableStreamDefaultControllerEnqueue(branch1._readableStreamController, chunk1); + } + if (!canceled2) { + ReadableStreamDefaultControllerEnqueue(branch2._readableStreamController, chunk2); + } + reading = false; + if (readAgain) { + pullAlgorithm(); + } + }); + }, + _closeSteps: () => { + reading = false; + if (!canceled1) { + ReadableStreamDefaultControllerClose(branch1._readableStreamController); + } + if (!canceled2) { + ReadableStreamDefaultControllerClose(branch2._readableStreamController); + } + if (!canceled1 || !canceled2) { + resolveCancelPromise(void 0); + } + }, + _errorSteps: () => { + reading = false; + } + }; + ReadableStreamDefaultReaderRead(reader, readRequest); + return promiseResolvedWith(void 0); + } + function cancel1Algorithm(reason) { + canceled1 = true; + reason1 = reason; + if (canceled2) { + const compositeReason = CreateArrayFromList([reason1, reason2]); + const cancelResult = ReadableStreamCancel(stream, compositeReason); + resolveCancelPromise(cancelResult); + } + return cancelPromise; + } + function cancel2Algorithm(reason) { + canceled2 = true; + reason2 = reason; + if (canceled1) { + const compositeReason = CreateArrayFromList([reason1, reason2]); + const cancelResult = ReadableStreamCancel(stream, compositeReason); + resolveCancelPromise(cancelResult); + } + return cancelPromise; + } + function startAlgorithm() { + } + branch1 = CreateReadableStream(startAlgorithm, pullAlgorithm, cancel1Algorithm); + branch2 = CreateReadableStream(startAlgorithm, pullAlgorithm, cancel2Algorithm); + uponRejection(reader._closedPromise, (r2) => { + ReadableStreamDefaultControllerError(branch1._readableStreamController, r2); + ReadableStreamDefaultControllerError(branch2._readableStreamController, r2); + if (!canceled1 || !canceled2) { + resolveCancelPromise(void 0); + } + return null; + }); + return [branch1, branch2]; + } + function ReadableByteStreamTee(stream) { + let reader = AcquireReadableStreamDefaultReader(stream); + let reading = false; + let readAgainForBranch1 = false; + let readAgainForBranch2 = false; + let canceled1 = false; + let canceled2 = false; + let reason1; + let reason2; + let branch1; + let branch2; + let resolveCancelPromise; + const cancelPromise = newPromise((resolve) => { + resolveCancelPromise = resolve; + }); + function forwardReaderError(thisReader) { + uponRejection(thisReader._closedPromise, (r2) => { + if (thisReader !== reader) { + return null; + } + ReadableByteStreamControllerError(branch1._readableStreamController, r2); + ReadableByteStreamControllerError(branch2._readableStreamController, r2); + if (!canceled1 || !canceled2) { + resolveCancelPromise(void 0); + } + return null; + }); + } + function pullWithDefaultReader() { + if (IsReadableStreamBYOBReader(reader)) { + ReadableStreamReaderGenericRelease(reader); + reader = AcquireReadableStreamDefaultReader(stream); + forwardReaderError(reader); + } + const readRequest = { + _chunkSteps: (chunk) => { + _queueMicrotask(() => { + readAgainForBranch1 = false; + readAgainForBranch2 = false; + const chunk1 = chunk; + let chunk2 = chunk; + if (!canceled1 && !canceled2) { + try { + chunk2 = CloneAsUint8Array(chunk); + } catch (cloneE) { + ReadableByteStreamControllerError(branch1._readableStreamController, cloneE); + ReadableByteStreamControllerError(branch2._readableStreamController, cloneE); + resolveCancelPromise(ReadableStreamCancel(stream, cloneE)); + return; + } + } + if (!canceled1) { + ReadableByteStreamControllerEnqueue(branch1._readableStreamController, chunk1); + } + if (!canceled2) { + ReadableByteStreamControllerEnqueue(branch2._readableStreamController, chunk2); + } + reading = false; + if (readAgainForBranch1) { + pull1Algorithm(); + } else if (readAgainForBranch2) { + pull2Algorithm(); + } + }); + }, + _closeSteps: () => { + reading = false; + if (!canceled1) { + ReadableByteStreamControllerClose(branch1._readableStreamController); + } + if (!canceled2) { + ReadableByteStreamControllerClose(branch2._readableStreamController); + } + if (branch1._readableStreamController._pendingPullIntos.length > 0) { + ReadableByteStreamControllerRespond(branch1._readableStreamController, 0); + } + if (branch2._readableStreamController._pendingPullIntos.length > 0) { + ReadableByteStreamControllerRespond(branch2._readableStreamController, 0); + } + if (!canceled1 || !canceled2) { + resolveCancelPromise(void 0); + } + }, + _errorSteps: () => { + reading = false; + } + }; + ReadableStreamDefaultReaderRead(reader, readRequest); + } + function pullWithBYOBReader(view, forBranch2) { + if (IsReadableStreamDefaultReader(reader)) { + ReadableStreamReaderGenericRelease(reader); + reader = AcquireReadableStreamBYOBReader(stream); + forwardReaderError(reader); + } + const byobBranch = forBranch2 ? branch2 : branch1; + const otherBranch = forBranch2 ? branch1 : branch2; + const readIntoRequest = { + _chunkSteps: (chunk) => { + _queueMicrotask(() => { + readAgainForBranch1 = false; + readAgainForBranch2 = false; + const byobCanceled = forBranch2 ? canceled2 : canceled1; + const otherCanceled = forBranch2 ? canceled1 : canceled2; + if (!otherCanceled) { + let clonedChunk; + try { + clonedChunk = CloneAsUint8Array(chunk); + } catch (cloneE) { + ReadableByteStreamControllerError(byobBranch._readableStreamController, cloneE); + ReadableByteStreamControllerError(otherBranch._readableStreamController, cloneE); + resolveCancelPromise(ReadableStreamCancel(stream, cloneE)); + return; + } + if (!byobCanceled) { + ReadableByteStreamControllerRespondWithNewView(byobBranch._readableStreamController, chunk); + } + ReadableByteStreamControllerEnqueue(otherBranch._readableStreamController, clonedChunk); + } else if (!byobCanceled) { + ReadableByteStreamControllerRespondWithNewView(byobBranch._readableStreamController, chunk); + } + reading = false; + if (readAgainForBranch1) { + pull1Algorithm(); + } else if (readAgainForBranch2) { + pull2Algorithm(); + } + }); + }, + _closeSteps: (chunk) => { + reading = false; + const byobCanceled = forBranch2 ? canceled2 : canceled1; + const otherCanceled = forBranch2 ? canceled1 : canceled2; + if (!byobCanceled) { + ReadableByteStreamControllerClose(byobBranch._readableStreamController); + } + if (!otherCanceled) { + ReadableByteStreamControllerClose(otherBranch._readableStreamController); + } + if (chunk !== void 0) { + if (!byobCanceled) { + ReadableByteStreamControllerRespondWithNewView(byobBranch._readableStreamController, chunk); + } + if (!otherCanceled && otherBranch._readableStreamController._pendingPullIntos.length > 0) { + ReadableByteStreamControllerRespond(otherBranch._readableStreamController, 0); + } + } + if (!byobCanceled || !otherCanceled) { + resolveCancelPromise(void 0); + } + }, + _errorSteps: () => { + reading = false; + } + }; + ReadableStreamBYOBReaderRead(reader, view, 1, readIntoRequest); + } + function pull1Algorithm() { + if (reading) { + readAgainForBranch1 = true; + return promiseResolvedWith(void 0); + } + reading = true; + const byobRequest = ReadableByteStreamControllerGetBYOBRequest(branch1._readableStreamController); + if (byobRequest === null) { + pullWithDefaultReader(); + } else { + pullWithBYOBReader(byobRequest._view, false); + } + return promiseResolvedWith(void 0); + } + function pull2Algorithm() { + if (reading) { + readAgainForBranch2 = true; + return promiseResolvedWith(void 0); + } + reading = true; + const byobRequest = ReadableByteStreamControllerGetBYOBRequest(branch2._readableStreamController); + if (byobRequest === null) { + pullWithDefaultReader(); + } else { + pullWithBYOBReader(byobRequest._view, true); + } + return promiseResolvedWith(void 0); + } + function cancel1Algorithm(reason) { + canceled1 = true; + reason1 = reason; + if (canceled2) { + const compositeReason = CreateArrayFromList([reason1, reason2]); + const cancelResult = ReadableStreamCancel(stream, compositeReason); + resolveCancelPromise(cancelResult); + } + return cancelPromise; + } + function cancel2Algorithm(reason) { + canceled2 = true; + reason2 = reason; + if (canceled1) { + const compositeReason = CreateArrayFromList([reason1, reason2]); + const cancelResult = ReadableStreamCancel(stream, compositeReason); + resolveCancelPromise(cancelResult); + } + return cancelPromise; + } + function startAlgorithm() { + return; + } + branch1 = CreateReadableByteStream(startAlgorithm, pull1Algorithm, cancel1Algorithm); + branch2 = CreateReadableByteStream(startAlgorithm, pull2Algorithm, cancel2Algorithm); + forwardReaderError(reader); + return [branch1, branch2]; + } + function isReadableStreamLike(stream) { + return typeIsObject(stream) && typeof stream.getReader !== "undefined"; + } + function ReadableStreamFrom(source2) { + if (isReadableStreamLike(source2)) { + return ReadableStreamFromDefaultReader(source2.getReader()); + } + return ReadableStreamFromIterable(source2); + } + function ReadableStreamFromIterable(asyncIterable) { + let stream; + const iteratorRecord = GetIterator(asyncIterable, "async"); + const startAlgorithm = noop2; + function pullAlgorithm() { + let nextResult; + try { + nextResult = IteratorNext(iteratorRecord); + } catch (e2) { + return promiseRejectedWith(e2); + } + const nextPromise = promiseResolvedWith(nextResult); + return transformPromiseWith(nextPromise, (iterResult) => { + if (!typeIsObject(iterResult)) { + throw new TypeError("The promise returned by the iterator.next() method must fulfill with an object"); + } + const done = IteratorComplete(iterResult); + if (done) { + ReadableStreamDefaultControllerClose(stream._readableStreamController); + } else { + const value = IteratorValue(iterResult); + ReadableStreamDefaultControllerEnqueue(stream._readableStreamController, value); + } + }); + } + function cancelAlgorithm(reason) { + const iterator = iteratorRecord.iterator; + let returnMethod; + try { + returnMethod = GetMethod(iterator, "return"); + } catch (e2) { + return promiseRejectedWith(e2); + } + if (returnMethod === void 0) { + return promiseResolvedWith(void 0); + } + let returnResult; + try { + returnResult = reflectCall(returnMethod, iterator, [reason]); + } catch (e2) { + return promiseRejectedWith(e2); + } + const returnPromise = promiseResolvedWith(returnResult); + return transformPromiseWith(returnPromise, (iterResult) => { + if (!typeIsObject(iterResult)) { + throw new TypeError("The promise returned by the iterator.return() method must fulfill with an object"); + } + return void 0; + }); + } + stream = CreateReadableStream(startAlgorithm, pullAlgorithm, cancelAlgorithm, 0); + return stream; + } + function ReadableStreamFromDefaultReader(reader) { + let stream; + const startAlgorithm = noop2; + function pullAlgorithm() { + let readPromise; + try { + readPromise = reader.read(); + } catch (e2) { + return promiseRejectedWith(e2); + } + return transformPromiseWith(readPromise, (readResult) => { + if (!typeIsObject(readResult)) { + throw new TypeError("The promise returned by the reader.read() method must fulfill with an object"); + } + if (readResult.done) { + ReadableStreamDefaultControllerClose(stream._readableStreamController); + } else { + const value = readResult.value; + ReadableStreamDefaultControllerEnqueue(stream._readableStreamController, value); + } + }); + } + function cancelAlgorithm(reason) { + try { + return promiseResolvedWith(reader.cancel(reason)); + } catch (e2) { + return promiseRejectedWith(e2); + } + } + stream = CreateReadableStream(startAlgorithm, pullAlgorithm, cancelAlgorithm, 0); + return stream; + } + function convertUnderlyingDefaultOrByteSource(source2, context) { + assertDictionary(source2, context); + const original = source2; + const autoAllocateChunkSize = original === null || original === void 0 ? void 0 : original.autoAllocateChunkSize; + const cancel = original === null || original === void 0 ? void 0 : original.cancel; + const pull = original === null || original === void 0 ? void 0 : original.pull; + const start = original === null || original === void 0 ? void 0 : original.start; + const type = original === null || original === void 0 ? void 0 : original.type; + return { + autoAllocateChunkSize: autoAllocateChunkSize === void 0 ? void 0 : convertUnsignedLongLongWithEnforceRange(autoAllocateChunkSize, `${context} has member 'autoAllocateChunkSize' that`), + cancel: cancel === void 0 ? void 0 : convertUnderlyingSourceCancelCallback(cancel, original, `${context} has member 'cancel' that`), + pull: pull === void 0 ? void 0 : convertUnderlyingSourcePullCallback(pull, original, `${context} has member 'pull' that`), + start: start === void 0 ? void 0 : convertUnderlyingSourceStartCallback(start, original, `${context} has member 'start' that`), + type: type === void 0 ? void 0 : convertReadableStreamType(type, `${context} has member 'type' that`) + }; + } + function convertUnderlyingSourceCancelCallback(fn, original, context) { + assertFunction(fn, context); + return (reason) => promiseCall(fn, original, [reason]); + } + function convertUnderlyingSourcePullCallback(fn, original, context) { + assertFunction(fn, context); + return (controller) => promiseCall(fn, original, [controller]); + } + function convertUnderlyingSourceStartCallback(fn, original, context) { + assertFunction(fn, context); + return (controller) => reflectCall(fn, original, [controller]); + } + function convertReadableStreamType(type, context) { + type = `${type}`; + if (type !== "bytes") { + throw new TypeError(`${context} '${type}' is not a valid enumeration value for ReadableStreamType`); + } + return type; + } + function convertIteratorOptions(options, context) { + assertDictionary(options, context); + const preventCancel = options === null || options === void 0 ? void 0 : options.preventCancel; + return { preventCancel: Boolean(preventCancel) }; + } + function convertPipeOptions(options, context) { + assertDictionary(options, context); + const preventAbort = options === null || options === void 0 ? void 0 : options.preventAbort; + const preventCancel = options === null || options === void 0 ? void 0 : options.preventCancel; + const preventClose = options === null || options === void 0 ? void 0 : options.preventClose; + const signal = options === null || options === void 0 ? void 0 : options.signal; + if (signal !== void 0) { + assertAbortSignal(signal, `${context} has member 'signal' that`); + } + return { + preventAbort: Boolean(preventAbort), + preventCancel: Boolean(preventCancel), + preventClose: Boolean(preventClose), + signal + }; + } + function assertAbortSignal(signal, context) { + if (!isAbortSignal2(signal)) { + throw new TypeError(`${context} is not an AbortSignal.`); + } + } + function convertReadableWritablePair(pair, context) { + assertDictionary(pair, context); + const readable = pair === null || pair === void 0 ? void 0 : pair.readable; + assertRequiredField(readable, "readable", "ReadableWritablePair"); + assertReadableStream(readable, `${context} has member 'readable' that`); + const writable = pair === null || pair === void 0 ? void 0 : pair.writable; + assertRequiredField(writable, "writable", "ReadableWritablePair"); + assertWritableStream(writable, `${context} has member 'writable' that`); + return { readable, writable }; + } + class ReadableStream2 { + constructor(rawUnderlyingSource = {}, rawStrategy = {}) { + if (rawUnderlyingSource === void 0) { + rawUnderlyingSource = null; + } else { + assertObject(rawUnderlyingSource, "First parameter"); + } + const strategy = convertQueuingStrategy(rawStrategy, "Second parameter"); + const underlyingSource = convertUnderlyingDefaultOrByteSource(rawUnderlyingSource, "First parameter"); + InitializeReadableStream(this); + if (underlyingSource.type === "bytes") { + if (strategy.size !== void 0) { + throw new RangeError("The strategy for a byte stream cannot have a size function"); + } + const highWaterMark = ExtractHighWaterMark(strategy, 0); + SetUpReadableByteStreamControllerFromUnderlyingSource(this, underlyingSource, highWaterMark); + } else { + const sizeAlgorithm = ExtractSizeAlgorithm(strategy); + const highWaterMark = ExtractHighWaterMark(strategy, 1); + SetUpReadableStreamDefaultControllerFromUnderlyingSource(this, underlyingSource, highWaterMark, sizeAlgorithm); + } + } + /** + * Whether or not the readable stream is locked to a {@link ReadableStreamDefaultReader | reader}. + */ + get locked() { + if (!IsReadableStream(this)) { + throw streamBrandCheckException$1("locked"); + } + return IsReadableStreamLocked(this); + } + /** + * Cancels the stream, signaling a loss of interest in the stream by a consumer. + * + * The supplied `reason` argument will be given to the underlying source's {@link UnderlyingSource.cancel | cancel()} + * method, which might or might not use it. + */ + cancel(reason = void 0) { + if (!IsReadableStream(this)) { + return promiseRejectedWith(streamBrandCheckException$1("cancel")); + } + if (IsReadableStreamLocked(this)) { + return promiseRejectedWith(new TypeError("Cannot cancel a stream that already has a reader")); + } + return ReadableStreamCancel(this, reason); + } + getReader(rawOptions = void 0) { + if (!IsReadableStream(this)) { + throw streamBrandCheckException$1("getReader"); + } + const options = convertReaderOptions(rawOptions, "First parameter"); + if (options.mode === void 0) { + return AcquireReadableStreamDefaultReader(this); + } + return AcquireReadableStreamBYOBReader(this); + } + pipeThrough(rawTransform, rawOptions = {}) { + if (!IsReadableStream(this)) { + throw streamBrandCheckException$1("pipeThrough"); + } + assertRequiredArgument(rawTransform, 1, "pipeThrough"); + const transform = convertReadableWritablePair(rawTransform, "First parameter"); + const options = convertPipeOptions(rawOptions, "Second parameter"); + if (IsReadableStreamLocked(this)) { + throw new TypeError("ReadableStream.prototype.pipeThrough cannot be used on a locked ReadableStream"); + } + if (IsWritableStreamLocked(transform.writable)) { + throw new TypeError("ReadableStream.prototype.pipeThrough cannot be used on a locked WritableStream"); + } + const promise = ReadableStreamPipeTo(this, transform.writable, options.preventClose, options.preventAbort, options.preventCancel, options.signal); + setPromiseIsHandledToTrue(promise); + return transform.readable; + } + pipeTo(destination, rawOptions = {}) { + if (!IsReadableStream(this)) { + return promiseRejectedWith(streamBrandCheckException$1("pipeTo")); + } + if (destination === void 0) { + return promiseRejectedWith(`Parameter 1 is required in 'pipeTo'.`); + } + if (!IsWritableStream(destination)) { + return promiseRejectedWith(new TypeError(`ReadableStream.prototype.pipeTo's first argument must be a WritableStream`)); + } + let options; + try { + options = convertPipeOptions(rawOptions, "Second parameter"); + } catch (e2) { + return promiseRejectedWith(e2); + } + if (IsReadableStreamLocked(this)) { + return promiseRejectedWith(new TypeError("ReadableStream.prototype.pipeTo cannot be used on a locked ReadableStream")); + } + if (IsWritableStreamLocked(destination)) { + return promiseRejectedWith(new TypeError("ReadableStream.prototype.pipeTo cannot be used on a locked WritableStream")); + } + return ReadableStreamPipeTo(this, destination, options.preventClose, options.preventAbort, options.preventCancel, options.signal); + } + /** + * Tees this readable stream, returning a two-element array containing the two resulting branches as + * new {@link ReadableStream} instances. + * + * Teeing a stream will lock it, preventing any other consumer from acquiring a reader. + * To cancel the stream, cancel both of the resulting branches; a composite cancellation reason will then be + * propagated to the stream's underlying source. + * + * Note that the chunks seen in each branch will be the same object. If the chunks are not immutable, + * this could allow interference between the two branches. + */ + tee() { + if (!IsReadableStream(this)) { + throw streamBrandCheckException$1("tee"); + } + const branches = ReadableStreamTee(this); + return CreateArrayFromList(branches); + } + values(rawOptions = void 0) { + if (!IsReadableStream(this)) { + throw streamBrandCheckException$1("values"); + } + const options = convertIteratorOptions(rawOptions, "First parameter"); + return AcquireReadableStreamAsyncIterator(this, options.preventCancel); + } + [SymbolAsyncIterator](options) { + return this.values(options); + } + /** + * Creates a new ReadableStream wrapping the provided iterable or async iterable. + * + * This can be used to adapt various kinds of objects into a readable stream, + * such as an array, an async generator, or a Node.js readable stream. + */ + static from(asyncIterable) { + return ReadableStreamFrom(asyncIterable); + } + } + Object.defineProperties(ReadableStream2, { + from: { enumerable: true } + }); + Object.defineProperties(ReadableStream2.prototype, { + cancel: { enumerable: true }, + getReader: { enumerable: true }, + pipeThrough: { enumerable: true }, + pipeTo: { enumerable: true }, + tee: { enumerable: true }, + values: { enumerable: true }, + locked: { enumerable: true } + }); + setFunctionName(ReadableStream2.from, "from"); + setFunctionName(ReadableStream2.prototype.cancel, "cancel"); + setFunctionName(ReadableStream2.prototype.getReader, "getReader"); + setFunctionName(ReadableStream2.prototype.pipeThrough, "pipeThrough"); + setFunctionName(ReadableStream2.prototype.pipeTo, "pipeTo"); + setFunctionName(ReadableStream2.prototype.tee, "tee"); + setFunctionName(ReadableStream2.prototype.values, "values"); + if (typeof Symbol.toStringTag === "symbol") { + Object.defineProperty(ReadableStream2.prototype, Symbol.toStringTag, { + value: "ReadableStream", + configurable: true + }); + } + Object.defineProperty(ReadableStream2.prototype, SymbolAsyncIterator, { + value: ReadableStream2.prototype.values, + writable: true, + configurable: true + }); + function CreateReadableStream(startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark = 1, sizeAlgorithm = () => 1) { + const stream = Object.create(ReadableStream2.prototype); + InitializeReadableStream(stream); + const controller = Object.create(ReadableStreamDefaultController.prototype); + SetUpReadableStreamDefaultController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, sizeAlgorithm); + return stream; + } + function CreateReadableByteStream(startAlgorithm, pullAlgorithm, cancelAlgorithm) { + const stream = Object.create(ReadableStream2.prototype); + InitializeReadableStream(stream); + const controller = Object.create(ReadableByteStreamController.prototype); + SetUpReadableByteStreamController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, 0, void 0); + return stream; + } + function InitializeReadableStream(stream) { + stream._state = "readable"; + stream._reader = void 0; + stream._storedError = void 0; + stream._disturbed = false; + } + function IsReadableStream(x2) { + if (!typeIsObject(x2)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x2, "_readableStreamController")) { + return false; + } + return x2 instanceof ReadableStream2; + } + function IsReadableStreamLocked(stream) { + if (stream._reader === void 0) { + return false; + } + return true; + } + function ReadableStreamCancel(stream, reason) { + stream._disturbed = true; + if (stream._state === "closed") { + return promiseResolvedWith(void 0); + } + if (stream._state === "errored") { + return promiseRejectedWith(stream._storedError); + } + ReadableStreamClose(stream); + const reader = stream._reader; + if (reader !== void 0 && IsReadableStreamBYOBReader(reader)) { + const readIntoRequests = reader._readIntoRequests; + reader._readIntoRequests = new SimpleQueue(); + readIntoRequests.forEach((readIntoRequest) => { + readIntoRequest._closeSteps(void 0); + }); + } + const sourceCancelPromise = stream._readableStreamController[CancelSteps](reason); + return transformPromiseWith(sourceCancelPromise, noop2); + } + function ReadableStreamClose(stream) { + stream._state = "closed"; + const reader = stream._reader; + if (reader === void 0) { + return; + } + defaultReaderClosedPromiseResolve(reader); + if (IsReadableStreamDefaultReader(reader)) { + const readRequests = reader._readRequests; + reader._readRequests = new SimpleQueue(); + readRequests.forEach((readRequest) => { + readRequest._closeSteps(); + }); + } + } + function ReadableStreamError(stream, e2) { + stream._state = "errored"; + stream._storedError = e2; + const reader = stream._reader; + if (reader === void 0) { + return; + } + defaultReaderClosedPromiseReject(reader, e2); + if (IsReadableStreamDefaultReader(reader)) { + ReadableStreamDefaultReaderErrorReadRequests(reader, e2); + } else { + ReadableStreamBYOBReaderErrorReadIntoRequests(reader, e2); + } + } + function streamBrandCheckException$1(name) { + return new TypeError(`ReadableStream.prototype.${name} can only be used on a ReadableStream`); + } + function convertQueuingStrategyInit(init, context) { + assertDictionary(init, context); + const highWaterMark = init === null || init === void 0 ? void 0 : init.highWaterMark; + assertRequiredField(highWaterMark, "highWaterMark", "QueuingStrategyInit"); + return { + highWaterMark: convertUnrestrictedDouble(highWaterMark) + }; + } + const byteLengthSizeFunction = (chunk) => { + return chunk.byteLength; + }; + setFunctionName(byteLengthSizeFunction, "size"); + class ByteLengthQueuingStrategy { + constructor(options) { + assertRequiredArgument(options, 1, "ByteLengthQueuingStrategy"); + options = convertQueuingStrategyInit(options, "First parameter"); + this._byteLengthQueuingStrategyHighWaterMark = options.highWaterMark; + } + /** + * Returns the high water mark provided to the constructor. + */ + get highWaterMark() { + if (!IsByteLengthQueuingStrategy(this)) { + throw byteLengthBrandCheckException("highWaterMark"); + } + return this._byteLengthQueuingStrategyHighWaterMark; + } + /** + * Measures the size of `chunk` by returning the value of its `byteLength` property. + */ + get size() { + if (!IsByteLengthQueuingStrategy(this)) { + throw byteLengthBrandCheckException("size"); + } + return byteLengthSizeFunction; + } + } + Object.defineProperties(ByteLengthQueuingStrategy.prototype, { + highWaterMark: { enumerable: true }, + size: { enumerable: true } + }); + if (typeof Symbol.toStringTag === "symbol") { + Object.defineProperty(ByteLengthQueuingStrategy.prototype, Symbol.toStringTag, { + value: "ByteLengthQueuingStrategy", + configurable: true + }); + } + function byteLengthBrandCheckException(name) { + return new TypeError(`ByteLengthQueuingStrategy.prototype.${name} can only be used on a ByteLengthQueuingStrategy`); + } + function IsByteLengthQueuingStrategy(x2) { + if (!typeIsObject(x2)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x2, "_byteLengthQueuingStrategyHighWaterMark")) { + return false; + } + return x2 instanceof ByteLengthQueuingStrategy; + } + const countSizeFunction = () => { + return 1; + }; + setFunctionName(countSizeFunction, "size"); + class CountQueuingStrategy { + constructor(options) { + assertRequiredArgument(options, 1, "CountQueuingStrategy"); + options = convertQueuingStrategyInit(options, "First parameter"); + this._countQueuingStrategyHighWaterMark = options.highWaterMark; + } + /** + * Returns the high water mark provided to the constructor. + */ + get highWaterMark() { + if (!IsCountQueuingStrategy(this)) { + throw countBrandCheckException("highWaterMark"); + } + return this._countQueuingStrategyHighWaterMark; + } + /** + * Measures the size of `chunk` by always returning 1. + * This ensures that the total queue size is a count of the number of chunks in the queue. + */ + get size() { + if (!IsCountQueuingStrategy(this)) { + throw countBrandCheckException("size"); + } + return countSizeFunction; + } + } + Object.defineProperties(CountQueuingStrategy.prototype, { + highWaterMark: { enumerable: true }, + size: { enumerable: true } + }); + if (typeof Symbol.toStringTag === "symbol") { + Object.defineProperty(CountQueuingStrategy.prototype, Symbol.toStringTag, { + value: "CountQueuingStrategy", + configurable: true + }); + } + function countBrandCheckException(name) { + return new TypeError(`CountQueuingStrategy.prototype.${name} can only be used on a CountQueuingStrategy`); + } + function IsCountQueuingStrategy(x2) { + if (!typeIsObject(x2)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x2, "_countQueuingStrategyHighWaterMark")) { + return false; + } + return x2 instanceof CountQueuingStrategy; + } + function convertTransformer(original, context) { + assertDictionary(original, context); + const cancel = original === null || original === void 0 ? void 0 : original.cancel; + const flush = original === null || original === void 0 ? void 0 : original.flush; + const readableType = original === null || original === void 0 ? void 0 : original.readableType; + const start = original === null || original === void 0 ? void 0 : original.start; + const transform = original === null || original === void 0 ? void 0 : original.transform; + const writableType = original === null || original === void 0 ? void 0 : original.writableType; + return { + cancel: cancel === void 0 ? void 0 : convertTransformerCancelCallback(cancel, original, `${context} has member 'cancel' that`), + flush: flush === void 0 ? void 0 : convertTransformerFlushCallback(flush, original, `${context} has member 'flush' that`), + readableType, + start: start === void 0 ? void 0 : convertTransformerStartCallback(start, original, `${context} has member 'start' that`), + transform: transform === void 0 ? void 0 : convertTransformerTransformCallback(transform, original, `${context} has member 'transform' that`), + writableType + }; + } + function convertTransformerFlushCallback(fn, original, context) { + assertFunction(fn, context); + return (controller) => promiseCall(fn, original, [controller]); + } + function convertTransformerStartCallback(fn, original, context) { + assertFunction(fn, context); + return (controller) => reflectCall(fn, original, [controller]); + } + function convertTransformerTransformCallback(fn, original, context) { + assertFunction(fn, context); + return (chunk, controller) => promiseCall(fn, original, [chunk, controller]); + } + function convertTransformerCancelCallback(fn, original, context) { + assertFunction(fn, context); + return (reason) => promiseCall(fn, original, [reason]); + } + class TransformStream { + constructor(rawTransformer = {}, rawWritableStrategy = {}, rawReadableStrategy = {}) { + if (rawTransformer === void 0) { + rawTransformer = null; + } + const writableStrategy = convertQueuingStrategy(rawWritableStrategy, "Second parameter"); + const readableStrategy = convertQueuingStrategy(rawReadableStrategy, "Third parameter"); + const transformer = convertTransformer(rawTransformer, "First parameter"); + if (transformer.readableType !== void 0) { + throw new RangeError("Invalid readableType specified"); + } + if (transformer.writableType !== void 0) { + throw new RangeError("Invalid writableType specified"); + } + const readableHighWaterMark = ExtractHighWaterMark(readableStrategy, 0); + const readableSizeAlgorithm = ExtractSizeAlgorithm(readableStrategy); + const writableHighWaterMark = ExtractHighWaterMark(writableStrategy, 1); + const writableSizeAlgorithm = ExtractSizeAlgorithm(writableStrategy); + let startPromise_resolve; + const startPromise = newPromise((resolve) => { + startPromise_resolve = resolve; + }); + InitializeTransformStream(this, startPromise, writableHighWaterMark, writableSizeAlgorithm, readableHighWaterMark, readableSizeAlgorithm); + SetUpTransformStreamDefaultControllerFromTransformer(this, transformer); + if (transformer.start !== void 0) { + startPromise_resolve(transformer.start(this._transformStreamController)); + } else { + startPromise_resolve(void 0); + } + } + /** + * The readable side of the transform stream. + */ + get readable() { + if (!IsTransformStream(this)) { + throw streamBrandCheckException("readable"); + } + return this._readable; + } + /** + * The writable side of the transform stream. + */ + get writable() { + if (!IsTransformStream(this)) { + throw streamBrandCheckException("writable"); + } + return this._writable; + } + } + Object.defineProperties(TransformStream.prototype, { + readable: { enumerable: true }, + writable: { enumerable: true } + }); + if (typeof Symbol.toStringTag === "symbol") { + Object.defineProperty(TransformStream.prototype, Symbol.toStringTag, { + value: "TransformStream", + configurable: true + }); + } + function InitializeTransformStream(stream, startPromise, writableHighWaterMark, writableSizeAlgorithm, readableHighWaterMark, readableSizeAlgorithm) { + function startAlgorithm() { + return startPromise; + } + function writeAlgorithm(chunk) { + return TransformStreamDefaultSinkWriteAlgorithm(stream, chunk); + } + function abortAlgorithm(reason) { + return TransformStreamDefaultSinkAbortAlgorithm(stream, reason); + } + function closeAlgorithm() { + return TransformStreamDefaultSinkCloseAlgorithm(stream); + } + stream._writable = CreateWritableStream(startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, writableHighWaterMark, writableSizeAlgorithm); + function pullAlgorithm() { + return TransformStreamDefaultSourcePullAlgorithm(stream); + } + function cancelAlgorithm(reason) { + return TransformStreamDefaultSourceCancelAlgorithm(stream, reason); + } + stream._readable = CreateReadableStream(startAlgorithm, pullAlgorithm, cancelAlgorithm, readableHighWaterMark, readableSizeAlgorithm); + stream._backpressure = void 0; + stream._backpressureChangePromise = void 0; + stream._backpressureChangePromise_resolve = void 0; + TransformStreamSetBackpressure(stream, true); + stream._transformStreamController = void 0; + } + function IsTransformStream(x2) { + if (!typeIsObject(x2)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x2, "_transformStreamController")) { + return false; + } + return x2 instanceof TransformStream; + } + function TransformStreamError(stream, e2) { + ReadableStreamDefaultControllerError(stream._readable._readableStreamController, e2); + TransformStreamErrorWritableAndUnblockWrite(stream, e2); + } + function TransformStreamErrorWritableAndUnblockWrite(stream, e2) { + TransformStreamDefaultControllerClearAlgorithms(stream._transformStreamController); + WritableStreamDefaultControllerErrorIfNeeded(stream._writable._writableStreamController, e2); + TransformStreamUnblockWrite(stream); + } + function TransformStreamUnblockWrite(stream) { + if (stream._backpressure) { + TransformStreamSetBackpressure(stream, false); + } + } + function TransformStreamSetBackpressure(stream, backpressure) { + if (stream._backpressureChangePromise !== void 0) { + stream._backpressureChangePromise_resolve(); + } + stream._backpressureChangePromise = newPromise((resolve) => { + stream._backpressureChangePromise_resolve = resolve; + }); + stream._backpressure = backpressure; + } + class TransformStreamDefaultController { + constructor() { + throw new TypeError("Illegal constructor"); + } + /** + * Returns the desired size to fill the readable side’s internal queue. It can be negative, if the queue is over-full. + */ + get desiredSize() { + if (!IsTransformStreamDefaultController(this)) { + throw defaultControllerBrandCheckException("desiredSize"); + } + const readableController = this._controlledTransformStream._readable._readableStreamController; + return ReadableStreamDefaultControllerGetDesiredSize(readableController); + } + enqueue(chunk = void 0) { + if (!IsTransformStreamDefaultController(this)) { + throw defaultControllerBrandCheckException("enqueue"); + } + TransformStreamDefaultControllerEnqueue(this, chunk); + } + /** + * Errors both the readable side and the writable side of the controlled transform stream, making all future + * interactions with it fail with the given error `e`. Any chunks queued for transformation will be discarded. + */ + error(reason = void 0) { + if (!IsTransformStreamDefaultController(this)) { + throw defaultControllerBrandCheckException("error"); + } + TransformStreamDefaultControllerError(this, reason); + } + /** + * Closes the readable side and errors the writable side of the controlled transform stream. This is useful when the + * transformer only needs to consume a portion of the chunks written to the writable side. + */ + terminate() { + if (!IsTransformStreamDefaultController(this)) { + throw defaultControllerBrandCheckException("terminate"); + } + TransformStreamDefaultControllerTerminate(this); + } + } + Object.defineProperties(TransformStreamDefaultController.prototype, { + enqueue: { enumerable: true }, + error: { enumerable: true }, + terminate: { enumerable: true }, + desiredSize: { enumerable: true } + }); + setFunctionName(TransformStreamDefaultController.prototype.enqueue, "enqueue"); + setFunctionName(TransformStreamDefaultController.prototype.error, "error"); + setFunctionName(TransformStreamDefaultController.prototype.terminate, "terminate"); + if (typeof Symbol.toStringTag === "symbol") { + Object.defineProperty(TransformStreamDefaultController.prototype, Symbol.toStringTag, { + value: "TransformStreamDefaultController", + configurable: true + }); + } + function IsTransformStreamDefaultController(x2) { + if (!typeIsObject(x2)) { + return false; + } + if (!Object.prototype.hasOwnProperty.call(x2, "_controlledTransformStream")) { + return false; + } + return x2 instanceof TransformStreamDefaultController; + } + function SetUpTransformStreamDefaultController(stream, controller, transformAlgorithm, flushAlgorithm, cancelAlgorithm) { + controller._controlledTransformStream = stream; + stream._transformStreamController = controller; + controller._transformAlgorithm = transformAlgorithm; + controller._flushAlgorithm = flushAlgorithm; + controller._cancelAlgorithm = cancelAlgorithm; + controller._finishPromise = void 0; + controller._finishPromise_resolve = void 0; + controller._finishPromise_reject = void 0; + } + function SetUpTransformStreamDefaultControllerFromTransformer(stream, transformer) { + const controller = Object.create(TransformStreamDefaultController.prototype); + let transformAlgorithm; + let flushAlgorithm; + let cancelAlgorithm; + if (transformer.transform !== void 0) { + transformAlgorithm = (chunk) => transformer.transform(chunk, controller); + } else { + transformAlgorithm = (chunk) => { + try { + TransformStreamDefaultControllerEnqueue(controller, chunk); + return promiseResolvedWith(void 0); + } catch (transformResultE) { + return promiseRejectedWith(transformResultE); + } + }; + } + if (transformer.flush !== void 0) { + flushAlgorithm = () => transformer.flush(controller); + } else { + flushAlgorithm = () => promiseResolvedWith(void 0); + } + if (transformer.cancel !== void 0) { + cancelAlgorithm = (reason) => transformer.cancel(reason); + } else { + cancelAlgorithm = () => promiseResolvedWith(void 0); + } + SetUpTransformStreamDefaultController(stream, controller, transformAlgorithm, flushAlgorithm, cancelAlgorithm); + } + function TransformStreamDefaultControllerClearAlgorithms(controller) { + controller._transformAlgorithm = void 0; + controller._flushAlgorithm = void 0; + controller._cancelAlgorithm = void 0; + } + function TransformStreamDefaultControllerEnqueue(controller, chunk) { + const stream = controller._controlledTransformStream; + const readableController = stream._readable._readableStreamController; + if (!ReadableStreamDefaultControllerCanCloseOrEnqueue(readableController)) { + throw new TypeError("Readable side is not in a state that permits enqueue"); + } + try { + ReadableStreamDefaultControllerEnqueue(readableController, chunk); + } catch (e2) { + TransformStreamErrorWritableAndUnblockWrite(stream, e2); + throw stream._readable._storedError; + } + const backpressure = ReadableStreamDefaultControllerHasBackpressure(readableController); + if (backpressure !== stream._backpressure) { + TransformStreamSetBackpressure(stream, true); + } + } + function TransformStreamDefaultControllerError(controller, e2) { + TransformStreamError(controller._controlledTransformStream, e2); + } + function TransformStreamDefaultControllerPerformTransform(controller, chunk) { + const transformPromise = controller._transformAlgorithm(chunk); + return transformPromiseWith(transformPromise, void 0, (r2) => { + TransformStreamError(controller._controlledTransformStream, r2); + throw r2; + }); + } + function TransformStreamDefaultControllerTerminate(controller) { + const stream = controller._controlledTransformStream; + const readableController = stream._readable._readableStreamController; + ReadableStreamDefaultControllerClose(readableController); + const error = new TypeError("TransformStream terminated"); + TransformStreamErrorWritableAndUnblockWrite(stream, error); + } + function TransformStreamDefaultSinkWriteAlgorithm(stream, chunk) { + const controller = stream._transformStreamController; + if (stream._backpressure) { + const backpressureChangePromise = stream._backpressureChangePromise; + return transformPromiseWith(backpressureChangePromise, () => { + const writable = stream._writable; + const state = writable._state; + if (state === "erroring") { + throw writable._storedError; + } + return TransformStreamDefaultControllerPerformTransform(controller, chunk); + }); + } + return TransformStreamDefaultControllerPerformTransform(controller, chunk); + } + function TransformStreamDefaultSinkAbortAlgorithm(stream, reason) { + const controller = stream._transformStreamController; + if (controller._finishPromise !== void 0) { + return controller._finishPromise; + } + const readable = stream._readable; + controller._finishPromise = newPromise((resolve, reject) => { + controller._finishPromise_resolve = resolve; + controller._finishPromise_reject = reject; + }); + const cancelPromise = controller._cancelAlgorithm(reason); + TransformStreamDefaultControllerClearAlgorithms(controller); + uponPromise(cancelPromise, () => { + if (readable._state === "errored") { + defaultControllerFinishPromiseReject(controller, readable._storedError); + } else { + ReadableStreamDefaultControllerError(readable._readableStreamController, reason); + defaultControllerFinishPromiseResolve(controller); + } + return null; + }, (r2) => { + ReadableStreamDefaultControllerError(readable._readableStreamController, r2); + defaultControllerFinishPromiseReject(controller, r2); + return null; + }); + return controller._finishPromise; + } + function TransformStreamDefaultSinkCloseAlgorithm(stream) { + const controller = stream._transformStreamController; + if (controller._finishPromise !== void 0) { + return controller._finishPromise; + } + const readable = stream._readable; + controller._finishPromise = newPromise((resolve, reject) => { + controller._finishPromise_resolve = resolve; + controller._finishPromise_reject = reject; + }); + const flushPromise = controller._flushAlgorithm(); + TransformStreamDefaultControllerClearAlgorithms(controller); + uponPromise(flushPromise, () => { + if (readable._state === "errored") { + defaultControllerFinishPromiseReject(controller, readable._storedError); + } else { + ReadableStreamDefaultControllerClose(readable._readableStreamController); + defaultControllerFinishPromiseResolve(controller); + } + return null; + }, (r2) => { + ReadableStreamDefaultControllerError(readable._readableStreamController, r2); + defaultControllerFinishPromiseReject(controller, r2); + return null; + }); + return controller._finishPromise; + } + function TransformStreamDefaultSourcePullAlgorithm(stream) { + TransformStreamSetBackpressure(stream, false); + return stream._backpressureChangePromise; + } + function TransformStreamDefaultSourceCancelAlgorithm(stream, reason) { + const controller = stream._transformStreamController; + if (controller._finishPromise !== void 0) { + return controller._finishPromise; + } + const writable = stream._writable; + controller._finishPromise = newPromise((resolve, reject) => { + controller._finishPromise_resolve = resolve; + controller._finishPromise_reject = reject; + }); + const cancelPromise = controller._cancelAlgorithm(reason); + TransformStreamDefaultControllerClearAlgorithms(controller); + uponPromise(cancelPromise, () => { + if (writable._state === "errored") { + defaultControllerFinishPromiseReject(controller, writable._storedError); + } else { + WritableStreamDefaultControllerErrorIfNeeded(writable._writableStreamController, reason); + TransformStreamUnblockWrite(stream); + defaultControllerFinishPromiseResolve(controller); + } + return null; + }, (r2) => { + WritableStreamDefaultControllerErrorIfNeeded(writable._writableStreamController, r2); + TransformStreamUnblockWrite(stream); + defaultControllerFinishPromiseReject(controller, r2); + return null; + }); + return controller._finishPromise; + } + function defaultControllerBrandCheckException(name) { + return new TypeError(`TransformStreamDefaultController.prototype.${name} can only be used on a TransformStreamDefaultController`); + } + function defaultControllerFinishPromiseResolve(controller) { + if (controller._finishPromise_resolve === void 0) { + return; + } + controller._finishPromise_resolve(); + controller._finishPromise_resolve = void 0; + controller._finishPromise_reject = void 0; + } + function defaultControllerFinishPromiseReject(controller, reason) { + if (controller._finishPromise_reject === void 0) { + return; + } + setPromiseIsHandledToTrue(controller._finishPromise); + controller._finishPromise_reject(reason); + controller._finishPromise_resolve = void 0; + controller._finishPromise_reject = void 0; + } + function streamBrandCheckException(name) { + return new TypeError(`TransformStream.prototype.${name} can only be used on a TransformStream`); + } + exports3.ByteLengthQueuingStrategy = ByteLengthQueuingStrategy; + exports3.CountQueuingStrategy = CountQueuingStrategy; + exports3.ReadableByteStreamController = ReadableByteStreamController; + exports3.ReadableStream = ReadableStream2; + exports3.ReadableStreamBYOBReader = ReadableStreamBYOBReader; + exports3.ReadableStreamBYOBRequest = ReadableStreamBYOBRequest; + exports3.ReadableStreamDefaultController = ReadableStreamDefaultController; + exports3.ReadableStreamDefaultReader = ReadableStreamDefaultReader; + exports3.TransformStream = TransformStream; + exports3.TransformStreamDefaultController = TransformStreamDefaultController; + exports3.WritableStream = WritableStream; + exports3.WritableStreamDefaultController = WritableStreamDefaultController; + exports3.WritableStreamDefaultWriter = WritableStreamDefaultWriter; + }); + } +}); + +// ../../node_modules/.pnpm/fetch-blob@3.2.0/node_modules/fetch-blob/streams.cjs +var require_streams = __commonJS({ + "../../node_modules/.pnpm/fetch-blob@3.2.0/node_modules/fetch-blob/streams.cjs"() { + var POOL_SIZE2 = 65536; + if (!globalThis.ReadableStream) { + try { + const process2 = require("node:process"); + const { emitWarning } = process2; + try { + process2.emitWarning = () => { + }; + Object.assign(globalThis, require("node:stream/web")); + process2.emitWarning = emitWarning; + } catch (error) { + process2.emitWarning = emitWarning; + throw error; + } + } catch (error) { + Object.assign(globalThis, require_ponyfill_es2018()); + } + } + try { + const { Blob: Blob3 } = require("buffer"); + if (Blob3 && !Blob3.prototype.stream) { + Blob3.prototype.stream = function name(params) { + let position = 0; + const blob = this; + return new ReadableStream({ + type: "bytes", + async pull(ctrl) { + const chunk = blob.slice(position, Math.min(blob.size, position + POOL_SIZE2)); + const buffer = await chunk.arrayBuffer(); + position += buffer.byteLength; + ctrl.enqueue(new Uint8Array(buffer)); + if (position === blob.size) { + ctrl.close(); + } + } + }); + }; + } + } catch (error) { + } + } +}); + +// ../../node_modules/.pnpm/fetch-blob@3.2.0/node_modules/fetch-blob/index.js +async function* toIterator(parts, clone2 = true) { + for (const part of parts) { + if ("stream" in part) { + yield* ( + /** @type {AsyncIterableIterator} */ + part.stream() + ); + } else if (ArrayBuffer.isView(part)) { + if (clone2) { + let position = part.byteOffset; + const end = part.byteOffset + part.byteLength; + while (position !== end) { + const size = Math.min(end - position, POOL_SIZE); + const chunk = part.buffer.slice(position, position + size); + position += chunk.byteLength; + yield new Uint8Array(chunk); + } + } else { + yield part; + } + } else { + let position = 0, b = ( + /** @type {Blob} */ + part + ); + while (position !== b.size) { + const chunk = b.slice(position, Math.min(b.size, position + POOL_SIZE)); + const buffer = await chunk.arrayBuffer(); + position += buffer.byteLength; + yield new Uint8Array(buffer); + } + } + } +} +var import_streams, POOL_SIZE, _Blob, Blob2, fetch_blob_default; +var init_fetch_blob = __esm({ + "../../node_modules/.pnpm/fetch-blob@3.2.0/node_modules/fetch-blob/index.js"() { + import_streams = __toESM(require_streams(), 1); + POOL_SIZE = 65536; + _Blob = class Blob { + /** @type {Array.<(Blob|Uint8Array)>} */ + #parts = []; + #type = ""; + #size = 0; + #endings = "transparent"; + /** + * The Blob() constructor returns a new Blob object. The content + * of the blob consists of the concatenation of the values given + * in the parameter array. + * + * @param {*} blobParts + * @param {{ type?: string, endings?: string }} [options] + */ + constructor(blobParts = [], options = {}) { + if (typeof blobParts !== "object" || blobParts === null) { + throw new TypeError("Failed to construct 'Blob': The provided value cannot be converted to a sequence."); + } + if (typeof blobParts[Symbol.iterator] !== "function") { + throw new TypeError("Failed to construct 'Blob': The object must have a callable @@iterator property."); + } + if (typeof options !== "object" && typeof options !== "function") { + throw new TypeError("Failed to construct 'Blob': parameter 2 cannot convert to dictionary."); + } + if (options === null) options = {}; + const encoder = new TextEncoder(); + for (const element of blobParts) { + let part; + if (ArrayBuffer.isView(element)) { + part = new Uint8Array(element.buffer.slice(element.byteOffset, element.byteOffset + element.byteLength)); + } else if (element instanceof ArrayBuffer) { + part = new Uint8Array(element.slice(0)); + } else if (element instanceof Blob) { + part = element; + } else { + part = encoder.encode(`${element}`); + } + this.#size += ArrayBuffer.isView(part) ? part.byteLength : part.size; + this.#parts.push(part); + } + this.#endings = `${options.endings === void 0 ? "transparent" : options.endings}`; + const type = options.type === void 0 ? "" : String(options.type); + this.#type = /^[\x20-\x7E]*$/.test(type) ? type : ""; + } + /** + * The Blob interface's size property returns the + * size of the Blob in bytes. + */ + get size() { + return this.#size; + } + /** + * The type property of a Blob object returns the MIME type of the file. + */ + get type() { + return this.#type; + } + /** + * The text() method in the Blob interface returns a Promise + * that resolves with a string containing the contents of + * the blob, interpreted as UTF-8. + * + * @return {Promise} + */ + async text() { + const decoder = new TextDecoder(); + let str = ""; + for await (const part of toIterator(this.#parts, false)) { + str += decoder.decode(part, { stream: true }); + } + str += decoder.decode(); + return str; + } + /** + * The arrayBuffer() method in the Blob interface returns a + * Promise that resolves with the contents of the blob as + * binary data contained in an ArrayBuffer. + * + * @return {Promise} + */ + async arrayBuffer() { + const data = new Uint8Array(this.size); + let offset = 0; + for await (const chunk of toIterator(this.#parts, false)) { + data.set(chunk, offset); + offset += chunk.length; + } + return data.buffer; + } + stream() { + const it = toIterator(this.#parts, true); + return new globalThis.ReadableStream({ + // @ts-ignore + type: "bytes", + async pull(ctrl) { + const chunk = await it.next(); + chunk.done ? ctrl.close() : ctrl.enqueue(chunk.value); + }, + async cancel() { + await it.return(); + } + }); + } + /** + * The Blob interface's slice() method creates and returns a + * new Blob object which contains data from a subset of the + * blob on which it's called. + * + * @param {number} [start] + * @param {number} [end] + * @param {string} [type] + */ + slice(start = 0, end = this.size, type = "") { + const { size } = this; + let relativeStart = start < 0 ? Math.max(size + start, 0) : Math.min(start, size); + let relativeEnd = end < 0 ? Math.max(size + end, 0) : Math.min(end, size); + const span = Math.max(relativeEnd - relativeStart, 0); + const parts = this.#parts; + const blobParts = []; + let added = 0; + for (const part of parts) { + if (added >= span) { + break; + } + const size2 = ArrayBuffer.isView(part) ? part.byteLength : part.size; + if (relativeStart && size2 <= relativeStart) { + relativeStart -= size2; + relativeEnd -= size2; + } else { + let chunk; + if (ArrayBuffer.isView(part)) { + chunk = part.subarray(relativeStart, Math.min(size2, relativeEnd)); + added += chunk.byteLength; + } else { + chunk = part.slice(relativeStart, Math.min(size2, relativeEnd)); + added += chunk.size; + } + relativeEnd -= size2; + blobParts.push(chunk); + relativeStart = 0; + } + } + const blob = new Blob([], { type: String(type).toLowerCase() }); + blob.#size = span; + blob.#parts = blobParts; + return blob; + } + get [Symbol.toStringTag]() { + return "Blob"; + } + static [Symbol.hasInstance](object) { + return object && typeof object === "object" && typeof object.constructor === "function" && (typeof object.stream === "function" || typeof object.arrayBuffer === "function") && /^(Blob|File)$/.test(object[Symbol.toStringTag]); + } + }; + Object.defineProperties(_Blob.prototype, { + size: { enumerable: true }, + type: { enumerable: true }, + slice: { enumerable: true } + }); + Blob2 = _Blob; + fetch_blob_default = Blob2; + } +}); + +// ../../node_modules/.pnpm/fetch-blob@3.2.0/node_modules/fetch-blob/file.js +var _File, File2, file_default; +var init_file = __esm({ + "../../node_modules/.pnpm/fetch-blob@3.2.0/node_modules/fetch-blob/file.js"() { + init_fetch_blob(); + _File = class File extends fetch_blob_default { + #lastModified = 0; + #name = ""; + /** + * @param {*[]} fileBits + * @param {string} fileName + * @param {{lastModified?: number, type?: string}} options + */ + // @ts-ignore + constructor(fileBits, fileName, options = {}) { + if (arguments.length < 2) { + throw new TypeError(`Failed to construct 'File': 2 arguments required, but only ${arguments.length} present.`); + } + super(fileBits, options); + if (options === null) options = {}; + const lastModified = options.lastModified === void 0 ? Date.now() : Number(options.lastModified); + if (!Number.isNaN(lastModified)) { + this.#lastModified = lastModified; + } + this.#name = String(fileName); + } + get name() { + return this.#name; + } + get lastModified() { + return this.#lastModified; + } + get [Symbol.toStringTag]() { + return "File"; + } + static [Symbol.hasInstance](object) { + return !!object && object instanceof fetch_blob_default && /^(File)$/.test(object[Symbol.toStringTag]); + } + }; + File2 = _File; + file_default = File2; + } +}); + +// ../../node_modules/.pnpm/formdata-polyfill@4.0.10/node_modules/formdata-polyfill/esm.min.js +function formDataToBlob(F2, B = fetch_blob_default) { + var b = `${r()}${r()}`.replace(/\./g, "").slice(-28).padStart(32, "-"), c = [], p = `--${b}\r +Content-Disposition: form-data; name="`; + F2.forEach((v, n) => typeof v == "string" ? c.push(p + e(n) + `"\r +\r +${v.replace(/\r(?!\n)|(? (a += "", /^(Blob|File)$/.test(b && b[t]) ? [(c = c !== void 0 ? c + "" : b[t] == "File" ? b.name : "blob", a), b.name !== c || b[t] == "blob" ? new file_default([b], c, b) : b] : [a, b + ""]); + e = (c, f3) => (f3 ? c : c.replace(/\r?\n|\r/g, "\r\n")).replace(/\n/g, "%0A").replace(/\r/g, "%0D").replace(/"/g, "%22"); + x = (n, a, e2) => { + if (a.length < e2) { + throw new TypeError(`Failed to execute '${n}' on 'FormData': ${e2} arguments required, but only ${a.length} present.`); + } + }; + FormData = class FormData2 { + #d = []; + constructor(...a) { + if (a.length) throw new TypeError(`Failed to construct 'FormData': parameter 1 is not of type 'HTMLFormElement'.`); + } + get [t]() { + return "FormData"; + } + [i]() { + return this.entries(); + } + static [h](o) { + return o && typeof o === "object" && o[t] === "FormData" && !m.some((m2) => typeof o[m2] != "function"); + } + append(...a) { + x("append", arguments, 2); + this.#d.push(f(...a)); + } + delete(a) { + x("delete", arguments, 1); + a += ""; + this.#d = this.#d.filter(([b]) => b !== a); + } + get(a) { + x("get", arguments, 1); + a += ""; + for (var b = this.#d, l = b.length, c = 0; c < l; c++) if (b[c][0] === a) return b[c][1]; + return null; + } + getAll(a, b) { + x("getAll", arguments, 1); + b = []; + a += ""; + this.#d.forEach((c) => c[0] === a && b.push(c[1])); + return b; + } + has(a) { + x("has", arguments, 1); + a += ""; + return this.#d.some((b) => b[0] === a); + } + forEach(a, b) { + x("forEach", arguments, 1); + for (var [c, d] of this) a.call(b, d, c, this); + } + set(...a) { + x("set", arguments, 2); + var b = [], c = true; + a = f(...a); + this.#d.forEach((d) => { + d[0] === a[0] ? c && (c = !b.push(a)) : b.push(d); + }); + c && b.push(a); + this.#d = b; + } + *entries() { + yield* this.#d; + } + *keys() { + for (var [a] of this) yield a; + } + *values() { + for (var [, a] of this) yield a; + } + }; + } +}); + +// ../../node_modules/.pnpm/node-domexception@1.0.0/node_modules/node-domexception/index.js +var require_node_domexception = __commonJS({ + "../../node_modules/.pnpm/node-domexception@1.0.0/node_modules/node-domexception/index.js"(exports2, module2) { + if (!globalThis.DOMException) { + try { + const { MessageChannel } = require("worker_threads"), port = new MessageChannel().port1, ab = new ArrayBuffer(); + port.postMessage(ab, [ab, ab]); + } catch (err) { + err.constructor.name === "DOMException" && (globalThis.DOMException = err.constructor); + } + } + module2.exports = globalThis.DOMException; + } +}); + +// ../../node_modules/.pnpm/fetch-blob@3.2.0/node_modules/fetch-blob/from.js +var import_node_fs, import_node_domexception, stat; +var init_from = __esm({ + "../../node_modules/.pnpm/fetch-blob@3.2.0/node_modules/fetch-blob/from.js"() { + import_node_fs = require("node:fs"); + import_node_domexception = __toESM(require_node_domexception(), 1); + init_file(); + init_fetch_blob(); + ({ stat } = import_node_fs.promises); + } +}); + +// ../../node_modules/.pnpm/node-fetch@3.3.2/node_modules/node-fetch/src/utils/multipart-parser.js +var multipart_parser_exports = {}; +__export(multipart_parser_exports, { + toFormData: () => toFormData +}); +function _fileName(headerValue) { + const m2 = headerValue.match(/\bfilename=("(.*?)"|([^()<>@,;:\\"/[\]?={}\s\t]+))($|;\s)/i); + if (!m2) { + return; + } + const match = m2[2] || m2[3] || ""; + let filename = match.slice(match.lastIndexOf("\\") + 1); + filename = filename.replace(/%22/g, '"'); + filename = filename.replace(/&#(\d{4});/g, (m3, code) => { + return String.fromCharCode(code); + }); + return filename; +} +async function toFormData(Body2, ct) { + if (!/multipart/i.test(ct)) { + throw new TypeError("Failed to fetch"); + } + const m2 = ct.match(/boundary=(?:"([^"]+)"|([^;]+))/i); + if (!m2) { + throw new TypeError("no or bad content-type header, no multipart boundary"); + } + const parser = new MultipartParser(m2[1] || m2[2]); + let headerField; + let headerValue; + let entryValue; + let entryName; + let contentType; + let filename; + const entryChunks = []; + const formData = new FormData(); + const onPartData = (ui8a) => { + entryValue += decoder.decode(ui8a, { stream: true }); + }; + const appendToFile = (ui8a) => { + entryChunks.push(ui8a); + }; + const appendFileToFormData = () => { + const file = new file_default(entryChunks, filename, { type: contentType }); + formData.append(entryName, file); + }; + const appendEntryToFormData = () => { + formData.append(entryName, entryValue); + }; + const decoder = new TextDecoder("utf-8"); + decoder.decode(); + parser.onPartBegin = function() { + parser.onPartData = onPartData; + parser.onPartEnd = appendEntryToFormData; + headerField = ""; + headerValue = ""; + entryValue = ""; + entryName = ""; + contentType = ""; + filename = null; + entryChunks.length = 0; + }; + parser.onHeaderField = function(ui8a) { + headerField += decoder.decode(ui8a, { stream: true }); + }; + parser.onHeaderValue = function(ui8a) { + headerValue += decoder.decode(ui8a, { stream: true }); + }; + parser.onHeaderEnd = function() { + headerValue += decoder.decode(); + headerField = headerField.toLowerCase(); + if (headerField === "content-disposition") { + const m3 = headerValue.match(/\bname=("([^"]*)"|([^()<>@,;:\\"/[\]?={}\s\t]+))/i); + if (m3) { + entryName = m3[2] || m3[3] || ""; + } + filename = _fileName(headerValue); + if (filename) { + parser.onPartData = appendToFile; + parser.onPartEnd = appendFileToFormData; + } + } else if (headerField === "content-type") { + contentType = headerValue; + } + headerValue = ""; + headerField = ""; + }; + for await (const chunk of Body2) { + parser.write(chunk); + } + parser.end(); + return formData; +} +var s, S, f2, F, LF, CR, SPACE, HYPHEN, COLON, A, Z, lower, noop, MultipartParser; +var init_multipart_parser = __esm({ + "../../node_modules/.pnpm/node-fetch@3.3.2/node_modules/node-fetch/src/utils/multipart-parser.js"() { + init_from(); + init_esm_min(); + s = 0; + S = { + START_BOUNDARY: s++, + HEADER_FIELD_START: s++, + HEADER_FIELD: s++, + HEADER_VALUE_START: s++, + HEADER_VALUE: s++, + HEADER_VALUE_ALMOST_DONE: s++, + HEADERS_ALMOST_DONE: s++, + PART_DATA_START: s++, + PART_DATA: s++, + END: s++ + }; + f2 = 1; + F = { + PART_BOUNDARY: f2, + LAST_BOUNDARY: f2 *= 2 + }; + LF = 10; + CR = 13; + SPACE = 32; + HYPHEN = 45; + COLON = 58; + A = 97; + Z = 122; + lower = (c) => c | 32; + noop = () => { + }; + MultipartParser = class { + /** + * @param {string} boundary + */ + constructor(boundary) { + this.index = 0; + this.flags = 0; + this.onHeaderEnd = noop; + this.onHeaderField = noop; + this.onHeadersEnd = noop; + this.onHeaderValue = noop; + this.onPartBegin = noop; + this.onPartData = noop; + this.onPartEnd = noop; + this.boundaryChars = {}; + boundary = "\r\n--" + boundary; + const ui8a = new Uint8Array(boundary.length); + for (let i2 = 0; i2 < boundary.length; i2++) { + ui8a[i2] = boundary.charCodeAt(i2); + this.boundaryChars[ui8a[i2]] = true; + } + this.boundary = ui8a; + this.lookbehind = new Uint8Array(this.boundary.length + 8); + this.state = S.START_BOUNDARY; + } + /** + * @param {Uint8Array} data + */ + write(data) { + let i2 = 0; + const length_ = data.length; + let previousIndex = this.index; + let { lookbehind, boundary, boundaryChars, index, state, flags } = this; + const boundaryLength = this.boundary.length; + const boundaryEnd = boundaryLength - 1; + const bufferLength = data.length; + let c; + let cl; + const mark = (name) => { + this[name + "Mark"] = i2; + }; + const clear = (name) => { + delete this[name + "Mark"]; + }; + const callback = (callbackSymbol, start, end, ui8a) => { + if (start === void 0 || start !== end) { + this[callbackSymbol](ui8a && ui8a.subarray(start, end)); + } + }; + const dataCallback = (name, clear2) => { + const markSymbol = name + "Mark"; + if (!(markSymbol in this)) { + return; + } + if (clear2) { + callback(name, this[markSymbol], i2, data); + delete this[markSymbol]; + } else { + callback(name, this[markSymbol], data.length, data); + this[markSymbol] = 0; + } + }; + for (i2 = 0; i2 < length_; i2++) { + c = data[i2]; + switch (state) { + case S.START_BOUNDARY: + if (index === boundary.length - 2) { + if (c === HYPHEN) { + flags |= F.LAST_BOUNDARY; + } else if (c !== CR) { + return; + } + index++; + break; + } else if (index - 1 === boundary.length - 2) { + if (flags & F.LAST_BOUNDARY && c === HYPHEN) { + state = S.END; + flags = 0; + } else if (!(flags & F.LAST_BOUNDARY) && c === LF) { + index = 0; + callback("onPartBegin"); + state = S.HEADER_FIELD_START; + } else { + return; + } + break; + } + if (c !== boundary[index + 2]) { + index = -2; + } + if (c === boundary[index + 2]) { + index++; + } + break; + case S.HEADER_FIELD_START: + state = S.HEADER_FIELD; + mark("onHeaderField"); + index = 0; + // falls through + case S.HEADER_FIELD: + if (c === CR) { + clear("onHeaderField"); + state = S.HEADERS_ALMOST_DONE; + break; + } + index++; + if (c === HYPHEN) { + break; + } + if (c === COLON) { + if (index === 1) { + return; + } + dataCallback("onHeaderField", true); + state = S.HEADER_VALUE_START; + break; + } + cl = lower(c); + if (cl < A || cl > Z) { + return; + } + break; + case S.HEADER_VALUE_START: + if (c === SPACE) { + break; + } + mark("onHeaderValue"); + state = S.HEADER_VALUE; + // falls through + case S.HEADER_VALUE: + if (c === CR) { + dataCallback("onHeaderValue", true); + callback("onHeaderEnd"); + state = S.HEADER_VALUE_ALMOST_DONE; + } + break; + case S.HEADER_VALUE_ALMOST_DONE: + if (c !== LF) { + return; + } + state = S.HEADER_FIELD_START; + break; + case S.HEADERS_ALMOST_DONE: + if (c !== LF) { + return; + } + callback("onHeadersEnd"); + state = S.PART_DATA_START; + break; + case S.PART_DATA_START: + state = S.PART_DATA; + mark("onPartData"); + // falls through + case S.PART_DATA: + previousIndex = index; + if (index === 0) { + i2 += boundaryEnd; + while (i2 < bufferLength && !(data[i2] in boundaryChars)) { + i2 += boundaryLength; + } + i2 -= boundaryEnd; + c = data[i2]; + } + if (index < boundary.length) { + if (boundary[index] === c) { + if (index === 0) { + dataCallback("onPartData", true); + } + index++; + } else { + index = 0; + } + } else if (index === boundary.length) { + index++; + if (c === CR) { + flags |= F.PART_BOUNDARY; + } else if (c === HYPHEN) { + flags |= F.LAST_BOUNDARY; + } else { + index = 0; + } + } else if (index - 1 === boundary.length) { + if (flags & F.PART_BOUNDARY) { + index = 0; + if (c === LF) { + flags &= ~F.PART_BOUNDARY; + callback("onPartEnd"); + callback("onPartBegin"); + state = S.HEADER_FIELD_START; + break; + } + } else if (flags & F.LAST_BOUNDARY) { + if (c === HYPHEN) { + callback("onPartEnd"); + state = S.END; + flags = 0; + } else { + index = 0; + } + } else { + index = 0; + } + } + if (index > 0) { + lookbehind[index - 1] = c; + } else if (previousIndex > 0) { + const _lookbehind = new Uint8Array(lookbehind.buffer, lookbehind.byteOffset, lookbehind.byteLength); + callback("onPartData", 0, previousIndex, _lookbehind); + previousIndex = 0; + mark("onPartData"); + i2--; + } + break; + case S.END: + break; + default: + throw new Error(`Unexpected state entered: ${state}`); + } + } + dataCallback("onHeaderField"); + dataCallback("onHeaderValue"); + dataCallback("onPartData"); + this.index = index; + this.state = state; + this.flags = flags; + } + end() { + if (this.state === S.HEADER_FIELD_START && this.index === 0 || this.state === S.PART_DATA && this.index === this.boundary.length) { + this.onPartEnd(); + } else if (this.state !== S.END) { + throw new Error("MultipartParser.end(): stream ended unexpectedly"); + } + } + }; + } +}); + +// ../../packages/common/src/bridge.ts +function saltyChatExport(method, cb) { + on(`__cfx_export_saltychat_${method}`, (setCb) => { + setCb(cb); + }); +} + +// ../../packages/common/src/config.ts +var import_json5 = __toESM(require_lib()); +function mergeAndValidate(defaultObj, parsedObj, path = []) { + const result = { ...defaultObj }; + for (const key in defaultObj) { + if (Object.prototype.hasOwnProperty.call(defaultObj, key) === false) { + continue; + } + const currentPath = [...path, key].join("."); + if (!(key in parsedObj)) { + console.warn( + `[YaCA] Missing config value for key '${currentPath}' setting to default value: ${defaultObj[key]} +Missing config values can cause unexpected behavior of the script.` + ); + } else if (typeof defaultObj[key] === "object" && defaultObj[key] !== null && !Array.isArray(defaultObj[key]) && typeof parsedObj[key] === "object" && parsedObj[key] !== null && !Array.isArray(parsedObj[key])) { + result[key] = mergeAndValidate(defaultObj[key], parsedObj[key], [...path, key]); + } else { + result[key] = parsedObj[key]; + } + } + for (const key of Object.keys(parsedObj)) { + const currentPath = [...path, key].join("."); + if (!(key in defaultObj)) { + console.warn(`[YaCA] Unknown config key '${currentPath}' found in config file. This key will be ignored and can be removed.`); + } + } + return result; +} +function loadConfig(filePath, defaultValues) { + const fileData = LoadResourceFile(GetCurrentResourceName(), filePath); + if (!fileData) { + return defaultValues; + } + const parsedData = import_json5.default.parse(fileData); + return mergeAndValidate(defaultValues, parsedData); +} + +// ../../packages/common/src/constants.ts +var MEGAPHONE_STATE_NAME = "yacaMegaphone"; +var PHONE_SPEAKER_STATE_NAME = "yacaPhoneSpeaker"; +var VOICE_RANGE_STATE_NAME = "yacaVoiceRange"; +var GLOBAL_ERROR_LEVEL_STATE_NAME = "yacaGlobalErrorLevel"; + +// ../../packages/common/src/errorlevel.ts +var setGlobalErrorLevel = (errorLevel) => { + GlobalState.set(GLOBAL_ERROR_LEVEL_STATE_NAME, clamp(errorLevel, 0, 1), true); +}; +var getGlobalErrorLevel = () => { + return GlobalState[GLOBAL_ERROR_LEVEL_STATE_NAME] ?? 0; +}; + +// ../../packages/common/src/locale.ts +var import_fast_printf = __toESM(require_printf()); +var resourceName = GetCurrentResourceName(); +var dict = {}; +function flattenDict(source2, target, prefix) { + for (const [key, value] of Object.entries(source2)) { + const fullKey = prefix ? `${prefix}.${key}` : key; + if (typeof value === "object") flattenDict(value, target, fullKey); + else target[fullKey] = String(value); + } + return target; +} +var locale = (str, ...args) => { + const localeStr = dict[str]; + if (localeStr) { + if (args.length > 0) { + return (0, import_fast_printf.printf)(localeStr, ...args); + } + return localeStr; + } + return str; +}; +var initLocale = (configLocale) => { + const lang = configLocale || "en"; + let locales = JSON.parse(LoadResourceFile(resourceName, `locales/${lang}.json`)); + if (!locales) { + console.warn(`could not load 'locales/${lang}.json'`); + if (lang !== "en") { + locales = JSON.parse(LoadResourceFile(resourceName, "locales/en.json")); + if (!locales) { + console.warn("could not load 'locales/en.json'"); + } + } + if (!locales) return; + } + const flattened = flattenDict(locales, {}); + for (const [k, v] of Object.entries(flattened)) { + const regExp = new RegExp(/\$\{([^}]+)}/g); + const matches = v.match(regExp); + if (matches) { + for (const match of matches) { + if (!match) break; + const variable = match.substring(2, match.length - 1); + const locale2 = flattened[variable]; + if (locale2) { + flattened[k] = v.replace(match, locale2); + } + } + } + dict[k] = v; + } +}; + +// ../../packages/common/src/index.ts +function clamp(value, min = 0, max = 1) { + return Math.max(min, Math.min(max, value)); +} + +// ../../packages/types/src/config.ts +var defaultSharedConfig = { + versionCheck: true, + autoConnectOnJoin: true, + buildType: 0 /* RELEASE */, + locale: "en", + unmuteDelay: 400, + maxPhoneSpeakerRange: 5, + phoneHearPlayersNearby: false, + notifications: { + oxLib: false, + okoknotify: false, + gta: true, + redm: false, + own: false + }, + keyBinds: { + increaseVoiceRange: "ADD", + decreaseVoiceRange: "SUBTRACT", + primaryRadioTransmit: "N", + secondaryRadioTransmit: "CAPITAL", + megaphone: "B", + voiceRangeWithMouseWheel: "LCONTROL" + }, + radioSettings: { + animation: { + dictionary: "random@arrests", + name: "generic_radio_chatter", + flag: 49 + }, + propWhileTalking: { + prop: false, + boneId: 28422, + position: [0, 0, 0], + rotation: [0, 0, 0] + }, + channelCount: 9, + mode: "None", + maxDistance: 1e3 + }, + voiceRange: { + defaultIndex: 1, + ranges: [1, 3, 8, 15, 20, 25, 30, 40], + sendNotification: true, + markerColor: { + enabled: true, + r: 0, + g: 255, + b: 0, + a: 50, + duration: 1e3, + type: 1, + rotate: true + } + }, + megaphone: { + range: 30, + automaticVehicleDetection: true, + allowedVehicleClasses: [18, 19], + allowedVehicleModels: ["polmav"] + }, + saltyChatBridge: false, + mufflingSettings: { + mufflingRange: -1, + vehicleMuffling: { + enabled: true, + vehicleWhitelist: [ + "gauntlet6", + "draugur", + "bodhi2", + "vagrant", + "outlaw", + "trophytruck", + "ratel", + "drifttampa", + "sm722", + "tornado4", + "swinger", + "locust", + "hotring" + ] + }, + intensities: { + differentRoom: 10, + bothCarsClosed: 10, + oneCarClosed: 6, + megaPhoneInCar: 6 + } + }, + radioAntiSpamCooldown: false, + useLocalLipSync: false +}; +var defaultServerConfig = { + uniqueServerId: "", + ingameChannelId: 3, + ingameChannelPassword: "", + defaultChannelId: 1, + useWhisper: false, + excludeChannels: [], + userNamePattern: "[{serverid}] {guid}" +}; +var defaultTowerConfig = { + towerPositions: [ + [2572, 5397, 56], + [2663, 4972, 56], + [2892, 3911, 56], + [2720, 3304, 64], + [2388, 2949, 64], + [1830, 2368, 64], + [1650, 1316, 102], + [1363, 680, 102], + [918, 230, 92], + [567, 303, 58], + [-47, -666, 74], + [-585, -902, 53], + [2572, 5397, 56], + [2338, 5940, 77], + [1916, 6244, 65], + [1591, 6371, 42], + [953, 6504, 42], + [76, 6606, 42], + [408, 6587, 42], + [-338, -579, 48], + [-293, -632, 47], + [-269, -962, 143], + [98, -870, 136], + [-214, -744, 219], + [-166, -590, 199], + [124, -654, 261], + [149, -769, 261], + [580, 89, 117], + [423, 15, 151], + [424, 18, 151], + [551, -28, 93], + [305, -284, 68], + [299, -313, 68], + [1240, -1090, 44], + [-418, -2804, 14], + [802, -2996, 27], + [253, -3145, 39], + [207, -3145, 39], + [207, -3307, 39], + [247, -3307, 39], + [484, -2178, 40], + [548, -2219, 67], + [-701, 58, 68], + [-696, 208, 139], + [-769, 255, 134], + [-150, -150, 96], + [-202, -327, 65], + [-1913, -3031, 22], + [-1918, -3028, 22], + [-1039, -2385, 27], + [-1042, -2390, 27], + [-1583, -3216, 28], + [-1590, -3212, 28], + [-1308, -2626, 36], + [-1311, -2624, 36], + [-984, -2778, 48], + [-991, -2774, 48], + [-556, -119, 50], + [-619, -106, 51], + [-1167, -575, 40], + [-1152, -443, 42], + [-1156, -498, 49], + [-1290, -445, 106], + [-928, -383, 135], + [-902, -443, 170], + [-770, -786, 83], + [-824, -719, 120], + [-598, -917, 35], + [-678, -717, 54], + [-669, -804, 31], + [-1463, -526, 83], + [-1525, -596, 66], + [-1375, -465, 83], + [-1711, 478, 127], + [-2311, 335, 187], + [-2214, 342, 198], + [-2234, 187, 193], + [202, 1204, 230], + [217, 1140, 230], + [668, 590, 136], + [722, 562, 134], + [838, 510, 138], + [773, 575, 138], + [735, 231, 145], + [450, 5566, 795], + [-449, 6019, 35], + [-142, 6286, 39], + [-368, 6105, 38], + [2792, 5996, 355], + [2796, 5992, 354], + [3460, 3653, 51], + [3459, 3659, 51], + [3615, 3642, 51], + [3614, 3636, 51], + [-2180, 3252, 54], + [-2124, 3219, 54], + [-2050, 3178, 54], + [1858, 3694, 37], + [1695, 3614, 37], + [1692, 2532, 60], + [1692, 2647, 60], + [1824, 2574, 60], + [1407, 2117, 104] + ] +}; + +// src/utils/cache.ts +var cache = { + resource: GetCurrentResourceName() +}; + +// src/utils/events.ts +var triggerClientEvent = (eventName, targetIds, ...args) => { + if (!Array.isArray(targetIds)) { + targetIds = [targetIds]; + } + if (targetIds.length < 1) { + return; + } + const dataSerialized = msgpack_pack(args); + for (const targetId of targetIds) { + TriggerClientEventInternal(eventName, targetId.toString(), dataSerialized, dataSerialized.length); + } +}; + +// src/utils/generator.ts +var import_node_crypto = require("node:crypto"); +function generateRandomName(src, nameSet, namePattern) { + let name; + const playerName = GetPlayerName(src.toString()); + for (let i2 = 0; i2 < 10; i2++) { + let generatedName = namePattern; + generatedName = generatedName.replace("{serverid}", src.toString()); + generatedName = generatedName.replace("{playername}", playerName); + generatedName = generatedName.replace("{guid}", (0, import_node_crypto.randomUUID)().replace(/-/g, "")); + generatedName = generatedName.slice(0, 30); + if (!nameSet.has(generatedName)) { + name = generatedName; + nameSet.add(generatedName); + break; + } + } + if (!name) { + console.error(`YaCA: Couldn't generate a random name for player ${playerName} (ID: ${src}).`); + } + return name; +} + +// ../../node_modules/.pnpm/node-fetch@3.3.2/node_modules/node-fetch/src/index.js +var import_node_http2 = __toESM(require("node:http"), 1); +var import_node_https = __toESM(require("node:https"), 1); +var import_node_zlib = __toESM(require("node:zlib"), 1); +var import_node_stream2 = __toESM(require("node:stream"), 1); +var import_node_buffer2 = require("node:buffer"); + +// ../../node_modules/.pnpm/data-uri-to-buffer@4.0.1/node_modules/data-uri-to-buffer/dist/index.js +function dataUriToBuffer(uri) { + if (!/^data:/i.test(uri)) { + throw new TypeError('`uri` does not appear to be a Data URI (must begin with "data:")'); + } + uri = uri.replace(/\r?\n/g, ""); + const firstComma = uri.indexOf(","); + if (firstComma === -1 || firstComma <= 4) { + throw new TypeError("malformed data: URI"); + } + const meta = uri.substring(5, firstComma).split(";"); + let charset = ""; + let base64 = false; + const type = meta[0] || "text/plain"; + let typeFull = type; + for (let i2 = 1; i2 < meta.length; i2++) { + if (meta[i2] === "base64") { + base64 = true; + } else if (meta[i2]) { + typeFull += `;${meta[i2]}`; + if (meta[i2].indexOf("charset=") === 0) { + charset = meta[i2].substring(8); + } + } + } + if (!meta[0] && !charset.length) { + typeFull += ";charset=US-ASCII"; + charset = "US-ASCII"; + } + const encoding = base64 ? "base64" : "ascii"; + const data = unescape(uri.substring(firstComma + 1)); + const buffer = Buffer.from(data, encoding); + buffer.type = type; + buffer.typeFull = typeFull; + buffer.charset = charset; + return buffer; +} +var dist_default = dataUriToBuffer; + +// ../../node_modules/.pnpm/node-fetch@3.3.2/node_modules/node-fetch/src/body.js +var import_node_stream = __toESM(require("node:stream"), 1); +var import_node_util = require("node:util"); +var import_node_buffer = require("node:buffer"); +init_fetch_blob(); +init_esm_min(); + +// ../../node_modules/.pnpm/node-fetch@3.3.2/node_modules/node-fetch/src/errors/base.js +var FetchBaseError = class extends Error { + constructor(message, type) { + super(message); + Error.captureStackTrace(this, this.constructor); + this.type = type; + } + get name() { + return this.constructor.name; + } + get [Symbol.toStringTag]() { + return this.constructor.name; + } +}; + +// ../../node_modules/.pnpm/node-fetch@3.3.2/node_modules/node-fetch/src/errors/fetch-error.js +var FetchError = class extends FetchBaseError { + /** + * @param {string} message - Error message for human + * @param {string} [type] - Error type for machine + * @param {SystemError} [systemError] - For Node.js system error + */ + constructor(message, type, systemError) { + super(message, type); + if (systemError) { + this.code = this.errno = systemError.code; + this.erroredSysCall = systemError.syscall; + } + } +}; + +// ../../node_modules/.pnpm/node-fetch@3.3.2/node_modules/node-fetch/src/utils/is.js +var NAME = Symbol.toStringTag; +var isURLSearchParameters = (object) => { + return typeof object === "object" && typeof object.append === "function" && typeof object.delete === "function" && typeof object.get === "function" && typeof object.getAll === "function" && typeof object.has === "function" && typeof object.set === "function" && typeof object.sort === "function" && object[NAME] === "URLSearchParams"; +}; +var isBlob = (object) => { + return object && typeof object === "object" && typeof object.arrayBuffer === "function" && typeof object.type === "string" && typeof object.stream === "function" && typeof object.constructor === "function" && /^(Blob|File)$/.test(object[NAME]); +}; +var isAbortSignal = (object) => { + return typeof object === "object" && (object[NAME] === "AbortSignal" || object[NAME] === "EventTarget"); +}; +var isDomainOrSubdomain = (destination, original) => { + const orig = new URL(original).hostname; + const dest = new URL(destination).hostname; + return orig === dest || orig.endsWith(`.${dest}`); +}; +var isSameProtocol = (destination, original) => { + const orig = new URL(original).protocol; + const dest = new URL(destination).protocol; + return orig === dest; +}; + +// ../../node_modules/.pnpm/node-fetch@3.3.2/node_modules/node-fetch/src/body.js +var pipeline = (0, import_node_util.promisify)(import_node_stream.default.pipeline); +var INTERNALS = Symbol("Body internals"); +var Body = class { + constructor(body, { + size = 0 + } = {}) { + let boundary = null; + if (body === null) { + body = null; + } else if (isURLSearchParameters(body)) { + body = import_node_buffer.Buffer.from(body.toString()); + } else if (isBlob(body)) { + } else if (import_node_buffer.Buffer.isBuffer(body)) { + } else if (import_node_util.types.isAnyArrayBuffer(body)) { + body = import_node_buffer.Buffer.from(body); + } else if (ArrayBuffer.isView(body)) { + body = import_node_buffer.Buffer.from(body.buffer, body.byteOffset, body.byteLength); + } else if (body instanceof import_node_stream.default) { + } else if (body instanceof FormData) { + body = formDataToBlob(body); + boundary = body.type.split("=")[1]; + } else { + body = import_node_buffer.Buffer.from(String(body)); + } + let stream = body; + if (import_node_buffer.Buffer.isBuffer(body)) { + stream = import_node_stream.default.Readable.from(body); + } else if (isBlob(body)) { + stream = import_node_stream.default.Readable.from(body.stream()); + } + this[INTERNALS] = { + body, + stream, + boundary, + disturbed: false, + error: null + }; + this.size = size; + if (body instanceof import_node_stream.default) { + body.on("error", (error_) => { + const error = error_ instanceof FetchBaseError ? error_ : new FetchError(`Invalid response body while trying to fetch ${this.url}: ${error_.message}`, "system", error_); + this[INTERNALS].error = error; + }); + } + } + get body() { + return this[INTERNALS].stream; + } + get bodyUsed() { + return this[INTERNALS].disturbed; + } + /** + * Decode response as ArrayBuffer + * + * @return Promise + */ + async arrayBuffer() { + const { buffer, byteOffset, byteLength } = await consumeBody(this); + return buffer.slice(byteOffset, byteOffset + byteLength); + } + async formData() { + const ct = this.headers.get("content-type"); + if (ct.startsWith("application/x-www-form-urlencoded")) { + const formData = new FormData(); + const parameters = new URLSearchParams(await this.text()); + for (const [name, value] of parameters) { + formData.append(name, value); + } + return formData; + } + const { toFormData: toFormData2 } = await Promise.resolve().then(() => (init_multipart_parser(), multipart_parser_exports)); + return toFormData2(this.body, ct); + } + /** + * Return raw response as Blob + * + * @return Promise + */ + async blob() { + const ct = this.headers && this.headers.get("content-type") || this[INTERNALS].body && this[INTERNALS].body.type || ""; + const buf = await this.arrayBuffer(); + return new fetch_blob_default([buf], { + type: ct + }); + } + /** + * Decode response as json + * + * @return Promise + */ + async json() { + const text = await this.text(); + return JSON.parse(text); + } + /** + * Decode response as text + * + * @return Promise + */ + async text() { + const buffer = await consumeBody(this); + return new TextDecoder().decode(buffer); + } + /** + * Decode response as buffer (non-spec api) + * + * @return Promise + */ + buffer() { + return consumeBody(this); + } +}; +Body.prototype.buffer = (0, import_node_util.deprecate)(Body.prototype.buffer, "Please use 'response.arrayBuffer()' instead of 'response.buffer()'", "node-fetch#buffer"); +Object.defineProperties(Body.prototype, { + body: { enumerable: true }, + bodyUsed: { enumerable: true }, + arrayBuffer: { enumerable: true }, + blob: { enumerable: true }, + json: { enumerable: true }, + text: { enumerable: true }, + data: { get: (0, import_node_util.deprecate)( + () => { + }, + "data doesn't exist, use json(), text(), arrayBuffer(), or body instead", + "https://github.com/node-fetch/node-fetch/issues/1000 (response)" + ) } +}); +async function consumeBody(data) { + if (data[INTERNALS].disturbed) { + throw new TypeError(`body used already for: ${data.url}`); + } + data[INTERNALS].disturbed = true; + if (data[INTERNALS].error) { + throw data[INTERNALS].error; + } + const { body } = data; + if (body === null) { + return import_node_buffer.Buffer.alloc(0); + } + if (!(body instanceof import_node_stream.default)) { + return import_node_buffer.Buffer.alloc(0); + } + const accum = []; + let accumBytes = 0; + try { + for await (const chunk of body) { + if (data.size > 0 && accumBytes + chunk.length > data.size) { + const error = new FetchError(`content size at ${data.url} over limit: ${data.size}`, "max-size"); + body.destroy(error); + throw error; + } + accumBytes += chunk.length; + accum.push(chunk); + } + } catch (error) { + const error_ = error instanceof FetchBaseError ? error : new FetchError(`Invalid response body while trying to fetch ${data.url}: ${error.message}`, "system", error); + throw error_; + } + if (body.readableEnded === true || body._readableState.ended === true) { + try { + if (accum.every((c) => typeof c === "string")) { + return import_node_buffer.Buffer.from(accum.join("")); + } + return import_node_buffer.Buffer.concat(accum, accumBytes); + } catch (error) { + throw new FetchError(`Could not create Buffer from response body for ${data.url}: ${error.message}`, "system", error); + } + } else { + throw new FetchError(`Premature close of server response while trying to fetch ${data.url}`); + } +} +var clone = (instance, highWaterMark) => { + let p1; + let p2; + let { body } = instance[INTERNALS]; + if (instance.bodyUsed) { + throw new Error("cannot clone body after it is used"); + } + if (body instanceof import_node_stream.default && typeof body.getBoundary !== "function") { + p1 = new import_node_stream.PassThrough({ highWaterMark }); + p2 = new import_node_stream.PassThrough({ highWaterMark }); + body.pipe(p1); + body.pipe(p2); + instance[INTERNALS].stream = p1; + body = p2; + } + return body; +}; +var getNonSpecFormDataBoundary = (0, import_node_util.deprecate)( + (body) => body.getBoundary(), + "form-data doesn't follow the spec and requires special treatment. Use alternative package", + "https://github.com/node-fetch/node-fetch/issues/1167" +); +var extractContentType = (body, request) => { + if (body === null) { + return null; + } + if (typeof body === "string") { + return "text/plain;charset=UTF-8"; + } + if (isURLSearchParameters(body)) { + return "application/x-www-form-urlencoded;charset=UTF-8"; + } + if (isBlob(body)) { + return body.type || null; + } + if (import_node_buffer.Buffer.isBuffer(body) || import_node_util.types.isAnyArrayBuffer(body) || ArrayBuffer.isView(body)) { + return null; + } + if (body instanceof FormData) { + return `multipart/form-data; boundary=${request[INTERNALS].boundary}`; + } + if (body && typeof body.getBoundary === "function") { + return `multipart/form-data;boundary=${getNonSpecFormDataBoundary(body)}`; + } + if (body instanceof import_node_stream.default) { + return null; + } + return "text/plain;charset=UTF-8"; +}; +var getTotalBytes = (request) => { + const { body } = request[INTERNALS]; + if (body === null) { + return 0; + } + if (isBlob(body)) { + return body.size; + } + if (import_node_buffer.Buffer.isBuffer(body)) { + return body.length; + } + if (body && typeof body.getLengthSync === "function") { + return body.hasKnownLength && body.hasKnownLength() ? body.getLengthSync() : null; + } + return null; +}; +var writeToStream = async (dest, { body }) => { + if (body === null) { + dest.end(); + } else { + await pipeline(body, dest); + } +}; + +// ../../node_modules/.pnpm/node-fetch@3.3.2/node_modules/node-fetch/src/headers.js +var import_node_util2 = require("node:util"); +var import_node_http = __toESM(require("node:http"), 1); +var validateHeaderName = typeof import_node_http.default.validateHeaderName === "function" ? import_node_http.default.validateHeaderName : (name) => { + if (!/^[\^`\-\w!#$%&'*+.|~]+$/.test(name)) { + const error = new TypeError(`Header name must be a valid HTTP token [${name}]`); + Object.defineProperty(error, "code", { value: "ERR_INVALID_HTTP_TOKEN" }); + throw error; + } +}; +var validateHeaderValue = typeof import_node_http.default.validateHeaderValue === "function" ? import_node_http.default.validateHeaderValue : (name, value) => { + if (/[^\t\u0020-\u007E\u0080-\u00FF]/.test(value)) { + const error = new TypeError(`Invalid character in header content ["${name}"]`); + Object.defineProperty(error, "code", { value: "ERR_INVALID_CHAR" }); + throw error; + } +}; +var Headers = class _Headers extends URLSearchParams { + /** + * Headers class + * + * @constructor + * @param {HeadersInit} [init] - Response headers + */ + constructor(init) { + let result = []; + if (init instanceof _Headers) { + const raw = init.raw(); + for (const [name, values] of Object.entries(raw)) { + result.push(...values.map((value) => [name, value])); + } + } else if (init == null) { + } else if (typeof init === "object" && !import_node_util2.types.isBoxedPrimitive(init)) { + const method = init[Symbol.iterator]; + if (method == null) { + result.push(...Object.entries(init)); + } else { + if (typeof method !== "function") { + throw new TypeError("Header pairs must be iterable"); + } + result = [...init].map((pair) => { + if (typeof pair !== "object" || import_node_util2.types.isBoxedPrimitive(pair)) { + throw new TypeError("Each header pair must be an iterable object"); + } + return [...pair]; + }).map((pair) => { + if (pair.length !== 2) { + throw new TypeError("Each header pair must be a name/value tuple"); + } + return [...pair]; + }); + } + } else { + throw new TypeError("Failed to construct 'Headers': The provided value is not of type '(sequence> or record)"); + } + result = result.length > 0 ? result.map(([name, value]) => { + validateHeaderName(name); + validateHeaderValue(name, String(value)); + return [String(name).toLowerCase(), String(value)]; + }) : void 0; + super(result); + return new Proxy(this, { + get(target, p, receiver) { + switch (p) { + case "append": + case "set": + return (name, value) => { + validateHeaderName(name); + validateHeaderValue(name, String(value)); + return URLSearchParams.prototype[p].call( + target, + String(name).toLowerCase(), + String(value) + ); + }; + case "delete": + case "has": + case "getAll": + return (name) => { + validateHeaderName(name); + return URLSearchParams.prototype[p].call( + target, + String(name).toLowerCase() + ); + }; + case "keys": + return () => { + target.sort(); + return new Set(URLSearchParams.prototype.keys.call(target)).keys(); + }; + default: + return Reflect.get(target, p, receiver); + } + } + }); + } + get [Symbol.toStringTag]() { + return this.constructor.name; + } + toString() { + return Object.prototype.toString.call(this); + } + get(name) { + const values = this.getAll(name); + if (values.length === 0) { + return null; + } + let value = values.join(", "); + if (/^content-encoding$/i.test(name)) { + value = value.toLowerCase(); + } + return value; + } + forEach(callback, thisArg = void 0) { + for (const name of this.keys()) { + Reflect.apply(callback, thisArg, [this.get(name), name, this]); + } + } + *values() { + for (const name of this.keys()) { + yield this.get(name); + } + } + /** + * @type {() => IterableIterator<[string, string]>} + */ + *entries() { + for (const name of this.keys()) { + yield [name, this.get(name)]; + } + } + [Symbol.iterator]() { + return this.entries(); + } + /** + * Node-fetch non-spec method + * returning all headers and their values as array + * @returns {Record} + */ + raw() { + return [...this.keys()].reduce((result, key) => { + result[key] = this.getAll(key); + return result; + }, {}); + } + /** + * For better console.log(headers) and also to convert Headers into Node.js Request compatible format + */ + [Symbol.for("nodejs.util.inspect.custom")]() { + return [...this.keys()].reduce((result, key) => { + const values = this.getAll(key); + if (key === "host") { + result[key] = values[0]; + } else { + result[key] = values.length > 1 ? values : values[0]; + } + return result; + }, {}); + } +}; +Object.defineProperties( + Headers.prototype, + ["get", "entries", "forEach", "values"].reduce((result, property) => { + result[property] = { enumerable: true }; + return result; + }, {}) +); +function fromRawHeaders(headers = []) { + return new Headers( + headers.reduce((result, value, index, array) => { + if (index % 2 === 0) { + result.push(array.slice(index, index + 2)); + } + return result; + }, []).filter(([name, value]) => { + try { + validateHeaderName(name); + validateHeaderValue(name, String(value)); + return true; + } catch { + return false; + } + }) + ); +} + +// ../../node_modules/.pnpm/node-fetch@3.3.2/node_modules/node-fetch/src/utils/is-redirect.js +var redirectStatus = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]); +var isRedirect = (code) => { + return redirectStatus.has(code); +}; + +// ../../node_modules/.pnpm/node-fetch@3.3.2/node_modules/node-fetch/src/response.js +var INTERNALS2 = Symbol("Response internals"); +var Response = class _Response extends Body { + constructor(body = null, options = {}) { + super(body, options); + const status = options.status != null ? options.status : 200; + const headers = new Headers(options.headers); + if (body !== null && !headers.has("Content-Type")) { + const contentType = extractContentType(body, this); + if (contentType) { + headers.append("Content-Type", contentType); + } + } + this[INTERNALS2] = { + type: "default", + url: options.url, + status, + statusText: options.statusText || "", + headers, + counter: options.counter, + highWaterMark: options.highWaterMark + }; + } + get type() { + return this[INTERNALS2].type; + } + get url() { + return this[INTERNALS2].url || ""; + } + get status() { + return this[INTERNALS2].status; + } + /** + * Convenience property representing if the request ended normally + */ + get ok() { + return this[INTERNALS2].status >= 200 && this[INTERNALS2].status < 300; + } + get redirected() { + return this[INTERNALS2].counter > 0; + } + get statusText() { + return this[INTERNALS2].statusText; + } + get headers() { + return this[INTERNALS2].headers; + } + get highWaterMark() { + return this[INTERNALS2].highWaterMark; + } + /** + * Clone this response + * + * @return Response + */ + clone() { + return new _Response(clone(this, this.highWaterMark), { + type: this.type, + url: this.url, + status: this.status, + statusText: this.statusText, + headers: this.headers, + ok: this.ok, + redirected: this.redirected, + size: this.size, + highWaterMark: this.highWaterMark + }); + } + /** + * @param {string} url The URL that the new response is to originate from. + * @param {number} status An optional status code for the response (e.g., 302.) + * @returns {Response} A Response object. + */ + static redirect(url, status = 302) { + if (!isRedirect(status)) { + throw new RangeError('Failed to execute "redirect" on "response": Invalid status code'); + } + return new _Response(null, { + headers: { + location: new URL(url).toString() + }, + status + }); + } + static error() { + const response = new _Response(null, { status: 0, statusText: "" }); + response[INTERNALS2].type = "error"; + return response; + } + static json(data = void 0, init = {}) { + const body = JSON.stringify(data); + if (body === void 0) { + throw new TypeError("data is not JSON serializable"); + } + const headers = new Headers(init && init.headers); + if (!headers.has("content-type")) { + headers.set("content-type", "application/json"); + } + return new _Response(body, { + ...init, + headers + }); + } + get [Symbol.toStringTag]() { + return "Response"; + } +}; +Object.defineProperties(Response.prototype, { + type: { enumerable: true }, + url: { enumerable: true }, + status: { enumerable: true }, + ok: { enumerable: true }, + redirected: { enumerable: true }, + statusText: { enumerable: true }, + headers: { enumerable: true }, + clone: { enumerable: true } +}); + +// ../../node_modules/.pnpm/node-fetch@3.3.2/node_modules/node-fetch/src/request.js +var import_node_url = require("node:url"); +var import_node_util3 = require("node:util"); + +// ../../node_modules/.pnpm/node-fetch@3.3.2/node_modules/node-fetch/src/utils/get-search.js +var getSearch = (parsedURL) => { + if (parsedURL.search) { + return parsedURL.search; + } + const lastOffset = parsedURL.href.length - 1; + const hash = parsedURL.hash || (parsedURL.href[lastOffset] === "#" ? "#" : ""); + return parsedURL.href[lastOffset - hash.length] === "?" ? "?" : ""; +}; + +// ../../node_modules/.pnpm/node-fetch@3.3.2/node_modules/node-fetch/src/utils/referrer.js +var import_node_net = require("node:net"); +function stripURLForUseAsAReferrer(url, originOnly = false) { + if (url == null) { + return "no-referrer"; + } + url = new URL(url); + if (/^(about|blob|data):$/.test(url.protocol)) { + return "no-referrer"; + } + url.username = ""; + url.password = ""; + url.hash = ""; + if (originOnly) { + url.pathname = ""; + url.search = ""; + } + return url; +} +var ReferrerPolicy = /* @__PURE__ */ new Set([ + "", + "no-referrer", + "no-referrer-when-downgrade", + "same-origin", + "origin", + "strict-origin", + "origin-when-cross-origin", + "strict-origin-when-cross-origin", + "unsafe-url" +]); +var DEFAULT_REFERRER_POLICY = "strict-origin-when-cross-origin"; +function validateReferrerPolicy(referrerPolicy) { + if (!ReferrerPolicy.has(referrerPolicy)) { + throw new TypeError(`Invalid referrerPolicy: ${referrerPolicy}`); + } + return referrerPolicy; +} +function isOriginPotentiallyTrustworthy(url) { + if (/^(http|ws)s:$/.test(url.protocol)) { + return true; + } + const hostIp = url.host.replace(/(^\[)|(]$)/g, ""); + const hostIPVersion = (0, import_node_net.isIP)(hostIp); + if (hostIPVersion === 4 && /^127\./.test(hostIp)) { + return true; + } + if (hostIPVersion === 6 && /^(((0+:){7})|(::(0+:){0,6}))0*1$/.test(hostIp)) { + return true; + } + if (url.host === "localhost" || url.host.endsWith(".localhost")) { + return false; + } + if (url.protocol === "file:") { + return true; + } + return false; +} +function isUrlPotentiallyTrustworthy(url) { + if (/^about:(blank|srcdoc)$/.test(url)) { + return true; + } + if (url.protocol === "data:") { + return true; + } + if (/^(blob|filesystem):$/.test(url.protocol)) { + return true; + } + return isOriginPotentiallyTrustworthy(url); +} +function determineRequestsReferrer(request, { referrerURLCallback, referrerOriginCallback } = {}) { + if (request.referrer === "no-referrer" || request.referrerPolicy === "") { + return null; + } + const policy = request.referrerPolicy; + if (request.referrer === "about:client") { + return "no-referrer"; + } + const referrerSource = request.referrer; + let referrerURL = stripURLForUseAsAReferrer(referrerSource); + let referrerOrigin = stripURLForUseAsAReferrer(referrerSource, true); + if (referrerURL.toString().length > 4096) { + referrerURL = referrerOrigin; + } + if (referrerURLCallback) { + referrerURL = referrerURLCallback(referrerURL); + } + if (referrerOriginCallback) { + referrerOrigin = referrerOriginCallback(referrerOrigin); + } + const currentURL = new URL(request.url); + switch (policy) { + case "no-referrer": + return "no-referrer"; + case "origin": + return referrerOrigin; + case "unsafe-url": + return referrerURL; + case "strict-origin": + if (isUrlPotentiallyTrustworthy(referrerURL) && !isUrlPotentiallyTrustworthy(currentURL)) { + return "no-referrer"; + } + return referrerOrigin.toString(); + case "strict-origin-when-cross-origin": + if (referrerURL.origin === currentURL.origin) { + return referrerURL; + } + if (isUrlPotentiallyTrustworthy(referrerURL) && !isUrlPotentiallyTrustworthy(currentURL)) { + return "no-referrer"; + } + return referrerOrigin; + case "same-origin": + if (referrerURL.origin === currentURL.origin) { + return referrerURL; + } + return "no-referrer"; + case "origin-when-cross-origin": + if (referrerURL.origin === currentURL.origin) { + return referrerURL; + } + return referrerOrigin; + case "no-referrer-when-downgrade": + if (isUrlPotentiallyTrustworthy(referrerURL) && !isUrlPotentiallyTrustworthy(currentURL)) { + return "no-referrer"; + } + return referrerURL; + default: + throw new TypeError(`Invalid referrerPolicy: ${policy}`); + } +} +function parseReferrerPolicyFromHeader(headers) { + const policyTokens = (headers.get("referrer-policy") || "").split(/[,\s]+/); + let policy = ""; + for (const token of policyTokens) { + if (token && ReferrerPolicy.has(token)) { + policy = token; + } + } + return policy; +} + +// ../../node_modules/.pnpm/node-fetch@3.3.2/node_modules/node-fetch/src/request.js +var INTERNALS3 = Symbol("Request internals"); +var isRequest = (object) => { + return typeof object === "object" && typeof object[INTERNALS3] === "object"; +}; +var doBadDataWarn = (0, import_node_util3.deprecate)( + () => { + }, + ".data is not a valid RequestInit property, use .body instead", + "https://github.com/node-fetch/node-fetch/issues/1000 (request)" +); +var Request = class _Request extends Body { + constructor(input, init = {}) { + let parsedURL; + if (isRequest(input)) { + parsedURL = new URL(input.url); + } else { + parsedURL = new URL(input); + input = {}; + } + if (parsedURL.username !== "" || parsedURL.password !== "") { + throw new TypeError(`${parsedURL} is an url with embedded credentials.`); + } + let method = init.method || input.method || "GET"; + if (/^(delete|get|head|options|post|put)$/i.test(method)) { + method = method.toUpperCase(); + } + if (!isRequest(init) && "data" in init) { + doBadDataWarn(); + } + if ((init.body != null || isRequest(input) && input.body !== null) && (method === "GET" || method === "HEAD")) { + throw new TypeError("Request with GET/HEAD method cannot have body"); + } + const inputBody = init.body ? init.body : isRequest(input) && input.body !== null ? clone(input) : null; + super(inputBody, { + size: init.size || input.size || 0 + }); + const headers = new Headers(init.headers || input.headers || {}); + if (inputBody !== null && !headers.has("Content-Type")) { + const contentType = extractContentType(inputBody, this); + if (contentType) { + headers.set("Content-Type", contentType); + } + } + let signal = isRequest(input) ? input.signal : null; + if ("signal" in init) { + signal = init.signal; + } + if (signal != null && !isAbortSignal(signal)) { + throw new TypeError("Expected signal to be an instanceof AbortSignal or EventTarget"); + } + let referrer = init.referrer == null ? input.referrer : init.referrer; + if (referrer === "") { + referrer = "no-referrer"; + } else if (referrer) { + const parsedReferrer = new URL(referrer); + referrer = /^about:(\/\/)?client$/.test(parsedReferrer) ? "client" : parsedReferrer; + } else { + referrer = void 0; + } + this[INTERNALS3] = { + method, + redirect: init.redirect || input.redirect || "follow", + headers, + parsedURL, + signal, + referrer + }; + this.follow = init.follow === void 0 ? input.follow === void 0 ? 20 : input.follow : init.follow; + this.compress = init.compress === void 0 ? input.compress === void 0 ? true : input.compress : init.compress; + this.counter = init.counter || input.counter || 0; + this.agent = init.agent || input.agent; + this.highWaterMark = init.highWaterMark || input.highWaterMark || 16384; + this.insecureHTTPParser = init.insecureHTTPParser || input.insecureHTTPParser || false; + this.referrerPolicy = init.referrerPolicy || input.referrerPolicy || ""; + } + /** @returns {string} */ + get method() { + return this[INTERNALS3].method; + } + /** @returns {string} */ + get url() { + return (0, import_node_url.format)(this[INTERNALS3].parsedURL); + } + /** @returns {Headers} */ + get headers() { + return this[INTERNALS3].headers; + } + get redirect() { + return this[INTERNALS3].redirect; + } + /** @returns {AbortSignal} */ + get signal() { + return this[INTERNALS3].signal; + } + // https://fetch.spec.whatwg.org/#dom-request-referrer + get referrer() { + if (this[INTERNALS3].referrer === "no-referrer") { + return ""; + } + if (this[INTERNALS3].referrer === "client") { + return "about:client"; + } + if (this[INTERNALS3].referrer) { + return this[INTERNALS3].referrer.toString(); + } + return void 0; + } + get referrerPolicy() { + return this[INTERNALS3].referrerPolicy; + } + set referrerPolicy(referrerPolicy) { + this[INTERNALS3].referrerPolicy = validateReferrerPolicy(referrerPolicy); + } + /** + * Clone this request + * + * @return Request + */ + clone() { + return new _Request(this); + } + get [Symbol.toStringTag]() { + return "Request"; + } +}; +Object.defineProperties(Request.prototype, { + method: { enumerable: true }, + url: { enumerable: true }, + headers: { enumerable: true }, + redirect: { enumerable: true }, + clone: { enumerable: true }, + signal: { enumerable: true }, + referrer: { enumerable: true }, + referrerPolicy: { enumerable: true } +}); +var getNodeRequestOptions = (request) => { + const { parsedURL } = request[INTERNALS3]; + const headers = new Headers(request[INTERNALS3].headers); + if (!headers.has("Accept")) { + headers.set("Accept", "*/*"); + } + let contentLengthValue = null; + if (request.body === null && /^(post|put)$/i.test(request.method)) { + contentLengthValue = "0"; + } + if (request.body !== null) { + const totalBytes = getTotalBytes(request); + if (typeof totalBytes === "number" && !Number.isNaN(totalBytes)) { + contentLengthValue = String(totalBytes); + } + } + if (contentLengthValue) { + headers.set("Content-Length", contentLengthValue); + } + if (request.referrerPolicy === "") { + request.referrerPolicy = DEFAULT_REFERRER_POLICY; + } + if (request.referrer && request.referrer !== "no-referrer") { + request[INTERNALS3].referrer = determineRequestsReferrer(request); + } else { + request[INTERNALS3].referrer = "no-referrer"; + } + if (request[INTERNALS3].referrer instanceof URL) { + headers.set("Referer", request.referrer); + } + if (!headers.has("User-Agent")) { + headers.set("User-Agent", "node-fetch"); + } + if (request.compress && !headers.has("Accept-Encoding")) { + headers.set("Accept-Encoding", "gzip, deflate, br"); + } + let { agent } = request; + if (typeof agent === "function") { + agent = agent(parsedURL); + } + const search = getSearch(parsedURL); + const options = { + // Overwrite search to retain trailing ? (issue #776) + path: parsedURL.pathname + search, + // The following options are not expressed in the URL + method: request.method, + headers: headers[Symbol.for("nodejs.util.inspect.custom")](), + insecureHTTPParser: request.insecureHTTPParser, + agent + }; + return { + /** @type {URL} */ + parsedURL, + options + }; +}; + +// ../../node_modules/.pnpm/node-fetch@3.3.2/node_modules/node-fetch/src/errors/abort-error.js +var AbortError = class extends FetchBaseError { + constructor(message, type = "aborted") { + super(message, type); + } +}; + +// ../../node_modules/.pnpm/node-fetch@3.3.2/node_modules/node-fetch/src/index.js +init_esm_min(); +init_from(); +var supportedSchemas = /* @__PURE__ */ new Set(["data:", "http:", "https:"]); +async function fetch(url, options_) { + return new Promise((resolve, reject) => { + const request = new Request(url, options_); + const { parsedURL, options } = getNodeRequestOptions(request); + if (!supportedSchemas.has(parsedURL.protocol)) { + throw new TypeError(`node-fetch cannot load ${url}. URL scheme "${parsedURL.protocol.replace(/:$/, "")}" is not supported.`); + } + if (parsedURL.protocol === "data:") { + const data = dist_default(request.url); + const response2 = new Response(data, { headers: { "Content-Type": data.typeFull } }); + resolve(response2); + return; + } + const send = (parsedURL.protocol === "https:" ? import_node_https.default : import_node_http2.default).request; + const { signal } = request; + let response = null; + const abort = () => { + const error = new AbortError("The operation was aborted."); + reject(error); + if (request.body && request.body instanceof import_node_stream2.default.Readable) { + request.body.destroy(error); + } + if (!response || !response.body) { + return; + } + response.body.emit("error", error); + }; + if (signal && signal.aborted) { + abort(); + return; + } + const abortAndFinalize = () => { + abort(); + finalize(); + }; + const request_ = send(parsedURL.toString(), options); + if (signal) { + signal.addEventListener("abort", abortAndFinalize); + } + const finalize = () => { + request_.abort(); + if (signal) { + signal.removeEventListener("abort", abortAndFinalize); + } + }; + request_.on("error", (error) => { + reject(new FetchError(`request to ${request.url} failed, reason: ${error.message}`, "system", error)); + finalize(); + }); + fixResponseChunkedTransferBadEnding(request_, (error) => { + if (response && response.body) { + response.body.destroy(error); + } + }); + if (process.version < "v14") { + request_.on("socket", (s2) => { + let endedWithEventsCount; + s2.prependListener("end", () => { + endedWithEventsCount = s2._eventsCount; + }); + s2.prependListener("close", (hadError) => { + if (response && endedWithEventsCount < s2._eventsCount && !hadError) { + const error = new Error("Premature close"); + error.code = "ERR_STREAM_PREMATURE_CLOSE"; + response.body.emit("error", error); + } + }); + }); + } + request_.on("response", (response_) => { + request_.setTimeout(0); + const headers = fromRawHeaders(response_.rawHeaders); + if (isRedirect(response_.statusCode)) { + const location = headers.get("Location"); + let locationURL = null; + try { + locationURL = location === null ? null : new URL(location, request.url); + } catch { + if (request.redirect !== "manual") { + reject(new FetchError(`uri requested responds with an invalid redirect URL: ${location}`, "invalid-redirect")); + finalize(); + return; + } + } + switch (request.redirect) { + case "error": + reject(new FetchError(`uri requested responds with a redirect, redirect mode is set to error: ${request.url}`, "no-redirect")); + finalize(); + return; + case "manual": + break; + case "follow": { + if (locationURL === null) { + break; + } + if (request.counter >= request.follow) { + reject(new FetchError(`maximum redirect reached at: ${request.url}`, "max-redirect")); + finalize(); + return; + } + const requestOptions = { + headers: new Headers(request.headers), + follow: request.follow, + counter: request.counter + 1, + agent: request.agent, + compress: request.compress, + method: request.method, + body: clone(request), + signal: request.signal, + size: request.size, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy + }; + if (!isDomainOrSubdomain(request.url, locationURL) || !isSameProtocol(request.url, locationURL)) { + for (const name of ["authorization", "www-authenticate", "cookie", "cookie2"]) { + requestOptions.headers.delete(name); + } + } + if (response_.statusCode !== 303 && request.body && options_.body instanceof import_node_stream2.default.Readable) { + reject(new FetchError("Cannot follow redirect with body being a readable stream", "unsupported-redirect")); + finalize(); + return; + } + if (response_.statusCode === 303 || (response_.statusCode === 301 || response_.statusCode === 302) && request.method === "POST") { + requestOptions.method = "GET"; + requestOptions.body = void 0; + requestOptions.headers.delete("content-length"); + } + const responseReferrerPolicy = parseReferrerPolicyFromHeader(headers); + if (responseReferrerPolicy) { + requestOptions.referrerPolicy = responseReferrerPolicy; + } + resolve(fetch(new Request(locationURL, requestOptions))); + finalize(); + return; + } + default: + return reject(new TypeError(`Redirect option '${request.redirect}' is not a valid value of RequestRedirect`)); + } + } + if (signal) { + response_.once("end", () => { + signal.removeEventListener("abort", abortAndFinalize); + }); + } + let body = (0, import_node_stream2.pipeline)(response_, new import_node_stream2.PassThrough(), (error) => { + if (error) { + reject(error); + } + }); + if (process.version < "v12.10") { + response_.on("aborted", abortAndFinalize); + } + const responseOptions = { + url: request.url, + status: response_.statusCode, + statusText: response_.statusMessage, + headers, + size: request.size, + counter: request.counter, + highWaterMark: request.highWaterMark + }; + const codings = headers.get("Content-Encoding"); + if (!request.compress || request.method === "HEAD" || codings === null || response_.statusCode === 204 || response_.statusCode === 304) { + response = new Response(body, responseOptions); + resolve(response); + return; + } + const zlibOptions = { + flush: import_node_zlib.default.Z_SYNC_FLUSH, + finishFlush: import_node_zlib.default.Z_SYNC_FLUSH + }; + if (codings === "gzip" || codings === "x-gzip") { + body = (0, import_node_stream2.pipeline)(body, import_node_zlib.default.createGunzip(zlibOptions), (error) => { + if (error) { + reject(error); + } + }); + response = new Response(body, responseOptions); + resolve(response); + return; + } + if (codings === "deflate" || codings === "x-deflate") { + const raw = (0, import_node_stream2.pipeline)(response_, new import_node_stream2.PassThrough(), (error) => { + if (error) { + reject(error); + } + }); + raw.once("data", (chunk) => { + if ((chunk[0] & 15) === 8) { + body = (0, import_node_stream2.pipeline)(body, import_node_zlib.default.createInflate(), (error) => { + if (error) { + reject(error); + } + }); + } else { + body = (0, import_node_stream2.pipeline)(body, import_node_zlib.default.createInflateRaw(), (error) => { + if (error) { + reject(error); + } + }); + } + response = new Response(body, responseOptions); + resolve(response); + }); + raw.once("end", () => { + if (!response) { + response = new Response(body, responseOptions); + resolve(response); + } + }); + return; + } + if (codings === "br") { + body = (0, import_node_stream2.pipeline)(body, import_node_zlib.default.createBrotliDecompress(), (error) => { + if (error) { + reject(error); + } + }); + response = new Response(body, responseOptions); + resolve(response); + return; + } + response = new Response(body, responseOptions); + resolve(response); + }); + writeToStream(request_, request).catch(reject); + }); +} +function fixResponseChunkedTransferBadEnding(request, errorCallback) { + const LAST_CHUNK = import_node_buffer2.Buffer.from("0\r\n\r\n"); + let isChunkedTransfer = false; + let properLastChunkReceived = false; + let previousChunk; + request.on("response", (response) => { + const { headers } = response; + isChunkedTransfer = headers["transfer-encoding"] === "chunked" && !headers["content-length"]; + }); + request.on("socket", (socket) => { + const onSocketClose = () => { + if (isChunkedTransfer && !properLastChunkReceived) { + const error = new Error("Premature close"); + error.code = "ERR_STREAM_PREMATURE_CLOSE"; + errorCallback(error); + } + }; + const onData = (buf) => { + properLastChunkReceived = import_node_buffer2.Buffer.compare(buf.slice(-5), LAST_CHUNK) === 0; + if (!properLastChunkReceived && previousChunk) { + properLastChunkReceived = import_node_buffer2.Buffer.compare(previousChunk.slice(-3), LAST_CHUNK.slice(0, 3)) === 0 && import_node_buffer2.Buffer.compare(buf.slice(-2), LAST_CHUNK.slice(3)) === 0; + } + previousChunk = buf; + }; + socket.prependListener("close", onSocketClose); + socket.on("data", onData); + request.on("close", () => { + socket.removeListener("close", onSocketClose); + socket.removeListener("data", onData); + }); + }); +} + +// src/utils/versioncheck.ts +var checkVersion = async () => { + const currentVersion = GetResourceMetadata(cache.resource, "version", 0); + if (!currentVersion) { + console.error("[YaCA] Version check failed, no version found in resource manifest."); + return; + } + const parsedVersion = currentVersion.match(/\d+\.\d+\.\d+/g); + if (!parsedVersion) { + console.error("[YaCA] Version check failed, version in resource manifest is not in the correct format."); + return; + } + const response = await fetch("https://api.github.com/repos/yaca-systems/fivem-yaca-typescript/releases/latest"); + if (response.status !== 200) { + console.error("[YaCA] Version check failed, unable to fetch latest release."); + return; + } + const data = await response.json(); + const latestVersion = data.tag_name; + if (!latestVersion && latestVersion === currentVersion) { + console.log("[YaCA] You are running the latest version of YaCA."); + return; + } + const parsedLatestVersion = latestVersion.match(/\d+\.\d+\.\d+/g); + if (!parsedLatestVersion) { + console.error("[YaCA] Version check failed, latest release is not in the correct format."); + return; + } + for (let i2 = 0; i2 < parsedVersion.length; i2++) { + const current = Number.parseInt(parsedVersion[i2]); + const latest = Number.parseInt(parsedLatestVersion[i2]); + if (current !== latest) { + if (current < latest) { + console.error( + `[YaCA] You are running an outdated version of YaCA. (current: ${currentVersion}, latest: ${latestVersion}) \r + ${data.html_url}` + ); + } else { + break; + } + } + } +}; + +// src/bridge/saltychat.ts +var YaCAServerSaltyChatBridge = class { + /** + * Creates an instance of the SaltyChat bridge. + * + * @param {YaCAServerModule} serverModule - The server module. + */ + constructor(serverModule) { + this.callMap = /* @__PURE__ */ new Map(); + this.serverModule = serverModule; + this.registerSaltyChatEvents(); + console.log("[YaCA] SaltyChat bridge loaded"); + on("onResourceStop", (resourceName2) => { + if (cache.resource !== resourceName2) { + return; + } + emit("onServerResourceStop", "saltychat"); + }); + } + /** + * Register SaltyChat events. + */ + registerSaltyChatEvents() { + saltyChatExport("GetPlayerAlive", (netId) => { + this.serverModule.getPlayerAliveStatus(netId); + }); + saltyChatExport("SetPlayerAlive", (netId, isAlive) => { + this.serverModule.changePlayerAliveStatus(netId, isAlive); + }); + saltyChatExport("GetPlayerVoiceRange", (netId) => { + this.serverModule.getPlayerVoiceRange(netId); + }); + saltyChatExport("SetPlayerVoiceRange", (netId, voiceRange) => { + this.serverModule.changeVoiceRange(netId, voiceRange); + }); + saltyChatExport("AddPlayerToCall", (callIdentifier, playerHandle) => this.addPlayerToCall(callIdentifier, playerHandle)); + saltyChatExport("AddPlayersToCall", (callIdentifier, playerHandles) => this.addPlayerToCall(callIdentifier, playerHandles)); + saltyChatExport("RemovePlayerFromCall", (callIdentifier, playerHandle) => this.removePlayerFromCall(callIdentifier, playerHandle)); + saltyChatExport("RemovePlayersFromCall", (callIdentifier, playerHandles) => this.removePlayerFromCall(callIdentifier, playerHandles)); + saltyChatExport("SetPhoneSpeaker", (playerHandle, toggle) => { + this.serverModule.phoneModule.enablePhoneSpeaker(playerHandle, toggle); + }); + saltyChatExport("SetPlayerRadioSpeaker", () => { + console.warn("SetPlayerRadioSpeaker is not implemented in YaCA"); + }); + saltyChatExport("GetPlayersInRadioChannel", (radioChannelName) => this.serverModule.radioModule.getPlayersInRadioFrequency(radioChannelName)); + saltyChatExport("SetPlayerRadioChannel", (netId, radioChannelName, primary = true) => { + const channel = primary ? 1 : 2; + const newRadioChannelName = radioChannelName === "" ? "0" : radioChannelName; + this.serverModule.radioModule.changeRadioFrequency(netId, channel, newRadioChannelName); + }); + saltyChatExport("RemovePlayerRadioChannel", (netId, primary) => { + const channel = primary ? 1 : 2; + this.serverModule.radioModule.changeRadioFrequency(netId, channel, "0"); + }); + saltyChatExport("SetRadioTowers", () => { + console.warn("SetRadioTowers is not implemented in YaCA"); + }); + saltyChatExport("EstablishCall", (callerId, targetId) => { + this.serverModule.phoneModule.callPlayer(callerId, targetId, true); + }); + saltyChatExport("EndCall", (callerId, targetId) => { + this.serverModule.phoneModule.callPlayer(callerId, targetId, false); + }); + } + /** + * Add a player to a call. + * + * @param callIdentifier - The call identifier. + * @param playerHandle - The player handles. + */ + addPlayerToCall(callIdentifier, playerHandle) { + if (!Array.isArray(playerHandle)) { + playerHandle = [playerHandle]; + } + const currentlyInCall = this.callMap.get(callIdentifier) ?? /* @__PURE__ */ new Set(); + const newInCall = /* @__PURE__ */ new Set(); + for (const player of playerHandle) { + if (!currentlyInCall.has(player)) { + currentlyInCall.add(player); + newInCall.add(player); + } + } + this.callMap.set(callIdentifier, currentlyInCall); + for (const player of currentlyInCall) { + for (const otherPlayer of newInCall) { + if (player !== otherPlayer) { + this.serverModule.phoneModule.callPlayer(player, otherPlayer, true); + } + } + } + } + /** + * Remove a player from a call. + * + * @param callIdentifier - The call identifier. + * @param playerHandle - The player handles. + */ + removePlayerFromCall(callIdentifier, playerHandle) { + if (!Array.isArray(playerHandle)) { + playerHandle = [playerHandle]; + } + const beforeInCall = this.callMap.get(callIdentifier); + if (!beforeInCall) { + return; + } + const nowInCall = new Set(beforeInCall); + const removedFromCall = /* @__PURE__ */ new Set(); + for (const player of playerHandle) { + if (beforeInCall.has(player)) { + nowInCall.delete(player); + removedFromCall.add(player); + } + } + this.callMap.set(callIdentifier, nowInCall); + for (const player of removedFromCall) { + for (const otherPlayer of beforeInCall) { + if (player !== otherPlayer) { + this.serverModule.phoneModule.callPlayer(player, otherPlayer, false); + } + } + } + } +}; + +// src/yaca/megaphone.ts +var YaCAServerMegaphoneModule = class { + /** + * Creates an instance of the megaphone module. + * + * @param serverModule - The server module. + */ + constructor(serverModule) { + this.serverModule = serverModule; + this.sharedConfig = serverModule.sharedConfig; + this.registerEvents(); + } + /** + * Register server events. + */ + registerEvents() { + onNet("server:yaca:useMegaphone", (state) => { + this.playerUseMegaphone(source, state); + }); + onNet("server:yaca:playerLeftVehicle", () => { + this.changeMegaphoneState(source, false, true); + }); + } + /** + * Apply the megaphone effect on a specific player via client event. + * + * @param {number} src - The source-id of the player to apply the megaphone effect to. + * @param {boolean} state - The state of the megaphone effect. + */ + playerUseMegaphone(src, state) { + const player = this.serverModule.getPlayer(src); + if (!player) { + return; + } + const playerState = Player(src).state; + if (!state && !playerState[MEGAPHONE_STATE_NAME] || state && playerState[MEGAPHONE_STATE_NAME]) { + return; + } + this.changeMegaphoneState(src, state); + emit("yaca:external:changeMegaphoneState", src, state); + } + /** + * Apply the megaphone effect on a specific player. + * + * @param {number} src - The source-id of the player to apply the megaphone effect to. + * @param {boolean} state - The state of the megaphone effect. + * @param {boolean} [forced=false] - Whether the change is forced. Defaults to false if not provided. + */ + changeMegaphoneState(src, state, forced = false) { + const playerState = Player(src).state; + if (!state && playerState[MEGAPHONE_STATE_NAME]) { + playerState.set(MEGAPHONE_STATE_NAME, null, true); + if (forced) { + emitNet("client:yaca:setLastMegaphoneState", src, false); + } + } else if (state && !playerState[MEGAPHONE_STATE_NAME]) { + playerState.set(MEGAPHONE_STATE_NAME, this.sharedConfig.megaphone.range, true); + } + } +}; + +// src/yaca/phone.ts +var YaCAServerPhoneModle = class { + /** + * Creates an instance of the phone module. + * + * @param {YaCAServerModule} serverModule - The server module. + */ + constructor(serverModule) { + this.serverModule = serverModule; + this.registerEvents(); + this.registerExports(); + } + /** + * Register server events. + */ + registerEvents() { + onNet("server:yaca:phoneSpeakerEmitWhisper", (enableForTargets, disableForTargets) => { + const player = this.serverModule.players.get(source); + if (!player) { + return; + } + const targets = /* @__PURE__ */ new Set(); + for (const callTarget of player.voiceSettings.inCallWith) { + const target = this.serverModule.players.get(callTarget); + if (!target) { + continue; + } + targets.add(callTarget); + } + if (targets.size && (enableForTargets == null ? void 0 : enableForTargets.length)) { + triggerClientEvent("client:yaca:playersToPhoneSpeakerEmitWhisper", Array.from(targets), enableForTargets, true); + } + if (targets.size && (disableForTargets == null ? void 0 : disableForTargets.length)) { + triggerClientEvent("client:yaca:playersToPhoneSpeakerEmitWhisper", Array.from(targets), disableForTargets, false); + } + }); + onNet("server:yaca:phoneEmit", (enableForTargets, disableForTargets) => { + if (!this.serverModule.sharedConfig.phoneHearPlayersNearby) return; + const player = this.serverModule.players.get(source); + if (!player) { + return; + } + const enableReceive = /* @__PURE__ */ new Set(); + const disableReceive = /* @__PURE__ */ new Set(); + if (enableForTargets == null ? void 0 : enableForTargets.length) { + for (const callTarget of player.voiceSettings.inCallWith) { + const target = this.serverModule.players.get(callTarget); + if (!target) continue; + enableReceive.add(callTarget); + for (const targetID of enableForTargets) { + const map = player.voiceSettings.emittedPhoneSpeaker; + const set = map.get(targetID) ?? /* @__PURE__ */ new Set(); + set.add(callTarget); + map.set(targetID, set); + } + } + } + if (disableForTargets == null ? void 0 : disableForTargets.length) { + for (const targetID of disableForTargets) { + const emittedFor = player.voiceSettings.emittedPhoneSpeaker.get(targetID); + if (!emittedFor) continue; + for (const emittedTarget of emittedFor) { + const target = this.serverModule.players.get(emittedTarget); + if (!target) continue; + disableReceive.add(emittedTarget); + } + player.voiceSettings.emittedPhoneSpeaker.delete(targetID); + } + } + if (enableReceive.size && (enableForTargets == null ? void 0 : enableForTargets.length)) { + const enableForTargetsData = /* @__PURE__ */ new Set(); + for (const enableTarget of enableForTargets) { + const target = this.serverModule.players.get(enableTarget); + if (!target || !target.voicePlugin) continue; + enableForTargetsData.add(target.voicePlugin.clientId); + } + triggerClientEvent("client:yaca:phoneHearAround", Array.from(enableReceive), Array.from(enableForTargetsData), true); + } + if (disableReceive.size && (disableForTargets == null ? void 0 : disableForTargets.length)) { + const disableForTargetsData = /* @__PURE__ */ new Set(); + for (const disableTarget of disableForTargets) { + const target = this.serverModule.players.get(disableTarget); + if (!target || !target.voicePlugin) continue; + disableForTargetsData.add(target.voicePlugin.clientId); + } + triggerClientEvent("client:yaca:phoneHearAround", Array.from(disableReceive), Array.from(disableForTargetsData), false); + } + }); + } + registerExports() { + exports("callPlayer", (src, target, state) => this.callPlayer(src, target, state)); + exports("callPlayerOldEffect", (src, target, state) => this.callPlayer(src, target, state, "PHONE_HISTORICAL" /* PHONE_HISTORICAL */)); + exports("muteOnPhone", (src, state) => this.muteOnPhone(src, state)); + exports("enablePhoneSpeaker", (src, state) => this.enablePhoneSpeaker(src, state)); + exports("isPlayerInCall", (src) => { + const player = this.serverModule.players.get(src); + if (!player) { + return [false, []]; + } + return [player.voiceSettings.inCallWith.size > 0, [...player.voiceSettings.inCallWith]]; + }); + } + /** + * Call another player. + * + * @param {number} src - The player who is making the call. + * @param {number} target - The player who is being called. + * @param {boolean} state - The state of the call. + * @param {YacaFilterEnum} filter - The filter to use for the call. Defaults to PHONE if not provided. + */ + callPlayer(src, target, state, filter = "PHONE" /* PHONE */) { + const player = this.serverModule.getPlayer(src); + const targetPlayer = this.serverModule.getPlayer(target); + if (!player || !targetPlayer) { + return; + } + emitNet("client:yaca:phone", target, src, state, filter); + emitNet("client:yaca:phone", src, target, state, filter); + const playerState = Player(src).state; + const targetState = Player(target).state; + if (state) { + player.voiceSettings.inCallWith.add(target); + targetPlayer.voiceSettings.inCallWith.add(src); + if (playerState[PHONE_SPEAKER_STATE_NAME]) { + this.enablePhoneSpeaker(src, true); + } + if (targetState[PHONE_SPEAKER_STATE_NAME]) { + this.enablePhoneSpeaker(target, true); + } + } else { + this.muteOnPhone(src, false, true); + this.muteOnPhone(target, false, true); + player.voiceSettings.inCallWith.delete(target); + targetPlayer.voiceSettings.inCallWith.delete(src); + if (playerState[PHONE_SPEAKER_STATE_NAME]) { + this.enablePhoneSpeaker(src, false); + } + if (targetState[PHONE_SPEAKER_STATE_NAME]) { + this.enablePhoneSpeaker(target, false); + } + } + emit("yaca:external:phoneCall", src, target, state, filter); + } + /** + * Mute a player during a phone call. + * + * @param {number} src - The source-id of the player to mute. + * @param {boolean} state - The mute state. + * @param {boolean} [onCallStop=false] - Whether the call has stopped. Defaults to false if not provided. + */ + muteOnPhone(src, state, onCallStop = false) { + const player = this.serverModule.getPlayer(src); + if (!player) { + return; + } + player.voiceSettings.mutedOnPhone = state; + emitNet("client:yaca:phoneMute", -1, src, state, onCallStop); + emit("yaca:external:phoneMute", src, state); + } + /** + * Enable or disable the phone speaker for a player. + * + * @param {number} src - The source-id of the player to enable the phone speaker for. + * @param {boolean} state - The state of the phone speaker. + */ + enablePhoneSpeaker(src, state) { + const player = this.serverModule.getPlayer(src); + if (!player) { + return; + } + const playerState = Player(src).state; + if (state && player.voiceSettings.inCallWith.size) { + playerState.set(PHONE_SPEAKER_STATE_NAME, Array.from(player.voiceSettings.inCallWith), true); + emit("yaca:external:phoneSpeaker", src, true); + } else { + playerState.set(PHONE_SPEAKER_STATE_NAME, null, true); + emit("yaca:external:phoneSpeaker", src, false); + } + } +}; + +// src/yaca/radio.ts +var YaCAServerRadioModule = class { + /** + * Creates an instance of the radio module. + * + * @param {YaCAServerModule} serverModule - The server module. + */ + constructor(serverModule) { + this.radioFrequencyMap = /* @__PURE__ */ new Map(); + this.serverModule = serverModule; + this.sharedConfig = serverModule.sharedConfig; + this.serverConfig = serverModule.serverConfig; + this.registerEvents(); + this.registerExports(); + } + /** + * Register server events. + */ + registerEvents() { + onNet("server:yaca:enableRadio", (state) => { + this.enableRadio(source, state); + }); + onNet("server:yaca:changeRadioFrequency", (channel, frequency) => { + this.changeRadioFrequency(source, channel, frequency); + }); + onNet("server:yaca:muteRadioChannel", (channel, state) => { + this.radioChannelMute(source, channel, state); + }); + onNet("server:yaca:radioTalking", (state, channel, distanceToTower = -1) => { + this.radioTalkingState(source, state, channel, distanceToTower); + }); + } + /** + * Register server exports. + */ + registerExports() { + exports("getPlayersInRadioFrequency", (frequency) => this.getPlayersInRadioFrequency(frequency)); + exports("setPlayerRadioChannel", (src, channel, frequency) => this.changeRadioFrequency(src, channel, frequency)); + exports("getPlayerHasLongRange", (src) => this.getPlayerHasLongRange(src)); + exports("setPlayerHasLongRange", (src, state) => this.setPlayerHasLongRange(src, state)); + } + /** + * Get all players in a radio frequency. + * + * @param frequency - The frequency to get the players for. + */ + getPlayersInRadioFrequency(frequency) { + const allPlayersInChannel = this.radioFrequencyMap.get(frequency); + const playersArray = []; + if (!allPlayersInChannel) { + return playersArray; + } + for (const [key] of allPlayersInChannel) { + const target = this.serverModule.getPlayer(key); + if (!target) { + continue; + } + playersArray.push(key); + } + return playersArray; + } + /** + * Gets if a player has long range radio. + * + * @param src - The player to get the long range radio for. + */ + getPlayerHasLongRange(src) { + const player = this.serverModule.getPlayer(src); + if (!player) { + return false; + } + return player.radioSettings.hasLong; + } + /** + * Sets if a player has long range radio. + * + * @param src - The player to set the long range radio for. + * @param state - The new state of the long range radio. + */ + setPlayerHasLongRange(src, state) { + const player = this.serverModule.getPlayer(src); + if (!player) { + return; + } + player.radioSettings.hasLong = state; + } + /** + * Enable or disable the radio for a player. + * + * @param {number} src - The player to enable or disable the radio for. + * @param {boolean} state - The new state of the radio. + */ + enableRadio(src, state) { + const player = this.serverModule.getPlayer(src); + if (!player) { + return; + } + player.radioSettings.activated = state; + emit("yaca:export:enabledRadio", src, state); + } + /** + * Change the radio frequency for a player. + * + * @param {number} src - The player to change the radio frequency for. + * @param {number} channel - The channel to change the frequency of. + * @param {string} frequency - The new frequency. + */ + changeRadioFrequency(src, channel, frequency) { + var _a; + const player = this.serverModule.getPlayer(src); + if (!player) { + return; + } + if (!player.radioSettings.activated) { + emitNet("client:yaca:notification", src, locale("radio_not_activated"), "error" /* ERROR */); + return; + } + if (Number.isNaN(channel) || channel < 1 || channel > this.sharedConfig.radioSettings.channelCount) { + emitNet("client:yaca:notification", src, locale("radio_channel_invalid"), "error" /* ERROR */); + return; + } + const oldFrequency = player.radioSettings.frequencies[channel]; + if (frequency === "0") { + this.leaveRadioFrequency(src, channel, oldFrequency); + return; + } + if (oldFrequency !== frequency) { + this.leaveRadioFrequency(src, channel, oldFrequency); + } + if (!this.radioFrequencyMap.has(frequency)) { + this.radioFrequencyMap.set(frequency, /* @__PURE__ */ new Map()); + } + (_a = this.radioFrequencyMap.get(frequency)) == null ? void 0 : _a.set(src, { muted: false }); + player.radioSettings.frequencies[channel] = frequency; + emitNet("client:yaca:setRadioFreq", src, channel, frequency); + emit("yaca:external:changedRadioFrequency", src, channel, frequency); + } + /** + * Make a player leave a radio frequency. + * + * @param {number} src - The player to leave the radio frequency. + * @param {number} channel - The channel to leave. + * @param {string} frequency - The frequency to leave. + */ + leaveRadioFrequency(src, channel, frequency) { + const player = this.serverModule.getPlayer(src); + if (!player) { + return; + } + const allPlayersInChannel = this.radioFrequencyMap.get(frequency); + if (!allPlayersInChannel) { + return; + } + player.radioSettings.frequencies[channel] = "0"; + const playersArray = []; + const allTargets = []; + for (const [key] of allPlayersInChannel) { + const target = this.serverModule.getPlayer(key); + if (!target) { + continue; + } + playersArray.push(key); + if (key === src) { + continue; + } + allTargets.push(key); + } + if (this.serverConfig.useWhisper) { + emitNet("client:yaca:radioTalking", src, allTargets, frequency, false, null, true); + } else if (player.voicePlugin) { + triggerClientEvent("client:yaca:leaveRadioChannel", playersArray, player.voicePlugin.clientId, frequency); + } + allPlayersInChannel.delete(src); + if (!allPlayersInChannel.size) { + this.radioFrequencyMap.delete(frequency); + } + } + /** + * Mute a radio channel for a player. + * + * @param {number} src - The player to mute the radio channel for. + * @param {number} channel - The channel to mute. + */ + radioChannelMute(src, channel, state) { + var _a; + const player = this.serverModule.getPlayer(src); + if (!player) { + return; + } + const radioFrequency = player.radioSettings.frequencies[channel]; + const foundPlayer = (_a = this.radioFrequencyMap.get(radioFrequency)) == null ? void 0 : _a.get(src); + if (!foundPlayer) { + return; + } + foundPlayer.muted = typeof state !== "undefined" ? state : !foundPlayer.muted; + emitNet("client:yaca:setRadioMuteState", src, channel, foundPlayer.muted); + emit("yaca:external:changedRadioMuteState", src, channel, foundPlayer.muted); + } + /** + * Change the talking state of a player on the radio. + * + * @param {number} src - The player to change the talking state for. + * @param {boolean} state - The new talking state. + * @param {number} channel - The channel to change the talking state for. + * @param {number} distanceToTower - The distance to the tower. + */ + radioTalkingState(src, state, channel, distanceToTower) { + const player = this.serverModule.getPlayer(src); + if (!player || !player.radioSettings.activated) { + return; + } + const radioFrequency = player.radioSettings.frequencies[channel]; + if (!radioFrequency || radioFrequency === "0") { + return; + } + const getPlayers2 = this.radioFrequencyMap.get(radioFrequency); + if (!getPlayers2) { + return; + } + let targets = []; + const targetsToSender = []; + const radioInfos = {}; + for (const [key, values] of getPlayers2) { + if (values.muted) { + if (key === src) { + targets = []; + break; + } + continue; + } + if (key === src) { + continue; + } + const target = this.serverModule.getPlayer(key); + if (!target || !target.radioSettings.activated) { + continue; + } + const shortRange = !player.radioSettings.hasLong && !target.radioSettings.hasLong; + if (player.radioSettings.hasLong && target.radioSettings.hasLong || shortRange) { + targets.push(key); + radioInfos[key] = { + shortRange + }; + targetsToSender.push(key); + } + } + triggerClientEvent( + "client:yaca:radioTalking", + targets, + src, + radioFrequency, + state, + radioInfos, + distanceToTower, + GetEntityCoords(GetPlayerPed(src.toString())) + ); + if (this.serverConfig.useWhisper) { + emitNet("client:yaca:radioTalkingWhisper", src, targetsToSender, radioFrequency, state, GetEntityCoords(GetPlayerPed(src.toString()))); + } + } +}; + +// src/yaca/main.ts +var YaCAServerModule = class { + /** + * Creates an instance of the server module. + */ + constructor() { + this.nameSet = /* @__PURE__ */ new Set(); + this.players = /* @__PURE__ */ new Map(); + console.log("~g~ --> YaCA: Server loaded"); + this.serverConfig = loadConfig("config/server.json5", defaultServerConfig); + this.sharedConfig = loadConfig("config/shared.json5", defaultSharedConfig); + this.towerConfig = loadConfig("config/tower.json5", defaultTowerConfig); + initLocale(this.sharedConfig.locale); + if (this.sharedConfig.voiceRange.ranges[this.sharedConfig.voiceRange.defaultIndex]) { + this.defaultVoiceRange = this.sharedConfig.voiceRange.ranges[this.sharedConfig.voiceRange.defaultIndex]; + } else { + this.defaultVoiceRange = 1; + this.sharedConfig.voiceRange.ranges = [1]; + console.error("[YaCA] Default voice range is not set correctly in the config."); + } + this.phoneModule = new YaCAServerPhoneModle(this); + this.radioModule = new YaCAServerRadioModule(this); + this.megaphoneModule = new YaCAServerMegaphoneModule(this); + this.registerExports(); + this.registerEvents(); + if (this.sharedConfig.saltyChatBridge) { + this.saltChatBridge = new YaCAServerSaltyChatBridge(this); + } + if (this.sharedConfig.versionCheck) { + checkVersion().then(); + } + GlobalState.set(GLOBAL_ERROR_LEVEL_STATE_NAME, 0, true); + } + /** + * Get the player data for a specific player. + */ + getPlayer(playerId) { + return this.players.get(playerId); + } + /** + * Initialize the player on first connect. + * + * @param {number} src - The source-id of the player to initialize. + */ + connectToVoice(src) { + const name = generateRandomName(src, this.nameSet, this.serverConfig.userNamePattern); + if (!name) { + DropPlayer(src.toString(), "[YaCA] Failed to generate a random name."); + return; + } + const playerState = Player(src).state; + playerState.set(VOICE_RANGE_STATE_NAME, this.defaultVoiceRange, true); + this.players.set(src, { + voiceSettings: { + voiceFirstConnect: false, + forceMuted: false, + ingameName: name, + mutedOnPhone: false, + inCallWith: /* @__PURE__ */ new Set(), + emittedPhoneSpeaker: /* @__PURE__ */ new Map() + }, + radioSettings: { + activated: false, + hasLong: true, + frequencies: {} + } + }); + this.connect(src); + } + /** + * Register all exports for the YaCA module. + */ + registerExports() { + exports("connectToVoice", (src) => this.connectToVoice(src)); + exports("getPlayerAliveStatus", (playerId) => this.getPlayerAliveStatus(playerId)); + exports("setPlayerAliveStatus", (playerId, state) => this.changePlayerAliveStatus(playerId, state)); + exports("getPlayerVoiceRange", (playerId) => this.getPlayerVoiceRange(playerId)); + exports("setPlayerVoiceRange", (playerId, range) => this.changeVoiceRange(playerId, range)); + exports("setGlobalErrorLevel", (errorLevel) => setGlobalErrorLevel(errorLevel)); + exports("getGlobalErrorLevel", () => getGlobalErrorLevel()); + } + /** + * Register all events for the YaCA module. + */ + registerEvents() { + on("playerDropped", (_reason) => { + this.handlePlayerDisconnect(source); + }); + onNet("server:yaca:nuiReady", () => { + if (!this.sharedConfig.autoConnectOnJoin) return; + this.connectToVoice(source); + }); + onNet("server:yaca:addPlayer", (clientId) => { + this.addNewPlayer(source, clientId); + }); + onNet("server:yaca:wsReady", () => { + this.playerReconnect(source); + }); + onNet("txsv:req:spectate:end", () => { + emitNet("client:yaca:txadmin:stopspectate", source); + }); + } + /** + * Handle various cases if player disconnects. + * + * @param {number} src - The source-id of the player who disconnected. + */ + handlePlayerDisconnect(src) { + var _a; + const player = this.players.get(src); + if (!player) { + return; + } + this.nameSet.delete((_a = player.voiceSettings) == null ? void 0 : _a.ingameName); + const allFrequencies = this.radioModule.radioFrequencyMap; + for (const [key, value] of allFrequencies) { + value.delete(src); + if (!value.size) { + this.radioModule.radioFrequencyMap.delete(key); + } + } + for (const [targetId, emitterTargets] of player.voiceSettings.emittedPhoneSpeaker) { + const target = this.players.get(targetId); + if (!target || !target.voicePlugin) { + continue; + } + triggerClientEvent("client:yaca:phoneHearAround", Array.from(emitterTargets), [target.voicePlugin.clientId], false); + } + emitNet("client:yaca:disconnect", -1, src); + } + /** + * Syncs player alive status and mute him if he is dead or whatever. + * + * @param {number} src - The source-id of the player to sync. + * @param {boolean} alive - The new alive status. + */ + changePlayerAliveStatus(src, alive) { + const player = this.players.get(src); + if (!player) { + return; + } + player.voiceSettings.forceMuted = !alive; + emitNet("client:yaca:muteTarget", -1, src, !alive); + if (player.voicePlugin) { + player.voicePlugin.forceMuted = !alive; + } + } + /** + * Get the alive status of a player. + * + * @param playerId - The ID of the player to get the alive status for. + */ + getPlayerAliveStatus(playerId) { + var _a; + return ((_a = this.players.get(playerId)) == null ? void 0 : _a.voiceSettings.forceMuted) ?? false; + } + /** + * Used if a player reconnects to the server. + * + * @param {number} src - The source-id of the player to reconnect. + */ + playerReconnect(src) { + const player = this.players.get(src); + if (!player) { + return; + } + if (!player.voiceSettings.voiceFirstConnect) { + return; + } + this.connect(src); + } + /** + * Change the voice range of a player. + * + * @param {number} src - The source-id of the player to change the voice range for. + * @param {number} range - The new voice range. Defaults to the default voice range if not provided. + */ + changeVoiceRange(src, range) { + const playerState = Player(src).state; + playerState.set(VOICE_RANGE_STATE_NAME, range ?? this.defaultVoiceRange, true); + emitNet("client:yaca:changeVoiceRange", src, range); + } + /** + * Get the voice range of a player. + * + * @param playerId - The ID of the player to get the voice range for. + */ + getPlayerVoiceRange(playerId) { + const playerState = Player(playerId).state; + return playerState[VOICE_RANGE_STATE_NAME] ?? this.defaultVoiceRange; + } + /** + * Sends initial data needed to connect to teamspeak plugin. + * + * @param {number} src - The source-id of the player to connect + */ + connect(src) { + const player = this.players.get(src); + if (!player) { + console.error(`YaCA: Missing player data for ${src}.`); + return; + } + player.voiceSettings.voiceFirstConnect = true; + const initObject = { + suid: this.serverConfig.uniqueServerId, + chid: this.serverConfig.ingameChannelId, + deChid: this.serverConfig.defaultChannelId, + channelPassword: this.serverConfig.ingameChannelPassword, + ingameName: player.voiceSettings.ingameName, + useWhisper: this.serverConfig.useWhisper, + excludeChannels: this.serverConfig.excludeChannels + }; + emitNet("client:yaca:init", src, initObject); + } + /** + * Add new player to all other players on connect or reconnect, so they know about some variables. + * + * @param src - The source-id of the player to add. + * @param {number} clientId - The client ID of the player. + */ + addNewPlayer(src, clientId) { + const player = this.players.get(src); + if (!player || !clientId) { + return; + } + player.voicePlugin = { + playerId: src, + clientId, + forceMuted: player.voiceSettings.forceMuted, + mutedOnPhone: player.voiceSettings.mutedOnPhone + }; + emitNet("client:yaca:addPlayers", -1, player.voicePlugin); + const allPlayersData = []; + for (const playerSource of getPlayers()) { + const intPlayerSource = Number.parseInt(playerSource); + const playerServer = this.players.get(intPlayerSource); + if (!playerServer) { + continue; + } + if (!playerServer.voicePlugin || intPlayerSource === src) { + continue; + } + allPlayersData.push(playerServer.voicePlugin); + } + emitNet("client:yaca:addPlayers", src, allPlayersData); + } +}; + +// src/index.ts +new YaCAServerModule(); +/*! Bundled license information: + +web-streams-polyfill/dist/ponyfill.es2018.js: + (** + * @license + * web-streams-polyfill v3.3.3 + * Copyright 2024 Mattias Buelens, Diwank Singh Tomer and other contributors. + * This code is released under the MIT license. + * SPDX-License-Identifier: MIT + *) + +fetch-blob/index.js: + (*! fetch-blob. MIT License. Jimmy Wärting *) + +formdata-polyfill/esm.min.js: + (*! formdata-polyfill. MIT License. Jimmy Wärting *) + +node-domexception/index.js: + (*! node-domexception. MIT License. Jimmy Wärting *) +*/ diff --git a/resources/[voice]/yaca-voice/fxmanifest.lua b/resources/[voice]/yaca-voice/fxmanifest.lua new file mode 100644 index 000000000..0c84b1bda --- /dev/null +++ b/resources/[voice]/yaca-voice/fxmanifest.lua @@ -0,0 +1,30 @@ +fx_version 'cerulean' +games { 'gta5', 'rdr3' } +rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.' + +name 'yaca-voice' +author 'MineMalox & LuftigerLuca' +version '3.0.5' +repository 'https://github.com/yaca-systems/fivem-yaca-typescript' +description 'YACA Voice Integration for FiveM & RedM' + +dependencies { + '/server:7290', + '/onesync', +} + +ui_page 'web/index.html' + +files { + 'web/index.html', + 'web/script.js', + 'config/shared.json5', + 'config/towers.json5', + 'locales/*.json', +} + +client_script 'dist/client.js' +server_script 'dist/server.js' + +provide 'saltychat' + diff --git a/resources/[voice]/yaca-voice/locales/de.json b/resources/[voice]/yaca-voice/locales/de.json new file mode 100644 index 000000000..8bac3117b --- /dev/null +++ b/resources/[voice]/yaca-voice/locales/de.json @@ -0,0 +1,33 @@ +{ + "connect_error": "Fehler beim Verbinden zum Teamspeak-Server, bitte verbinde dich erneut!", + "plugin_not_initialized": "Das Voice-Plugin wurde noch nicht initialisiert!", + + "outdated_plugin": "Dein Voice-Plugin ist veraltet! Bitte aktualisiere auf die Version %s!", + "wrong_ts_server": "Du bist auf dem falschen Teamspeak-Server!", + "move_error": "Fehler beim Verschieben auf den Teamspeak-Server!", + "max_players_reached": "Ihre Lizenz hat die maximale Spieleranzahl erreicht. Bitte erweitern Sie Ihre Lizenz.", + "license_server_timed_out": "Die Verbindung zum Lizenzserver wurde beim Überprüfen der Lizenz unterbrochen. Bitte warte einen Moment.", + "unknown_error": "Unbekannter Fehlercode: %s", + + "changed_stereo_mode": "Kanal %s ist jetzt auf %s zu hören.", + "left_ear": "dem linken Ohr", + "right_ear": "dem rechten Ohr", + "both_ears": "beiden Ohren", + "secondary_radio_channel_disabled": "Der sekundäre Funkkanal wurde deaktiviert.", + "secondary_radio_channel_enabled": "Kanal %s ist nun der Sekundäre Funkkanal.", + + "use_megaphone": "Megaphon benutzen", + "use_radio": "Im primären Funkkanal senden", + "use_secondary_radio": "Im sekundären Funkkanal senden", + + "use_salty_primary_radio": "Primäres Funkgerät benutzen", + "use_salty_secondary_radio": "Sekundäres Funkgerät benutzen", + + "change_voice_range_increase": "Sprachreichweite erhöhen", + "change_voice_range_decrease": "Sprachreichweite verringern", + "voice_range_changed": "Sprachreichweite auf %s Meter geändert.", + "change_voice_range_via_mousewheel": "Sprachreichweite je 1 Meter ändern", + + "radio_not_activated": "Das Funkgerät ist nicht aktiviert!", + "radio_channel_invalid": "Ungültiger Funkkanal!" +} diff --git a/resources/[voice]/yaca-voice/locales/en.json b/resources/[voice]/yaca-voice/locales/en.json new file mode 100644 index 000000000..c0318f949 --- /dev/null +++ b/resources/[voice]/yaca-voice/locales/en.json @@ -0,0 +1,33 @@ +{ + "connect_error": "Error while connecting to voice server, please reconnect!", + "plugin_not_initialized": "Plugin not initialized!", + + "outdated_plugin": "Your plugin is outdated, please update to version %s!", + "wrong_ts_server": "You are connected to the wrong teamspeak server!", + "move_error": "Error while moving to the channel!", + "max_players_reached": "Your license reached the maximum player count. Please upgrade your license", + "license_server_timed_out": "The connection to the license server timed out, while verifying the license. Please wait a moment.", + "unknown_error": "Unknown error code: %s", + + "changed_stereo_mode": "Channel %s is now only heard in %s.", + "left_ear": "the left ear", + "right_ear": "the right ear", + "both_ears": "both ears", + "secondary_radio_channel_disabled": "The secondary radio channel has been disabled.", + "secondary_radio_channel_enabled": "Channel %s is now the secondary radio channel.", + + "use_megaphone": "Use megaphone", + "use_radio": "Use radio", + "use_secondary_radio": "Use secondary radio", + + "use_salty_primary_radio": "Use primary radio", + "use_salty_secondary_radio": "Use secondary radio", + + "change_voice_range_increase": "Increase voice range", + "change_voice_range_decrease": "Decrease voice range", + "voice_range_changed": "Voice range changed to %s meters.", + "change_voice_range_via_mousewheel": "Change voice range by 1 meter", + + "radio_not_activated": "Your radio is not activated!", + "radio_channel_invalid": "The radio channel is invalid!" +} diff --git a/resources/[voice]/yaca-voice/web/index.html b/resources/[voice]/yaca-voice/web/index.html new file mode 100644 index 000000000..a8d251332 --- /dev/null +++ b/resources/[voice]/yaca-voice/web/index.html @@ -0,0 +1,13 @@ + + + + + YACA WebSocket + + + + + + + + \ No newline at end of file diff --git a/resources/[voice]/yaca-voice/web/script.js b/resources/[voice]/yaca-voice/web/script.js new file mode 100644 index 000000000..3ca63e39c --- /dev/null +++ b/resources/[voice]/yaca-voice/web/script.js @@ -0,0 +1,85 @@ +let webSocket = null + +/** + * Connect to the YaCA voice plugin + */ +function connect() { + console.log('[YaCA-Websocket] Trying to Connect to YaCA WebSocket...') + + try { + webSocket = new window.WebSocket('ws://127.0.0.1:30125/') + } catch { + connect() + } + + webSocket.onmessage = (event) => { + if (!event) return + sendNuiData('YACA_OnMessage', event.data) + } + + webSocket.onopen = (event) => { + if (!event) return + sendNuiData('YACA_OnConnected') + } + + webSocket.onclose = (event) => { + if (!event) return + + sendNuiData('YACA_OnDisconnected', { + code: event.code, + reason: event.reason, + }) + + setTimeout(() => { + connect() + }, 1000) + } +} + +/** + * Send a command to the YaCA voice plugin + * + * @param command - The command to send as a object + */ +function runCommand(command) { + if (!webSocket || webSocket.readyState !== WebSocket.OPEN) { + return + } + + webSocket.send(JSON.stringify(command)) +} + +/** + * Send a NUI message to the client + * + * @param event - The name of the callback + * @param data - The data to send + */ +function sendNuiData(event, data = {}) { + // skipcq: JS-0125 + fetch(`https://${GetParentResourceName()}/${event}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: JSON.stringify(data), + }).catch((error) => console.error('[YaCA-Websocket] Error sending NUI Message:', error)) +} + +$(() => { + window.addEventListener('DOMContentLoaded', () => { + sendNuiData('YACA_OnNuiReady') + }) + + window.addEventListener('message', (event) => { + if (event.data.action === 'connect') { + connect() + } else if (event.data.action === 'command') { + runCommand(event.data.data) + } else if (event.data.action === 'close') { + if (webSocket) webSocket.close() + } else { + console.error('[YaCA-Websocket] Unknown message:', event.data) + } + }) +})