Repository navigation
Expand file tree
/
Copy pathShare.lua
More file actions
1166 lines (1091 loc) · 34.3 KB
/
Copy pathShare.lua
File metadata and controls
1166 lines (1091 loc) · 34.3 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
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- DMorph for OctoWoW: other DMorph users see your morph, and you see theirs.
--
-- Each client tells the others what it looks like (body, item morphs, and
-- the mount skin while it rides). Each client then morphs those players on
-- its own screen through VanillaHelpers whenever it has a unit token for
-- them: target, mouseover, target of target, party, raid, or with SuperWoW
-- their GUID. VanillaHelpers only morphs friendly players. Nobody without
-- DMorph sees anything different.
--
-- Talking as little as possible (the server slows down anyone who chats fast):
-- * Group, raid, battleground and guild mates get hidden addon messages.
-- Only players outside those hear us on DMorph's own chat channel, which
-- no chat window shows.
-- * Your changes go out once they stand still for a second (one message for
-- a whole outfit), and only while a DMorph user is in range of you (about
-- 28 yards, seen in the last minute). Nothing goes out otherwise: anyone
-- else asks when they come near you.
-- * Only players near you (or in your group) that you target or mouse over
-- are asked whether they have DMorph, and a player without it isn't asked
-- again for a day (remembered between sessions).
-- * Mounting and dismounting is a tiny message of its own that goes first.
-- * Looks are short (base 36) and remembered between sessions. A question
-- carries a fingerprint of the look we remember, and the answer is a
-- fingerprint too when nothing changed.
--
-- VanillaHelpers keeps a morph on the player's GUID, but when a player leaves
-- your sight and comes back the game builds them afresh without it. So every
-- fifth of a second the addon looks at the players it knows and puts the look
-- straight back on anyone who has just come back into view.
--
-- Messages (prefix "DMO2"). A look is "<display> <mount> <items>": display and
-- mount IDs in base 36 (0 = none), items as a slot letter (a = 1 ... s = 19)
-- and an item ID in base 36 each, or "-".
-- L <look> <guid|-> [s<size>] [<ported>] this is my look
-- A look may carry a fourth part for ported models (Data/Ported.lua, only on
-- PCs that installed the same pack): "P<pack>_<display>_<mount>_<items>".
-- It goes last in L messages, so older DMorph reads only the first three
-- parts and keeps showing the normal look; receivers use it only when their
-- pack is the same (M.GetPortedPack). Fingerprints cover all four parts.
-- M <mount> my mount skin changed, the rest didn't
-- H <fingerprint> <guid|-> my look is still the one with this fingerprint
-- Q <name>[:<fingerprint>],... what do these players look like now?
-- Questions are answered on the way they came (channel, group or guild).
local M = DmorphOcto
local CHANNEL, PASSWORD = "DmorphShare", "dmo2"
local OLD_CHANNEL = "DmorphOcto" -- 1.7's channel: left after login
local TAG = "DMO2"
local TICK, SCAN_GAP = 0.1, 0.2
local SETTLE, MAX_WAIT = 1, 3 -- send a changed look once it stands still this long, at most this late
local CHANNEL_GAP, ADDON_GAP = 0.6, 0.2 -- between our messages
local JOIN_DELAY, READY_DELAY = 3, 2 -- after login: join the channel, then talk
local REPLY_GAP = 3 -- answer questions about us at most this often (per way)
local QUERY_GAP = 10 -- ask about players at most this often
local ASK_AGAIN = 3600 -- ask a player again after this long (this session)
local NO_DMORPH_KEEP = 86400 -- a player who didn't answer isn't asked again for a day (any session)
local RECHECK = 120 -- ask a DMorph user who comes back into range whether they changed, at most this often
local CONFIRM_WAIT = 20 -- a player who doesn't answer in this long has no DMorph (now)
local AUDIENCE = 60 -- DMorph users in range this recently, or who asked about us, get our changes
local GUILD_GAP = 120 -- refresh the guild list this often
local KEEP = 14 * 86400 -- forget remembered looks after two weeks
local MAX_ASK = 12
local MAX_PEERS = 500
local clock, scanID = 0, 0
local peers, peerCount = {}, 0 -- name -> { display, mount, items, d, m, it, look, print, guid, version, heard, confirmed }
local applied = {} -- name -> what we put on them: { display, mount, items, version, scan, away }
local asked, queue, queued = {}, {}, {} -- name -> when we last asked; names to ask about; name -> fingerprint or true
local pending = {} -- name -> when a player we know nothing about was asked (no answer = no DMorph)
local noDmorph -- DmorphOctoAccount.noDmorph for this realm: name -> time() they didn't answer
local audience = {} -- name -> when they last showed they see us
local inGroup, inGuild = {}, {} -- names
local replies = {} -- way -> "L" (full look) or "H" (fingerprint)
local timers = {} -- clock times: join, ready, send, first, mount, reply, replied, query, scan, guild, next
local sent = { d = "0", m = "0", it = "-", s = "", p = "" } -- what the others were last told
local cache -- DmorphOctoAccount.peerCache for this realm
local guidWorks -- whether VanillaHelpers took a GUID as a unit token (nil: not tried)
local counts = { channel = 0, hidden = 0 }
local function Sharing()
return not M.GetAccount().shareOff
end
local function Seeing()
return not M.GetAccount().othersOff
end
-- Short numbers --------------------------------------------------------------------
local DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
local function Encode(n)
n = math.floor(tonumber(n) or 0)
if n <= 0 then
return "0"
end
local text = ""
while n > 0 do
local digit = math.mod(n, 36)
text = strsub(DIGITS, digit + 1, digit + 1) .. text
n = math.floor(n / 36)
end
return text
end
local function Decode(text)
if text and strfind(text, "^[0-9A-Z]+$") and strlen(text) <= 6 then
return tonumber(text, 36)
end
end
-- A four-character fingerprint of a look.
local function Fingerprint(look)
local h = 5381
for i = 1, strlen(look) do
h = math.mod(h * 33 + string.byte(look, i), 1679609)
end
return Encode(h)
end
-- Our look's three parts, as the others should see it.
local function OwnParts()
if not Sharing() then
return "0", "0", "-"
end
local display, items, mount = M.GetSharedLook()
local parts = {}
for slot = 1, 19 do
if items[slot] then
table.insert(parts, string.char(96 + slot) .. Encode(items[slot]))
end
end
local itemText = "-"
if table.getn(parts) > 0 then
itemText = table.concat(parts)
end
return Encode(display), Encode(mount), itemText
end
local function OwnLook()
local d, m, it = OwnParts()
return d .. " " .. m .. " " .. it
end
-- The ported part of our look ("P<pack>_<d>_<m>_<items>"), or "".
local function OwnPorted()
local pack = M.GetPortedPack and M.GetPortedPack()
if not (pack and Sharing() and M.GetSharedPorted) then
return ""
end
local display, items, mount = M.GetSharedPorted()
local parts = {}
for slot = 1, 19 do
if items[slot] then
table.insert(parts, string.char(96 + slot) .. Encode(items[slot]))
end
end
if not display and not mount and table.getn(parts) == 0 then
return ""
end
return "P" .. pack .. "_" .. Encode(display) .. "_" .. Encode(mount) .. "_" .. (table.getn(parts) > 0 and table.concat(parts) or "-")
end
-- Everything a fingerprint covers: the look and its ported part.
local function OwnFull()
local ported = OwnPorted()
if ported ~= "" then
return OwnLook() .. " " .. ported
end
return OwnLook()
end
-- SuperWoW's UnitExists also returns the unit's GUID.
local function GUIDOf(unit)
local ok, _, guid = pcall(UnitExists, unit)
if ok and type(guid) == "string" and strfind(guid, "^0x%x+$") then
return guid
end
end
-- Ways to reach players ------------------------------------------------------------
local function GroupChannel()
for i = 1, (MAX_BATTLEFIELD_QUEUES or 3) do
local ok, status = pcall(GetBattlefieldStatus, i)
if ok and status == "active" then
return "BATTLEGROUND"
end
end
if GetNumRaidMembers() > 0 then
return "RAID"
elseif GetNumPartyMembers() > 0 then
return "PARTY"
end
end
-- The quietest way to reach a player: their group or guild, or the channel.
local function Way(name)
local group = GroupChannel()
if group == "BATTLEGROUND" or (group and inGroup[name]) then
return group
elseif inGuild[name] then
return "GUILD"
end
return "CHANNEL"
end
local function ChannelID(name)
local id = GetChannelName(name or CHANNEL)
if id and id > 0 then
return id
end
end
local function Send(text, way)
if way == "CHANNEL" then
local id = ChannelID()
if id and pcall(SendChatMessage, TAG .. " " .. text, "CHANNEL", nil, id) then
counts.channel = counts.channel + 1
end
elseif way then
if pcall(SendAddonMessage, TAG, text, way) then
counts.hidden = counts.hidden + 1
end
end
-- The server is slower to forgive the channel.
local gap = ADDON_GAP
if way == "CHANNEL" then
gap = CHANNEL_GAP
end
timers.next = math.max(timers.next or 0, clock + gap)
end
-- Where a change of ours has to go: the group always, and whoever else is
-- around with DMorph, through their guild if all of them are guild mates.
local function ChangeWays()
local ways = {}
local group = GroupChannel()
if group then
ways[group] = true
end
for name, at in pairs(audience) do
if clock - at > AUDIENCE then
audience[name] = nil
else
ways[Way(name)] = true
end
end
if ways.CHANNEL then
return { CHANNEL = true } -- everyone with DMorph is on it
end
return ways
end
local function OurChannel(name)
if type(name) ~= "string" then
return false
end
name = strupper((string.gsub(name, "^%d+%.%s*", "")))
return name == strupper(CHANNEL) or name == strupper(OLD_CHANNEL)
end
-- No chat window shows DMorph's channel: its messages, joins and notices are
-- dropped before the chat frames see them, whatever channels they listen to.
local function HideChannel()
if M.shareChatHooked or type(ChatFrame_OnEvent) ~= "function" then
return
end
M.shareChatHooked = true
local original = ChatFrame_OnEvent
ChatFrame_OnEvent = function(event)
if type(event) == "string" and strsub(event, 1, 16) == "CHAT_MSG_CHANNEL"
and (OurChannel(arg9) or OurChannel(arg4)) then
return
end
return original(event)
end
end
local function JoinChannel()
HideChannel()
if ChannelID(OLD_CHANNEL) and LeaveChannelByName then
pcall(LeaveChannelByName, OLD_CHANNEL)
end
if not ChannelID() then
pcall(JoinChannelByName, CHANNEL, PASSWORD)
end
for i = 1, (NUM_CHAT_WINDOWS or 7) do
local chatFrame = getglobal("ChatFrame" .. i)
if chatFrame and chatFrame.channelList and ChatFrame_RemoveChannel then
local found = true
while found do
found = nil
for _, name in pairs(chatFrame.channelList) do
if OurChannel(name) then
ChatFrame_RemoveChannel(chatFrame, name)
found = true
break
end
end
end
end
end
end
local function ReadGuild()
inGuild = {}
if not (IsInGuild and IsInGuild()) then
return
end
local ok, count = pcall(GetNumGuildMembers)
for i = 1, (ok and count or 0) do
local name = GetGuildRosterInfo(i)
if name then
inGuild[name] = true
end
end
end
-- Rebuilds the group's names; a newcomer hears our look.
local function ReadGroup()
local before = inGroup
inGroup = {}
local raid = GetNumRaidMembers()
if raid > 0 then
for i = 1, 40 do
local name = UnitName("raid" .. i)
if name then
inGroup[name] = true
end
end
else
for i = 1, 4 do
local name = UnitName("party" .. i)
if name then
inGroup[name] = true
end
end
end
if timers.introduced then
for name in pairs(inGroup) do
if not before[name] then
timers.group = timers.group or clock + 1
break
end
end
end
end
-- Remembered looks -----------------------------------------------------------------
local function Cache()
if not cache then
local account = M.GetAccount()
if type(account.peerCache) ~= "table" then
account.peerCache = {}
end
local realm = (GetRealmName and GetRealmName()) or "?"
if type(account.peerCache[realm]) ~= "table" then
account.peerCache[realm] = {}
end
cache = account.peerCache[realm]
local now = time()
for name, entry in pairs(cache) do
if type(entry) ~= "table" or type(entry.look) ~= "string" or now - (tonumber(entry.seen) or 0) > KEEP then
cache[name] = nil
end
end
end
return cache
end
-- Players who didn't answer a question: they have no DMorph, for a day.
local function NoDmorph()
if not noDmorph then
local account = M.GetAccount()
if type(account.noDmorph) ~= "table" then
account.noDmorph = {}
end
local realm = (GetRealmName and GetRealmName()) or "?"
if type(account.noDmorph[realm]) ~= "table" then
account.noDmorph[realm] = {}
end
noDmorph = account.noDmorph[realm]
local now = time()
for name, at in pairs(noDmorph) do
if type(at) ~= "number" or now - at > NO_DMORPH_KEEP then
noDmorph[name] = nil
end
end
end
return noDmorph
end
local function KnownWithout(name)
local at = NoDmorph()[name]
return at and time() - at <= NO_DMORPH_KEEP
end
-- Someone answered: they have DMorph after all.
local function Answered(name)
pending[name] = nil
NoDmorph()[name] = nil
end
-- Putting looks on other players -------------------------------------------
local NONE = { items = {} }
-- Only display IDs the client has (a bad one could break its model).
local function Display(text)
local id = Decode(text)
if id and id > 0 and M.GetDisplayName(id) then
return id
end
end
-- A ported part puts ported models on a look, if this PC has the same pack
-- and can show them (a model it has, an item DmorphHelper can copy).
local function Overlay(look, ported)
local _, _, pack, pd, pm, pit = strfind(ported or "", "^P(%w+)_([0-9A-Z]+)_([0-9A-Z]+)_([a-s0-9A-Z%-]+)$")
if not pack then
return false
end
if pack ~= (M.GetPortedPack and M.GetPortedPack()) then
return true
end
local display, mount = Display(pd), Display(pm)
if display then
look.display = display
end
if mount then
look.mount = mount
end
if pit ~= "-" then
for letter, id in string.gfind(pit, "([a-s])([0-9A-Z]+)") do
local item = Decode(id)
if item and M.CanShowPortedItem and M.CanShowPortedItem(item) then
look.items[string.byte(letter) - 96] = item
end
end
end
return true
end
-- A look from its three parts (and a ported part), or nil if they don't read as one.
local function MakeLook(d, m, it, ported)
if not (Decode(d) and Decode(m) and it and (it == "-" or strfind(it, "^[a-s][0-9A-Z]+[a-s0-9A-Z]*$"))) then
return nil
end
local look = { display = Display(d), mount = Display(m), items = {}, d = d, m = m, it = it }
if it ~= "-" then
for letter, id in string.gfind(it, "([a-s])([0-9A-Z]+)") do
local item = Decode(id)
-- 0: a slot their set hides (shows nothing); DMorph before 2.16 skips it.
if item and item >= 0 and item < 1000000 then
look.items[string.byte(letter) - 96] = item
end
end
end
look.look = d .. " " .. m .. " " .. it
if ported and ported ~= "" and Overlay(look, ported) then
look.ported = ported
look.look = look.look .. " " .. ported
end
look.print = Fingerprint(look.look)
return look
end
local function IsGUID(token)
return strfind(token, "^0x") ~= nil
end
-- Party, raid and GUID tokens stay with their player; the others don't, so a
-- mount skin only goes on through the first kind (it has to come off the
-- moment that player dismounts).
local function Steady(token)
return IsGUID(token) or strfind(token, "^party%d$") ~= nil or strfind(token, "^raid%d+$") ~= nil
end
local function GroupToken(token)
return strfind(token, "^party%d$") ~= nil or strfind(token, "^raid%d+$") ~= nil
end
-- In range: within follow distance (about 28 yards), or a group member
-- (who hears us through hidden group messages, wherever they are).
local function Near(token)
if GroupToken(token) then
return true
end
local ok, close = pcall(CheckInteractDistance, token, 4)
return ok and close and true or false
end
-- VanillaHelpers errors on players it can't morph (out of sight, hostile).
local function Call(fn, token, a, b)
if b then
return pcall(fn, token, a, b)
end
return pcall(fn, token, a)
end
-- Puts a player's look on them. `force` sets every part again, for a player
-- the game has just built afresh (their morph is gone on our screen).
local function ApplyTo(name, token)
local peer = peers[name]
local want = NONE
if peer and Seeing() then
want = peer
end
local done = applied[name]
if not done and want == NONE then
return
end
local ok, shown = pcall(UnitDisplayInfo, token)
local force = not done or done.away or (ok and want.display and shown ~= want.display)
if not force and done.version == want.version then
done.scan = scanID
return
end
done = done or { items = {} }
if force or want.display ~= done.display then
local worked = Call(SetUnitDisplayID, token, want.display)
if IsGUID(token) then
guidWorks = worked
end
if not worked then
return
end
done.display = want.display
end
for slot = 1, 19 do
local item = want.items[slot]
if item ~= done.items[slot] or (force and item) then
if Call(SetUnitVisibleItemID, token, slot, item) then
done.items[slot] = item
end
end
end
local mount = want.mount
if mount and not Steady(token) then
mount = done.mount
end
if (mount ~= done.mount or (force and mount)) and type(SetUnitMountDisplayID) == "function" then
if Call(SetUnitMountDisplayID, token, mount) then
done.mount = mount
end
end
-- Their size, through DmorphHelper (which every DMorph user may not have):
-- their own size is read first, to put back when they go back to it.
local scale = want.scale
if (scale ~= done.scale or (force and scale)) and type(DmorphHelper_SetScale) == "function" then
if scale and not done.scale then
local ok, real = pcall(DmorphHelper_GetScale, token)
done.scaleReal = ok and type(real) == "number" and real or 1
end
if Call(DmorphHelper_SetScale, token, scale or done.scaleReal or 1) then
done.scale = scale
end
end
done.version, done.scan, done.away = want.version, scanID, nil
if want == NONE and not done.display and not done.mount and not next(done.items) and not done.scale then
applied[name] = nil
else
applied[name] = done
end
end
-- Asks a player for their look; with the fingerprint we have, they only
-- send it all if it changed.
local function Ask(name, print)
if not queued[name] then
table.insert(queue, name)
end
queued[name] = print or true
end
local function Unask(name)
if queued[name] then
queued[name] = nil
for i = 1, table.getn(queue) do
if queue[i] == name then
table.remove(queue, i)
break
end
end
end
end
-- Stores a player's look (new or remembered); returns the peer.
local function SetPeer(name, look, guid, confirmed)
local peer = peers[name]
if not peer then
if peerCount >= MAX_PEERS then
return
end
peerCount = peerCount + 1
end
look.guid = guid or (peer and peer.guid)
look.scale = peer and peer.scale -- a full look message sets it again after
look.version = (peer and peer.version or 0) + 1
look.heard = confirmed and clock or -1000
look.confirmed = confirmed
peers[name] = look
if confirmed then
Cache()[name] = { look = look.look, guid = look.guid, seen = time() }
end
return look
end
local function DropPeer(name)
if peers[name] then
peers[name] = nil
peerCount = peerCount - 1
end
Cache()[name] = nil
audience[name] = nil
end
-- A player we heard from earlier, maybe in another session.
local function Remembered(name)
local entry = Cache()[name]
if entry then
local _, _, d, m, it, ported = strfind(entry.look, "^(%S+) (%S+) (%S+) ?(%S*)$")
local look = MakeLook(d, m, it, ported)
if look then
return SetPeer(name, look, entry.guid, false)
end
Cache()[name] = nil
end
end
-- A unit token: put a known player's look on, or ask about a new face.
local function Visit(token)
local ok, isPlayer = pcall(UnitIsPlayer, token)
if not (ok and isPlayer) then
return
end
-- A group member far away has a token but no model to morph.
local seen, visible = pcall(UnitIsVisible, token)
if seen and not visible then
return
end
local name = UnitName(token)
if not name or name == UnitName("player") then
return
end
local peer = peers[name]
if not peer and Seeing() and UnitCanAssist("player", token) then
peer = Remembered(name)
end
-- Looks go on whatever the distance; messages only go to players in range.
local near = Near(token)
if peer or applied[name] then
local back = not applied[name] or applied[name].away
ApplyTo(name, token)
if peer and near then
audience[name] = clock -- they can see us too
if (back or not peer.confirmed) and clock - peer.heard > RECHECK
and clock - (asked[name] or -1000) > RECHECK then
Ask(name, peer.print) -- in case they changed while away
end
end
elseif near and (token == "target" or token == "mouseover" or GroupToken(token))
and Seeing() and UnitCanAssist("player", token) and not KnownWithout(name)
and (not asked[name] or clock - asked[name] > ASK_AGAIN) then
Ask(name)
end
end
local function Scan()
if type(SetUnitDisplayID) ~= "function" then
return
end
scanID = scanID + 1
ReadGroup()
Visit("target")
Visit("mouseover")
Visit("targettarget")
if GetNumRaidMembers() > 0 then
for i = 1, 40 do
Visit("raid" .. i)
end
else
for i = 1, 4 do
Visit("party" .. i)
Visit("party" .. i .. "target")
end
end
-- Without SuperWoW a GUID isn't a unit token, and UnitName finds nothing.
for name, peer in pairs(peers) do
if peer.guid then
local found, unitName = pcall(UnitName, peer.guid)
if found and unitName == name then
Visit(peer.guid)
end
end
end
-- Anyone we didn't see this time may be rebuilt without their look when
-- we next do, so it goes on again in full then.
for _, done in pairs(applied) do
if done.scan ~= scanID then
done.away = true
end
end
end
-- Sends the waiting questions, one message per way.
local function AskQueued()
local byWay, n = {}, 0
while table.getn(queue) > 0 and n < MAX_ASK do
local name = table.remove(queue, 1)
local print = queued[name]
queued[name] = nil
asked[name] = clock
if not peers[name] then
pending[name] = clock
end
local way = Way(name)
byWay[way] = byWay[way] or {}
if print == true then
table.insert(byWay[way], name)
else
table.insert(byWay[way], name .. ":" .. print)
end
n = n + 1
end
for way, names in pairs(byWay) do
Send("Q " .. table.concat(names, ","), way)
end
end
-- Messages from other clients ---------------------------------------------------
-- A player's size from a message's "s<percent>" token (none: normal size).
-- Clients before 2.6 send no token and ignore it.
local function SizeOf(token)
local _, _, pct = strfind(token or "", "^s(%d+)$")
pct = tonumber(pct)
if pct and pct >= 10 and pct <= 1000 and pct ~= 100 then
return pct / 100
end
end
local function SetPeerSize(name, scale)
local peer = peers[name]
if peer and peer.scale ~= scale then
peer.scale = scale
peer.version = peer.version + 1
Scan()
end
end
local function ReadLook(sender, rest)
local _, _, d, m, it, guid, more = strfind(rest, "^(%S+) (%S+) (%S+) ?(%S*) ?(.*)$")
local size, ported
for token in string.gfind(more or "", "%S+") do
if strfind(token, "^s%d+$") then
size = token
elseif strfind(token, "^P") then
ported = token
end
end
local look = MakeLook(d, m, it, ported)
if not look then
return
end
local scale = SizeOf(size)
if not strfind(guid or "", "^0x%x+$") then
guid = nil
end
Unask(sender)
Answered(sender)
asked[sender] = clock
local peer = peers[sender]
if peer and peer.look == look.look then
peer.heard, peer.guid = clock, guid or peer.guid
if not peer.confirmed then
peer.confirmed = true
Cache()[sender] = { look = peer.look, guid = peer.guid, seen = time() }
end
SetPeerSize(sender, scale)
return
end
if SetPeer(sender, look, guid, true) then
look.scale = scale
Scan()
end
end
-- Mounting or dismounting: only the mount part changed.
local function ReadMount(sender, m)
local peer = peers[sender]
if not (peer and peer.confirmed) then
return
end
local look = MakeLook(peer.d, m, peer.it, peer.ported)
if look and look.look ~= peer.look then
SetPeer(sender, look, nil, true)
Scan()
end
end
-- "Nothing changed": confirms what we remember, else we ask for it all.
local function ReadPrint(sender, print, guid, size)
Unask(sender)
Answered(sender)
asked[sender] = clock
local peer = peers[sender]
if peer and peer.print == print then
peer.heard = clock
SetPeerSize(sender, SizeOf(size))
if strfind(guid or "", "^0x%x+$") then
peer.guid = guid
end
if not peer.confirmed then
peer.confirmed = true
Cache()[sender] = { look = peer.look, guid = peer.guid, seen = time() }
end
else
asked[sender] = nil
Ask(sender)
end
end
local function ReadQuestion(sender, rest, way)
local me = UnitName("player")
for entry in string.gfind(rest, "[^,]+") do
local _, _, name, print = strfind(entry, "^([^:]+):?(.*)$")
if name == me then
if Sharing() then
audience[sender] = clock
if print ~= "" and print == Fingerprint(OwnFull()) then
replies[way] = replies[way] or "H"
else
replies[way] = "L"
end
local at = math.max(clock + 0.2 + math.random() * 0.8, (timers.replied or -100) + REPLY_GAP)
if not timers.reply or timers.reply > at then
timers.reply = at
end
end
elseif way == "CHANNEL" and queued[name] and Way(name) == "CHANNEL" then
-- Someone else asked first: the answer comes on the channel anyway
-- (and if none comes, they have no DMorph).
Unask(name)
asked[name] = clock
if not peers[name] then
pending[name] = clock
end
end
end
end
local function Receive(sender, text, way)
if not sender or sender == "" or sender == UnitName("player") then
return
end
local _, _, kind, rest = strfind(text or "", "^(%u) (.*)$")
if kind == "L" then
ReadLook(sender, rest)
elseif kind == "M" then
ReadMount(sender, rest)
elseif kind == "H" then
local _, _, print, guid, size = strfind(rest, "^(%S+) ?(%S*) ?(%S*)")
if print then
ReadPrint(sender, print, guid, size)
end
elseif kind == "S" then
-- Only the size changed.
local peer = peers[sender]
if peer and peer.confirmed then
SetPeerSize(sender, SizeOf("s" .. rest))
end
elseif kind == "Q" then
ReadQuestion(sender, rest, way)
end
end
-- Your own changes ------------------------------------------------------------
-- Your size as a message token: "s150", or "" at normal size.
local function OwnSize()
local scale = Sharing() and M.GetScale and M.GetScale()
local pct = scale and math.floor(scale * 100 + 0.5) or 100
if pct == 100 then
return ""
end
return "s" .. pct
end
local function OnChange()
local d, m, it = OwnParts()
-- Mounting and dismounting go out at once; other changes wait until
-- they stand still a moment.
if m ~= sent.m then
timers.mount = timers.mount or clock
end
if OwnSize() ~= sent.s then
timers.size = timers.size or clock + SETTLE
end
if OwnPorted() ~= sent.p then
-- A ported mount or model: the whole look goes (old DMorph ignores that part).
timers.first = timers.first or clock
timers.send = math.min(timers.send or clock, clock)
return
end
if d ~= sent.d or it ~= sent.it then
timers.first = timers.first or clock
timers.send = math.min(clock + SETTLE, timers.first + MAX_WAIT)
elseif timers.send then
timers.send, timers.first = nil, nil -- changed back before it went out
end
end
local function WithSize(text)
local size = OwnSize()
if size ~= "" then
return text .. " " .. size
end
return text
end
local function LookText()
local text = WithSize("L " .. OwnLook() .. " " .. (GUIDOf("player") or "-"))
local ported = OwnPorted()
if ported ~= "" then
text = text .. " " .. ported
end
return text
end
-- Tells everyone who needs it; that answers any waiting question too.
local function SendLook(ways)
sent.d, sent.m, sent.it = OwnParts()
sent.s = OwnSize()
sent.p = OwnPorted()
timers.send, timers.first, timers.mount, timers.size = nil, nil, nil, nil
local text = LookText()
for way in pairs(ways) do
Send(text, way)
replies[way] = nil
end
if ways.CHANNEL then
replies = {} -- everyone with DMorph is on it
end
end
-- At most one message (to each way it needs) per pass, the most urgent first.
local function SendNext()
if timers.next and clock < timers.next then
return
end
if timers.mount and clock >= timers.mount then
timers.mount = nil
local _, m = OwnParts()
if m ~= sent.m then
sent.m = m
for way in pairs(ChangeWays()) do
Send("M " .. m, way)
end
end
elseif timers.send and clock >= timers.send then
SendLook(ChangeWays())
elseif timers.size and clock >= timers.size then
timers.size = nil
local size = OwnSize()
if size ~= sent.s then
sent.s = size
for way in pairs(ChangeWays()) do
Send("S " .. (size == "" and "100" or strsub(size, 2)), way)
end
end
elseif timers.reply and clock >= timers.reply then
timers.reply, timers.replied = nil, clock
local look = OwnFull()
for way, kind in pairs(replies) do
replies[way] = nil
if kind == "L" then
Send(LookText(), way)
else
Send(WithSize("H " .. Fingerprint(look) .. " " .. (GUIDOf("player") or "-")), way)
end
end
elseif table.getn(queue) > 0 and clock >= (timers.query or 0) then
timers.query = clock + QUERY_GAP
AskQueued()
end
end
-- Remembered players who don't answer have stopped using DMorph, and new
-- faces who don't answer have none: neither is asked again for a day.
local function ForgetSilent()
for name, peer in pairs(peers) do
if not peer.confirmed and asked[name] and not queued[name] and clock - asked[name] > CONFIRM_WAIT then
DropPeer(name)
NoDmorph()[name] = time()
end
end