• Check out the results of the Techtree Contest #19!
  • Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.
  • Create a void inspired texture for Warcraft 3 and enter Hive's 34th Texturing Contest: Void! Click here to enter!
  • The Hive's 22nd Icon Contest: Creep Abilities is now concluded, time to vote for your favourite set of icons! Click here to vote!

LUA Unlimited Unit Sound Sets 0.0.4

This bundle is marked as pending. It has not been reviewed by a staff member yet.
LUA Unlimited Unit Sound Set System
Originally created in JASS by Baradé
Ported to LUA and extended by Macielos


Using custom unit voices (self-recorded, imported from WoW or other games, generated using Elevenlabs etc.) has always had lots of limitations. You had to override sound files of existing characters and only had limited number of voicelines of each sound type.

For some time I've been working on a system to overcome those limitations. Long ago I found an old system in JASS by Baradé, took some parts of it and rewrote in LUA, vastly extended, refactored and added some new features. And I believe I can finally show it for the community to use. So here it is ;).

Features

Source Code

Usage

Notes & Credits


• Native unit sounds logic (on click voices, on order voices, pissed voices etc.) recreated in LUA
• Works for player units as well as allies with shared control
• Assign custom voicelines to unit types with single lines of script, without overiding existing sounds. The files only need to follow a naming convention (your prefix + sound type + number) and you can just mass import them to your map
• (Practically) unlimited number of voiceline files for each sound type. You're so creative that you recorded 20 pissed sounds or 8 attack sounds for your unit? Now you can use them all!
• New voiceline type - voicelines played on usage of specific ability by a unit type (randomly if multiple ones are configured for one ability)
• You can add multiple sound sets additively and reuse voicelines for multiple unit types, creating sound sets that share some of the lines, e.g. for a specific maps in the campaign or for different variants of the same unit
• Extra utility function to track player's main selected unit

The source code is available on Github:


1. Import the library
  • Make sure your map has scripting language set to LUA in map options
  • Copy Libraries folder from test map's triggers to your map - OR if you prefer to copy dependencies separately (or you have them already), just copy UnitSoundSetsAndUtils folder
  • If you also use my Dialog System, you will find some scripts that are shared across the two. They are all in UnitSoundSets/Utils folder. They are (or at least should be) backward-compatible, so, just take them from the resource that’s newer (based on asset update date on Hive)

2. Import your sound files
- Supported extensions are .mp3, .wav and .flac. Best to use .mp3 format because of its small file size.

- Sound files for each unit type should start with the same prefix (it can contain a path, e.g. war3mapImported/Paladin), followed by a sound type and a sound number. Generally it follows Blizzard’s convention. Here's how the example sound set files could be named:

war3mapImported/PaladinReady1.mp3
war3mapImported/PaladinDeath1.mp3
war3mapImported/PaladinWarcry1.mp3
war3mapImported/PaladinWhat1.mp3
war3mapImported/PaladinWhat2.mp3
war3mapImported/PaladinWhat3.mp3
war3mapImported/PaladinYes1.mp3
war3mapImported/PaladinYes2.mp3
war3mapImported/PaladinYesAttack1.mp3
war3mapImported/PaladinYesAttack2.mp3
war3mapImported/PaladinPissed1.mp3
war3mapImported/PaladinPissed2.mp3
war3mapImported/PaladinPissed3.mp3
war3mapImported/PaladinPissed4.mp3

- The following sound types are supported:

Death sounds:
Death1

On-click sounds:
What1
What2
...

On-order (move, patrol etc.) sounds:
Yes1
Yes2
...

On-attack sounds (2 alternative suffixes, you can use either Attack or YesAttack):
Attack1
Attack2
...
OR
YesAttack1
YesAttack2
...

Sounds played when unit's trained/hired/revived:
Ready1

Sounds played with a certain chance when attacking a hero:
Warcry1

Secret/Hidden/Pissed sounds (again, several alternatives available because Nyctaeus and I couldn't stick to a single convention when recording our sound sets):
Pissed1
Pissed2
...
OR
Hidden1
Hidden2
...
OR
Gag1
Gag2
...

Ability sounds: played when a unit casts a specific ability. Here you can give any name if there's only one sound. If you specify multiple sounds for an ability, they should share a prefix and be ended with a proper number:
war3mapImported/PaladinHolyLight1.mp3
war3mapImported/PaladinHolyLight2.mp3
war3mapImported/PaladinHolyLight3.mp3

Yes, we implemented Margoth's Torrent of Profanity on this in Exodus 2.

3. Assign sound sets to unit types

  • Make sure your unit's sound set in object editor is set to NONE (so that it doesn't overlap with the custom one)
  • Place a script that will add sound sets on map initialization. Here are some examples (using Total Initialization library, but you can also call UnitSoundSets methods in your own scripts or triggers):

Lua:
do
    OnInit.final(function()
        --import sound set for a Paladin taking files imported as war3mapImported\Paladin<soundType><number>
        UnitSoundSets:addUnitSoundSet(FourCC('Hpal'), 'war3mapImported\\Paladin')
        --import ability single sound for a Paladin casting Divine Shield
        UnitSoundSets:addUnitAbilitySingleSound(FourCC('Hpal'), FourCC('AHds'), 'war3mapImported\\PaladinDivineShield')
        --import ability multiple sounds for a Paladin casting Holy Light - this will search for files war3mapImported\PaladinHolyLight1, war3mapImported\PaladinHolyLight2 etc. for the consequtive numbers unless no sound is found
        UnitSoundSets:addUnitAbilityMultipleSounds(FourCC('Hpal'), FourCC('AHhb'), 'war3mapImported\\PaladinHolyLight')
    end)
end
Lua:
do
    OnInit.final(function()
        --a more complex example - You have footman versions for every kingdom. A footman can have a basic sound set,
        --but you can also define some voices specific for different kingdoms
        local FOOTMAN_LORDAERON = FourCC('U123')
        local FOOTMAN_STROMGARDE = FourCC('U124')
        local FOOTMAN_GILNEAS = FourCC('U125')
        UnitSoundSets:addUnitSoundSet(FOOTMAN_LORDAERON, 'war3mapImported\\FootmanBase')
        UnitSoundSets:addUnitSoundSet(FOOTMAN_STROMGARDE, 'war3mapImported\\FootmanBase')
        UnitSoundSets:addUnitSoundSet(FOOTMAN_GILNEAS, 'war3mapImported\\FootmanBase')
        UnitSoundSets:addUnitSoundSet(FOOTMAN_LORDAERON, 'war3mapImported\\FootmanLordaeron')
        UnitSoundSets:addUnitSoundSet(FOOTMAN_STROMGARDE, 'war3mapImported\\FootmanStromgarde')
        UnitSoundSets:addUnitSoundSet(FOOTMAN_GILNEAS, 'war3mapImported\\FootmanGilneas')
    end)
end
Lua:
-- an example from the demo map which adds custom voices for Reynart and Inaylia as well as for Inaylia's abilities:
do
    --your unit codes from object editor
    local REYNART = FourCC('H000')
    local INAYLIA = FourCC('H001')

    --your ability codes from object editor
    local KNIVES = FourCC('AEfk')
    local SHIFT = FourCC('AEbl')
    local STEALTH = FourCC('AOwk')
    local LIQUIDATION = FourCC('ANfd')

    local PREFIX = 'war3mapImported\\'

    --just some utility functions so you don't have to repeat the full path for every unit
    local function addSoundSet(unitId, filePrefix)
        UnitSoundSets:addUnitSoundSet(unitId, PREFIX .. filePrefix)
    end

    local function addAbilitySoundSet(unitId, abilityId, filePrefix)
        UnitSoundSets:addUnitAbilitySingleSound(unitId, abilityId, PREFIX .. filePrefix)
    end

    --your function, it will be called on map initialization

    OnInit.final(function()
        print('InitDemoSoundSets START')
        addSoundSet(REYNART, 'Reynart')
        addSoundSet(INAYLIA, 'Inaylia')
        addAbilitySoundSet(INAYLIA, KNIVES, 'InalkaKnives')
        addAbilitySoundSet(INAYLIA, SHIFT, 'InalkaShift')
        addAbilitySoundSet(INAYLIA, STEALTH, 'InalkaStealth')
        addAbilitySoundSet(INAYLIA, LIQUIDATION, 'InalkaLiquidation')
        print('InitDemoSoundSets DONE')
    end)
end

Below you can find a basic API.

Lua:
--Add custom sound set for given unit type, reading sound files with given prefix
UnitSoundSets:addUnitSoundSet(unitTypeId, filePathPrefix)

--Remove custom sound sets from all units
UnitSoundSets:removeAllUnitSoundSets()

--Remove a sound set from a unit type
UnitSoundSets:removeUnitSoundSet(unitTypeId)

--true if a unit type has custom sound set defined
UnitSoundSets:hasUnitSoundSet(unitTypeId)

--true if a unit type has custom sound defined when casting a specific ability
UnitSoundSets:hasAbilitySoundSet(unitTypeId, abilityCode)

--Add custom sound when a unit of specific type casts a specific ability, in that case a number is not added to filePath
UnitSoundSets:addUnitAbilitySingleSound(unitTypeId, abilityId, filePath)

--Add custom sounds when a unit of specific type casts a specific ability
UnitSoundSets:addUnitAbilityMultipleSounds(unitTypeId, abilityId, filePathPrefix)

Unsupported features from Baradé's system:
  • Syncing user selections in a multiplayer game (to be honest, I couldn't really figure out what the point was of that code)
  • Tracking user selections per player - now I only track the local player
  • Unit sounds per unit instance
  • Shop's custom sound set
Limitations:
  • The library was made mainly with singleplayer in mind, it hasn't been tested in multiplayer so far, there may be desyncs
  • The library creates custom UI frames - health and mana "fake bars" to display when original ones are hidden. If you use customized UI, these fake bars' positions may not match the original (so they are in a different position when a unit is speaking). If that's the case, you may need to adjust these constants in FakeBars script:
local FAKE_BAR_X = 0.2145
local FAKE_HP_BAR_Y = 0.027
local FAKE_MANA_BAR_Y = 0.0135
Width/height is adjusted automatically, but x/y positions are hardcoded because there's no native to get them.

Dependencies:
- Debug Utils (in theory you don't need them, but IMO they're a must for anyone working with LUA)

- Total Initialization:

Changelog:

0.0.1
- Initial version

0.0.2
- Rearranged folders in trigger editor

0.0.3
  • Removed limits for the numbers of sound files - the lib will search for files with given prefix, e.g PaladinWhat1, PaladinWhat2 etc. and stop when no sound is found
  • Sounds no longer play when a unit is paused or sleeping
  • Sounds no longer play in cutscenes (it previously happened when a player had a unit selected, a cinematic started and a unit received an order via trigger)
  • Sounds no longer play when another sound is playing (so when we quickly select different units, we don't hear overlapping sounds)
0.0.4
- Made FakeBars use dynamic size from original health/mana bar frames. Adjusted x/y positions to better match originals

Credits:
- Baradé - for his original JASS system that used to be here:

Despite heavy changes and new features I still use some parts of his solution rewritten in LUA. Unfortunately he's no longer on Hive and didn't leave any contact info, so I couldn't reach him for permission, but my changes are so significant that I hope it won't be a problem. If he returns one day, I'll surely contact him.
Previews
Contents

LUA Unlimited Unit Sound Sets (Map)

Why does the user have to import all these debug functions? Please remove everything that isn't needed from the folder that's supposed to be imported. Similarly, virtually everyone has DebugUtils installed. There's no need to put that into the same folder.
Okay, I rearranged the folders so that there's no problem to import either the entirety of it, the library + my shared utils or just the library.
If you set the UI scale differently it shifts the HP.
Hmm, I see. Right now the positions are hardcoded as in the original system, so you can adjust them at the top of FakeBars script. But I'll see if I can get these positions from the current UI frames.

Other questions :
☼ Is it synced multiplayer ?
What do you mean? There's no data synced between players, voicelines are played for the local player only. But to be honest, I never made a multiplayer map, so maybe there are some issues I need to learn about.

☼ How do you check for sound length without calling the async GetSoundLength (can't remember the name) ?
I call GetSoundFileDuration(filePath). Are there problems with it in multiplayer?

☼ Does it work with localized sounds ?
It does, as long as the filepaths match and follow the naming convention. I just tested it for the Archmage in English and Polish version.
Lua:
        UnitSoundSets:addUnitSoundSet(FourCC('Hamg'), 'units\\human\\HeroArchMage\\HeroArchMage')

When you change game localization, a different file is loaded under the same path, so it works like any other imported sound.
 
Last edited:
I call GetSoundFileDuration(filePath). Are there problems with it in multiplayer?
Yes, I had the exact issue. It immediately throws a desync with everyone ; even if all players have the same locale.
I had to hardcode a 1.5s default duration ; too lazy yet to insert all durations one by one (and it gets more complicated if localized soundsets are implemented...).
What do you mean? There's no data synced between players, voicelines are played for the local player only. But to be honest, I never made a multiplayer map, so maybe there are some issues I need to learn about.
Another problem is syncing the timer for all players, else it becomes very unstable. Also I suspect getting main selected unit to be async as well.
 

Can this be fixed? This seems not great.

If we have to use a fake health and mana bar some of the time, why are we not just using it all of the time?

The MAX_SOUNDS constants are completely unnecessary, as far as I can tell. You already have logic to determine how many sound files of a certain type are imported, so there is no need to hardcode a limit.

The SOUND_DEATH, SOUND_WHAT etc. constants should also not be in the config because there is not reason to change them. These could also just be string literals instead of integers actually, but that's just a nitpick/personal preference.

Why are you using colon syntax? You're not using the self parameter in any of the functions.
 
Last edited:
Can this be fixed? This seems not great.
Okay, I made a version that takes these coordinates from frame. I could only take width/height, since there is no native to get x/y positions of frame points. X/Y positions I adjusted manually by some magic numbers stuff. You can try if it's better now.

Although it could be dependent on screen resolution - previous hardcoded numbers were working for me both on wide and laptop monitor. And of course, if you use customized fdf files for these UI elements, they won't match - you'd have to adjust the positions in my script.

If we have to use a fake health and mana bar some of the time, why are we not just using it all of the time?
I'd have to hide the original bars, and I'd rather avoid doing that because blizz will change UI structure and my script will break the game like it happened already in the past. Maybe I could display mine on top of the original, but then their positions would have to match anyway.

The MAX_SOUNDS constants are completely unnecessary, as far as I can tell. You already have logic to determine how many sound files of a certain type are imported, so there is no need to hardcode a limit.
aaand its gone.jpg

The SOUND_DEATH, SOUND_WHAT etc. constants should also not be in the config because there is not reason to change them. These could also just be string literals instead of integers actually, but that's just a nitpick/personal preference.
Yeah, they're constants as you said. It should be more clear now after I removed the limits, so in fact there's no config someone might want to modify in standard cases.

Why are you using colon syntax? You're not using the self parameter in any of the functions.
I think I used self in previous versions, I'm not sure now. But I'd rather not change it now not to break compatibility.
 
Okay, I made a version that takes these coordinates from frame. I could only take width/height, since there is no native to get x/y positions of frame points. X/Y positions I adjusted manually by some magic numbers stuff. You can try if it's better now.

Although it could be dependent on screen resolution - previous hardcoded numbers were working for me both on wide and laptop monitor. And of course, if you use customized fdf files for these UI elements, they won't match - you'd have to adjust the positions in my script.
The mismatch seems to be resolution-dependent, so I guess there's no way to make it perfect. RIP

I'd have to hide the original bars, and I'd rather avoid doing that because blizz will change UI structure and my script will break the game like it happened already in the past. Maybe I could display mine on top of the original, but then their positions would have to match anyway.
Scripts can be updated. I don't find this a compelling argument.

Is it actually not possible to show the health/mana bars again after sending the unit transmission? Did you try that?

Lua:
local playerId = LoadInteger(h2, GetHandleId(GetExpiredTimer()), 0)
GetHandleId is not safe to use in Lua. You'd have to replace this with a regular old table.
 
For some reason, I can't access the map in-game. Am I missing something, or is it just a bug?
 
Back
Top