• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

Baradé's Unit Sound Sets 1.0

Motivation
This system helps you using custom unit sound sets without replacing existing ones.
Usually, you would import the sound set files and use paths of an existing sound set, making the existing one unavailable for your map.
This system allows you registering unit sound sets with different file paths using a simple JASS function.
The whole functionality of the unit sound set is emulated with JASS.

Usage
- Import your sound set files into your map.
- Set the sound set for the specific unit type to "NONE" in the object editor.
- Register your sound set for the specific unit type of your map with this JASS function call:
JASS:
call AddUnitSoundSetFromFiles('hhes', "war3mapImported\\Elven Swordman ")
This will play the correct sound files. It will check for the sound files with the extensions "mp3", "flac", and "wav" automatically.

- Optional: The HP and mana texts below the unit portrait are faked by default during a cinematic scene. This is done by the library UnitSoundSetsFakeBars. You can disable the trigger with the system since it is optional.

- Optional: If you have shop buildings which should play custom sounds when selected by other players, use the following JASS code to register them:
JASS:
call AddUnitShopSoundSet('ngme')
Replace the raw code with the one of your shop building type.
- Optional: If you you want to use different unit sound sets per unit, add the optional extension UnitSoundSetsPerUnit and use the following JASS code to register them:
JASS:
call AddUnitSoundSetFromFilesPerUnit(GetSoldUnit(), "war3mapImported\\Elven Swordman ")
Replace GetSoldUnit() with any unit you want to use.

Dependencies
- Modified GetMainSelectedUnit for working with savegames by Tasyen

TODOs
-
FIXED (using fake frames): The portrait talk animation (SetCinematicScene) hides mana and health numbers and moves the latest game message one row to the top: https://www.hiveworkshop.com/threads/playing-portrait-talk-animation-without-hiding-hp-and-mana-or-showing-message.350900/
- FIXED: The portrait team color still has to recognize
JASS:
GetAllyColorFilterState
.

- The unit portrait can not be selected to move the camera to the unit during the portrait talk animation played with SetCinematicScene. Probably, this cannot be fixed.
- Game messages are moved one line to the top by SetCinematicScene even without subtitles on. Probably, this cannot be fixed.
- Check for desyncs in multiplayer (not tested yet).
- Handle attack sounds (not sure when they are used).
- DONE with the help of GetMainSelectedUnit: Does not detect the primary selected unit when selecting the whole group but plays a sound for the first selected unit. I don't know of any function which can return the primary selected unit which has the focus. Maybe I need to add a delay, get the group of selected units and it is the first one?
- Get the ordering player for unit orders like attack and move and play the sounds for the ordering player not the owner since the control could be shared. I do not know how to achieve this since there is no native for the ordering player. I could extract the selecting players and check for which players it is the primary selected unit and just play it for them?
- Does not recognize native unit sounds and wait for them to be played until it starts a new sound. Actually this is even Warcraft's default behavior.
- Custom sound sets for shops are supported but Warcraft would only play them if a unit of yours is selected by the shop. This still has to be supported by this system. For now, the sounds will always be played when you select the shop even if none of your units is targeted by the shop.
- Support unit skins via
JASS:
BlzGetUnitSkin
since it might change the unit sound set.
Contents

Baradé's Unit Sound Sets 1.0 (Map)

Reviews
Wrda
I haven't found desyncs. There's this small issue when you click to select a different unit while it is the same unit type as the previous unit before the sound ends, you'll hear a new "what" sound. There's a superposition of the hp/mana text when...
Level 25
Joined
Feb 2, 2006
Messages
1,689
Well I didn't test it yet in multiplayer and it has a number of limitations:
  • Hiding HP and mana for the talk animation.
  • Not playing the sound for the ordering player but the owner -> issue with shared control.
  • Does not recognize native unit sounds and wait for them to be played until it starts a new sound. This is Warcraft's default behavior. If you select a Paladin and then a footman, the footman sound starts immediately, so this is not an issue.
  • I don't know how to detect the focused unit in a selection. When you select a group, Warcraft waits until all units are selected and plays the sound for the focused unit only. My system plays the sound for the first selected unit or if currently a sound is being played for the one after the sound is done.
  • Someone wrote me that there is a sound limit of 16 concurrently played sounds, so this will take one of those sounds for constant clicking.

edit:
Updated the system:
  • The Ready1 sound is now played for Cenarius when he is revived.
  • Ready sounds are now played for training and hiring units.
  • Dying cancels the current sound immediately if the unit was talking.
  • Checks if unit is selected without using IsUnitSelected.
  • Death sound is required to be called Death1 now (seems to be the standard in Reforged).
  • Initial support custom sound sets for neutral shops. Still missing: Detect if the player can currently use the shop.
  • Refactoring: Use order IDs and more local variables.
  • Added kill function for the Escape key to the example map.
  • Added hero Funny Bunny, intro music and custom shop sounds for the Goblin Merchant to the example map.

edit2:
Updated the system again:
  • Do play sounds whenever you select a different unit.
  • Added animation types like PORTRAIT_ANIMATION_TYPE_ANIMATION which allows using a custom portrait model and does not hide HP or mana texts and does not display an empty game message.
  • Refactoring and less asynchronous code.

The type PORTRAIT_ANIMATION_TYPE_ANIMATION is still not finished. I still have to hide the standard portrait and change the team color of the custom portrait.
 
Last edited:
Level 25
Joined
Feb 2, 2006
Messages
1,689
I don't think it requires Reforged since the natives should exist in pre Reforged. I could even add an option for Reforged if some do not exist. Maybe just try.

edit:

Another update:
  • Use [Lua] - GetMainSelectedUnit to always play the sound for the main selected unit.
  • Run more code with GetLocalPlayer asynchronously and use more trigger actions because of this new dependency.
  • Add optional extension UnitSoundSetsPerUnit which allows to use sound sets per unit not only per unit type.
  • Increase the limit of different sounds to 9 (which seems to be the highest number in the standard sound sets).
 
Last edited:
Tested the 1.0 version. Still rough around the edges, the base is there but the rest is very annoying.

In game :
☼ Sounds have a delay
☼ Units do not use their YesAttack when ordered to manually attack (they don't use any sound)
☼ Units use the Yes instead of the YesAttack when ordered to attack neutral hostile
[see EDIT]
☼ Sounds tends to be repeated from one action to another, it must NEVER repeat the same line one after another if possible.

Quick view at JASS code :
☼ Much could be factorized since you repeat everything for each number.
☼ You may save per unit type the latest played sound so you could select another one (skipped for units with a single occurrence of a sound type)
☼ Use Trollbrain's Local Helper if needed
☼ You may replace TriggerSleepAction by a 0s timer, this would make the code more verbose but much safer & responsive.
☼ [EDIT] Warcry might be used for attack instead, but is quite rare ; this is told from experience so take this with a grain of salt. I don't know why your system seperates Warcry and YesAttack. I would put a boolean for each soundset if the mapper wants this unit to have the vanilla logic or yours.
☼ [EDIT] You may also check if the unit don't have any attack sound and use a Yes instead.

I might do my own experimentation to see if I can help you fix those things, but time is fleeting.
 
Last edited:
Level 25
Joined
Feb 2, 2006
Messages
1,689
okay I can try to fix some of the stuff.

The delay on selection might come from the TriggerSleepAction which I replace with a timer as you suggested. It is necessary to detect the main selected unit with GetMainSelectedUnitEx :(
I hope there is no other delay. Delays might come from the code execution if something is too slow.

There is a Warcry sound effect which is used when you attack a specific unit. Apparently, it is just mixed with the YesAttack sounds. When I use a footman, it randomly plays any of the Warcry and YesAttack sound effects, so I will change that.
It also seems that when you order attack ground, it should only play the yes sound and no yes attack.

I have to add some neutral hostile units to check that. I filtered some orders and identified them as attack orders (maybe not enough).

I did not know about not repeating sounds, so I will store the last sound index to avoid this and store the unit type, so it will work when you select another unit.

At some point I might refactor stuff. I don't think it is that bad to have functions to set each sound but you could also pass an index instead.

edit:
It seems that the order against creeps is "smart" so I have to check this and check if it is against an enemy unit.

edit2:
Thanks for the feedback. Here is a small update:

  • Use two hashtables to avoid collisions between unit type IDs and handle IDs.
  • Play Yes sounds for attack ground order instead of YesAttack.
  • Mix YesAttack and Warcry sounds.
  • Replace TriggerSleepAction with timer on unit selection.
  • Never play the same sound twice for the same unit type if there are multiple sounds.
  • Recognize attack order against creeps (smart).
 
Last edited:
If this would help you :
JASS:
public function PlayUnitYes takes unit u returns nothing
    call PlayRandomSound(u,SOUND_YES_1,SOUND_YES_9)
endfunction
public function PlayUnitYesAttack takes unit u returns nothing
    call PlayRandomSound(u,SOUND_YES_ATTACK_1,SOUND_YES_ATTACK_9)
endfunction
public function PlayUnitWhat takes unit u returns nothing
    call PlayRandomSound(u,SOUND_WHAT_1,SOUND_WHAT_9)
endfunction
public function PlayUnitPissed takes unit u returns nothing
    call PlayRandomSound(u,SOUND_PISSED_1,SOUND_PISSED_9)
endfunction

private function AfterSelect takes nothing returns nothing
    local timer t=GetExpiredTimer()
    local integer kT=GetHandleId(t)
    local unit triggerUnit=LoadUnitHandle(soundHash,kT,0)
    local player triggerPlayer=LoadPlayerHandle(soundHash,kT,1)

    if triggerPlayer==GetLocalPlayer() then
        set triggerUnit=GetMainSelectedUnitEx()
        if triggerUnit!=null and triggerUnit==GetMainSelectedUnitEx() and (HasControl(triggerPlayer,triggerUnit) or HasUnitShopSoundSet(GetUnitTypeId(triggerUnit))) then
            if IsPlayerSelectionPissed(triggerUnit) then
                call PlayUnitPissed(triggerUnit)
            else
                call PlayUnitWhat(triggerUnit)
            endif
        endif
    endif

    call FlushChildHashtable(soundHash,kT)
    call RemoveSavedHandle(soundHash,GetHandleId(triggerUnit),0)
    call PauseTimer(t)
    call DestroyTimer(t)

    set t=null
    set triggerUnit=null
    set triggerPlayer=null
endfunction

private function TriggerActionSelect takes nothing returns nothing
    local unit triggerUnit = GetTriggerUnit()
    local integer kU=GetHandleId(triggerUnit)
    local timer t=LoadTimerHandle(soundHash,kU,0)
    if t==null then
        set t=CreateTimer()
        call SaveTimerHandle(soundHash,kU,0,t)
        call SaveUnitHandle(soundHash,GetHandleId(t),0,triggerUnit)
        call SavePlayerHandle(soundHash,GetHandleId(t),1,GetTriggerPlayer())
        call TimerStart(t,0,false,function AfterSelect)
    endif
    set t=null
    set triggerUnit=null
endfunction


private function TriggerActionOrder takes nothing returns nothing
    local unit triggerUnit = GetTriggerUnit()
    local unit target=GetOrderTargetUnit()
    local integer orderId = 0
    if (HasControl(GetLocalPlayer(), triggerUnit) and GetMainSelectedUnitEx() == triggerUnit) then
        set orderId = GetIssuedOrderId()
        if orderId==ORDER_ID_SMART then
            if target!=null and IsUnitEnemy(triggerUnit,GetOwningPlayer(target)) then
                call PlayUnitYesAttack(triggerUnit)
            else
                call PlayUnitYes(triggerUnit)
            endif
        else
            if orderId == ORDER_ID_ATTACK then
                call PlayUnitYesAttack(triggerUnit)
            elseif orderId == ORDER_ID_MOVE or orderId == ORDER_ID_PATROL or orderId == ORDER_ID_ATTACK_GROUND then
                call PlayUnitYes(triggerUnit)
            endif
        endif
    endif
    set triggerUnit=null
    set target=null
endfunction

Other remarks while I am at it :
☼ Different unit types with the same soundset interfere with each other
Ready somewhat doesn't work, I might have broken something [EDIT] You put Ready1 instead of Ready in AddUnitSoundSetFromFiles.
☼ [EDIT 2] When a unit is ordered to smart the enemy it is already attacking, it doesn't play an attack sound. This might come from Warcraft 3 not launching the second smart order if the previous one is the same.
 
Last edited:
Level 15
Joined
Sep 26, 2007
Messages
369
When Warcraft 3 came out "ages" ago, the Warcry attack sound WAS and IS always heard when repeatedly ordering anyone to attack a Hero (no matter if the targeted Hero is an enemy, an ally, or even your own). Try it with the original sound sets and you'll get to hear it, which usually occurs at a random chance.
 
When Warcraft 3 came out "ages" ago, the Warcry attack sound WAS and IS always heard when repeatedly ordering anyone to attack a Hero (no matter if the targeted Hero is an enemy, an ally, or even your own). Try it with the original sound sets and you'll get to hear it, which usually occurs at a random chance.
In this case, check if the target is a hero, then roll the dice !
You made me learn something new today.
 

Kyrbi0

Arena Moderator
Level 45
Joined
Jul 29, 2008
Messages
9,502
My pleasure... but wait! Here's a nice and short video showing how this occurs, just for the sake of completeness...
derisive.gif


Notice when the Archmage shouts "For Dalaran!".
Wait..... seriously??

How am I just learning this? Where did you learn this?
 
Level 15
Joined
Sep 26, 2007
Messages
369
I think a better question would be "How did you discover this?". Also, I'm surprised you guys didn't even know! I mean, I thought this Warcry thing was a normal thing, not a rare thing.
shok.gif
Anyway, I'll gladly tell you how I discovered this while playing the original "Blizzie"'s Campaigns many years ago (and I still play nowadays, but only when the mood hits me)...

When I attack (or get attacked), I usually go for the enemy Heroes first (from which their armies get most support from), so I just select and order my units/Heroes to quickly take him/her down by right-clicking on them (besides using my Heroes' and spellcasters' spells for both offense and defense)... and I often have a habit of repeatedly (but not quickly) right-clicking on them, because not only I need my units to focus on my intended target but I also like hearing their YesAttack lines sometimes (which give me an adrenergic feeling they're actually talking to or taunting their enemies during heated and especially inspiring epic battles), and then... Boom Shaka Laka! That's how I discovered the "mysterious" origin of the Warcry line.

Off-topic:
Sure, I also use the attack-move (short for pressing the "A" key and then targeting intended part of an area), but sometimes it's not effective when the enemy Heroes try to outrun/outsmart/outmatch you with their spells (let alone the Ultimate ones), their supportive or offensive auras, and their tactics, and that's why I want to quickly "get rid of" any enemy Heroes I perceive so I can turn the tide of war in my favor!
dirol.gif
 
Last edited:
Made further improvements for fun. I only took care of the sound aspect of your library since manipulating portraits is still foreign to me. I don't know if I broke the rest so you may confirm yourself ! I also haven't changed the comments and description too.
Attacking a hero have 25% of chances to launch the WarCry
A missing soundstack will use another one :
WarCry -> YesAttack -> Yes
Pissed -> What
Factorized some functions for clarity
SOUND_PER_STACK to scale everything on one value, I put 9 to keep the same logic
Putting auto in AddUnitSoundSetFromFiles will load for the 3 extensions recursively
For the first occurence of Death, Warcry and Ready it will check for the unumbered version too, to be more forgiving about paths (for example looking for Ready instead of Ready1)
GetRandomSoundEx do not pick a sound which is already playing. This doesn't fix the whole problem but every polished bit counts.
Cleaned some minor reference leaks
Some functions return the played/selected sound to be easier to branch everything
Attack once order never happens from player perspective since it's trigger based. It is simply removed
Death is now properly a 3D sound, which can be played on a unit

Remaining fields of improvements :
Do not repeat the same sound twice in a row, even from one unittype from another
Detect sound duplicates to reduce the number of stored values and sound variables
Create a soundset data structure to centralise several unittypes with the same soundset
Slight pitch random variations on soundsets
Make unit answers 3D like in the base game (sound is lower when far from it)
Add options to specify the rarity of each sounds
Add optionnal sound categories, for example effort attack, low HP, healed, attack ground, repair, gather, build, spell, etc
JASS:
library UnitSoundSets initializer Init requires GetMainSelectedUnit, optional UnitSoundSetsPerUnit

/**
 * Baradé's Unit Sound Sets 1.0
 *
 * Allows using custom unit sound sets without replacing existing ones or using a custom SLK file.
 *
 * Usage:
 * - Disable the sound set of your unit type and then register the sounds using this system.
 * - Import custom soundset files into your map.
 * - Call this JASS code during the map initialization:
 *   call AddUnitSoundSetFromFiles('H0F2', "war3Imported\\HeroPaladin", "flac")
 *
 *   'H0F2' should be the raw code of your custom hero unit type.
 *   "war3Imported\\HeroPaladin" should be the prefix for all unit sound set files.
 *   "flac" should be the file extension for all unit sound set files.
 *
 * - Optional: If you want to have the talk animation in the portrait like in Warcraft III cinematics,
 * use the following JASS code to configure it:
 *   call SetUnitSoundSetAnimationType('H0F2', PORTRAIT_ANIMATION_TYPE_CINEMATIC)
 *
 * - Optional: If you want to have the talk animation in the portrait with a prepared portrait model,
 * use the following JASS code to configure it:
 *   call SetUnitSoundSetAnimationType('H0F2', PORTRAIT_ANIMATION_TYPE_ANIMATION)
 *   call SetUnitSoundSetPortraitModel('H0F2', "war3mapImported\\EasterBunny_Portrait.mdx")
 * Modify the portrait model and rename "Portrait Talk" into "morph" and the other animations into "stand".
 *
 * - Optional: If you have shop buildings which should play custom sounds when selected by other
 * players, use the following JASS code to register them:
 *   call AddUnitShopSoundSet('ngme')
 *
 *   Replace the raw code with the one of your shop building type.
 *
 * The following suffixes for sound files are supported:
 * Death1
 * What1
 * What2
 * What3
 * What4
 * What5
 * What6
 * What7
 * What8
 * What9
 * Yes1
 * Yes2
 * Yes3
 * Yes4
 * Yes5
 * Yes6
 * Yes7
 * Yes8
 * Yes9
 * YesAttack1
 * YesAttack2
 * YesAttack3
 * YesAttack4
 * YesAttack5
 * YesAttack6
 * YesAttack7
 * YesAttack8
 * YesAttack9
 * Ready1
 * WarCry1
 * Pissed1
 * Pissed2
 * Pissed3
 * Pissed4
 * Pissed5
 * Pissed6
 * Pissed7
 * Pissed8
 * Pissed9
 *
 * Disable cinematic sub titles with ForceCinematicSubtitles or in the game settings.
 * Selecting the same unit again will not play the next sound immediately but wait for the current sound
 * to be finished. Selecting another unit will immediately play the sound of the other unit. This seems to be
 * Warcraft's behavior.
 */

globals
    // You will hear the pissed sounds after this number of clicks on the same unit.
    constant integer PISSED_COUNTER = 3
  
    // Shows no talk animation in the portrait (default).
    constant integer PORTRAIT_ANIMATION_TYPE_NONE = 0
    // Uses custom portrait frames without hidden HP and mana.
    constant integer PORTRAIT_ANIMATION_TYPE_ANIMATION = 1
    // Shows talk animations during the sounds as long as the unit is selected if this value is true.
    constant integer PORTRAIT_ANIMATION_TYPE_CINEMATIC = 2

    // Death : Whenver the unit dies, the Death Sound will be played.
    constant integer array SOUND_DEATH
    // What : Occurs when you click on the unit only. Replaced by Pissed Sounds if you click multiple times on the unit in a short time.
    constant integer array SOUND_WHAT
    // Yes : Occurs when you order the unit to do a "Friendly" Action.
    constant integer array SOUND_YES
    // YesAttack : Occurs when you order the unit to "Attack-Move", or an order similar to it.
    constant integer array SOUND_YES_ATTACK
    // Ready : Occurs only one time for a single unit, it is played when the unit is spawned by a building (Example : Barracks for Rifleman), or when they got revived by an Altar (Example : Paladin).
    constant integer array SOUND_READY
    // Warcry : Occurs when you order the unit to attack a specific unit.
    constant integer array SOUND_WARCRY
    // Pissed : When you click a lot of times on the unit, weird sounds will be played instead of normals, which are called Pissed Sounds.
    constant integer array SOUND_PISSED
    // Attack : Occurs when the unit starts attacking an enemy. (You don't have to click on the unit)
  
    constant integer KEY_HAS_CUSTOM_SOUNDSET = 39
    constant integer KEY_HAS_SHOP_SOUNDSET = 40
    constant integer KEY_PORTRAIT_ANIMATION_TYPE = 41
    constant integer KEY_PORTRAIT_MODEL = 42

    private constant integer ORDER_ID_ATTACK = 851983
    private constant integer ORDER_ID_ATTACK_GROUND = 851984
    private constant integer ORDER_ID_ATTACK_ONCE = 851985
    private constant integer ORDER_ID_MOVE = 851986
    private constant integer ORDER_ID_PATROL = 851990
    private constant integer ORDER_ID_SMART = 851971
    private constant integer SOUND_PER_STACK=9
  
    private hashtable h = InitHashtable()
  
    // all this exists for the local player only and is never synchronized between players
    private integer playerClickCounter = 0
    private unit playerClickTarget = null
    private sound playerSound = null
    private unit playerSpeaker = null
    private timer playerAnimationTimer = CreateTimer()
    private framehandle playerPortrait = null

    private trigger selectionTrigger = CreateTrigger()
    private trigger deselectionTrigger = CreateTrigger()
    private trigger orderTrigger = CreateTrigger()
    private trigger revivalTrigger = CreateTrigger()
    private trigger trainTrigger = CreateTrigger()
    private trigger hireTrigger = CreateTrigger()
    private trigger deathTrigger = CreateTrigger()
endglobals

function RemoveAllUnitSoundSets takes nothing returns nothing
    call FlushParentHashtable(h)
endfunction

function RemoveUnitSoundSet takes integer unitTypeId returns nothing
    call FlushChildHashtable(h, unitTypeId)
endfunction

function AddUnitSoundSet takes integer unitTypeId, integer t, sound whichSound returns sound
    if (whichSound != null) then
        call SaveSoundHandle(h, unitTypeId, t, whichSound)
        call SaveBoolean(h, unitTypeId, KEY_HAS_CUSTOM_SOUNDSET, true)
    endif
    return whichSound
endfunction

function HasUnitSoundSet takes integer unitTypeId returns boolean
    return LoadBoolean(h, unitTypeId, KEY_HAS_CUSTOM_SOUNDSET)
endfunction

function AddUnitSoundSetDeath takes integer unitTypeId, sound whichSound, integer index returns sound
    return AddUnitSoundSet(unitTypeId,SOUND_DEATH[index],whichSound)
endfunction

function AddUnitSoundSetWhat takes integer unitTypeId, sound whichSound, integer index returns sound
    return AddUnitSoundSet(unitTypeId,SOUND_WHAT[index],whichSound)
endfunction

function AddUnitSoundSetYes takes integer unitTypeId, sound whichSound, integer index returns sound
    return AddUnitSoundSet(unitTypeId,SOUND_YES[index], whichSound)
endfunction

function AddUnitSoundSetYesAttack takes integer unitTypeId, sound whichSound, integer index returns sound
    return AddUnitSoundSet(unitTypeId,SOUND_YES_ATTACK[index],whichSound)
endfunction

function AddUnitSoundSetReady takes integer unitTypeId, sound whichSound, integer index returns sound
    return AddUnitSoundSet(unitTypeId,SOUND_READY[index],whichSound)
endfunction

function AddUnitSoundSetWarCry takes integer unitTypeId, sound whichSound, integer index returns sound
    return AddUnitSoundSet(unitTypeId,SOUND_WARCRY[index],whichSound)
endfunction

function AddUnitSoundSetPissed takes integer unitTypeId, sound whichSound, integer index returns sound
    return AddUnitSoundSet(unitTypeId,SOUND_PISSED[index],whichSound)
endfunction

private function CreateSoundFromFile takes string filePath, string eaxSetting returns sound
    local sound soundHandle = null
    local integer duration = GetSoundFileDuration(filePath)
    if (duration > 0) then
        set soundHandle = CreateSound(filePath, false, false, true, 12700, 12700, eaxSetting)
        call SetSoundDuration(soundHandle, duration)
    endif
    return soundHandle
endfunction

private function CreateSound3DFromFile takes string filePath, string eaxSetting returns sound
    local sound soundHandle = null
    local integer duration = GetSoundFileDuration(filePath)
    if (duration > 0) then
        set soundHandle = CreateSound(filePath, false, true, true, 12700, 12700, eaxSetting)
        call SetSoundDuration(soundHandle, duration)
    endif
    return soundHandle
endfunction

function AddUnitSoundSetFromFiles takes integer unitTypeId, string filePath, string extension returns nothing
    local integer i=0
    if extension=="auto" then
        call AddUnitSoundSetFromFiles(unitTypeId,filePath,"mp3")
        call AddUnitSoundSetFromFiles(unitTypeId,filePath,"flac")
        call AddUnitSoundSetFromFiles(unitTypeId,filePath,"wav")
    else
        loop
            exitwhen i>=SOUND_PER_STACK
            if i==0 then
                if AddUnitSoundSetDeath(unitTypeId, CreateSound3DFromFile(filePath + "Death." + extension, "DefaultEAXON"),0)==null then
                    call AddUnitSoundSetDeath(unitTypeId, CreateSound3DFromFile(filePath + "Death1." + extension, "DefaultEAXON"),0)
                endif
                if AddUnitSoundSetWarCry(unitTypeId, CreateSoundFromFile(filePath + "WarCry." + extension, "DefaultEAXON"),0)==null then
                    call AddUnitSoundSetWarCry(unitTypeId, CreateSoundFromFile(filePath + "WarCry1." + extension, "DefaultEAXON"),0)
                endif
                if AddUnitSoundSetReady(unitTypeId, CreateSoundFromFile(filePath + "Ready." + extension, "DefaultEAXON"),0)==null then
                    call AddUnitSoundSetReady(unitTypeId, CreateSoundFromFile(filePath + "Ready1." + extension, "DefaultEAXON"),0)
                endif
            else
                call AddUnitSoundSetDeath(unitTypeId, CreateSoundFromFile(filePath + "Death"+I2S(i+1)+"." + extension, "DefaultEAXON"),i)
                call AddUnitSoundSetReady(unitTypeId, CreateSoundFromFile(filePath + "Ready"+I2S(i+1)+"." + extension, "HeroAcksEAX"),i)
                call AddUnitSoundSetWarCry(unitTypeId, CreateSoundFromFile(filePath + "WarCry"+I2S(i+1)+"." + extension, "HeroAcksEAX"),i)
            endif
            call AddUnitSoundSetWhat(unitTypeId, CreateSoundFromFile(filePath + "What"+I2S(i+1)+"." + extension, "HeroAcksEAX"),i)
            call AddUnitSoundSetYes(unitTypeId, CreateSoundFromFile(filePath + "Yes"+I2S(i+1)+"." + extension, "HeroAcksEAX"),i)
            call AddUnitSoundSetYesAttack(unitTypeId, CreateSoundFromFile(filePath + "YesAttack"+I2S(i+1)+"." + extension, "HeroAcksEAX"),i)
            call AddUnitSoundSetPissed(unitTypeId, CreateSoundFromFile(filePath + "Pissed"+I2S(i+1)+"." + extension, "HeroAcksEAX"),i)
            set i=i+1
        endloop
    endif
endfunction

function HasUnitShopSoundSet takes integer unitTypeId returns boolean
    return LoadBoolean(h, unitTypeId, KEY_HAS_SHOP_SOUNDSET)
endfunction

function AddUnitShopSoundSet takes integer unitTypeId returns nothing
    call SaveBoolean(h, unitTypeId, KEY_HAS_SHOP_SOUNDSET, true)
endfunction

function RemoveUnitShopSoundSet takes integer unitTypeId returns nothing
    call SaveBoolean(h, unitTypeId, KEY_HAS_SHOP_SOUNDSET, false)
endfunction

function GetUnitSoundSetAnimationType takes integer unitTypeId returns integer
    return LoadInteger(h, unitTypeId, KEY_PORTRAIT_ANIMATION_TYPE)
endfunction

function SetUnitSoundSetAnimationType takes integer unitTypeId, integer t returns nothing
    call SaveInteger(h, unitTypeId, KEY_PORTRAIT_ANIMATION_TYPE, t)
endfunction

function GetUnitSoundSetPortraitModel takes integer unitTypeId returns string
    return LoadStr(h, unitTypeId, KEY_PORTRAIT_MODEL)
endfunction

function SetUnitSoundSetPortraitModel takes integer unitTypeId, string model returns nothing
    call SaveStr(h, unitTypeId, KEY_PORTRAIT_MODEL, model)
endfunction

private function HasControl takes player whichPlayer, unit whichUnit returns boolean
    return GetOwningPlayer(whichUnit) == whichPlayer or GetPlayerAlliance(whichPlayer, GetOwningPlayer(whichUnit), ALLIANCE_SHARED_CONTROL)
endfunction

private function AlliesWithSharedControl takes player whichPlayer returns force
    local force whichForce = CreateForce()
    local integer i = 0
    loop
        exitwhen (i == bj_MAX_PLAYERS)
        if (whichPlayer == Player(i) or GetPlayerAlliance(whichPlayer, Player(i), ALLIANCE_SHARED_CONTROL)) then
            call ForceAddPlayer(whichForce, Player(i))
        endif
        set i = i + 1
    endloop
    return whichForce
endfunction

private function IsUnitSpeakingForPlayer takes unit whichUnit returns boolean
    local boolean sameUnit = playerSpeaker == whichUnit
    return sameUnit and playerSound != null and GetSoundIsPlaying(playerSound)
endfunction

private function UpdatePlayerSound takes sound whichSound, unit whichUnit returns boolean
    // only update if it is a different speaker or the current unit is not speaking
    if (playerSpeaker == whichUnit and IsUnitSpeakingForPlayer(playerSpeaker)) then
        return false
    endif
  
    set playerSound = whichSound
    set playerSpeaker = whichUnit
    call StartSound(whichSound)
  
    return true
endfunction

private function GetRandomSoundEx takes unit whichUnit, integer t0, integer t1 returns sound
    local integer soundsCounter = 0
    local sound array sounds
    local sound whichSound = null
    loop
        exitwhen t0>t1
        static if LIBRARY_UnitSoundSetsPerUnit then
                set whichSound = GetUnitSoundSetPerUnit(whichUnit, t0)
                if (whichSound == null) then
                    set whichSound = LoadSoundHandle(h, GetUnitTypeId(whichUnit), t0)
                endif
        else
                set whichSound = LoadSoundHandle(h, GetUnitTypeId(whichUnit), t0)
        endif
        if whichSound!=null and not GetSoundIsPlaying(whichSound) then
            set sounds[soundsCounter] = whichSound
            set soundsCounter = soundsCounter + 1
        endif
        set t0 = t0 + 1
    endloop
    if soundsCounter>0 then
        set whichSound=sounds[GetRandomInt(0, soundsCounter - 1)]
    endif
    loop
        set sounds[soundsCounter]=null
        exitwhen soundsCounter==0
        set soundsCounter=soundsCounter-1
    endloop
    return whichSound
endfunction

private function GetRandomSound takes unit whichUnit, integer t0, integer t1 returns sound
    local sound whichSound = null
    if (t0 != t1) then
        set whichSound = GetRandomSoundEx(whichUnit, t0, t1)
    else
        static if LIBRARY_UnitSoundSetsPerUnit then
                set whichSound = GetUnitSoundSetPerUnit(whichUnit, t0)
                if whichSound==null then
                    set whichSound = LoadSoundHandle(h, GetUnitTypeId(whichUnit), t0)
                endif  
        else
                set whichSound = LoadSoundHandle(h, GetUnitTypeId(whichUnit), t0)
        endif
    endif

    return whichSound
endfunction

private function UpdatePlayerSelect takes unit whichUnit returns nothing
    if (playerClickTarget == whichUnit) then
        set playerClickCounter = playerClickCounter + 1
    else
        set playerClickCounter = 1
    endif
  
    set playerClickTarget = whichUnit
endfunction

private function IsPlayerSelectionPissed takes unit whichUnit returns boolean
    return playerClickTarget == whichUnit and playerClickCounter > PISSED_COUNTER
endfunction

private function TimerFunctionEndAnimation takes nothing returns nothing
    local integer playerId = LoadInteger(h, GetHandleId(GetExpiredTimer()), 0)
    if (GetLocalPlayer() == Player(playerId)) then
        call BlzFrameSetSpriteAnimate(playerPortrait, 2, 0) // stand
        call BlzFrameSetVisible(playerPortrait, false)
    endif
endfunction

private function PortraitTalkAnimation takes integer unitTypeId, sound whichSound returns nothing
    /*
    -- birth = 0
    -- death = 1
    -- stand = 2
    -- morph = 3
    -- alternate = 4
    */
    call BlzFrameSetModel(playerPortrait, GetUnitSoundSetPortraitModel(unitTypeId), 0)
    call BlzFrameSetSpriteAnimate(playerPortrait, 3, 0) // morph
    call BlzFrameSetVisible(playerPortrait, true)
  
    call PauseTimer(playerAnimationTimer)
    call TimerStart(playerAnimationTimer, GetSoundDurationBJ(whichSound), false, function TimerFunctionEndAnimation)
    call SaveInteger(h, GetHandleId(playerAnimationTimer), 0, GetPlayerId(GetLocalPlayer()))
endfunction

private function PortraitAnimation takes unit whichUnit, sound whichSound returns nothing
    local integer unitTypeId = GetUnitTypeId(whichUnit)
    local integer t = GetUnitSoundSetAnimationType(unitTypeId)
    if (t == PORTRAIT_ANIMATION_TYPE_CINEMATIC) then
        // TODO Recognize GetAllyColorFilterState for the player color
        call SetCinematicScene(unitTypeId, GetPlayerColor(GetOwningPlayer(whichUnit)), null, null, GetSoundDurationBJ(whichSound), GetSoundDurationBJ(whichSound))
        //call BlzFrameSetVisible(BlzGetOriginFrame(ORIGIN_FRAME_PORTRAIT_HP_TEXT, 0), true)
        //call BlzFrameSetVisible(BlzGetOriginFrame(ORIGIN_FRAME_PORTRAIT_MANA_TEXT, 0), true)
    elseif (t == PORTRAIT_ANIMATION_TYPE_ANIMATION) then
        call PortraitTalkAnimation(unitTypeId, whichSound)
    endif
endfunction

private function IsSoundWhat takes integer t0 returns boolean
    return t0>=SOUND_WHAT[0] and t0<=SOUND_WHAT[SOUND_PER_STACK-1]
endfunction

// this should be safe to use with GetLocalPlayer()
private function PlayRandomSound takes unit whichUnit, integer t0, integer t1 returns sound
    local sound whichSound = GetRandomSound(whichUnit, t0, t1)
    if whichSound != null then
        if UpdatePlayerSound(whichSound, whichUnit) then
            if IsSoundWhat(t0) then
                call UpdatePlayerSelect(whichUnit)
            endif
            if (IsUnitSelected(whichUnit, GetLocalPlayer())) then
                call PortraitAnimation(whichUnit, whichSound)
            endif
        endif
    // update selected unit for pissed even if it has no sound
    elseif IsSoundWhat(t0) then
        call UpdatePlayerSelect(whichUnit)
    endif
    return whichSound
endfunction

private function PlayRandomSoundForForce takes force whichForce, unit whichUnit, integer t0, integer t1 returns nothing
    if (IsPlayerInForce(GetLocalPlayer(), whichForce)) then
        call PlayRandomSound(whichUnit, t0, t1)
    endif
endfunction

function PlayRandomSoundForAllies takes unit whichUnit, integer t0, integer t1 returns nothing
    local force allies = AlliesWithSharedControl(GetOwningPlayer(whichUnit))
    call ForceAddPlayer(allies,GetOwningPlayer(whichUnit))
    call PlayRandomSoundForForce(allies, whichUnit, t0, t1)
    call ForceClear(allies)
    call DestroyForce(allies)
    set allies = null
endfunction

private function StopUnitSound takes unit whichUnit returns nothing
    // the death sound will stop any currently played sound from the unit
    if (IsUnitSpeakingForPlayer(whichUnit)) then
        call StopSound(playerSound, false, false)
    endif
endfunction

private function PlayRandom3DDeathSound takes unit whichUnit returns nothing
    local sound soundHandle = GetRandomSound(whichUnit, SOUND_DEATH[0], SOUND_DEATH[SOUND_PER_STACK-1])
    if soundHandle!=null then
        call StartSound(soundHandle)
        call AttachSoundToUnit(soundHandle, whichUnit)
        set soundHandle = null
    endif
endfunction

public function PlayUnitYes takes unit u returns sound
    return PlayRandomSound(u,SOUND_YES[0],SOUND_YES[SOUND_PER_STACK-1])
endfunction
public function PlayUnitYesAttack takes unit u returns sound
    local sound whichSound=PlayRandomSound(u,SOUND_YES_ATTACK[0],SOUND_YES_ATTACK[SOUND_PER_STACK-1])
    if whichSound==null then
        return PlayUnitYes(u)
    endif
    return whichSound
endfunction
public function PlayUnitWhat takes unit u returns sound
    return PlayRandomSound(u,SOUND_WHAT[0],SOUND_WHAT[SOUND_PER_STACK-1])
endfunction
public function PlayUnitPissed takes unit u returns sound
    local sound whichSound=PlayRandomSound(u,SOUND_PISSED[0],SOUND_PISSED[SOUND_PER_STACK-1])
    if whichSound==null then
        return PlayUnitWhat(u)
    endif
    return whichSound
endfunction
public function PlayUnitReady takes unit u returns sound
    return PlayRandomSound(u,SOUND_READY[0],SOUND_READY[SOUND_PER_STACK-1])
endfunction
public function PlayUnitWarCry takes unit u returns sound
    local sound whichSound=PlayRandomSound(u,SOUND_WARCRY[0],SOUND_WARCRY[SOUND_PER_STACK-1])
    if whichSound==null then
        return PlayUnitYesAttack(u)
    endif
    return whichSound
endfunction

private function AfterSelect takes nothing returns nothing
    local timer t=GetExpiredTimer()
    local integer kT=GetHandleId(t)
    local unit triggerUnit=LoadUnitHandle(h,kT,0)
    local player triggerPlayer=LoadPlayerHandle(h,kT,1)

    if triggerPlayer==GetLocalPlayer() then
        set triggerUnit=GetMainSelectedUnitEx()
        if triggerUnit!=null and triggerUnit==GetMainSelectedUnitEx() and (HasControl(triggerPlayer,triggerUnit) or HasUnitShopSoundSet(GetUnitTypeId(triggerUnit))) then
            if IsPlayerSelectionPissed(triggerUnit) then
                call PlayUnitPissed(triggerUnit)
            else
                call PlayUnitWhat(triggerUnit)
            endif
        endif
    endif

    call FlushChildHashtable(h,kT)
    call RemoveSavedHandle(h,GetHandleId(triggerUnit),0)
    call PauseTimer(t)
    call DestroyTimer(t)

    set t=null
    set triggerUnit=null
    set triggerPlayer=null
endfunction

private function TriggerActionSelect takes nothing returns nothing
    local unit triggerUnit = GetTriggerUnit()
    local integer kU=GetHandleId(triggerUnit)
    local timer t=LoadTimerHandle(h,kU,0)
    if t==null then
        set t=CreateTimer()
        call SaveTimerHandle(h,kU,0,t)
        call SaveUnitHandle(h,GetHandleId(t),0,triggerUnit)
        call SavePlayerHandle(h,GetHandleId(t),1,GetTriggerPlayer())
        call TimerStart(t,0,false,function AfterSelect)
    endif
    set t=null
    set triggerUnit=null
endfunction

private function EndUnitTalkPortrait takes unit whichUnit returns nothing
    local integer unitTypeId = GetUnitTypeId(whichUnit)
    local integer t = GetUnitSoundSetAnimationType(unitTypeId)
    local integer playerId = GetPlayerId(GetLocalPlayer())
    if (HasUnitSoundSet(GetUnitTypeId(whichUnit)) and playerSpeaker == whichUnit) then
        if (t == PORTRAIT_ANIMATION_TYPE_CINEMATIC) then
            // Do not end talk animations for native sound sets.
            // TODO Deslecting a unit with custom sound set and selecting a unit with a native sound set seems to stop the talk animation because of this.
            call EndCinematicScene()
        elseif (t == PORTRAIT_ANIMATION_TYPE_ANIMATION) then
            call PauseTimer(playerAnimationTimer)
            call BlzFrameSetVisible(playerPortrait, false)
        endif
    endif
endfunction

private function TriggerConditionDeselect takes nothing returns boolean
    if (GetTriggerPlayer() == GetLocalPlayer()) then
        call EndUnitTalkPortrait(GetTriggerUnit())
    endif
    return false
endfunction

private function TriggerActionOrder takes nothing returns nothing
    local unit triggerUnit = GetTriggerUnit()
    local unit target=GetOrderTargetUnit()
    local integer orderId = 0
    if (HasControl(GetLocalPlayer(), triggerUnit) and GetMainSelectedUnitEx() == triggerUnit) then
        set orderId = GetIssuedOrderId()
        if orderId==ORDER_ID_SMART then
            if target!=null and IsUnitEnemy(triggerUnit,GetOwningPlayer(target)) then
                call PlayUnitYesAttack(triggerUnit)
            else
                call PlayUnitYes(triggerUnit)
            endif
        else
            if orderId == ORDER_ID_ATTACK then
                if IsUnitType(target,UNIT_TYPE_HERO) and GetRandomInt(0,5)==0 then
                    call PlayUnitWarCry(triggerUnit)
                else
                    call PlayUnitYesAttack(triggerUnit)
                endif
            elseif (orderId == ORDER_ID_MOVE or orderId == ORDER_ID_PATROL or orderId == ORDER_ID_ATTACK_GROUND) then
                call PlayUnitYes(triggerUnit)
            endif
        endif
    endif
    set triggerUnit=null
    set target=null
endfunction

private function TriggerConditionReviveFinish takes nothing returns boolean
    call PlayRandomSoundForAllies(GetRevivingUnit(), SOUND_READY[0], SOUND_READY[SOUND_PER_STACK-1])
    return false
endfunction

private function TriggerConditionTrainFinish takes nothing returns boolean
    call PlayRandomSoundForAllies(GetTrainedUnit(), SOUND_READY[0], SOUND_READY[SOUND_PER_STACK-1])
    return false
endfunction

private function TriggerConditionHire takes nothing returns boolean
    call PlayRandomSoundForAllies(GetSoldUnit(), SOUND_READY[0], SOUND_READY[SOUND_PER_STACK-1])
    return false
endfunction

private function TriggerConditionDeath takes nothing returns boolean
    call PlayRandom3DDeathSound(GetDyingUnit())
    return false
endfunction

private function TimerFunctionCreatePlayerPortraits takes nothing returns nothing
    set playerPortrait = BlzCreateFrameByType("SPRITE", "SpriteName", BlzGetOriginFrame(ORIGIN_FRAME_PORTRAIT, 0), "", 0)
    call BlzFrameSetAllPoints(playerPortrait, BlzGetOriginFrame(ORIGIN_FRAME_PORTRAIT, 0))
    call BlzFrameSetVisible(playerPortrait, false)
    call PauseTimer(GetExpiredTimer())
    call DestroyTimer(GetExpiredTimer())
endfunction

private function Init takes nothing returns nothing
    local integer i=0
    loop
        exitwhen i==bj_MAX_PLAYERS
        call TriggerRegisterPlayerUnitEvent(selectionTrigger, Player(i), EVENT_PLAYER_UNIT_SELECTED, null)
        call TriggerRegisterPlayerUnitEvent(deselectionTrigger, Player(i), EVENT_PLAYER_UNIT_DESELECTED, null)
        set i=i+1
    endloop
    call TriggerAddAction(selectionTrigger, function TriggerActionSelect)
    call TriggerAddCondition(deselectionTrigger, Condition(function TriggerConditionDeselect))

    call TriggerRegisterAnyUnitEventBJ(orderTrigger, EVENT_PLAYER_UNIT_ISSUED_ORDER)
    call TriggerRegisterAnyUnitEventBJ(orderTrigger, EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER)
    call TriggerRegisterAnyUnitEventBJ(orderTrigger, EVENT_PLAYER_UNIT_ISSUED_UNIT_ORDER)
    call TriggerRegisterAnyUnitEventBJ(orderTrigger, EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER)
    call TriggerAddAction(orderTrigger, function TriggerActionOrder)

    call TriggerRegisterAnyUnitEventBJ(revivalTrigger, EVENT_PLAYER_HERO_REVIVE_FINISH)
    call TriggerAddCondition(revivalTrigger, Condition(function TriggerConditionReviveFinish))

    call TriggerRegisterAnyUnitEventBJ(trainTrigger, EVENT_PLAYER_UNIT_TRAIN_FINISH)
    call TriggerAddCondition(trainTrigger, Condition(function TriggerConditionTrainFinish))
  
    call TriggerRegisterAnyUnitEventBJ(hireTrigger, EVENT_PLAYER_UNIT_SELL)
    call TriggerAddCondition(hireTrigger, Condition(function TriggerConditionHire))

    call TriggerRegisterAnyUnitEventBJ(deathTrigger, EVENT_PLAYER_UNIT_DEATH)
    call TriggerAddCondition(deathTrigger, Condition(function TriggerConditionDeath))

    call TimerStart(CreateTimer(), 0.0, false, function TimerFunctionCreatePlayerPortraits)
  
    set i=0
    loop
        exitwhen i>=SOUND_PER_STACK
        set SOUND_DEATH[i]        =i
        set SOUND_WHAT[i]            =SOUND_PER_STACK+i
        set SOUND_YES[i]            =SOUND_PER_STACK*2+i
        set SOUND_YES_ATTACK[i]    =SOUND_PER_STACK*3+i
        set SOUND_READY[i]        =SOUND_PER_STACK*4+i
        set SOUND_WARCRY[i]        =SOUND_PER_STACK*5+i
        set SOUND_PISSED[i]        =SOUND_PER_STACK*6+i
        set i=i+1
    endloop
endfunction

endlibrary

[EDIT] While I am at it :
The system doesn't work for localized sounds
Why do you bother replacing the portrait with a talking one when you can artificially display the HP and mana when the cinematic mode is on ? This would remove the need of having custom models for everything !
To simplify things I will store in the hash by an integer index the soundsets, at -1 the path and at positive all the sounds. This would make duplicates easier to check.
 
Last edited:
Allright, I ground this for the past 48h just to fix some problems... and I am not sure if I provoked no more.
☼ I kicked out all portrait things for me to be easier to work with
☼ A unit will no longer say the same line twice in a row, except if it has only one occurrence of this stack ; this was made possible by storing for each sound their SoundSet ID and index, plus the latest played sound in the SoundSet ID key.
☼ Reworked the soundset attribution : you create them by ID and you assign their ID to a unit
► You can assign a unit or unittype soundset per path, and it will automatically load if needed once
► You can remove a unit or unittype soundset by giving them the ID 0 (all custom soundsets starts at 1 for this reason)
► All sound indexes are shifted by 1 so a 0 index correspond to an absent sound (previously the index 0 was used by the Death/Death1)
☼ To know if a sound is played for a player, instead of using GetLocalPlayer() I'm using timers having the same duration of the played sound.
☼ I don't know if using async hashtable read/write could break the sync, this have to be tested ; if it breaks then I might have done all of this for nothing.
JASS:
library UnitSoundSets initializer Init requires GetMainSelectedUnit, optional UnitSoundSetsPerUnit

/**
 * Baradé's Unit Sound Sets 1.0
 *
 * Allows using custom unit sound sets without replacing existing ones or using a custom SLK file.
 *
 * Usage:
 * - Disable the sound set of your unit type and then register the sounds using this system.
 * - Import custom soundset files into your map.
 * - Call this JASS code during the map initialization:
 *   call AddUnitSoundSetFromFiles('H0F2', "war3Imported\\HeroPaladin", "flac")
 *
 *   'H0F2' should be the raw code of your custom hero unit type.
 *   "war3Imported\\HeroPaladin" should be the prefix for all unit sound set files.
 *   "flac" should be the file extension for all unit sound set files.
 *
 * - Optional: If you want to have the talk animation in the portrait like in Warcraft III cinematics,
 * use the following JASS code to configure it:
 *   call SetUnitSoundSetAnimationType('H0F2', PORTRAIT_ANIMATION_TYPE_CINEMATIC)
 *
 * - Optional: If you want to have the talk animation in the portrait with a prepared portrait model,
 * use the following JASS code to configure it:
 *   call SetUnitSoundSetAnimationType('H0F2', PORTRAIT_ANIMATION_TYPE_ANIMATION)
 *   call SetUnitSoundSetPortraitModel('H0F2', "war3mapImported\\EasterBunny_Portrait.mdx")
 * Modify the portrait model and rename "Portrait Talk" into "morph" and the other animations into "stand".
 *
 * - Optional: If you have shop buildings which should play custom sounds when selected by other
 * players, use the following JASS code to register them:
 *   call AddUnitShopSoundSet('ngme')
 *
 *   Replace the raw code with the one of your shop building type.
 *
 * The following suffixes for sound files are supported:
 * Death
 * What
 * Yes
 * YesAttack
 * Ready
 * WarCry
 * Pissed
 *
 * Disable cinematic sub titles with ForceCinematicSubtitles or in the game settings.
 * Selecting the same unit again will not play the next sound immediately but wait for the current sound
 * to be finished. Selecting another unit will immediately play the sound of the other unit. This seems to be 
 * Warcraft's behavior.
 */

globals
    // You will hear the pissed sounds after this number of clicks on the same unit.
    constant integer PISSED_COUNTER = 3
    
    // Shows no talk animation in the portrait (default).
    constant integer PORTRAIT_ANIMATION_TYPE_NONE = 0
    // Uses custom portrait frames without hidden HP and mana.
    constant integer PORTRAIT_ANIMATION_TYPE_ANIMATION = 1
    // Shows talk animations during the sounds as long as the unit is selected if this value is true.
    constant integer PORTRAIT_ANIMATION_TYPE_CINEMATIC = 2

    // Death : Whenver the unit dies, the Death Sound will be played.
    constant integer array SOUND_DEATH
    // What : Occurs when you click on the unit only. Replaced by Pissed Sounds if you click multiple times on the unit in a short time.
    constant integer array SOUND_WHAT
    // Yes : Occurs when you order the unit to do a "Friendly" Action.
    constant integer array SOUND_YES
    // YesAttack : Occurs when you order the unit to "Attack-Move", or an order similar to it.
    constant integer array SOUND_YES_ATTACK
    // Ready : Occurs only one time for a single unit, it is played when the unit is spawned by a building (Example : Barracks for Rifleman), or when they got revived by an Altar (Example : Paladin).
    constant integer array SOUND_READY
    // Warcry : Occurs when you order the unit to attack a specific unit.
    constant integer array SOUND_WARCRY
    // Pissed : When you click a lot of times on the unit, weird sounds will be played instead of normals, which are called Pissed Sounds.
    constant integer array SOUND_PISSED
    // Attack : Occurs when the unit starts attacking an enemy. (You don't have to click on the unit)
    
    constant integer KEY_HAS_CUSTOM_SOUNDSET = 39
    constant integer KEY_HAS_SHOP_SOUNDSET = 40
    constant integer KEY_PORTRAIT_ANIMATION_TYPE = 41
    constant integer KEY_PORTRAIT_MODEL = 42

    private constant integer ORDER_ID_ATTACK = 851983
    private constant integer ORDER_ID_ATTACK_GROUND = 851984
    private constant integer ORDER_ID_ATTACK_ONCE = 851985
    private constant integer ORDER_ID_MOVE = 851986
    private constant integer ORDER_ID_PATROL = 851990
    private constant integer ORDER_ID_SMART = 851971
    private constant integer SOUND_PER_STACK=9
    
    private hashtable h = InitHashtable()
    private integer soundSetNb=0
    
    // all this exists for the local player only and is never synchronized between players
    private integer playerClickCounter = 0
    private unit playerClickTarget = null
    private sound playerSound = null
    private unit playerSpeaker = null

    private trigger selectionTrigger = CreateTrigger()
    private trigger deselectionTrigger = CreateTrigger()
    private trigger orderTrigger = CreateTrigger()
    private trigger revivalTrigger = CreateTrigger()
    private trigger trainTrigger = CreateTrigger()
    private trigger hireTrigger = CreateTrigger()
    private trigger deathTrigger = CreateTrigger()
endglobals

function RemoveAllUnitSoundSets takes nothing returns nothing
    call FlushParentHashtable(h)
endfunction

function RemoveUnitSoundSet takes integer unitTypeId returns nothing
    call FlushChildHashtable(h, unitTypeId)
endfunction

function AddUnitSoundSet takes integer unitTypeId, integer t, sound whichSound returns sound
    if (whichSound != null) then
        call SaveSoundHandle(h, unitTypeId, t, whichSound)
        call SaveBoolean(h, unitTypeId, KEY_HAS_CUSTOM_SOUNDSET, true)
    endif
    return whichSound
endfunction

function HasUnitSoundSet takes integer unitTypeId returns boolean
    return LoadBoolean(h, unitTypeId, KEY_HAS_CUSTOM_SOUNDSET)
endfunction

function SetUnitSoundSetById takes unit u, integer soundSetId returns nothing
    call SaveInteger(h,GetHandleId(u),0,soundSetId)
endfunction

function GetSoundSetPath takes integer soundSetId returns string
    return LoadStr(h,soundSetId,-1)
endfunction

// Returns 0 if not found
function GetSoundSetId takes string path returns integer
    local integer i=1
    loop
        exitwhen i>soundSetNb
        if GetSoundSetPath(i)==path then
            return i
        endif
        set i=i+1
    endloop
    return 0 // not found
endfunction

function SetUnitSoundSet takes unit u, string path returns nothing
    call SetUnitSoundSetById(u,GetSoundSetId(path))
endfunction

function GetSoundSoundSet takes sound s returns integer
    return LoadInteger(h,GetHandleId(s),1)
endfunction

function GetSoundIndex takes sound s returns integer
    return LoadInteger(h,GetHandleId(s),2)
endfunction

function AddSoundSetSound takes integer soundSetId, integer index, sound s returns sound
    local integer sK=GetHandleId(s)
    call SaveSoundHandle(h,soundSetId,index,s)
    call SaveInteger(h,sK,1,soundSetId)
    call SaveInteger(h,sK,2,index)
    return s
endfunction

function AddSoundSetDeath takes integer soundSetId, sound whichSound, integer index returns sound
    return AddSoundSetSound(soundSetId,SOUND_DEATH[index],whichSound)
endfunction

function AddSoundSetWhat takes integer soundSetId, sound whichSound, integer index returns sound
    return AddSoundSetSound(soundSetId,SOUND_WHAT[index],whichSound)
endfunction

function AddSoundSetYes takes integer soundSetId, sound whichSound, integer index returns sound
    return AddSoundSetSound(soundSetId,SOUND_YES[index], whichSound)
endfunction

function AddSoundSetYesAttack takes integer soundSetId, sound whichSound, integer index returns sound
    return AddSoundSetSound(soundSetId,SOUND_YES_ATTACK[index],whichSound)
endfunction

function AddSoundSetReady takes integer soundSetId, sound whichSound, integer index returns sound
    return AddSoundSetSound(soundSetId,SOUND_READY[index],whichSound)
endfunction

function AddSoundSetWarCry takes integer soundSetId, sound whichSound, integer index returns sound
    return AddSoundSetSound(soundSetId,SOUND_WARCRY[index],whichSound)
endfunction

function AddSoundSetPissed takes integer soundSetId, sound whichSound, integer index returns sound
    return AddSoundSetSound(soundSetId,SOUND_PISSED[index],whichSound)
endfunction

private function CreateSoundFromFile takes string filePath, string eaxSetting returns sound
    local sound s = null
    local integer duration = GetSoundFileDuration(filePath)
    if duration>0 then
        set s = CreateSound(filePath, false, false, true, 12700, 12700, eaxSetting)
        call SetSoundDuration(s, duration)
    endif
    return s
endfunction

private function CreateSound3DFromFile takes string filePath, string eaxSetting returns sound
    local sound soundHandle = null
    local integer duration = GetSoundFileDuration(filePath)
    if (duration > 0) then
        set soundHandle = CreateSound(filePath, false, true, true, 12700, 12700, eaxSetting)
        call SetSoundDuration(soundHandle, duration)
    endif
    return soundHandle
endfunction

function LoadSoundSetSub takes integer soundSetId, string extension returns nothing
    local integer i=0
    local string path=GetSoundSetPath(soundSetId)
    if extension=="auto" then
        call LoadSoundSetSub(soundSetId,"mp3")
        call LoadSoundSetSub(soundSetId,"flac")
        call LoadSoundSetSub(soundSetId,"wav")
    else
        loop
            exitwhen i>=SOUND_PER_STACK
            if i==0 then
                if AddSoundSetDeath(soundSetId, CreateSound3DFromFile(path + "Death." + extension, "DefaultEAXON"),0)==null then
                    call AddSoundSetDeath(soundSetId, CreateSound3DFromFile(path + "Death1." + extension, "DefaultEAXON"),0)
                endif
                if AddSoundSetWarCry(soundSetId, CreateSoundFromFile(path + "WarCry." + extension, "DefaultEAXON"),0)==null then
                    call AddSoundSetWarCry(soundSetId, CreateSoundFromFile(path + "WarCry1." + extension, "DefaultEAXON"),0)
                endif
                if AddSoundSetReady(soundSetId, CreateSoundFromFile(path + "Ready." + extension, "DefaultEAXON"),0)==null then
                    call AddSoundSetReady(soundSetId, CreateSoundFromFile(path + "Ready1." + extension, "DefaultEAXON"),0)
                endif
            else
                call AddSoundSetDeath(soundSetId, CreateSoundFromFile(path + "Death"+I2S(i+1)+"." + extension, "DefaultEAXON"),i)
                call AddSoundSetReady(soundSetId, CreateSoundFromFile(path + "Ready"+I2S(i+1)+"." + extension, "HeroAcksEAX"),i)
                call AddSoundSetWarCry(soundSetId, CreateSoundFromFile(path + "WarCry"+I2S(i+1)+"." + extension, "HeroAcksEAX"),i)
            endif
            call AddSoundSetWhat(soundSetId, CreateSoundFromFile(path + "What"+I2S(i+1)+"." + extension, "HeroAcksEAX"),i)
            call AddSoundSetYes(soundSetId, CreateSoundFromFile(path + "Yes"+I2S(i+1)+"." + extension, "HeroAcksEAX"),i)
            call AddSoundSetYesAttack(soundSetId, CreateSoundFromFile(path + "YesAttack"+I2S(i+1)+"." + extension, "HeroAcksEAX"),i)
            call AddSoundSetPissed(soundSetId, CreateSoundFromFile(path + "Pissed"+I2S(i+1)+"." + extension, "HeroAcksEAX"),i)
            set i=i+1
        endloop
    endif
endfunction

function IsSoundSetLoaded takes integer soundSetId returns boolean
    return LoadBoolean(h,soundSetId,-2)
endfunction

function LoadSoundSet takes integer soundSetId returns integer
    if soundSetId>0 and soundSetId<=soundSetNb and not IsSoundSetLoaded(soundSetId) then
        call SaveBoolean(h,soundSetId,-2,true)
        call LoadSoundSetSub(soundSetId,"auto")
    endif
    return soundSetId
endfunction

function CreateSoundSet takes string path returns integer
    local integer soundSetId=GetSoundSetId(path)
    if soundSetId==0 then
        set soundSetNb=soundSetNb+1
        call SaveStr(h,soundSetNb,-1,path)
        set soundSetId=soundSetNb
    endif
    return soundSetId
endfunction

function GenerateSoundSet takes string path returns integer
    return LoadSoundSet(CreateSoundSet(path))
endfunction

// Setting 0 for the unit will remove it
function SetUnitTypeSoundSetById takes integer uT, integer soundSetId returns nothing
    call SaveInteger(h,uT,0,soundSetId)
endfunction

function SetUnitTypeSoundSet takes integer uT, string path returns nothing
    call GenerateSoundSet(path)
    call SetUnitTypeSoundSetById(uT,GetSoundSetId(path))
endfunction

function HasUnitShopSoundSet takes integer unitTypeId returns boolean
    return LoadBoolean(h, unitTypeId, KEY_HAS_SHOP_SOUNDSET)
endfunction

function AddUnitShopSoundSet takes integer unitTypeId returns nothing
    call SaveBoolean(h, unitTypeId, KEY_HAS_SHOP_SOUNDSET, true)
endfunction

function RemoveUnitShopSoundSet takes integer unitTypeId returns nothing
    call SaveBoolean(h, unitTypeId, KEY_HAS_SHOP_SOUNDSET, false)
endfunction

private function HasControl takes player whichPlayer, unit whichUnit returns boolean
    return GetOwningPlayer(whichUnit) == whichPlayer or GetPlayerAlliance(whichPlayer, GetOwningPlayer(whichUnit), ALLIANCE_SHARED_CONTROL)
endfunction

private function AlliesWithSharedControl takes player whichPlayer returns force
    local force whichForce = CreateForce()
    local integer i = 0
    loop
        exitwhen (i == bj_MAX_PLAYERS)
        if (whichPlayer == Player(i) or GetPlayerAlliance(whichPlayer, Player(i), ALLIANCE_SHARED_CONTROL)) then
            call ForceAddPlayer(whichForce, Player(i))
        endif
        set i = i + 1
    endloop
    return whichForce
endfunction

private function IsUnitSpeakingForPlayer takes unit whichUnit returns boolean
    local boolean sameUnit = playerSpeaker == whichUnit
    return sameUnit and playerSound != null and GetSoundIsPlaying(playerSound)
endfunction

function GetPlayerSoundTimer takes sound s, player p returns timer
    return LoadTimerHandle(h,GetHandleId(p),GetHandleId(s))
endfunction

function IsSoundPlayedForPlayer takes sound s, player p returns boolean
    return GetPlayerSoundTimer(s,p)!=null
endfunction

function GetPlayerLatestSound takes player p returns sound
    return LoadSoundHandle(h,GetHandleId(p),0)
endfunction

function SoundExpire takes nothing returns nothing
    local timer t=GetExpiredTimer()
    local integer kT=GetHandleId(t)
    local player p=LoadPlayerHandle(h,kT,0)
    local integer kP=GetHandleId(p)
    if p==GetLocalPlayer() then
        //call RemoveSavedHandle(h,kT,GetHandleId(LoadSoundHandle(h,kT,1)))
        //call RemoveSavedHandle(h,kP,0)
    endif
    call PauseTimer(t)
    call FlushChildHashtable(h,kT)
    call DestroyTimer(t)
    set t=null
endfunction

function PlaySoundForPlayer takes player p, sound s returns boolean
    local timer t
    local integer kT
    if IsSoundPlayedForPlayer(s,p) or s==null then
        return false
    endif
    if GetLocalPlayer()==p then
        call StartSound(s)
        set t=CreateTimer()
        set kT=GetHandleId(t)
        call SavePlayerHandle(h,kT,0,p)
        call SaveSoundHandle(h,kT,1,s)
        call SaveTimerHandle(h,GetHandleId(p),GetHandleId(s),t)
        // Duration un milliseconds
        call TimerStart(t,GetSoundDuration(s)*0.001,false,function SoundExpire)
        call SaveSoundHandle(h,GetHandleId(p),0,s)
        call SaveInteger(h,GetSoundSoundSet(s),-3,GetSoundIndex(s))
    endif
    set t=null
    return true
endfunction

private function UpdatePlayerSound takes sound whichSound, unit whichUnit returns boolean
    // only update if it is a different speaker or the current unit is not speaking
    if (playerSpeaker == whichUnit and IsUnitSpeakingForPlayer(playerSpeaker)) then
        return false
    endif
    
    set playerSound = whichSound
    set playerSpeaker = whichUnit
    call PlaySoundForPlayer(GetLocalPlayer(),whichSound)
    
    return true
endfunction

function GetUnitSoundSetId takes unit u returns integer
    local integer soundSetId=LoadInteger(h,GetHandleId(u),0)
    if soundSetId==0 then
        set soundSetId=LoadInteger(h,GetUnitTypeId(u),0)
    endif
    return soundSetId
endfunction

function GetSound takes integer soundSetId, integer index returns sound
    return LoadSoundHandle(h,soundSetId,index)
endfunction

private function GetSoundSetLastPlayed takes integer soundSetId returns integer
    return LoadInteger(h,soundSetId,-3)
endfunction

private function SetSoundSetLastPlayed takes integer soundSetId, integer index returns nothing
    call SaveInteger(h,soundSetId,-3,index)
endfunction

private function GetRandomSound takes unit u, integer index, player p returns sound
    local sound whichSound=null
    local integer soundSetId=GetUnitSoundSetId(u)
    local sound array sounds
    local integer i=0
    local integer j=0
    if soundSetId!=0 then
        loop
            exitwhen i>=SOUND_PER_STACK
            if (p==null or not IsSoundPlayedForPlayer(whichSound,p)) and index+i!=GetSoundSetLastPlayed(soundSetId) then
                set sounds[j]=GetSound(soundSetId,index+i)
            endif
            if sounds[j]!=null then
                set j=j+1
            endif
            set i=i+1
        endloop
        if j>1 then
            set whichSound=sounds[GetRandomInt(0,j-1)]
        endif
        loop
            exitwhen j<0
            set sounds[j]=null
            set j=j-1
        endloop
        // if not found, we do it again with one less condition
        if whichSound==null then
            set j=0
            loop
                exitwhen i>=SOUND_PER_STACK
                if p==null or not IsSoundPlayedForPlayer(whichSound,p) then
                    set sounds[j]=GetSound(soundSetId,index+i)
                endif
                if sounds[j]!=null then
                    set j=j+1
                endif
                set i=i+1
            endloop
            if j>1 then
                set whichSound=sounds[GetRandomInt(0,j-1)]
            endif
            loop
                exitwhen j<0
                set sounds[j]=null
                set j=j-1
            endloop
        endif
    endif
    return whichSound
endfunction

private function UpdatePlayerSelect takes unit whichUnit returns nothing
    if (playerClickTarget == whichUnit) then
        set playerClickCounter = playerClickCounter + 1
    else
        set playerClickCounter = 1
    endif
    
    set playerClickTarget = whichUnit
endfunction

private function IsPlayerSelectionPissed takes unit whichUnit returns boolean
    return playerClickTarget == whichUnit and playerClickCounter > PISSED_COUNTER
endfunction

private function IsSoundWhat takes integer t0 returns boolean
    return t0>=SOUND_WHAT[0] and t0<=SOUND_WHAT[SOUND_PER_STACK-1]
endfunction

// this should be safe to use with GetLocalPlayer()
private function PlayRandomSound takes unit u, integer index, player p returns sound
    local sound s=null
    if GetLocalPlayer()==p then
        set s=GetRandomSound(u,index,p)
        if s!=null then
            if UpdatePlayerSound(s,u) then
                call SetSoundSetLastPlayed(GetUnitSoundSetId(u),GetSoundIndex(s))
                if IsSoundWhat(index) then
                    call UpdatePlayerSelect(u)
                endif
            endif
        // update selected unit for pissed even if it has no sound
        elseif IsSoundWhat(index) then
            call UpdatePlayerSelect(u)
        endif
    endif
    return s
endfunction

function PlayRandomSoundForAllies takes unit u, integer index returns nothing
    local force allies = AlliesWithSharedControl(GetOwningPlayer(u))
    local player p=GetOwningPlayer(u)
    local integer i=0
    call ForceAddPlayer(allies,p)
    loop
        exitwhen i==24
        if IsPlayerInForce(Player(i),allies) then
            call PlayRandomSound(u,index,Player(i))
        endif
        set i=i+1
    endloop
    call ForceClear(allies)
    call DestroyForce(allies)
    set allies=null
    set p=null
endfunction

private function StopUnitSound takes unit whichUnit returns nothing
    // the death sound will stop any currently played sound from the unit
    if (IsUnitSpeakingForPlayer(whichUnit)) then
        call StopSound(playerSound, false, false)
    endif
endfunction

private function PlayRandom3DDeathSound takes unit whichUnit returns nothing
    local sound soundHandle = GetRandomSound(whichUnit, SOUND_DEATH[0],null)
    if soundHandle!=null then
        call StartSound(soundHandle)
        call AttachSoundToUnit(soundHandle, whichUnit)
        set soundHandle = null
    endif
endfunction

public function PlayUnitYes takes unit u, player p returns sound
    return PlayRandomSound(u,SOUND_YES[0],p)
endfunction
public function PlayUnitYesAttack takes unit u, player p returns sound
    local sound whichSound=PlayRandomSound(u,SOUND_YES_ATTACK[0],p)
    if whichSound==null then
        return PlayUnitYes(u,p)
    endif
    return whichSound
endfunction
public function PlayUnitWhat takes unit u, player p returns sound
    return PlayRandomSound(u,SOUND_WHAT[0],p)
endfunction
public function PlayUnitPissed takes unit u, player p returns sound
    local sound whichSound=PlayRandomSound(u,SOUND_PISSED[0],p)
    if whichSound==null then
        return PlayUnitWhat(u,p)
    endif
    return whichSound
endfunction
public function PlayUnitReady takes unit u returns nothing
    call PlayRandomSoundForAllies(u,SOUND_READY[0])
endfunction
public function PlayUnitWarCry takes unit u, player p returns sound
    local sound whichSound=PlayRandomSound(u,SOUND_WARCRY[0],p)
    if whichSound==null then
        return PlayUnitYesAttack(u,p)
    endif
    return whichSound
endfunction

private function AfterSelect takes nothing returns nothing
    local timer t=GetExpiredTimer()
    local integer kT=GetHandleId(t)
    local unit u=LoadUnitHandle(h,kT,0)
    local player p=LoadPlayerHandle(h,kT,1)

    if p==GetLocalPlayer() then
        set u=GetMainSelectedUnitEx()
        if u!=null and u==GetMainSelectedUnitEx() and (HasControl(p,u) or HasUnitShopSoundSet(GetUnitTypeId(u))) then
            if IsPlayerSelectionPissed(u) then
                call PlayUnitPissed(u,p)
            else
                call PlayUnitWhat(u,p)
            endif
        endif
    endif

    call FlushChildHashtable(h,kT)
    call RemoveSavedHandle(h,GetHandleId(u),0)
    call PauseTimer(t)
    call DestroyTimer(t)

    set t=null
    set u=null
    set p=null
endfunction

private function TriggerActionSelect takes nothing returns nothing
    local unit triggerUnit = GetTriggerUnit()
    local integer kU=GetHandleId(triggerUnit)
    local timer t=LoadTimerHandle(h,kU,0)
    if t==null then
        set t=CreateTimer()
        call SaveTimerHandle(h,kU,0,t)
        call SaveUnitHandle(h,GetHandleId(t),0,triggerUnit)
        call SavePlayerHandle(h,GetHandleId(t),1,GetTriggerPlayer())
        call TimerStart(t,0,false,function AfterSelect)
    endif
    set t=null
    set triggerUnit=null
endfunction

private function TriggerActionOrder takes nothing returns nothing
    local unit u = GetTriggerUnit()
    local unit target=GetOrderTargetUnit()
    local integer orderId = 0
    if (HasControl(GetLocalPlayer(), u) and GetMainSelectedUnitEx() == u) then
        set orderId = GetIssuedOrderId()
        if orderId==ORDER_ID_SMART then
            if target!=null and IsUnitEnemy(u,GetOwningPlayer(target)) then
                call PlayUnitYesAttack(u,GetLocalPlayer())
            else
                call PlayUnitYes(u,GetLocalPlayer())
            endif
        else
            if orderId == ORDER_ID_ATTACK then
                if IsUnitType(target,UNIT_TYPE_HERO) and GetRandomInt(0,5)==0 then
                    call PlayUnitWarCry(u,GetLocalPlayer())
                else
                    call PlayUnitYesAttack(u,GetLocalPlayer())
                endif
            elseif (orderId == ORDER_ID_MOVE or orderId == ORDER_ID_PATROL or orderId == ORDER_ID_ATTACK_GROUND) then
                call PlayUnitYes(u,GetLocalPlayer())
            endif
        endif
    endif
    set u=null
    set target=null
endfunction

private function TriggerConditionReviveFinish takes nothing returns boolean
    call PlayRandomSoundForAllies(GetRevivingUnit(), SOUND_READY[0])
    return false
endfunction

private function TriggerConditionTrainFinish takes nothing returns boolean
    call PlayRandomSoundForAllies(GetTrainedUnit(), SOUND_READY[0])
    return false
endfunction

private function TriggerConditionHire takes nothing returns boolean
    call PlayRandomSoundForAllies(GetSoldUnit(), SOUND_READY[0])
    return false
endfunction

private function TriggerConditionDeath takes nothing returns boolean
    call PlayRandom3DDeathSound(GetDyingUnit())
    return false
endfunction

function SoundSetVolumeById takes integer soundSetId, integer volume returns nothing
    local integer i=0
    loop
        exitwhen i>SOUND_PISSED[SOUND_PER_STACK-1]
        call SetSoundVolume(LoadSoundHandle(h,soundSetId,i),volume)
        set i=i+1
    endloop
endfunction

function SoundSetVolume takes string path, integer volume returns nothing
    local integer soundSetId=GetSoundSetId(path)
    if soundSetId>0 then
        call SoundSetVolumeById(soundSetId,volume)
    endif
endfunction

private function Init takes nothing returns nothing
    local integer i=0
    loop
        exitwhen i==bj_MAX_PLAYERS
        call TriggerRegisterPlayerUnitEvent(selectionTrigger, Player(i), EVENT_PLAYER_UNIT_SELECTED, null)
        call TriggerRegisterPlayerUnitEvent(deselectionTrigger, Player(i), EVENT_PLAYER_UNIT_DESELECTED, null)
        set i=i+1
    endloop
    call TriggerAddAction(selectionTrigger, function TriggerActionSelect)

    call TriggerRegisterAnyUnitEventBJ(orderTrigger, EVENT_PLAYER_UNIT_ISSUED_ORDER)
    call TriggerRegisterAnyUnitEventBJ(orderTrigger, EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER)
    call TriggerRegisterAnyUnitEventBJ(orderTrigger, EVENT_PLAYER_UNIT_ISSUED_UNIT_ORDER)
    call TriggerRegisterAnyUnitEventBJ(orderTrigger, EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER)
    call TriggerAddAction(orderTrigger, function TriggerActionOrder)

    call TriggerRegisterAnyUnitEventBJ(revivalTrigger, EVENT_PLAYER_HERO_REVIVE_FINISH)
    call TriggerAddCondition(revivalTrigger, Condition(function TriggerConditionReviveFinish))

    call TriggerRegisterAnyUnitEventBJ(trainTrigger, EVENT_PLAYER_UNIT_TRAIN_FINISH)
    call TriggerAddCondition(trainTrigger, Condition(function TriggerConditionTrainFinish))
    
    call TriggerRegisterAnyUnitEventBJ(hireTrigger, EVENT_PLAYER_UNIT_SELL)
    call TriggerAddCondition(hireTrigger, Condition(function TriggerConditionHire))

    call TriggerRegisterAnyUnitEventBJ(deathTrigger, EVENT_PLAYER_UNIT_DEATH)
    call TriggerAddCondition(deathTrigger, Condition(function TriggerConditionDeath))
    
    set i=0
    loop
        exitwhen i>=SOUND_PER_STACK
        // +1 here because a 0 index corresponds to no previously played sound
        set SOUND_DEATH[i]        =i+1
        set SOUND_WHAT[i]            =SOUND_PER_STACK+i+1
        set SOUND_YES[i]            =SOUND_PER_STACK*2+i+1
        set SOUND_YES_ATTACK[i]    =SOUND_PER_STACK*3+i+1
        set SOUND_READY[i]        =SOUND_PER_STACK*4+i+1
        set SOUND_WARCRY[i]        =SOUND_PER_STACK*5+i+1
        set SOUND_PISSED[i]        =SOUND_PER_STACK*6+i+1
        set i=i+1
    endloop
endfunction

endlibrary
 

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,888
It's not very clear what "t" is as a function parameter, for example in AddUnitSoundSet:
JASS:
function AddUnitSoundSet takes integer unitTypeId, integer t, sound whichSound returns nothing
There's a reference leak on the returning force:
JASS:
private function AlliesWithSharedControl takes player whichPlayer returns force
    local force whichForce = CreateForce()
    local integer i = 0
    loop
        exitwhen (i == bj_MAX_PLAYERS)
        if (whichPlayer == Player(i) or GetPlayerAlliance(whichPlayer, Player(i), ALLIANCE_SHARED_CONTROL)) then
            call ForceAddPlayer(whichForce, Player(i))
        endif
        set i = i + 1
    endloop
    return whichForce
endfunction
A global variable easily solves it.
Same issue arises here, but with sounds.
JASS:
private function GetRandomSoundEx takes unit whichUnit, integer t0, integer t1 returns sound
    local integer unitTypeId = GetUnitTypeId(whichUnit)
    local integer speakerUnitTypeId = GetUnitTypeId(playerSpeaker)
    local integer soundsCounter = 0
    local sound array sounds
    local sound whichSound = null
    loop
        exitwhen (t0 > t1)
static if (LIBRARY_UnitSoundSetsPerUnit) then
        set whichSound = GetUnitSoundSetPerUnit(whichUnit, t0)
        if (whichSound == null) then
            set whichSound = LoadSoundHandle(h, GetUnitTypeId(whichUnit), t0)
        endif
else
        set whichSound = LoadSoundHandle(h, GetUnitTypeId(whichUnit), t0)
endif
        // never play the same sound twice if there are multiple sounds
        if (whichSound != null and (unitTypeId != speakerUnitTypeId or whichSound != playerSound)) then
            set sounds[soundsCounter] = whichSound
            set soundsCounter = soundsCounter + 1
        endif
        set t0 = t0 + 1
    endloop
    if (soundsCounter > 0) then
        return sounds[GetRandomInt(0, soundsCounter - 1)]
    endif
  
    return null
endfunction
GetRandomSound is also similar.

I assume this function is incomplete, since it always gets the same sound:
JASS:
private function PlayRandom3DDeathSound takes unit whichUnit returns nothing
    local sound soundHandle = GetRandomSound(whichUnit, SOUND_DEATH_1, SOUND_DEATH_1)
    if (soundHandle != null) then
        call StartSound(soundHandle)
        call AttachSoundToUnit(soundHandle, whichUnit)
        set soundHandle = null
    endif
endfunction
The system is using local player in a very unsafe way, I tested for desyncs, and they did happen on the merchant shop and the captain unit, it is instant desync.
It almost behaves as the original Warcraft 3 unit soundset. The most prevalent difference being the unit sound set being played over and over on the same unit type.

Overall, this system is very innovating, with lots of potential, and a dream for a lot of people for so long. Despite this, it will remain for now as needing to be fixed mainly because of the desyncs.
 
Level 25
Joined
Feb 2, 2006
Messages
1,689
Made further improvements for fun. I only took care of the sound aspect of your library since manipulating portraits is still foreign to me. I don't know if I broke the rest so you may confirm yourself ! I also haven't changed the comments and description too.
Attacking a hero have 25% of chances to launch the WarCry
A missing soundstack will use another one :
WarCry -> YesAttack -> Yes
Pissed -> What
Factorized some functions for clarity
SOUND_PER_STACK to scale everything on one value, I put 9 to keep the same logic
Putting auto in AddUnitSoundSetFromFiles will load for the 3 extensions recursively
For the first occurence of Death, Warcry and Ready it will check for the unumbered version too, to be more forgiving about paths (for example looking for Ready instead of Ready1)
GetRandomSoundEx do not pick a sound which is already playing. This doesn't fix the whole problem but every polished bit counts.
Cleaned some minor reference leaks
Some functions return the played/selected sound to be easier to branch everything
Attack once order never happens from player perspective since it's trigger based. It is simply removed
Death is now properly a 3D sound, which can be played on a unit

Remaining fields of improvements :
Do not repeat the same sound twice in a row, even from one unittype from another
Detect sound duplicates to reduce the number of stored values and sound variables
Create a soundset data structure to centralise several unittypes with the same soundset
Slight pitch random variations on soundsets
Make unit answers 3D like in the base game (sound is lower when far from it)
Add options to specify the rarity of each sounds
Add optionnal sound categories, for example effort attack, low HP, healed, attack ground, repair, gather, build, spell, etc
JASS:
library UnitSoundSets initializer Init requires GetMainSelectedUnit, optional UnitSoundSetsPerUnit

/**
 * Baradé's Unit Sound Sets 1.0
 *
 * Allows using custom unit sound sets without replacing existing ones or using a custom SLK file.
 *
 * Usage:
 * - Disable the sound set of your unit type and then register the sounds using this system.
 * - Import custom soundset files into your map.
 * - Call this JASS code during the map initialization:
 *   call AddUnitSoundSetFromFiles('H0F2', "war3Imported\\HeroPaladin", "flac")
 *
 *   'H0F2' should be the raw code of your custom hero unit type.
 *   "war3Imported\\HeroPaladin" should be the prefix for all unit sound set files.
 *   "flac" should be the file extension for all unit sound set files.
 *
 * - Optional: If you want to have the talk animation in the portrait like in Warcraft III cinematics,
 * use the following JASS code to configure it:
 *   call SetUnitSoundSetAnimationType('H0F2', PORTRAIT_ANIMATION_TYPE_CINEMATIC)
 *
 * - Optional: If you want to have the talk animation in the portrait with a prepared portrait model,
 * use the following JASS code to configure it:
 *   call SetUnitSoundSetAnimationType('H0F2', PORTRAIT_ANIMATION_TYPE_ANIMATION)
 *   call SetUnitSoundSetPortraitModel('H0F2', "war3mapImported\\EasterBunny_Portrait.mdx")
 * Modify the portrait model and rename "Portrait Talk" into "morph" and the other animations into "stand".
 *
 * - Optional: If you have shop buildings which should play custom sounds when selected by other
 * players, use the following JASS code to register them:
 *   call AddUnitShopSoundSet('ngme')
 *
 *   Replace the raw code with the one of your shop building type.
 *
 * The following suffixes for sound files are supported:
 * Death1
 * What1
 * What2
 * What3
 * What4
 * What5
 * What6
 * What7
 * What8
 * What9
 * Yes1
 * Yes2
 * Yes3
 * Yes4
 * Yes5
 * Yes6
 * Yes7
 * Yes8
 * Yes9
 * YesAttack1
 * YesAttack2
 * YesAttack3
 * YesAttack4
 * YesAttack5
 * YesAttack6
 * YesAttack7
 * YesAttack8
 * YesAttack9
 * Ready1
 * WarCry1
 * Pissed1
 * Pissed2
 * Pissed3
 * Pissed4
 * Pissed5
 * Pissed6
 * Pissed7
 * Pissed8
 * Pissed9
 *
 * Disable cinematic sub titles with ForceCinematicSubtitles or in the game settings.
 * Selecting the same unit again will not play the next sound immediately but wait for the current sound
 * to be finished. Selecting another unit will immediately play the sound of the other unit. This seems to be
 * Warcraft's behavior.
 */

globals
    // You will hear the pissed sounds after this number of clicks on the same unit.
    constant integer PISSED_COUNTER = 3
 
    // Shows no talk animation in the portrait (default).
    constant integer PORTRAIT_ANIMATION_TYPE_NONE = 0
    // Uses custom portrait frames without hidden HP and mana.
    constant integer PORTRAIT_ANIMATION_TYPE_ANIMATION = 1
    // Shows talk animations during the sounds as long as the unit is selected if this value is true.
    constant integer PORTRAIT_ANIMATION_TYPE_CINEMATIC = 2

    // Death : Whenver the unit dies, the Death Sound will be played.
    constant integer array SOUND_DEATH
    // What : Occurs when you click on the unit only. Replaced by Pissed Sounds if you click multiple times on the unit in a short time.
    constant integer array SOUND_WHAT
    // Yes : Occurs when you order the unit to do a "Friendly" Action.
    constant integer array SOUND_YES
    // YesAttack : Occurs when you order the unit to "Attack-Move", or an order similar to it.
    constant integer array SOUND_YES_ATTACK
    // Ready : Occurs only one time for a single unit, it is played when the unit is spawned by a building (Example : Barracks for Rifleman), or when they got revived by an Altar (Example : Paladin).
    constant integer array SOUND_READY
    // Warcry : Occurs when you order the unit to attack a specific unit.
    constant integer array SOUND_WARCRY
    // Pissed : When you click a lot of times on the unit, weird sounds will be played instead of normals, which are called Pissed Sounds.
    constant integer array SOUND_PISSED
    // Attack : Occurs when the unit starts attacking an enemy. (You don't have to click on the unit)
 
    constant integer KEY_HAS_CUSTOM_SOUNDSET = 39
    constant integer KEY_HAS_SHOP_SOUNDSET = 40
    constant integer KEY_PORTRAIT_ANIMATION_TYPE = 41
    constant integer KEY_PORTRAIT_MODEL = 42

    private constant integer ORDER_ID_ATTACK = 851983
    private constant integer ORDER_ID_ATTACK_GROUND = 851984
    private constant integer ORDER_ID_ATTACK_ONCE = 851985
    private constant integer ORDER_ID_MOVE = 851986
    private constant integer ORDER_ID_PATROL = 851990
    private constant integer ORDER_ID_SMART = 851971
    private constant integer SOUND_PER_STACK=9
 
    private hashtable h = InitHashtable()
 
    // all this exists for the local player only and is never synchronized between players
    private integer playerClickCounter = 0
    private unit playerClickTarget = null
    private sound playerSound = null
    private unit playerSpeaker = null
    private timer playerAnimationTimer = CreateTimer()
    private framehandle playerPortrait = null

    private trigger selectionTrigger = CreateTrigger()
    private trigger deselectionTrigger = CreateTrigger()
    private trigger orderTrigger = CreateTrigger()
    private trigger revivalTrigger = CreateTrigger()
    private trigger trainTrigger = CreateTrigger()
    private trigger hireTrigger = CreateTrigger()
    private trigger deathTrigger = CreateTrigger()
endglobals

function RemoveAllUnitSoundSets takes nothing returns nothing
    call FlushParentHashtable(h)
endfunction

function RemoveUnitSoundSet takes integer unitTypeId returns nothing
    call FlushChildHashtable(h, unitTypeId)
endfunction

function AddUnitSoundSet takes integer unitTypeId, integer t, sound whichSound returns sound
    if (whichSound != null) then
        call SaveSoundHandle(h, unitTypeId, t, whichSound)
        call SaveBoolean(h, unitTypeId, KEY_HAS_CUSTOM_SOUNDSET, true)
    endif
    return whichSound
endfunction

function HasUnitSoundSet takes integer unitTypeId returns boolean
    return LoadBoolean(h, unitTypeId, KEY_HAS_CUSTOM_SOUNDSET)
endfunction

function AddUnitSoundSetDeath takes integer unitTypeId, sound whichSound, integer index returns sound
    return AddUnitSoundSet(unitTypeId,SOUND_DEATH[index],whichSound)
endfunction

function AddUnitSoundSetWhat takes integer unitTypeId, sound whichSound, integer index returns sound
    return AddUnitSoundSet(unitTypeId,SOUND_WHAT[index],whichSound)
endfunction

function AddUnitSoundSetYes takes integer unitTypeId, sound whichSound, integer index returns sound
    return AddUnitSoundSet(unitTypeId,SOUND_YES[index], whichSound)
endfunction

function AddUnitSoundSetYesAttack takes integer unitTypeId, sound whichSound, integer index returns sound
    return AddUnitSoundSet(unitTypeId,SOUND_YES_ATTACK[index],whichSound)
endfunction

function AddUnitSoundSetReady takes integer unitTypeId, sound whichSound, integer index returns sound
    return AddUnitSoundSet(unitTypeId,SOUND_READY[index],whichSound)
endfunction

function AddUnitSoundSetWarCry takes integer unitTypeId, sound whichSound, integer index returns sound
    return AddUnitSoundSet(unitTypeId,SOUND_WARCRY[index],whichSound)
endfunction

function AddUnitSoundSetPissed takes integer unitTypeId, sound whichSound, integer index returns sound
    return AddUnitSoundSet(unitTypeId,SOUND_PISSED[index],whichSound)
endfunction

private function CreateSoundFromFile takes string filePath, string eaxSetting returns sound
    local sound soundHandle = null
    local integer duration = GetSoundFileDuration(filePath)
    if (duration > 0) then
        set soundHandle = CreateSound(filePath, false, false, true, 12700, 12700, eaxSetting)
        call SetSoundDuration(soundHandle, duration)
    endif
    return soundHandle
endfunction

private function CreateSound3DFromFile takes string filePath, string eaxSetting returns sound
    local sound soundHandle = null
    local integer duration = GetSoundFileDuration(filePath)
    if (duration > 0) then
        set soundHandle = CreateSound(filePath, false, true, true, 12700, 12700, eaxSetting)
        call SetSoundDuration(soundHandle, duration)
    endif
    return soundHandle
endfunction

function AddUnitSoundSetFromFiles takes integer unitTypeId, string filePath, string extension returns nothing
    local integer i=0
    if extension=="auto" then
        call AddUnitSoundSetFromFiles(unitTypeId,filePath,"mp3")
        call AddUnitSoundSetFromFiles(unitTypeId,filePath,"flac")
        call AddUnitSoundSetFromFiles(unitTypeId,filePath,"wav")
    else
        loop
            exitwhen i>=SOUND_PER_STACK
            if i==0 then
                if AddUnitSoundSetDeath(unitTypeId, CreateSound3DFromFile(filePath + "Death." + extension, "DefaultEAXON"),0)==null then
                    call AddUnitSoundSetDeath(unitTypeId, CreateSound3DFromFile(filePath + "Death1." + extension, "DefaultEAXON"),0)
                endif
                if AddUnitSoundSetWarCry(unitTypeId, CreateSoundFromFile(filePath + "WarCry." + extension, "DefaultEAXON"),0)==null then
                    call AddUnitSoundSetWarCry(unitTypeId, CreateSoundFromFile(filePath + "WarCry1." + extension, "DefaultEAXON"),0)
                endif
                if AddUnitSoundSetReady(unitTypeId, CreateSoundFromFile(filePath + "Ready." + extension, "DefaultEAXON"),0)==null then
                    call AddUnitSoundSetReady(unitTypeId, CreateSoundFromFile(filePath + "Ready1." + extension, "DefaultEAXON"),0)
                endif
            else
                call AddUnitSoundSetDeath(unitTypeId, CreateSoundFromFile(filePath + "Death"+I2S(i+1)+"." + extension, "DefaultEAXON"),i)
                call AddUnitSoundSetReady(unitTypeId, CreateSoundFromFile(filePath + "Ready"+I2S(i+1)+"." + extension, "HeroAcksEAX"),i)
                call AddUnitSoundSetWarCry(unitTypeId, CreateSoundFromFile(filePath + "WarCry"+I2S(i+1)+"." + extension, "HeroAcksEAX"),i)
            endif
            call AddUnitSoundSetWhat(unitTypeId, CreateSoundFromFile(filePath + "What"+I2S(i+1)+"." + extension, "HeroAcksEAX"),i)
            call AddUnitSoundSetYes(unitTypeId, CreateSoundFromFile(filePath + "Yes"+I2S(i+1)+"." + extension, "HeroAcksEAX"),i)
            call AddUnitSoundSetYesAttack(unitTypeId, CreateSoundFromFile(filePath + "YesAttack"+I2S(i+1)+"." + extension, "HeroAcksEAX"),i)
            call AddUnitSoundSetPissed(unitTypeId, CreateSoundFromFile(filePath + "Pissed"+I2S(i+1)+"." + extension, "HeroAcksEAX"),i)
            set i=i+1
        endloop
    endif
endfunction

function HasUnitShopSoundSet takes integer unitTypeId returns boolean
    return LoadBoolean(h, unitTypeId, KEY_HAS_SHOP_SOUNDSET)
endfunction

function AddUnitShopSoundSet takes integer unitTypeId returns nothing
    call SaveBoolean(h, unitTypeId, KEY_HAS_SHOP_SOUNDSET, true)
endfunction

function RemoveUnitShopSoundSet takes integer unitTypeId returns nothing
    call SaveBoolean(h, unitTypeId, KEY_HAS_SHOP_SOUNDSET, false)
endfunction

function GetUnitSoundSetAnimationType takes integer unitTypeId returns integer
    return LoadInteger(h, unitTypeId, KEY_PORTRAIT_ANIMATION_TYPE)
endfunction

function SetUnitSoundSetAnimationType takes integer unitTypeId, integer t returns nothing
    call SaveInteger(h, unitTypeId, KEY_PORTRAIT_ANIMATION_TYPE, t)
endfunction

function GetUnitSoundSetPortraitModel takes integer unitTypeId returns string
    return LoadStr(h, unitTypeId, KEY_PORTRAIT_MODEL)
endfunction

function SetUnitSoundSetPortraitModel takes integer unitTypeId, string model returns nothing
    call SaveStr(h, unitTypeId, KEY_PORTRAIT_MODEL, model)
endfunction

private function HasControl takes player whichPlayer, unit whichUnit returns boolean
    return GetOwningPlayer(whichUnit) == whichPlayer or GetPlayerAlliance(whichPlayer, GetOwningPlayer(whichUnit), ALLIANCE_SHARED_CONTROL)
endfunction

private function AlliesWithSharedControl takes player whichPlayer returns force
    local force whichForce = CreateForce()
    local integer i = 0
    loop
        exitwhen (i == bj_MAX_PLAYERS)
        if (whichPlayer == Player(i) or GetPlayerAlliance(whichPlayer, Player(i), ALLIANCE_SHARED_CONTROL)) then
            call ForceAddPlayer(whichForce, Player(i))
        endif
        set i = i + 1
    endloop
    return whichForce
endfunction

private function IsUnitSpeakingForPlayer takes unit whichUnit returns boolean
    local boolean sameUnit = playerSpeaker == whichUnit
    return sameUnit and playerSound != null and GetSoundIsPlaying(playerSound)
endfunction

private function UpdatePlayerSound takes sound whichSound, unit whichUnit returns boolean
    // only update if it is a different speaker or the current unit is not speaking
    if (playerSpeaker == whichUnit and IsUnitSpeakingForPlayer(playerSpeaker)) then
        return false
    endif
 
    set playerSound = whichSound
    set playerSpeaker = whichUnit
    call StartSound(whichSound)
 
    return true
endfunction

private function GetRandomSoundEx takes unit whichUnit, integer t0, integer t1 returns sound
    local integer soundsCounter = 0
    local sound array sounds
    local sound whichSound = null
    loop
        exitwhen t0>t1
        static if LIBRARY_UnitSoundSetsPerUnit then
                set whichSound = GetUnitSoundSetPerUnit(whichUnit, t0)
                if (whichSound == null) then
                    set whichSound = LoadSoundHandle(h, GetUnitTypeId(whichUnit), t0)
                endif
        else
                set whichSound = LoadSoundHandle(h, GetUnitTypeId(whichUnit), t0)
        endif
        if whichSound!=null and not GetSoundIsPlaying(whichSound) then
            set sounds[soundsCounter] = whichSound
            set soundsCounter = soundsCounter + 1
        endif
        set t0 = t0 + 1
    endloop
    if soundsCounter>0 then
        set whichSound=sounds[GetRandomInt(0, soundsCounter - 1)]
    endif
    loop
        set sounds[soundsCounter]=null
        exitwhen soundsCounter==0
        set soundsCounter=soundsCounter-1
    endloop
    return whichSound
endfunction

private function GetRandomSound takes unit whichUnit, integer t0, integer t1 returns sound
    local sound whichSound = null
    if (t0 != t1) then
        set whichSound = GetRandomSoundEx(whichUnit, t0, t1)
    else
        static if LIBRARY_UnitSoundSetsPerUnit then
                set whichSound = GetUnitSoundSetPerUnit(whichUnit, t0)
                if whichSound==null then
                    set whichSound = LoadSoundHandle(h, GetUnitTypeId(whichUnit), t0)
                endif
        else
                set whichSound = LoadSoundHandle(h, GetUnitTypeId(whichUnit), t0)
        endif
    endif

    return whichSound
endfunction

private function UpdatePlayerSelect takes unit whichUnit returns nothing
    if (playerClickTarget == whichUnit) then
        set playerClickCounter = playerClickCounter + 1
    else
        set playerClickCounter = 1
    endif
 
    set playerClickTarget = whichUnit
endfunction

private function IsPlayerSelectionPissed takes unit whichUnit returns boolean
    return playerClickTarget == whichUnit and playerClickCounter > PISSED_COUNTER
endfunction

private function TimerFunctionEndAnimation takes nothing returns nothing
    local integer playerId = LoadInteger(h, GetHandleId(GetExpiredTimer()), 0)
    if (GetLocalPlayer() == Player(playerId)) then
        call BlzFrameSetSpriteAnimate(playerPortrait, 2, 0) // stand
        call BlzFrameSetVisible(playerPortrait, false)
    endif
endfunction

private function PortraitTalkAnimation takes integer unitTypeId, sound whichSound returns nothing
    /*
    -- birth = 0
    -- death = 1
    -- stand = 2
    -- morph = 3
    -- alternate = 4
    */
    call BlzFrameSetModel(playerPortrait, GetUnitSoundSetPortraitModel(unitTypeId), 0)
    call BlzFrameSetSpriteAnimate(playerPortrait, 3, 0) // morph
    call BlzFrameSetVisible(playerPortrait, true)
 
    call PauseTimer(playerAnimationTimer)
    call TimerStart(playerAnimationTimer, GetSoundDurationBJ(whichSound), false, function TimerFunctionEndAnimation)
    call SaveInteger(h, GetHandleId(playerAnimationTimer), 0, GetPlayerId(GetLocalPlayer()))
endfunction

private function PortraitAnimation takes unit whichUnit, sound whichSound returns nothing
    local integer unitTypeId = GetUnitTypeId(whichUnit)
    local integer t = GetUnitSoundSetAnimationType(unitTypeId)
    if (t == PORTRAIT_ANIMATION_TYPE_CINEMATIC) then
        // TODO Recognize GetAllyColorFilterState for the player color
        call SetCinematicScene(unitTypeId, GetPlayerColor(GetOwningPlayer(whichUnit)), null, null, GetSoundDurationBJ(whichSound), GetSoundDurationBJ(whichSound))
        //call BlzFrameSetVisible(BlzGetOriginFrame(ORIGIN_FRAME_PORTRAIT_HP_TEXT, 0), true)
        //call BlzFrameSetVisible(BlzGetOriginFrame(ORIGIN_FRAME_PORTRAIT_MANA_TEXT, 0), true)
    elseif (t == PORTRAIT_ANIMATION_TYPE_ANIMATION) then
        call PortraitTalkAnimation(unitTypeId, whichSound)
    endif
endfunction

private function IsSoundWhat takes integer t0 returns boolean
    return t0>=SOUND_WHAT[0] and t0<=SOUND_WHAT[SOUND_PER_STACK-1]
endfunction

// this should be safe to use with GetLocalPlayer()
private function PlayRandomSound takes unit whichUnit, integer t0, integer t1 returns sound
    local sound whichSound = GetRandomSound(whichUnit, t0, t1)
    if whichSound != null then
        if UpdatePlayerSound(whichSound, whichUnit) then
            if IsSoundWhat(t0) then
                call UpdatePlayerSelect(whichUnit)
            endif
            if (IsUnitSelected(whichUnit, GetLocalPlayer())) then
                call PortraitAnimation(whichUnit, whichSound)
            endif
        endif
    // update selected unit for pissed even if it has no sound
    elseif IsSoundWhat(t0) then
        call UpdatePlayerSelect(whichUnit)
    endif
    return whichSound
endfunction

private function PlayRandomSoundForForce takes force whichForce, unit whichUnit, integer t0, integer t1 returns nothing
    if (IsPlayerInForce(GetLocalPlayer(), whichForce)) then
        call PlayRandomSound(whichUnit, t0, t1)
    endif
endfunction

function PlayRandomSoundForAllies takes unit whichUnit, integer t0, integer t1 returns nothing
    local force allies = AlliesWithSharedControl(GetOwningPlayer(whichUnit))
    call ForceAddPlayer(allies,GetOwningPlayer(whichUnit))
    call PlayRandomSoundForForce(allies, whichUnit, t0, t1)
    call ForceClear(allies)
    call DestroyForce(allies)
    set allies = null
endfunction

private function StopUnitSound takes unit whichUnit returns nothing
    // the death sound will stop any currently played sound from the unit
    if (IsUnitSpeakingForPlayer(whichUnit)) then
        call StopSound(playerSound, false, false)
    endif
endfunction

private function PlayRandom3DDeathSound takes unit whichUnit returns nothing
    local sound soundHandle = GetRandomSound(whichUnit, SOUND_DEATH[0], SOUND_DEATH[SOUND_PER_STACK-1])
    if soundHandle!=null then
        call StartSound(soundHandle)
        call AttachSoundToUnit(soundHandle, whichUnit)
        set soundHandle = null
    endif
endfunction

public function PlayUnitYes takes unit u returns sound
    return PlayRandomSound(u,SOUND_YES[0],SOUND_YES[SOUND_PER_STACK-1])
endfunction
public function PlayUnitYesAttack takes unit u returns sound
    local sound whichSound=PlayRandomSound(u,SOUND_YES_ATTACK[0],SOUND_YES_ATTACK[SOUND_PER_STACK-1])
    if whichSound==null then
        return PlayUnitYes(u)
    endif
    return whichSound
endfunction
public function PlayUnitWhat takes unit u returns sound
    return PlayRandomSound(u,SOUND_WHAT[0],SOUND_WHAT[SOUND_PER_STACK-1])
endfunction
public function PlayUnitPissed takes unit u returns sound
    local sound whichSound=PlayRandomSound(u,SOUND_PISSED[0],SOUND_PISSED[SOUND_PER_STACK-1])
    if whichSound==null then
        return PlayUnitWhat(u)
    endif
    return whichSound
endfunction
public function PlayUnitReady takes unit u returns sound
    return PlayRandomSound(u,SOUND_READY[0],SOUND_READY[SOUND_PER_STACK-1])
endfunction
public function PlayUnitWarCry takes unit u returns sound
    local sound whichSound=PlayRandomSound(u,SOUND_WARCRY[0],SOUND_WARCRY[SOUND_PER_STACK-1])
    if whichSound==null then
        return PlayUnitYesAttack(u)
    endif
    return whichSound
endfunction

private function AfterSelect takes nothing returns nothing
    local timer t=GetExpiredTimer()
    local integer kT=GetHandleId(t)
    local unit triggerUnit=LoadUnitHandle(h,kT,0)
    local player triggerPlayer=LoadPlayerHandle(h,kT,1)

    if triggerPlayer==GetLocalPlayer() then
        set triggerUnit=GetMainSelectedUnitEx()
        if triggerUnit!=null and triggerUnit==GetMainSelectedUnitEx() and (HasControl(triggerPlayer,triggerUnit) or HasUnitShopSoundSet(GetUnitTypeId(triggerUnit))) then
            if IsPlayerSelectionPissed(triggerUnit) then
                call PlayUnitPissed(triggerUnit)
            else
                call PlayUnitWhat(triggerUnit)
            endif
        endif
    endif

    call FlushChildHashtable(h,kT)
    call RemoveSavedHandle(h,GetHandleId(triggerUnit),0)
    call PauseTimer(t)
    call DestroyTimer(t)

    set t=null
    set triggerUnit=null
    set triggerPlayer=null
endfunction

private function TriggerActionSelect takes nothing returns nothing
    local unit triggerUnit = GetTriggerUnit()
    local integer kU=GetHandleId(triggerUnit)
    local timer t=LoadTimerHandle(h,kU,0)
    if t==null then
        set t=CreateTimer()
        call SaveTimerHandle(h,kU,0,t)
        call SaveUnitHandle(h,GetHandleId(t),0,triggerUnit)
        call SavePlayerHandle(h,GetHandleId(t),1,GetTriggerPlayer())
        call TimerStart(t,0,false,function AfterSelect)
    endif
    set t=null
    set triggerUnit=null
endfunction

private function EndUnitTalkPortrait takes unit whichUnit returns nothing
    local integer unitTypeId = GetUnitTypeId(whichUnit)
    local integer t = GetUnitSoundSetAnimationType(unitTypeId)
    local integer playerId = GetPlayerId(GetLocalPlayer())
    if (HasUnitSoundSet(GetUnitTypeId(whichUnit)) and playerSpeaker == whichUnit) then
        if (t == PORTRAIT_ANIMATION_TYPE_CINEMATIC) then
            // Do not end talk animations for native sound sets.
            // TODO Deslecting a unit with custom sound set and selecting a unit with a native sound set seems to stop the talk animation because of this.
            call EndCinematicScene()
        elseif (t == PORTRAIT_ANIMATION_TYPE_ANIMATION) then
            call PauseTimer(playerAnimationTimer)
            call BlzFrameSetVisible(playerPortrait, false)
        endif
    endif
endfunction

private function TriggerConditionDeselect takes nothing returns boolean
    if (GetTriggerPlayer() == GetLocalPlayer()) then
        call EndUnitTalkPortrait(GetTriggerUnit())
    endif
    return false
endfunction

private function TriggerActionOrder takes nothing returns nothing
    local unit triggerUnit = GetTriggerUnit()
    local unit target=GetOrderTargetUnit()
    local integer orderId = 0
    if (HasControl(GetLocalPlayer(), triggerUnit) and GetMainSelectedUnitEx() == triggerUnit) then
        set orderId = GetIssuedOrderId()
        if orderId==ORDER_ID_SMART then
            if target!=null and IsUnitEnemy(triggerUnit,GetOwningPlayer(target)) then
                call PlayUnitYesAttack(triggerUnit)
            else
                call PlayUnitYes(triggerUnit)
            endif
        else
            if orderId == ORDER_ID_ATTACK then
                if IsUnitType(target,UNIT_TYPE_HERO) and GetRandomInt(0,5)==0 then
                    call PlayUnitWarCry(triggerUnit)
                else
                    call PlayUnitYesAttack(triggerUnit)
                endif
            elseif (orderId == ORDER_ID_MOVE or orderId == ORDER_ID_PATROL or orderId == ORDER_ID_ATTACK_GROUND) then
                call PlayUnitYes(triggerUnit)
            endif
        endif
    endif
    set triggerUnit=null
    set target=null
endfunction

private function TriggerConditionReviveFinish takes nothing returns boolean
    call PlayRandomSoundForAllies(GetRevivingUnit(), SOUND_READY[0], SOUND_READY[SOUND_PER_STACK-1])
    return false
endfunction

private function TriggerConditionTrainFinish takes nothing returns boolean
    call PlayRandomSoundForAllies(GetTrainedUnit(), SOUND_READY[0], SOUND_READY[SOUND_PER_STACK-1])
    return false
endfunction

private function TriggerConditionHire takes nothing returns boolean
    call PlayRandomSoundForAllies(GetSoldUnit(), SOUND_READY[0], SOUND_READY[SOUND_PER_STACK-1])
    return false
endfunction

private function TriggerConditionDeath takes nothing returns boolean
    call PlayRandom3DDeathSound(GetDyingUnit())
    return false
endfunction

private function TimerFunctionCreatePlayerPortraits takes nothing returns nothing
    set playerPortrait = BlzCreateFrameByType("SPRITE", "SpriteName", BlzGetOriginFrame(ORIGIN_FRAME_PORTRAIT, 0), "", 0)
    call BlzFrameSetAllPoints(playerPortrait, BlzGetOriginFrame(ORIGIN_FRAME_PORTRAIT, 0))
    call BlzFrameSetVisible(playerPortrait, false)
    call PauseTimer(GetExpiredTimer())
    call DestroyTimer(GetExpiredTimer())
endfunction

private function Init takes nothing returns nothing
    local integer i=0
    loop
        exitwhen i==bj_MAX_PLAYERS
        call TriggerRegisterPlayerUnitEvent(selectionTrigger, Player(i), EVENT_PLAYER_UNIT_SELECTED, null)
        call TriggerRegisterPlayerUnitEvent(deselectionTrigger, Player(i), EVENT_PLAYER_UNIT_DESELECTED, null)
        set i=i+1
    endloop
    call TriggerAddAction(selectionTrigger, function TriggerActionSelect)
    call TriggerAddCondition(deselectionTrigger, Condition(function TriggerConditionDeselect))

    call TriggerRegisterAnyUnitEventBJ(orderTrigger, EVENT_PLAYER_UNIT_ISSUED_ORDER)
    call TriggerRegisterAnyUnitEventBJ(orderTrigger, EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER)
    call TriggerRegisterAnyUnitEventBJ(orderTrigger, EVENT_PLAYER_UNIT_ISSUED_UNIT_ORDER)
    call TriggerRegisterAnyUnitEventBJ(orderTrigger, EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER)
    call TriggerAddAction(orderTrigger, function TriggerActionOrder)

    call TriggerRegisterAnyUnitEventBJ(revivalTrigger, EVENT_PLAYER_HERO_REVIVE_FINISH)
    call TriggerAddCondition(revivalTrigger, Condition(function TriggerConditionReviveFinish))

    call TriggerRegisterAnyUnitEventBJ(trainTrigger, EVENT_PLAYER_UNIT_TRAIN_FINISH)
    call TriggerAddCondition(trainTrigger, Condition(function TriggerConditionTrainFinish))
 
    call TriggerRegisterAnyUnitEventBJ(hireTrigger, EVENT_PLAYER_UNIT_SELL)
    call TriggerAddCondition(hireTrigger, Condition(function TriggerConditionHire))

    call TriggerRegisterAnyUnitEventBJ(deathTrigger, EVENT_PLAYER_UNIT_DEATH)
    call TriggerAddCondition(deathTrigger, Condition(function TriggerConditionDeath))

    call TimerStart(CreateTimer(), 0.0, false, function TimerFunctionCreatePlayerPortraits)
 
    set i=0
    loop
        exitwhen i>=SOUND_PER_STACK
        set SOUND_DEATH[i]        =i
        set SOUND_WHAT[i]            =SOUND_PER_STACK+i
        set SOUND_YES[i]            =SOUND_PER_STACK*2+i
        set SOUND_YES_ATTACK[i]    =SOUND_PER_STACK*3+i
        set SOUND_READY[i]        =SOUND_PER_STACK*4+i
        set SOUND_WARCRY[i]        =SOUND_PER_STACK*5+i
        set SOUND_PISSED[i]        =SOUND_PER_STACK*6+i
        set i=i+1
    endloop
endfunction

endlibrary

[EDIT] While I am at it :
The system doesn't work for localized sounds
Why do you bother replacing the portrait with a talking one when you can artificially display the HP and mana when the cinematic mode is on ? This would remove the need of having custom models for everything !
To simplify things I will store in the hash by an integer index the soundsets, at -1 the path and at positive all the sounds. This would make duplicates easier to check.
while I apperciate your work, you basically create your own system here. I think I will try to improve my code with some hints from your work.

What do you mean by
Why do you bother replacing the portrait with a talking one when you can artificially display the HP and mana when the cinematic mode is on ?
Do you mean that I should create custom frames with the HP and mana numbers?

Thx to Wrda I will refactor stuff. I thought my current version uploaded here already played different sounds per unit type.
I can check the target unit type for hero and randomly play Warcry and make sounds 3D if this is how the game behaves. Does anyone know what the chance for Warcry in the game is?

edit:
Updated the system:
  • Added suggested refactorings.
  • Hopefully less desyncs by moving some function calls outside of the async blocks.
  • Automatically detect file extensions by just trying "mp3", "flac" and "wav".
  • Do not play the same sound per unit type even if you click on another unit in between (stores the last played sound per unit type).

I like the idea of creating sound sets as separate instances/structs/IDs/whatever and reusing them for multiple unit types/units.
This will simplify the system no doubt. I can also allow to add sounds for more categories and let users implement their own callbacks.

For the HP stuff I think the only way is to create a a fake HP/mana frame and put it over the portrait frame during a cinematic scene.
The custom portrait frame with the animation requires too much work since you have to modify all portrait models.

To support localized sounds you would have to create the sound handle instances per player since they might have different sound durations.
I am not sure how to fix this. Playing the sound might not be the problem but starting a timer to end the cinematic scene with a certain duration would probably cause a desync. Maybe I should recommend playing Warcraft in English only with the system or never using standard sound paths?
Creating the sounds in the beginning might not cause a desync since it should be done by Warcraft itself like that in some maps?
 
Last edited:
Level 15
Joined
Sep 26, 2007
Messages
369
Does anyone know what the chance for Warcry in the game is?
Regretfully, I don't know... and I highly doubt anyone else does because the game's sooooo hardcoded. Oh, if only "Blizzie" made Warcraft 3 open-sourced (when pigs fly)...

All I know is that the occurrence is random (which is why you need to order anyone to attack a Hero a few times). I think the game calculates (priority-wise) the targeted Hero's level, HP, and/or other things (determining whether he/she is stronger or weaker for the attacker to let out a warcry line), though this is purely speculation and not yet proven due to the game--as stated above--being so hardcoded. I guess we have no choice but to improvise...

warcraft 1.26 ? classic?
Please be more specific and also use common sense when reporting problem(s)/bug(s). Just simply saying the Warcraft 3's version (1.00/1.36+) and type (Legacy/Classic/Reforged) isn't enough, let alone a screenshot displaying non-English texts... we neither have crystal balls nor we are psychics to read people's minds and magically figure out what it means in English (since this forum is English-only) and what went wrong (more troubleshooting information is needed).

However... and according to your screenshot (and especially the World Editor's main window title and the extra commands), you seem to be using UMSWE (pretty outdated, in my opinion) and I believe that it must be the source of the problem you're having, because UMSWE overrides most of "Blizzie"'s stuff with its own (like adding extra triggers which use additional scripts), and thus probably conflicting with this script. Therefore, could you please try it again but with the original World Editor?
 
Last edited:
Level 25
Joined
Feb 2, 2006
Messages
1,689
I made another major update which removes the animation types and simplifies the usage of the system. I have added fake HP and mana texts now which still show you the HP and mana of the talking unit during the cinematic scene. Besides, I recognize the team colors set by the player (
JASS:
GetAllyColorFilterState
).
I had to modify the system GetMainSelectedUnit and add my own system GetMainSelectedUnitForPlayer which synchronizes the main selected unit to all players to remove a lot of GetLocalPlayer blocks which might cause desyncs. No timers should be started in such blocks anymore.
Besides, the cinematic scene ends now at the correct time (after the timer has ended) and not too late.
The system uses the modified version of GetMainSelectedUnit now: [Lua] - GetMainSelectedUnit
This should support save games.
For Warcry: I still use a chance you can change in a constant because the exact logic behind it is unlcear at this moment.

There is one known bug: If you select a unit with a custom sound set and deselect it and select one with a native sound set it will end the talk animation immediately of the newly selected unit (for example select Cenarius and then the Paladin). This is because I have to end the customly played scenes when changing the unit and it is done too early with the deselect event?

If you want to help me improving the system, you could provide me the following information:
  • Exact font size and frame location of HP and mana texts.
  • Exact coloring behavior of the HP text: It gets yellow/orange/red at some point and I don't know the exact logic behind that.

This would make the fake texts look exactly like the native texts.

You can also test it for desyncs and tell me at what points it desyncs.
 
Last edited:

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,888
If you want to help me improving the system, you could provide me the following information:
  • Exact font size and frame location of HP and mana texts.
  • Exact coloring behavior of the HP text: It gets yellow/orange/red at some point and I don't know the exact logic behind that.
C++:
      v55 = *(float *)CWidget::GetLifePercent(v38, (unsigned int *)&v56);
      if ( v55 > 0.6 )
      {
        BYTE1(v53) = 0xFF;
        BYTE2(v53) = (signed int)(255.0 - ((lifePercent - 0.5) * 255.0 + (lifePercent - 0.5) * 255.0));
LABEL_29:
        CSimpleRegion::SetColour((_BYTE *)v1[144], &v53);
        goto LABEL_30;
      }
      if ( lifePercent <= 0.3 )
      {
      }
      else
      {
        if ( lifePercent <= 0.6 )
        {
          v54 = (signed int)(lifePercent / 0.6 * 255.0);
          BYTE1(v53) = v54;
LABEL_28:
          BYTE2(v53) = -1;
          goto LABEL_29;
        }
      }
      v54 = (signed int)(lifePercent / 0.8 * 255.0);
      BYTE1(v53) = v54;
      goto LABEL_28;
    }
Font: frizqt
C++:
CLayoutFrame::SetRelativePoint( hpbar, 0, portrait, 6, 0.214375, 0.0283, 1);
CLayoutFrame::SetRelativePoint( mpbar, 0, portrait, 6, 0.214375, 0.01375, 1);
Size:
(ui/miscui.txt)
PortraitStats=0.011

I got this from Tasyen and Unryze on discord.
 
Level 25
Joined
Feb 2, 2006
Messages
1,689
Hey thank you. This is really useful.

The only thing which will not work during the cinematic scene now is clicking on the portrait model to lock the camera on the unit but I guess that's not too bad.

edit:
Some more explanation would be helpful.
Is PortraitSTats=0.011 the width of both texts?
Is v53 and v54 the red part of the text? Is the green part always FF?


edit2:
Fixed playing sounds for local players only.
 
Last edited:
Level 12
Joined
Feb 13, 2012
Messages
403
i think im implemnting the script wrong can i get some help so i can use it for my map pleass ? 🙏
 
Level 25
Joined
Feb 2, 2006
Messages
1,689
C++:
      v55 = *(float *)CWidget::GetLifePercent(v38, (unsigned int *)&v56);
      if ( v55 > 0.6 )
      {
        BYTE1(v53) = 0xFF;
        BYTE2(v53) = (signed int)(255.0 - ((lifePercent - 0.5) * 255.0 + (lifePercent - 0.5) * 255.0));
LABEL_29:
        CSimpleRegion::SetColour((_BYTE *)v1[144], &v53);
        goto LABEL_30;
      }
      if ( lifePercent <= 0.3 )
      {
      }
      else
      {
        if ( lifePercent <= 0.6 )
        {
          v54 = (signed int)(lifePercent / 0.6 * 255.0);
          BYTE1(v53) = v54;
LABEL_28:
          BYTE2(v53) = -1;
          goto LABEL_29;
        }
      }
      v54 = (signed int)(lifePercent / 0.8 * 255.0);
      BYTE1(v53) = v54;
      goto LABEL_28;
    }
Font: frizqt
C++:
CLayoutFrame::SetRelativePoint( hpbar, 0, portrait, 6, 0.214375, 0.0283, 1);
CLayoutFrame::SetRelativePoint( mpbar, 0, portrait, 6, 0.214375, 0.01375, 1);
Size:
(ui/miscui.txt)
PortraitStats=0.011

I got this from Tasyen and Unryze on discord.
Finally got some time to do the coloring. I think I got it in JASS now:

JASS:
// Does not divide by 100.
private function GetUniLifePercent takes real value, real maxValue returns real
    // Return 0 for null units.
    if (maxValue == 0) then
        return 0.0
    endif

    return value / maxValue
endfunction

private function I2X takes integer int returns string
    local string hexas = "0123456789abcdef"
    local string hex = ""
    local integer dev
   
    loop
        if int > 15 then
            set dev = int - (int / 16) * 16
            set int = int / 16
            set hex = SubString(hexas, dev, dev + 1) + hex
        else
            set hex = SubString(hexas, int, int + 1) + hex
            return hex
        endif
    endloop
    return hex
endfunction

private function I2XW takes integer int, integer width returns string
    local string hex = I2X(int)
    local integer i = StringLength(hex)
    loop
        exitwhen (i >= width)
        set hex = "0" + hex
        set i = i + 1
    endloop
    return hex
endfunction

private function GetHPTextEx takes real life, real maxLife returns string
    local real lifePercent = GetUniLifePercent(life, maxLife)
    local integer red = 255
    local integer green = 255
   
    if (lifePercent > 0.6) then
        /*
         BYTE2(v53) = (signed int)(255.0 - ((lifePercent - 0.5) * 255.0 + (lifePercent - 0.5) * 255.0));
         y = 255 - ((x - 0.5) * 255 + (x - 0.5) * 255)
         y = 255 - 255 * ((x - 0.5) + (x - 0.5))
         y = 255 - 255 * (x - 0.5 + x - 0.5)
         y = 255 - 255 * (2x - 1)
         y = 255 - (510x - 255)
         y = 255 - 510x + 255
         y = 510 - 510x
         */
        set red = R2I(510 - 510 * lifePercent)
    elseif (lifePercent > 0.3) then
        set green = R2I(lifePercent / 0.6 * 255.0)
    else
        set green = R2I(lifePercent / 0.8 * 255.0)
    endif
   
    return "|cff" + I2XW(red, 2) + I2XW(green, 2) + "00" + I2S(R2I(life)) + " / " + I2S(R2I(maxLife)) + "|r"
endfunction

private function GetHPText takes unit whichUnit returns string
    return GetHPTextEx(GetUnitState(whichUnit, UNIT_STATE_LIFE), GetUnitState(whichUnit, UNIT_STATE_MAX_LIFE))
endfunction

and will update the system soon.
I have copied I2X from Hexadecimals to Decimals and added I2XW to fill it up to two digits for color codes.
I finally got that the C/C++ code set two different bytes with BYTE1 and BYTE2 und it is red and green. Besides, I simplified to goto Spaghetti code and tried to simplify the math stuff.

About the positioning of the text. Not sure if PortraitSTats=0.011 is really the width and I have the coordinates but not the layout of the text like CENTER/MIDDLE and not the width and height maybe.
 
Last edited:

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,888
About the positioning of the text. Not sure if PortraitSTats=0.011 is really the width and I have the coordinates but not the layout of the text like CENTER/MIDDLE and not the width and height maybe.
Sorry, it is the font size. I misunderstood Tasyen. I really don't know the alignment at all, but try MIDDLE on both directions. I think the issue also lies on the frame scale, but it appears very difficult to tune it.

There's still desync issues, take for example, the easter bunny. If you select one and then select the second one before the sound dissipates, desync happens. Likewise if you order it to move before the sound finishes.
Clicking the Goblin Merchant not only displays the hp while normally it shouldn't, but also invades the mana text slightly.
 
Level 25
Joined
Feb 2, 2006
Messages
1,689
At what amount HP is not displayed anymore? I think high numbers like 15 0000 are not displayed.

I will check the desync stuff.

edit:
It seems that for 10 000 HP life it does not show maximum life anymore but for 1000 it still shows it, so maybe if the total number of characters is 13 it is too much to show? Like "10000 / 10000" is too long but not "1000 / 1000".

For the ability invulnerable HP is never shown?

edit2:
It seems that the limit for mana and hp is 11 characters and that the hp is never shown for invulnerable structures no matter who the owner is.

edit3:
I suspect GetSoundIsPlaying is causing the desync since it only returns true for the one player who the sound was played for and only after it has been played once. My solution would be to start a timer per player which takes as long as the sound and check the timer instead of using GetSoundIsPlaying.

edit4:
Update
  • Make the fake bar system modular and optional by moving it into the vJass library UnitSoundSetsFakeBars.
  • Do not show max mana and max life if the text is too long (FAKE_BAR_CHARACTERS_LIMIT).
  • Do not show anything if even the mana and life text without maximum is too long.
  • Never show HP for invulnerable structures by default (HIDE_HP_INVULNERABLE_STRUCTURE).
  • Hopefully fix desync by replacing GetSoundIsPlaying with a timer per player.

Please check if the desync is gone :)
 
Last edited:
Level 49
Joined
Apr 18, 2008
Messages
8,421
- Handle attack sounds (not sure when they are used).
Hey @Barade, figured I could help you with that one.
Attack sounds (if you mean "attack effort" sounds, like the Blademaster's, Brewmaster's, Pit Lord's etc?) are attached to individual models, like so:
1706086940388.png

Identical to the way that death sounds are played.
This is probably beyond the scope of a "soundset replacer" unless you can somehow add extra attack sounds with IDs to the .slk which can then somehow be attached to a model, but this seems like it would be in the scope of a full mod or total conversion, rather than something any mapmaker can plug-and-play into their map.

Also! I have a question, if you don't mind.
  • The unit portrait can not be selected to move the camera to the unit during the portrait talk animation played with SetCinematicScene. Probably, this cannot be fixed.
  • Game messages are moved one line to the top by SetCinematicScene even without subtitles on. Probably, this cannot be fixed.
Does this mean that whenever a unit talks, these problems happen?
Or does this mean that if the mapmaker uses the SetCinematicScene native (?), only then do these problems occur? So I'm assuming "SetCinematicScene" is how Blizzard simulates units talking inside a campaign mission, but outside of a cinematic?
 
Last edited:
Level 13
Joined
Jun 23, 2009
Messages
299
After skimming this thread a bit, I'll give you a quick tip in case you're interested:
In game :
☼ Sounds have a delay
If this still happens with your system it means the sounds have to be preloaded first (as far as I can remember), I strongly suggest adding a sound management system as a dependency to this one and working with that, it could do wonders in fixing small things like this.
I personally use Rising_Dusk's SoundUtils to this day for handling sounds but it's very likely there's more recent, similar systems available here at the Hive that can be used to the same effect. Do not rely on vanilla's implementation alone, that shit has always been broken as heck.
Deolrin:

This is probably beyond the scope of a "soundset replacer" unless you can somehow add extra attack sounds with IDs to the .slk which can then somehow be attached to a model, but this seems like it would be in the scope of a full mod or total conversion, rather than something any mapmaker can plug-and-play into their map.
Actually, it doesn't really matter how vanilla does it, you could just trigger the attack sounds with code (iirc using one of the damage events, the one that is triggered just before damage is actually dealt?) and it would sound alright for the purposes of this system I think.
 
Last edited:
Level 25
Joined
Feb 2, 2006
Messages
1,689
Whenever the unit talks you cannot click on the unit's portrait to move the camera to the unit and an empty cinematic message is added because the system uses the native.

About the attack sounds: I can just ignore them then. The death sound is also optional and you could rely on the model's death sound. Or do you want me to add something with the damage trigger for them?

About using a third party sound system: I found this one here: [System] SoundTools
Would this fix the delay/missing preloads?
 
Level 13
Joined
Jun 23, 2009
Messages
299
About using a third party sound system: I found this one here: [System] SoundTools
Would this fix the delay/missing preloads?
Looking at it, it should fix those issues, but beware that according to the author that system has issues of its own (although they don't seem to be related to your usecase). The version posted by BPower here is promising though.
About the attack sounds: I can just ignore them then. The death sound is also optional and you could rely on the model's death sound. Or do you want me to add something with the damage trigger for them?
Nah, it's totally fine if you don't cover that part, vanilla has those sounds attached to the models and it works well so I think we should do the same and it ultimately falls outside of the scope of your system.
 
Level 15
Joined
Sep 26, 2007
Messages
369
Hey @Barade, figured I could help you with that one.
Attack sounds (if you mean "attack effort" sounds, like the Blademaster's, Brewmaster's, Pit Lord's etc?) are attached to individual models
Hmm... speaking of "attack effort" sounds, here's a little something I've done during my spare time (though this is completely unrelated to sound sets, hence the spoiler button)...


The script (which is used to take care of the sound attachment, pitch, volume, et cetera) and the trigger I've humbly made mimics the attack effort sounds of certain Heroes while respecting the sound's position and distance, and also the player's vision (especially if there's someone fighting in the fog of war). Unfortunately, the "Is attacked" part of the "Unit - A unit" Event can't be triggered by attacking units with the "Attack-Ground" order issued, but such an order is only used by siege units so it's of little consequence.

But hey, this isn't limited only to the "Is attacked" part of the "Unit - A unit" Event! Try replacing the "Is attacked" part with something else (like "Unit - A unit Dies" Event), then change/update everything in the variables, and optionally add some extra stuff in the trigger (like creating effects, adding more sounds to be played a few seconds later, triggering this and that, et cetera), and TA-DAAAH!

One last thing--and most importantly, this is NO substitute to the sound events attached to models! I only use this just to... y'know, spice some things up.

Having said all that, I'll share the map (requires 1.26 version and beyond) for y'all... and feel free to use, modify, and/or even own everything in it without crediting me. Hey, maybe the experts here could vastly improve the script and/or trigger, make it/them even better, and then share it/them to the world... of Warcraft.
 

Attachments

  • AttackEffortSoundsTest.w3x
    22.4 KB · Views: 2

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,888
I haven't found desyncs. There's this small issue when you click to select a different unit while it is the same unit type as the previous unit before the sound ends, you'll hear a new "what" sound. There's a superposition of the hp/mana text when clicking from a unit with custom sound set to a unit with normal sound set.

Even though to get both texts' exact position as far as I am aware is extremely hard, fixing the issues above would make this go from Recommended to High Quality.

Approved
 
Level 25
Joined
Feb 2, 2006
Messages
1,689
Thanks for approving. I would try to get the texts right if I had the exact positions/size from someone. I don't know exactly how to get them from the game. Not sure if PortraitStats=0.011 alone helped.

I will check the unit type sound stopping.

Did you experience any lags which would require preloading sounds as mentioned by others? I didn't really and I don't get how all these SoundUtils systems actually preload sounds. I only see that they recycle sounds to keep the RAM low.
 

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,888
I would try to get the texts right if I had the exact positions/size from someone. I don't know exactly how to get them from the game. Not sure if PortraitStats=0.011 alone helped.
Even drinking for the tasyen's Bible I won't reach his feet, can't help you much there :(
Did you experience any lags which would require preloading sounds as mentioned by others? I didn't really and I don't get how all these SoundUtils systems actually preload sounds.
I didn't expire any at all to be honest. Using [System] SoundTools with local player sounds a recipe for disaster. [Snippet] Resource Preloader creates the sound, sets volume to 0, plays it, kill when done.
I only see that they recycle sounds to keep the RAM low.
I was under the impression KillSoundWhenDone would result in destroying the sound and freeing memory, so this isn't the case then?
I have doubts in the benefit of doing this while playing sounds for certain is needed, quite frankly.
 
Top