@@ -232,7 +232,6 @@ BLOOD_ELF_FALLBACK_ZONES = BuildNormalizedStringSet(
232232
233233BLOOD_ELF_VOICE_SCOPE_TOKENS = BuildNormalizedStringSet (
234234 GetConfigValue (" voice" , " scope" , " scopeTokens" ) or {
235- " quelthalas" ,
236235 " silvermooncity" ,
237236 " eversongwoods" ,
238237 " ghostlands" ,
@@ -241,6 +240,12 @@ BLOOD_ELF_VOICE_SCOPE_TOKENS = BuildNormalizedStringSet(
241240 }
242241)
243242
243+ BLOOD_ELF_VOICE_NATIVE_ONLY_TOKENS = BuildNormalizedStringList (
244+ GetConfigValue (" voice" , " scope" , " nativeOnlyTokens" ) or {
245+ " harandar" ,
246+ }
247+ )
248+
244249BLOOD_ELF_TOOLTIP_RACE_TOKENS = BuildNormalizedStringList (
245250 GetConfigValue (" voice" , " classification" , " tooltipBloodElfRaceTokens" ) or {
246251 " blood elf" ,
@@ -261,6 +266,17 @@ CHILD_NAME_TOKENS = BuildNormalizedStringList(
261266 }
262267)
263268
269+ BLOOD_ELF_HIDDEN_RACE_NAME_TOKENS = BuildNormalizedStringList (
270+ GetConfigValue (" voice" , " classification" , " hiddenRaceNameTokens" ) or {
271+ " silvermoon" ,
272+ " sindorei" ,
273+ " farstrider" ,
274+ " magister" ,
275+ " spellbreaker" ,
276+ " bloodknight" ,
277+ }
278+ )
279+
264280KNOWN_NON_BLOOD_ELF_RACE_TOKENS = BuildNormalizedStringList (
265281 GetConfigValue (" voice" , " classification" , " knownNonBloodElfRaceTokens" ) or {
266282 " human" ,
@@ -448,13 +464,13 @@ function NormalizeAreaMatchToken(text)
448464 return normalized
449465end
450466
451- function AreaHasNativeOnlyMusic (text )
467+ local function AreaMatchesTokenList (text , tokenList )
452468 local normalized = NormalizeAreaMatchToken (text )
453- if normalized == " " then
469+ if normalized == " " or not tokenList then
454470 return false
455471 end
456472
457- for _ , token in ipairs (BLOOD_ELF_MUSIC_NATIVE_ONLY_TOKENS ) do
473+ for _ , token in ipairs (tokenList ) do
458474 if strfind (normalized , token , 1 , true ) ~= nil then
459475 return true
460476 end
@@ -463,6 +479,14 @@ function AreaHasNativeOnlyMusic(text)
463479 return false
464480end
465481
482+ function AreaHasVoiceNativeOnly (text )
483+ return AreaMatchesTokenList (text , BLOOD_ELF_VOICE_NATIVE_ONLY_TOKENS )
484+ end
485+
486+ function AreaHasNativeOnlyMusic (text )
487+ return AreaMatchesTokenList (text , BLOOD_ELF_MUSIC_NATIVE_ONLY_TOKENS )
488+ end
489+
466490function GetCurrentMapLineageTokens ()
467491 local tokens = {}
468492 if not (C_Map and C_Map .GetBestMapForUnit and C_Map .GetMapInfo ) then
@@ -1075,11 +1099,22 @@ local function GetNPCIDFromGUID(guid)
10751099 return npcID
10761100end
10771101
1078- local function IsInBloodElfFallbackArea ()
1102+ local function IsInBloodElfVoiceArea ()
10791103 local zoneName = GetRealZoneText () or GetZoneText () or " "
10801104 local subZoneName = GetSubZoneText () or " "
10811105 local zoneKey = string.lower (zoneName )
10821106 local subZoneKey = string.lower (subZoneName )
1107+ local mapTokens = GetCurrentMapLineageTokens ()
1108+
1109+ if AreaHasVoiceNativeOnly (zoneName ) or AreaHasVoiceNativeOnly (subZoneName ) then
1110+ return false , " native-only" , zoneName , subZoneName
1111+ end
1112+
1113+ for _ , token in ipairs (BLOOD_ELF_VOICE_NATIVE_ONLY_TOKENS ) do
1114+ if mapTokens [token ] then
1115+ return false , " native-only-map" , zoneName , subZoneName
1116+ end
1117+ end
10831118
10841119 if BLOOD_ELF_FALLBACK_ZONES [zoneKey ] == true then
10851120 return true , " zone" , zoneName , subZoneName
@@ -1099,7 +1134,6 @@ local function IsInBloodElfFallbackArea()
10991134 return true , " subzone-token" , zoneName , subZoneName
11001135 end
11011136
1102- local mapTokens = GetCurrentMapLineageTokens ()
11031137 for token in pairs (mapTokens ) do
11041138 if BLOOD_ELF_VOICE_SCOPE_TOKENS [token ] then
11051139 return true , " map" , zoneName , subZoneName
@@ -1177,25 +1211,47 @@ local function GetUnitTooltipIdentity(unit)
11771211 return info
11781212end
11791213
1214+ local function IsUnitDeadForVoice (unit )
1215+ return UnitExists (unit ) and UnitIsDeadOrGhost and UnitIsDeadOrGhost (unit )
1216+ end
1217+
1218+ local NameLooksLikeBloodElfHiddenRaceFallback
1219+
11801220local function IsLikelyBloodElfFallback (unit , allowWithoutGossip )
11811221 if not UnitExists (unit ) or UnitIsPlayer (unit ) then
11821222 return false
11831223 end
11841224
1225+ if IsUnitDeadForVoice (unit ) then
1226+ if BElfVRDB and BElfVRDB .verbose then
1227+ Log (" Fallback blocked because target is dead." )
1228+ end
1229+ return false
1230+ end
1231+
11851232 local creatureType = UnitCreatureType (unit )
11861233 local sex = UnitSex (unit )
11871234 local canGossip = GossipFrame and GossipFrame :IsShown ()
1235+ local attackable = UnitCanAttack and UnitCanAttack (" player" , unit )
11881236
11891237 if BElfVRDB and BElfVRDB .verbose then
11901238 Log (" Fallback check: creatureType=" .. tostring (creatureType or " ?" ) ..
11911239 " sex=" .. tostring (sex or " ?" ) ..
1192- " gossipShown=" .. tostring (canGossip ))
1240+ " gossipShown=" .. tostring (canGossip ) ..
1241+ " attackable=" .. tostring (attackable ))
1242+ end
1243+
1244+ if attackable then
1245+ if BElfVRDB and BElfVRDB .verbose then
1246+ Log (" Fallback blocked because target is hostile/attackable." )
1247+ end
1248+ return false
11931249 end
11941250
1195- local zoneAllowed , scopeSource , zoneName , subZoneName = IsInBloodElfFallbackArea ()
1251+ local zoneAllowed , scopeSource , zoneName , subZoneName = IsInBloodElfVoiceArea ()
11961252 if not zoneAllowed then
11971253 if BElfVRDB and BElfVRDB .verbose then
1198- Log (" Fallback blocked outside Blood Elf zones : zone=" ..
1254+ Log (" Fallback blocked outside supported Blood Elf voice scope ( " .. tostring ( scopeSource ) .. " ) : zone=" ..
11991255 tostring (zoneName ~= " " and string.lower (zoneName ) or " ?" ) ..
12001256 " subzone=" .. tostring (subZoneName ~= " " and string.lower (subZoneName ) or " ?" ))
12011257 end
@@ -1206,8 +1262,21 @@ local function IsLikelyBloodElfFallback(unit, allowWithoutGossip)
12061262 Log (" Fallback zone scope accepted via " .. tostring (scopeSource ))
12071263 end
12081264
1209- if creatureType == " Humanoid" and (sex == 2 or sex == 3 ) and (canGossip or allowWithoutGossip ) then
1210- return true
1265+ if creatureType == " Humanoid" and (sex == 2 or sex == 3 ) then
1266+ if canGossip then
1267+ return true
1268+ end
1269+
1270+ if allowWithoutGossip and NameLooksLikeBloodElfHiddenRaceFallback (unit ) then
1271+ if BElfVRDB and BElfVRDB .verbose then
1272+ Log (" Fallback accepted before gossip because target name/profile matches Blood Elf hints." )
1273+ end
1274+ return true
1275+ end
1276+ end
1277+
1278+ if BElfVRDB and BElfVRDB .verbose and allowWithoutGossip and not canGossip then
1279+ Log (" Fallback blocked before gossip because target name/profile lacks Blood Elf hints." )
12111280 end
12121281
12131282 return false
@@ -2120,6 +2189,24 @@ local function GetUnitVORangeTier(unit)
21202189 return nil
21212190end
21222191
2192+ local function CanPlayTargetSelectGreeting (unit )
2193+ if not UnitExists (unit ) then
2194+ return false
2195+ end
2196+
2197+ if IsUnitDeadForVoice (unit ) then
2198+ Log (" Skipping target-select greet because target is dead." )
2199+ return false
2200+ end
2201+
2202+ if UnitCanAttack and UnitCanAttack (" player" , unit ) then
2203+ Log (" Skipping target-select greet because target is hostile/attackable." )
2204+ return false
2205+ end
2206+
2207+ return true
2208+ end
2209+
21232210local function ShouldPlayForRangeTier (rangeTier )
21242211 if rangeTier == " close" then
21252212 return true
@@ -2235,6 +2322,26 @@ local function NameLooksLikeChildNPC(unit)
22352322 return false
22362323end
22372324
2325+ NameLooksLikeBloodElfHiddenRaceFallback = function (unit )
2326+ local nameKey = NormalizeUserConfigKey (UnitName (unit ))
2327+ if nameKey == " " then
2328+ return false
2329+ end
2330+
2331+ local profile = GetDefaultNameProfile (unit )
2332+ if profile and not profile .exclude then
2333+ return true
2334+ end
2335+
2336+ for _ , token in ipairs (BLOOD_ELF_HIDDEN_RACE_NAME_TOKENS ) do
2337+ if strfind (nameKey , token , 1 , true ) ~= nil then
2338+ return true
2339+ end
2340+ end
2341+
2342+ return false
2343+ end
2344+
22382345local function GetConfiguredNameGenderOverride (unit )
22392346 local name = UnitName (unit )
22402347 if not name then
@@ -2487,6 +2594,19 @@ local function GetBloodElfNPCGender(unit, allowHiddenRaceFallbackWithoutGossip)
24872594 -- We only want NPCs, not player characters
24882595 if UnitIsPlayer (unit ) then return nil end
24892596
2597+ if IsUnitDeadForVoice (unit ) then
2598+ Log (" Target is dead; keeping native voice: " .. tostring (UnitName (unit )))
2599+ return nil
2600+ end
2601+
2602+ local zoneAllowed , scopeSource , zoneName , subZoneName = IsInBloodElfVoiceArea ()
2603+ if not zoneAllowed then
2604+ Log (" Skipping TBC voice outside supported areas (" .. tostring (scopeSource ) .. " ): zone=" ..
2605+ tostring (zoneName ~= " " and string.lower (zoneName ) or " ?" ) ..
2606+ " subzone=" .. tostring (subZoneName ~= " " and string.lower (subZoneName ) or " ?" ))
2607+ return nil
2608+ end
2609+
24902610 if IsExcludedByNameProfile (unit ) then
24912611 Log (" Target is excluded by built-in name profile: " .. tostring (UnitName (unit )))
24922612 return nil
@@ -2525,6 +2645,10 @@ local function GetBloodElfNPCGender(unit, allowHiddenRaceFallbackWithoutGossip)
25252645
25262646 local tooltipIdentity = GetUnitTooltipIdentity (unit )
25272647
2648+ if BElfVRDB and BElfVRDB .verbose then
2649+ Log (" Voice scope accepted via " .. tostring (scopeSource ))
2650+ end
2651+
25282652 if tooltipIdentity .hasChildMarker then
25292653 Log (" Target looks like a child NPC; keeping native voice: " .. tostring (UnitName (unit )))
25302654 return nil
@@ -2826,15 +2950,30 @@ local function BeginStartupMusicChannelPurge(reason)
28262950
28272951 C_Timer .After (MUSIC_STARTUP_PURGE_DELAY_SECONDS , function ()
28282952 local restoreValue = GetPendingCVarRestore (" Sound_EnableMusic" ) or currentMusicEnabled
2953+ local resumedReason = state .musicStartupPurgeReason or " startup purge"
2954+ local resumeContext = GetMusicContext ()
2955+ local shouldMuteTrackedMusic = resumeContext .supported and IsMusicReplacementActive ()
2956+
2957+ SetTrackedMusicMutesActive (shouldMuteTrackedMusic , resumeContext .regionKey , resumedReason .. " pre-enable" )
2958+
2959+ if StopMusic then
2960+ StopMusic ()
2961+ state .musicLastNativeStopAt = GetTime ()
2962+ end
2963+
28292964 if GetCVar (" Sound_EnableMusic" ) ~= tostring (restoreValue ) then
28302965 state .musicStartupPurgeIgnoreNextCVar = true
28312966 SetCVar (" Sound_EnableMusic" , tostring (restoreValue ))
2967+
2968+ if StopMusic then
2969+ StopMusic ()
2970+ state .musicLastNativeStopAt = GetTime ()
2971+ end
28322972 end
28332973
28342974 ClearPendingCVarRestore (" Sound_EnableMusic" )
28352975 state .musicStartupPurgeInProgress = false
28362976
2837- local resumedReason = state .musicStartupPurgeReason or " startup purge"
28382977 state .musicStartupPurgeReason = nil
28392978
28402979 EvaluateMusicState (resumedReason .. " resume" , true )
@@ -3476,6 +3615,10 @@ local function OnTargetChanged()
34763615 return
34773616 end
34783617
3618+ if not CanPlayTargetSelectGreeting (" target" ) then
3619+ return
3620+ end
3621+
34793622 local rangeTier = GetUnitVORangeTier (" target" )
34803623 if not rangeTier then
34813624 Log (" Skipping target-select greet because target is out of VO range." )
@@ -3492,6 +3635,8 @@ local function OnTargetChanged()
34923635 return
34933636 end
34943637
3638+ -- Hidden-race fallback on target-select is restricted to positive
3639+ -- Blood Elf name/profile hints, not generic humanoids.
34953640 local gender = GetBloodElfNPCGender (" target" , true )
34963641 if not gender then
34973642 return
0 commit comments