Skip to content

Commit 3c205c4

Browse files
committed
release: cut v0.6.1-alpha
1 parent fbd009e commit 3c205c4

6 files changed

Lines changed: 218 additions & 50 deletions

File tree

BElfRestore.lua

Lines changed: 157 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,6 @@ BLOOD_ELF_FALLBACK_ZONES = BuildNormalizedStringSet(
232232

233233
BLOOD_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+
244249
BLOOD_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+
264280
KNOWN_NON_BLOOD_ELF_RACE_TOKENS = BuildNormalizedStringList(
265281
GetConfigValue("voice", "classification", "knownNonBloodElfRaceTokens") or {
266282
"human",
@@ -448,13 +464,13 @@ function NormalizeAreaMatchToken(text)
448464
return normalized
449465
end
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
464480
end
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+
466490
function 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
10761100
end
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
11781212
end
11791213

1214+
local function IsUnitDeadForVoice(unit)
1215+
return UnitExists(unit) and UnitIsDeadOrGhost and UnitIsDeadOrGhost(unit)
1216+
end
1217+
1218+
local NameLooksLikeBloodElfHiddenRaceFallback
1219+
11801220
local 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
21212190
end
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+
21232210
local function ShouldPlayForRangeTier(rangeTier)
21242211
if rangeTier == "close" then
21252212
return true
@@ -2235,6 +2322,26 @@ local function NameLooksLikeChildNPC(unit)
22352322
return false
22362323
end
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+
22382345
local 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

CHANGELOG.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Changelog
22

3+
## 0.6.1-alpha - 2026-03-08
4+
5+
- Applied voice-area gating to all TBC replacement playback so out-of-scope NPCs no longer inherit Blood Elf voices just because their tooltip exposes Blood Elf race text.
6+
- Added voice-side native-only exclusions so areas such as `Harandar` stay on Blizzard voice just like they already do on the music side.
7+
- Narrowed hidden-race target-select fallback from generic humanoids to positive Blood Elf name/profile hints, preventing false positives on unrelated nearby humanoids.
8+
- Blocked target-select TBC playback for dead and hostile or attackable units.
9+
- Restored hidden-race target-select recognition for `Doomsayer` and `Household Attendant` via exact built-in Blood Elf name profiles.
10+
- Fixed the Lua reload crash caused by a forward-referenced hidden-race fallback helper.
11+
- Tightened startup music purge ordering so tracked music mutes are armed before music is re-enabled, reducing Silvermoon intro overlap on login and `/reload`.
12+
- Reworked the README testing guidance around exploratory issue-tracker-driven testing and trimmed the README limitations list to durable user-facing constraints.
13+
314
## 0.6.0-alpha - 2026-03-07
415

516
- Tightened music ownership to Midnight Quel'Thalas only by adding scope checks from zone text, subzone text, and parent map lineage.
@@ -121,11 +132,3 @@
121132
- Added NPC names to key verbose trigger logs for easier troubleshooting.
122133
- Restricted the hidden-race humanoid fallback to Blood Elf zones to prevent false positives like Zul'Aman trolls.
123134

124-
## Notes For Next Iteration
125-
126-
- Expand the Silvermoon music allow-list using trace recordings from more interiors and subzones.
127-
- Validate whether additional Midnight music FileDataIDs still need muting.
128-
- Consider exposing music timing values in the UI once the zone map stabilizes.
129-
- Consider one-time migration cleanup for legacy saved variables instead of clearing them every load.
130-
- Replace placeholder TOC metadata before any public release.
131-

0 commit comments

Comments
 (0)