-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsync.lua
More file actions
327 lines (308 loc) · 9.39 KB
/
Copy pathsync.lua
File metadata and controls
327 lines (308 loc) · 9.39 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
-- HandGuard sync.lua - HGRD addon protocol + requester routing logic
local HG = HandGuard
HG.pending = nil -- { key, pala, state = "ack"/"cast", expires, tried = {} }
local lastBroadcast = 0
local lastState = nil
local lastHello = 0
local DENY_TEXT = {
cd = "spell on cooldown",
dead = "paladin is dead",
tdead = "you are dead",
reagent = "no Symbol of Divinity",
forbearance = "you have Forbearance",
range = "you are out of range",
los = "no line of sight",
manual = "request declined",
timeout = "no answer",
unknown = "spell not learned",
busy = "paladin is busy",
error = "paladin addon error",
castfail = "cast failed",
}
HG.DENY_TEXT = DENY_TEXT
-- ------------------------------------------------------------------ transport
function HG.Sync_Send(msg)
local channel
if GetNumRaidMembers() > 0 then
channel = "RAID"
elseif GetNumPartyMembers() > 0 then
channel = "PARTY"
else
return
end
if HG.db and HG.db.debug then
HG.Print("|cff888888[send " .. channel .. "]|r " .. msg)
end
pcall(SendAddonMessage, HG.PREFIX, msg, channel)
end
function HG.Sync_Hello()
local now = GetTime()
if now - lastHello < 5 then return end
lastHello = now
local pala = 0
if HG.isPaladin then pala = 1 end
HG.Sync_Send("HELLO:" .. HG.VERSION .. ":" .. pala)
if HG.isPaladin then
HG.Sync_BroadcastCD(true)
end
end
-- CD:BOP:n:BOF:n:DI:n:reagentCount (n in seconds, -1 = spell not learned)
function HG.Sync_BroadcastCD(force)
if not HG.isPaladin then return false end
local now = GetTime()
if not force and now - lastBroadcast < 3 then return false end
lastBroadcast = now
local msg = "CD"
for i = 1, 3 do
local key = HG.SPELL_ORDER[i]
local rem = HG.CD_GetOwn(key)
if rem == nil then
rem = -1
else
rem = math.floor(rem)
end
msg = msg .. ":" .. key .. ":" .. rem
end
msg = msg .. ":" .. HG.reagentCount
HG.Sync_Send(msg)
return true
end
-- broadcast when the ready-state changes; full resync every 60s
function HG.Sync_Tick()
HG.Req_Tick()
if not HG.isPaladin or not HG.inGroup then return end
local state = ""
for i = 1, 3 do
local key = HG.SPELL_ORDER[i]
local rem = HG.CD_GetOwn(key)
if rem == nil then
state = state .. "x"
elseif rem <= 0 then
state = state .. "1"
else
state = state .. "0"
end
end
state = state .. ":" .. HG.reagentCount
local now = GetTime()
if state ~= lastState then
if HG.Sync_BroadcastCD() then
lastState = state
end
elseif now - lastBroadcast > 60 then
HG.Sync_BroadcastCD(true)
end
end
-- ------------------------------------------------------------------ receive
function HG.Sync_OnAddonMessage(prefix, msg, dist, sender)
if prefix ~= HG.PREFIX or not msg or not sender then return end
if sender == HG.playerName then return end
if HG.db and HG.db.debug then
HG.Print("|cff888888[recv " .. sender .. "]|r " .. msg)
end
local parts = HG.Split(msg, ":")
local kind = parts[1]
if kind == "HELLO" then
-- the roster scan may not have seen this paladin yet: create the entry here
local p = HG.paladins[sender]
if not p and parts[3] == "1" then
p = { addon = false, cd = {} }
HG.paladins[sender] = p
end
local firstSeen = p and not p.addon
if p then
p.addon = true
p.version = parts[2]
end
-- answer a first contact with our own HELLO so both sides discover each other
if firstSeen then
HG.After(math.random() * 2, function() HG.Sync_Hello() end)
end
if HG.isPaladin then
-- staggered answer so 8 paladins do not broadcast in the same instant
HG.After(math.random() * 3, function() HG.Sync_BroadcastCD(true) end)
end
elseif kind == "CD" then
local p = HG.paladins[sender]
if not p then
-- only paladins broadcast CD: trust the message over the roster scan
p = { addon = true, cd = {} }
HG.paladins[sender] = p
end
p.addon = true
p.estimated = false
local now = GetTime()
local idx = 2
while parts[idx + 1] do
local key = parts[idx]
local rem = tonumber(parts[idx + 1])
if HG.SPELLS[key] and rem then
if rem < 0 then
p.cd[key] = nil
else
p.cd[key] = now + rem
end
end
idx = idx + 2
end
p.reagent = tonumber(parts[idx])
HG.UI_Refresh()
elseif kind == "REQ" then
local key, target = parts[2], parts[3]
if target == HG.playerName and HG.isPaladin and HG.SPELLS[key] then
-- if request handling breaks, answer with a DENY instead of silence
local ok, err = pcall(HG.Paladin_OnRequest, key, sender)
if not ok then
HG.Print("|cffff5555Error handling request: " .. tostring(err) .. "|r")
HG.Sync_Send("DENY:" .. key .. ":" .. sender .. ":error")
end
end
elseif kind == "ACK" then
local key, requester = parts[2], parts[3]
if requester == HG.playerName then
HG.Req_OnAck(key, sender)
end
elseif kind == "DENY" then
local key, requester, reason = parts[2], parts[3], parts[4]
if requester == HG.playerName then
HG.Req_OnDeny(key, sender, reason)
end
elseif kind == "CASTED" then
local key, target = parts[2], parts[3]
if HG.SPELLS[key] then
local p = HG.paladins[sender]
if p then
-- temporary estimate, refined by the CD broadcast that follows
p.cd[key] = GetTime() + HG.SPELLS[key].cdFallback
end
if key == "BOP" and target then
HG.forbearance[target] = GetTime() + 60
end
if target == HG.playerName then
HG.Req_OnCastedOnMe(key, sender)
end
HG.UI_Refresh()
end
end
end
-- ------------------------------------------------------------------ requester
function HG.RequestSpell(key)
if not HG.db then return end
local def = HG.SPELLS[key]
if not def then return end
if not HG.inGroup then
HG.Error("You are not in a group")
return
end
if UnitIsDeadOrGhost("player") then
HG.Error("You are dead")
return
end
if HG.pending then
HG.Error("Request already in progress")
return
end
if key == "BOP" and HG.HasForbearance("player") then
HG.Error("You have Forbearance - cannot receive BoP")
HG.Sound("igQuestFailed")
return
end
local tried = {}
local pala = HG.PickPaladin(key, tried)
if not pala then
HG.Error("No paladin has " .. def.name .. " available")
HG.Sound("igQuestFailed")
return
end
tried[pala] = true
HG.pending = { key = key, pala = pala, state = "ack", expires = GetTime() + 3, tried = tried }
HG.Sync_Send("REQ:" .. key .. ":" .. pala)
HG.Print("Requesting " .. def.name .. " from " .. HG.ColorName(pala) .. "...")
HG.Sound("igMainMenuOptionCheckBoxOn")
HG.UI_Refresh()
end
-- best candidate: priority list first, then closest, in-range strongly preferred
function HG.PickPaladin(key, tried)
local now = GetTime()
local prio = {}
local plist = HG.Split(HG.db.priority or "", ",")
for i = 1, table.getn(plist) do
local nm = string.gsub(plist[i], "^%s*(.-)%s*$", "%1")
if nm ~= "" then
prio[string.lower(nm)] = i
end
end
local best, bestScore
for name, p in pairs(HG.paladins) do
local ok = p.addon and not tried[name]
if ok and p.cd[key] == nil then ok = false end
if ok and p.cd[key] > now then ok = false end
if ok and key == "DI" and (not p.reagent or p.reagent < 1) then ok = false end
local unit = HG.UnitIdByName(name)
if ok and unit then
if UnitIsDeadOrGhost(unit) or not UnitIsConnected(unit) then ok = false end
end
if ok then
local score = 1000
local pr = prio[string.lower(name)]
if pr then score = pr end
local dist = unit and HG.Distance(unit)
if dist then score = score + dist / 1000 end
if dist and dist > 32 then score = score + 5000 end
if not bestScore or score < bestScore then
best = name
bestScore = score
end
end
end
return best
end
function HG.Req_Tick()
local pd = HG.pending
if not pd then return end
if GetTime() < pd.expires then return end
HG.Req_Reroute(pd, "timeout")
end
function HG.Req_Reroute(pd, reason)
local def = HG.SPELLS[pd.key]
local nxt = HG.PickPaladin(pd.key, pd.tried)
if nxt then
pd.tried[nxt] = true
pd.pala = nxt
pd.state = "ack"
pd.expires = GetTime() + 3
HG.Sync_Send("REQ:" .. pd.key .. ":" .. nxt)
HG.Print("Rerouting to " .. HG.ColorName(nxt) .. " (" .. (DENY_TEXT[reason] or reason or "?") .. ")")
else
HG.pending = nil
local txt = def.label .. " request failed: " .. (DENY_TEXT[reason] or reason or "no paladin available")
HG.Error(txt)
HG.Print("|cffff5555" .. txt .. "|r")
HG.Sound("igQuestFailed")
HG.UI_Refresh()
end
end
function HG.Req_OnAck(key, pala)
local pd = HG.pending
if pd and pd.key == key and pd.pala == pala then
pd.state = "cast"
pd.expires = GetTime() + 10
HG.Print(HG.ColorName(pala) .. " accepted - waiting for the cast...")
end
end
function HG.Req_OnDeny(key, pala, reason)
local pd = HG.pending
if pd and pd.key == key and pd.pala == pala then
HG.Print(HG.ColorName(pala) .. " denied: " .. (DENY_TEXT[reason] or reason or "?"))
HG.Req_Reroute(pd, reason)
end
end
function HG.Req_OnCastedOnMe(key, caster)
local pd = HG.pending
if pd and pd.key == key then
HG.pending = nil
HG.Print(HG.SPELLS[key].name .. " received from " .. HG.ColorName(caster or "?") .. "!")
HG.Sound("QUESTCOMPLETED")
HG.UI_Refresh()
end
end