1
0
Fork 0
forked from Simnation/Main
This commit is contained in:
Miho931 2025-06-30 22:15:13 +02:00
parent cc47e529cc
commit f495c860bf
68 changed files with 0 additions and 11192 deletions

View file

@ -1,19 +0,0 @@
version = 1
exclude_patterns = [
"resources/**",
"dist/**"
]
[[analyzers]]
name = "secrets"
[[analyzers]]
name = "javascript"
[analyzers.meta]
environment = [
"jquery",
"nodejs",
"browser"
]

View file

@ -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
<details>
<summary style="font-size: x-large">Client</summary>
### 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 |
</details>
<details>
<summary style="font-size: x-large">Server</summary>
### 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`.
</details>
# Events
<details>
<summary style="font-size: x-large">Client</summary>
### 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. |
</details>
<details>
<summary style="font-size: x-large">Server</summary>
### 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 |
</details>
# 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.

View file

@ -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))

View file

@ -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:*"
}
}

File diff suppressed because it is too large Load diff

View file

@ -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()
}
}

View file

@ -1,8 +0,0 @@
/// <reference types="@citizenfx/client" />
import { initCache } from './utils'
import { YaCAClientModule } from './yaca'
initCache()
new YaCAClientModule()

View file

@ -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 = <T = never>(key: keyof ClientCache, cb: (value: T) => void) => {
on(`yaca:cache:${key}`, cb)
}
export { initCache, cache }

View file

@ -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]),
}
}

View file

@ -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
}

View file

@ -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()
}
})
}

View file

@ -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<T extends string | number>(
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)
}

View file

@ -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]),
}
}

View file

@ -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
}

View file

@ -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',
}),
)
}
}

View file

@ -1,97 +0,0 @@
const localLipSyncAnimations: Record<'fivem' | 'redm', Record<string, { name: string; dict: string }>> = {
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<string, number | null> = {
// 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 }

View file

@ -1,6 +0,0 @@
export * from './data'
export * from './intercom'
export * from './main'
export * from './megaphone'
export * from './phone'
export * from './radio'

View file

@ -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<YacaPlayerData> = 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,
)
})
}
}

File diff suppressed because it is too large Load diff

View file

@ -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<number>()
/**
* 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<number | false>('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)
}
}

View file

@ -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<number>()
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<YacaPlayerData>()
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,
)
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,9 +0,0 @@
{
"extends": "@yaca-voice/typescript-config/fivem.json",
"compilerOptions": {
"baseUrl": "./",
"rootDir": "./src",
"types": ["@citizenfx/client"]
},
"include": ["./"]
}

View file

@ -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))

View file

@ -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"
}
}

File diff suppressed because it is too large Load diff

View file

@ -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<string, Set<number>>()
/**
* 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<number>()
const newInCall = new Set<number>()
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<number>(beforeInCall)
const removedFromCall = new Set<number>()
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)
}
}
}
}
}

View file

@ -1,5 +0,0 @@
/// <reference types="@citizenfx/server" />
import { YaCAServerModule } from 'src/yaca'
new YaCAServerModule()

View file

@ -1,8 +0,0 @@
import type { ServerCache } from '@yaca-voice/types'
/**
* Cached values for the server.
*/
export const cache: ServerCache = {
resource: GetCurrentResourceName(),
}

View file

@ -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)
}
}

View file

@ -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<string>, 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
}

View file

@ -1,4 +0,0 @@
export * from './cache'
export * from './events'
export * from './generator'
export * from './versioncheck'

View file

@ -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
}
}
}
}

View file

@ -1,4 +0,0 @@
export * from './main'
export * from './megaphone'
export * from './phone'
export * from './radio'

View file

@ -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<number>
emittedPhoneSpeaker: Map<number, Set<number>>
}
radioSettings: {
activated: boolean
hasLong: boolean
frequencies: Record<number, string>
}
voicePlugin?: {
playerId: number
clientId: number
forceMuted: boolean
mutedOnPhone: boolean
}
}
/**
* The main server module for YaCA.
*/
export class YaCAServerModule {
cache: ServerCache
nameSet: Set<string> = new Set()
players: Map<number, YaCAPlayer> = 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<YacaServerConfig>('config/server.json5', defaultServerConfig)
this.sharedConfig = loadConfig<YacaSharedConfig>('config/shared.json5', defaultSharedConfig)
this.towerConfig = loadConfig<YacaTowerConfig>('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<number>(),
emittedPhoneSpeaker: new Map<number, Set<number>>(),
},
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)
}
}

View file

@ -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)
}
}
}

View file

@ -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<number>()
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<number>()
const disableReceive = new Set<number>()
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<number>()
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<number>()
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<number>()
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)
}
}
}

View file

@ -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<string, Map<number, { muted: boolean }>>()
/**
* 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<number, { muted: boolean }>())
}
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<number, { shortRange: boolean }> = {}
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())))
}
}
}

View file

@ -1,9 +0,0 @@
{
"extends": "@yaca-voice/typescript-config/fivem.json",
"compilerOptions": {
"baseUrl": "./",
"rootDir": "./src",
"types": ["@types/node", "@citizenfx/server"]
},
"include": ["./"]
}

View file

@ -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}"
}

View file

@ -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
}

View file

@ -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
]
]
}

View file

@ -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!"
}

View file

@ -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!"
}

View file

@ -1,13 +0,0 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<title>YACA WebSocket</title>
<script src="nui://game/ui/jquery.js" type="text/javascript"></script>
<script src="script.js" type="text/javascript"></script>
</head>
<body style="display: none;"></body>
</html>

View file

@ -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)
}
})
})

View file

@ -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": []
}
}

View file

@ -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"
}

View file

@ -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:*"
}
}

View file

@ -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

View file

@ -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)
})
}

View file

@ -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<T extends object>(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<T extends object>(filePath: string, defaultValues: T): T {
const fileData = LoadResourceFile(GetCurrentResourceName(), filePath)
if (!fileData) {
return defaultValues
}
const parsedData = JSON5.parse(fileData) as T
return mergeAndValidate<T>(defaultValues, parsedData)
}

View file

@ -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'

View file

@ -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
}

View file

@ -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<T>(cb: () => T, errMessage?: string, timeout?: number | false): Promise<T> {
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<T>((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))
}

View file

@ -1,91 +0,0 @@
import { printf } from 'fast-printf'
const resourceName = GetCurrentResourceName()
const dict: Record<string, string> = {}
/**
* 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<string, string | number | boolean>, target: Record<string, string>, 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
}
}

View file

@ -1,8 +0,0 @@
{
"extends": "@yaca-voice/typescript-config/fivem.json",
"compilerOptions": {
"baseUrl": "./",
"rootDir": "./src"
},
"include": ["./"]
}

View file

@ -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
}
}

View file

@ -1,3 +0,0 @@
{
"name": "@yaca-voice/typescript-config"
}

View file

@ -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"
}
}

View file

@ -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],
],
}

View file

@ -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',
}

View file

@ -1,3 +0,0 @@
export * from './config'
export * from './enums'
export * from './types'

View file

@ -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
}

View file

@ -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: {}

View file

@ -1,3 +0,0 @@
packages:
- "apps/*"
- "packages/*"

View file

@ -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!')

View file

@ -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
}
}
}