-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathactiontext_client.lua
More file actions
336 lines (305 loc) · 13.2 KB
/
actiontext_client.lua
File metadata and controls
336 lines (305 loc) · 13.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
local activeTexts = {}
Citizen.CreateThread(function()
SetNuiFocus(false, false)
Wait(500)
SendNUIMessage({ type = 'actiontext:update', items = {} })
end)
local function rgbToHex(tbl)
if type(tbl) ~= 'table' then return nil end
local r = math.floor((tbl[1] or 0))
local g = math.floor((tbl[2] or 0))
local b = math.floor((tbl[3] or 0))
return ('#%02x%02x%02x'):format(r, g, b)
end
local function getBubbleScale()
local baseScale = 0.5
local configured = (Config.GLOBAL and Config.GLOBAL.textScale) or baseScale
if baseScale == 0 then return 1.0 end
return configured / baseScale
end
local function getBubbleSizePx()
local nuiCfg = (Config.GLOBAL and Config.GLOBAL.nui) or nil
if nuiCfg and nuiCfg.bubbleSizePx then
return nuiCfg.bubbleSizePx
end
return 180
end
local cachedNuiOptions = nil
local function getNuiOptions()
if cachedNuiOptions then return cachedNuiOptions end
local nuiOptions = {}
local nuiCfg = (Config.GLOBAL and Config.GLOBAL.nui) or {}
if nuiCfg and nuiCfg.indicator then
nuiOptions.indicatorEnabled = nuiCfg.indicator.enabled
nuiOptions.indicatorStyle = nuiCfg.indicator.style
nuiOptions.indicatorMeColor = (Config.ME and Config.ME.color) or nuiCfg.indicator.meColor
nuiOptions.indicatorDoColor = (Config.DO and Config.DO.color) or nuiCfg.indicator.doColor
nuiOptions.indicatorIntensity = nuiCfg.indicator.intensity
end
cachedNuiOptions = nuiOptions
return nuiOptions
end
local function addText(serverId, actionType, text, avatarUrl, displayName)
local player = GetPlayerFromServerId(serverId)
if player == -1 then return end
local ped = GetPlayerPed(player)
if not DoesEntityExist(ped) then return end
local color = (Config.GLOBAL and Config.GLOBAL.textColor) or {255,255,255}
local bgHex = nil
if actionType == 'me' then
if Config.ME and Config.ME.color then color = Config.ME.color end
bgHex = rgbToHex((Config.ME and Config.ME.bgColor) or Config.ME.color)
elseif actionType == 'do' then
if Config.DO and Config.DO.color then color = Config.DO.color end
bgHex = rgbToHex((Config.DO and Config.DO.bgColor) or Config.DO.color)
end
local fmt = '%s'
if actionType == 'me' and Config.ME and Config.ME.overheadFormat then
fmt = Config.ME.overheadFormat
elseif actionType == 'do' and Config.DO and Config.DO.overheadFormat then
fmt = Config.DO.overheadFormat
end
text = string.format(fmt, text)
do
local nuiCfg = (Config.GLOBAL and Config.GLOBAL.nui) or {}
local maxLen = nuiCfg.maxTextLength or 0
if type(text) == 'string' and maxLen > 0 and #text > maxLen then
text = text:sub(1, maxLen) .. '…'
end
end
activeTexts[serverId] = {
serverId = serverId,
actionType = actionType,
text = text,
avatar = avatarUrl,
displayName = displayName,
color = color,
bgHex = bgHex,
expireAt = GetGameTimer() + ((Config.GLOBAL and Config.GLOBAL.displayTime or 6) * 1000)
}
lastSentItems = nil
lastSentTime = 0
local pname = displayName or GetPlayerName(GetPlayerFromServerId(serverId)) or ('Player' .. tostring(serverId))
local immediateX, immediateY = 0.5, 0.5
if (Config.GLOBAL and Config.GLOBAL.nui and Config.GLOBAL.nui.sendImmediateWithProjection) then
local p = GetPlayerFromServerId(serverId)
if p ~= -1 then
local ped = GetPlayerPed(p)
if DoesEntityExist(ped) then
local head = GetPedBoneCoords(ped, 31086, 0.0, 0.0, 0.0)
local cam = GetGameplayCamCoords()
local dirx = head.x - cam.x
local diry = head.y - cam.y
local dirz = head.z - cam.z
local dist = math.sqrt(dirx * dirx + diry * diry + dirz * dirz)
if dist == 0 then dist = 0.0001 end
dirx = dirx / dist
diry = diry / dist
dirz = dirz / dist
local nuiCfg = (Config.GLOBAL and Config.GLOBAL.nui) or {}
local baseForward = nuiCfg.forwardAnchor or 0.18
local baseHead = nuiCfg.headOffset or 0.14
local adapt = math.max(0, -dirz)
local forwardDist = baseForward + (adapt * 0.25)
local upOffset = baseHead + (adapt * 0.12)
local anchorX = head.x - dirx * forwardDist
local anchorY = head.y - diry * forwardDist
local anchorZ = head.z + upOffset
local onScreenA, asx, asy = World3dToScreen2d(anchorX, anchorY, anchorZ)
local onScreenH, hsx, hsy = World3dToScreen2d(head.x, head.y, head.z)
if onScreenH and hsx and hsy then
immediateX, immediateY = math.floor(hsx * 1000 + 0.5) / 1000, math.floor(hsy * 1000 + 0.5) / 1000
elseif onScreenA and asx and asy then
immediateX, immediateY = math.floor(asx * 1000 + 0.5) / 1000, math.floor(asy * 1000 + 0.5) / 1000
end
end
end
end
local items = {{
id = serverId,
x = immediateX,
y = immediateY,
text = text,
playerName = pname,
color = color,
bg = bgHex,
actionType = actionType,
avatar = avatarUrl,
scale = getBubbleScale(),
sizePx = getBubbleSizePx(),
}}
SendNUIMessage({ type = 'actiontext:update', items = items, options = getNuiOptions() })
end
lastSentItems = nil
lastSentTime = 0
local minSendInterval = 33 -- ms
local posThreshold = 0.02
local function itemsChanged(a, b, threshold)
if not a and not b then return false end
if (not a) ~= (not b) then return true end
if #a ~= #b then return true end
for i=1,#a do
local ai = a[i]
local bi = b[i]
if not bi then return true end
if ai.id ~= bi.id then return true end
if ai.text ~= bi.text then return true end
if ai.avatar ~= bi.avatar then return true end
if ai.playerName ~= bi.playerName then return true end
if ai.color ~= bi.color then return true end
if ai.bg ~= bi.bg then return true end
if ai.actionType ~= bi.actionType then return true end
local th = threshold or posThreshold
local dx = math.abs((ai.x or 0) - (bi.x or 0))
local dy = math.abs((ai.y or 0) - (bi.y or 0))
if dx > th or dy > th then return true end
end
return false
end
Citizen.CreateThread(function()
local lastActive = false
while true do
local now = GetGameTimer()
local items = {}
local hasActive = false
local myPed = PlayerPedId()
local myCoords = GetEntityCoords(myPed)
local radius = ((Config.GLOBAL and Config.GLOBAL.radius) or 20.0)
local bubbleScale = getBubbleScale()
local bubbleSizePx = getBubbleSizePx()
local hOffset = ((Config.GLOBAL and Config.GLOBAL.nui and Config.GLOBAL.nui.horizontalOffsetPx) or 0)
local vOffsetPx = ((Config.GLOBAL and Config.GLOBAL.nui and Config.GLOBAL.nui.verticalOffsetPx) or 10)
local badgeSize = ((Config.GLOBAL and Config.GLOBAL.nui and Config.GLOBAL.nui.badgeSizePx) or 56)
local nuiCfg = (Config.GLOBAL and Config.GLOBAL.nui) or {}
local minSendIntervalLocal = nuiCfg.minSendIntervalMs or 33
local posThresholdLocal = nuiCfg.posThreshold or 0.02
local maxProjections = nuiCfg.maxProjectionsPerFrame or 16
local candidates = {}
for id, item in pairs(activeTexts) do
if item.expireAt <= now then
activeTexts[id] = nil
else
hasActive = true
local player = GetPlayerFromServerId(item.serverId)
if player ~= -1 then
local ped = GetPlayerPed(player)
if DoesEntityExist(ped) then
local coords = GetEntityCoords(ped)
local distance = #(myCoords - coords)
if distance <= radius then
table.insert(candidates, { id = id, item = item, distance = distance, player = player, ped = ped })
end
end
end
end
end
table.sort(candidates, function(a,b) return a.distance < b.distance end)
for i=1, math.min(#candidates, maxProjections) do
local c = candidates[i]
local item = c.item
local player = c.player
local ped = c.ped
local headBone = GetPedBoneCoords(ped, 31086, 0.0, 0.0, 0.0)
local onScreen, sx, sy = World3dToScreen2d(headBone.x, headBone.y, headBone.z)
if onScreen then
if not item.cachedName then
item.cachedName = item.displayName or GetPlayerName(player) or ('Player' .. tostring(item.serverId))
end
local qsx = math.floor((sx or 0) * 1000 + 0.5) / 1000
local qsy = math.floor((sy or 0) * 1000 + 0.5) / 1000
table.insert(items, {
id = item.serverId,
x = qsx,
y = qsy,
text = item.text,
playerName = item.cachedName,
color = item.color,
bg = item.bgHex,
actionType = item.actionType,
headOffsetX = hOffset,
verticalOffsetPx = vOffsetPx,
avatar = (nuiCfg.disableAvatars and nil) or item.avatar,
badgeSizePx = badgeSize,
scale = bubbleScale,
sizePx = bubbleSizePx,
})
end
end
if hasActive then
local nowTime = GetGameTimer()
local shouldSend = itemsChanged(items, lastSentItems, posThresholdLocal)
if shouldSend and (nowTime - lastSentTime) >= minSendIntervalLocal then
SendNUIMessage({ type = 'actiontext:update', items = items, options = getNuiOptions() })
lastSentItems = items
lastSentTime = nowTime
end
lastActive = true
Wait(0)
else
if lastActive then
SendNUIMessage({ type = 'actiontext:update', items = {}, options = getNuiOptions() })
lastSentItems = nil
lastSentTime = GetGameTimer()
lastActive = false
end
Wait(500)
end
end
end)
local function sendAction(actionType, text)
if not text or text == '' then
local title = (Config.GLOBAL and Config.GLOBAL.lang and Config.GLOBAL.lang.usageTitle) or '^1Actions'
local fmt = (Config.GLOBAL and Config.GLOBAL.lang and Config.GLOBAL.lang.usageFormat) or 'Usage: /%s <message>'
TriggerEvent('chat:addMessage', { args = { title, string.format(fmt, actionType) } })
return
end
TriggerServerEvent('actiontext:send', actionType, text)
end
RegisterCommand('me', function(source, args)
local text = table.concat(args, ' ')
sendAction('me', text)
end, false)
RegisterCommand('do', function(source, args)
local text = table.concat(args, ' ')
sendAction('do', text)
end, false)
Citizen.CreateThread(function()
local lang = (Config.GLOBAL and Config.GLOBAL.lang) or {}
TriggerEvent('chat:addSuggestion', '/me', (Config.ME and Config.ME.suggestion) or lang.suggestionMe or 'Perform a roleplay emote', {{ name = 'message', help = lang.suggestionHelpMe or 'Describe your action' }})
TriggerEvent('chat:addSuggestion', '/do', (Config.DO and Config.DO.suggestion) or lang.suggestionDo or 'Describe the environment or state', {{ name = 'message', help = lang.suggestionHelpDo or 'Describe scene/state' }})
end)
RegisterNetEvent('actiontext:display')
AddEventHandler('actiontext:display', function(serverId, actionType, text, avatarUrl, displayName)
addText(serverId, actionType, text, avatarUrl, displayName)
end)
RegisterNetEvent('actiontext:avatarUpdate')
AddEventHandler('actiontext:avatarUpdate', function(serverId, avatarUrl)
local s = activeTexts[serverId]
if s then
s.avatar = avatarUrl
local items = {}
for _, item in pairs(activeTexts) do
if item then
if not item.cachedName then
local p = GetPlayerFromServerId(item.serverId)
if p ~= -1 then
item.cachedName = item.displayName or GetPlayerName(p) or ('Player' .. tostring(item.serverId))
end
end
table.insert(items, {
id = item.serverId,
x = 0.5, y = 0.5,
text = item.text,
playerName = item.cachedName or 'Player',
color = item.color,
actionType = item.actionType,
avatar = item.avatar,
scale = getBubbleScale(),
sizePx = getBubbleSizePx(),
})
end
end
lastSentItems = nil
SendNUIMessage({ type = 'actiontext:update', items = items, options = getNuiOptions() })
end
end)