Skip to content

Commit eaf585b

Browse files
authored
feat(speech): support isNetworked param
2 parents 3940f5b + c8f073f commit eaf585b

File tree

10 files changed

+129
-88
lines changed

10 files changed

+129
-88
lines changed

client/cl_main.lua

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,13 @@ local resourceName = GetCurrentResourceName()
2828

2929
-- Event Registration ------------------------------------------------
3030
-- Handles playing voice response
31-
RegisterNetEvent(resourceName .. ':client:playVoice', function(data)
31+
---@param data SpeechData
32+
RegisterNetEvent(resourceName .. ':client:playVoice', function(data, message)
3233
if not data then
3334
return
3435
end
3536

36-
speech.playResponse(data)
37+
speech.playResponse(data, message)
3738
end)
3839

3940
-- Handles setting character
@@ -42,7 +43,11 @@ RegisterNetEvent(resourceName .. ':client:setAI', function(character, location)
4243
return
4344
end
4445

45-
state.setCharacter(character)
46+
local success = state.setCharacter(character)
47+
if not success then
48+
return
49+
end
50+
4651
speech.playSpeech('XM25_GENERIC_HI', character, state.getAddressal(), location)
4752
target.updateTargets()
4853
object.updateObjectForCharacter(character)
@@ -54,7 +59,11 @@ RegisterNetEvent(resourceName .. ':client:setAddressal', function(addressal, loc
5459
return
5560
end
5661

57-
state.setAddressal(addressal)
62+
local success = state.setAddressal(addressal)
63+
if not success then
64+
return
65+
end
66+
5867
speech.playSpeech('XM25_GENERIC_POSITIVE', state.getCharacter(), addressal, location)
5968
end)
6069

@@ -173,15 +182,15 @@ end
173182

174183
-- Event Handlers --------------------------------------------------
175184
AddEventHandler('onClientResourceStart', function(resName)
176-
if GetCurrentResourceName() ~= resName then
185+
if resourceName ~= resName then
177186
return
178187
end
179188

180189
init()
181190
end)
182191

183192
AddEventHandler('onResourceStop', function(resName)
184-
if GetCurrentResourceName() ~= resName then
193+
if resourceName ~= resName then
185194
return
186195
end
187196

locales/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"ai_selection": "${ai} ${selection}",
2828
"ai_updated": "${ai} ${updated}",
2929
"invalid_ai_character": "Invalid ${ai}: '%s'. Choose '${angel}', '${haviland}', or '${og}'.",
30-
"invalid_addressal_choice": "Invalid choice. Use ${male} or ${female}.",
30+
"invalid_addressal_choice": "Invalid choice '%s'. Use ${male} or ${female}.",
3131
"change_ai_character": "${change} ${ai} ${character}",
3232
"change_addressal": "${change} ${addressal}",
3333
"addressal_selection": "${addressal} ${selection}",

modules/client/menu.lua

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ local state = lib.require('modules.client.state')
55
local utils = lib.require('modules.shared.utils')
66
local speech = lib.require('modules.client.speech')
77

8+
-- Localised Functions ----------------------------------------------
9+
local exports = exports
10+
811
-- Local Variables ----------------------------------------------
912
local resourceName = GetCurrentResourceName()
1013
local submenusRegistered = false

modules/client/speech.lua

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ local type = type
1212
local GetEntityCoords = GetEntityCoords
1313
local vector3 = vector3
1414
local PlayAmbientSpeechFromPositionNative = PlayAmbientSpeechFromPositionNative
15+
local exports = exports
1516

1617
-- Local Variables ----------------------------------------------
1718
local resourceName = GetCurrentResourceName()
1819

1920
-- Functions --------------------------------------------------------
2021
---Resolve location for speech playback
21-
---@alias LocationInput nil|vector3|number|string|{x:number,y:number,z:number}|number[]
2222
---@param location? LocationInput Location to resolve
2323
---@return vector3 coords Resolved world coordinates
2424
local function resolveLocation(location)
@@ -137,36 +137,36 @@ end
137137
---@param speechName string Base speech name
138138
---@param character CharacterName Character voice to use
139139
---@param addressal Addressal Player's addressal preference
140-
---@param location? vector3|string|table Optional location for speech
141-
local function playSpeech(speechName, character, addressal, location)
142-
assert(type(speechName) == 'string', 'speechName must be a string')
143-
assert(type(character) == 'string', 'character must be a string')
144-
assert(constants.isValidCharacter(character), 'invalid character name: ' .. tostring(character))
145-
assert(type(addressal) == 'string', 'addressal must be a string')
146-
assert(constants.isValidAddressal(addressal), 'invalid addressal: ' .. tostring(addressal))
147-
140+
---@param isNetworked boolean Whether to play for nearby players
141+
---@param location? LocationInput Optional location for speech playback
142+
local function playSpeech(speechName, character, addressal, isNetworked, location)
148143
local baseName = getBaseSpeechName(speechName)
149144
local finalName = resolveSpeechName(baseName, addressal)
150145
local voiceName = constants.getVoiceName(character)
151146
local finalLocation = resolveLocation(location)
152147

153-
-- this client
154148
PlayAmbientSpeechFromPositionNative(finalName, voiceName, finalLocation.x, finalLocation.y, finalLocation.z, constants.speechParams.default)
149+
lib.print.debug(format('Playing Speech: "%s" | Location: %s | Voice: "%s"', finalName, tostring(finalLocation), voiceName))
150+
151+
if isNetworked == nil then
152+
isNetworked = true
153+
end
154+
155+
if not isNetworked then
156+
return
157+
end
155158

156-
-- nearby clients
157159
TriggerServerEvent(resourceName .. ':server:playSpeech', {
158160
speechName = finalName,
159161
voiceName = voiceName,
160162
location = finalLocation,
161163
speechParams = constants.speechParams.default,
162164
})
163-
164-
lib.print.debug(format('Playing Speech: "%s" | Location: %s | Voice: "%s"', finalName, tostring(finalLocation), voiceName))
165165
end
166166

167167
---Play voice response for an intent/topic
168-
---@param data table Data containing speechName, topic, location
169-
local function playResponse(data)
168+
---@param data SpeechData
169+
local function playResponse(data, message)
170170
if not data.speechName then
171171
return
172172
end
@@ -183,7 +183,7 @@ local function playResponse(data)
183183
'AI: "%s" | Responding to: "%s" | Message: "%s" | Topic: "%s"',
184184
utils.capital(currentCharacter),
185185
utils.capital(currentAddressal),
186-
data.message,
186+
message or 'no message',
187187
utils.capital(data.topic)
188188
)
189189
)
@@ -198,26 +198,27 @@ local function playResponse(data)
198198
duration = sharedConfig.notify.duration,
199199
})
200200

201-
playSpeech(data.speechName, currentCharacter, currentAddressal, data.location)
201+
playSpeech(data.speechName, currentCharacter, currentAddressal, data.isNetworked, data.location)
202202
end
203203

204204
---Send a message to the AI and get a voice response (routes to server NLP processing)
205205
---@param message string The message to send to the AI
206+
---@param isNetworked boolean Whether to play for nearby players
206207
---@param location? LocationInput Optional location for speech playback (defaults to player ped)
207208
---@return boolean success Whether the request was successful
208-
local function talk(message, location)
209+
local function talk(message, isNetworked, location)
209210
if not message or type(message) ~= 'string' or #message == 0 then
210211
lib.print.warn('talk: Invalid message')
211212
return false
212213
end
213214

214-
local success = lib.callback.await(resourceName .. ':server:talk', false, message, location)
215+
local success = lib.callback.await(resourceName .. ':server:talk', false, message, isNetworked, location)
215216

216217
return success or false
217218
end
218219

219220
---Plays a random voice line
220-
local function playRandomLine(location)
221+
local function playRandomLine(isNetworked, location)
221222
local speechName = lib.callback.await(resourceName .. ':server:getRandomLine', false)
222223
if not speechName then
223224
lib.print.warn('Failed to get random line')
@@ -227,7 +228,7 @@ local function playRandomLine(location)
227228
local currentCharacter = state.getCharacter()
228229
local currentAddressal = state.getAddressal()
229230

230-
playSpeech(speechName, currentCharacter, currentAddressal, location)
231+
playSpeech(speechName, currentCharacter, currentAddressal, isNetworked, location)
231232
end
232233

233234
-- Exports -----------------------------------------------------------

modules/client/state.lua

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ local lower = string.lower
1010
local SetResourceKvp = SetResourceKvp
1111
local GetResourceKvpString = GetResourceKvpString
1212
local IsPedMale = IsPedMale
13+
local exports = exports
1314

1415
-- Local Variables ----------------------------------------------
1516
local resourceName = GetCurrentResourceName()
@@ -70,14 +71,11 @@ local function setCharacter(character)
7071
return false
7172
end
7273

73-
if not constants.isValidCharacter(character) then
74-
return false
75-
end
76-
7774
local normalised = normaliseCharacterName(character)
7875
local theme = utils.getTheme('characters', normalised)
7976

80-
if not normalised then
77+
if type(character) ~= 'string' or not constants.isValidCharacter(character) or not normalised then
78+
lib.print.warn('verifySpeechInput: invalid character name: ' .. tostring(character))
8179
ClientNotify.Notify({
8280
title = locale('ai_selection'),
8381
description = locale('invalid_ai_character', character),
@@ -111,17 +109,14 @@ local function setAddressal(addressal)
111109
return false
112110
end
113111

114-
if not constants.isValidAddressal(addressal) then
115-
return false
116-
end
117-
118112
local normalised = normaliseAddressal(addressal)
119113
local addrTheme = utils.getTheme('addressals', normalised)
120114

121-
if not normalised then
115+
if type(addressal) ~= 'string' or not constants.isValidAddressal(addressal) or not normalised then
116+
lib.print.warn('verifySpeechInput: invalid addressal: ' .. tostring(addressal))
122117
ClientNotify.Notify({
123118
title = locale('addressal_selection'),
124-
description = locale('invalid_addressal_choice'),
119+
description = locale('invalid_addressal_choice', addressal),
125120
icon = addrTheme.icon,
126121
iconColor = addrTheme.colour,
127122
type = 'error',

modules/server/speech.lua

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ local math = math
1818
local random = math.random
1919
local TriggerClientEvent = TriggerClientEvent
2020
local GetCurrentResourceName = GetCurrentResourceName
21+
local exports = exports
2122

2223
-- Local Variables ----------------------------------------------
2324
local resourceName = GetCurrentResourceName()
@@ -111,9 +112,10 @@ end
111112
---Processes player input through NLP and triggers voice response (central talking point)
112113
---@param source number Client source
113114
---@param message string Message to process
114-
---@param location? any Optional location data to pass to client
115+
---@param isNetworked boolean Whether to play for nearby players
116+
---@param location? LocationInput Optional location data to pass to client
115117
---@return boolean success
116-
local function talk(source, message, location)
118+
local function talk(source, message, isNetworked, location)
117119
if not nlp.isModelReady() then
118120
lib.print.warn('NLP model not ready')
119121
return false
@@ -126,26 +128,28 @@ local function talk(source, message, location)
126128
end
127129

128130
local clientState = lib.callback.await(resourceName .. ':client:getState', source)
129-
local genderTarget = clientState?.isMale and 'male' or 'female'
130131
local clientHour = clientState?.hour
131132

132133
local res = nlp.classifyToTopic(input)
134+
local genderTarget = clientState?.isMale and 'male' or 'female'
133135
local timeContext = getTimeContext(clientHour)
134-
135136
local bestBucket, highestFinalScore = pickBestBucket(res.scores, voiceLines.speech_buckets, genderTarget, timeContext)
136137

137138
local minScoreThreshold = sharedConfig.nlp.thresholds.score.minimum
138-
if not bestBucket or highestFinalScore < minScoreThreshold then
139+
local isFallback = (highestFinalScore < minScoreThreshold)
140+
141+
if not bestBucket or isFallback then
139142
bestBucket = (genderTarget == 'male') and 'XM25_GENERIC_NEGATIVE_MALE' or 'XM25_GENERIC_NEGATIVE_FEMALE'
140143
lib.print.debug(format('Fallback triggered for: "%s" (Score: %.2f)', input, highestFinalScore))
141144
end
142145

146+
local topic = res.topic
143147
TriggerClientEvent(resourceName .. ':client:playVoice', source, {
144-
topic = (highestFinalScore < minScoreThreshold) and 'fallback' or res.topic,
148+
topic = topic,
145149
speechName = bestBucket,
146-
message = message,
150+
isNetworked = isNetworked,
147151
location = location,
148-
})
152+
}, message)
149153

150154
if serverConfig.logs.talk.enabled then
151155
logs.talk(source, message)

modules/shared/constants.lua

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,53 +2,73 @@
22
local string = string
33
local lower = string.lower
44
local pairs = pairs
5+
local exports = exports
56

67
-- Types --------------------------------------------------------
7-
---@alias CharacterName 'angel'|'haviland'|'og'
8-
---@alias Addressal 'male'|'female'
9-
---@alias ModelName string
8+
9+
---Available AI character names
10+
---@alias CharacterName
11+
---| 'angel' # Female secretary AI
12+
---| 'haviland' # Male butler AI
13+
---| 'og' # Male gang AI
14+
15+
---Player addressal preference for gendered speech lines
16+
---@alias Addressal
17+
---| 'male' # Use male speech variants
18+
---| 'female' # Use female speech variants
19+
20+
---@alias LocationInput nil|vector3|number|string|{x:number,y:number,z:number}|number[]
21+
22+
---Data structure for speech playback
23+
---@class SpeechData
24+
---@field speechName string Base speech name (gender suffix auto-resolved)
25+
---@field character CharacterName AI character voice to use
26+
---@field addressal Addressal Player's gender preference for speech
27+
---@field isNetworked? boolean Whether to play for nearby players
28+
---@field location? LocationInput Optional location for speech playback
1029

1130
-- Constants ----------------------------------------------------
12-
---Valid AI character names
13-
---@type table<CharacterName, boolean>
31+
32+
---Valid AI character names for validation
33+
---@enum ValidCharacters
1434
local validCharacters = {
15-
['angel'] = true,
16-
['haviland'] = true,
17-
['og'] = true,
35+
angel = true,
36+
haviland = true,
37+
og = true,
1838
}
1939

20-
---Valid addressal options
21-
---@type table<Addressal, boolean>
40+
---Valid addressal options for validation
41+
---@enum ValidAddressals
2242
local validAddressals = {
23-
['male'] = true,
24-
['female'] = true,
43+
male = true,
44+
female = true,
2545
}
2646

27-
---Character to voice name mapping
28-
---@type table<CharacterName, string>
47+
---Character to GTA voice name mapping
48+
---@enum CharacterVoices
2949
local characterVoices = {
30-
['angel'] = 'XM25_AISECRETARY',
31-
['haviland'] = 'XM25_AIBUTLER',
32-
['og'] = 'XM25_AIGANG',
50+
angel = 'XM25_AISECRETARY',
51+
haviland = 'XM25_AIBUTLER',
52+
og = 'XM25_AIGANG',
3353
}
3454

35-
---Speech parameters
36-
---@type table<string, string>
55+
---Speech parameters for native calls
56+
---@enum SpeechParams
3757
local speechParams = {
3858
default = 'SPEECH_PARAMS_FORCE',
3959
}
4060

41-
---Character tablet model mappings
42-
---@type table<CharacterName|'blank', string>
61+
---Character tablet prop model mappings
62+
---@enum CharacterTabletModels
4363
local characterTabletModels = {
4464
blank = 'm25_2_prop_m52_aitablet',
4565
og = 'm25_2_prop_m52_aitablet_01a',
4666
haviland = 'm25_2_prop_m52_aitablet_02a',
4767
angel = 'm25_2_prop_m52_aitablet_03a',
4868
}
4969

50-
---Character TV model mappings
51-
---@type table<CharacterName, string>
70+
---Character TV prop model mappings
71+
---@enum CharacterTVModels
5272
local characterTVModels = {
5373
og = 'm25_2_prop_m52_mansiontv_ogai',
5474
haviland = 'm25_2_prop_m52_mansiontv_havilandai',

0 commit comments

Comments
 (0)