· 5 years ago · Dec 17, 2020, 02:54 AM
1local normalized_unique_map_name_to_id = {} -- Lowered and spaces removed for searching, uses "/" for disambiguation separator internally (since there are map names with colons in them), front-end uses ":" to match other addons
2local map_id_to_unique_name = {} -- For displaying unique names
3do
4 -- Disambiguation is complex. It is written to be linear, fast, and in-line since it has to be done at every reload.
5 -- Do not alter without understanding. If something looks complicated, or the ordering seems strange, there's probably a reason.
6
7 -- All maps can be referenced by map ID number
8 -- If maps have a name, reference is:
9 -- map name if unique (e.g., "Duskwood")
10 -- or zone-and-ancestor if possible to construct an unambiguous one (e.g., "Nagrand:Outland")
11 -- or zone and ID (e.g., "Dalaran:212")
12 -- Special rule: if a map name is doubled, like "Orgrimmar - Orgrimmar", then it disambiguates alongside any other "Orgrimmar" maps, then becomes the canonical "Orgrimmar"
13 -- this elegantly handles several awkward cases like Orgrimmar and Dalaran - the double-name map is usually the main map the players want (e.g., "Orgrimmar - Orgrimmar" is the normal Orgrimmar, plain "Orgrimmar" is some other Orgrimmar, yet if the player does /way orgrimmar 52 35, we want them to get the map for "Orgrimmar - Orgrimmar", not the other random map)
14
15 -- Get the root map by finding the furthest ancestor of the fallback world map
16 local root_map = C_Map.GetFallbackWorldMapID()
17 while true do
18 local parent = C_Map.GetMapInfo(root_map).parentMapID
19 if parent == 0 then
20 break
21 else
22 root_map = parent
23 end
24 end
25
26 -- Get the map data for all child maps
27 local map_data = C_Map.GetMapChildrenInfo(root_map, nil, true)
28
29 -- Find pinnable maps (there are some maps where API doesn't support pins, but we can pin to parent map instead)
30 -- Maps that pin to a parent have a "proxy" key with the map ID of the map we pin to instead
31 local map_id_to_data = {}
32
33 for _, map in ipairs(map_data) do
34 map_id_to_data[map.mapID] = map
35 end
36
37 for id, map in pairs(map_id_to_data) do
38 if map.pinnable == nil then
39 local chain = {} -- if we end up having to check ancestors, we can set the whole chain at the same time
40 local parent_id = map.parentMapID
41 while true do
42 if C_Map.CanSetUserWaypointOnMap(id) then
43 -- chain is pinnable
44 map_id_to_data[id].pinnable = true
45 for _, chain_id in ipairs(chain) do
46 map_id_to_data[chain_id].pinnable = true
47 map_id_to_data[chain_id].proxy = id
48 end
49 break
50 else
51 chain[#chain + 1] = id
52 if parent_id == 0 then
53 -- no more parents to check, so this chain is not pinnable
54 for _, id in ipairs(chain) do
55 map_id_to_data[id].pinnable = false
56 end
57 break
58 else
59 -- check if we can translate coords onto the parent map
60 local continent, world_pos = C_Map.GetWorldPosFromMapPos(id, CreateVector2D(0.5, 0.5))
61 if continent and C_Map.GetMapPosFromWorldPos(continent, world_pos, parent_id) then
62 -- can translate the coords to the parent map
63 local pinnable = map_id_to_data[parent_id].pinnable
64 if pinnable == true then
65 -- parent map is pinnable, so this chain must be pinnable
66 for _, id in ipairs(chain) do
67 map_id_to_data[id].pinnable = true
68 map_id_to_data[id].proxy = parent_id
69 end
70 break
71 elseif pinnable == false then
72 -- parent map isn't pinnable, so this chain is not pinnable
73 for _, id in ipairs(chain) do
74 map_id_to_data[id].pinnable = false
75 end
76 break
77 else
78 -- parent map pinnability is unknown, so check next ancestor
79 id = parent_id
80 parent_id = C_Map.GetMapInfo(id).parentMapID
81 end
82 else
83 -- we can't translate pin coords to parent map, so chain is not pinnable
84 for _, id in ipairs(chain) do
85 map_id_to_data[id].pinnable = false
86 end
87 break
88 end
89 end
90 end
91 end
92 end
93 end
94
95 -- goal is to build unique_map_name_to_id and populate with only references to map IDs and disambiguated names
96 local unique_map_name_to_id = {} -- stores map id and proxy id (for maps that place pins on parent maps)
97
98 -- first pass to find duplicates
99 local is_dupe = {}
100 local dupes_map_data = {}
101 for id, map in pairs(map_id_to_data) do
102 if map.pinnable then
103 local name = map.name
104 local proxy = map.proxy
105 unique_map_name_to_id[tostring(id)] = {id = id, proxy = proxy} -- all maps can be referenced by ID number
106 if name and name ~= "" then
107 if is_dupe[name] then
108 -- it's a duplicate
109 dupes_map_data[#dupes_map_data + 1] = {name = name, id = id, proxy = proxy}
110 else
111 -- it might be a duplicate
112 local maybe_dupe = unique_map_name_to_id[name]
113 if maybe_dupe then
114 -- it's the first duplicate pair for this name
115 is_dupe[name] = true
116 dupes_map_data[#dupes_map_data + 1] = {name = name, id = maybe_dupe.id, proxy = maybe_dupe.proxy}
117 dupes_map_data[#dupes_map_data + 1] = {name = name, id = id, proxy = proxy}
118 unique_map_name_to_id[name] = nil
119 else
120 -- it's not a duplicate (so far)
121 unique_map_name_to_id[name] = {id = id, proxy = proxy}
122 end
123 end
124 end
125 end
126 end
127
128 -- Add floor names to disambiguate dupes, create inverted table for remaining dupes
129 is_dupe = {}
130 local double_names = {} -- keep track of names like "Orgrimmar - Orgrimmar"
131 local dupe_names_to_data = {}
132 for _, map in ipairs(dupes_map_data) do
133 local id = map.id
134 local proxy = map.proxy
135 local map_group = C_Map.GetMapGroupID(id)
136 local name = map.name
137 -- Get floor names
138 if map_group then
139 local floors = C_Map.GetMapGroupMembersInfo(map_group)
140 for _, floor in pairs(floors) do
141 if floor.mapID == id then
142 name = strjoin(" - ", map.name, floor.name)
143 if floor.name == map.name then
144 -- It's a name like "Orgrimmar - Orgrimmar", which we handle in a special way
145 double_names[name] = true
146 end
147 break
148 end
149 end
150 end
151 if is_dupe[name] then
152 -- it's a duplicate
153 tinsert(dupe_names_to_data[name], {id = id, ancestor_id = id, proxy = proxy}) -- initialize ancestor to self
154 else
155 -- it might be a duplicate
156 local maybe_dupe = unique_map_name_to_id[name]
157 if maybe_dupe then
158 -- it's the first duplicate pair for this name
159 is_dupe[name] = true
160 local maybe_dupe_id = maybe_dupe.id
161 dupe_names_to_data[name] = {
162 {id = id, ancestor_id = id, proxy = proxy},
163 {id = maybe_dupe_id, ancestor_id = maybe_dupe_id, proxy = maybe_dupe.proxy}
164 }
165 unique_map_name_to_id[name] = nil
166 else
167 -- it's not a duplicate (so far)
168 unique_map_name_to_id[name] = {id = id, proxy = map.proxy}
169 end
170 end
171 end
172
173 -- If it's a name like "Orgrimmar - Orgrimmar", and it's the only such double-name, then it ultimately gets promoted to single-name as the true "Orgrimmar"
174 local to_promote_name_to_id = {}
175 for double_name, _ in pairs(double_names) do
176 if unique_map_name_to_id[double_name] then
177 -- There was only one instance of this double-name, so we can proceed
178 local dash_pos = strfind(double_name, " - ", 1, true)
179 local single_name = strsub(double_name, 1, dash_pos - 1)
180 local old_single_name = unique_map_name_to_id[single_name]
181 if old_single_name then
182 -- there's already a single name in our list of disambiguated names, we need to disambiguate both, then promote the double-name at the end by removing its suffix
183 local old_single_name_id = old_single_name.id
184 local double_name_id_and_proxy = unique_map_name_to_id[double_name]
185 local double_name_id = double_name_id_and_proxy.id
186 dupe_names_to_data[single_name] = {
187 {id = old_single_name_id, ancestor_id = old_single_name_id, proxy = old_single_name.proxy},
188 {id = double_name_id, ancestor_id = double_name_id, proxy = double_name_id_and_proxy.proxy}
189 }
190 unique_map_name_to_id[single_name] = nil
191 to_promote_name_to_id[single_name] = double_name_id
192 elseif dupe_names_to_data[single_name] then
193 -- we need to add this to the dupes so the others will disambiguate, then promote at the end by removing the suffix
194 local double_name_id_and_proxy = unique_map_name_to_id[double_name]
195 local double_name_id = double_name_id_and_proxy.id
196 tinsert(dupe_names_to_data[single_name], {id = double_name_id, ancestor_id = double_name_id, proxy = double_name_id_and_proxy.proxy})
197 to_promote_name_to_id[single_name] = unique_map_name_to_id[double_name].id
198 else
199 -- we can promote the double-name right now
200 unique_map_name_to_id[single_name] = unique_map_name_to_id[double_name]
201 -- we don't need to do anything later
202 end
203 unique_map_name_to_id[double_name] = nil
204 end
205 end
206
207 -- Try to disambiguate with other information
208 for name, maps in pairs(dupe_names_to_data) do
209 -- If multiple maps have the same proxy and same coordinate system, we can just use the same name for all (since that means we'll just be pinning to the same map anyway)
210 -- (There might not be any such maps, but there may be some added later)
211 do
212 local first_map = maps[1]
213 local id = first_map.id
214 local proxy = first_map.proxy
215 local found_unmatched = false
216 if proxy then
217 local _, world_pos_one, world_pos_two
218 -- if two points on both maps project to the same world coords, the maps share the same space
219 _, world_pos_one = C_Map.GetWorldPosFromMapPos(id, CreateVector2D(0.25, 0.25))
220 _, world_pos_two = C_Map.GetWorldPosFromMapPos(id, CreateVector2D(0.75, 0.75))
221 -- see if the two points match on the other maps:
222 for i = 2, #maps do
223 if maps[i].proxy == proxy then
224 local test_world_pos_one, test_world_pos_two
225 _, test_world_pos_one = C_Map.GetWorldPosFromMapPos(id, CreateVector2D(0.25, 0.25))
226 _, test_world_pos_two = C_Map.GetWorldPosFromMapPos(id, CreateVector2D(0.75, 0.75))
227 if test_world_pos_one.x ~= world_pos_one.x or test_world_pos_one.y ~= world_pos_one.y or test_world_pos_two.x ~= world_pos_two.x or test_world_pos_two.y ~= world_pos_two.y then
228 found_unmatched = true
229 break
230 end
231 else
232 found_unmatched = true
233 break
234 end
235 end
236 if not found_unmatched then
237 unique_map_name_to_id[name] = {id = id, proxy = proxy}
238 end
239 end
240 end
241 if not proxy or found_unmatched then
242 -- See if we can use ancestor maps to disambiguate (e.g., Nagrand:Outland)
243 repeat
244 -- Get the next ancestor map IDs
245 for key, map in pairs(maps) do -- maps may have holes in it
246 local parent_id = C_Map.GetMapInfo(map.ancestor_id).parentMapID
247 if parent_id == 0 then
248 -- No more parents available to disambiguate with, so best suffix is map ID
249 local id = map.id
250 if to_promote_name_to_id[name] == id then
251 unique_map_name_to_id[name] = {id = id, proxy = map.proxy}
252 else
253 unique_map_name_to_id[strjoin("/", name, id)] = {id = id, proxy = map.proxy}
254 end
255 maps[key] = nil
256 else
257 map.ancestor_id = parent_id
258 end
259 end
260 -- Find duplicate ancestor IDs among the duplicate map names (since this means all further ancestors will be shared too)
261 local ancestor_ids_to_maps = {}
262 for key, map in pairs(maps) do
263 local ancestor_id = map.ancestor_id
264 if not ancestor_ids_to_maps[ancestor_id] then
265 ancestor_ids_to_maps[ancestor_id] = {}
266 end
267 tinsert(ancestor_ids_to_maps[ancestor_id], {key = key, map = map}) -- save the key so we can nil it in maps if there are duplicate ancestors
268 end
269 for ancestor_id, child_maps in pairs(ancestor_ids_to_maps) do
270 if #child_maps > 1 then
271 -- Found duplicate ancestor IDs, so best suffix is map ID
272 for child_map_key, child_map in ipairs(child_maps) do
273 local id = child_map.map.id
274 if to_promote_name_to_id[name] == id then
275 unique_map_name_to_id[name] = {id = id, proxy = child_map.map.proxy}
276 else
277 unique_map_name_to_id[strjoin("/", name, id)] = {id = id, proxy = child_map.map.proxy}
278 end
279 maps[child_map.key] = nil
280 end
281 end
282 end
283 -- Find remaining duplicate suffixes
284 local suffixes_to_maps = {}
285 for key, map in pairs(maps) do
286 local suffix = C_Map.GetMapInfo(map.ancestor_id).name
287 if suffix then
288 map.suffix = suffix
289 if not suffixes_to_maps[suffix] then
290 suffixes_to_maps[suffix] = {}
291 end
292 tinsert(suffixes_to_maps[suffix], {key = key, map = map})
293 end
294 end
295 local found_dupe = false
296 for suffix, suffixed_maps in pairs(suffixes_to_maps) do
297 if #suffixed_maps == 1 then
298 -- Suffix is unique
299 local unique_map_entry = suffixed_maps[1]
300 if to_promote_name_to_id[name] == unique_map_entry.map.id then
301 unique_map_name_to_id[name] = {id = unique_map_entry.map.id, proxy = unique_map_entry.map.proxy}
302 else
303 unique_map_name_to_id[strjoin("/", name, suffix)] = {id = unique_map_entry.map.id, proxy = unique_map_entry.map.proxy}
304 end
305 maps[unique_map_entry.key] = nil
306 else
307 found_dupe = true
308 end
309 end
310 until not found_dupe
311 end
312 end
313
314 -- Build the outputs for searching and displaying
315 for name, id_and_proxy in pairs(unique_map_name_to_id) do
316 normalized_unique_map_name_to_id[strlower(gsub(name, "%s+", ""))] = id_and_proxy
317
318 -- Get the shortest, non-numeric (if possible) name for the map
319 local id = id_and_proxy.id
320 local map_id_to_unique_name_entry = map_id_to_unique_name[id]
321 if map_id_to_unique_name_entry == nil or tonumber(map_id_to_unique_name_entry) then
322 map_id_to_unique_name[id] = gsub(name, "/", ":") -- front-end uses ":" as separator
323 end
324 end
325end
326