This repository was archived by the owner on Aug 27, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCore.lua
More file actions
2670 lines (2500 loc) · 142 KB
/
Copy pathCore.lua
File metadata and controls
2670 lines (2500 loc) · 142 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
-- SkuGatherRoute -- optional local companion addon.
-- Turns GatherMate2's recorded node databases into voice-guided farming
-- routes through Sku, reachable from Shift+F1 -> "Route de minage" (mining)
-- and "Route d'herbes" (herbs, 2026-08-18, secondary priority) -- both
-- appended at the very end of the root menu, per the user's request. The two
-- share one route engine (RESOURCE_CATEGORIES descriptor below) -- reading
-- the mining explanations in this header applies equally to herbs unless
-- said otherwise.
--
-- ---------------------------------------------------------------------------
-- HOW THIS WORKS (read this before touching the route logic)
--
-- 1. DATA SOURCE: GatherMate2's own SavedVariables -- GatherMate2MineDB for
-- mining, GatherMate2HerbDB for herbs (RESOURCE_CATEGORIES.dbGlobal picks
-- the right one) -- shaped identically for both, exactly like GatherMate2/
-- GatherMate2.lua's GatherMate:AddNode leaves them:
-- GatherMate2MineDB[uiMapId][encodedXY] = nodeTypeId
-- uiMapId is a standard Blizzard map id (GatherMate2 is built on
-- HereBeDragons/C_Map, same id space Sku itself uses). encodedXY packs a
-- 0..1 map-fraction (x,y) pair; decoded with the EXACT inverse of
-- GatherMate2's own GatherMate:EncodeLoc (GatherMate2/GatherMate2.lua):
-- encode(x,y) = floor(x*10000+0.5)*1000000 + floor(y*10000+0.5)*100
-- decode(id) = floor(id/1000000)/10000, floor(id%1000000/100)/10000
-- Replicated locally below (DecodeGatherMateCoord) rather than calling
-- into GatherMate2 -- this is just arithmetic, no need for a dependency
-- on GatherMate2's internals beyond reading its SavedVariable.
--
-- GatherMate2 only POPULATES this from data pack imports or the player's
-- own mining if the user has actually run the import (GatherMate2's own
-- options -> Import tab, or "Auto Import" toggled on there) -- that is a
-- one-time in-game settings action for the user, not something this addon
-- does for them.
--
-- 2. COORDINATE CONVERSION: a GatherMate2 node is (uiMapId, xFrac, yFrac).
-- SkuNav:SetWaypoint (SkuNav/Core.lua) wants (contintentId, areaId,
-- worldX, worldY). The conversion below is copied from how Sku's OWN code
-- does exactly this in SkuNav/Core.lua (SkuNav:ProcessPlayerDead uses the
-- identical C_Map.GetWorldPosFromMapPos + GetAreaData pattern):
-- local tAreaId = SkuNav:GetAreaIdFromUiMapId(uiMapId)
-- local _, worldPos = C_Map.GetWorldPosFromMapPos(uiMapId, CreateVector2D(x, y))
-- local worldX, worldY = worldPos:GetXY()
-- local contintentId = select(3, SkuNav:GetAreaData(tAreaId))
-- (Sku really does spell it "contintentId" throughout SkuNav -- kept
-- verbatim so this addon's calls match its field names exactly.)
--
-- 3. NAVIGATION: [rewritten 2026-08-17, per "pas à pas, en mode close route,
-- pas en mode waypoint"] Each node is visited via a REAL close route
-- (metaroute) -- the exact same feature reachable by hand through Shift+
-- F9 -> a waypoint -> "Nahe Routen" (SkuNav/Options.lua), which follows
-- Sku's own pre-built path network instead of a straight-line beacon.
-- StartCloseRouteTo (below) reproduces that computation directly instead
-- of driving it through the menu, since the target here is picked
-- programmatically. When no graph coverage is found near the player or
-- the target, it falls back to a plain SkuNav:SelectWP for that one node
-- -- still fully functional, just a straight beacon instead of a real
-- path. Order of visits across nodes is still nearest-first, live and
-- adaptive: SkuNav:GetClosestWaypointFromBaseName (the same primitive
-- Sku's own SKU_KEY_SELECTNEXTBASEWAYPOINT keybind uses) picks the next
-- node fresh from the player's actual position every time one finishes,
-- rather than following a precomputed order -- a waypoint named "Route
-- de minage;7" has base name "Route de minage" (everything before the
-- first ";" -- SkuNav:StripBaseNameFromWaypointName), which is how they
-- all get found as one family.
--
-- "Arrival" at the current target and the 50m presence check are both
-- driven by this addon's own 0.15s ticker (WatchRouteProgress) polling
-- SkuNav:GetDistanceToWp directly against the CURRENT target -- not by
-- watching SkuSettings:Sub("SkuNav").selectedWaypoint, which cycles
-- through every intermediate path waypoint while a close route is under
-- way and so cannot tell "reached one hop" from "reached the actual ore
-- node". Manual skip is its OWN dedicated action (SkuGatherRoute:
-- SkipCurrentTarget, see its own comment) rather than piggybacking on
-- Sku's native SKU_KEY_MOVETONEXTWP/SkuNav.MoveToWp -- that flag turned
-- out to be inherently racy for any OUTSIDE code to poll (Sku's own
-- OnUpdate driver resets it unconditionally on roughly every 0.1s tick,
-- which can beat a slower addon ticker to it) and, even when caught,
-- only steps ONE path hop per press rather than abandoning the current
-- target -- confirmed unreliable by the user's own testing.
--
-- Each node is deleted from SkuNav the moment it's finished (reached,
-- skipped, or found absent), so it can never be re-offered and a
-- cancelled/restarted route cleans up completely instead of leaking
-- waypoints. Deleting on completion is used instead of Sku's own
-- trackVisited/waypointWasVisited setting deliberately -- that only
-- works if the user happens to have it enabled, and it can time-expire
-- (SkuNav/Visited.lua); an outright delete cannot silently fail either
-- way.
-- ---------------------------------------------------------------------------
local ADDON_NAME = ...
---------------------------------------------------------------------------------------------------------------------------------------
-- Self-diagnostic log -- SAME pattern as SkuBagnonBridge/Core.lua (proven
-- across that addon's own debugging cycle), including the pre-restoration
-- buffering caveat: WoW swaps the real SavedVariable table in AFTER this
-- file finishes executing, so anything logged before that point is buffered
-- locally and flushed on this addon's own ADDON_LOADED.
local tLogBuffer = {}
local tLogFlushed = false
local function Log(aFmt, ...)
local tOk, tMsg = pcall(string.format, aFmt, ...)
if not tOk then tMsg = tostring(aFmt) end
local tLine = "[" .. ((date and date("%H:%M:%S")) or "?") .. "] " .. tMsg
if tLogFlushed then
table.insert(SkuGatherRouteLog, tLine)
while #SkuGatherRouteLog > 500 do table.remove(SkuGatherRouteLog, 1) end
else
table.insert(tLogBuffer, tLine)
end
end
local tLogFrame = CreateFrame("Frame")
tLogFrame:RegisterEvent("ADDON_LOADED")
tLogFrame:SetScript("OnEvent", function(self, aEvent, aName)
if aEvent == "ADDON_LOADED" and aName == ADDON_NAME then
SkuGatherRouteLog = (type(SkuGatherRouteLog) == "table") and SkuGatherRouteLog or {}
for _, tLine in ipairs(tLogBuffer) do
table.insert(SkuGatherRouteLog, tLine)
end
tLogBuffer = {}
tLogFlushed = true
while #SkuGatherRouteLog > 500 do table.remove(SkuGatherRouteLog, 1) end
self:UnregisterEvent("ADDON_LOADED")
end
end)
SLASH_SGRLOG1 = "/sgrlog"
SlashCmdList["SGRLOG"] = function(aMsg)
aMsg = (aMsg or ""):lower():match("^%s*(.-)%s*$")
local tLog = (tLogFlushed and SkuGatherRouteLog) or tLogBuffer
if aMsg == "clear" then
if tLogFlushed then
for i = #SkuGatherRouteLog, 1, -1 do SkuGatherRouteLog[i] = nil end
else
tLogBuffer = {}
end
DEFAULT_CHAT_FRAME:AddMessage("|cff80c0ffSkuGatherRoute|r: log efface.")
return
end
local tN = #tLog
if tN == 0 then
DEFAULT_CHAT_FRAME:AddMessage("|cff80c0ffSkuGatherRoute|r: aucune entree.")
return
end
local tCount = tonumber(aMsg) or 20
local tStart = math.max(1, tN - tCount + 1)
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff80c0ffSkuGatherRoute|r: %d entree(s), affichage de %d a %d :", tN, tStart, tN))
for i = tStart, tN do
DEFAULT_CHAT_FRAME:AddMessage(tLog[i])
end
end
-- Labels for the dedicated "skip this ore" keybind (Bindings.xml, see
-- SkipCurrentTarget/InstallDefaultKeybind below) -- read by Blizzard's own
-- Key Bindings panel to show a category header and binding name instead of
-- the raw internal names. Set unconditionally, before the Sku/SkuNav guard
-- below, since Blizzard's UI expects these globals to simply exist.
-- [2026-08-18] Full label set for all 5 keybindable actions (was just the
-- one "skip" binding). Resolved via Sku.deEn right away -- Sku is a hard
-- TOC dependency (## Dependencies: Sku) so it's already fully loaded and
-- executed by the time this file runs, same reasoning already applied to
-- every Sku.deEn call elsewhere in this file. Blizzard's Key Bindings panel
-- reads these as plain globals ONCE at binding-list-build time (no live
-- re-localization), so this is the client's language at first load -- exactly
-- as good as Sku's own BINDING_NAME_SKU_KEY_* labels, which have the same
-- one-shot-at-load characteristic.
local function tBindLabel(aDe, aEn, aFr)
return (Sku and Sku.deEn and Sku.deEn(aDe, aEn, aFr)) or aFr
end
BINDING_HEADER_SKUGATHERROUTE = tBindLabel("Sku - Abbauroute", "Sku - Gather route", "Sku - Route de minage")
BINDING_NAME_SKUGATHERROUTE_SKIP = tBindLabel("Dieses Vorkommen ueberspringen (naechstes)", "Skip this node (go to next)", "Sauter ce minerai/cette herbe (aller au suivant)")
BINDING_NAME_SKUGATHERROUTE_STARTMINING = tBindLabel("Abbauroute starten (alle)", "Start mining route (all)", "Démarrer : route de minage (tout)")
BINDING_NAME_SKUGATHERROUTE_STARTHERB = tBindLabel("Kraeuterroute starten (alle)", "Start herb route (all)", "Démarrer : route d'herbes (tout)")
BINDING_NAME_SKUGATHERROUTE_STOP = tBindLabel("Route stoppen", "Stop route", "Arrêter la route")
BINDING_NAME_SKUGATHERROUTE_STATUS = tBindLabel("Routenstatus ansagen", "Announce route status", "Annoncer l'état de la route")
Log("Core.lua executing. Sku=%s SkuCore=%s SkuNav=%s", tostring(Sku ~= nil), tostring(SkuCore ~= nil), tostring(SkuNav ~= nil))
if not Sku or not SkuCore or not SkuNav then
Log("ABORT: Sku, SkuCore or SkuNav global missing at file-load time -- addon inert this session.")
return
end
---------------------------------------------------------------------------------------------------------------------------------------
local SkuGatherRoute = LibStub("AceAddon-3.0"):NewAddon("SkuGatherRoute", "AceConsole-3.0")
Log("AceAddon object created.")
-- [2026-08-18, ROOT CAUSE FIX] AceAddon:NewAddon does NOT expose the created
-- object as a global -- confirmed by reading Libs/AceAddon-3.0/AceAddon-3.0
-- .lua directly: it only stores it in AceAddon's OWN internal registry
-- (self.addons[name]), never touches _G. `local SkuGatherRoute = ...` above
-- is therefore a plain chunk-local upvalue, visible only to closures defined
-- LATER IN THIS SAME FILE (every menu action below correctly resolves it
-- that way) -- but Bindings.xml's <Binding> body is compiled as its OWN
-- separate chunk by the WoW client, with only _G reachable for any name it
-- doesn't declare itself. All 5 of this addon's keybinds
-- (SKUGATHERROUTE_SKIP/STARTMINING/STARTHERB/STOP/STATUS) were therefore
-- ALWAYS evaluating SkuGatherRoute as a nonexistent global (nil) -- a
-- complete no-op -- since day one, NOT a regression from the recent keybind
-- work. (The earlier "confirmed working in-game" read of the skip keybind
-- from SkuGatherRouteLog was a misdiagnosis: SkipCurrentTarget's log line is
-- identical whether reached via the keybind or the always-working "Sauter
-- ce minerai" MENU action, which calls it as a normal same-file closure --
-- the log couldn't actually distinguish the two paths. See this addon's own
-- memory entry for the correction.) Fixed by explicitly publishing the
-- object as a real global right here, once, immediately after creation.
_G.SkuGatherRoute = SkuGatherRoute
-- Shows up as "Route de minage" in Sku's Features on/off menu. Defaults ON
-- (unset = on, Sku's normal rule) -- no forced-default-off here. See
-- SkuBagnonBridge/Core.lua's [2026-08-17, finding #1] comment for exactly
-- why that pattern is a trap: forcing SetModuleEnabled(false) at file-load
-- time disables the AceAddon object before AceAddon's own PLAYER_LOGIN
-- enable pass ever runs, and OnEnable then never fires again that session.
-- IsGatherMatePresent() below already keeps this addon fully inert with no
-- visible effect whenever GatherMate2 isn't installed, so "on by default"
-- is exactly as safe here as it is for every other Sku standalone addon.
SkuCore:RegisterToggleableAddon("SkuGatherRoute", function()
return "GatherMate2 SKU Access"
end)
Log("Registered as toggleable addon with SkuCore.")
---------------------------------------------------------------------------------------------------------------------------------------
local function IsGatherMatePresent()
local tIsLoaded = (C_AddOns and C_AddOns.IsAddOnLoaded) or IsAddOnLoaded
if not tIsLoaded then return false end
return tIsLoaded("GatherMate2") == true
end
local function Announce(aText)
if SkuOptions and SkuOptions.Voice and SkuOptions.Voice.OutputStringBTtts then
SkuOptions.Voice:OutputStringBTtts(aText, true, true, 0.2)
else
print(aText)
end
end
---------------------------------------------------------------------------------------------------------------------------------------
-- [2026-08-17] Reachable from the menu itself -- the user cannot operate
-- GatherMate2's own options panel (a standard Blizzard/AceConfig settings
-- tree: tabs, a multiselect, an "Import" button) with a screen reader, so
-- this reproduces exactly what that panel's own "Import GatherMate2Data"
-- button does (GatherMate2/Config.lua, importOptions.args.GatherMateData
-- .args.loadData.func) by calling the SAME underlying functions directly:
-- 1. C_AddOns.LoadAddOn("GatherMate2_Data") -- it's LoadOnDemand=1, so
-- nothing in it runs until something asks for it.
-- 2. GatherMate2_Data:PerformMerge({Mines = true, Herbs = true}, "Merge",
-- nil) -- confirmed by reading GatherMate2_Data/Tbc/GatherMateData.lua
-- directly: "Merge" style means ADD to the existing DB (GatherMate:
-- ClearDB is only called for style ~= "Merge") -- this never wipes
-- anything the player has already found themselves.
--
-- [2026-08-18] Imports BOTH Mines and Herbs together now that this addon
-- covers both categories (was Mines-only) -- one button primes everything,
-- no need to remember which menu to import from first.
--
-- [2026-08-18] NOT actually idempotent, found by testing: GatherMate2_Data:
-- CleanupImportData() (called at the end of PerformMerge) nils out ALL FOUR
-- raw GatherMateData2*DB globals once consumed, and a LoadOnDemand addon's
-- file body never re-runs once loaded -- so calling this a second time in
-- the same session made MergeMines crash on pairs(nil) (GatherMate2_Data/
-- Tbc/GatherMateData.lua:19). Guarded below: if the raw mining data is
-- already gone, that means an earlier import this session already
-- succeeded (Mines and Herbs are always cleared together), so this is
-- reported as a no-op rather than attempted (and crashing).
local function ImportGatherMateData()
if not IsGatherMatePresent() then
Announce(Sku.deEn and Sku.deEn("GatherMate2 nicht geladen", "GatherMate2 not loaded", "GatherMate2 non chargé") or "GatherMate2 non chargé")
return
end
local tLoaded, tReason = C_AddOns.LoadAddOn("GatherMate2_Data")
Log("ImportGatherMateData: LoadAddOn(GatherMate2_Data) loaded=%s reason=%s", tostring(tLoaded), tostring(tReason))
if not tLoaded then
Announce(Sku.deEn and Sku.deEn("GatherMate2Data konnte nicht geladen werden", "GatherMate2Data could not be loaded", "Impossible de charger GatherMate2Data") or "Impossible de charger GatherMate2Data")
return
end
local tGatherMateData = LibStub("AceAddon-3.0"):GetAddon("GatherMate2_Data", true)
if not tGatherMateData or not tGatherMateData.PerformMerge then
Announce(Sku.deEn and Sku.deEn("GatherMate2Data-Objekt nicht gefunden", "GatherMate2Data object not found", "Objet GatherMate2Data introuvable") or "Objet GatherMate2Data introuvable")
Log("ImportGatherMateData: GatherMate2_Data AceAddon object or PerformMerge missing after load.")
return
end
if type(_G.GatherMateData2MineDB) ~= "table" then
Announce(Sku.deEn and Sku.deEn("Bereits importiert diese Sitzung", "Already imported this session", "Déjà importé pour cette session") or "Déjà importé pour cette session")
Log("ImportGatherMateData: GatherMateData2MineDB already consumed -- skipping re-import (already done this session).")
return
end
local tOk, tErr = pcall(tGatherMateData.PerformMerge, tGatherMateData, { Mines = true, Herbs = true }, "Merge", nil)
if not tOk then
Announce(Sku.deEn and Sku.deEn("Fehler beim Import", "Error during import", "Erreur pendant l'import") or "Erreur pendant l'import")
Log("ImportGatherMateData: PerformMerge THREW: %s", tostring(tErr))
return
end
local function tCountDB(aGlobalName)
local tN = 0
local tDB = _G[aGlobalName]
if type(tDB) == "table" then
for _, tZoneDb in pairs(tDB) do
if type(tZoneDb) == "table" then
for _ in pairs(tZoneDb) do tN = tN + 1 end
end
end
end
return tN
end
local tMineCount = tCountDB("GatherMate2MineDB")
local tHerbCount = tCountDB("GatherMate2HerbDB")
Announce((Sku.deEn and Sku.deEn("GatherMate2-Daten importiert", "GatherMate2 data imported", "Données GatherMate2 importées") or "Données GatherMate2 importées")
.. " " .. tMineCount .. " " .. (Sku.deEn and Sku.deEn("Erzknoten", "ore nodes", "nœuds de minerai") or "nœuds de minerai")
.. ", " .. tHerbCount .. " " .. (Sku.deEn and Sku.deEn("Kraeuterknoten", "herb nodes", "nœuds d'herbe") or "nœuds d'herbe"))
Log("ImportGatherMateData: import complete, %d mining node(s), %d herb node(s).", tMineCount, tHerbCount)
end
---------------------------------------------------------------------------------------------------------------------------------------
-- [2026-08-17] Root cause the user found by testing: GatherMate2 draws its
-- OWN persistent minimap icon at every node coordinate it has ever recorded
-- -- regardless of whether that node is currently really there. The
-- presence check above (CheckNodePresence) reads minimap child frames via
-- Sku's own MinimapScanChildFrames, which cannot tell a live Blizzard
-- resource blip apart from GatherMate2's own icon sitting at the same
-- pixel -- so it was matching GatherMate2's icon and always reporting
-- "present", even for a genuinely depleted node. GatherMate2 itself exposes
-- exactly one relevant setting for this: GatherMate2.db.profile.showMinimap
-- (GatherMate2/Display.lua gates its whole icon-draw/update path on it;
-- GatherMate2/Config.lua's own "Show Minimap Icons" checkbox just flips
-- this same field). Toggled here instead of asking the user to find that
-- checkbox themselves (same accessibility reasoning as ImportGatherMateData
-- above -- GatherMate2's own options panel is not usable with a screen
-- reader). Display:UpdateMaps() is called right after flipping it OFF so
-- already-drawn icons clear immediately rather than lingering until the
-- next unrelated minimap update.
local function ToggleGatherMateMinimapIcons()
if not IsGatherMatePresent() then
Announce(Sku.deEn and Sku.deEn("GatherMate2 nicht geladen", "GatherMate2 not loaded", "GatherMate2 non chargé") or "GatherMate2 non chargé")
return
end
local tGM = LibStub("AceAddon-3.0"):GetAddon("GatherMate2", true)
if not tGM or not tGM.db or not tGM.db.profile then
Announce(Sku.deEn and Sku.deEn("GatherMate2-Einstellungen nicht gefunden", "GatherMate2 settings not found", "Réglages GatherMate2 introuvables") or "Réglages GatherMate2 introuvables")
Log("ToggleGatherMateMinimapIcons: GatherMate2 AceAddon object or db.profile missing.")
return
end
tGM.db.profile.showMinimap = not tGM.db.profile.showMinimap
local tNewState = tGM.db.profile.showMinimap
local tOkMod, tDisplay = pcall(tGM.GetModule, tGM, "Display", true)
if tOkMod and tDisplay and tDisplay.UpdateMaps then
pcall(tDisplay.UpdateMaps, tDisplay)
end
if tNewState then
Announce(Sku.deEn and Sku.deEn("GatherMate2-Minikartensymbole aktiviert", "GatherMate2 minimap icons enabled", "Icônes minicarte GatherMate2 activées") or "Icônes minicarte GatherMate2 activées")
else
Announce(Sku.deEn and Sku.deEn("GatherMate2-Minikartensymbole deaktiviert", "GatherMate2 minimap icons disabled", "Icônes minicarte GatherMate2 désactivées") or "Icônes minicarte GatherMate2 désactivées")
end
Log("ToggleGatherMateMinimapIcons: showMinimap now %s.", tostring(tNewState))
end
-- [2026-08-20, feature] "Tu peux masquer toutes les icônes de GatherMate2
-- sur la grande carte ? Ou une option pour ?" -- same idea as the minimap
-- toggle above, but for the WORLD map (Blizzard's WorldMapFrame), a
-- separate GatherMate2 setting (db.profile.showWorldMap, confirmed by
-- reading GatherMate2/Display.lua directly -- `WorldMapDataProvider:
-- RefreshAllData` gates its whole pin-add loop on it). Refreshing this one
-- is NOT the same call as the minimap: world map pins go through
-- Blizzard's modern MapCanvasDataProviderMixin system (RefreshAllData is
-- normally invoked BY that framework itself whenever the map opens/changes
-- zone, not something addons poke directly the way Display:UpdateMaps()
-- pokes the minimap). GatherMate2's OWN two native toggle points for this
-- EXACT setting (Config.lua's AceConfig checkbox AND its LDB icon's
-- Shift-click handler) both settle for calling Config:UpdateConfig() after
-- flipping it -- confirmed by reading both call sites directly -- which
-- just broadcasts a "GatherMate2ConfigChanged" AceEvent message for
-- GatherMate2's own modules to react to. Reused here rather than guessing
-- at Blizzard's MapCanvas refresh internals -- it's GatherMate2's own
-- established mechanism for this precise setting, not a guess.
local function ToggleGatherMateWorldMapIcons()
if not IsGatherMatePresent() then
Announce(Sku.deEn and Sku.deEn("GatherMate2 nicht geladen", "GatherMate2 not loaded", "GatherMate2 non chargé") or "GatherMate2 non chargé")
return
end
local tGM = LibStub("AceAddon-3.0"):GetAddon("GatherMate2", true)
if not tGM or not tGM.db or not tGM.db.profile then
Announce(Sku.deEn and Sku.deEn("GatherMate2-Einstellungen nicht gefunden", "GatherMate2 settings not found", "Réglages GatherMate2 introuvables") or "Réglages GatherMate2 introuvables")
Log("ToggleGatherMateWorldMapIcons: GatherMate2 AceAddon object or db.profile missing.")
return
end
tGM.db.profile.showWorldMap = not tGM.db.profile.showWorldMap
local tNewState = tGM.db.profile.showWorldMap
local tOkMod, tConfig = pcall(tGM.GetModule, tGM, "Config", true)
if tOkMod and tConfig and tConfig.UpdateConfig then
pcall(tConfig.UpdateConfig, tConfig)
end
if tNewState then
Announce(Sku.deEn and Sku.deEn("GatherMate2-Weltkartensymbole aktiviert", "GatherMate2 world map icons enabled", "Icônes carte du monde GatherMate2 activées") or "Icônes carte du monde GatherMate2 activées")
else
Announce(Sku.deEn and Sku.deEn("GatherMate2-Weltkartensymbole deaktiviert", "GatherMate2 world map icons disabled", "Icônes carte du monde GatherMate2 désactivées") or "Icônes carte du monde GatherMate2 désactivées")
end
Log("ToggleGatherMateWorldMapIcons: showWorldMap now %s.", tostring(tNewState))
end
---------------------------------------------------------------------------------------------------------------------------------------
-- [2026-08-18, LOCALIZATION FIX] GatherMate2 mining/herb node-type ids ->
-- {deDE=,enUS=,frFR=} display name, one entry per client language instead of
-- a single hardcoded French string. THIS MATTERS FOR CORRECTNESS, not just
-- cosmetics: CheckNodePresence (below) compares this name against
-- SkuCore.MinimapScanner:MinimapScanChildFrames()'s blip table, and that
-- table is keyed by `tChildRessourceTypes[r][x][Sku.LocP]` (SkuCore/
-- minimapScanner.lua) -- i.e. the name in the CLIENT's OWN locale, not
-- always French. A hardcoded French name only ever matched by coincidence
-- on a frFR client; on an enUS or deDE client the presence check would NEVER
-- find a match and every single node would be wrongly reported "absent" and
-- skipped immediately, silently breaking the whole addon. Found and fixed
-- during the 2026-08-18 translation/compatibility pass, before any non-FR
-- client ever ran this code.
--
-- Values are taken from Sku's OWN already-3-language SkuCore.RessourceTypes
-- .mining / .herbs tables (SkuCore/minimapScanner.lua) -- NOT re-translated
-- here -- so the wording matches exactly what Sku itself already speaks for
-- the same ore/herb elsewhere. Sku indexes those tables by ITS OWN internal
-- number (1-27 mining, 1-45 herbs), completely different from GatherMate2's
-- ids (201-224, 401-442), so bridging them required matching each entry by
-- its English node name (both ultimately come from the same Blizzard game
-- data, so the English text is identical either side) -- done by hand,
-- cross-referencing GatherMate2/Constants.lua's node_ids literally against
-- SkuCore/minimapScanner.lua's SkuCore.RessourceTypes, both re-read fresh
-- from disk for this pass rather than trusted from memory.
--
-- Ids with NO match in Sku's table (218-220 Anniversary-only Lesser
-- Bloodstone/Incendicite/Indurium ores, 441-442 Flame Cap/Netherdust Bush --
-- none of which Sku's own scanner recognizes by name in any language) keep
-- a single raw GatherMate2 English string for all three languages -- exactly
-- as before, just now explicit about it being the same in each language
-- rather than a silent French-only gap.
local function tRT(aEn, aDe, aFr) return { enUS = aEn, deDE = aDe, frFR = aFr } end
local MINING_NAMES = {
[201] = tRT("Copper Vein", "Kupfervorkommen", "Filon de cuivre"),
[202] = tRT("Tin Vein", "Zinnvorkommen", "Filon d'étain"),
[203] = tRT("Iron Deposit", "Eisenvorkommen", "Gisement de fer"),
[204] = tRT("Silver Vein", "Silbervorkommen", "Filon d'argent"),
[205] = tRT("Gold Vein", "Goldvorkommen", "Filon d'or"),
[206] = tRT("Mithril Deposit", "Mithrilablagerung", "Gisement de mithril"),
[207] = tRT("Ooze Covered Mithril Deposit", "Brühschlammbedeckte Mithrilablagerung", "Gisement de mithril couvert de vase"),
[208] = tRT("Truesilver Deposit", "Echtsilberablagerung", "Gisement de vrai-argent"),
[209] = tRT("Ooze Covered Silver Vein", "Brühschlammbedecktes Silbervorkommen", "Filon d'argent couvert de limon"),
[210] = tRT("Ooze Covered Gold Vein", "Brühschlammbedecktes Goldvorkommen", "Filon d'or couvert de limon"),
[211] = tRT("Ooze Covered Truesilver Deposit", "Brühschlammbedeckte Echtsilberablagerung", "Gisement de vrai-argent couvert de vase"),
[212] = tRT("Ooze Covered Rich Thorium Vein", "Brühschlammbedecktes reiches Thoriumvorkommen", "Riche filon de thorium couvert de limon"),
[213] = tRT("Ooze Covered Thorium Vein", "Brühschlammbedecktes Thoriumvorkommen", "Filon de thorium couvert de limon"),
[214] = tRT("Small Thorium Vein", "Kleines Thoriumvorkommen", "Petit filon de thorium"),
[215] = tRT("Rich Thorium Vein", "Reiches Thoriumvorkommen", "Riche filon de thorium"),
[217] = tRT("Dark Iron Deposit", "Dunkeleisenablagerung", "Gisement de sombrefer"),
[218] = tRT("Lesser Bloodstone Deposit", "Lesser Bloodstone Deposit", "Lesser Bloodstone Deposit"),
[219] = tRT("Incendicite Mineral Vein", "Incendicite Mineral Vein", "Incendicite Mineral Vein"),
[220] = tRT("Indurium Mineral Vein", "Indurium Mineral Vein", "Indurium Mineral Vein"),
[221] = tRT("Fel Iron Deposit", "Teufelseisenvorkommen", "Gisement de gangrefer"),
[222] = tRT("Adamantite Deposit", "Adamantitablagerung", "Gisement d'adamantite"),
[223] = tRT("Rich Adamantite Deposit", "Reiche Adamantitablagerung", "Riche gisement d'adamantite"),
[224] = tRT("Khorium Vein", "Khoriumvorkommen", "Filon de khorium"),
}
-- [2026-08-18, secondary priority] Same treatment for herbs, GatherMate2 ids
-- 401-442 (GatherMate2/Constants.lua node_ids["Herb Gathering"]) -- verified
-- against GatherMate2/Constants.lua's own node_expansion table: 401-431 are
-- Classic-era, 432-442 are BC -- 443+ is Wrath-only and cannot spawn on this
-- client (same cutoff reasoning as MINING_NAMES above). Ids 406/419/430
-- (Swiftthistle/Wildvine/Bloodvine) are commented out in GatherMate2's own
-- table -- they're picked up as part of another herb's node, never their
-- own -- so there is nothing to map for them.
local HERB_NAMES = {
[401] = tRT("Peacebloom", "Friedensblume", "Pacifique"),
[402] = tRT("Silverleaf", "Silberblatt", "Feuillargent"),
[403] = tRT("Earthroot", "Erdwurzel", "Terrestrine"),
[404] = tRT("Mageroyal", "Maguskönigskraut", "Mage royal"),
[405] = tRT("Briarthorn", "Wilddornrose", "Eglantine"),
[407] = tRT("Stranglekelp", "Würgetang", "Etouffante"),
[408] = tRT("Bruiseweed", "Beulengras", "Doulourante"),
[409] = tRT("Wild Steelbloom", "Wildstahlblume", "Aciérite sauvage"),
[410] = tRT("Grave Moss", "Grabmoos", "Tombeline"),
[411] = tRT("Kingsblood", "Königsblut", "Sang-royal"),
[412] = tRT("Liferoot", "Lebenswurz", "Viétérule"),
[413] = tRT("Fadeleaf", "Blassblatt", "Pâlerette"),
[414] = tRT("Goldthorn", "Golddorn", "Dorépine"),
[415] = tRT("Khadgar's Whisker", "Khadgars Schnurrbart", "Moustache de Khadgar"),
[416] = tRT("Wintersbite", "Winterbiss", "Hivernale"),
[417] = tRT("Firebloom", "Feuerblüte", "Fleur de feu"),
[418] = tRT("Purple Lotus", "Lila Lotus", "Lotus pourpre"),
[420] = tRT("Arthas' Tears", "Arthas' Tränen", "Larme d'Arthas"),
[421] = tRT("Sungrass", "Sonnengras", "Soleillette"),
[422] = tRT("Blindweed", "Blindkraut", "Aveuglette"),
[423] = tRT("Ghost Mushroom", "Geisterpilz", "Champignon fantôme"),
[424] = tRT("Gromsblood", "Gromsblut", "Sang de Grom"),
[425] = tRT("Golden Sansam", "Goldener Sansam", "Sansam doré"),
[426] = tRT("Dreamfoil", "Traumblatt", "Feuille de rêve"),
[427] = tRT("Mountain Silversage", "Bergsilbersalbei", "Sauge-argent de montagne"),
[428] = tRT("Plaguebloom", "Pestblüte", "Peste fleurie"),
[429] = tRT("Icecap", "Eiskappe", "Cap glacé"),
[431] = tRT("Black Lotus", "Schwarzer Lotus", "Lotus noir"),
[432] = tRT("Felweed", "Teufelsgras", "Gangreherbe"),
[433] = tRT("Dreaming Glory", "Traumwinde", "Gloire des rêves"),
[434] = tRT("Terocone", "Terozapfen", "Cône de terre"),
[435] = tRT("Ancient Lichen", "Urflechte", "Lichen ancien"),
[436] = tRT("Bloodthistle", "Blutdistel", "Chardon sanglant"),
[437] = tRT("Mana Thistle", "Manadistel", "Chardon de mana"),
[438] = tRT("Netherbloom", "Netherblüte", "Pétale-de-néant"),
[439] = tRT("Nightmare Vine", "Alptraumranke", "Vigne cauchemar"),
[440] = tRT("Ragveil", "Zottelkappe", "Voile-de-raz"),
[441] = tRT("Flame Cap", "Flame Cap", "Flame Cap"),
[442] = tRT("Netherdust Bush", "Netherdust Bush", "Netherdust Bush"),
}
-- [2026-08-18] Resource "category" descriptor -- lets the exact same route
-- engine below (StartCloseRouteTo, AdvanceToTarget, FinishCurrentTarget,
-- CheckNodePresence, WatchRouteProgress, StartRoute, ...) serve more than
-- one GatherMate2 database without duplicating any of that logic. Mining
-- was the first, fully-tested category; Herb Gathering reuses every one of
-- those functions completely unchanged -- only the category descriptor
-- passed in differs.
local RESOURCE_CATEGORIES = {
Mining = {
dbGlobal = "GatherMate2MineDB",
importArg = "Mines",
baseName = "Route de minage",
names = MINING_NAMES,
fallbackPrefix = "Minerai",
},
Herb = {
dbGlobal = "GatherMate2HerbDB",
importArg = "Herbs",
baseName = "Route d'herbes",
names = HERB_NAMES,
fallbackPrefix = "Herbe",
},
}
-- [2026-08-18, LOCALIZATION FIX] Resolves to the CLIENT's own language
-- (Sku.LocP -- "deDE"/"enUS"/"frFR", set from GetLocale() by Sku itself,
-- Sku/Core.lua) instead of always French. Falls back to enUS then deDE if
-- Sku.LocP is unavailable or the specific entry has no frFR/whatever-locale
-- variant -- same safety-net order Sku's own minimapScanner.lua uses for its
-- identical FR-extension table. This is what CheckNodePresence's minimap
-- match, the "Choisir un type" submenu labels, and every spoken node name
-- are built from -- see MINING_NAMES/HERB_NAMES's own comment above for why
-- matching Sku's own displayed language here is a correctness requirement,
-- not a cosmetic choice.
local function ResourceTypeName(aCategory, aTypeId)
local tEntry = aCategory.names[aTypeId]
if not tEntry then return aCategory.fallbackPrefix .. " #" .. tostring(aTypeId) end
local tLoc = (Sku and Sku.LocP) or "enUS"
return tEntry[tLoc] or tEntry.enUS or tEntry.deDE
end
---------------------------------------------------------------------------------------------------------------------------------------
-- Exact inverse of GatherMate2's own GatherMate:EncodeLoc (GatherMate2/
-- GatherMate2.lua) -- see the file header comment for why this is
-- replicated rather than called into GatherMate2 (it's pure arithmetic on
-- data already read from GatherMate2's own SavedVariable).
local mfloor = math.floor
local function DecodeGatherMateCoord(aId)
return mfloor(aId / 1000000) / 10000, mfloor(aId % 1000000 / 100) / 10000
end
---------------------------------------------------------------------------------------------------------------------------------------
-- (uiMapId, xFrac, yFrac) -> Sku waypoint fields, or nil if the conversion
-- fails (unmapped uiMapId, or C_Map has nothing for this position -- e.g. an
-- indoor/instance uiMapId with no world terrain). Mirrors SkuNav:
-- ProcessPlayerDead's own C_Map.GetWorldPosFromMapPos + GetAreaData usage
-- (SkuNav/Core.lua) -- see the file header comment for the full reasoning.
local function NodeToWaypointData(aUiMapId, aX, aY)
local tAreaId = SkuNav:GetAreaIdFromUiMapId(aUiMapId)
if not tAreaId then return nil end
local tOk, tInstanceId, tWorldPos = pcall(C_Map.GetWorldPosFromMapPos, aUiMapId, CreateVector2D(aX, aY))
if not tOk or not tWorldPos then return nil end
local tWorldX, tWorldY = tWorldPos:GetXY()
if not tWorldX or not tWorldY then return nil end
local tContinentId = select(3, SkuNav:GetAreaData(tAreaId)) or -1
return {
contintentId = tContinentId,
areaId = tAreaId,
worldX = tWorldX,
worldY = tWorldY,
}
end
---------------------------------------------------------------------------------------------------------------------------------------
-- Every uiMapId worth checking for the player's CURRENT position: Sku's own
-- GetBestMapForUnit (which has extra edge-case overrides layered on top of
-- Blizzard's, see SkuNav/Geo.lua) first, then the raw C_Map.GetBestMapForUnit
-- as a fallback in case GatherMate2 stored a node under the id Sku's
-- overrides steer away from for a given subzone. Cheap to check both;
-- avoids a real "0 nodes found" false negative in an edge-case zone at the
-- cost of a couple of extra table lookups.
local function GetCandidateUiMapIds()
local tIds, tSeen = {}, {}
local tSkuMap = SkuNav:GetBestMapForUnit("player")
local tRawMap = C_Map and C_Map.GetBestMapForUnit and C_Map.GetBestMapForUnit("player")
for _, id in ipairs({ tSkuMap, tRawMap }) do
if id and not tSeen[id] then
tSeen[id] = true
tIds[#tIds + 1] = id
end
end
return tIds
end
---------------------------------------------------------------------------------------------------------------------------------------
-- [2026-08-19, feature] "Garder en mémoire pendant environ 1h le cheminement
-- déjà suivi, les spots déjà passés, ainsi que ceux où il n'y avait rien,
-- pour optimiser la route ou la reprise" -- requested directly. Every node
-- this addon ever finishes with (reached, manually skipped, or found absent
-- -- any FinishCurrentTarget reason, deliberately unified rather than
-- special-cased: all three mean "already dealt with, don't re-offer") is
-- recorded here, keyed by its STABLE GatherMate2 identity -- category +
-- uiMapId + the raw coordinate key GatherMate2's own database already uses
-- (tCoordId in ScanZoneNodes below -- a perfect, already-unique per-position
-- id, no new derivation needed). ScanZoneNodes skips any node whose entry
-- here is still fresh when building a route, so stopping and restarting
-- naturally resumes with only what's left instead of re-walking everything.
--
-- Persisted as its own SavedVariable (SkuGatherRouteRecentDB, declared in
-- the .toc) rather than session-only state, since "stopped and restarted
-- LATER" in the request includes a full relog, not just a same-session
-- route restart. Timestamps use time() (Unix epoch, stable across a relog)
-- rather than GetTime() (resets on client restart, would make every entry
-- look instantly stale after exactly the kind of restart this needs to
-- survive).
local RECENT_MEMORY_TTL = 3600 -- ~1h, per the request
local function tRecentNodeKey(aCategory, aUiMapId, aCoordId)
return aCategory.dbGlobal .. ":" .. aUiMapId .. ":" .. aCoordId
end
-- Records aReason (whatever FinishCurrentTarget was called with) for one
-- node. Safe to call liberally -- a later call for the same node just
-- refreshes its timestamp.
local function RecordRecentNode(aCategory, aUiMapId, aCoordId, aReason)
if type(SkuGatherRouteRecentDB) ~= "table" then SkuGatherRouteRecentDB = {} end
SkuGatherRouteRecentDB[tRecentNodeKey(aCategory, aUiMapId, aCoordId)] = { t = time(), reason = aReason }
end
local function IsNodeRecent(aCategory, aUiMapId, aCoordId)
if type(SkuGatherRouteRecentDB) ~= "table" then return false end
local tEntry = SkuGatherRouteRecentDB[tRecentNodeKey(aCategory, aUiMapId, aCoordId)]
if not tEntry or type(tEntry) ~= "table" or not tEntry.t then return false end
return (time() - tEntry.t) < RECENT_MEMORY_TTL
end
-- Sweeps genuinely expired entries out of the persisted table so it doesn't
-- grow unbounded over long play sessions / many sessions. IsNodeRecent above
-- already treats an expired-but-not-yet-swept entry as "not recent" on its
-- own, so this is pure housekeeping, not a correctness dependency -- called
-- once at OnEnable and again at the start of every ScanZoneNodes (i.e. every
-- route start), which is plenty frequent for that purpose without needing a
-- dedicated background timer.
local function PruneRecentNodeMemory()
if type(SkuGatherRouteRecentDB) ~= "table" then return end
local tNow = time()
local tRemoved = 0
for tKey, tEntry in pairs(SkuGatherRouteRecentDB) do
if type(tEntry) ~= "table" or not tEntry.t or (tNow - tEntry.t) >= RECENT_MEMORY_TTL then
SkuGatherRouteRecentDB[tKey] = nil
tRemoved = tRemoved + 1
end
end
if tRemoved > 0 then Log("PruneRecentNodeMemory: removed %d expired entrie(s).", tRemoved) end
end
-- Menu-reachable manual reset (Shift+F1 -> Route de minage/d'herbes ->
-- "Vider la mémoire des nœuds récents") -- clears BOTH categories at once
-- (the memory isn't split by which menu you happen to be in), for anyone who
-- wants a full fresh scan without waiting out the hour.
local function ClearRecentNodeMemory()
local tCount = 0
if type(SkuGatherRouteRecentDB) == "table" then
for _ in pairs(SkuGatherRouteRecentDB) do tCount = tCount + 1 end
end
SkuGatherRouteRecentDB = {}
Announce((Sku.deEn and Sku.deEn("Erinnerung geloescht: ", "Memory cleared: ", "Mémoire effacée : ") or "Mémoire effacée : ")
.. tCount .. " " .. (Sku.deEn and Sku.deEn("Eintraege", "entries", "entrées") or "entrées"))
Log("ClearRecentNodeMemory: cleared %d entrie(s).", tCount)
end
local MAX_ROUTE_NODES = 300 -- soft safety cap, well above any single zone/ore-type's real node count
-- Scans aCategory's GatherMate2 database (GatherMate2MineDB / HerbDB) for
-- the player's current zone. aTypeFilter is nil (every type in the
-- category) or a set {[gatherMateTypeId]=true, ...} to restrict to. Skips
-- any node still within RECENT_MEMORY_TTL of a previous finish (see
-- RecordRecentNode/IsNodeRecent above). Returns a list of {worldX, worldY,
-- contintentId, areaId, typeId, uiMapId, coordId}.
local function ScanZoneNodes(aCategory, aTypeFilter)
PruneRecentNodeMemory()
local tResults = {}
local tDB = _G[aCategory.dbGlobal]
if type(tDB) ~= "table" then
Log("ScanZoneNodes: %s missing or not a table.", aCategory.dbGlobal)
return tResults
end
local tSkippedRecent = 0
for _, tUiMapId in ipairs(GetCandidateUiMapIds()) do
local tZoneDb = tDB[tUiMapId]
if type(tZoneDb) == "table" then
for tCoordId, tTypeId in pairs(tZoneDb) do
if (not aTypeFilter or aTypeFilter[tTypeId]) and #tResults < MAX_ROUTE_NODES then
if IsNodeRecent(aCategory, tUiMapId, tCoordId) then
tSkippedRecent = tSkippedRecent + 1
else
local tX, tY = DecodeGatherMateCoord(tCoordId)
local tWpData = NodeToWaypointData(tUiMapId, tX, tY)
if tWpData then
tWpData.typeId = tTypeId
tWpData.uiMapId = tUiMapId
tWpData.coordId = tCoordId
tResults[#tResults + 1] = tWpData
end
end
end
end
end
end
Log("ScanZoneNodes: db=%s filter=%s found=%d skippedRecent=%d", aCategory.dbGlobal, aTypeFilter and "set" or "all", #tResults, tSkippedRecent)
return tResults
end
-- Every distinct node type of aCategory actually present around the player
-- right now, sorted by name -- used to build the "choose one" submenu so it
-- only ever lists types that can actually be found here, not the whole
-- category every time.
local function GetPresentTypesInZone(aCategory)
local tSeen = {}
local tDB = _G[aCategory.dbGlobal]
if type(tDB) == "table" then
for _, tUiMapId in ipairs(GetCandidateUiMapIds()) do
local tZoneDb = tDB[tUiMapId]
if type(tZoneDb) == "table" then
for _, tTypeId in pairs(tZoneDb) do
tSeen[tTypeId] = true
end
end
end
end
local tList = {}
for tTypeId in pairs(tSeen) do
tList[#tList + 1] = { typeId = tTypeId, name = ResourceTypeName(aCategory, tTypeId) }
end
table.sort(tList, function(a, b) return a.name < b.name end)
return tList
end
---------------------------------------------------------------------------------------------------------------------------------------
-- Active-route state. tActiveRouteNames tracks the waypoint names THIS
-- addon created for the current run, in no particular order. tCurrentTarget
-- is the ONE node currently being navigated to (real close route or
-- fallback direct waypoint -- see StartCloseRouteTo below). tActiveCategory
-- is the RESOURCE_CATEGORIES entry (Mining or Herb) the current/last route
-- was started with -- a route is always one category at a time, never
-- mixed, so this single shared variable (rather than per-node) is enough;
-- set at the top of StartRoute, read anywhere the waypoint base name is
-- needed (waypoint naming, GetClosestWaypointFromBaseName).
local tActiveCategory = RESOURCE_CATEGORIES.Mining
local tActiveRouteNames = {}
local tActiveRouteNameSet = {}
local tActiveRouteNodeName = {} -- [wpName] = expected French ore name, for the presence check below
local tActiveRouteNodeIdentity = {} -- [wpName] = {uiMapId=, coordId=} -- the GatherMate2 identity, for RecordRecentNode on finish
local tPresenceChecked = {} -- [wpName] = true once the presence check has CONCLUDED for it (confirmed via an ambient scan, or timed out -- see PRESENCE_GIVE_UP_AFTER)
local tPresenceCheckStartTime -- set the moment the player first comes within range of tCurrentTarget -- see CheckNodePresence
local tMinedMissStreak = 0 -- consecutive "not found" scans while AT the target, for the mined-confirmation check -- see CheckMinedAndAdvance
local tLastMinedCheckTime = 0
local tRouteTicker
local tCurrentTarget
-- [2026-08-18, ROOT-CAUSE REWRITE] The presence/mined checks used to call
-- SkuCore.MinimapScanner:MinimapScanChildFrames() directly, synchronously.
-- The user's own SkuGatherRouteLog (with temporary diagnostic logging added
-- to confirm this) showed it finding ZERO blips, for EVERY resource type,
-- 100% of the time -- not a name-matching bug, that scan path finds nothing
-- to match against at all on this client. Root cause, confirmed by reading
-- Sku's OWN source comment on MinimapScanFast (SkuCore/minimapScanner.lua):
-- "Auf Anniversary/Classic sind die nativen Ressourcen-Blips keine
-- adressierbaren Child-Frames mit OnEnter-Skripten" -- i.e. Sku's own author
-- already knew the fast child-frame scan doesn't work on this client build,
-- which is exactly why MinimapScanFast() falls back to a slower "shrink the
-- minimap to 15x15px, hide it, park it under the (pre-centered) cursor, read
-- whatever tooltip appears" trick when the fast path finds nothing. THAT
-- fallback is what actually works here -- it's why the user's own Ctrl+Shift+R
-- and the passive "notify on resources" feature both work fine even though
-- this addon's direct fast-path call never did.
--
-- Fix: call MinimapScanner:MinimapScanFast() itself (the real entry point,
-- fast-path-then-fallback, exactly what Sku's own features use) instead of
-- the broken fast path directly. The catch: MinimapScanFast() is
-- asynchronous (the fallback path waits on a C_Timer.After(0.1, ...)) and
-- communicates its result ONLY through the MinimapScanFastStop(aResult) hook
-- this addon already installs in OnEnable for the opportunistic-switch
-- feature -- never a return value, and (confirmed by reading every call site
-- of MinimapScanFastStop in Sku's source) NEVER any position data, only a
-- resource NAME. That second point is why RefineTargetPositionFromBlip
-- (the earlier "correct the final approach point from the live minimap"
-- feature) has been removed rather than adapted: there is no dx/dy available
-- from the one scan path that actually works on this client, so that
-- feature was silently dead code from the moment it was written, on this
-- client specifically -- not a regression to fix, just something to stop
-- pretending still exists.
--
-- tScanRequest tracks the ONE in-flight request this addon itself is
-- waiting on (presence check OR mined check, never both at once in
-- practice -- see WatchRouteProgress). A MinimapScanFastStop firing for a
-- scan this addon did NOT request (e.g. Sku's own passive notify-on-
-- resources tick) is still useful for TryOpportunisticSwitch, but simply has
-- nothing here to resolve.
local tScanRequest -- nil, or {purpose="presence"|"mined", target=wpName, expectedName=name}
-- Starts a real MinimapScanFast() scan and records what this addon is
-- waiting to hear back about. Returns false (nothing started, caller should
-- just retry next tick) if a request is already in flight, the API is
-- missing, or Sku itself is already mid-scan (MinimapScanFast has its own
-- busy-guard and would silently no-op).
-- [2026-08-19] Self-requested scans almost never succeed (confirmed by the
-- v1.8.0 diagnostic: 100% "raw aResult=nil") because of Sku's own one-time-
-- only mouse-centering quirk -- see the ROOT-CAUSE REWRITE comment above
-- tScanRequest. Requesting one aggressively (every 0.15s tick) mostly just
-- holds Sku's OWN shared busy-lock (MinimapScanner.MinimapScanFastRunning)
-- more often, which can silently block a MANUAL scan (Ctrl+Shift+R) or an
-- ambient passive hit from Sku's own notify-on-resources -- both of which
-- have a real chance of succeeding -- from ever running at that exact
-- moment. Throttled down since 1.8.1: ambient/manual hits (TryAmbientPresenceConfirm)
-- are the primary success path now, not this addon's own requests, so there's
-- no benefit to requesting one often -- only a cost in contention.
-- [2026-08-19] Lowered from 2s -- this comment's own reasoning ("aren't the
-- primary success path... only a cost in contention") was written back when
-- CheckNodePresence ALSO self-scanned; since v1.10.0 CheckMinedAndAdvance is
-- the only remaining self-scan consumer, so there's nothing left of this
-- addon's own to contend with. User report: "même avec le scan ça passe pas
-- au prochain minerai... il faudrait que ce soit plus réactif" -- combined
-- with the OnMinimapScanFastResult fix below (an ambient/manual scan no
-- longer gets wasted once already at the node), this and that together
-- should let CheckMinedAndAdvance's 3-consecutive-miss streak fill in
-- noticeably faster.
local SELF_SCAN_MIN_INTERVAL = 1 -- seconds
local tLastSelfScanRequestTime = 0
local function RequestPresenceScan(aPurpose, aTarget, aExpectedName)
if tScanRequest then return false end
if not SkuCore.MinimapScanner or not SkuCore.MinimapScanner.MinimapScanFast then return false end
if SkuCore.MinimapScanner.MinimapScanFastRunning then return false end
local tNow = GetTime()
if tNow - tLastSelfScanRequestTime < SELF_SCAN_MIN_INTERVAL then return false end
tLastSelfScanRequestTime = tNow
tScanRequest = { purpose = aPurpose, target = aTarget, expectedName = aExpectedName }
local tOk, tErr = pcall(SkuCore.MinimapScanner.MinimapScanFast, SkuCore.MinimapScanner)
if not tOk then
Log("RequestPresenceScan: MinimapScanFast THREW: %s", tostring(tErr))
tScanRequest = nil
return false
end
return true
end
-- [2026-08-17] "Check at ~50m that the ore is actually still there, skip
-- ahead if not, so time isn't wasted flying/walking to an empty spot" --
-- requested directly. GatherMate2's data can be stale (mined out since it
-- was recorded, or by someone else moments ago); this catches that BEFORE
-- committing to the full approach, not just after physically arriving.
-- Uses Sku's OWN minimap-blip detection (SkuCore.MinimapScanner
-- :MinimapScanChildFrames, SkuCore/minimapScanner.lua -- the exact routine
-- Sku's own passive resource-scanner uses) rather than reimplementing
-- minimap reading.
--
-- [2026-08-18, revised after a real false-negative was reported] Used to
-- read the minimap AS-IS with no zoom change, on the reasoning that a
-- zoomed-in minimap's small accuracy loss was an acceptable trade for
-- simplicity. That accuracy loss turned out to actually bite: a node well
-- within tPresenceCheckRange by ground distance can still be OUTSIDE a
-- tightly zoomed-in minimap's visible radius, so it never renders as a
-- minimap child frame and the scan honestly (but wrongly) finds nothing.
-- CheckNodePresence now zooms the minimap fully out for the scan and
-- restores the player's own zoom immediately after (synchronous, same
-- tick), plus requires several consecutive misses before concluding
-- absence rather than trusting a single scan -- see that function's own
-- comment for the full reasoning.
-- Biased toward NOT skipping when unsure (missing MinimapScanner, a failed
-- scan, or simply not being within range yet all leave the node alone) --
-- a false "still there" costs nothing beyond today's behaviour (manual
-- Ctrl+Shift+W skip is always available); a false "it's gone" would rob the
-- player of a node that was actually there, which is the worse mistake.
-- Requires GatherMate2's own minimap icons to be OFF (see
-- ToggleGatherMateMinimapIcons above) -- otherwise its persistent icon at
-- this exact spot is what gets matched, not a live Blizzard blip, and the
-- check always reports "present" (found by the user testing this).
--
-- [2026-08-18] A mutable local (not a true constant) -- adjustable at
-- runtime via the "Portée de vérification" menu picker (see
-- SetPresenceCheckRange below), so the 50y default can be tuned per
-- session without editing code. Resets to 50 on the next /reload -- kept
-- session-scoped rather than a persisted SavedVariable on purpose, to avoid
-- adding a settings schema for one number; simple enough to re-pick each
-- session from the menu if a different value is wanted again.
local tPresenceCheckRange = 50 -- yards
-- [2026-08-19, feature] "Rajouter une option pour lancer l'itinéraire mais
-- avec choix close route ou waypoint" -- requested directly. "closeroute"
-- (default, unchanged prior behavior) drives every advance through
-- StartCloseRouteTo's own real path search (falls back to a plain waypoint
-- automatically only when no path is found near the player/target).
-- "waypoint" skips that search entirely and always selects a plain direct
-- waypoint -- useful when the close-route graph is sparse/unhelpful for
-- wherever the player is farming, or when the extra path-search cost isn't
-- worth it for a short/simple route. Session-scoped, same reasoning as
-- tPresenceCheckRange above (simple to re-pick from the menu, not worth a
-- persisted settings schema for one value).
local tNavigationMode = "closeroute" -- "closeroute" | "waypoint"
-- Matches this addon's own waypoint size=1 (SkuNav/data.lua SkuNavWpSize[1]
-- = 1 yard) -- the same "arrived" precision every other size=1 Sku waypoint
-- uses (e.g. minimapScanner.lua's Quick Waypoints).
local ARRIVAL_RANGE = 1 -- yards
-- [2026-08-18] "Guide point par point... pas me retrouver bloqué dans une
-- montagne" -- how far from the actual ore node StartCloseRouteTo will
-- accept a graph-linked waypoint as the route's landing point. Sku's own
-- close-route algorithm (and this addon's copy of it) only ever PATHFINDS
-- between waypoints that are already part of its known network -- the very
-- last stretch, from that landing point to the real target, is always a
-- straight beacon line with no terrain awareness at all (true of Sku's
-- distance/direction math everywhere: SkuNav:Distance is a flat 2D
-- calculation, it has no notion of elevation, so it cannot tell "walk
-- straight there" from "that's a cliff face"). A SHORTER straight-line tail
-- is a shorter stretch where that blind spot can strand the player, so this
-- was tightened from Sku's own menu default of 500 yards down to 150. The
-- trade-off: a genuinely remote node (nothing graph-linked within 150y) now
-- falls back to a plain waypoint MORE readily -- which is the honest
-- outcome anyway, since a "close route" whose last 400+ yards were just as
-- blind as a full fallback would have been was never really safer, just
-- nominally labelled "precise".
local MAX_TARGET_APPROACH_DISTANCE = 150 -- yards
-- [2026-08-18] "Pas me retrouver bloqué dans une montagne" -- a second,
-- complementary mitigation. Sku's distance/direction math (SkuNav:Distance,
-- used everywhere including this addon) is flat 2D -- it has no concept of
-- elevation at all, and neither GatherMate2 nor C_Map.GetWorldPosFromMapPos
-- expose a node's height, so there is no data available anywhere in this
-- pipeline to compute "climb" or "descend" guidance from (checked before
-- writing this comment, not assumed). What CAN be detected without height
-- data: the player is actively moving but not actually getting any closer
-- to the target -- exactly the signature of being stuck against a cliff
-- face or wall while still holding a direction key / flying forward. When
-- that happens for STUCK_CHECK_INTERVAL seconds in a row, this announces it
-- once, so the player knows to stop, look around, or use "Sauter ce
-- minerai" instead of continuing to push into whatever is blocking them.
--
-- [2026-08-19, FALSE-POSITIVE FIX] Originally measured "progress" as the
-- straight-line distance to the FINAL target getting shorter. That's wrong
-- for a real close route: the path to a graph-linked entry point is rarely
-- a straight line to the ultimate target -- it can legitimately curve away
-- from it for a while (around a mountain, through a pass, along a road) while
-- the player is making completely normal progress ALONG THE PATH. Straight-
-- line distance to a point kilometers past the next few hops can easily
-- stay flat or even increase during a perfectly healthy leg, which is
-- exactly the false "bloqué" the user reported happening "sans raison".
-- Fixed: measure the player's own WORLD POSITION change instead (raw
-- displacement, direction-agnostic) -- this only reads "am I actually
-- moving through space at all", completely independent of the path's shape
-- or how it relates to the final target. A player genuinely wedged against
-- terrain barely moves at all regardless of which way the path curves;
-- a player making normal progress along ANY path (straight or curved)
-- reliably displaces several yards over STUCK_CHECK_INTERVAL.
local STUCK_CHECK_INTERVAL = 15 -- seconds
local STUCK_MIN_PROGRESS = 10 -- yards -- must physically move at least this much per interval to count as "making progress"
local tStuckLastWorldX, tStuckLastWorldY
local tStuckLastCheckTime
local tStuckAnnounced
-- [2026-08-18] Menu-driven customisation for tPresenceCheckRange -- see
-- that variable's own comment for why this is session-scoped rather than a
-- persisted setting.
local function SetPresenceCheckRange(aRange)
tPresenceCheckRange = aRange
Announce((Sku.deEn and Sku.deEn("Erkennungsreichweite", "Detection range", "Portée de détection") or "Portée de détection")
.. " " .. aRange .. " " .. (Sku.deEn and Sku.deEn("Meter", "yards", "mètres") or "mètres"))
Log("SetPresenceCheckRange: now %d yards.", aRange)
end
-- Menu-driven customisation for tNavigationMode -- see that variable's own
-- comment above for the full reasoning.