UI-Custom Race System

This bundle is marked as pending. It has not been reviewed by a staff member yet.
An in-game race faction selection system which seamlessly integrates to the classic melee gameplay.
Useful for Techtree Contests and custom melee scenarios.

Features:
  • Virtually unlimited number of factions per race.
  • Custom UI tailor-made to allow both functionality and elegance.
  • Tournament Compatibility (v.1.2.0 and above)
  • Race Preview Box
  • Delayed melee game start (up to 3 seconds. This can be configured as you wish.).
Pros:
  • Custom UI is not dialog-based.
  • Accessing multiple factions is as easy as dragging a slider.
Cons:
  • Requires 1.31.1 or higher
  • Visual settings based on 1.31.1

How to Import

How to Use

Lua API

Changelog

Credits


Simply follow the instructions below:
  1. Import the toc file included in the downloadable map, CustomRaceTOC.toc. Leave the file path as-is.
  2. Import the fdf file included in the downloadable map, CustomRaceFrame.fdf (vJASS only).
  3. Import the corresponding race seal blp files from the downloadable map. Without these files, the default preview images will display a green texture.
  4. Copy and paste the Custom Race System folder into your Trigger Editor.



At any point in your script, you can call the following function in order to retrieve a faction "object".
Lua:
-- race takes a JASS race type, such as RACE_HUMAN or RACE_ORC
-- name is a string that represents the faction's name. (This is optional, more or less)
local faction = CustomRaceSystem.create(race [, name])


One can, at any point, redefine the faction name if it wasn't defined already at the moment of creation.
Lua:
faction.name = "Some String"


One can now include the town hall ids, as well as the custom Hero ids, in the faction.
Lua:
-- The parameters can either be strings or integers. (In the case of strings, they must
-- be rawcodes).
faction:addHall(...)
faction:addHero(...)


Note: When adding a town hall id to a faction, it is also added to the global list of town hall ids. The same goes for hero ids. This extends the default behavior as seen in normal melee maps.


Now, the race has some handy town hall ids and hero ids. Let's include some description and an image preview for our race.
Lua:
-- In case of any ambiguities, secretFilePath is a string
-- containing the path to the image texture.
faction:defDescription("This is some juicy handtext")
faction:defRacePic( <secretFilePath>)


Alright. It looks like we're almost finished now. Let's now define our setup function.
Lua:
faction:defSetup(function(whichPlayer, startLoc, doHeroes, doCamera, doPreload)
    CreateUnitAtLoc(whichPlayer, <townHallID>, startLoc, bj_UNIT_FACING)
    CreateUnitAtLoc(whichPlayer, <unitID>, startLoc, bj_UNIT_FACING)
end)


Hold on .. where did the function arguments come from? Let's take a look at MeleeStartingUnitsHuman to figure it out.
JASS:
function MeleeStartingUnitsHuman takes player whichPlayer, location startLoc, boolean doHeroes, boolean doCamera, boolean doPreload returns nothing
    local boolean  useRandomHero = IsMapFlagSet(MAP_RANDOM_HERO)
    local real     unitSpacing   = 64.00
    local unit     nearestMine
    local location nearMineLoc
    local location heroLoc
    local real     peonX
    local real     peonY
    local unit     townHall = null


    if (doPreload) then
        call Preloader( "scripts\\HumanMelee.pld" )
    endif


    set nearestMine = MeleeFindNearestMine(startLoc, bj_MELEE_MINE_SEARCH_RADIUS)
    if (nearestMine != null) then
        // Spawn Town Hall at the start location.
        set townHall = CreateUnitAtLoc(whichPlayer, 'htow', startLoc, bj_UNIT_FACING)


        // Spawn Peasants near the mine.
        set nearMineLoc = MeleeGetProjectedLoc(GetUnitLoc(nearestMine), startLoc, 320, 0)
        set peonX = GetLocationX(nearMineLoc)
        set peonY = GetLocationY(nearMineLoc)
        call CreateUnit(whichPlayer, 'hpea', peonX + 0.00 * unitSpacing, peonY + 1.00 * unitSpacing, bj_UNIT_FACING)
        call CreateUnit(whichPlayer, 'hpea', peonX + 1.00 * unitSpacing, peonY + 0.15 * unitSpacing, bj_UNIT_FACING)
        call CreateUnit(whichPlayer, 'hpea', peonX - 1.00 * unitSpacing, peonY + 0.15 * unitSpacing, bj_UNIT_FACING)
        call CreateUnit(whichPlayer, 'hpea', peonX + 0.60 * unitSpacing, peonY - 1.00 * unitSpacing, bj_UNIT_FACING)
        call CreateUnit(whichPlayer, 'hpea', peonX - 0.60 * unitSpacing, peonY - 1.00 * unitSpacing, bj_UNIT_FACING)


        // Set random hero spawn point to be off to the side of the start location.
        set heroLoc = MeleeGetProjectedLoc(GetUnitLoc(nearestMine), startLoc, 384, 45)
    else
        // Spawn Town Hall at the start location.
        set townHall = CreateUnitAtLoc(whichPlayer, 'htow', startLoc, bj_UNIT_FACING)


        // Spawn Peasants directly south of the town hall.
        set peonX = GetLocationX(startLoc)
        set peonY = GetLocationY(startLoc) - 224.00
        call CreateUnit(whichPlayer, 'hpea', peonX + 2.00 * unitSpacing, peonY + 0.00 * unitSpacing, bj_UNIT_FACING)
        call CreateUnit(whichPlayer, 'hpea', peonX + 1.00 * unitSpacing, peonY + 0.00 * unitSpacing, bj_UNIT_FACING)
        call CreateUnit(whichPlayer, 'hpea', peonX + 0.00 * unitSpacing, peonY + 0.00 * unitSpacing, bj_UNIT_FACING)
        call CreateUnit(whichPlayer, 'hpea', peonX - 1.00 * unitSpacing, peonY + 0.00 * unitSpacing, bj_UNIT_FACING)
        call CreateUnit(whichPlayer, 'hpea', peonX - 2.00 * unitSpacing, peonY + 0.00 * unitSpacing, bj_UNIT_FACING)


        // Set random hero spawn point to be just south of the start location.
        set heroLoc = Location(peonX, peonY - 2.00 * unitSpacing)
    endif


    if (townHall != null) then
        call UnitAddAbilityBJ('Amic', townHall)
        call UnitMakeAbilityPermanentBJ(true, 'Amic', townHall)
    endif


    if (doHeroes) then
        // If the "Random Hero" option is set, start the player with a random hero.
        // Otherwise, give them a "free hero" token.
        if useRandomHero then
            call MeleeRandomHeroLoc(whichPlayer, 'Hamg', 'Hmkg', 'Hpal', 'Hblm', heroLoc)
        else
            call SetPlayerState(whichPlayer, PLAYER_STATE_RESOURCE_HERO_TOKENS, bj_MELEE_STARTING_HERO_TOKENS)
        endif
    endif


    if (doCamera) then
        // Center the camera on the initial Peasants.
        call SetCameraPositionForPlayer(whichPlayer, peonX, peonY)
        call SetCameraQuickPositionForPlayer(whichPlayer, peonX, peonY)
    endif
endfunction


It we look closely at the function arguments, the argument names appear to be identical, down to the letter (ignoring syntax). This means that the setup function must be based on the above function. This will be the case for all setup functions for each race, so a few helper functions are included for your convenience.


Lua:
faction:defSetup(CustomRaceSetup.createSetup(
        CustomRaceSetup.createSetupHelper(function(whichPlayer, hall, mine)
        end, 'htow', 'hpea'),
        "scripts\\HumanMelee.pld", faction))


That's a lot to absorb, but not to worry. Let's look at what each function does, starting from the inner closure:


Lua:
-- The closure function expects a player whichPlayer, a unit hall and a unit mine
CustomRaceSetup.createSetupHelper(function(whichPlayer, hall, mine)
end, 'htow', 'hpea', 5)


What's the hall got to do with our initializer, and what's with the last three arguments?
The hall is the town hall that's created at game start, while the mine is the nearest mine from the player starting location. In most situations, the hall is also situated at the player starting location.


Now, the second argument is the hall id that's going to be created; the third argument is our worker id, and the fourth argument specifies the number of workers we'll create. In most cases, it's best to just ignore the last argument, since it defaults to 5.


Okay, we're done with the inner function. How about the outer function?
Lua:
faction:defSetup(CustomRaceSetup.createSetup(function()
end, "scripts\\HumanMelee.pld", faction))


Assuming that our closure exists, we look at the last two parameters.
The second parameter appears to be a string to be passed to a Preloader function. One can pass an empty string or a nil value to it, and it'll still work. The third parameter appears to refer back to the faction object. This implies that the faction object can't be used to create a setup function that directly refers to itself.


Interestingly, the parameter can also be any faction object, since the generated setup function will throw a random Hero if the appropriate map flag bit is set. It will throw a random Hero based on the faction's list of hero ids.


If you want to know more about using Custom Race System, you can check out the Setup chunk of the library.

Lua:
-- race is a jass-defined race type
-- name is a string
-- This creates a faction object.
CustomRaceSystem.create(race, name) => faction


-- The vararg can take either a string or an integer.
-- This adds a hall id to the faction's list, as well as the global list of hall ids.
faction:addHall(...)


-- The vararg can take either a string or an integer.
-- This adds a hero id to the faction's list, as well as the global list of hero ids.
-- If MAP_RANDOM_HERO is set, this will produce a random hero based on the
-- faction's hero list.
faction:addHero(...)


-- func should always be something which behaves as a function.
-- func syntax: function(whichPlayer, startLoc, doHeroes, doCamera, doPreload) end
faction:defSetup(func)


-- func should always be something which behaves as a function.
-- func syntax: function(whichPlayer) end
function:defAISetup(func)


-- All functions below take a string as a parameter.
faction:defRacePic(racePath)
faction:defDescription(desc)
faction:defName(name)
faction:defPlaylist(playlist)

  • v.1.0.0:
    • Release (Lua Version)
  • v.1.0.1: (Lua)
    • Added a camera pan effect once the game starts. User control is disabled during the transition to the game.
    • Added High Elves as a possible faction choice.
    • To play High Elves, select the Human Race.
    • Split the setup chunk into the main Setup chunk and the Default chunk for clarity.
  • v.1.0.2: (Lua)
    • Fixed highlight bug where a selected frame would remain highlighted.
    • The countdown display is now moved to a text frame. Enjoy!
    • Test Map:
      • Swordsman's hitpoints reduced from 420 to 300.
      • Archer's hitpoints reduced from 310 to 220.
  • v.1.1.0: (Lua)
    • Added a music playlist option.
    • Probably fixed the leave game bug where the victory is not given to the remaining player.
    • If a player who has an ally has left the game, that player is no longer given a victory when the ally wins the game.
    • Added GUI support.
    • Added a countdown timer mechanic when selecting your faction.
      • If you haven't chosen any faction or have not finalized your choice, it will default to the normal race)
      • Also added a warning sound that plays 5 seconds before the system automatically selects the normal race for you.
  • v.1.2.0: Release (vJASS)
    • Revamped Custom UI
      • The units and buildings used in a certain faction can now be previewed, although the mapper is expected to feed the data to the system (just the raw code will do).
      • Players now have a more visible time limit to select their faction.
      • The Custom UI now has a transitional effect when showing or hiding.
      • Defining the image to appear via defRacePic with an empty path will result into the display being hidden whenever that faction is being displayed.
      • The warning sound that plays 5 seconds before the selection ends is removed in favor of the time limit display.
      • An error sound will now play when the confirm button is selected without selecting any faction.
        • This will only happen if the confirm button is not disabled in function DrawUIFromPlayer.
        • Due to the Revamped Custom UI, the following imports must be included/updated:
          • war3mapImported\CustomRaceFrame.fdf
          • war3mapImported\CustomRaceTOC.toc
    • Revamped Observer UI
      • Whether the player is an observer or not, the observer UI frame will now display the list of players that have not yet selected a faction (if more than 1 faction exists within the race the player is currently using). The observer UI frame updates whenever a player clicks on a choice button frame.
      • Depending on the system settings, the faction of choice for each active player who has not chosen yet may also be displayed to other players who already have selected a faction or do not have any other faction options. This is always displayed for observers.
      • If there are more active players who have not chosen yet than the maximum displayable amount of players, a slider will be shown for the observer's convenience. (Applies to maps with more than 8 players).
    • Tournament Compatibility has been introduced.

    • Renamed CustomRaceSystem to CustomRace.

    • Bundle:
      • Removed the code section among the tabs.
  • v.1.2.1: (JASS)
    • (Hotfix) Minor Template Fixes
      • Now worker units are no longer created exactly to the right of the gold mine by default. This fix also applies to the default function incorporated with the CustomRaceTemplate module. Credits to GaLaxY_256 for finding this out.

      • Fixed a problem with default night elf AI not working as intended.
  • v.1.2.2: (JASS)
    • (Hotfix) Text Frame bugfix
      • Hopefully, the text frames that appear upon countdown will no longer grab any space.

Without these guys, this bundle would not have been possible to make.

  • Blizzard - Race Seal Images
  • Tasyen - UI Tutorials and FrameLoader.
  • Hive - Without the Techtree Contests, this wouldn't have been created.
  • Riki - For the music playlist trick
  • GaLaxY_256 - For finding the worker unit displacement bug.
Previews
Contents

Typhoon (Map)

Typhoon (Map)

Level 9
Joined
Feb 7, 2020
Messages
297
This is awesome. It was always clunky going through functional but convoluted command bar pages in the mash-up maps.

I think it's quite grand in its current state, but if you ever wanted a stretch goal: one of the things missing from the race compilations (for me) was unit previews. It wouldn't have to be fancy, but I think with some of the new 1.31+ natives it'd be feasible to fetch unit tooltips (not sure if icons are possible yet), making the end user work somewhat minimal--they just insert the rawcode into a table or something, and icon if needed. I envision the main UI widget being nudged to the left or right and having a column of unit icons that the player can hover over to read about. It could just be a plugin, too.

But, that was just a random idea I had. This current package still makes the mash-up potential so much better.
 
Level 1
Joined
Mar 9, 2010
Messages
6
Hi
This works well on the map you gave us, but when I import to an other map and try to save it i get an error messege.
I imported all the files and the triggers as well.
Can you help solve the problem?
error.jpg

nwN4QBd
 
Last edited:
Level 2
Joined
Oct 29, 2020
Messages
14
This is really cool! I would love to add it into my map but unfortunately I do not use Lua and I have way too many scripts using vJass :(

is there any similar kind of menus for vJass users?
 

MyPad

Spell Reviewer
Level 20
Joined
May 9, 2014
Messages
1,634
This is really cool! I would love to add it into my map but unfortunately I do not use Lua and I have way too many scripts using vJass :(

is there any similar kind of menus for vJass users?

I'm not sure about that. I'll consider adding vJASS compatibility once I have enough time on my hands. (Maybe throw in GUI support as well)
 
Level 1
Joined
Mar 9, 2010
Messages
6
Hi
Can you give me example how to set up an undead like custom race?
I couldn't make it work. I managed the nigth elven one, but for undead like race. Only hall appear, but none of the workers.
Can you tell me why is the ai mostly loose on start?
Will you add an add altar() code and add music() later on?
Thanks in advance.
 

MyPad

Spell Reviewer
Level 20
Joined
May 9, 2014
Messages
1,634
Hello Lusta.

I've copied and pasted the following code from the defaults section of the system:
Lua:
local faction                  = CustomRaceSystem.create(RACE_UNDEAD, "The Fallen Elves")
faction:addHall(<your rawcodes here>)
faction:addHero(<your Hero rawcodes here>)
-- [[
    Some interesting default sections
]]
faction:defSetup(CustomRaceSetup.createSetup(
    CustomRaceSetup.createSetupHelper(function(whichPlayer, hall, mine, peonX, peonY)
        if mine then
            local cx, cy    = GetUnitX(mine), GetUnitY(mine)
            local theta     = math.atan(GetUnitY(hall) - cy, GetUnitX(hall) - cx)
            ShowUnit(hall, false)
            SetUnitPosition(hall, cx + 600*math.cos(theta), cy + 600*math.sin(theta))
            ShowUnit(hall, true)
             IssueTargetOrder(hall, "entangleinstant", mine)
        end
    end, <insert hall and worker rawcodes respectively>[,5]),
    "scripts\\NightElfMelee.pld", faction)
)
faction:defAISetup(function(whichPlayer)
    PickMeleeAI(whichPlayer, "elf.ai", nil, nil)
end)

In your case, it might be likely that setupTable was used instead of the exposed global CustomRaceSetup. Try replacing all instances of setupTable in your script (except that from the system) with CustomRaceSetup.

About the music, I might consider adding it in the future, but for now, a work-around would have to do. Unfortunately, the computer players only pick the default faction, but I might change it in the future.

Lua:
-- Assume that faction exists as an upvalue, and is defined
local faction
local coreSetup = CustomRaceSetup.createSetup(
    CustomRaceSetup.createSetupHelper(function(whichPlayer, hall, mine, peonX, peonY)
        if mine then
            local cx, cy    = GetUnitX(mine), GetUnitY(mine)
            local theta     = math.atan(GetUnitY(hall) - cy, GetUnitX(hall) - cx)
            ShowUnit(hall, false)
            SetUnitPosition(hall, cx + 600*math.cos(theta), cy + 600*math.sin(theta))
            ShowUnit(hall, true)
             IssueTargetOrder(hall, "entangleinstant", mine)
        end
    end, 'etol', 'ewsp', 5),
    "scripts\\NightElfMelee.pld", faction)
)

faction:defSetup(function(whichPlayer, startLoc, doHeroes, doCamera, doPreload)
    coreSetup(whichPlayer, startLoc, doHeroes, doCamera, doPreload)
    if GetLocalPlayer() == whichPlayer then
        -- Do some music stuff here.
    end
end)

EDIT:
Music playlist feature added
 
Last edited:
Level 1
Joined
Mar 9, 2010
Messages
6
Hello MyPad
Thanks for the reply.
But you misunderstand. I managed to get nigth elfs working.
I can't make an undead faction that has to worker types (Acolyte, Ghoul) and a Haunted Gold Mine.
Only the Necropolis spawns, without units.
 
Level 1
Joined
Mar 9, 2010
Messages
6
Well I can, but models and sounds are gona be missing for lot of others stuff, becouse local files.
Problem is at undead Scourge of Lordearon faction. I am useing a copy of the undead units with original models.
Rest of the factions are working as intended.
Sorry for the late reply, internet connection decided to mess with me.
Thanks
 

Attachments

  • (L)Northshire2.w3x
    1.4 MB · Views: 25

MyPad

Spell Reviewer
Level 20
Joined
May 9, 2014
Messages
1,634
Turns out it was an attempt at referencing setupTable which caused the function to fail.
Try copy and pasting this script into your Scourge of Lordaeron script:

Lua:
do
    local elvens = CustomRaceSystem.create(RACE_UNDEAD, "Scourge of Lordearon")
    elvens:defDescription("After preparing for many long months, Kel'Thuzad and his Cult of the Damned finally struck the first blow by releasing the plague of undeath upon Lordaeron. Uther and his fellow paladins investigated the infected regions in the hope of finding a way to stop the plague. Despite their efforts, the plague continued to spread and threatened to tear the Alliance apart.")
    elvens:addHall('u005:unpl')
    elvens:addHall('u006:unp1')
    elvens:addHall('u007:unp2')
    elvens:addHero('U00W:Uktl')
    elvens:defRacePic("war3mapImported\\undeadseal.blp")
    elvens:defSetup(CustomRaceSetup.createSetup(
        CustomRaceSetup.createSetupHelper(function(whichPlayer, hall, mine, peonX, peonY)
            local cx    = peonX + 1.00*CustomRaceSetup.unitSpacing
            local cy    = peonY + 0.00*CustomRaceSetup.unitSpacing
            if mine and GetUnitTypeId(mine) == FourCC('ngol') then
                local mineX, mineY  = GetUnitX(mine), GetUnitY(mine)
                local mineGold      = GetResourceAmount(mine)
                local theta         = math.atan(GetUnitY(hall)-mineY, GetUnitX(hall)-mineX)
                cx                  = mineX + 288*math.cos(theta)
                cy                  = mineY + 288*math.sin(theta)
                RemoveUnit(mine)
                mine            = CreateBlightedGoldmine(whichPlayer, mineX, mineY, bj_UNIT_FACING)
                SetResourceAmount(mine, mineGold)
                ShowUnit(mine, false)
                SetUnitPosition(mine, mineX, mineY)
                ShowUnit(mine, true)
            end
            bj_ghoul[GetPlayerId(whichPlayer)]  = CreateUnit(whichPlayer, FourCC('u00B'), cx, cy, bj_UNIT_FACING)
        end, 'u005', 'u004', 3), "scripts\\UndeadMelee.pld", elvens))
    elvens:defAISetup(function(whichPlayer)
        PickMeleeAI(whichPlayer, "undead.ai", nil, nil)
        RecycleGuardPosition(bj_ghoul[GetPlayerId(whichPlayer)])
    end)
end
 

MyPad

Spell Reviewer
Level 20
Joined
May 9, 2014
Messages
1,634
Can't believe it took me this long, but the vJASS version is finally released with GUI compatibility right out of the box. As a result of this, the way factions are created in the vJASS version should be expected to be a much smoother experience than the original Lua version. Due to focusing on writing this system for vJASS, the Lua version is currently outdated, but I hope I can update that version as well.

Here are some previews on what the current UI frame looks like:

Default UI Appearance:
1630900940849.png


UI (A faction is hovered at):
1630901030642.png


UI (with tooltip):
1630901076913.png


Revamped Observer UI:

As a result of writing the updated version of the system in vJASS, some spells in the High Elves faction might not work as expected.
Now, for the complete changelog for this version:

  • Revamped Custom UI
    • The units and buildings used in a certain faction can now be previewed, although the mapper is expected to feed the data to the system (just the raw code will do).

    • Players now have a more visible time limit to select their faction. If the time limit runs out, the default race will be selected for them.
    • The Custom UI now has a transitional effect when showing or hiding.

    • Defining the image to appear via defRacePic with an empty path will result into the display being hidden whenever that faction is being displayed.

    • The warning sound that plays 5 seconds before the selection ends is removed in favor of the time limit display.

    • An error sound will now play when the confirm button is selected without selecting any faction.
      • This will only happen if the confirm button is not disabled in function DrawUIFromPlayer.
    • Due to the Revamped Custom UI, the following imports must be included/updated:
      • war3mapImported\CustomRaceFrame.fdf

      • war3mapImported\CustomRaceTOC.toc
  • Revamped Observer UI
    • Whether the player is an observer or not, the observer UI frame will now display the list of players that have not yet selected a faction (if more than 1 faction exists within the race the player is currently using). The observer UI frame updates whenever a player clicks on a choice button frame.
      • Depending on the system settings, the faction of choice for each active player who has not chosen yet may also be displayed to other players who already have selected a faction or do not have any other faction options. This is always displayed for observers.

      • If there are more active players who have not chosen yet than the maximum displayable amount of players, a slider will be shown for
        the observer's convenience. (Applies to maps with more than 8 players).
  • Tournament Compatibility has been introduced.
 
Level 13
Joined
Mar 21, 2014
Messages
893
Finally vJass version! I've been keeping an eye on this thread and I think it is worth the wait. It looks better as well!👍

And by the way, how can I make it work together with Tasyen's Custom UI? I'm not that good at triggers so I'd be happy if you can help me. :)

Edit: I dunno why but my custom workers are not spawned at their starting location, instead slightly far away from the hall. Also, music playlist doesn't seem to be working, does it support imported mp3 or not?🤔
 
Last edited:

MyPad

Spell Reviewer
Level 20
Joined
May 9, 2014
Messages
1,634
Finally vJass version! I've been keeping an eye on this thread and I think it is worth the wait. It looks better as well!👍

And by the way, how can I make it work together with Tasyen's Custom UI? I'm not that good at triggers so I'd be happy if you can help me. :)

Edit: I dunno why but my custom workers are not spawned at their starting location, instead slightly far away from the hall. Also, music playlist doesn't seem to be working, does it support imported mp3 or not?🤔

AFAIK, this should work independently of CustomConsoleUI, though I'll check it out anyway if needed. So, there's no need to worry about it conflicting with that, unless there are some frame names that might cause problems down the line.

It should be capable of supporting music playlists. Also, the default behavior for spawning units is based on how it's done internally in regular melee games (particularly Human race), but this can be specifically changed to the user's liking.

Anyway, may I ask for the relevant script/trigger so that I can help?
 

HKB

HKB

Level 2
Joined
Jun 17, 2012
Messages
12
Great race system. I'm also making a new race map. Unfortunately, the Warcraft Version I use is 1.24-1.27. I can't use it. I'm very sad.
 

MyPad

Spell Reviewer
Level 20
Joined
May 9, 2014
Messages
1,634
Looks like another bug on my part. I used nightelf.ai instead of elf.ai, which might've caused the bug.
Luckily, the fix to that is simple. Just copy and paste to the following (in CustomRaceDefault)

JASS:
// Replace nightelf in the third line with elf.
//! runtextmacro CRDefault_DEF_SETUP("Human", "human")
//! runtextmacro CRDefault_DEF_SETUP("Orc", "orc")
//! runtextmacro CRDefault_DEF_SETUP("NightElf", "elf")

I'll update the JASS version with the fix.
 
Level 13
Joined
Mar 21, 2014
Messages
893
Hi, I don't know if this only happen to me or not, but there is a specific spot on screen where I can't click anything. I attached a screenshot so you can see where it is. Screenshot is taken from JASS template, where Night Elf doesn't have additional races and thus UI doesn't pop up. My guess is it has something to do with UI setup.
image_2021-09-15_185259.png
 

MyPad

Spell Reviewer
Level 20
Joined
May 9, 2014
Messages
1,634
Perhaps it might've been the text frames that appear when the match actually begins (not at the selection point). Nevertheless, this may hopefully resolve that issue.

Note: Replace the contents of the Custom Race Match trigger with the following:
JASS:
library CustomRaceMatch requires /*
    --------------------------
    */  CustomRaceCore,     /*
    --------------------------
    --------------------------
    */  CustomRaceUI,       /*
    --------------------------
    ------------------------------
    */  CustomRacePSelection,   /*
    ------------------------------
    ----------------------------------
    */  CustomRaceMatchConditions,  /*
    ----------------------------------
    ------------------------------
    */  optional Init           /*
    ------------------------------
*/
globals
    private constant boolean FOGGED_START                   = false
    private constant boolean USE_EXTRA_TICK                 = true
    public  constant boolean APPLY_TIMER_IN_SINGLE_PLAYER   = false
    
    private constant integer GAME_START_TICKS               = 3
    private constant integer EXTRA_TICK_FOR_START           = 1
    private constant real    TICK_INTERVAL                  = 1.0
    private constant real    DISPLAY_LIFETIME               = 0.80
    private constant real    DISPLAY_INTERVAL               = 1.0 / 100.0
endglobals
//  =============================================================================   //
private function ClearMusicPlaylist takes nothing returns nothing
    //  Observers can't play any faction playlist,
    //  so return at this point. Comment later
    //  if this causes desyncs.
    if IsPlayerObserver(GetLocalPlayer()) then
        return
    endif
    call ClearMapMusic()
    call StopMusic(false)
endfunction
//  =============================================================================   //
//  =============================================================================   //
//  In previous versions, visibility was actually affected.
//  In modern versions, visibility is kept intact and only
//  the time of day is affected.
//  =============================================================================   //
private function StartingVisibility takes nothing returns nothing
    call SetFloatGameState(GAME_STATE_TIME_OF_DAY, bj_MELEE_STARTING_TOD)
    call SuspendTimeOfDay(true)
    static if FOGGED_START then
        call FogMaskEnable(true)
        call FogEnable(true)
    endif
endfunction
//  =============================================================================   //
//  =============================================================================   //
private function StartingHeroLimit takes nothing returns nothing
    local integer index         = 0
    local integer i             = 1
    local integer maxHeroIndex  = CustomRace.getGlobalHeroMaxIndex()
    local player whichPlayer
    loop
        exitwhen index > bj_MAX_PLAYERS
        set whichPlayer = Player(index)
        set i           = 1
        call SetPlayerTechMaxAllowed(whichPlayer, 'HERO', bj_MELEE_HERO_LIMIT)
        loop
            exitwhen i > maxHeroIndex
            call SetPlayerTechMaxAllowed(whichPlayer, CustomRace.getGlobalHero(i), /*
                                      */ bj_MELEE_HERO_TYPE_LIMIT)
            set i   = i + 1
        endloop
        set index   = index + 1
    endloop
endfunction
//  =============================================================================   //
//  =============================================================================   //
private function GrantItem takes unit hero returns nothing
    if IsUnitType(hero, UNIT_TYPE_HERO) then
        call MeleeGrantItemsToHero(hero)
    endif
endfunction
private function OnNeutralHeroHired takes nothing returns nothing
    call GrantItem(GetSoldUnit())
endfunction
private function OnTrainedHeroFinish takes nothing returns nothing
    call GrantItem(GetTrainedUnit())
endfunction
private function GrantHeroItems takes nothing returns nothing
    local integer index         = 0
    local trigger trig          = CreateTrigger()
    local player whichPlayer    = null
    call TriggerAddAction(trig, function OnTrainedHeroFinish)
    loop
        exitwhen index > bj_MAX_PLAYER_SLOTS
        // Initialize the twinked hero counts.
        set bj_meleeTwinkedHeroes[index]    = 0
        set whichPlayer                     = Player(index)
        
        // Register for an event whenever a hero is trained, so that we can give
        // him/her their starting items. Exclude
        if (index < bj_MAX_PLAYERS) and CustomRaceMatchConditions_IsPlayerActive(whichPlayer) then
            call TriggerRegisterPlayerUnitEvent(trig, whichPlayer, /*
                                             */ EVENT_PLAYER_UNIT_TRAIN_FINISH, null)
        endif
        set index                           = index + 1
    endloop
    // Register for an event whenever a neutral hero is hired, so that we
    // can give him/her their starting items.
    set trig = CreateTrigger()
    call TriggerRegisterPlayerUnitEvent(trig, Player(PLAYER_NEUTRAL_PASSIVE), /*
                                     */ EVENT_PLAYER_UNIT_SELL, null)
    call TriggerAddAction(trig, function OnNeutralHeroHired)
    // Flag that we are giving starting items to heroes, so that the melee
    // starting units code can create them as necessary.
    set bj_meleeGrantHeroItems = true
endfunction
//  =============================================================================   //
//  =============================================================================   //
private function StartingResources takes nothing returns nothing
    local integer index
    local player  whichPlayer
    local version v
    local integer startingGold      = bj_MELEE_STARTING_GOLD_V1
    local integer startingLumber    = bj_MELEE_STARTING_LUMBER_V1
    set v = VersionGet()
    if (v == VERSION_REIGN_OF_CHAOS) then
        set startingGold = bj_MELEE_STARTING_GOLD_V0
        set startingLumber = bj_MELEE_STARTING_LUMBER_V0
    endif
    // Set each player's starting resources.
    set index = 0
    loop
        set whichPlayer = Player(index)
        if CustomRaceMatchConditions_IsPlayerActive(whichPlayer) then
            call SetPlayerState(whichPlayer, PLAYER_STATE_RESOURCE_GOLD, startingGold)
            call SetPlayerState(whichPlayer, PLAYER_STATE_RESOURCE_LUMBER, startingLumber)
        endif
        set index = index + 1
        exitwhen index == bj_MAX_PLAYERS
    endloop
endfunction
//  =============================================================================   //
//  =============================================================================   //
private function RemoveNearbyUnits takes real x, real y, real radius returns nothing
    local integer i         = 0
    local integer owner     = 0
    local integer size      = 0
    local group nearbyUnits = CreateGroup()
    local unit  enumUnit
    call GroupEnumUnitsInRange(nearbyUnits, x, y, radius, null)
    set size        = BlzGroupGetSize(nearbyUnits)
    loop
        exitwhen i >= size
        set enumUnit    = BlzGroupUnitAt(nearbyUnits, i)
        set owner       = GetPlayerId(GetOwningPlayer(enumUnit))
        if (owner == PLAYER_NEUTRAL_AGGRESSIVE) or /*
        */ ((owner == PLAYER_NEUTRAL_PASSIVE) and /*
        */  (not IsUnitType(enumUnit, UNIT_TYPE_STRUCTURE))) then
            // Remove any Neutral Hostile units or
            // Neutral Passive units (not structures) from the area.
            call RemoveUnit(enumUnit)
        endif
        set i   = i + 1
    endloop
    call DestroyGroup(nearbyUnits)
    set enumUnit    = null
    set nearbyUnits = null
endfunction
private function ClearExcessUnits takes nothing returns nothing
    local integer index         = 0
    local real    locX
    local real    locY
    local player  indexPlayer
    loop
        set indexPlayer = Player(index)
        // If the player slot is being used, clear any nearby creeps.
        if CustomRaceMatchConditions_IsPlayerActive(indexPlayer) then
            set locX = GetStartLocationX(GetPlayerStartLocation(indexPlayer))
            set locY = GetStartLocationY(GetPlayerStartLocation(indexPlayer))
            call RemoveNearbyUnits(locX, locY, bj_MELEE_CLEAR_UNITS_RADIUS)
        endif
        set index = index + 1
        exitwhen index == bj_MAX_PLAYERS
    endloop
endfunction
//  =============================================================================   //
//  =============================================================================   //
private function DefineVictoryDefeat takes nothing returns nothing
    //  Unravelling this function will open a can of worms
    //  the likes which would not likely be appreciated.
    //  Leave it as it is, and make changes in a separate
    //  library specifically for this function.
    call CustomRaceMatchConditions_DefineVictoryDefeat()
endfunction
//  =============================================================================   //
//  =============================================================================   //
private function OnStartCheckAlliance takes nothing returns nothing
    local timer whichTimer  = GetExpiredTimer()
    call PauseTimer(whichTimer)
    call DestroyTimer(whichTimer)
    call CustomRaceMatchConditions_OnAllianceChange()
endfunction
public  function TestVictoryDefeat takes nothing returns nothing
    // Test for victory / defeat at startup, in case the user has already won / lost.
    // Allow for a short time to pass first, so that the map can finish loading.
    call TimerStart(CreateTimer(), 2.0, false, function OnStartCheckAlliance)
endfunction
//  =============================================================================   //
//  =============================================================================   //
globals
    private integer  tempStart          = 0
    private location tempStartLoc       = null
    private player   tempStartPlayer    = null
endglobals
public  function OnStartGetPlayer takes nothing returns player
    return tempStartPlayer
endfunction
public  function OnStartGetLoc takes nothing returns location
    return tempStartLoc
endfunction
private function StartingUnits takes nothing returns nothing
    local integer  index            = 1
    local CustomRacePSelection obj  = 0
    local CustomRace faction        = 0
    local player   indexPlayer
    local race     pRace
    call Preloader( "scripts\\SharedMelee.pld" )
    loop
        exitwhen index > CustomRacePSelection.unchoicedPlayerSize
        set indexPlayer     = CustomRacePSelection.unchoicedPlayers[index]
        set tempStartPlayer = indexPlayer
        set tempStart       = GetPlayerStartLocation(indexPlayer)
        set tempStartLoc    = GetStartLocationLoc(tempStart)
        set pRace           = GetPlayerRace(indexPlayer)
        set obj             = CRPSelection[indexPlayer]
        set faction         = CustomRace.getRaceFaction(pRace, obj.faction)
        
        call faction.execSetup()
        if GetPlayerController(indexPlayer) == MAP_CONTROL_COMPUTER then
            call faction.execSetupAI()
        endif
        call RemoveLocation(tempStartLoc)
        set index = index + 1
    endloop
    //  Do NOT make these usable afterwards!
    set tempStartPlayer = null
    set tempStart       = 0
    set tempStartLoc    = null
endfunction
//  =============================================================================   //
//  =============================================================================   //
private struct FrameInterpolation
    private static  constant real FRAME_SCALE       = 10.0
    private static  constant real FRAME_ENDSCALE    = 2.0
    private static  constant real START_X           = 0.40
    private static  constant real END_X             = 0.40
    private static  constant real START_Y           = 0.45
    private static  constant real END_Y             = 0.25
    private static  constant framepointtype POINT   = FRAMEPOINT_CENTER
    
    private static  thistype array objectList
    private static  integer objectCurIndex          = 0
    private static  timer   interpolator            = CreateTimer()
    private string  message
    private integer maxTicks
    private integer ticks
    private framehandle frame
    private static method alphaResponse takes real x returns real
        set x   = x - 0.5
        return -16.0*(x*x*x*x) + 1.0
    endmethod
    private static method slideResponse takes real x returns real
        set x   = x - 0.5
        return -4.0*(x*x*x) + 0.5
    endmethod
    private static method scaleResponse takes real x returns real
        return -(x*x*x*x*x*x) + 1.0
    endmethod
    private method destroy takes nothing returns nothing
        set this.ticks      = 0
        set this.maxTicks   = 0
        call BlzFrameSetVisible(this.frame, false)
        call BlzDestroyFrame(this.frame)
        call this.deallocate()
    endmethod
    private static method onUpdate takes nothing returns nothing
        local integer i     = 1
        local thistype this = 0
        local real ratio    = 0.0
        local real resp     = 0.0
        local real cx       = 0.0
        local real cy       = 0.0
        local real scale    = 0.0
        loop
            exitwhen i > objectCurIndex
            set this        = objectList[i]
            set this.ticks  = this.ticks + 1
            set ratio       = I2R(this.ticks) / I2R(this.maxTicks)
            call BlzFrameSetAlpha(this.frame, R2I(255.0*thistype.alphaResponse(ratio)))
            set resp        = slideResponse(ratio)
            set cx          = START_X*resp + END_X*(1-resp)
            set cy          = START_Y*resp + END_Y*(1-resp)
            set resp        = scaleResponse(ratio)
            set scale       = FRAME_SCALE*resp + FRAME_ENDSCALE*(1-resp)
            call BlzFrameSetAbsPoint(this.frame, POINT, cx, cy)
            call BlzFrameSetScale(this.frame, scale)
            if this.ticks >= this.maxTicks then
                set objectList[i]   = objectList[objectCurIndex]
                set objectCurIndex  = objectCurIndex - 1
                set i               = i - 1
                call this.destroy()
            endif
            set i = i + 1
        endloop
        if objectCurIndex < 1 then
            call PauseTimer(interpolator)
        endif
    endmethod
    private static method insert takes thistype this returns nothing
        set objectCurIndex              = objectCurIndex + 1
        set objectList[objectCurIndex]  = this
        if objectCurIndex == 1 then
            call TimerStart(interpolator, DISPLAY_INTERVAL, true, function thistype.onUpdate)
        endif
    endmethod
    static method request takes string msg, real lifetime returns nothing
        local thistype this = thistype.allocate()
        set this.message    = msg
        set this.maxTicks   = R2I(lifetime / DISPLAY_INTERVAL + 0.01)
        set this.ticks      = 0
        set this.frame      = BlzCreateFrameByType("TEXT", "CustomRaceMatchDisplayText", /*
                                                */ BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0), /*
                                                */ "", integer(this))
        call BlzFrameSetText(this.frame, message)
        call BlzFrameSetScale(this.frame, FRAME_SCALE)
        call BlzFrameSetAlpha(this.frame, 0)
        call BlzFrameSetAbsPoint(this.frame, POINT, START_X, START_Y)
        call thistype.insert(this)
    endmethod
endstruct
private function DisplayToWorld takes string msg, real lifetime returns nothing
    call FrameInterpolation.request(msg, lifetime)
endfunction
//  =============================================================================   //
//  =============================================================================   //
globals
    private integer beginTick   = 0
    private integer extraTick   = 0
    private group   tickGroup   = null
    private sound   tempSound   = null
endglobals
private function GenerateTickSound takes nothing returns sound
    set tempSound   = CreateSound( "Sound\\Interface\\BattleNetTick.wav", false, false, false, 10, 10, "" )
    call SetSoundParamsFromLabel( tempSound, "ChatroomTimerTick" )
    call SetSoundDuration( tempSound, 476 )
    return tempSound
endfunction
private function GenerateHornSound takes nothing returns sound
    set tempSound   = CreateSound( "Sound\\Ambient\\DoodadEffects\\TheHornOfCenarius.wav", false, false, false, 10, 10, "DefaultEAXON" )
    call SetSoundParamsFromLabel( tempSound, "HornOfCenariusSound" )
    call SetSoundDuration( tempSound, 12120 )
    return tempSound
endfunction
private function PlaySoundForPlayer takes sound snd, player p returns nothing
    if GetLocalPlayer() != p then
        call SetSoundVolume(snd, 0)
    endif
    call StartSound(snd)
    call KillSoundWhenDone(snd)
endfunction
//  =============================================================================   //
//  =============================================================================   //
private function SetupPlaylist takes nothing returns nothing
    local player whichPlayer        = GetLocalPlayer()
    local CustomRacePSelection obj  = CRPSelection[whichPlayer]
    local CustomRace faction        = CustomRace.getRaceFaction(GetPlayerRace(whichPlayer), obj.faction)
    if faction == 0 then
        return
    endif
    call SetMapMusic(faction.playlist, true, 0)
    call PlayMusic(faction.playlist)
endfunction
private function ResetVisuals takes nothing returns nothing
    call EnableDragSelect(true, true)
    call EnablePreSelect(true, true)
    call EnableSelect(true, true)
    call EnableUserControl(true)
    call EnableUserUI(true)
    call SuspendTimeOfDay(false)
endfunction
private function MatchTickDown takes nothing returns nothing
    local integer i     = 0
    local integer size  = 0
    set beginTick       = beginTick - 1
    if beginTick > 0 then
        call StartSound(GenerateTickSound())
        call KillSoundWhenDone(tempSound)
        call DisplayToWorld(I2S(beginTick), DISPLAY_LIFETIME)
        return
    endif
    set extraTick       = extraTick - 1
    if extraTick > 0 then
        return
    endif
    call StartSound(GenerateHornSound())
    call KillSoundWhenDone(tempSound)
    call DisplayToWorld("|cffff4040Start!|r", 1.20)
    
    call PauseTimer(GetExpiredTimer())
    call DestroyTimer(GetExpiredTimer())
    call TestVictoryDefeat()
    call ResetVisuals()
    call SetupPlaylist()
    set size            = BlzGroupGetSize(tickGroup)
    loop
        exitwhen i >= size
        call PauseUnit(BlzGroupUnitAt(tickGroup, i), false)
        set i = i + 1
    endloop
endfunction
private function SetupVisuals takes nothing returns nothing
    local real zdist    = GetCameraField(CAMERA_FIELD_TARGET_DISTANCE)
    local real ndist    = zdist + 1250.0
    local real dur      = (GAME_START_TICKS)*TICK_INTERVAL
    if IsPlayerInForce(GetLocalPlayer(), CustomRaceForce.activePlayers) then
        call SetCameraField(CAMERA_FIELD_TARGET_DISTANCE, ndist, 0.00)
        call SetCameraField(CAMERA_FIELD_TARGET_DISTANCE, zdist, 0.00)
    endif
    call EnableDragSelect(false, false)
    call EnablePreSelect(false, false)
    call EnableSelect(false, false)
    call EnableUserControl(false)
    call EnableUserUI(false)
endfunction
private function BeginMatch takes nothing returns nothing
    local rect world    = GetWorldBounds()
    local integer i     = 0
    local integer size  = 0
    set tickGroup       = CreateGroup()
    set beginTick       = GAME_START_TICKS + 1
    if USE_EXTRA_TICK then
        set extraTick   = EXTRA_TICK_FOR_START
    endif
    call TimerStart(CreateTimer(), TICK_INTERVAL, true, function MatchTickDown)
    call SetupVisuals()
    call GroupEnumUnitsInRect(tickGroup, world, null)
    set size            = BlzGroupGetSize(tickGroup)
    loop
        exitwhen i >= size
        call PauseUnit(BlzGroupUnitAt(tickGroup, i), true)
        set i = i + 1
    endloop
endfunction
//  =============================================================================   //
//  =============================================================================   //
public function MeleeInitialization takes nothing returns nothing
    call ClearMusicPlaylist()
    call StartingVisibility()
    call StartingHeroLimit()
    call GrantHeroItems()
    call StartingResources()
    call ClearExcessUnits()
endfunction
public function MeleeInitializationFinish takes nothing returns nothing
    call DefineVictoryDefeat()
    call StartingUnits()
    call BeginMatch()
endfunction
endlibrary
 
Is it normal that when I start the game, there's no UI showing the race pick ?:ogre_rage:
The countdown starts immediately, then the game goes normally with no bugs.
I just imported everything without touching anything like you wrote ; I just disabled the vanilla melee initialization.
I'm using the Jass version, since the map I'm doing is in JASS.
I got the last Reforged version.
 

MyPad

Spell Reviewer
Level 20
Joined
May 9, 2014
Messages
1,634
Is it normal that when I start the game, there's no UI showing the race pick ?:ogre_rage:
The countdown starts immediately, then the game goes normally with no bugs.
I just imported everything without touching anything like you wrote ; I just disabled the vanilla melee initialization.
I'm using the Jass version, since the map I'm doing is in JASS.
I got the last Reforged version.
If there are no human players whose race has at least 2 factions, then that behaviour is to be expected.

This system is designed to facilitate the selection of custom factions. The implementation/design of custom factions is all up to the user.
 
Top