• 🏆 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!
  • 🏆 Hive's 6th HD Modeling Contest: Mechanical is now open! Design and model a mechanical creature, mechanized animal, a futuristic robotic being, or anything else your imagination can tinker with! 📅 Submissions close on June 30, 2024. Don't miss this opportunity to let your creativity shine! Enter now and show us your mechanical masterpiece! 🔗 Click here to enter!

[vJASS] HostDetection (GetHost)

Introduction

For the longest time map developers have wanted the ability to find out who the host is. Maps would generally mark Player 1 (Red) as the host, but that has it's flaws. You either need to create a new team specifically for the host, or just hope that the players know that Player 1 (Red) picks the game settings, or whatever functionality is host-specific.

In the past developers would try to measure latency with the game cache natives and whoever had the lowest latency was marked as the host. That method was inaccurate, but it also no longer works as of patch 1.30. Blizzard's servers are the real hosts now, not the player, so latency is much more variable. The method used in this script doesn't rely on latency.

Explanation

Patch 1.31 introduced natives to manipulate the game's frame data (UI). It turns out some of these natives also work inside of the game lobby. I figured this could be used in some way to detect the host. Unfortunately you cannot get a frame's text from inside the lobby, you can only set it. This means we can't simply get the text of the "Game Creator" frame to detect the host like I initially planned. However, I noticed a frame that existed for everyone but the host. BlzGetFrameByName("NameMenu", 1) returns null only for the host. I'm not entirely sure why, but I suspect the frame doesn't exist for the host until after the map's data has been parsed. When joining a lobby the frame must exist before the map data is parsed.

Knowing this I could inject some code into the map's config function and use that to detect the host. Although, the problem with injecting code into the config function that every time you make an update to your map's settings, you also have to update the config function. I wanted a solution where you can simply copy a script, use it, and forget about it. Then, I realized that code from within globals blocks are executed in the lobby as well. That's when I came up with

JASS:
globals
    boolean LocalHostFlag = (BlzGetFrameByName("NameMenu", 1) == null)
endglobals
There are a couple issues with the above code though.
  1. The map desyncs because we are creating a frame handle for everyone except for the host.
  2. It runs twice. Once at the lobby (config) and once at the loading screen. During the loading screen it is null for everyone.
The solution was to store the host in a hashtable in the lobby and not to overwrite the global during the loading screen if the value was found in the hashtable.

As for fixing the desync, we just need to make the handle counter the same for everyone, so simply calling Location(0,0) for the host would accomplish that. So, the code now becomes

JASS:
globals
    boolean LocalHostFlag = not HaveSavedString(HostDetectHT, 0, 0) and (BlzGetFrameByName("NameMenu", 1) == null) and Location(0, 0) != null and SaveStr(HostDetectHT, 0, 0, "1")
endglobals
All that's left is syncing the value to the rest of the players so they know who the host is. This system handles all of it automatically.

Script

JASS:
library HostDetection initializer Init
/***************************************************************
*
*   v1.0.0, by TriggerHappy
*   ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
*   Detect which player hosted the current match.
*   _________________________________________________________________________
*   1. Requirements
*   ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
*       - Patch 1.31 or higher.
*       - JassHelper (vJASS)
*   _________________________________________________________________________
*   2. Installation
*   ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
*   Copy the script to your map and save it.
*   _________________________________________________________________________
*   3. API
*   ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
*       function GetHost takes nothing returns boolean
*       function IsHostDetected nothing returnrs boolean
*       function IsLocalPlayerHost nothing returnrs boolean
*
*       function OnHostDetect takes code callback returns triggeraction
*       function RemoveHostDetect takes triggeraction action returns nothing
*
***************************************************************/

    globals
        private hashtable HostDetectHT = InitHashtable()
        private boolean LocalHostFlag = not HaveSavedString(HostDetectHT, 0, 0) and (BlzGetFrameByName("NameMenu", 1) == null) and Location(0, 0) != null and SaveStr(HostDetectHT, 0, 0, "1")
        private trigger SyncTrig = CreateTrigger()
        private trigger EventTrig = CreateTrigger()
        private player HostPlayer = null
    endglobals
  
    function GetHost takes nothing returns player
        return HostPlayer
    endfunction
  
    function IsHostDetected takes nothing returns boolean
        return HostPlayer != null
    endfunction
  
    function IsLocalPlayerHost takes nothing returns boolean // async
        return LocalHostFlag
    endfunction
  
    function OnHostDetect takes code callback returns triggeraction
        return TriggerAddAction(EventTrig, callback)
    endfunction
  
    function RemoveHostDetect takes triggeraction action returns nothing
        call TriggerRemoveAction(EventTrig, action)
    endfunction
  
    private function OnHostSync takes nothing returns nothing
        set HostPlayer = GetTriggerPlayer()
        call TriggerExecute(EventTrig)
        call DisableTrigger(GetTriggeringTrigger())
    endfunction

    private function Init takes nothing returns nothing
        local integer i = 0
        loop
            exitwhen i > bj_MAX_PLAYERS
            call BlzTriggerRegisterPlayerSyncEvent(SyncTrig, Player(i), "hostdetect", false)
            set i = i + 1
        endloop
        call TriggerAddAction(SyncTrig, function OnHostSync)
        if (IsLocalPlayerHost()) then
            call BlzSendSyncData("hostdetect", "1")
        endif
    endfunction
  
endlibrary
 
Last edited:
JASS:
globals
    boolean LocalHostFlag = (BlzGetFrameByName("NameMenu", 1) == null)
endglobals
There are a couple issues with the above code though.
  1. The map desyncs because we are creating a frame handle for everyone except for the host.
  2. It runs twice. Once at the lobby (config) and once at the loading screen. During the loading screen it is null for everyone.
....
JASS:
globals
    hashtable HostDetectHT = InitHashtable()
    boolean LocalHostFlag = not HaveSavedString(HostDetectHT, 0, 0) and (BlzGetFrameByName("NameMenu", 1) == null) and Location(0, 0) != null and SaveStr(HostDetectHT, 0, 0, "1")
endglobals
Isn't the prior hashtable (with entry) also lost in the second solution if the code, so the initialization, runs twice?
 
Isn't the prior hashtable (with entry) also lost in the second solution if the code, so the initialization, runs twice?

You would think so but no.

JASS:
//!inject config
    // ...
    call SaveStr(HT, 1, 1, "config") // this only runs once at lobby, confirmed with Preload natives
//! endinject

//! inject main
    // ...
    if (not HaveSavedString(HT, 1, 1) then
        call SaveStr(HT, 1, 1, "main")
    endif
    call BJDebugMsg(LoadStr(HT, 1, 1)) // displays config
//! endinject
 
Level 9
Joined
Mar 26, 2017
Messages
376
This code is really nice for my map, as I would like to have the host be able to put game modes/settings at game start.

I have tried converting this to lua, but unfortunately failed.
I put the 'globals' statements in main, right after the InitGlobals line.

Lua:
       HostDetectHT = InitHashtable()
       LocalHostFlag = not HaveSavedString(HostDetectHT, 0, 0) and (BlzGetFrameByName("NameMenu", 1) == nil) and Location(0, 0) ~= nil and SaveStr(HostDetectHT, 0, 0, "1")
       SyncTrig = CreateTrigger()
       EventTrig = CreateTrigger()


Putting in main does create a LocalHostFlag, but it is set to false.
Running the JASS code works well, but my lua implementation finds the "NameMenu" frame by the time the LocalHostFlag is set (which is right after InitGlobals). I don't know vJASS well, but I expect the 'globals' part to be ran from the InitGlobals function?
Perhaps you have an idea why this won't work?


war3map.lua
Lua:
gg_trg_Melee_Initialization = nil
gg_trg_Untitled_Trigger_001 = nil
function InitGlobals()
end

    function GetHost()
        return HostPlayer
    end
 
    function IsHostDetected()
        return HostPlayer ~= nil
    end
 
    function IsLocalPlayerHost()
        return LocalHostFlag
    end
 
    function OnHostDetect()
        return TriggerAddAction(EventTrig, callback)
    end
 
    function RemoveHostDetect(action)
        TriggerRemoveAction(EventTrig, action)
    end
 
    function OnHostSync()
        HostPlayer = GetTriggerPlayer()
        TriggerExecute(EventTrig)
        DisableTrigger(GetTriggeringTrigger())
    print("hostsync")
    end

    function Init()


    BJDebugMsg("init")
        for i=0,11 do
            BlzTriggerRegisterPlayerSyncEvent(SyncTrig, Player(i), "hostdetect", false)
        end
        TriggerAddAction(SyncTrig, OnHostSync)
        if (IsLocalPlayerHost()) then
            BlzSendSyncData("hostdetect", "1")
        end
    BJDebugMsg("endinit")
    end
 

function Trig_Melee_Initialization_Actions()
        Init()
end

function InitTrig_Melee_Initialization()
    gg_trg_Melee_Initialization = CreateTrigger()
    TriggerAddAction(gg_trg_Melee_Initialization, Trig_Melee_Initialization_Actions)
end

function Trig_Untitled_Trigger_001_Actions()
        print(LocalHostFlag)
        print(GetPlayerName(HostPlayer))
        print("end")
end

function InitTrig_Untitled_Trigger_001()
    gg_trg_Untitled_Trigger_001 = CreateTrigger()
    TriggerRegisterTimerEventSingle(gg_trg_Untitled_Trigger_001, 2.00)
    TriggerAddAction(gg_trg_Untitled_Trigger_001, Trig_Untitled_Trigger_001_Actions)
end

function InitCustomTriggers()
    InitTrig_Melee_Initialization()
    InitTrig_Untitled_Trigger_001()
end

function RunInitializationTriggers()
    ConditionalTriggerExecute(gg_trg_Melee_Initialization)
end

function InitCustomPlayerSlots()
    SetPlayerStartLocation(Player(0), 0)
    SetPlayerColor(Player(0), ConvertPlayerColor(0))
    SetPlayerRacePreference(Player(0), RACE_PREF_HUMAN)
    SetPlayerRaceSelectable(Player(0), true)
    SetPlayerController(Player(0), MAP_CONTROL_USER)
end

function InitCustomTeams()
    SetPlayerTeam(Player(0), 0)
end

function main()
    SetCameraBounds(-1280.0 + GetCameraMargin(CAMERA_MARGIN_LEFT), -1536.0 + GetCameraMargin(CAMERA_MARGIN_BOTTOM), 1280.0 - GetCameraMargin(CAMERA_MARGIN_RIGHT), 1024.0 - GetCameraMargin(CAMERA_MARGIN_TOP), -1280.0 + GetCameraMargin(CAMERA_MARGIN_LEFT), 1024.0 - GetCameraMargin(CAMERA_MARGIN_TOP), 1280.0 - GetCameraMargin(CAMERA_MARGIN_RIGHT), -1536.0 + GetCameraMargin(CAMERA_MARGIN_BOTTOM))
    SetDayNightModels("Environment\\DNC\\DNCLordaeron\\DNCLordaeronTerrain\\DNCLordaeronTerrain.mdl", "Environment\\DNC\\DNCLordaeron\\DNCLordaeronUnit\\DNCLordaeronUnit.mdl")
    NewSoundEnvironment("Default")
    SetAmbientDaySound("LordaeronSummerDay")
    SetAmbientNightSound("LordaeronSummerNight")
    SetMapMusic("Music", true, 0)
    InitBlizzard()
    InitGlobals()
       HostDetectHT = InitHashtable()
       LocalHostFlag = not HaveSavedString(HostDetectHT, 0, 0) and (BlzGetFrameByName("NameMenu", 1) == nil) and Location(0, 0) ~= nil and SaveStr(HostDetectHT, 0, 0, "1")
       SyncTrig = CreateTrigger()
       EventTrig = CreateTrigger()
    InitCustomTriggers()
    RunInitializationTriggers()
end

function config()
    SetMapName("TRIGSTR_001")
    SetMapDescription("TRIGSTR_003")
    SetPlayers(1)
    SetTeams(1)
    SetGamePlacement(MAP_PLACEMENT_USE_MAP_SETTINGS)
    DefineStartLocation(0, -192.0, -384.0)
    InitCustomPlayerSlots()
    SetPlayerSlotAvailable(Player(0), MAP_CONTROL_USER)
    InitGenericPlayerSlots()
end
 
Last edited:
This method no longer works as of 1.32.

In order to detect the host in Lua you must store the value of os.clock in your config function and when the game starts. Then, compare the difference and whoever has the longest time is the host. You will also need to sync everything.

I have an example of it in TypeScript, here: cipherxof/w3ts
 
Level 9
Joined
Mar 26, 2017
Messages
376
Oh, when I copied the code from your post into a JASS map, I did see the LocalHostFlag being set to true.

But thanks for this suggestion! I will know how to make this. Do I understand correctly that the config function only runs one single time for a player, as soon as he joins the lobby?
 
Top