-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathFarmMap.lua
More file actions
4075 lines (3579 loc) · 165 KB
/
Copy pathFarmMap.lua
File metadata and controls
4075 lines (3579 loc) · 165 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
-- ============================================================
-- FarmMap — Addon principal
-- Auteur : Kroosstii (Dkroosstii-Dalaran)
--
-- Version et date : dans FarmMap.toc (## Version, ## X-Date), lues
-- depuis celui-ci. Ne pas les recopier ici : c'est ce qui a laisse le
-- .toc annoncer une version pendant que l'addon en affichait une autre.
-- ============================================================
-- ns: table shared by every file of the addon.
-- The lang\*.lua files register their translations in it before
-- this file is loaded (see the .toc order).
local addonName, ns = ...
-- Single source of truth for both: the .toc. C_AddOns.GetAddOnMetadata is the
-- modern entry point, the bare global having been removed in 11.0 - the
-- fallback only covers a client older than every interface this addon targets.
local GetMeta = C_AddOns and C_AddOns.GetAddOnMetadata or GetAddOnMetadata
local addonVersion = GetMeta(addonName, "Version") or "?"
local lastUpdate = GetMeta(addonName, "X-Date") or "?"
-- Libs
local HBD = LibStub("HereBeDragons-2.0")
local HBDPins = LibStub("HereBeDragons-Pins-2.0")
-- Main frame for events
local eventFrame = CreateFrame("Frame")
eventFrame:RegisterAllEvents()
-- DB schema version (migrations)
local DB_VERSION = 4
-- Client locale, read once at load
local gameLocale = GetLocale()
-- Font of the floating harvest text. Contributed by bluse: the path was
-- hardcoded to FRIZQT__.TTF, which carries no CJK glyph - on a Chinese or
-- Korean client the harvested item name showed up as empty boxes.
--
-- Indexed on GetLocale() and not on gameLocale: the floating text shows item
-- names, which the game always returns in the CLIENT language, never in the
-- language forced in the options. Both values match here, but gameLocale
-- changes when the player forces a language - indexing on it would pick the
-- Korean font to display French names.
--
-- Falls back to STANDARD_TEXT_FONT rather than FRIZQT__.TTF: Blizzard resolves
-- the correct client font there, including for locales absent from the table.
local LOCALE_FONTS = {
zhCN = "Fonts\\ARKai_T.ttf",
koKR = "Fonts\\2002.TTF",
zhTW = "Fonts\\blei00d.TTF",
ruRU = "Fonts\\FRIZQT___CYRILLIC.TTF",
}
local FLOATING_FONT = LOCALE_FONTS[GetLocale()] or STANDARD_TEXT_FONT or "Fonts\\FRIZQT__.TTF"
-- ============================================================
-- FORWARD DECLARATIONS
-- (functions defined further down but referenced above)
-- ============================================================
local RefreshAllPins
local RefreshWorldMapPins
local OpenDebugCopyPopup
local RecordStat
local RecordNodeStat
local GetNodeStat
local OpenExportPopup
local OpenImportPopup
-- ============================================================
-- BUTTON SIZING
-- ============================================================
-- Every button label goes through a translation, and a label that fits
-- in English rarely fits everywhere: "Clear database" is 14 characters,
-- "Vider la base de données" is 24 and "Очистить базу данных" is 20.
-- A fixed width silently spills the text past the button border.
--
-- Grows only. The width passed to SetSize stays the floor, so a language
-- that already fits keeps the exact layout it has today.
local BUTTON_TEXT_PADDING = 24
local function FitButton(btn, minWidth)
local fs = btn:GetFontString()
if not fs then return end
minWidth = minWidth or btn:GetWidth()
local function apply()
local w = fs:GetStringWidth()
if w and w > 0 then
btn:SetWidth(math.max(minWidth, math.ceil(w) + BUTTON_TEXT_PADDING))
end
end
-- Usually right away, but GetStringWidth returns 0 on a string the
-- client has not laid out yet - the next frame always has it.
apply()
C_Timer.After(0, apply)
end
-- ============================================================
-- CONSTANTS & STATIC DATA
-- ============================================================
-- Expansion names by expID (returned by GetItemInfo).
-- Last resort only: resolution goes through the active language file first,
-- then through Blizzard's global, which is already localized.
local EXP_FALLBACK = {
[0] = "Classic", [1] = "TBC",
[2] = "WotLK", [3] = "Cata",
[4] = "MoP", [5] = "WoD",
[6] = "Legion", [7] = "BfA",
[8] = "Shadowlands", [9] = "Dragonflight",
[10] = "The War Within",[11] = "Midnight",
}
-- Resolves an expansion name for display.
--
-- The order is deliberate:
--
-- 1. the EXP_<id> key of the ACTIVE language file, read straight from its
-- own table via rawget and never from L. L holds the English fallback:
-- relying on it would return "Midnight" to a Korean player when Blizzard
-- already knows how to say it in Korean. That is the reported bug itself.
-- 2. EXPANSION_NAME<id>, Blizzard's global. Correct in all 12 client
-- languages, with no translator work at all, and with the official
-- terminology rather than an approximation.
-- 3. the table above, if the global does not exist yet for an expansion
-- that is too recent.
--
-- A translator who wants to impose their own wording only has to define
-- EXP_<id> in their file: it then takes precedence over Blizzard.
local function ExpansionName(expID)
if type(expID) ~= "number" then return nil end
local loc = ns.locales[gameLocale]
local own = loc and loc.strings and rawget(loc.strings, "EXP_" .. expID)
if type(own) == "string" and own ~= "" then return own end
local blizzard = _G["EXPANSION_NAME" .. expID]
if type(blizzard) == "string" and blizzard ~= "" then return blizzard end
return EXP_FALLBACK[expID]
end
-- Skill line IDs used for profession detection
local PROFESSION_SKILL_IDS = {
Herbo = 182,
Minage = 186,
Peche = 356,
}
-- Harvest spellIDs -> node type
-- Add here the IDs discovered through the [SPELL_RAW] debug output
local HARVEST_SPELLS = {
-- Herbo
-- Midnight
[471009] = "Herbo",
-- Classic
[265819] = "Herbo",
-- Minage
-- Midnight
[471013] = "Minage",
-- Classic
[265837] = "Minage",
-- Fishing
[131474] = "Peche",
[131476] = "Peche",
[1225292] = "Peche",
-- Logging
[1239682] = "Bois",
}
-- RGB colors per type (used for pins and for the UI)
local TYPE_COLORS = {
Herbo = {0.2, 0.8, 0.2},
Minage = {0.8, 0.5, 0.0},
Peche = {0.2, 0.6, 1.0},
Bois = {0.9, 0.6, 0.1},
-- Primordial / abundant nodes (gold tint)
HerboR = {0.6, 1.0, 0.2},
MinageR = {1.0, 0.82, 0.0},
PecheR = {0.4, 0.9, 1.0},
}
-- Fast lookup for rich nodes (gold tint on the pins)
local RICH_TYPES = { HerboR = true, MinageR = true, PecheR = true }
-- Display order of the types in the colors panel
local TYPE_ORDER = { "Peche", "Herbo", "Minage", "Bois" }
-- Chemins de textures
local TEX_PATH = "Interface\\AddOns\\FarmMap\\Textures\\"
local BLIP_DEFAULT = "Interface\\Minimap\\ObjectIconsAtlas"
-- 12.0.7+ compat: Minimap:SetBlipTexture() (and the sibling methods
-- SetPlayerTexture, SetPOIArrowTexture, SetCorpsePOIArrowTexture, SetStaticPOIArrowTexture)
-- were removed by Blizzard in 12.0.7, with NO replacement (unofficial Widgets
-- changelog: 6 removals, 0 additions). The native atlas replacement feature is
-- disabled as a result: HAS_NATIVE_BLIP is pinned to false below. Should Blizzard
-- ever restore those methods, uncomment the dynamic detection line (and drop the
-- "= false" underneath): everything else (ApplyMinimapStyle, the useBlip checks,
-- and so on) falls back to the native system with no further change needed.
-- local HAS_NATIVE_BLIP = type(Minimap.SetBlipTexture) == "function"
local HAS_NATIVE_BLIP = false
local function SetNativeBlip(tex)
if HAS_NATIVE_BLIP then
Minimap:SetBlipTexture(tex)
end
end
-- Blip atlas for the Blizzard replacement (minimap only)
local BLIP_TEXTURES = {
blank = TEX_PATH .. "atlas-blip-farmmap-whiteoutline",
vivid = TEX_PATH .. "atlas-blip-farmmap-vivid",
deuteranopia = TEX_PATH .. "atlas-blip-farmmap-deuteranopia",
protanopia = TEX_PATH .. "atlas-blip-farmmap-protanopia",
tritanopia = TEX_PATH .. "atlas-blip-farmmap-tritanopia",
}
-- UV coordinates inside the Blizzard atlas (ObjectIconsAtlas) - world map and fallback.
-- Calibrated for patch 12.0.7 with the /fm atlas calibrator (drag and drop,
-- 1024x1024 canvas, 32x32 icons). MinageR keeps its former position
-- (the shiny ore did not move): it is the only "rich" variant that does not
-- automatically follow its base type.
local WORLD_MAP_TEXCOORDS = {
Minage = {0.5073, 0.5385, 0.6070, 0.6352},
Herbo = {0.5095, 0.5407, 0.5753, 0.6066},
Peche = {0.5081, 0.5401, 0.5389, 0.5684},
Bois = {0.4420, 0.4732, 0.4728, 0.5041},
MinageR = {0.5073, 0.5385, 0.6070, 0.6352},
HerboR = {0.5095, 0.5407, 0.5753, 0.6066},
PecheR = {0.5081, 0.5401, 0.5389, 0.5684},
}
-- Built-in pins: UV coordinates inside each blip atlas for the HBDPins pins.
-- Same structure as external packs (pins per type). References
-- WORLD_MAP_TEXCOORDS instead of duplicating the coordinates: one single table
-- to update per patch (see /fm atlas).
local BUILTIN_PINS = {}
for presetKey, texPath in pairs(BLIP_TEXTURES) do
BUILTIN_PINS[presetKey] = {
Minage = { tex = texPath, coords = WORLD_MAP_TEXCOORDS.Minage },
Herbo = { tex = texPath, coords = WORLD_MAP_TEXCOORDS.Herbo },
Peche = { tex = texPath, coords = WORLD_MAP_TEXCOORDS.Peche },
Bois = { tex = texPath, coords = WORLD_MAP_TEXCOORDS.Bois },
MinageR = { tex = texPath, coords = WORLD_MAP_TEXCOORDS.MinageR },
HerboR = { tex = texPath, coords = WORLD_MAP_TEXCOORDS.HerboR },
PecheR = { tex = texPath, coords = WORLD_MAP_TEXCOORDS.PecheR },
}
end
-- WoW rarity colors (quality 0->6)
local QUALITY_COLORS = {
[0] = "ff9d9d9d", -- Gris
[1] = "ffffffff", -- Blanc
[2] = "ff1eff00", -- Vert
[3] = "ff0070dd", -- Bleu
[4] = "ffa335ee", -- Violet
[5] = "ffff8000", -- Orange
[6] = "ffe6cc80", -- Beige
}
-- Gathering reagent tier icons: the silver diamond and the gold pentagon.
-- "quality12" is the 1-of-2 set, which is all gathered reagents ever have -
-- the five-tier set belongs to crafting reagents and does not apply here.
local TIER_TEXTURES = {
[1] = "Interface\\Professions\\professionsquality12tier1.blp",
[2] = "Interface\\Professions\\professionsquality12tier2.blp",
}
-- ============================================================
-- EXTERNAL STYLE SYSTEM
-- Public API for sub-addons (FarmMap_Colors_*)
--
-- Exemple d'utilisation depuis un sous-addon :
-- FarmMapStyles.Register("monpack", {
-- label = "Mon Pack",
-- pins = {
-- Herbo = { tex = "Interface\\AddOns\\MonPack\\icons", coords = {...} },
-- Minage = { tex = "...", coords = {...} },
-- Peche = { tex = "...", coords = {...} },
-- Bois = { tex = "...", coords = {...} },
-- },
-- blip = "Interface\\AddOns\\MonPack\\atlas-blip", -- optionnel
-- })
-- ============================================================
FarmMapStyles = {}
local _registeredStyles = {}
-- Registers a style pack. Triggers a rebuild of the Colors panel if the UI is already loaded.
function FarmMapStyles.Register(styleKey, data)
if _registeredStyles[styleKey] then
print("|cffffd100FarmMap :|r Style déjà enregistré : " .. styleKey)
return
end
if not data or not data.label then
print("|cffffd100FarmMap :|r Style invalide (label manquant) : " .. tostring(styleKey))
return
end
_registeredStyles[styleKey] = data
if FarmMap_OnStyleRegistered then FarmMap_OnStyleRegistered(styleKey, data) end
end
-- Returns the data of a registered style (nil if unknown)
function FarmMapStyles.Get(styleKey)
return _registeredStyles[styleKey]
end
-- Returns the list of every style: { key, label, hasBlip }
function FarmMapStyles.GetAll()
local list = {}
for k, v in pairs(_registeredStyles) do
table.insert(list, { key = k, label = v.label, hasBlip = v.blip ~= nil })
end
return list
end
-- ============================================================
-- LOCALISATION
-- Languages live in lang\<locale>.lua and register themselves
-- into ns.locales (see lang\README.md).
--
-- Adding a language = drop a file in and add one line to the
-- .toc. No change to this file is ever required.
--
-- Any key a translation leaves out falls back automatically
-- to English: a partial translation never breaks
-- l'addon et n'affiche jamais de texte vide.
-- ============================================================
ns.locales = ns.locales or {}
local L = {}
-- Rebuilds L in place: the UI closures keep the same table
-- reference. English base, then overridden by the requested
-- language - hence the automatic fallback to English.
local function ApplyLanguage(lang)
wipe(L)
local base = ns.locales.enUS and ns.locales.enUS.strings
if base then
for k, v in pairs(base) do L[k] = v end
end
local target = ns.locales[lang] and ns.locales[lang].strings
if target and target ~= base then
for k, v in pairs(target) do L[k] = v end
end
gameLocale = lang
end
-- List of available languages, sorted for the Language panel.
local function GetAvailableLocales()
local list = {}
for code, data in pairs(ns.locales) do
list[#list + 1] = { code = code, data = data }
end
table.sort(list, function(a, b)
local oa, ob = a.data.order or 999, b.data.order or 999
if oa ~= ob then return oa < ob end
return a.code < b.code
end)
return list
end
-- Label of a language: endonym plus latin name as a fallback.
-- On a FR/EN client "한국어" may render as empty boxes; the latin
-- suffix guarantees the entry stays identifiable.
local function GetLocaleLabel(data)
if data.latinName and data.latinName ~= data.name then
return data.name .. " (" .. data.latinName .. ")"
end
return data.name or "?"
end
-- Returns a string in the CLIENT language, regardless of the language
-- forced in the addon. Used by /fm default: it is the emergency command
-- for when someone forced a language they cannot read - or can read
-- without understanding it. Confirming in the language we switch back
-- to is the only thing that is reliably useful.
local function ClientString(key)
local loc = ns.locales[GetLocale()] or ns.locales.enUS
return (loc and loc.strings and loc.strings[key]) or L[key] or ""
end
ApplyLanguage(gameLocale)
-- ============================================================
-- DATABASE MIGRATIONS
-- ============================================================
local migrations = {}
-- V1: added the name, expName, items and type fields
migrations[1] = function()
local fixed = 0
for mapID, nodes in pairs(FarmMapDB) do
if type(nodes) == "table" then
for _, node in ipairs(nodes) do
if node.name == nil then node.name = node.type or L.UNKNOWN ; fixed = fixed + 1 end
if node.expName == nil then node.expName = L.UNKNOWN_EXP ; fixed = fixed + 1 end
if node.items == nil then node.items = {} ; fixed = fixed + 1 end
if node.type == nil then node.type = L.UNKNOWN ; fixed = fixed + 1 end
end
end
end
return fixed
end
-- V2: added the itemIDs, nameID and locale fields
migrations[2] = function()
local fixed = 0
for mapID, nodes in pairs(FarmMapDB) do
if type(nodes) == "table" then
for _, node in ipairs(nodes) do
if node.itemIDs == nil then node.itemIDs = {} ; fixed = fixed + 1 end
if node.nameID == nil then node.nameID = 0 ; fixed = fixed + 1 end
if node.locale == nil then node.locale = "unknown" ; fixed = fixed + 1 end
end
end
end
return fixed
end
-- Retrouve l'itemID a partir d'un nom d'objet.
--
-- GetItemInfo accepts a name but does not return the id: it has to go through
-- the link and extract it. Two limits drive everything else:
-- - the item must be in the client cache, otherwise nil;
-- - the name must be in the client language.
-- A node imported from a French database is therefore unrecoverable on a Korean
-- client: the id was never written, and the name cannot be looked up.
local function ItemIDFromName(name)
if type(name) ~= "string" or name == "" then return nil end
local _, link = GetItemInfo(name)
if type(link) ~= "string" then return nil end
local id = link:match("item:(%d+)")
return id and tonumber(id) or nil
end
-- Rebuilds the missing identifiers of already recorded nodes, so that the
-- display can re-localize them instead of staying stuck on the stored name.
--
-- Deliberately re-runnable: GetItemInfo depends on the client cache, which
-- fills up over the session. A first pass at load recovers some of them, a
-- later click on "Update DB" recovers more. That is why ManualMigration
-- always calls it, even when the schema version is already up to date.
local function BackfillItemIDs()
local fixed = 0
for _, nodes in pairs(FarmMapDB) do
if type(nodes) == "table" then
for _, node in ipairs(nodes) do
if type(node) == "table" then
if (not node.nameID or node.nameID == 0) and node.name then
local id = ItemIDFromName(node.name)
if id then node.nameID = id ; fixed = fixed + 1 end
end
if type(node.items) == "table" then
node.itemIDs = node.itemIDs or {}
for idx, itemName in ipairs(node.items) do
if not node.itemIDs[idx] or node.itemIDs[idx] == 0 then
local id = ItemIDFromName(itemName)
if id then node.itemIDs[idx] = id ; fixed = fixed + 1 end
end
end
end
end
end
end
end
return fixed
end
-- V3: recovery of missing identifiers (old nodes, or nodes imported from a
-- database exported before v1.5.2, which did not carry the ids).
migrations[3] = BackfillItemIDs
-- V4 : recuperation de l'expID a partir du nom d'extension stocke.
--
-- expName has always been written from the hardcoded English table, whatever
-- the player's language: the reverse lookup is therefore reliable and does not
-- depend on the client. A node harvested before this version thus recovers its
-- identifier, and its tooltip starts speaking the player's language.
--
-- Also covers the "ID: 11" strings produced by the old fallback when the item
-- was not yet cached at harvest time.
migrations[4] = function()
local byName = {}
for id, name in pairs(EXP_FALLBACK) do byName[name] = id end
local fixed = 0
for _, nodes in pairs(FarmMapDB) do
if type(nodes) == "table" then
for _, node in ipairs(nodes) do
if type(node) == "table" and node.expID == nil
and type(node.expName) == "string" then
local id = byName[node.expName]
if not id then
id = tonumber(node.expName:match("^ID:%s*(%d+)$") or "")
end
if id then node.expID = id ; fixed = fixed + 1 end
end
end
end
end
return fixed
end
local function RunMigrations(verbose)
local currentVersion = FarmMapDB.version or 0
if currentVersion >= DB_VERSION then
if verbose then
print("|cffffd100FarmMap :|r " .. L.MIGR_DONE .. " (v" .. DB_VERSION .. ").")
end
return
end
local totalFixed = 0
for v = currentVersion + 1, DB_VERSION do
if migrations[v] then
local fixed = migrations[v]()
totalFixed = totalFixed + (fixed or 0)
print("|cffffd100" .. L.MIGR_PREFIX .. "|r v" .. v .. " : " .. (fixed or 0) .. (gameLocale == "frFR" and " correction(s)" or " fix(es)"))
end
end
FarmMapDB.version = DB_VERSION
if totalFixed > 0 then
print("|cffffd100FarmMap :|r " .. L.MIGR_TOTAL .. " " .. totalFixed .. L.MIGR_ENTRIES)
else
print("|cffffd100FarmMap :|r " .. L.MIGR_DONE .. " (v" .. DB_VERSION .. ").")
end
end
-- The "Update DB" button always re-runs the id recovery, even when the schema
-- is already up to date: it depends on the client item cache, so a second click
-- later in the session recovers more. Without this, a player whose cache was
-- cold at load would be left with nodes that cannot be re-localized, and no way
-- to try again.
local function ManualMigration()
RunMigrations(true)
local recovered = BackfillItemIDs()
if recovered > 0 then
print("|cffffd100" .. L.MIGR_PREFIX .. "|r " .. recovered .. L.MIGR_ENTRIES)
RefreshAllPins()
end
end
-- ============================================================
-- PROFESSIONS
-- ============================================================
local playerProfessions = {}
local function CheckProfessions()
playerProfessions = {}
-- GetProfessions(): prof1, prof2, archaeology, fishing, cooking
-- ipairs stops at the first nil -> fishing never detected without this fix
local p1, p2, p3, p4, p5 = GetProfessions()
for _, index in ipairs({ p1 or false, p2 or false, p3 or false, p4 or false, p5 or false }) do
if index then
local _, _, _, _, _, _, skillLine = GetProfessionInfo(index)
for profType, id in pairs(PROFESSION_SKILL_IDS) do
if skillLine == id then playerProfessions[profType] = true end
end
end
end
-- If no profession was detected, the data is not ready yet:
-- leave the persisted states alone rather than wrongly overwriting them.
local anyProfFound = next(playerProfessions) ~= nil
if not anyProfFound then return end
local checks = {
{ key = "showHerbo", prof = "Herbo", label = L.TYPE_Herbo },
{ key = "showMinage", prof = "Minage", label = L.TYPE_Minage },
{ key = "showPeche", prof = "Peche", label = L.TYPE_Peche },
}
for _, c in ipairs(checks) do
if FarmMapDB[c.key] and not playerProfessions[c.prof] then
FarmMapDB[c.key] = false
print("|cffffd100FarmMap :|r " .. c.label .. L.PROF_DISABLED)
end
end
end
-- ============================================================
-- DB SERIALIZATION (import / export)
-- ============================================================
-- The item identifiers (nameID, itemIDs) are exported alongside the names.
-- Without them an imported node stays frozen in the language of whoever
-- exported it: the display can re-localize through GetItemInfo(id) (see the
-- tooltip), but only when the id is present. It was absent from the original
-- format, so re-localization only ever served one's own harvests - never a
-- shared database, which is precisely where it matters.
--
-- items and itemIDs must stay aligned index by index: the tooltip reads
-- itemIDs[idx] for item idx. Old nodes without itemIDs therefore emit 0,
-- a value the display already treats as "no id".
local function SerializeDB()
local lines = { "return {" }
for mapID, nodes in pairs(FarmMapDB) do
if type(nodes) == "table" and #nodes > 0 then
table.insert(lines, " [" .. mapID .. "]={")
for _, n in ipairs(nodes) do
local items, itemIDs = {}, {}
for idx, item in ipairs(n.items or {}) do
table.insert(items, string.format("%q", item))
-- tonumber() and not tostring(): a non-numeric id would come
-- out unquoted and produce an export with invalid Lua,
-- hence a database that cannot be imported back.
local iid = tonumber(n.itemIDs and n.itemIDs[idx]) or 0
table.insert(itemIDs, string.format("%d", iid))
end
table.insert(lines, string.format(
" {x=%.6f,y=%.6f,type=%q,name=%q,nameID=%d,expID=%d,expName=%q,items={%s},itemIDs={%s}},",
n.x, n.y, n.type or "", n.name or "", tonumber(n.nameID) or 0,
tonumber(n.expID) or -1, n.expName or "",
table.concat(items, ","), table.concat(itemIDs, ",")
))
end
table.insert(lines, " },")
end
end
table.insert(lines, "}")
return table.concat(lines, "\n")
end
local function DeserializeDB(str)
local fn, err = loadstring(str)
if not fn then return nil, "Syntaxe invalide : " .. (err or "") end
local ok, data = pcall(fn)
if not ok then return nil, "Erreur d'exécution : " .. (data or "") end
if type(data) ~= "table" then return nil, "Format non reconnu (table attendue)" end
return data
end
-- ============================================================
-- MINIMAP : STYLE & BLIP
-- ============================================================
-- Returns the pins table of a style (built-in or external)
local function GetStylePins(styleKey)
local ext = FarmMapStyles.Get(styleKey)
if ext and ext.pins then return ext.pins end
return BUILTIN_PINS[styleKey]
end
local function ApplyMinimapStyle(style)
local extStyle = FarmMapStyles.Get(style)
local blipTex = (extStyle and extStyle.blip) or BLIP_TEXTURES[style]
if HAS_NATIVE_BLIP and FarmMapDB and FarmMapDB.replaceBlip and blipTex then
SetNativeBlip(blipTex)
HBDPins:RemoveAllMinimapIcons(addonName)
RefreshWorldMapPins()
else
SetNativeBlip(BLIP_DEFAULT)
RefreshAllPins()
end
end
-- ============================================================
-- DEBUG WINDOW
-- ============================================================
local debugHistory = {}
local debugActive = false
local debugFrame = CreateFrame("Frame", "FarmMapDebug", UIParent, "BackdropTemplate")
debugFrame:SetSize(300, 200)
debugFrame:SetPoint("CENTER", UIParent, "CENTER", 0, -120)
debugFrame:SetBackdrop({
bgFile = "Interface\\ChatFrame\\ChatFrameBackground",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 12,
insets = {left=3, right=3, top=3, bottom=3}
})
debugFrame:SetBackdropColor(0, 0, 0.15, 0.92)
debugFrame:SetBackdropBorderColor(0.4, 0.4, 0.6, 1)
debugFrame:SetMovable(true)
debugFrame:SetResizable(true)
debugFrame:SetResizeBounds(220, 120)
debugFrame:EnableMouse(true)
debugFrame:Hide()
local debugTitleBar = CreateFrame("Frame", nil, debugFrame, "BackdropTemplate")
debugTitleBar:SetHeight(20)
debugTitleBar:SetPoint("TOPLEFT", debugFrame, "TOPLEFT", 3, -3)
debugTitleBar:SetPoint("TOPRIGHT", debugFrame, "TOPRIGHT", -3, -3)
debugTitleBar:SetBackdrop({bgFile="Interface\\ChatFrame\\ChatFrameBackground"})
debugTitleBar:SetBackdropColor(0, 0.05, 0.2, 0.95)
debugTitleBar:EnableMouse(true)
debugTitleBar:RegisterForDrag("LeftButton")
debugTitleBar:SetScript("OnDragStart", function() debugFrame:StartMoving() end)
debugTitleBar:SetScript("OnDragStop", function() debugFrame:StopMovingOrSizing() end)
local debugIcon = debugTitleBar:CreateTexture(nil, "OVERLAY")
debugIcon:SetSize(14, 14)
debugIcon:SetPoint("LEFT", debugTitleBar, "LEFT", 4, 0)
debugIcon:SetTexture("Interface\\Minimap\\ObjectIconsAtlas")
debugIcon:SetTexCoord(0.444, 0.475, 0.805, 0.837)
local debugTitleText = debugTitleBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
debugTitleText:SetPoint("LEFT", debugTitleBar, "LEFT", 22, 0)
debugTitleText:SetText(L.DEBUG_TITLE)
debugTitleText:SetTextColor(0.7, 0.8, 1, 1)
local debugCloseBtn = CreateFrame("Button", nil, debugTitleBar)
debugCloseBtn:SetSize(16, 16)
debugCloseBtn:SetPoint("RIGHT", debugTitleBar, "RIGHT", -4, 0)
local debugCloseTex = debugCloseBtn:CreateTexture(nil, "OVERLAY")
debugCloseTex:SetAllPoints()
debugCloseTex:SetTexture("Interface\\Buttons\\UI-Panel-MinimizeButton-Up")
debugCloseBtn:SetScript("OnClick", function()
FarmMapDB.showDebug = false
debugFrame:Hide()
if FarmMapShowDebugCheck then FarmMapShowDebugCheck:SetChecked(false) end
end)
local debugText = debugFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
debugText:SetPoint("TOPLEFT", debugFrame, "TOPLEFT", 8, -28)
debugText:SetPoint("BOTTOMRIGHT", debugFrame, "BOTTOMRIGHT", -8, 34)
debugText:SetJustifyH("LEFT")
debugText:SetJustifyV("TOP")
debugText:SetWordWrap(true)
local debugBottomBar = CreateFrame("Frame", nil, debugFrame, "BackdropTemplate")
debugBottomBar:SetHeight(26)
debugBottomBar:SetPoint("BOTTOMLEFT", debugFrame, "BOTTOMLEFT", 3, 3)
debugBottomBar:SetPoint("BOTTOMRIGHT", debugFrame, "BOTTOMRIGHT", -3, 3)
debugBottomBar:SetBackdrop({bgFile="Interface\\ChatFrame\\ChatFrameBackground"})
debugBottomBar:SetBackdropColor(0, 0, 0.1, 0.8)
local debugCheckCapture = CreateFrame("CheckButton", nil, debugBottomBar, "UICheckButtonTemplate")
debugCheckCapture:SetSize(20, 20)
debugCheckCapture:SetPoint("LEFT", debugBottomBar, "LEFT", 2, 0)
local debugCheckLabel = debugBottomBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
debugCheckLabel:SetPoint("LEFT", debugCheckCapture, "RIGHT", 0, 0)
debugCheckLabel:SetText("|cffaaaaaa" .. L.DEBUG_CAPTURE .. "|r")
debugCheckCapture:SetScript("OnClick", function(self)
debugActive = self:GetChecked()
FarmMapDB.debugCapture = debugActive
end)
local debugClearBtn = CreateFrame("Button", nil, debugBottomBar, "UIPanelButtonTemplate")
debugClearBtn:SetSize(55, 18)
debugClearBtn:SetPoint("LEFT", debugCheckLabel, "RIGHT", 8, 0)
debugClearBtn:SetText(L.DEBUG_CLEAR)
FitButton(debugClearBtn)
debugClearBtn:SetScript("OnClick", function()
debugHistory = {}
debugFrame:SetContent("")
end)
local debugCopyBtn = CreateFrame("Button", nil, debugBottomBar, "UIPanelButtonTemplate")
debugCopyBtn:SetSize(55, 18)
debugCopyBtn:SetPoint("LEFT", debugClearBtn, "RIGHT", 4, 0)
debugCopyBtn:SetText(L.DEBUG_COPY)
FitButton(debugCopyBtn)
debugCopyBtn:SetScript("OnClick", function() OpenDebugCopyPopup() end)
local debugGrip = CreateFrame("Button", nil, debugFrame)
debugGrip:SetSize(14, 14)
debugGrip:SetPoint("BOTTOMRIGHT", debugFrame, "BOTTOMRIGHT", -2, 26)
local debugGripTex = debugGrip:CreateTexture(nil, "OVERLAY")
debugGripTex:SetAllPoints()
debugGripTex:SetTexture("Interface\\Buttons\\UI-MicroButton-MainMenu-Up")
debugGripTex:SetTexCoord(0, 1, 0, 1)
debugGrip:SetScript("OnMouseDown", function() debugFrame:StartSizing("BOTTOMRIGHT") end)
debugGrip:SetScript("OnMouseUp", function() debugFrame:StopMovingOrSizing() end)
debugFrame:SetScript("OnSizeChanged", function(self)
debugText:SetPoint("TOPLEFT", self, "TOPLEFT", 8, -28)
debugText:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", -8, 34)
end)
debugFrame.SetContent = function(self, txt)
debugText:SetText(txt or "")
end
local function RenderDebug()
local txt = ""
for _, line in ipairs(debugHistory) do txt = txt .. line .. "\n" end
debugFrame:SetContent(txt)
end
local function AddDebug(event, detail)
if not debugActive then return end
local msg = "|cffaaaaaa[" .. event .. "]|r " .. (detail or "")
table.insert(debugHistory, 1, msg)
if #debugHistory > 200 then table.remove(debugHistory) end
RenderDebug()
end
OpenDebugCopyPopup = function()
local function PopulateEditBox(eb)
local rawLines = {}
for _, line in ipairs(debugHistory) do
local clean = line:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", "")
table.insert(rawLines, clean)
end
eb:SetText(table.concat(rawLines, "\n"))
eb:HighlightText()
end
if FarmMapDebugCopy then
if FarmMapDebugCopy.editBox then PopulateEditBox(FarmMapDebugCopy.editBox) end
FarmMapDebugCopy:Show()
return
end
local popup = CreateFrame("Frame", "FarmMapDebugCopy", UIParent, "BackdropTemplate")
popup:SetSize(400, 300)
popup:SetPoint("CENTER")
popup:SetFrameStrata("DIALOG")
popup:SetBackdrop({bgFile="Interface\\ChatFrame\\ChatFrameBackground", edgeFile="Interface\\Tooltips\\UI-Tooltip-Border", tile=true, tileSize=16, edgeSize=16, insets={left=4,right=4,top=4,bottom=4}})
popup:SetBackdropColor(0, 0, 0, 0.95)
popup:EnableMouse(true)
local popupTitle = popup:CreateFontString(nil, "OVERLAY", "GameFontNormal")
popupTitle:SetPoint("TOP", popup, "TOP", 0, -10)
popupTitle:SetText(L.DEBUG_COPY_TITLE)
popupTitle:SetTextColor(1, 0.82, 0, 1)
local editBox = CreateFrame("EditBox", nil, popup)
editBox:SetMultiLine(true)
editBox:SetMaxLetters(0)
editBox:SetFontObject(GameFontNormalSmall)
editBox:SetPoint("TOPLEFT", popup, "TOPLEFT", 10, -30)
editBox:SetPoint("BOTTOMRIGHT", popup, "BOTTOMRIGHT", -10, 30)
editBox:SetAutoFocus(true)
popup.editBox = editBox
local closePopup = CreateFrame("Button", nil, popup, "UIPanelButtonTemplate")
closePopup:SetSize(80, 22)
closePopup:SetPoint("BOTTOM", popup, "BOTTOM", 0, 6)
closePopup:SetText(L.CLOSE)
FitButton(closePopup)
closePopup:SetScript("OnClick", function() popup:Hide() end)
PopulateEditBox(editBox)
popup:Show()
end
-- ============================================================
-- PINS & TOOLTIPS
-- ============================================================
-- Identity of a node inside the per-node statistics store. Built from the
-- coordinates ALREADY STORED on the node, never from the player's position at
-- harvest time: the proximity merge at record time refreshes a node's name and
-- loot but never its x/y, so those stay a fixed anchor for the life of the
-- entry. The raw type is kept in the key (a rich vein is its own database
-- entry), while the entry itself stores the type stripped of its R suffix,
-- which is the granularity the summary groups on.
local function NodeStatKey(node)
return string.format("%s:%.4f:%.4f", node.type or "?", node.x or 0, node.y or 0)
end
-- Gathering reagent tiers. Each tier is its OWN itemID in retail, so the
-- statistics store already tells them apart on its own; the icon is what tells
-- the READER, since every tier of a reagent carries the same name.
--
-- One size and one crop for every tier icon, in every list. Hardcoding either
-- at each call site is what let them drift apart between the tooltip and the
-- zone summary. The crop matches the one the floating harvest text applies to
-- these same files.
local TIER_ICON_SIZE = 16
local TIER_TEXCOORD = { 0.078, 0.094, 0.898, 0.930 }
local function ItemTier(itemID)
local r = C_TradeSkillUI and C_TradeSkillUI.GetItemReagentQualityByItemInfo
and C_TradeSkillUI.GetItemReagentQualityByItemInfo(itemID)
return (r and r > 0) and r or 0
end
-- Item name for display. The live client cache first (it is localized), then
-- the name captured at harvest time, which is all a cold cache leaves us.
local function StatItemName(itemID)
local n = GetItemInfo(itemID)
if n then return n end
local cached = FarmMapStatsDB and FarmMapStatsDB.itemNames
return (cached and cached[itemID]) or ("#" .. tostring(itemID))
end
-- Quantities as a list ordered by descending amount, so the tooltip and the
-- zone summary always print the same item in the same place. pairs() alone
-- would reshuffle the lines between two openings.
local function SortedItemList(items)
local list = {}
for id, qty in pairs(items or {}) do
-- The tier is resolved once here and carried on the row: the comparator
-- runs O(n log n) times and the display reads it again, so querying the
-- API at either point would repeat the same lookup for nothing.
list[#list + 1] = { id = id, qty = qty, tier = ItemTier(id) }
end
-- Tiered items first, untiered underneath. Then by name, which groups the
-- tiers of one reagent since they all share it, and finally by id, which
-- runs in tier order. Sorting by quantity would scatter those tiers across
-- the whole list.
table.sort(list, function(a, b)
local ra = (a.tier > 0) and 0 or 1
local rb = (b.tier > 0) and 0 or 1
if ra ~= rb then return ra < rb end
local na, nb = StatItemName(a.id), StatItemName(b.id)
if na ~= nb then return na < nb end
return a.id < b.id
end)
return list
end
-- Tier rows inside the node tooltip. NOT a second frame: the lines belong to
-- GameTooltip, we only take over their geometry.
--
-- The recipe is the floating harvest text's, and the reason it works is that
-- the icon is SMALLER than the box holding it and both are centred on that box:
-- the tooltip line is forced to 20px, its own FontString is centred inside it,
-- and the 16px icon is anchored to that same FontString. Left alone, a tooltip
-- line is only as tall as its glyphs and the icon can only ride the baseline -
-- which is why no offset ever brought the two level.
local TIP_LINE_H = 20
local TIP_INDENT = " " -- blank run reserving the icon's column
local tierIcons = {}
local touchedRows = {}
-- GameTooltip FontStrings are global and shared with every other tooltip in the
-- game, so the forced height MUST be handed back or every later tooltip inherits
-- 20px lines.
local function ResetTooltipRows()
for _, i in ipairs(touchedRows) do
local fs = _G["GameTooltipTextLeft" .. i]
if fs then fs:SetHeight(0) end
if tierIcons[i] then tierIcons[i]:Hide() end
end
wipe(touchedRows)
end
local tooltipHooked = false
local function AddTierRow(text, tier)
if not tooltipHooked then
GameTooltip:HookScript("OnHide", ResetTooltipRows)
tooltipHooked = true
end
GameTooltip:AddLine(TIP_INDENT .. text, 1, 1, 1)
local i = GameTooltip:NumLines()
local fs = _G["GameTooltipTextLeft" .. i]
if not fs then return end
fs:SetHeight(TIP_LINE_H)
fs:SetJustifyV("MIDDLE")
touchedRows[#touchedRows + 1] = i
local tex = tier and tier > 0 and TIER_TEXTURES[tier]
if not tex then return end
local icon = tierIcons[i]
if not icon then
icon = GameTooltip:CreateTexture(nil, "OVERLAY")
icon:SetSize(TIER_ICON_SIZE, TIER_ICON_SIZE)
icon:SetTexCoord(unpack(TIER_TEXCOORD))
tierIcons[i] = icon
end
icon:SetTexture(tex)
icon:ClearAllPoints()
icon:SetPoint("LEFT", fs, "LEFT", 0, 0)
icon:Show()
end
-- Base edge of a pin, in pixels, before the player's size setting is applied.
local PIN_BASE_SIZE = 16
-- Every world map pin currently handed to HBDPins. The size slider walks this
-- list to resize the pins in place: HBDPins anchors our icon CENTER-on-CENTER
-- to its own pin frame, so a resized icon stays exactly on its coordinate and
-- no refresh is needed. Rebuilding the pins instead would recreate thousands
-- of frames on every step of the slider.
local activeWorldPins = {}
-- Dropping the pins and forgetting them has to happen together, or the list
-- keeps resizing frames HBDPins has already released. Every removal goes
-- through here for that reason.
local function ClearWorldMapPins()
HBDPins:RemoveAllWorldMapIcons(addonName)
wipe(activeWorldPins)
end
local function CreatePoint(nodeData, isMinimap, mapID)
local f = CreateFrame("Frame", nil, nil)
if isMinimap then
f:SetSize(PIN_BASE_SIZE, PIN_BASE_SIZE)
else