Autobalance issues

5 replies
Posts:
4
Votes:
+1
LEVEL 1
I seem to be moved every time there is an autobalance. Why is that?

Even when I am the first player to join a team I get moved. It is kind of annoying...

Is it due to admins priority or what?
Posted Aug 24, 18 · OP
Posts:
4
Votes:
+1
LEVEL 1
Today I was the first one on the allies team, then Jorcri joined my team and I was moved to axis in autobalance.
Posted Aug 24, 18 · OP
Posts:
15
Votes:
+12
LEVEL 1
Hello RagnaRock, I'll try to make clear how autobalance works and why you are getting moved, if you have any question feel free to ask anything.

How autobalance works

First of all, let me explain what 'game world' is. The game world is the map itself, the instance of a map running into the server. When a player joins the game world, is when they are able to spawn and move freely on a map for the first time. This happens twice in a map: After it's loaded (in warmup), and when warmup ends ('Fight' sound, when the real action begins). When a player is spectator and joins a team, also counts as joining in the game world.

You can see when a player joins the game world if you open the console, you will see 'X player entered the game'.

The autobalancer is a Lua module, it's a script programmed in a language that allows to modify the game behaviour. These lua modules are loaded every time the server creates the game world, this is when warmup begins and when the real fight begins (when players join the game world).

The autobalancer does this:

  1. It makes two lists of players, one of axis team and other of allies team. It's an ordered list, from 0 to n-1 players in the team.
  2. When teams get unbalanced (2 or more players down), it moves the last player of the most numbered team (stronger team) to the other team (weaker team, with less players).
  3. When this happens, the player moved is placed in position #1 on the enemy team list, so he will never be moved again in the map session, until he goes spec and joins back in a team.
  4. When new players join during the match, they will be the last ones in the team list, and will be the first ones to be moved when there is an unbalanced situation.

Now some things to explain:

  • In the map beginning (warmup begin or fight begin), as all players join at the same time, the order is almost random, this means the order of the list will be based on who joins the game world first. This usually means the player with fastest PC or connection joining the game world will be the first ones in the list. You can guess when a map begins who will be moved if you open console and check who was the last who 'entered the game', that person will be moved.
  • In normal cases during a map, if you get moved by autobalance you will only be moved once, unless you go spec and join back (no matter the team, if you go spec and join a team you will be the last on the list again).
  • There is absolutely no priority on the list, admins or not. I see some cases of people with slow computers or connections that are always the last ones joining (Monkeylord, phil some times...)
  • The lists are made exclusively in the order of appearing the game world or the order of players joining the teams, not admin levels or XP. The list also excludes the bots, because the bots have their own balance programmed in the bot scripts.
  • The autobalancer resets every new map, not because admins want but because the lua scripts / modules are unloaded every map end and loaded back every map begin.

The situation you explaining with jorcri, probably he joined during warmup and then when game started you were the last one to join the game world.

If anyone wants to audit the code to confirm what I'm saying just say it and I'll make it public ;)
Posted Aug 24, 18
Posts:
4
Votes:
+1
LEVEL 1
I do not think it is "almost random", and I can assure you that it is not about the fastest pc or connection.

It is kind of annoying to "always" be the one that has to move way when a new player joins my team. I got autoblanced 3 times in one map, back and forth from axis to allies and back again.
Posted Nov 3, 18 · OP
Posts:
15
Votes:
+12
LEVEL 1
If you got autobalanced 3 times in a map, this is exclusively because the first time you got moved, you went to spec or switched.

Later when I get my laptop I will post the full code as it is now, and the same code explained by parts so maybe you understand how it works and when the world is created (map loading or after warmup) it's kinda random, and later it's the last player to get moved.
Posted Nov 3, 18
Posts:
15
Votes:
+12
LEVEL 1
So there you have:
Copy
unevenDiff = 2 max_unevenTime = 15 max_unevenDiff = 4 axisPlayers = {} alliedPlayers = {} unevenTime = -1 balanceSwitch = 1 function et_ClientCommand( clientNum, command ) -- get the first argument arg = string.lower(et.trap_Argv(0)) cmd = string.lower(et.trap_Argv(1)) arg1 = string.lower(et.trap_Argv(2)) adminlvl = et.G_shrubbot_level(clientNum) if adminlvl >= 12 then if arg == "say" and cmd == "!even" then --gamestate = tonumber(et.trap_Cvar_Get("gamestate")) if arg1 == "off" or arg1 == "0" then balanceSwitch = 0 et.trap_SendConsoleCommand( et.EXEC_APPEND, "qsay ^6--- BALANCER IS OFF!!!!" ) else balanceSwitch = 1 et.trap_SendConsoleCommand( et.EXEC_APPEND, "qsay ^6--- BALANCER IS ON!!!!" ) end end end end function et_RunFrame( levelTime ) if balanceSwitch == 1 then gamestate = tonumber(et.trap_Cvar_Get("gamestate")) if gamestate == 1 or gamestate == 3 then return end local numAlliedPlayers = table.getn( alliedPlayers ) local numAxisPlayers = table.getn( axisPlayers ) -- et.trap_SendConsoleCommand( et.EXEC_APPEND, "qsay Allies table... " .. numAlliedPlayers .. "^7 Axis table "..numAxisPlayers.."" ) if numAlliedPlayers >= numAxisPlayers + max_unevenDiff then local clientNum = alliedPlayers[ numAlliedPlayers ] et.trap_SendConsoleCommand( et.EXEC_APPEND, "!put " .. clientNum .. " r ; qsay ^6--- AUTO-BALANCING TEAMS:^7 " .. et.gentity_get( clientNum, "pers.netname" ) .. "^6 MOVED TO ^1AXIS" ) elseif numAxisPlayers >= numAlliedPlayers + max_unevenDiff then local clientNum = axisPlayers[ numAxisPlayers ] et.trap_SendConsoleCommand( et.EXEC_APPEND, "!put " .. clientNum .. " b ; qsay ^6--- AUTO-BALANCING TEAMS:^7 " .. et.gentity_get( clientNum, "pers.netname" ) .. "^6 MOVED TO ^4ALLIES" ) elseif numAlliedPlayers >= numAxisPlayers + unevenDiff then if unevenTime > 0 then if tonumber( levelTime ) - unevenTime >= max_unevenTime * 1000 then local clientNum = alliedPlayers[ numAlliedPlayers ] et.trap_SendConsoleCommand( et.EXEC_APPEND, "!put " .. clientNum .. " r ; qsay ^6--- AUTO-BALANCING TEAMS:^7 " .. et.gentity_get( clientNum, "pers.netname" ) .. "^6 MOVED TO ^1AXIS" ) end else unevenTime = tonumber( levelTime ) end elseif numAxisPlayers >= numAlliedPlayers + unevenDiff then if unevenTime > 0 then if tonumber( levelTime ) - unevenTime >= max_unevenTime * 1000 then local clientNum = axisPlayers[ numAxisPlayers ] et.trap_SendConsoleCommand( et.EXEC_APPEND, "!put " .. clientNum .. " b ; qsay ^6--- AUTO-BALANCING TEAMS:^7 " .. et.gentity_get( clientNum, "pers.netname" ) .. "^6 MOVED TO ^4ALLIES" ) end else unevenTime = tonumber( levelTime ) end else unevenTime = -1 end end end function et_ClientSpawn( clientNum, revived, teamChange, restoreHealth ) local thisGuid = string.upper( et.Info_ValueForKey( et.trap_GetUserinfo( clientNum ), "cl_guid" )) --local gamestate = tonumber(et.trap_Cvar_Get("gamestate")) if teamChange ~= 0 and string.sub(thisGuid, 1, 7) ~= "OMNIBOT" then local team = tonumber( et.gentity_get( clientNum, "sess.sessionTeam" ) ) -- these were the teamnumbers prior to the move local numAlliedPlayers = table.getn( alliedPlayers ) local numAxisPlayers = table.getn( axisPlayers ) if team == 1 then for i, num in ipairs( alliedPlayers ) do if num == clientNum then table.remove( alliedPlayers, i ) break end end -- this should not happen but still check for it to avoid doubles for i, num in ipairs( axisPlayers ) do if num == clientNum then return end end -- make sure a player who (got) moved when teams were uneven doesn't get moved right back if numAlliedPlayers >= numAxisPlayers + unevenDiff then table.insert( axisPlayers, 1, clientNum ) else table.insert( axisPlayers, clientNum ) end elseif team == 2 then for i, num in ipairs( axisPlayers ) do if num == clientNum then table.remove( axisPlayers, i ) break end end for i, num in ipairs( alliedPlayers ) do if num == clientNum then return end end if numAxisPlayers >= numAlliedPlayers + unevenDiff then table.insert( alliedPlayers, 1, clientNum ) else table.insert( alliedPlayers, clientNum ) end else for i, num in ipairs( alliedPlayers ) do if num == clientNum then table.remove( alliedPlayers, i ) return end end for i, num in ipairs( axisPlayers ) do if num == clientNum then table.remove( axisPlayers, i ) return end end end end end function et_ClientDisconnect( clientNum ) for i, num in ipairs( alliedPlayers ) do if num == clientNum then table.remove( alliedPlayers, i ) return end end for i, num in ipairs( axisPlayers ) do if num == clientNum then table.remove( axisPlayers, i ) return end end end


Now the code explained:

1) Variable definition.

unevenDiff = 2 Minimum number of player difference to make the script work
max_unevenTime = 15 Max time, in seconds, between teams get unbalanced and script moves someone
max_unevenDiff = 4 If the player difference between teams exceeds this value, it'll work inmediately without waiting the 'maxTime'

axisPlayers = {} Both player table definition, by default empty when the script is initialized.
alliedPlayers = {}
unevenTime = -1 By default, teams are supposed 'not uneven' until the tables are full.

balanceSwitch = 1 This is used as 'switch', to be able to turn balancer off with !balance off command.

2) ClientCommand function. This one just intercepts the !balance command and enables or disables the balancer.

function et_ClientCommand( clientNum, command )

-- get the first argument
arg = string.lower(et.trap_Argv(0))
cmd = string.lower(et.trap_Argv(1))
arg1 = string.lower(et.trap_Argv(2))
adminlvl = et.G_shrubbot_level(clientNum)
if adminlvl >= 12 then
if arg == "say" and cmd == "!even" then If someone writes !even in chat, we analyze it and change the 'switch' variable, and announce it.

if arg1 == "off" or arg1 == "0" then
balanceSwitch = 0
et.trap_SendConsoleCommand( et.EXEC_APPEND, "qsay ^6--- BALANCER IS OFF!!!!" )
else
balanceSwitch = 1
et.trap_SendConsoleCommand( et.EXEC_APPEND, "qsay ^6--- BALANCER IS ON!!!!" )
end
end
end
end

3) Function et_clientSpawn function. This is executed every time a player spawns, and for every player, meaning: - When you join a team, switch a team, when you die and respawn, if you die and are revived. Every time. It checks if the spawn was because of a team change, and if player is not a bot (because BOTS are auto-balanced by omni-bot), to build the team lists that will be used to move players.

function et_ClientSpawn( clientNum, revived, teamChange, restoreHealth )
Get player guid, to check that it's not a bot
local thisGuid = string.upper( et.Info_ValueForKey( et.trap_GetUserinfo( clientNum ), "cl_guid" ))
Execute the code if teamChange is different from 0 (aka the player teamswitched), and player is not a bot.
if teamChange ~= 0 and string.sub(thisGuid, 1, 7) ~= "OMNIBOT" then
local team = tonumber( et.gentity_get( clientNum, "sess.sessionTeam" ) )
-- these were the teamnumbers prior to the move of the player
local numAlliedPlayers = table.getn( alliedPlayers )
local numAxisPlayers = table.getn( axisPlayers )
If a player moves from allies to axis, it removes the player from the allied players table
if team == 1 then
for i, num in ipairs( alliedPlayers ) do
if num == clientNum then
table.remove( alliedPlayers, i )
break
end
end
-- this should not happen but still check for it to avoid doubles
for i, num in ipairs( axisPlayers ) do
if num == clientNum then
return
end
end
If player is moved by evener (or a player moves while teams are uneven, without being moved by the script), the player is inserted in POSITION 1 of the table, meaning he will be the first one.
-- make sure a player who (got) moved when teams were uneven doesn't get moved right back
if numAlliedPlayers >= numAxisPlayers + unevenDiff then
table.insert( axisPlayers, 1, clientNum )
else
If not moved while teams were uneven, he will be added in the end of the table.
table.insert( axisPlayers, clientNum )
end
Same as above, but while switching from axis to allies
elseif team == 2 then
for i, num in ipairs( axisPlayers ) do
if num == clientNum then
table.remove( axisPlayers, i )
break
end
end
for i, num in ipairs( alliedPlayers ) do
if num == clientNum then
return
end
end
if numAxisPlayers >= numAlliedPlayers + unevenDiff then
table.insert( alliedPlayers, 1, clientNum )
else
table.insert( alliedPlayers, clientNum )
end
else
If team is not axis or allies (aka spectator), remove the player from the team list whatever it is (remove it from 'game').
for i, num in ipairs( alliedPlayers ) do
if num == clientNum then
table.remove( alliedPlayers, i )
return
end
end
for i, num in ipairs( axisPlayers ) do
if num == clientNum then
table.remove( axisPlayers, i )
return
end
end
end
end
end

4) Function et_runTime function. This function runs every server frame, and if i'm not wrong, server runs 20 frames per second

function et_RunFrame( levelTime )
if balanceSwitch == 1 then We only exec this code if balancer is 'on'. If it's turned off, it does nothing.

gamestate = tonumber(et.trap_Cvar_Get("gamestate"))
if gamestate == 1 or gamestate == 3 then return end We check which gamestate are we. Gamestate 1 and 3 are warmup and intermission, we only want balancer to work during the 'gametime'.

Here we get the number of players from each team table, which has been filled in the function before.
local numAlliedPlayers = table.getn( alliedPlayers )
local numAxisPlayers = table.getn( axisPlayers )
If number of allied players is higher or equal to axis+unevenDiff (in our case, allies >= axis+4) we execute the command without waiting time, and we get the LAST ENTRY FROM THE TABLE.
if numAlliedPlayers >= numAxisPlayers + max_unevenDiff then
local clientNum = alliedPlayers[ numAlliedPlayers ] PLayer is chosen and sent to the axis team, in the next line.
et.trap_SendConsoleCommand( et.EXEC_APPEND, "!put " .. clientNum .. " r ; qsay ^6--- AUTO-BALANCING TEAMS:^7 " .. et.gentity_get( clientNum, "pers.netname" ) .. "^6 MOVED TO ^1AXIS" )
This part of code does the same, but moving from axis to allies

elseif numAxisPlayers >= numAlliedPlayers + max_unevenDiff then
local clientNum = axisPlayers[ numAxisPlayers ]
et.trap_SendConsoleCommand( et.EXEC_APPEND, "!put " .. clientNum .. " b ; qsay ^6--- AUTO-BALANCING TEAMS:^7 " .. et.gentity_get( clientNum, "pers.netname" ) .. "^6 MOVED TO ^4ALLIES" )
This part of code does the same as above, but waiting the unevenTime
elseif numAlliedPlayers >= numAxisPlayers + unevenDiff then
if unevenTime > 0 then
if tonumber( levelTime ) - unevenTime >= max_unevenTime * 1000 then
local clientNum = alliedPlayers[ numAlliedPlayers ]
et.trap_SendConsoleCommand( et.EXEC_APPEND, "!put " .. clientNum .. " r ; qsay ^6--- AUTO-BALANCING TEAMS:^7 " .. et.gentity_get( clientNum, "pers.netname" ) .. "^6 MOVED TO ^1AXIS" )
end
else
unevenTime = tonumber( levelTime )
end
elseif numAxisPlayers >= numAlliedPlayers + unevenDiff then
if unevenTime > 0 then
if tonumber( levelTime ) - unevenTime >= max_unevenTime * 1000 then
local clientNum = axisPlayers[ numAxisPlayers ]
et.trap_SendConsoleCommand( et.EXEC_APPEND, "!put " .. clientNum .. " b ; qsay ^6--- AUTO-BALANCING TEAMS:^7 " .. et.gentity_get( clientNum, "pers.netname" ) .. "^6 MOVED TO ^4ALLIES" )
end
else
unevenTime = tonumber( levelTime )
end
else
Once uneven difference is below 2, the unevenTime is set to -1 so the balance script stops working.
unevenTime = -1
end
end
end

5) function et_clientDisconnect. This function removes players from team tables when they disconnect, it's called every time a player quits the game.

function et_ClientDisconnect( clientNum )
for i, num in ipairs( alliedPlayers ) do
if num == clientNum then
table.remove( alliedPlayers, i )
return
end
end
for i, num in ipairs( axisPlayers ) do
if num == clientNum then
table.remove( axisPlayers, i )
return
end
end
end


As you can see, the step 3) is the one making the random fill at beginning. When game starts after warmup, if you open console you can see everyone 'entering the game', meaning the function et_clientSpawn being called and at that moment the first time, everyone is 'team changing'.

In step 4), et_runFrame when the player is being moved, we call the last position of the table, meaning it is the last player who joined. Example: alliedPlayers[ numAlliedPlayers ] will call the numAlliedPlayers from allied table. If there are 10 players, it will choose the player in the position 10 of the table, which will be the last. When a player is being moved, by balancer or while teams are uneven, they're inserted in position 1, that's why your argument of favoritism is invalid.

The only flaw of this script is, as I've said many times, that the first fill is random, because of the order of players entering the world. I can't tell exactly how it's made, if it's by speed of connection, by better pc, faster load... You can check this opening console right after a map is loaded in warmup, and checking how players 'enter the game' (the last 3-5) and then do the same right after the fight start.

PS: The code is unindented because of the reformat of the forum, excuse the bad time reading the code ;)
Posted Nov 3, 18 · Last edited Nov 3, 18 by leroy
Top Posters
54 Posts
46 Posts
34 Posts
23 Posts
15 Posts
12 Posts
11 Posts
11 Posts
8 Posts
7 Posts
NoticeNotices