Merge branch 'master' into terencehill/bot_waypoints
authorterencehill <piuntn@gmail.com>
Wed, 6 Sep 2017 16:36:50 +0000 (18:36 +0200)
committerterencehill <piuntn@gmail.com>
Wed, 6 Sep 2017 16:36:50 +0000 (18:36 +0200)
70 files changed:
.gitlab-ci.yml
CMakeLists.txt
defaultOverkill.cfg
mutators.cfg
qcsrc/client/main.qc
qcsrc/client/mapvoting.qc
qcsrc/common/mutators/mutator/bloodloss/sv_bloodloss.qc
qcsrc/common/mutators/mutator/buffs/sv_buffs.qc
qcsrc/common/mutators/mutator/campcheck/sv_campcheck.qc
qcsrc/common/mutators/mutator/cloaked/sv_cloaked.qc
qcsrc/common/mutators/mutator/hook/sv_hook.qc
qcsrc/common/mutators/mutator/instagib/sv_instagib.qc
qcsrc/common/mutators/mutator/invincibleproj/sv_invincibleproj.qc
qcsrc/common/mutators/mutator/melee_only/sv_melee_only.qc
qcsrc/common/mutators/mutator/midair/sv_midair.qc
qcsrc/common/mutators/mutator/multijump/multijump.qc
qcsrc/common/mutators/mutator/nades/nades.qc
qcsrc/common/mutators/mutator/new_toys/sv_new_toys.qc
qcsrc/common/mutators/mutator/nix/sv_nix.qc
qcsrc/common/mutators/mutator/overkill/sv_overkill.qc
qcsrc/common/mutators/mutator/physical_items/sv_physical_items.qc
qcsrc/common/mutators/mutator/pinata/sv_pinata.qc
qcsrc/common/mutators/mutator/rocketflying/sv_rocketflying.qc
qcsrc/common/mutators/mutator/sandbox/sv_sandbox.qc
qcsrc/common/mutators/mutator/spawn_near_teammate/sv_spawn_near_teammate.qc
qcsrc/common/mutators/mutator/superspec/sv_superspec.qc
qcsrc/common/mutators/mutator/touchexplode/sv_touchexplode.qc
qcsrc/common/mutators/mutator/vampire/sv_vampire.qc
qcsrc/common/mutators/mutator/vampirehook/sv_vampirehook.qc
qcsrc/common/mutators/mutator/walljump/walljump.qc
qcsrc/common/physics/movetypes/movetypes.qh
qcsrc/common/physics/player.qh
qcsrc/common/t_items.qc
qcsrc/common/t_items.qh
qcsrc/common/weapons/all.qc
qcsrc/common/weapons/projectiles.qh
qcsrc/dpdefs/post.qh
qcsrc/lib/_all.inc
qcsrc/lib/macro.qh
qcsrc/lib/misc.qh
qcsrc/lib/net.qh
qcsrc/lib/registry.qh
qcsrc/lib/self.qh
qcsrc/lib/spawnfunc.qh
qcsrc/lib/static.qh
qcsrc/lib/stats.qh
qcsrc/server/_mod.inc
qcsrc/server/_mod.qh
qcsrc/server/autocvars.qh
qcsrc/server/client.qc
qcsrc/server/client.qh
qcsrc/server/command/cmd.qc
qcsrc/server/command/sv_cmd.qc
qcsrc/server/command/vote.qc
qcsrc/server/g_damage.qc
qcsrc/server/g_world.qc
qcsrc/server/miscfunctions.qc
qcsrc/server/mutators/events.qh
qcsrc/server/player.qc
qcsrc/server/player.qh
qcsrc/server/resources.qc [new file with mode: 0644]
qcsrc/server/resources.qh [new file with mode: 0644]
qcsrc/server/scores.qc
qcsrc/server/scores_rules.qc
qcsrc/server/sv_main.qc
qcsrc/server/sv_main.qh
qcsrc/server/teamplay.qc
qcsrc/server/teamplay.qh
qcsrc/server/utils.qh
qcsrc/tools/compilationunits.sh

index 9305f52..596f61a 100644 (file)
@@ -29,7 +29,7 @@ test_sv_game:
     - wget -O data/maps/stormkeep.waypoints https://gitlab.com/xonotic/xonotic-maps.pk3dir/raw/master/maps/stormkeep.waypoints
     - wget -O data/maps/stormkeep.waypoints.cache https://gitlab.com/xonotic/xonotic-maps.pk3dir/raw/master/maps/stormkeep.waypoints.cache
     - make
-    - EXPECT=ed9be8d1b1a544f89bcdd7d36876fede
+    - EXPECT=0a08daa9132d147f533776deda07643e
     - HASH=$(${ENGINE} -noconfig -nohome +exec serverbench.cfg
       | tee /dev/stderr
       | grep '^:'
index 2e5fc79..9a66f9f 100644 (file)
@@ -8,9 +8,18 @@ add_custom_target(${all})
 set(checks qc-checks)
 add_custom_target(${checks})
 
-# depend on qcc
-if (TARGET gmqcc)
-    add_dependencies(${checks} gmqcc)
+if (gmqcc_BINARY_DIR)
+    set(compilerinfo "${gmqcc_BINARY_DIR}/gmqcc.h")
+    add_custom_command(
+            OUTPUT "${compilerinfo}"
+            DEPENDS "${gmqcc_BINARY_DIR}/gmqcc"
+            VERBATIM
+            COMMAND ${CMAKE_COMMAND} -E
+                md5sum "${gmqcc_BINARY_DIR}/gmqcc" > "${compilerinfo}"
+            )
+    add_custom_target(qcc ALL
+            DEPENDS "${compilerinfo}"
+            )
 endif ()
 
 add_dependencies(${checks} data-check-cvars)
@@ -45,6 +54,25 @@ add_custom_target(qc-whitespace
         VERBATIM COMMAND ./tools/whitespace.sh
         )
 
+function(prog name dir)
+    add_executable(${name} qcsrc/${dir}/progs.inc)
+    add_dependencies(${all} ${name})
+    add_dependencies(${name} ${checks})
+    add_dependencies(${name} qcc)
+    set_source_files_properties(qcsrc/${dir}/progs.inc PROPERTIES OBJECT_DEPENDS "${compilerinfo}")
+endfunction()
+
+function(set_prelude target prelude)
+    get_target_property(MY_PROJECT_SOURCES ${target} SOURCES)
+    foreach (source IN LISTS MY_PROJECT_SOURCES)
+        set_property(
+                SOURCE ${source}
+                APPEND PROPERTY COMPILE_FLAGS
+                "-include ${prelude}"
+        )
+    endforeach ()
+endfunction()
+
 include_directories(qcsrc)
 
 add_definitions(-DXONOTIC=1)
@@ -75,33 +103,16 @@ set_source_files_properties(
         HEADER_FILE_ONLY FALSE
 )
 
-add_executable(csprogs qcsrc/client/progs.inc)
-add_dependencies(${all} csprogs)
-add_dependencies(csprogs ${checks})
+prog(csprogs client)
 target_compile_definitions(csprogs PRIVATE -DGAMEQC -DCSQC)
+# set_prelude(csprogs "${PROJECT_SOURCE_DIR}/qcsrc/lib/_all.inc")
 
-add_executable(progs qcsrc/server/progs.inc)
-add_dependencies(${all} progs)
-add_dependencies(progs ${checks})
+prog(progs server)
 target_compile_definitions(progs PRIVATE -DGAMEQC -DSVQC)
 
-add_executable(menu qcsrc/menu/progs.inc)
-add_dependencies(${all} menu)
-add_dependencies(menu ${checks})
+prog(menu menu)
 target_compile_definitions(menu PRIVATE -DMENUQC)
 
-function(set_prelude target prelude)
-    get_target_property(MY_PROJECT_SOURCES target SOURCES)
-    foreach (source IN LISTS MY_PROJECT_SOURCES)
-        set_property(
-                SOURCE ${source}
-                APPEND PROPERTY COMPILE_FLAGS
-                "-include ${PROJECT_SOURCE_DIR}/${prelude}"
-        )
-    endforeach ()
-endfunction()
-# set_prelude(csprogs qcsrc/lib/_all.inc)
-
 function(copy prog)
     add_custom_command(TARGET ${prog} POST_BUILD
             COMMAND ${CMAKE_COMMAND} -E copy "$<TARGET_FILE_DIR:${prog}>/${prog}.dat" "${prog}.dat"
index 2444301..f63f689 100644 (file)
@@ -28,7 +28,7 @@ set g_nades_nade_newton_style 2
 set g_dodging 1
 set sv_dodging_wall_dodging 1
 
-set g_spawn_near_teammate 1
+set g_spawn_near_teammate "!g_assault !g_freezetag"
 set g_spawn_near_teammate_ignore_spawnpoint 1
 set g_spawnshieldtime 0.5
 set g_respawn_delay_forced 2
index 23c7243..5eb8d1b 100644 (file)
@@ -108,7 +108,7 @@ set g_rocket_flying 0 "set to 1 to enable rocket flying in all balance configs"
 //  spawn near teammate
 // =====================
 seta cl_spawn_near_teammate 1 "toggle for spawning near teammates (only effective if g_spawn_near_teammate_ignore_spawnpoint is 2)"
-set g_spawn_near_teammate 0 "if set, players prefer spawns near a team mate"
+set g_spawn_near_teammate 0 "players prefer spawns near a team mate"
 set g_spawn_near_teammate_distance 640 "max distance to consider a spawn to be near a team mate"
 set g_spawn_near_teammate_ignore_spawnpoint 0 "ignore spawnpoints and spawn right at team mates, if 2, clients can ignore this option"
 set g_spawn_near_teammate_ignore_spawnpoint_max 10 "if set, test at most this many of the available teammates"
index ae624ef..988cdf3 100644 (file)
@@ -435,11 +435,10 @@ NET_HANDLE(ENT_CLIENT_SCORES, bool isnew)
        //      RegisterPlayer(o);
        //playerchecker will do this for us later, if it has not already done so
 
-    int sf, lf;
-       sf = ReadShort();
-       lf = ReadShort();
+       int sf = ReadShort();
+       int lf = ReadShort();
        FOREACH(Scores, true, {
-        int p = 1 << (i % 16);
+               int p = 1 << (i % 16);
                if (sf & p)
                {
                        if (lf & p)
@@ -447,7 +446,7 @@ NET_HANDLE(ENT_CLIENT_SCORES, bool isnew)
                        else
                                o.(scores(it)) = ReadChar();
                }
-    });
+       });
 
        return = true;
 
@@ -461,24 +460,21 @@ NET_HANDLE(ENT_CLIENT_TEAMSCORES, bool isnew)
 {
        make_pure(this);
        int i;
-       entity o;
 
        this.team = ReadByte();
-       o = this.owner = GetTeam(this.team, true); // these team numbers can always be trusted
+       entity o = this.owner = GetTeam(this.team, true); // these team numbers can always be trusted
 
-    int sf, lf;
 #if MAX_TEAMSCORE <= 8
-       sf = ReadByte();
-       lf = ReadByte();
+       int sf = ReadByte();
+       int lf = ReadByte();
 #else
-       sf = ReadShort();
-       lf = ReadShort();
+       int sf = ReadShort();
+       int lf = ReadShort();
 #endif
-       int p;
-       for(i = 0, p = 1; i < MAX_TEAMSCORE; ++i, p *= 2)
-               if(sf & p)
+       for(i = 0; i < MAX_TEAMSCORE; ++i)
+               if(sf & BIT(i))
                {
-                       if(lf & p)
+                       if(lf & BIT(i))
                                o.(teamscores(i)) = ReadInt24_t();
                        else
                                o.(teamscores(i)) = ReadChar();
@@ -494,7 +490,7 @@ NET_HANDLE(ENT_CLIENT_CLIENTDATA, bool isnew)
        make_pure(this);
        float newspectatee_status;
 
-    int f = ReadByte();
+       int f = ReadByte();
 
        scoreboard_showscores_force = (f & BIT(0));
 
@@ -551,9 +547,9 @@ NET_HANDLE(ENT_CLIENT_CLIENTDATA, bool isnew)
 NET_HANDLE(ENT_CLIENT_NAGGER, bool isnew)
 {
        make_pure(this);
-    int i, j, b, f;
+       int i, j, b, f;
 
-    int nags = ReadByte(); // NAGS NAGS NAGS NAGS NAGS NAGS NADZ NAGS NAGS NAGS
+       int nags = ReadByte(); // NAGS NAGS NAGS NAGS NAGS NAGS NADZ NAGS NAGS NAGS
 
        if(!(nags & BIT(2)))
        {
@@ -590,7 +586,7 @@ NET_HANDLE(ENT_CLIENT_NAGGER, bool isnew)
                for(i = 1; i <= maxclients; i += 8)
                {
                        f = ReadByte();
-                       for(j = i-1, b = 1; b < 256; b *= 2, ++j)
+                       for(j = i-1, b = BIT(0); b < BIT(8); b <<= 1, ++j)
                                if (!(f & b))
                                        if(playerslots[j])
                                                playerslots[j].ready = 0;
@@ -609,21 +605,23 @@ NET_HANDLE(ENT_CLIENT_NAGGER, bool isnew)
 NET_HANDLE(ENT_CLIENT_ELIMINATEDPLAYERS, bool isnew)
 {
        make_pure(this);
-    int i, j, b, f;
-
-    int sf = ReadByte();
-       if(sf & 1)
-       {
-               for(j = 0; j < maxclients; ++j)
-                       if(playerslots[j])
+       int sf; serialize(byte, 0, sf);
+       if (sf & 1) {
+               for (int j = 0; j < maxclients; ++j) {
+                       if (playerslots[j]) {
                                playerslots[j].eliminated = 1;
-               for(i = 1; i <= maxclients; i += 8)
-               {
-                       f = ReadByte();
-                       for(j = i-1, b = 1; b < 256; b *= 2, ++j)
-                               if (!(f & b))
-                                       if(playerslots[j])
-                                               playerslots[j].eliminated = 0;
+                       }
+               }
+               for (int i = 1; i <= maxclients; i += 8) {
+                       int f = 0;
+                       serialize(byte, 0, f);
+                       for (int b = 0; b < 8; ++b) {
+                               if (f & BIT(b)) continue;
+                               int j = i - 1 + b;
+                               if (playerslots[j]) {
+                                       playerslots[j].eliminated = 0;
+                               }
+                       }
                }
        }
        return true;
@@ -641,7 +639,7 @@ NET_HANDLE(ENT_CLIENT_RANDOMSEED, bool isnew)
 NET_HANDLE(ENT_CLIENT_ACCURACY, bool isnew)
 {
        make_pure(this);
-    int sf = ReadInt24_t();
+       int sf = ReadInt24_t();
        if (sf == 0) {
                for (int w = 0; w <= WEP_LAST - WEP_FIRST; ++w)
                        weapon_accuracy[w] = -1;
@@ -651,7 +649,7 @@ NET_HANDLE(ENT_CLIENT_ACCURACY, bool isnew)
        int f = 1;
        for (int w = 0; w <= WEP_LAST - WEP_FIRST; ++w) {
                if (sf & f) {
-            int b = ReadByte();
+                       int b = ReadByte();
                        if (b == 0)
                                weapon_accuracy[w] = -1;
                        else if (b == 255)
index b34bd0f..87b6d58 100644 (file)
@@ -563,7 +563,7 @@ void MapVote_ReadMask()
        int i;
        if ( mv_num_maps < 24 )
        {
-               int mask, power;
+               int mask;
                if(mv_num_maps < 8)
                        mask = ReadByte();
                else if(mv_num_maps < 16)
@@ -571,9 +571,9 @@ void MapVote_ReadMask()
                else
                        mask = ReadLong();
 
-               for(i = 0, power = 1; i < mv_num_maps; ++i, power *= 2)
+               for(i = 0; i < mv_num_maps; ++i)
                {
-                       if ( mask & power )
+                       if (mask & BIT(i))
                                mv_flags[i] |= GTV_AVAILABLE;
                        else
                                mv_flags[i] &= ~GTV_AVAILABLE;
index 61b0c06..1164e0a 100644 (file)
@@ -1,6 +1,7 @@
 #include "sv_bloodloss.qh"
 
-REGISTER_MUTATOR(bloodloss, cvar("g_bloodloss"));
+float autocvar_g_bloodloss;
+REGISTER_MUTATOR(bloodloss, autocvar_g_bloodloss);
 
 .float bloodloss_timer;
 
@@ -9,7 +10,7 @@ MUTATOR_HOOKFUNCTION(bloodloss, PlayerPreThink)
        entity player = M_ARGV(0, entity);
 
        if(IS_PLAYER(player))
-       if(player.health <= autocvar_g_bloodloss && !IS_DEAD(player))
+       if(GetResourceAmount(player, RESOURCE_HEALTH) <= autocvar_g_bloodloss && !IS_DEAD(player))
        {
                PHYS_INPUT_BUTTON_CROUCH(player) = true;
 
@@ -28,7 +29,7 @@ MUTATOR_HOOKFUNCTION(bloodloss, PlayerJump)
 {
        entity player = M_ARGV(0, entity);
 
-       if(player.health <= autocvar_g_bloodloss)
+       if(GetResourceAmount(player, RESOURCE_HEALTH) <= autocvar_g_bloodloss)
                return true;
 }
 
index 925525f..e039a96 100644 (file)
@@ -424,12 +424,17 @@ void buff_Medic_Heal(entity this)
 {
        FOREACH_CLIENT(IS_PLAYER(it) && it != this && vdist(it.origin - this.origin, <=, autocvar_g_buffs_medic_heal_range),
        {
-               if(SAME_TEAM(it, this))
-               if(it.health < autocvar_g_balance_health_regenstable)
+               if (!SAME_TEAM(it, this))
                {
-                       Send_Effect(EFFECT_HEALING, it.origin, '0 0 0', 1);
-                       it.health = bound(0, it.health + autocvar_g_buffs_medic_heal_amount, autocvar_g_balance_health_regenstable);
+                       continue;
                }
+               float hp = GetResourceAmount(it, RESOURCE_HEALTH);
+               if(hp >= autocvar_g_balance_health_regenstable)
+               {
+                       continue;
+               }
+               Send_Effect(EFFECT_HEALING, it.origin, '0 0 0', 1);
+               SetResourceAmount(it, RESOURCE_HEALTH, bound(0, hp + autocvar_g_buffs_medic_heal_amount, autocvar_g_balance_health_regenstable));
        });
 }
 
@@ -460,11 +465,11 @@ MUTATOR_HOOKFUNCTION(buffs, Damage_Calculate)
                frag_damage *= autocvar_g_buffs_speed_damage_take;
 
        if(frag_target.buffs & BUFF_MEDIC.m_itemid)
-       if((frag_target.health - frag_damage) <= 0)
+       if((GetResourceAmount(frag_target, RESOURCE_HEALTH) - frag_damage) <= 0)
        if(!ITEM_DAMAGE_NEEDKILL(frag_deathtype))
        if(frag_attacker)
        if(random() <= autocvar_g_buffs_medic_survive_chance)
-               frag_damage = max(5, frag_target.health - autocvar_g_buffs_medic_survive_health);
+               frag_damage = max(5, GetResourceAmount(frag_target, RESOURCE_HEALTH) - autocvar_g_buffs_medic_survive_health);
 
        if(frag_target.buffs & BUFF_JUMP.m_itemid)
        if(frag_deathtype == DEATH_FALL.m_id)
@@ -537,9 +542,15 @@ MUTATOR_HOOKFUNCTION(buffs, Damage_Calculate)
        if(frag_target.takedamage)
        if(DIFF_TEAM(frag_attacker, frag_target))
        {
-               frag_attacker.health = bound(0, frag_attacker.health + bound(0, frag_damage * autocvar_g_buffs_vampire_damage_steal, frag_target.health), g_pickup_healthsmall_max);
-               if(frag_target.armorvalue)
-                       frag_attacker.armorvalue = bound(0, frag_attacker.armorvalue + bound(0, frag_damage * autocvar_g_buffs_vampire_damage_steal, frag_target.armorvalue), g_pickup_armorsmall_max);
+               float amount = bound(0, frag_damage * autocvar_g_buffs_vampire_damage_steal,
+                       GetResourceAmount(frag_target, RESOURCE_HEALTH));
+               GiveResourceWithLimit(frag_attacker, RESOURCE_HEALTH, amount, g_pickup_healthsmall_max);
+               if (frag_target.armorvalue)
+               {
+                       amount = bound(0, frag_damage * autocvar_g_buffs_vampire_damage_steal,
+                               GetResourceAmount(frag_target, RESOURCE_ARMOR));
+                       GiveResourceWithLimit(frag_attacker, RESOURCE_ARMOR, amount, g_pickup_armorsmall_max);
+               }
        }
 
        M_ARGV(4, float) = frag_damage;
index 52fc524..987645a 100644 (file)
@@ -1,10 +1,11 @@
 #include "sv_campcheck.qh"
 
+string autocvar_g_campcheck;
 float autocvar_g_campcheck_damage;
 float autocvar_g_campcheck_distance;
 float autocvar_g_campcheck_interval;
 
-REGISTER_MUTATOR(campcheck, cvar("g_campcheck"));
+REGISTER_MUTATOR(campcheck, expr_evaluate(autocvar_g_campcheck));
 
 .float campcheck_nextcheck;
 .float campcheck_traveled_distance;
@@ -64,7 +65,7 @@ MUTATOR_HOOKFUNCTION(campcheck, PlayerPreThink)
                                if(player.vehicle)
                                        Damage(player.vehicle, NULL, NULL, autocvar_g_campcheck_damage * 2, DEATH_CAMP.m_id, player.vehicle.origin, '0 0 0');
                                else
-                                       Damage(player, NULL, NULL, bound(0, autocvar_g_campcheck_damage, player.health + player.armorvalue * autocvar_g_balance_armor_blockpercent + 5), DEATH_CAMP.m_id, player.origin, '0 0 0');
+                                       Damage(player, NULL, NULL, bound(0, autocvar_g_campcheck_damage, GetResourceAmount(player, RESOURCE_HEALTH) + GetResourceAmount(player, RESOURCE_ARMOR) * autocvar_g_balance_armor_blockpercent + 5), DEATH_CAMP.m_id, player.origin, '0 0 0');
                        }
                        player.campcheck_nextcheck = time + autocvar_g_campcheck_interval;
                        player.campcheck_traveled_distance = 0;
index eb45d4b..a1fe27a 100644 (file)
@@ -1,6 +1,7 @@
 #include "sv_cloaked.qh"
 
-REGISTER_MUTATOR(cloaked, cvar("g_cloaked"));
+string autocvar_g_cloaked;
+REGISTER_MUTATOR(cloaked, expr_evaluate(autocvar_g_cloaked));
 
 float autocvar_g_balance_cloaked_alpha;
 
index 5dfdf43..c396781 100644 (file)
@@ -5,7 +5,7 @@
 #ifdef SVQC
 AUTOCVAR(g_grappling_hook_useammo, bool, false, "Use ammunition with the off-hand grappling hook");
 
-REGISTER_MUTATOR(hook, cvar("g_grappling_hook")) {
+REGISTER_MUTATOR(hook, expr_evaluate(cvar_string("g_grappling_hook"))) {
     MUTATOR_ONADD {
         g_grappling_hook = true;
         if(!autocvar_g_grappling_hook_useammo)
index fefad54..1561dc1 100644 (file)
@@ -1,5 +1,10 @@
 #include "sv_instagib.qh"
 
+bool autocvar_g_instagib_damagedbycontents = true;
+bool autocvar_g_instagib_blaster_keepdamage = false;
+bool autocvar_g_instagib_blaster_keepforce = false;
+bool autocvar_g_instagib_mirrordamage;
+bool autocvar_g_instagib_friendlypush = true;
 //int autocvar_g_instagib_ammo_drop;
 bool autocvar_g_instagib_ammo_convert_cells;
 bool autocvar_g_instagib_ammo_convert_rockets;
@@ -12,7 +17,7 @@ float autocvar_g_instagib_speed_highspeed;
 
 #include <common/items/_mod.qh>
 
-REGISTER_MUTATOR(mutator_instagib, cvar("g_instagib") && !g_nexball);
+REGISTER_MUTATOR(mutator_instagib, autocvar_g_instagib && !g_nexball);
 
 spawnfunc(item_minst_cells)
 {
@@ -55,7 +60,7 @@ void instagib_ammocheck(entity this)
 
        if(IS_DEAD(this) || game_stopped)
                instagib_stop_countdown(this);
-       else if (this.ammo_cells > 0 || (this.items & IT_UNLIMITED_WEAPON_AMMO) || (this.flags & FL_GODMODE))
+       else if (GetResourceAmount(this, RESOURCE_CELLS) > 0 || (this.items & IT_UNLIMITED_WEAPON_AMMO) || (this.flags & FL_GODMODE))
                instagib_stop_countdown(this);
        else if(autocvar_g_rm && autocvar_g_rm_laser)
        {
@@ -67,53 +72,54 @@ void instagib_ammocheck(entity this)
        }
        else
        {
+               float hp = GetResourceAmount(this, RESOURCE_HEALTH);
                this.instagib_needammo = true;
-               if (this.health <= 5)
+               if (hp <= 5)
                {
                        Damage(this, this, this, 5, DEATH_NOAMMO.m_id, this.origin, '0 0 0');
                        Send_Notification(NOTIF_ONE, this, MSG_ANNCE, ANNCE_INSTAGIB_TERMINATED);
                }
-               else if (this.health <= 10)
+               else if (hp <= 10)
                {
                        Damage(this, this, this, 5, DEATH_NOAMMO.m_id, this.origin, '0 0 0');
                        Send_Notification(NOTIF_ONE, this, MSG_ANNCE, ANNCE_NUM_1);
                }
-               else if (this.health <= 20)
+               else if (hp <= 20)
                {
                        Damage(this, this, this, 10, DEATH_NOAMMO.m_id, this.origin, '0 0 0');
                        Send_Notification(NOTIF_ONE, this, MSG_ANNCE, ANNCE_NUM_2);
                }
-               else if (this.health <= 30)
+               else if (hp <= 30)
                {
                        Damage(this, this, this, 10, DEATH_NOAMMO.m_id, this.origin, '0 0 0');
                        Send_Notification(NOTIF_ONE, this, MSG_ANNCE, ANNCE_NUM_3);
                }
-               else if (this.health <= 40)
+               else if (hp <= 40)
                {
                        Damage(this, this, this, 10, DEATH_NOAMMO.m_id, this.origin, '0 0 0');
                        Send_Notification(NOTIF_ONE, this, MSG_ANNCE, ANNCE_NUM_4);
                }
-               else if (this.health <= 50)
+               else if (hp <= 50)
                {
                        Damage(this, this, this, 10, DEATH_NOAMMO.m_id, this.origin, '0 0 0');
                        Send_Notification(NOTIF_ONE, this, MSG_ANNCE, ANNCE_NUM_5);
                }
-               else if (this.health <= 60)
+               else if (hp <= 60)
                {
                        Damage(this, this, this, 10, DEATH_NOAMMO.m_id, this.origin, '0 0 0');
                        Send_Notification(NOTIF_ONE, this, MSG_ANNCE, ANNCE_NUM_6);
                }
-               else if (this.health <= 70)
+               else if (hp <= 70)
                {
                        Damage(this, this, this, 10, DEATH_NOAMMO.m_id, this.origin, '0 0 0');
                        Send_Notification(NOTIF_ONE, this, MSG_ANNCE, ANNCE_NUM_7);
                }
-               else if (this.health <= 80)
+               else if (hp <= 80)
                {
                        Damage(this, this, this, 10, DEATH_NOAMMO.m_id, this.origin, '0 0 0');
                        Send_Notification(NOTIF_ONE, this, MSG_ANNCE, ANNCE_NUM_8);
                }
-               else if (this.health <= 90)
+               else if (hp <= 90)
                {
                        Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_INSTAGIB_FINDAMMO);
                        Damage(this, this, this, 10, DEATH_NOAMMO.m_id, this.origin, '0 0 0');
@@ -289,13 +295,15 @@ MUTATOR_HOOKFUNCTION(mutator_instagib, Damage_Calculate)
                        if(!autocvar_g_instagib_friendlypush && SAME_TEAM(frag_target, frag_attacker))
                                frag_force = '0 0 0';
 
-                       if(frag_target.armorvalue)
+                       float armor = GetResourceAmount(frag_target, RESOURCE_ARMOR);
+                       if(armor)
                        {
-                               frag_target.armorvalue -= 1;
+                               armor -= 1;
+                               SetResourceAmount(frag_target, RESOURCE_ARMOR, armor);
                                frag_damage = 0;
                                frag_target.damage_dealt += 1;
                                frag_attacker.damage_dealt += 1;
-                               Send_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CENTER_INSTAGIB_LIVES_REMAINING, frag_target.armorvalue);
+                               Send_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CENTER_INSTAGIB_LIVES_REMAINING, armor);
                        }
                }
 
@@ -312,7 +320,7 @@ MUTATOR_HOOKFUNCTION(mutator_instagib, Damage_Calculate)
 
                                if(frag_target != frag_attacker)
                                {
-                                       if(frag_damage <= 0 && frag_target.health > 0) { Send_Notification(NOTIF_ONE, frag_attacker, MSG_CENTER, CENTER_SECONDARY_NODAMAGE); }
+                                       if(frag_damage <= 0 && GetResourceAmount(frag_target, RESOURCE_HEALTH) > 0) { Send_Notification(NOTIF_ONE, frag_attacker, MSG_CENTER, CENTER_SECONDARY_NODAMAGE); }
                                        if(!autocvar_g_instagib_blaster_keepforce)
                                                frag_force = '0 0 0';
                                }
@@ -325,10 +333,12 @@ MUTATOR_HOOKFUNCTION(mutator_instagib, Damage_Calculate)
        if(frag_mirrordamage > 0)
        {
                // just lose extra LIVES, don't kill the player for mirror damage
-               if(frag_attacker.armorvalue > 0)
+               float armor = GetResourceAmount(frag_attacker, RESOURCE_ARMOR);
+               if(armor > 0)
                {
-                       frag_attacker.armorvalue -= 1;
-                       Send_Notification(NOTIF_ONE, frag_attacker, MSG_CENTER, CENTER_INSTAGIB_LIVES_REMAINING, frag_attacker.armorvalue);
+                       armor -= 1;
+                       SetResourceAmount(frag_attacker, RESOURCE_ARMOR, armor);
+                       Send_Notification(NOTIF_ONE, frag_attacker, MSG_CENTER, CENTER_INSTAGIB_LIVES_REMAINING, armor);
                        frag_attacker.damage_dealt += frag_mirrordamage;
                }
                frag_mirrordamage = 0;
@@ -415,7 +425,7 @@ MUTATOR_HOOKFUNCTION(mutator_instagib, FilterItem)
 
        if(item.weapon == WEP_VAPORIZER.m_id && item.classname == "droppedweapon")
        {
-               item.ammo_cells = autocvar_g_instagib_ammo_drop;
+               SetResourceAmount(item, RESOURCE_CELLS, autocvar_g_instagib_ammo_drop);
                return false;
        }
 
@@ -428,10 +438,11 @@ MUTATOR_HOOKFUNCTION(mutator_instagib, FilterItem)
        if(item.flags & FL_POWERUP)
                return false;
 
-       if(item.ammo_cells > autocvar_g_instagib_ammo_drop && item.classname != "item_minst_cells")
-               item.ammo_cells = autocvar_g_instagib_ammo_drop;
+       float cells = GetResourceAmount(item, RESOURCE_CELLS);
+       if(cells > autocvar_g_instagib_ammo_drop && item.classname != "item_minst_cells")
+               SetResourceAmount(item, RESOURCE_CELLS, autocvar_g_instagib_ammo_drop);
 
-       if(item.ammo_cells && !item.weapon)
+       if(cells && !item.weapon)
                return false;
 
        return true;
@@ -464,26 +475,27 @@ MUTATOR_HOOKFUNCTION(mutator_instagib, ItemTouch)
        entity item = M_ARGV(0, entity);
        entity toucher = M_ARGV(1, entity);
 
-       if(item.ammo_cells)
+       if(GetResourceAmount(item, RESOURCE_CELLS))
        {
                // play some cool sounds ;)
+               float hp = GetResourceAmount(toucher, RESOURCE_HEALTH);
                if (IS_CLIENT(toucher))
                {
-                       if(toucher.health <= 5)
+                       if(hp <= 5)
                                Send_Notification(NOTIF_ONE, toucher, MSG_ANNCE, ANNCE_INSTAGIB_LASTSECOND);
-                       else if(toucher.health < 50)
+                       else if(hp < 50)
                                Send_Notification(NOTIF_ONE, toucher, MSG_ANNCE, ANNCE_INSTAGIB_NARROWLY);
                }
 
-               if(toucher.health < 100)
-                       toucher.health = 100;
+               if(hp < 100)
+                       SetResourceAmount(toucher, RESOURCE_HEALTH, 100);
 
                return MUT_ITEMTOUCH_CONTINUE;
        }
 
        if(item.itemdef == ITEM_ExtraLife)
        {
-               toucher.armorvalue = bound(toucher.armorvalue, 999, toucher.armorvalue + autocvar_g_instagib_extralives);
+               GiveResource(toucher, RESOURCE_ARMOR, autocvar_g_instagib_extralives);
                Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_EXTRALIVES);
                return MUT_ITEMTOUCH_PICKUP;
        }
index 23e0d0d..e68c687 100644 (file)
@@ -1,6 +1,7 @@
 #include "sv_invincibleproj.qh"
 
-REGISTER_MUTATOR(invincibleprojectiles, cvar("g_invincible_projectiles"));
+string autocvar_g_invincible_projectiles;
+REGISTER_MUTATOR(invincibleprojectiles, expr_evaluate(autocvar_g_invincible_projectiles));
 
 MUTATOR_HOOKFUNCTION(invincibleprojectiles, EditProjectile)
 {
index a542921..ac06a8f 100644 (file)
@@ -1,6 +1,7 @@
 #include "sv_melee_only.qh"
 
-REGISTER_MUTATOR(melee_only, cvar("g_melee_only") && !cvar("g_instagib") && !cvar("g_overkill") && !g_nexball);
+string autocvar_g_melee_only;
+REGISTER_MUTATOR(melee_only, expr_evaluate(autocvar_g_melee_only) && !cvar("g_instagib") && !cvar("g_overkill") && !g_nexball);
 
 MUTATOR_HOOKFUNCTION(melee_only, SetStartItems, CBC_ORDER_LAST)
 {
index 92bbacc..54b3673 100644 (file)
@@ -1,8 +1,9 @@
 #include "sv_midair.qh"
 
+string autocvar_g_midair;
 float autocvar_g_midair_shieldtime;
 
-REGISTER_MUTATOR(midair, cvar("g_midair"));
+REGISTER_MUTATOR(midair, expr_evaluate(autocvar_g_midair));
 
 .float midair_shieldtime;
 
index 47dcfd4..081a1fd 100644 (file)
@@ -9,7 +9,7 @@
 
 
 #if defined(SVQC)
-REGISTER_MUTATOR(multijump, cvar("g_multijump"));
+REGISTER_MUTATOR(multijump, autocvar_g_multijump);
 #elif defined(CSQC)
 REGISTER_MUTATOR(multijump, true);
 #endif
index 18edd48..92e16b4 100644 (file)
@@ -81,7 +81,7 @@ MUTATOR_HOOKFUNCTION(cl_nades, EditProjectile)
 
        entity nade_type = Nade_FromProjectile(proj.cnt);
        if (nade_type == NADE_TYPE_Null) return;
-       if(STAT(NADES_SMALL, NULL))
+       if(STAT(NADES_SMALL))
        {
                proj.mins = '-8 -8 -8';
                proj.maxs = '8 8 8';
@@ -150,7 +150,7 @@ void DrawAmmoNades(vector myPos, vector mySize, bool draw_expanding, float expan
 #include <common/monsters/sv_monsters.qh>
 #include <server/g_subs.qh>
 
-REGISTER_MUTATOR(nades, cvar("g_nades"));
+REGISTER_MUTATOR(nades, autocvar_g_nades);
 
 .float nade_time_primed;
 .float nade_lifetime;
@@ -431,7 +431,7 @@ void nade_ice_think(entity this)
 
        float current_freeze_time = this.ltime - time - 0.1;
 
-       FOREACH_ENTITY_RADIUS(this.origin, autocvar_g_nades_nade_radius, it != this && it.takedamage && !IS_DEAD(it) && it.health > 0 && current_freeze_time > 0,
+       FOREACH_ENTITY_RADIUS(this.origin, autocvar_g_nades_nade_radius, it != this && it.takedamage && !IS_DEAD(it) && GetResourceAmount(it, RESOURCE_HEALTH) > 0 && current_freeze_time > 0,
        {
                if(!autocvar_g_nades_ice_teamcheck || (DIFF_TEAM(it, this.realowner) || it == this.realowner))
                if(!it.revival_time || ((time - it.revival_time) >= 1.5))
@@ -623,13 +623,15 @@ void nade_heal_touch(entity this, entity toucher)
                if ( health_factor > 0 )
                {
                        maxhealth = (IS_MONSTER(toucher)) ? toucher.max_health : g_pickup_healthmega_max;
-                       if ( toucher.health < maxhealth )
+                       float hp = GetResourceAmount(toucher, RESOURCE_HEALTH);
+                       if (hp < maxhealth)
                        {
-                               if ( this.nade_show_particles )
+                               if (this.nade_show_particles)
+                               {
                                        Send_Effect(EFFECT_HEALING, toucher.origin, '0 0 0', 1);
-                               toucher.health = min(toucher.health+health_factor, maxhealth);
+                               }
+                               GiveResourceWithLimit(toucher, RESOURCE_HEALTH, health_factor, maxhealth);
                        }
-                       toucher.pauserothealth_finished = max(toucher.pauserothealth_finished, time + autocvar_g_balance_pause_health_rot);
                }
                else if ( health_factor < 0 )
                {
@@ -768,7 +770,7 @@ void nade_touch(entity this, entity toucher)
 
        if(autocvar_g_nades_pickup)
        if(time >= this.spawnshieldtime)
-       if(!toucher.nade && this.health == this.max_health) // no boosted shot pickups, thank you very much
+       if(!toucher.nade && GetResourceAmount(this, RESOURCE_HEALTH) == this.max_health) // no boosted shot pickups, thank you very much
        if(CanThrowNade(toucher)) // prevent some obvious things, like dead players
        if(IS_REAL_CLIENT(toucher)) // above checks for IS_PLAYER, don't need to do it here
        {
@@ -796,7 +798,7 @@ void nade_touch(entity this, entity toucher)
 
        //setsize(this, '-2 -2 -2', '2 2 2');
        //UpdateCSQCProjectile(this);
-       if(this.health == this.max_health)
+       if(GetResourceAmount(this, RESOURCE_HEALTH) == this.max_health)
        {
                spamsound(this, CH_SHOTS, SND_GRENADE_BOUNCE_RANDOM(), VOL_BASE, ATTEN_NORM);
                return;
@@ -860,19 +862,22 @@ void nade_damage(entity this, entity inflictor, entity attacker, float damage, i
        if(damage <= 0 || ((IS_ONGROUND(this)) && IS_PLAYER(attacker)))
                return;
 
-       if(this.health == this.max_health)
+       float hp = GetResourceAmount(this, RESOURCE_HEALTH);
+       if(hp == this.max_health)
        {
                sound(this, CH_SHOTS_SINGLE, SND_Null, VOL_BASE, 0.5 *(ATTEN_LARGE + ATTEN_MAX));
                this.nextthink = max(time + this.nade_lifetime, time);
                setthink(this, nade_beep);
        }
 
-       this.health -= damage;
+       hp -= damage;
+       SetResourceAmount(this, RESOURCE_HEALTH, hp);
+
 
        if ( this.nade_type != NADE_TYPE_HEAL.m_id || IS_PLAYER(attacker) )
                this.realowner = attacker;
 
-       if(this.health <= 0)
+       if(hp <= 0)
                W_PrepareExplosionByDamage(this, attacker, nade_boom);
        else
                nade_burn_spawn(this);
@@ -928,7 +933,7 @@ void toss_nade(entity e, bool set_owner, vector _velocity, float _time)
 
        settouch(_nade, nade_touch);
        _nade.spawnshieldtime = time + 0.1; // prevent instantly picking up again
-       _nade.health = autocvar_g_nades_nade_health;
+       SetResourceAmount(_nade, RESOURCE_HEALTH, autocvar_g_nades_nade_health);
        _nade.max_health = _nade.health;
        _nade.takedamage = DAMAGE_AIM;
        _nade.event_damage = nade_damage;
@@ -1294,7 +1299,7 @@ MUTATOR_HOOKFUNCTION(nades, PlayerPreThink)
        if(n && STAT(FROZEN, player) == 3) // OK, there is at least one teammate reviving us
        {
                player.revive_progress = bound(0, player.revive_progress + frametime * max(1/60, autocvar_g_freezetag_revive_speed), 1);
-               player.health = max(1, player.revive_progress * start_health);
+               SetResourceAmount(player, RESOURCE_HEALTH, max(1, player.revive_progress * start_health));
 
                if(player.revive_progress >= 1)
                {
@@ -1411,7 +1416,7 @@ MUTATOR_HOOKFUNCTION(nades, Damage_Calculate)
        if(time - frag_inflictor.toss_time <= 0.1)
        {
                Unfreeze(frag_target);
-               frag_target.health = autocvar_g_freezetag_revive_nade_health;
+               SetResourceAmount(frag_target, RESOURCE_HEALTH, autocvar_g_freezetag_revive_nade_health);
                Send_Effect(EFFECT_ICEORGLASS, frag_target.origin, '0 0 0', 3);
                M_ARGV(4, float) = 0;
                M_ARGV(6, vector) = '0 0 0';
index 288c2d5..af36499 100644 (file)
@@ -68,9 +68,11 @@ roflsound "New toys, new toys!" sound.
 
 */
 
+string autocvar_g_new_toys;
+
 bool nt_IsNewToy(int w);
 
-REGISTER_MUTATOR(nt, cvar("g_new_toys") && !cvar("g_instagib") && !cvar("g_overkill"))
+REGISTER_MUTATOR(nt, expr_evaluate(autocvar_g_new_toys) && !cvar("g_instagib") && !cvar("g_overkill"))
 {
        MUTATOR_ONADD
        {
index 425b8c2..4de24a5 100644 (file)
@@ -1,5 +1,6 @@
 #include "sv_nix.qh"
 
+string autocvar_g_nix;
 int autocvar_g_balance_nix_ammo_cells;
 int autocvar_g_balance_nix_ammo_plasma;
 int autocvar_g_balance_nix_ammo_fuel;
@@ -35,7 +36,7 @@ float nix_nextweapon;
 
 bool NIX_CanChooseWeapon(int wpn);
 
-REGISTER_MUTATOR(nix, cvar("g_nix") && !cvar("g_instagib") && !cvar("g_overkill"))
+REGISTER_MUTATOR(nix, expr_evaluate(autocvar_g_nix) && !cvar("g_instagib") && !cvar("g_overkill"))
 {
        MUTATOR_ONADD
        {
@@ -56,12 +57,12 @@ REGISTER_MUTATOR(nix, cvar("g_nix") && !cvar("g_instagib") && !cvar("g_overkill"
        {
                // as the PlayerSpawn hook will no longer run, NIX is turned off by this!
                FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
-                       it.ammo_cells = start_ammo_cells;
-                       it.ammo_plasma = start_ammo_plasma;
-                       it.ammo_shells = start_ammo_shells;
-                       it.ammo_nails = start_ammo_nails;
-                       it.ammo_rockets = start_ammo_rockets;
-                       it.ammo_fuel = start_ammo_fuel;
+                       SetResourceAmount(it, RESOURCE_SHELLS, start_ammo_shells);
+                       SetResourceAmount(it, RESOURCE_BULLETS, start_ammo_nails);
+                       SetResourceAmount(it, RESOURCE_ROCKETS, start_ammo_rockets);
+                       SetResourceAmount(it, RESOURCE_CELLS, start_ammo_cells);
+                       SetResourceAmount(it, RESOURCE_PLASMA, start_ammo_plasma);
+                       SetResourceAmount(it, RESOURCE_FUEL, start_ammo_fuel);
                        it.weapons = start_weapons;
                        for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
                        {
@@ -133,30 +134,34 @@ void NIX_GiveCurrentWeapon(entity this)
 
        if(nix_nextchange != this.nix_lastchange_id) // this shall only be called once per round!
        {
-               this.ammo_shells = this.ammo_nails = this.ammo_rockets = this.ammo_cells = this.ammo_plasma = this.ammo_fuel = 0;
-
+               SetResourceAmount(this, RESOURCE_SHELLS, 0);
+               SetResourceAmount(this, RESOURCE_BULLETS, 0);
+               SetResourceAmount(this, RESOURCE_ROCKETS, 0);
+               SetResourceAmount(this, RESOURCE_CELLS, 0);
+               SetResourceAmount(this, RESOURCE_PLASMA, 0);
+               SetResourceAmount(this, RESOURCE_FUEL, 0);
                if(this.items & IT_UNLIMITED_WEAPON_AMMO)
                {
                        switch(e.ammo_field)
                        {
-                               case ammo_shells:  this.ammo_shells  = autocvar_g_pickup_shells_max;  break;
-                               case ammo_nails:   this.ammo_nails   = autocvar_g_pickup_nails_max;   break;
-                               case ammo_rockets: this.ammo_rockets = autocvar_g_pickup_rockets_max; break;
-                               case ammo_cells:   this.ammo_cells   = autocvar_g_pickup_cells_max;   break;
-                               case ammo_plasma:  this.ammo_plasma  = autocvar_g_pickup_plasma_max;   break;
-                               case ammo_fuel:    this.ammo_fuel    = autocvar_g_pickup_fuel_max;    break;
+                               case ammo_shells:  SetResourceAmount(this, RESOURCE_SHELLS, autocvar_g_pickup_shells_max);  break;
+                               case ammo_nails:   SetResourceAmount(this, RESOURCE_BULLETS, autocvar_g_pickup_nails_max);   break;
+                               case ammo_rockets: SetResourceAmount(this, RESOURCE_ROCKETS, autocvar_g_pickup_rockets_max); break;
+                               case ammo_cells:   SetResourceAmount(this, RESOURCE_CELLS, autocvar_g_pickup_cells_max);   break;
+                               case ammo_plasma:  SetResourceAmount(this, RESOURCE_PLASMA, autocvar_g_pickup_plasma_max);   break;
+                               case ammo_fuel:    SetResourceAmount(this, RESOURCE_FUEL, autocvar_g_pickup_fuel_max);    break;
                        }
                }
                else
                {
                        switch(e.ammo_field)
                        {
-                               case ammo_shells:  this.ammo_shells  = autocvar_g_balance_nix_ammo_shells;  break;
-                               case ammo_nails:   this.ammo_nails   = autocvar_g_balance_nix_ammo_nails;   break;
-                               case ammo_rockets: this.ammo_rockets = autocvar_g_balance_nix_ammo_rockets; break;
-                               case ammo_cells:   this.ammo_cells   = autocvar_g_balance_nix_ammo_cells;   break;
-                               case ammo_plasma:  this.ammo_plasma  = autocvar_g_balance_nix_ammo_plasma;   break;
-                               case ammo_fuel:    this.ammo_fuel    = autocvar_g_balance_nix_ammo_fuel;    break;
+                               case ammo_shells:  SetResourceAmount(this, RESOURCE_SHELLS, autocvar_g_balance_nix_ammo_shells);  break;
+                               case ammo_nails:   SetResourceAmount(this, RESOURCE_BULLETS, autocvar_g_balance_nix_ammo_nails);   break;
+                               case ammo_rockets: SetResourceAmount(this, RESOURCE_ROCKETS, autocvar_g_balance_nix_ammo_rockets); break;
+                               case ammo_cells:   SetResourceAmount(this, RESOURCE_CELLS, autocvar_g_balance_nix_ammo_cells);   break;
+                               case ammo_plasma:  SetResourceAmount(this, RESOURCE_PLASMA, autocvar_g_balance_nix_ammo_plasma);   break;
+                               case ammo_fuel:    SetResourceAmount(this, RESOURCE_FUEL, autocvar_g_balance_nix_ammo_fuel);    break;
                        }
                }
 
@@ -204,12 +209,12 @@ void NIX_GiveCurrentWeapon(entity this)
        {
                switch(e.ammo_field)
                {
-                       case ammo_shells:  this.ammo_shells  += autocvar_g_balance_nix_ammoincr_shells;  break;
-                       case ammo_nails:   this.ammo_nails   += autocvar_g_balance_nix_ammoincr_nails;   break;
-                       case ammo_rockets: this.ammo_rockets += autocvar_g_balance_nix_ammoincr_rockets; break;
-                       case ammo_cells:   this.ammo_cells   += autocvar_g_balance_nix_ammoincr_cells;   break;
-                       case ammo_plasma:  this.ammo_plasma  += autocvar_g_balance_nix_ammoincr_plasma;   break;
-                       case ammo_fuel:    this.ammo_fuel    += autocvar_g_balance_nix_ammoincr_fuel;    break;
+                       case ammo_shells:  GiveResource(this, RESOURCE_SHELLS, autocvar_g_balance_nix_ammoincr_shells);  break;
+                       case ammo_nails:   GiveResource(this, RESOURCE_BULLETS, autocvar_g_balance_nix_ammoincr_nails);   break;
+                       case ammo_rockets: GiveResource(this, RESOURCE_ROCKETS, autocvar_g_balance_nix_ammoincr_rockets); break;
+                       case ammo_cells:   GiveResource(this, RESOURCE_CELLS, autocvar_g_balance_nix_ammoincr_cells);   break;
+                       case ammo_plasma:  GiveResource(this, RESOURCE_PLASMA, autocvar_g_balance_nix_ammoincr_plasma);   break;
+                       case ammo_fuel:    GiveResource(this, RESOURCE_FUEL, autocvar_g_balance_nix_ammoincr_fuel);    break;
                }
 
                this.nix_nextincr = time + autocvar_g_balance_nix_incrtime;
index d2a7308..b47e587 100644 (file)
@@ -3,6 +3,8 @@
 #include "hmg.qh"
 #include "rpc.qh"
 
+string autocvar_g_overkill;
+
 bool autocvar_g_overkill_powerups_replace;
 
 bool autocvar_g_overkill_itemwaypoints = true;
@@ -18,7 +20,7 @@ bool autocvar_g_overkill_filter_armormega;
 
 void ok_Initialize();
 
-REGISTER_MUTATOR(ok, cvar("g_overkill") && !cvar("g_instagib") && !g_nexball && cvar_string("g_mod_balance") == "Overkill")
+REGISTER_MUTATOR(ok, expr_evaluate(autocvar_g_overkill) && !cvar("g_instagib") && !g_nexball && cvar_string("g_mod_balance") == "Overkill")
 {
        MUTATOR_ONADD
        {
index 62781c9..38cd747 100644 (file)
@@ -4,7 +4,7 @@ int autocvar_g_physical_items;
 float autocvar_g_physical_items_damageforcescale;
 float autocvar_g_physical_items_reset;
 
-REGISTER_MUTATOR(physical_items, cvar("g_physical_items"))
+REGISTER_MUTATOR(physical_items, autocvar_g_physical_items)
 {
        // check if we have a physics engine
        MUTATOR_ONADD
index 53e4f49..1084ff7 100644 (file)
@@ -1,6 +1,7 @@
 #include "sv_pinata.qh"
 
-REGISTER_MUTATOR(pinata, cvar("g_pinata") && !cvar("g_instagib") && !cvar("g_overkill"));
+string autocvar_g_pinata;
+REGISTER_MUTATOR(pinata, expr_evaluate(autocvar_g_pinata) && !cvar("g_instagib") && !cvar("g_overkill"));
 
 MUTATOR_HOOKFUNCTION(pinata, PlayerDies)
 {
index 9f0d8fb..d3c1922 100644 (file)
@@ -1,6 +1,7 @@
 #include "sv_rocketflying.qh"
 
-REGISTER_MUTATOR(rocketflying, cvar("g_rocket_flying"));
+string autocvar_g_rocket_flying;
+REGISTER_MUTATOR(rocketflying, expr_evaluate(autocvar_g_rocket_flying));
 
 MUTATOR_HOOKFUNCTION(rocketflying, EditProjectile)
 {
index 93dc602..d121cf1 100644 (file)
@@ -1,5 +1,6 @@
 #include "sv_sandbox.qh"
 
+string autocvar_g_sandbox;
 int autocvar_g_sandbox_info;
 bool autocvar_g_sandbox_readonly;
 string autocvar_g_sandbox_storage_name;
@@ -18,7 +19,7 @@ float autocvar_g_sandbox_object_material_velocity_factor;
 float autosave_time;
 void sandbox_Database_Load();
 
-REGISTER_MUTATOR(sandbox, cvar("g_sandbox"))
+REGISTER_MUTATOR(sandbox, expr_evaluate(autocvar_g_sandbox))
 {
        MUTATOR_ONADD
        {
index 9ff3644..61c302c 100644 (file)
@@ -2,6 +2,7 @@
 
 #include <lib/float.qh>
 
+string autocvar_g_spawn_near_teammate;
 float autocvar_g_spawn_near_teammate_distance;
 int autocvar_g_spawn_near_teammate_ignore_spawnpoint;
 int autocvar_g_spawn_near_teammate_ignore_spawnpoint_max;
@@ -10,7 +11,7 @@ float autocvar_g_spawn_near_teammate_ignore_spawnpoint_delay_death;
 bool autocvar_g_spawn_near_teammate_ignore_spawnpoint_check_health;
 bool autocvar_g_spawn_near_teammate_ignore_spawnpoint_closetodeath;
 
-REGISTER_MUTATOR(spawn_near_teammate, cvar("g_spawn_near_teammate"));
+REGISTER_MUTATOR(spawn_near_teammate, expr_evaluate(autocvar_g_spawn_near_teammate));
 
 .entity msnt_lookat;
 
@@ -20,6 +21,8 @@ REGISTER_MUTATOR(spawn_near_teammate, cvar("g_spawn_near_teammate"));
 
 MUTATOR_HOOKFUNCTION(spawn_near_teammate, Spawn_Score)
 {
+       if (!teamplay) return;
+
        entity player = M_ARGV(0, entity);
        entity spawn_spot = M_ARGV(1, entity);
        vector spawn_score = M_ARGV(2, vector);
@@ -29,9 +32,6 @@ MUTATOR_HOOKFUNCTION(spawn_near_teammate, Spawn_Score)
 
        spawn_spot.msnt_lookat = NULL;
 
-       if(!teamplay)
-               return;
-
        RandomSelection_Init();
        FOREACH_CLIENT(IS_PLAYER(it) && it != player && SAME_TEAM(it, player) && !IS_DEAD(it), {
                if(vdist(spawn_spot.origin - it.origin, >, autocvar_g_spawn_near_teammate_distance))
@@ -56,7 +56,8 @@ MUTATOR_HOOKFUNCTION(spawn_near_teammate, Spawn_Score)
 
 MUTATOR_HOOKFUNCTION(spawn_near_teammate, PlayerSpawn)
 {
-       if(!teamplay) { return; }
+       if (!teamplay) return;
+
        entity player = M_ARGV(0, entity);
        entity spawn_spot = M_ARGV(1, entity);
 
index 4c99095..eb20082 100644 (file)
@@ -1,6 +1,7 @@
 #include "sv_superspec.qh"
 
-REGISTER_MUTATOR(superspec, cvar("g_superspectate"));
+string autocvar_g_superspectate;
+REGISTER_MUTATOR(superspec, expr_evaluate(autocvar_g_superspectate));
 
 #define _SSMAGIX "SUPERSPEC_OPTIONSFILE_V1"
 #define _ISLOCAL(ent) ((edict_num(1) == (ent)) ? true : false)
index 3e6edb0..a1b38fb 100644 (file)
@@ -1,11 +1,12 @@
 #include "sv_touchexplode.qh"
 
+string autocvar_g_touchexplode;
 float autocvar_g_touchexplode_radius;
 float autocvar_g_touchexplode_damage;
 float autocvar_g_touchexplode_edgedamage;
 float autocvar_g_touchexplode_force;
 
-REGISTER_MUTATOR(touchexplode, cvar("g_touchexplode"));
+REGISTER_MUTATOR(touchexplode, expr_evaluate(autocvar_g_touchexplode));
 
 .float touchexplode_time;
 
index 92c5943..199b4e2 100644 (file)
@@ -1,6 +1,7 @@
 #include "sv_vampire.qh"
 
-REGISTER_MUTATOR(vampire, cvar("g_vampire") && !cvar("g_instagib"));
+string autocvar_g_vampire;
+REGISTER_MUTATOR(vampire, expr_evaluate(autocvar_g_vampire) && !cvar("g_instagib"));
 
 MUTATOR_HOOKFUNCTION(vampire, PlayerDamage_SplitHealthArmor)
 {
@@ -12,7 +13,8 @@ MUTATOR_HOOKFUNCTION(vampire, PlayerDamage_SplitHealthArmor)
        if(frag_target != frag_attacker)
        if(!IS_DEAD(frag_target))
        {
-               GivePlayerHealth(frag_attacker, bound(0, damage_take, frag_target.health));
+               GiveResource(frag_attacker, RESOURCE_HEALTH,
+                       bound(0, damage_take, frag_target.health));
        }
 }
 
index e2b0f57..ce9e270 100644 (file)
@@ -1,6 +1,7 @@
 #include "sv_vampirehook.qh"
 
-REGISTER_MUTATOR(vh, cvar("g_vampirehook"));
+string autocvar_g_vampirehook;
+REGISTER_MUTATOR(vh, expr_evaluate(autocvar_g_vampirehook));
 
 bool autocvar_g_vampirehook_teamheal;
 float autocvar_g_vampirehook_damage;
index b0d95ea..c462a7e 100644 (file)
@@ -4,7 +4,7 @@
 #ifdef CSQC
 REGISTER_MUTATOR(walljump, true);
 #elif defined(SVQC)
-REGISTER_MUTATOR(walljump, cvar("g_walljump"));
+REGISTER_MUTATOR(walljump, autocvar_g_walljump);
 #endif
 
 #define PHYS_WALLJUMP(s)                                               STAT(WALLJUMP, s)
index dbd765d..85912ee 100644 (file)
@@ -13,18 +13,18 @@ const int WATERLEVEL_SUBMERGED = 3;
 #define SET_ONSLICK(s)                                         ((s).flags |= FL_ONSLICK)
 #define UNSET_ONSLICK(s)                                       ((s).flags &= ~FL_ONSLICK)
 
-#define GAMEPLAYFIX_DOWNTRACEONGROUND(s)    STAT(GAMEPLAYFIX_DOWNTRACEONGROUND, NULL)
-#define GAMEPLAYFIX_EASIERWATERJUMP(s)      STAT(GAMEPLAYFIX_EASIERWATERJUMP, NULL)
-#define GAMEPLAYFIX_STEPDOWN(s)             STAT(GAMEPLAYFIX_STEPDOWN, NULL)
-#define GAMEPLAYFIX_STEPMULTIPLETIMES(s)    STAT(GAMEPLAYFIX_STEPMULTIPLETIMES, NULL)
-#define GAMEPLAYFIX_UNSTICKPLAYERS(s)       STAT(GAMEPLAYFIX_UNSTICKPLAYERS, NULL)
-#define GAMEPLAYFIX_WATERTRANSITION(s)                 STAT(GAMEPLAYFIX_WATERTRANSITION, NULL)
-#define UPWARD_VELOCITY_CLEARS_ONGROUND(s)  STAT(GAMEPLAYFIX_UPVELOCITYCLEARSONGROUND, NULL)
-
-#define PHYS_STEPHEIGHT(s)                  STAT(MOVEVARS_STEPHEIGHT, NULL)
-#define PHYS_NOSTEP(s)                      STAT(NOSTEP, NULL)
-#define PHYS_JUMPSTEP(s)                    STAT(MOVEVARS_JUMPSTEP, NULL)
-#define PHYS_WALLFRICTION(s)                STAT(MOVEVARS_WALLFRICTION, NULL)
+#define GAMEPLAYFIX_DOWNTRACEONGROUND(s)    STAT(GAMEPLAYFIX_DOWNTRACEONGROUND)
+#define GAMEPLAYFIX_EASIERWATERJUMP(s)      STAT(GAMEPLAYFIX_EASIERWATERJUMP)
+#define GAMEPLAYFIX_STEPDOWN(s)             STAT(GAMEPLAYFIX_STEPDOWN)
+#define GAMEPLAYFIX_STEPMULTIPLETIMES(s)    STAT(GAMEPLAYFIX_STEPMULTIPLETIMES)
+#define GAMEPLAYFIX_UNSTICKPLAYERS(s)       STAT(GAMEPLAYFIX_UNSTICKPLAYERS)
+#define GAMEPLAYFIX_WATERTRANSITION(s)                 STAT(GAMEPLAYFIX_WATERTRANSITION)
+#define UPWARD_VELOCITY_CLEARS_ONGROUND(s)  STAT(GAMEPLAYFIX_UPVELOCITYCLEARSONGROUND)
+
+#define PHYS_STEPHEIGHT(s)                  STAT(MOVEVARS_STEPHEIGHT)
+#define PHYS_NOSTEP(s)                      STAT(NOSTEP)
+#define PHYS_JUMPSTEP(s)                    STAT(MOVEVARS_JUMPSTEP)
+#define PHYS_WALLFRICTION(s)                STAT(MOVEVARS_WALLFRICTION)
 
 #ifdef CSQC
 .float bouncestop;
index d5a8e60..ae59381 100644 (file)
@@ -80,7 +80,7 @@ bool IsFlying(entity a);
 #define PHYS_JETPACK_MAXSPEED_UP(s)         STAT(JETPACK_MAXSPEED_UP, s)
 #define PHYS_JETPACK_REVERSE_THRUST(s)         STAT(JETPACK_REVERSE_THRUST, s)
 
-#define PHYS_JUMPSPEEDCAP_DISABLE_ONRAMPS(s) STAT(MOVEVARS_JUMPSPEEDCAP_DISABLE_ONRAMPS, NULL)
+#define PHYS_JUMPSPEEDCAP_DISABLE_ONRAMPS(s) STAT(MOVEVARS_JUMPSPEEDCAP_DISABLE_ONRAMPS)
 #define PHYS_JUMPVELOCITY(s)                STAT(MOVEVARS_JUMPVELOCITY, s)
 
 #define PHYS_MAXAIRSPEED(s)                 STAT(MOVEVARS_MAXAIRSPEED, s)
@@ -97,7 +97,7 @@ bool IsFlying(entity a);
 #define PHYS_WARSOWBUNNY_TOPSPEED(s)        STAT(MOVEVARS_WARSOWBUNNY_TOPSPEED, s)
 #define PHYS_WARSOWBUNNY_TURNACCEL(s)       STAT(MOVEVARS_WARSOWBUNNY_TURNACCEL, s)
 
-#define PHYS_SLICK_APPLYGRAVITY(s)             STAT(SLICK_APPLYGRAVITY, NULL)
+#define PHYS_SLICK_APPLYGRAVITY(s)             STAT(SLICK_APPLYGRAVITY)
 
 #define PHYS_INPUT_BUTTON_ATCK(s)           PHYS_INPUT_BUTTON_BUTTON1(s)
 #define PHYS_INPUT_BUTTON_JUMP(s)           PHYS_INPUT_BUTTON_BUTTON2(s)
index 6a6ce3f..c116b44 100644 (file)
@@ -682,146 +682,29 @@ void Item_ScheduleInitialRespawn(entity e)
        Item_ScheduleRespawnIn(e, max(0, game_starttime - time) + ((e.respawntimestart) ? e.respawntimestart : ITEM_RESPAWNTIME_INITIAL(e)));
 }
 
-void GivePlayerResource(entity player, .float resource_type, float amount)
+float Item_GiveAmmoTo(entity item, entity player, int resource_type, float ammomax)
 {
+       float amount = GetResourceAmount(item, resource_type);
        if (amount == 0)
        {
-               return;
-       }
-       switch (resource_type)
-       {
-               case health:
-               {
-                       // Ugly hack. We do not check if health goes beyond hard limit since
-                       // currently it is done in player_regen. We need to bring back this
-                       // check when other code is ported to this function.
-                       player.health = bound(player.health, player.health + amount,
-                               autocvar_g_balance_health_limit);
-                       // Correct code:
-                       //player.health = bound(player.health, player.health + amount,
-                       //      min(autocvar_g_balance_health_limit,
-                       //      RESOURCE_AMOUNT_HARD_LIMIT));
-                       player.pauserothealth_finished = max(player.pauserothealth_finished,
-                               time + autocvar_g_balance_pause_health_rot);
-                       return;
-               }
-               case armorvalue:
-               {
-                       // Ugly hack. We do not check if armor goes beyond hard limit since
-                       // currently it is done in player_regen. We need to bring back this
-                       // check when other code is ported to this function.
-                       player.armorvalue = bound(player.armorvalue, player.armorvalue +
-                               amount, autocvar_g_balance_armor_limit);
-                       // Correct code:
-                       //player.armorvalue = bound(player.armorvalue, player.armorvalue +
-                       //      amount, min(autocvar_g_balance_armor_limit,
-                       //      RESOURCE_AMOUNT_HARD_LIMIT));
-                       player.pauserotarmor_finished = max(player.pauserotarmor_finished,
-                               time + autocvar_g_balance_pause_armor_rot);
-                       return;
-               }
-               case ammo_shells:
-               case ammo_nails:
-               case ammo_rockets:
-               case ammo_cells:
-               case ammo_plasma:
-               {
-                       GivePlayerAmmo(player, resource_type, amount);
-                       return;
-               }
-               case ammo_fuel:
-               {
-                       player.ammo_fuel = bound(player.ammo_fuel, player.ammo_fuel +
-                               amount, min(g_pickup_fuel_max, RESOURCE_AMOUNT_HARD_LIMIT));
-                       player.pauserotfuel_finished = max(player.pauserotfuel_finished,
-                               time + autocvar_g_balance_pause_fuel_rot);
-                       return;
-               }
-       }
-}
-
-void GivePlayerHealth(entity player, float amount)
-{
-       GivePlayerResource(player, health, amount);
-}
-
-void GivePlayerArmor(entity player, float amount)
-{
-       GivePlayerResource(player, armorvalue, amount);
-}
-
-void GivePlayerAmmo(entity player, .float ammotype, float amount)
-{
-       if (amount == 0)
-       {
-               return;
-       }
-       float maxvalue = RESOURCE_AMOUNT_HARD_LIMIT;
-       switch (ammotype)
-       {
-               case ammo_shells:
-               {
-                       maxvalue = g_pickup_shells_max;
-                       break;
-               }
-               case ammo_cells:
-               {
-                       maxvalue = g_pickup_cells_max;
-                       break;
-               }
-               case ammo_rockets:
-               {
-                       maxvalue = g_pickup_rockets_max;
-                       break;
-               }
-               case ammo_plasma:
-               {
-                       maxvalue = g_pickup_plasma_max;
-                       break;
-               }
-               case ammo_nails:
-               {
-                       maxvalue = g_pickup_nails_max;
-                       break;
-               }
-       }
-       player.(ammotype) = min(player.(ammotype) + amount,
-               min(maxvalue, RESOURCE_AMOUNT_HARD_LIMIT));
-}
-
-void GivePlayerFuel(entity player, float amount)
-{
-       GivePlayerResource(player, ammo_fuel, amount);
-}
-
-float Item_GiveAmmoTo(entity item, entity player, .float ammotype, float ammomax)
-{
-       if (!item.(ammotype))
                return false;
-
+       }
+       float player_amount = GetResourceAmount(player, resource_type);
        if (item.spawnshieldtime)
        {
-               if ((player.(ammotype) < ammomax) || item.pickup_anyway > 0)
+               if ((player_amount >= ammomax) && (item.pickup_anyway <= 0))
                {
-                       float amount = item.(ammotype);
-                       if ((player.(ammotype) + amount) > ammomax)
-                       {
-                               amount = ammomax - player.(ammotype);
-                       }
-                       GivePlayerResource(player, ammotype, amount);
-                       return true;
+                       return false;
                }
+               GiveResourceWithLimit(player, resource_type, amount, ammomax);
+               return true;
        }
-       else if(g_weapon_stay == 2)
+       if (g_weapon_stay != 2)
        {
-               float mi = min(item.(ammotype), ammomax);
-               if (player.(ammotype) < mi)
-               {
-                       GivePlayerResource(player, ammotype, mi - player.(ammotype));
-               }
-               return true;
+               return false;
        }
-       return false;
+       GiveResourceWithLimit(player, resource_type, amount, min(amount, ammomax));
+       return true;
 }
 
 float Item_GiveTo(entity item, entity player)
@@ -850,14 +733,14 @@ float Item_GiveTo(entity item, entity player)
                        }
                }
        }
-       pickedup |= Item_GiveAmmoTo(item, player, health, item.max_health);
-       pickedup |= Item_GiveAmmoTo(item, player, armorvalue, item.max_armorvalue);
-       pickedup |= Item_GiveAmmoTo(item, player, ammo_shells, g_pickup_shells_max);
-       pickedup |= Item_GiveAmmoTo(item, player, ammo_nails, g_pickup_nails_max);
-       pickedup |= Item_GiveAmmoTo(item, player, ammo_rockets, g_pickup_rockets_max);
-       pickedup |= Item_GiveAmmoTo(item, player, ammo_cells, g_pickup_cells_max);
-       pickedup |= Item_GiveAmmoTo(item, player, ammo_plasma, g_pickup_plasma_max);
-       pickedup |= Item_GiveAmmoTo(item, player, ammo_fuel, g_pickup_fuel_max);
+       pickedup |= Item_GiveAmmoTo(item, player, RESOURCE_HEALTH, item.max_health);
+       pickedup |= Item_GiveAmmoTo(item, player, RESOURCE_ARMOR, item.max_armorvalue);
+       pickedup |= Item_GiveAmmoTo(item, player, RESOURCE_SHELLS, g_pickup_shells_max);
+       pickedup |= Item_GiveAmmoTo(item, player, RESOURCE_BULLETS, g_pickup_nails_max);
+       pickedup |= Item_GiveAmmoTo(item, player, RESOURCE_ROCKETS, g_pickup_rockets_max);
+       pickedup |= Item_GiveAmmoTo(item, player, RESOURCE_CELLS, g_pickup_cells_max);
+       pickedup |= Item_GiveAmmoTo(item, player, RESOURCE_PLASMA, g_pickup_plasma_max);
+       pickedup |= Item_GiveAmmoTo(item, player, RESOURCE_FUEL, g_pickup_fuel_max);
        if (item.itemdef.instanceOfWeaponPickup)
        {
                WepSet w;
@@ -1018,6 +901,7 @@ LABEL(pickup)
                                if(it.itemdef) // is a registered item
                                {
                                        Item_Show(it, -1);
+                                       it.scheduledrespawntime = 0;
                                        RandomSelection_AddEnt(it, it.cnt, 0);
                                }
                        });
index f557e10..fa78ff4 100644 (file)
@@ -4,9 +4,6 @@
 #include <server/defs.qh>
 #endif
 
-/// \brief Unconditional maximum amount of resources the player can have.
-const int RESOURCE_AMOUNT_HARD_LIMIT = 999;
-
 const int AMMO_COUNT = 4; // amount of ammo types to show in the inventory panel
 
 // item networking
@@ -87,39 +84,7 @@ void Item_ScheduleRespawn(entity e);
 
 void Item_ScheduleInitialRespawn(entity e);
 
-/// \brief Gives player a resource such as health, armor or ammo.
-/// \param[in,out] player Player to give resource to.
-/// \param[in] resource_type Type of the resource.
-/// \param[in] amount Amount of resource to give.
-/// \return No return.
-void GivePlayerResource(entity player, .float resource_type, float amount);
-
-/// \brief Gives health to the player.
-/// \param[in,out] player Player to give health to.
-/// \param[in] amount Amount of health to give.
-/// \return No return.
-void GivePlayerHealth(entity player, float amount);
-
-/// \brief Gives armor to the player.
-/// \param[in,out] player Player to give armor to.
-/// \param[in] amount Amount of armor to give.
-/// \return No return.
-void GivePlayerArmor(entity player, float amount);
-
-/// \brief Gives ammo of the specified type to the player.
-/// \param[in,out] player Player to give ammo to.
-/// \param[in] type Ammo type property.
-/// \param[in] amount Amount of ammo to give.
-/// \return No return.
-void GivePlayerAmmo(entity player, .float ammotype, float amount);
-
-/// \brief Gives fuel to the player.
-/// \param[in,out] player Player to give fuel to.
-/// \param[in] amount Amount of fuel to give.
-/// \return No return.
-void GivePlayerFuel(entity player, float amount);
-
-float Item_GiveAmmoTo(entity item, entity player, .float ammotype, float ammomax);
+float Item_GiveAmmoTo(entity item, entity player, int resource_type, float ammomax);
 
 float Item_GiveTo(entity item, entity player);
 
index afbf79e..6605f00 100644 (file)
@@ -530,7 +530,9 @@ void CL_WeaponEntity_SetModel(entity this, string name, bool _anim)
        int compressed_shotorg = compressShotOrigin(this.movedir);
        // make them match perfectly
 #ifdef SVQC
-       this.movedir = decompressShotOrigin(this.owner.stat_shotorg = compressed_shotorg);
+    // null during init
+    if (this.owner) this.owner.stat_shotorg = compressed_shotorg;
+       this.movedir = decompressShotOrigin(compressed_shotorg);
 #else
        this.movedir = decompressShotOrigin(compressed_shotorg);
 #endif
index 73cd00d..5a782b9 100644 (file)
@@ -41,3 +41,5 @@ const int PROJECTILE_ARC_BOLT = 35;
 // projectile IDs 40-50 reserved
 
 const int PROJECTILE_RPC = 60;
+
+// projectile IDs 70-100 reserved
index 16fd934..70e5f37 100644 (file)
@@ -11,8 +11,8 @@
 #undef setcolor
 
 #ifdef MENUQC
-       #define NULL (0, null_entity)
+       #define NULL (RVALUE, null_entity)
        #define world NULL
 #else
-       #define NULL (0, world)
+       #define NULL (RVALUE, world)
 #endif
index 4da78f1..9d58209 100644 (file)
@@ -182,7 +182,17 @@ void make_safe_for_remove(entity this);
        #define SV_Shutdown _SV_Shutdown
 
        void _StartFrame();
-       void StartFrame() { if (_StartFrame) _StartFrame(); }
+       bool _StartFrame_init;
+       void spawnfunc_worldspawn(entity);
+       void StartFrame() {
+               if (!_StartFrame_init) {
+                       _StartFrame_init = true;
+                       float oldtime = time; time = 1;
+                       __spawnfunc_expecting = 2; NULL.__spawnfunc_constructor(NULL);
+                       time = oldtime;
+        }
+        if (_StartFrame) _StartFrame();
+       }
        #define StartFrame _StartFrame
 
        void _SetNewParms();
index 1541b99..f2ec6df 100644 (file)
@@ -9,6 +9,9 @@
     #define MACRO_END } while (0)
 #endif
 
+/** Marker for use in (RVALUE, (expr)) */
+#define RVALUE 0
+
 #define _CAT(a, b) a ## b
 #define CAT(a, b) _CAT(a, b)
 
index 21e0c52..6c29a4b 100644 (file)
@@ -10,6 +10,7 @@
 
        #include "p99.qh"
        #define OVERLOAD(F, ...) P99_IF_EMPTY(__VA_ARGS__)(P99_PASTE2(F, _00)())(P99_PASTE3(F, _, P00_NARG(__VA_ARGS__))(__VA_ARGS__))
+       /** for use within a macro */
        #define OVERLOAD_(F, ...) P99_IF_EMPTY(__VA_ARGS__)(P99_PASTE2(F, _00)())(P99_PASTE3(F, _, P00_NARG(__VA_ARGS__))(__VA_ARGS__))
 #else
        #define EVAL(...) __VA_ARGS__
index 0a3dd8c..b1f5326 100644 (file)
@@ -4,19 +4,30 @@
 #include "sort.qh"
 #include "yenc.qh"
 
+// netcode mismatch and not sure what caused it? developer_csqcentities 1
+
 .string netname;
 .int m_id;
 .bool(entity this, entity sender, bool isNew) m_read;
 #define NET_HANDLE(id, param) bool Net_Handle_##id(entity this, entity sender, param)
 
+#define NET_GUARD(id) \
+    bool Net_Handle_##id##_guard(entity this, entity sender, bool isNew) { \
+        bool valid = false; \
+        serialize_marker(to, valid); \
+        if (!valid) LOG_FATALF("Last message not fully parsed: %s", _net_prevmsgstr); \
+        _net_prevmsgstr = #id; \
+        return Net_Handle_##id(this, sender, isNew); \
+    }
 
 #ifdef CSQC
+string _net_prevmsgstr;
        #define REGISTER_NET_TEMP(id) \
                NET_HANDLE(id, bool); \
-               REGISTER(TempEntities, NET, id, m_id, new_pure(net_temp_packet)) \
-               { \
+        NET_GUARD(id); \
+               REGISTER(TempEntities, NET, id, m_id, new_pure(net_temp_packet)) { \
                        this.netname = #id; \
-                       this.m_read = Net_Handle_##id; \
+                       this.m_read = Net_Handle_##id##_guard; \
                }
 #else
        #define REGISTER_NET_TEMP(id) \
@@ -45,10 +56,11 @@ STATIC_INIT(RegisterTempEntities_renumber) { FOREACH(TempEntities, true, it.m_id
                        this.sourceLoc = __FILE__ ":" STR(__LINE__); \
                        if (!this) isnew = true; \
                } \
+               NET_GUARD(id); \
                REGISTER(LinkedEntities, NET, id, m_id, new_pure(net_linked_packet)) \
                { \
                        this.netname = #id; \
-                       this.m_read = Net_Handle_##id; \
+                       this.m_read = Net_Handle_##id##_guard; \
                }
 #else
        #define REGISTER_NET_LINKED(id) \
@@ -205,6 +217,7 @@ STATIC_INIT(C2S_Protocol_renumber) { FOREACH(C2S_Protocol, true, it.m_id = i); }
                MACRO_BEGIN { \
                        if (NET_##id##_istemp) WriteByte(to, SVC_TEMPENTITY); \
                        WriteByte(to, NET_##id.m_id); \
+                       bool _net_valid = false; serialize_marker(to, _net_valid); \
                } MACRO_END
 #endif
 
@@ -219,7 +232,11 @@ USING(Stream, int);
        #define stream_writing(stream) false
 #endif
 
-#define serialize(T, stream, ...) serialize_##T(stream, __VA_ARGS__)
+#define serialize(T, stream, ...) \
+MACRO_BEGIN \
+    noref Stream _stream = stream; \
+    serialize_##T(_stream, __VA_ARGS__); \
+MACRO_END
 
 #if defined(SVQC)
        #define serialize_byte(stream, this) \
@@ -246,13 +263,27 @@ USING(Stream, int);
 #endif
 
 #define serialize_vector(stream, this) \
-    MACRO_BEGIN \
+MACRO_BEGIN \
     vector _v = this; \
     serialize_float(stream, _v.x); \
     serialize_float(stream, _v.y); \
     serialize_float(stream, _v.z); \
     this = _v; \
-    MACRO_END
+MACRO_END
+
+#define serialize_marker(stream, this) \
+MACRO_BEGIN \
+    if (NDEBUG) { \
+        this = true; \
+    } else { \
+        int _de = 0xDE, _ad = 0xAD, _be = 0xBE, _ef = 0xEF; \
+        serialize_byte(stream, _de); \
+        serialize_byte(stream, _ad); \
+        serialize_byte(stream, _be); \
+        serialize_byte(stream, _ef); \
+        this = (_de == 0xDE && _ad == 0xAD && _be == 0xBE && _ef == 0xEF); \
+    } \
+MACRO_END
 
 // serialization: old
 
index 8ca07b4..2d41e5d 100644 (file)
@@ -187,7 +187,7 @@ void Registry_send(string id, string hash);
 #define EVAL_REGISTER_REGISTRY(...) __VA_ARGS__
 #define REGISTER_REGISTRY_1(id) REGISTER_REGISTRY_2(id, #id)
 #define REGISTER_REGISTRY_2(id, str) \
-       ACCUMULATE_FUNCTION(__static_init, Register##id) \
+       ACCUMULATE_FUNCTION(__static_init_1, Register##id) \
        CLASS(id##Registry, Object) \
                ATTRIB(id##Registry, m_name, string, str); \
                ATTRIB(id##Registry, REGISTRY_NEXT, entity, id##_first); \
index f4c246f..0a61cc0 100644 (file)
@@ -1,5 +1,7 @@
 #pragma once
 
+#include "macro.qh"
+
 // Transition from global 'self' to local 'this'
 
 // Step 1: auto oldself
 
 // Step 2: const self
 #if 1
-    #define self (0, self)
+    #define self (RVALUE, self)
     [[alias("self")]] entity __self;
     #define setself(s) (__self = s)
-    #define WITHSELF(value, block) WITH(entity, __self, value, (0, block))
+    #define WITHSELF(value, block) WITH(entity, __self, value, (RVALUE, block))
 #endif
 
 // Step 3: propagate SELFPARAM()
@@ -32,7 +34,7 @@
 // Step 5: this should work
 #if 1
     #undef self
-    #define self (0, this)
+    #define self (RVALUE, this)
 #endif
 
 // Step 6: remove SELFPARAM, add parameters
@@ -56,11 +58,11 @@ noref entity _selftemp;
 #define SELFWRAP_SET(T, e, f) \
     (_selftemp = (e), _selftemp.__##T = ((f) ? T##_self : func_null), _selftemp.self##T = (f))
 #define SELFWRAP_GET(T, e) \
-    (0, (e).self##T)
+    (RVALUE, (e).self##T)
 #define _SELFWRAP_SET(T, e, f) \
     ((e).__##T = (f))
 #define _SELFWRAP_GET(T, e) \
-    (0, (e).__##T)
+    (RVALUE, (e).__##T)
 
 SELFWRAP(think, void, (), (entity this), (this))
 #define setthink(e, f) SELFWRAP_SET(think, e, f)
index e0605c9..4638bca 100644 (file)
@@ -27,34 +27,77 @@ noref bool require_spawnfunc_prefix;
        #define _spawnfunc_check(fld) \
                if (fieldname == #fld) continue;
 
-       noref bool __spawnfunc_expecting;
+       noref int __spawnfunc_expecting;
        noref entity __spawnfunc_expect;
        noref bool __spawnfunc_unreachable_workaround = true;
 
+    .void(entity) __spawnfunc_constructor;
+    noref IntrusiveList g_spawn_queue;
+
+    #define SPAWNFUNC_INTERNAL_FIELDS(X) \
+        X(string, classname, "spawnfunc") \
+        X(string, targetname, string_null) \
+        /**/
+
+    #define X(T, fld, def) .T fld, __spawnfunc_##fld;
+    SPAWNFUNC_INTERNAL_FIELDS(X)
+    #undef X
+
+    void __spawnfunc_defer(entity prototype, void(entity) constructor)
+    {
+        IL_PUSH(g_spawn_queue, prototype);
+        #define X(T, fld, def) { prototype.__spawnfunc_##fld = prototype.fld; prototype.fld = def; }
+        SPAWNFUNC_INTERNAL_FIELDS(X);
+        #undef X
+        prototype.__spawnfunc_constructor = constructor;
+    }
+
+    noref IntrusiveList g_map_entities;
+    #define __spawnfunc_spawn_all() MACRO_BEGIN \
+        g_map_entities = IL_NEW(); \
+        IL_EACH(g_spawn_queue, true, __spawnfunc_spawn(it)); \
+    MACRO_END
+
+    void __spawnfunc_spawn(entity prototype)
+    {
+        entity e = new(clone);
+        copyentity(prototype, e);
+        IL_PUSH(g_map_entities, e);
+        #define X(T, fld, def) { e.fld = e.__spawnfunc_##fld; e.__spawnfunc_##fld = def; }
+        SPAWNFUNC_INTERNAL_FIELDS(X);
+        #undef X
+        e.__spawnfunc_constructor(e);
+    }
+
+       noref bool __spawnfunc_first;
+
        #define spawnfunc_1(id) spawnfunc_2(id, FIELDS_UNION)
        #define spawnfunc_2(id, whitelist) \
                void __spawnfunc_##id(entity this); \
                [[accumulate]] void spawnfunc_##id(entity this) \
                { \
-                       if (__spawnfunc_expecting) \
-                       { \
+                   if (!__spawnfunc_first) { \
+                __spawnfunc_first = true; \
+                static_init_early(); \
+                   } \
+                   bool dospawn = true; \
+                   if (__spawnfunc_expecting > 1) { __spawnfunc_expecting = false; } \
+                       else if (__spawnfunc_expecting) { \
                                /* engine call */ \
+                if (!g_spawn_queue) { g_spawn_queue = IL_NEW(); } \
                                __spawnfunc_expecting = false; \
                                this = __spawnfunc_expect; \
                                __spawnfunc_expect = NULL; \
-                       } \
-                       else \
-                       { \
+                dospawn = false; \
+                       } else { \
+                           /* userland call */ \
                                assert(this); \
                        } \
-                       if (!this.sourceLoc) \
-                       { \
+                       if (!this.sourceLoc) { \
                                this.sourceLoc = __FILE__ ":" STR(__LINE__); \
                        } \
-                       if (!this.spawnfunc_checked) \
-                       { \
-                               for (int i = 0, n = numentityfields(); i < n; ++i) \
-                               { \
+                       if (!this.spawnfunc_checked) { \
+                               for (int i = 0, n = numentityfields(); i < n; ++i) { \
                                        string value = getentityfieldstring(i, this); \
                                        string fieldname = entityfieldname(i); \
                                        whitelist(_spawnfunc_checktypes) \
@@ -65,8 +108,15 @@ noref bool require_spawnfunc_prefix;
                                        LOG_WARNF(_("Entity field %s.%s (%s) is not whitelisted. If you believe this is an error, please file an issue."), #id, fieldname, value); \
                                } \
                                this.spawnfunc_checked = true; \
+                               if (this) { \
+                    /* not worldspawn, delay spawn */ \
+                    __spawnfunc_defer(this, __spawnfunc_##id); \
+                } else { \
+                    /* world might not be "worldspawn" */ \
+                    this.__spawnfunc_constructor = __spawnfunc_##id; \
+                } \
                        } \
-                       __spawnfunc_##id(this); \
+                       if (dospawn) { __spawnfunc_##id(this); } \
                        if (__spawnfunc_unreachable_workaround) return; \
                } \
                void __spawnfunc_##id(entity this)
index e2c5fd4..6f511fc 100644 (file)
@@ -1,14 +1,5 @@
 #pragma once
 
-void __static_init() {}
-#define static_init() CALL_ACCUMULATED_FUNCTION(__static_init)
-void __static_init_late() {}
-#define static_init_late() CALL_ACCUMULATED_FUNCTION(__static_init_late)
-void __static_init_precache() {}
-#define static_init_precache() CALL_ACCUMULATED_FUNCTION(__static_init_precache)
-void __shutdown() {}
-#define shutdownhooks() CALL_ACCUMULATED_FUNCTION(__shutdown)
-
 #define GETTIME_REALTIME 1
 #ifdef MENUQC
 float(int tmr) _gettime = #67;
@@ -25,12 +16,34 @@ void profile(string s)
        LOG_TRACEF("[%f] %s", rt - g_starttime, s);
 }
 
-#define _STATIC_INIT(where, func) \
+#define _STATIC_INIT(func, where) \
        [[accumulate]] void _static_##func() { profile(#func); } \
        ACCUMULATE_FUNCTION(where, _static_##func) \
        void _static_##func()
 
-#define STATIC_INIT(func) _STATIC_INIT(__static_init,           func)
-#define STATIC_INIT_LATE(func) _STATIC_INIT(__static_init_late, func##_late)
-#define PRECACHE(func) _STATIC_INIT(__static_init_precache,     func##_precache)
-#define SHUTDOWN(func) _STATIC_INIT(__shutdown,                        func##_shutdown)
+/** before worldspawn */
+#define STATIC_INIT_EARLY(func) _STATIC_INIT(func##_0,    __static_init_0)
+#define static_init_early()     CALL_ACCUMULATED_FUNCTION(__static_init_0)
+void __static_init_0() {}
+
+/** during worldspawn */
+#define STATIC_INIT(func)       _STATIC_INIT(func##_1,    __static_init_1)
+#define static_init()           CALL_ACCUMULATED_FUNCTION(__static_init_1)
+void __static_init_1() {}
+
+/** directly after STATIC_INIT */
+#define STATIC_INIT_LATE(func)  _STATIC_INIT(func##_2,    __static_init_2)
+#define static_init_late()      CALL_ACCUMULATED_FUNCTION(__static_init_2)
+void __static_init_2() {}
+
+/** directly after STATIC_INIT_LATE */
+#define PRECACHE(func)          _STATIC_INIT(func##_3,    __static_init_3)
+#define static_init_precache()  CALL_ACCUMULATED_FUNCTION(__static_init_3)
+void __static_init_3() {}
+
+/* other map entities spawn now */
+
+/** before shutdown */
+#define SHUTDOWN(func)          _STATIC_INIT(func##_shutdown, __shutdown)
+#define shutdownhooks()         CALL_ACCUMULATED_FUNCTION(    __shutdown)
+void __shutdown() {}
index 4642f76..1100c47 100644 (file)
@@ -33,8 +33,8 @@ int g_magic_stats_hole = 0;
        void stats_get() {}
        #define STAT(...) EVAL_STAT(OVERLOAD(STAT, __VA_ARGS__))
        #define EVAL_STAT(...) __VA_ARGS__
-    #define STAT_1(id) STAT_2(id, NULL)
-       #define STAT_2(id, cl) (0, _STAT(id))
+    #define STAT_1(id) (RVALUE, _STAT(id))
+       #define STAT_2(id, cl) STAT_1(id)
 
        #define getstat_int(id) getstati(id, 0, 24)
        #define getstat_bool(id) boolean(getstati(id))
@@ -61,9 +61,14 @@ int g_magic_stats_hole = 0;
                }
        #define REGISTER_STAT_3(x, T, expr) REGISTER_STAT_2(x, T)
 #elif defined(SVQC)
+    /** Internal use only */
+    entity STATS;
        /** Add all registered stats, access with `STAT(ID, player)` or `.type stat = _STAT(ID); player.stat` */
        void stats_add() {}
-       #define STAT(id, cl) (cl)._STAT(id)
+       #define STAT(...) EVAL_STAT(OVERLOAD_(STAT, __VA_ARGS__))
+    #define EVAL_STAT(...) __VA_ARGS__
+    #define STAT_1(id) (RVALUE, STAT_2(id, STATS))
+       #define STAT_2(id, cl) (cl)._STAT(id)
 
        #define addstat_int(id, fld) addstat(id, AS_INT, fld)
        #define addstat_bool(id, fld) addstat(id, AS_INT, fld)
@@ -83,9 +88,10 @@ int g_magic_stats_hole = 0;
        const int AS_FLOAT = 8;
 
        .int __stat_null;
-       /** Prevent engine stats being sent */
-       STATIC_INIT(stats_clear)
+       STATIC_INIT(stats)
        {
+           STATS = new(stats);
+           // Prevent engine stats being sent
                int r = STATS_ENGINE_RESERVE;
                for (int i = 0, n = 256 - r; i < n; ++i) {
                        #define X(_, name, id) if (i == id) continue;
@@ -111,10 +117,11 @@ int g_magic_stats_hole = 0;
                        addstat_##T(STAT_##id.m_id, fld); \
                }
        void GlobalStats_update(entity this) {}
+    /** TODO: do we want the global copy to update? */
     #define REGISTER_STAT_3(id, T, expr) \
        REGISTER_STAT_2(id, T); \
        [[accumulate]] void GlobalStats_update(entity this) { STAT(id, this) = (expr); } \
-       STATIC_INIT(worldstat_##id) { entity this = NULL; STAT(id, this) = (expr); }
+       STATIC_INIT(worldstat_##id) { entity this = STATS; STAT(id, this) = (expr); }
 #else
        #define REGISTER_STAT_2(id, type)
     #define REGISTER_STAT_3(id, T, expr)
index 87a8d56..99115fb 100644 (file)
@@ -20,6 +20,7 @@
 #include <server/playerdemo.qc>
 #include <server/portals.qc>
 #include <server/race.qc>
+#include <server/resources.qc>
 #include <server/round_handler.qc>
 #include <server/scores.qc>
 #include <server/scores_rules.qc>
index 2967c11..3a88986 100644 (file)
@@ -20,6 +20,7 @@
 #include <server/playerdemo.qh>
 #include <server/portals.qh>
 #include <server/race.qh>
+#include <server/resources.qh>
 #include <server/round_handler.qh>
 #include <server/scores.qh>
 #include <server/scores_rules.qh>
index f585a9a..4303aa9 100644 (file)
@@ -88,7 +88,7 @@ float autocvar_g_balance_superweapons_time;
 float autocvar_g_balance_selfdamagepercent;
 bool autocvar_g_balance_teams;
 bool autocvar_g_balance_teams_prevent_imbalance;
-float autocvar_g_balance_teams_scorefactor;
+//float autocvar_g_balance_teams_scorefactor;
 float autocvar_g_ballistics_density_corpse;
 float autocvar_g_ballistics_density_player;
 float autocvar_g_ballistics_mindistance;
@@ -165,12 +165,7 @@ int autocvar_g_maxplayers;
 float autocvar_g_maxplayers_spectator_blocktime;
 float autocvar_g_maxpushtime;
 float autocvar_g_maxspeed;
-#define autocvar_g_instagib cvar("g_instagib")
-bool autocvar_g_instagib_damagedbycontents = true;
-bool autocvar_g_instagib_blaster_keepdamage = false;
-bool autocvar_g_instagib_blaster_keepforce = false;
-bool autocvar_g_instagib_mirrordamage;
-bool autocvar_g_instagib_friendlypush = true;
+bool autocvar_g_instagib;
 #define autocvar_g_mirrordamage cvar("g_mirrordamage")
 #define autocvar_g_mirrordamage_virtual cvar("g_mirrordamage_virtual")
 bool autocvar_g_mirrordamage_onlyweapons;
@@ -416,7 +411,6 @@ float autocvar_g_monsters_armor_blockpercent;
 float autocvar_g_monsters_healthbars;
 bool autocvar_g_monsters_lineofsight = true;
 //bool autocvar_g_monsters_ignoretraces = true;
-#define autocvar_g_bloodloss cvar("g_bloodloss")
 bool autocvar_g_nades;
 bool autocvar_g_nades_override_dropweapon = true;
 vector autocvar_g_nades_throw_offset;
index f1d417d..6a978c6 100644 (file)
@@ -12,6 +12,7 @@
 #include "teamplay.qh"
 #include "playerdemo.qh"
 #include "spawnpoints.qh"
+#include "resources.qh"
 #include "g_damage.qh"
 #include "g_hook.qh"
 #include "command/common.qh"
@@ -268,7 +269,9 @@ void PutObserverInServer(entity this)
        if (mutator_returnvalue) {
            // mutator prevents resetting teams+score
        } else {
+               int oldteam = this.team;
                this.team = -1;  // move this as it is needed to log the player spectating in eventlog
+               MUTATOR_CALLHOOK(Player_ChangedTeam, this, oldteam, this.team);
         this.frags = FRAGS_SPECTATOR;
         PlayerScore_Clear(this);  // clear scores when needed
     }
@@ -892,8 +895,10 @@ void ClientKill_Now(entity this)
        if(CS(this).killindicator_teamchange)
                ClientKill_Now_TeamChange(this);
 
-       if(!IS_SPEC(this) && !IS_OBSERVER(this))
+       if (!IS_SPEC(this) && !IS_OBSERVER(this) && MUTATOR_CALLHOOK(ClientKill_Now, this) == false)
+       {
                Damage(this, this, this, 100000, DEATH_KILL.m_id, this.origin, '0 0 0');
+       }
 
        // now I am sure the player IS dead
 }
@@ -1628,8 +1633,8 @@ void player_regen(entity this)
                float mina, maxa, limith, limita;
                maxa = autocvar_g_balance_armor_rotstable;
                mina = autocvar_g_balance_armor_regenstable;
-               limith = autocvar_g_balance_health_limit;
-               limita = autocvar_g_balance_armor_limit;
+               limith = GetResourceLimit(this, RESOURCE_HEALTH);
+               limita = GetResourceLimit(this, RESOURCE_ARMOR);
 
                regen_health_rotstable = regen_health_rotstable * max_mod;
                regen_health_stable = regen_health_stable * max_mod;
@@ -1656,7 +1661,7 @@ void player_regen(entity this)
 
                maxf = autocvar_g_balance_fuel_rotstable;
                minf = autocvar_g_balance_fuel_regenstable;
-               limitf = autocvar_g_balance_fuel_limit;
+               limitf = GetResourceLimit(this, RESOURCE_FUEL);
 
                this.ammo_fuel = CalcRotRegen(this.ammo_fuel, minf, autocvar_g_balance_fuel_regen, autocvar_g_balance_fuel_regenlinear, frametime * (time > this.pauseregen_finished) * ((this.items & ITEM_JetpackRegen.m_itemid) != 0), maxf, autocvar_g_balance_fuel_rot, autocvar_g_balance_fuel_rotlinear, frametime * (time > this.pauserotfuel_finished), limitf);
        }
index 9674872..2282c09 100644 (file)
@@ -224,8 +224,6 @@ METHOD(Client, m_unwind, bool(Client this))
     return false;
 }
 
-float c1, c2, c3, c4;
-
 void play_countdown(entity this, float finished, Sound samp);
 
 float CalcRotRegen(float current, float regenstable, float regenfactor, float regenlinear, float regenframetime, float rotstable, float rotfactor, float rotlinear, float rotframetime, float limit);
index a2c037c..09a308a 100644 (file)
@@ -325,82 +325,90 @@ void ClientCommand_selectteam(entity caller, float request, float argc)
        {
                case CMD_REQUEST_COMMAND:
                {
-                       if (argv(1) != "")
+                       if (argv(1) == "")
                        {
-                               if (IS_CLIENT(caller))
+                               return;
+                       }
+                       if (!IS_CLIENT(caller))
+                       {
+                               return;
+                       }
+                       if (!teamplay)
+                       {
+                               sprint(caller, "^7selectteam can only be used in teamgames\n");
+                               return;
+                       }
+                       if (caller.team_forced > 0)
+                       {
+                               sprint(caller, "^7selectteam can not be used as your team is forced\n");
+                               return;
+                       }
+                       if (lockteams)
+                       {
+                               sprint(caller, "^7The game has already begun, you must wait until the next map to be able to join a team.\n");
+                               return;
+                       }
+                       float selection;
+                       switch (argv(1))
+                       {
+                               case "red":
                                {
-                                       if (teamplay)
-                                       {
-                                               if (caller.team_forced <= 0)
-                                               {
-                                                       if (!lockteams)
-                                                       {
-                                                               float selection;
-
-                                                               switch (argv(1))
-                                                               {
-                                                                       case "red": selection = NUM_TEAM_1;
-                                                                               break;
-                                                                       case "blue": selection = NUM_TEAM_2;
-                                                                               break;
-                                                                       case "yellow": selection = NUM_TEAM_3;
-                                                                               break;
-                                                                       case "pink": selection = NUM_TEAM_4;
-                                                                               break;
-                                                                       case "auto": selection = (-1);
-                                                                               break;
-
-                                                                       default: selection = 0;
-                                                                               break;
-                                                               }
-
-                                                               if (selection)
-                                                               {
-                                                                       if (caller.team == selection && selection != -1 && !IS_DEAD(caller))
-                                                                       {
-                                                                               sprint(caller, "^7You already are on that team.\n");
-                                                                       }
-                                                                       else if (CS(caller).wasplayer && autocvar_g_changeteam_banned)
-                                                                       {
-                                                                               sprint(caller, "^1You cannot change team, forbidden by the server.\n");
-                                                                       }
-                                                                       else
-                                                                       {
-                                                                               if (autocvar_g_balance_teams && autocvar_g_balance_teams_prevent_imbalance)
-                                                                               {
-                                                                                       CheckAllowedTeams(caller);
-                                                                                       GetTeamCounts(caller);
-                                                                                       if (!TeamSmallerEqThanTeam(Team_TeamToNumber(selection), Team_TeamToNumber(caller.team), caller))
-                                                                                       {
-                                                                                               Send_Notification(NOTIF_ONE, caller, MSG_INFO, INFO_TEAMCHANGE_LARGERTEAM);
-                                                                                               return;
-                                                                                       }
-                                                                               }
-                                                                               ClientKill_TeamChange(caller, selection);
-                                                                       }
-                                                                       if(!IS_PLAYER(caller))
-                                                                               caller.team_selected = true; // avoids asking again for team selection on join
-                                                               }
-                                                       }
-                                                       else
-                                                       {
-                                                               sprint(caller, "^7The game has already begun, you must wait until the next map to be able to join a team.\n");
-                                                       }
-                                               }
-                                               else
-                                               {
-                                                       sprint(caller, "^7selectteam can not be used as your team is forced\n");
-                                               }
-                                       }
-                                       else
-                                       {
-                                               sprint(caller, "^7selectteam can only be used in teamgames\n");
-                                       }
+                                       selection = NUM_TEAM_1;
+                                       break;
+                               }
+                               case "blue":
+                               {
+                                       selection = NUM_TEAM_2;
+                                       break;
+                               }
+                               case "yellow":
+                               {
+                                       selection = NUM_TEAM_3;
+                                       break;
+                               }
+                               case "pink":
+                               {
+                                       selection = NUM_TEAM_4;
+                                       break;
                                }
+                               case "auto":
+                               {
+                                       selection = (-1);
+                                       break;
+                               }
+                               default:
+                               {
+                                       return;
+                               }
+                       }
+                       if (caller.team == selection && selection != -1 && !IS_DEAD(caller))
+                       {
+                               sprint(caller, "^7You already are on that team.\n");
+                               return;
+                       }
+                       if (CS(caller).wasplayer && autocvar_g_changeteam_banned)
+                       {
+                               sprint(caller, "^1You cannot change team, forbidden by the server.\n");
                                return;
                        }
+                       if ((selection != -1) && autocvar_g_balance_teams &&
+                               autocvar_g_balance_teams_prevent_imbalance)
+                       {
+                               CheckAllowedTeams(caller);
+                               GetTeamCounts(caller);
+                               if ((BIT(Team_TeamToNumber(selection) - 1) & FindBestTeams(caller, false)) == 0)
+                               {
+                                       Send_Notification(NOTIF_ONE, caller, MSG_INFO, INFO_TEAMCHANGE_LARGERTEAM);
+                                       return;
+                               }
+                       }
+                       ClientKill_TeamChange(caller, selection);
+                       if (!IS_PLAYER(caller))
+                       {
+                               caller.team_selected = true; // avoids asking again for team selection on join
+                       }
+                       return;
                }
-
                default:
                        sprint(caller, "Incorrect parameters for ^2selectteam^7\n");
                case CMD_REQUEST_USAGE:
index 43ce291..6de4507 100644 (file)
@@ -1095,9 +1095,15 @@ void GameCommand_moveplayer(float request, float argc)
 
                                                                // If so, lets continue and finally move the player
                                                                client.team_forced = 0;
-                                                               MoveToTeam(client, team_id, 6);
-                                                               successful = strcat(successful, (successful ? ", " : ""), playername(client, false));
-                                                               LOG_INFO("Player ", ftos(GetFilteredNumber(t)), " (", playername(client, false), ") has been moved to the ", Team_ColoredFullName(team_id), "^7.");
+                                                               if (MoveToTeam(client, team_id, 6))
+                                                               {
+                                                                       successful = strcat(successful, (successful ? ", " : ""), playername(client, false));
+                                                                       LOG_INFO("Player ", ftos(GetFilteredNumber(t)), " (", playername(client, false), ") has been moved to the ", Team_ColoredFullName(team_id), "^7.");
+                                                               }
+                                                               else
+                                                               {
+                                                                       LOG_INFO("Unable to move player ", ftos(GetFilteredNumber(t)), " (", playername(client, false), ")");
+                                                               }
                                                                continue;
                                                        }
                                                        else
index 8cfed5b..1a786cc 100644 (file)
@@ -81,8 +81,9 @@ bool Nagger_SendEntity(entity this, entity to, float sendflags)
        {
                for (i = 1; i <= maxclients; i += 8)
                {
-                       for (f = 0, e = edict_num(i), b = 1; b < 256; b *= 2, e = nextent(e))
-                               if (!IS_REAL_CLIENT(e) || e.ready) f |= b;
+                       for (f = 0, e = edict_num(i), b = BIT(0); b < BIT(8); b <<= 1, e = nextent(e))
+                               if (!IS_REAL_CLIENT(e) || e.ready)
+                                       f |= b;
                        WriteByte(MSG_ENTITY, f);
                }
        }
index 8a02238..0270462 100644 (file)
@@ -9,6 +9,7 @@
 #include "../common/state.qh"
 #include "../common/physics/player.qh"
 #include "../common/t_items.qh"
+#include "resources.qh"
 #include "../common/vehicles/all.qh"
 #include "../common/items/_mod.qh"
 #include "../common/mutators/mutator/waypoints/waypointsprites.qh"
@@ -275,7 +276,7 @@ bool frag_centermessage_override(entity attacker, entity targ, int deathtype, in
        if(deathtype == DEATH_FIRE.m_id)
        {
                Send_Notification(NOTIF_ONE, attacker, MSG_CHOICE, CHOICE_FRAG_FIRE, targ.netname, kill_count_to_attacker, (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping));
-               Send_Notification(NOTIF_ONE, targ, MSG_CHOICE, CHOICE_FRAGGED_FIRE, attacker.netname, kill_count_to_target, attacker.health, attacker.armorvalue, (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping));
+               Send_Notification(NOTIF_ONE, targ, MSG_CHOICE, CHOICE_FRAGGED_FIRE, attacker.netname, kill_count_to_target, GetResourceAmount(attacker, RESOURCE_HEALTH), GetResourceAmount(attacker, RESOURCE_ARMOR), (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping));
                return true;
        }
 
@@ -427,8 +428,8 @@ void Obituary(entity attacker, entity inflictor, entity targ, int deathtype)
                                        CHOICE_TYPEFRAGGED,
                                        attacker.netname,
                                        kill_count_to_target,
-                                       attacker.health,
-                                       attacker.armorvalue,
+                                       GetResourceAmount(attacker, RESOURCE_HEALTH),
+                                       GetResourceAmount(attacker, RESOURCE_ARMOR),
                                        (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
                                );
                        }
@@ -450,8 +451,8 @@ void Obituary(entity attacker, entity inflictor, entity targ, int deathtype)
                                        CHOICE_FRAGGED,
                                        attacker.netname,
                                        kill_count_to_target,
-                                       attacker.health,
-                                       attacker.armorvalue,
+                                       GetResourceAmount(attacker, RESOURCE_HEALTH),
+                                       GetResourceAmount(attacker, RESOURCE_ARMOR),
                                        (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
                                );
                        }
@@ -543,7 +544,7 @@ void Freeze (entity targ, float freeze_time, float frozen_type, float show_waypo
 
        STAT(FROZEN, targ) = frozen_type;
        targ.revive_progress = ((frozen_type == 3) ? 1 : 0);
-       targ.health = ((frozen_type == 3) ? targ_maxhealth : 1);
+       SetResourceAmount(targ, RESOURCE_HEALTH, ((frozen_type == 3) ? targ_maxhealth : 1));
        targ.revive_speed = freeze_time;
        if(targ.bot_attack)
                IL_REMOVE(g_bot_targets, targ);
@@ -588,7 +589,7 @@ void Unfreeze (entity targ)
 
        if(STAT(FROZEN, targ) && STAT(FROZEN, targ) != 3) // only reset health if target was frozen
        {
-               targ.health = ((IS_PLAYER(targ)) ? start_health : targ.max_health);
+               SetResourceAmount(targ, RESOURCE_HEALTH, ((IS_PLAYER(targ)) ? start_health : targ.max_health));
                targ.pauseregen_finished = time + autocvar_g_balance_pause_health_regen;
        }
 
@@ -646,9 +647,9 @@ void Damage (entity targ, entity inflictor, entity attacker, float damage, int d
                // These are ALWAYS lethal
                // No damage modification here
                // Instead, prepare the victim for his death...
-               targ.armorvalue = 0;
+               SetResourceAmount(targ, RESOURCE_ARMOR, 0);
                targ.spawnshieldtime = 0;
-               targ.health = 0.9; // this is < 1
+               SetResourceAmount(targ, RESOURCE_HEALTH, 0.9); // this is < 1
                targ.flags -= targ.flags & FL_GODMODE;
                damage = 100000;
        }
@@ -748,7 +749,7 @@ void Damage (entity targ, entity inflictor, entity attacker, float damage, int d
                        if(damage >= autocvar_g_frozen_revive_falldamage)
                        {
                                Unfreeze(targ);
-                               targ.health = autocvar_g_frozen_revive_falldamage_health;
+                               SetResourceAmount(targ, RESOURCE_HEALTH, autocvar_g_frozen_revive_falldamage_health);
                                Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
                                Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
                                Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
index 64aa03b..ce25dfa 100644 (file)
@@ -521,7 +521,7 @@ void detect_maptype()
                o.y += random() * (world.maxs.y - world.mins.y);
                o.z += random() * (world.maxs.z - world.mins.z);
 
-               tracebox(o, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), o - '0 0 32768', MOVE_WORLDONLY, NULL);
+               tracebox(o, STAT(PL_MIN), STAT(PL_MAX), o - '0 0 32768', MOVE_WORLDONLY, NULL);
                if(trace_fraction == 1)
                        continue;
 
@@ -580,7 +580,15 @@ spawnfunc(__init_dedicated_server)
 
        e = new(info_player_deathmatch);  // safeguard against player joining
 
-       this.classname = "worldspawn"; // safeguard against various stuff ;)
+    // assign reflectively to avoid "assignment to world" warning
+    for (int i = 0, n = numentityfields(); i < n; ++i) {
+        string k = entityfieldname(i);
+        if (k == "classname") {
+            // safeguard against various stuff ;)
+            putentityfieldstring(i, this, "worldspawn");
+            break;
+        }
+    }
 
        // needs to be done so early because of the constants they create
        static_init();
@@ -597,6 +605,14 @@ void __init_dedicated_server_shutdown() {
        MapInfo_Shutdown();
 }
 
+STATIC_INIT_EARLY(maxclients)
+{
+       maxclients = 0;
+       for (entity head = nextent(NULL); head; head = nextent(head)) {
+               ++maxclients;
+       }
+}
+
 void Map_MarkAsRecent(string m);
 float world_already_spawned;
 void Nagger_Init();
@@ -680,12 +696,6 @@ spawnfunc(worldspawn)
 
        cvar_changes_init(); // do this very early now so it REALLY matches the server config
 
-       maxclients = 0;
-       for (entity head = nextent(NULL); head; head = nextent(head))
-       {
-               ++maxclients;
-       }
-
        // needs to be done so early because of the constants they create
        static_init();
 
@@ -936,6 +946,7 @@ spawnfunc(worldspawn)
        WinningConditionHelper(this); // set worldstatus
 
        world_initialized = 1;
+       __spawnfunc_spawn_all();
 }
 
 spawnfunc(light)
index 7bb8e92..554da5f 100644 (file)
@@ -933,24 +933,21 @@ void InitializeEntitiesRun()
 .float(entity) isEliminated;
 bool EliminatedPlayers_SendEntity(entity this, entity to, float sendflags)
 {
-       float i, f, b;
-       entity e;
-       WriteHeader(MSG_ENTITY, ENT_CLIENT_ELIMINATEDPLAYERS);
-       WriteByte(MSG_ENTITY, sendflags);
-
-       if(sendflags & 1)
-       {
-               for(i = 1; i <= maxclients; i += 8)
-               {
-                       for(f = 0, e = edict_num(i), b = 1; b < 256; b *= 2, e = nextent(e))
-                       {
-                               if(eliminatedPlayers.isEliminated(e))
-                                       f |= b;
+       Stream out = MSG_ENTITY;
+       WriteHeader(out, ENT_CLIENT_ELIMINATEDPLAYERS);
+       serialize(byte, out, sendflags);
+       if (sendflags & 1) {
+               for (int i = 1; i <= maxclients; i += 8) {
+                       int f = 0;
+                       entity e = edict_num(i);
+                       for (int b = 0; b < 8; ++b, e = nextent(e)) {
+                               if (eliminatedPlayers.isEliminated(e)) {
+                                       f |= BIT(b);
+                               }
                        }
-                       WriteByte(MSG_ENTITY, f);
+                       serialize(byte, out, f);
                }
        }
-
        return true;
 }
 
index bc09d7a..0110bdf 100644 (file)
@@ -130,6 +130,31 @@ MUTATOR_HOOKABLE(MatchEnd, EV_NO_ARGS);
     /**/
 MUTATOR_HOOKABLE(CheckAllowedTeams, EV_CheckAllowedTeams);
 
+/** return true to manually override team counts */
+MUTATOR_HOOKABLE(GetTeamCounts, EV_NO_ARGS);
+
+/** allow overriding of team counts */
+#define EV_GetTeamCount(i, o) \
+    /** team to count                   */ i(float, MUTATOR_ARGV_0_float) \
+    /** player to ignore                */ i(entity, MUTATOR_ARGV_1_entity) \
+    /** number of players in a team     */ i(float, MUTATOR_ARGV_2_float) \
+    /**/                                   o(float, MUTATOR_ARGV_2_float) \
+    /** number of bots in a team        */ i(float, MUTATOR_ARGV_3_float) \
+    /**/                                   o(float, MUTATOR_ARGV_3_float) \
+    /** lowest scoring human in a team  */ i(entity, MUTATOR_ARGV_4_entity) \
+    /**/                                   o(entity, MUTATOR_ARGV_4_entity) \
+    /** lowest scoring bot in a team    */ i(entity, MUTATOR_ARGV_5_entity) \
+    /**/                                   o(entity, MUTATOR_ARGV_5_entity) \
+    /**/
+MUTATOR_HOOKABLE(GetTeamCount, EV_GetTeamCount);
+
+/** allows overriding best teams */
+#define EV_FindBestTeams(i, o) \
+    /** player checked   */ i(entity, MUTATOR_ARGV_0_entity) \
+    /** bitmask of teams */ o(float, MUTATOR_ARGV_1_float) \
+    /**/
+MUTATOR_HOOKABLE(FindBestTeams, EV_FindBestTeams);
+
 /** copies variables for spectating "spectatee" to "this" */
 #define EV_SpectateCopy(i, o) \
     /** spectatee   */ i(entity, MUTATOR_ARGV_0_entity) \
@@ -620,6 +645,38 @@ enum {
        MUT_ITEMTOUCH_PICKUP // return this flag to have the item "picked up" and taken even after mutator handled it
 };
 
+/** Called when the amount of entity resources changes. Can be used to override
+resource limit. */
+#define EV_GetResourceLimit(i, o) \
+       /** checked entity */ i(entity, MUTATOR_ARGV_0_entity) \
+       /** resource type */  i(int, MUTATOR_ARGV_1_int) \
+       /** limit */          i(float, MUTATOR_ARGV_2_float) \
+       /**/                  o(float, MUTATOR_ARGV_2_float) \
+       /**/
+MUTATOR_HOOKABLE(GetResourceLimit, EV_GetResourceLimit);
+
+/** Called when the amount of resource of an entity changes. See RESOURCE_*
+constants for resource types. Return true to forbid the change. */
+#define EV_SetResourceAmount(i, o) \
+       /** checked entity */ i(entity, MUTATOR_ARGV_0_entity) \
+       /** resource type */  i(int, MUTATOR_ARGV_1_int) \
+       /**/                  o(int, MUTATOR_ARGV_1_int) \
+       /** amount */         i(float, MUTATOR_ARGV_2_float) \
+       /**/                  o(float, MUTATOR_ARGV_2_float) \
+       /**/
+MUTATOR_HOOKABLE(SetResourceAmount, EV_SetResourceAmount);
+
+/** Called when entity is being given some resource. See RESOURCE_* constants
+for resource types. Return true to forbid giving. */
+#define EV_GiveResource(i, o) \
+       /** receiver */      i(entity, MUTATOR_ARGV_0_entity) \
+       /** resource type */ i(int, MUTATOR_ARGV_1_int) \
+       /**/                 o(int, MUTATOR_ARGV_1_int) \
+       /** amount */        i(float, MUTATOR_ARGV_2_float) \
+       /**/                 o(float, MUTATOR_ARGV_2_float) \
+       /**/
+MUTATOR_HOOKABLE(GiveResource, EV_GiveResource);
+
 /** called at when a player connect */
 #define EV_ClientConnect(i, o) \
     /** player */ i(entity, MUTATOR_ARGV_0_entity) \
@@ -728,6 +785,12 @@ MUTATOR_HOOKABLE(Race_FinalCheckpoint, EV_Race_FinalCheckpoint);
     /**/
 MUTATOR_HOOKABLE(ClientKill, EV_ClientKill);
 
+/** called when player is about to be killed during kill command or changing teams */
+#define EV_ClientKill_Now(i, o) \
+    /** player */        i(entity, MUTATOR_ARGV_0_entity) \
+    /**/
+MUTATOR_HOOKABLE(ClientKill_Now, EV_ClientKill_Now);
+
 #define EV_FixClientCvars(i, o) \
     /** player */        i(entity, MUTATOR_ARGV_0_entity) \
     /**/
@@ -875,7 +938,9 @@ MUTATOR_HOOKABLE(PrepareExplosionByDamage, EV_PrepareExplosionByDamage);
     /**/
 MUTATOR_HOOKABLE(MonsterModel, EV_MonsterModel);
 
-/**/
+/**
+ * Called before player changes their team. Return true to block team change.
+ */
 #define EV_Player_ChangeTeam(i, o) \
     /** player */         i(entity, MUTATOR_ARGV_0_entity) \
        /** current team */   i(float, MUTATOR_ARGV_1_float) \
@@ -883,6 +948,24 @@ MUTATOR_HOOKABLE(MonsterModel, EV_MonsterModel);
     /**/
 MUTATOR_HOOKABLE(Player_ChangeTeam, EV_Player_ChangeTeam);
 
+/**
+ * Called after player has changed their team.
+ */
+#define EV_Player_ChangedTeam(i, o) \
+    /** player */         i(entity, MUTATOR_ARGV_0_entity) \
+       /** old team */       i(float, MUTATOR_ARGV_1_float) \
+       /** current team */   i(float, MUTATOR_ARGV_2_float) \
+    /**/
+MUTATOR_HOOKABLE(Player_ChangedTeam, EV_Player_ChangedTeam);
+
+/**
+ * Called when player is about to be killed when changing teams. Return true to block killing.
+ */
+#define EV_Player_ChangeTeamKill(i, o) \
+    /** player */    i(entity, MUTATOR_ARGV_0_entity) \
+    /**/
+MUTATOR_HOOKABLE(Player_ChangeTeamKill, EV_Player_ChangeTeamKill);
+
 /**/
 #define EV_URI_GetCallback(i, o) \
        /** id */       i(float, MUTATOR_ARGV_0_float) \
index 44a8777..dd06152 100644 (file)
@@ -668,15 +668,19 @@ void PlayerDamage(entity this, entity inflictor, entity attacker, float damage,
        }
 }
 
-void MoveToTeam(entity client, int team_colour, int type)
+bool MoveToTeam(entity client, int team_colour, int type)
 {
        int lockteams_backup = lockteams;  // backup any team lock
        lockteams = 0;  // disable locked teams
        TeamchangeFrags(client);  // move the players frags
-       SetPlayerColors(client, team_colour - 1);  // set the players colour
+       if (!SetPlayerTeamSimple(client, team_colour))
+       {
+               return false;
+       }
        Damage(client, client, client, 100000, DEATH_AUTOTEAMCHANGE.m_id, client.origin, '0 0 0');  // kill the player
        lockteams = lockteams_backup;  // restore the team lock
        LogTeamchange(client.playerid, client.team, type);
+       return true;
 }
 
 /** print(), but only print if the server is not local */
index 1834bb8..dfa485e 100644 (file)
@@ -69,7 +69,12 @@ void calculate_player_respawn_time(entity this);
 
 void ClientKill_Now_TeamChange(entity this);
 
-void MoveToTeam(entity client, float team_colour, float type);
+/// \brief Moves player to the specified team.
+/// \param[in,out] client Client to move.
+/// \param[in] team_colour Color of the team.
+/// \param[in] type ???
+/// \return True on success, false otherwise.
+bool MoveToTeam(entity client, float team_colour, float type);
 
 void PlayerDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force);
 
diff --git a/qcsrc/server/resources.qc b/qcsrc/server/resources.qc
new file mode 100644 (file)
index 0000000..edf4ff1
--- /dev/null
@@ -0,0 +1,190 @@
+#include "resources.qh"
+/// \file
+/// \brief Source file that contains implementation of the resource system.
+/// \author Lyberta
+/// \copyright GNU GPLv2 or any later version.
+
+#include "autocvars.qh"
+#include "miscfunctions.qh"
+
+float GetResourceLimit(entity e, int resource_type)
+{
+       float limit;
+       switch (resource_type)
+       {
+               case RESOURCE_HEALTH:
+               {
+                       limit = autocvar_g_balance_health_limit;
+                       break;
+               }
+               case RESOURCE_ARMOR:
+               {
+                       limit = autocvar_g_balance_armor_limit;
+                       break;
+               }
+               case RESOURCE_SHELLS:
+               {
+                       limit = g_pickup_shells_max;
+                       break;
+               }
+               case RESOURCE_BULLETS:
+               {
+                       limit = g_pickup_nails_max;
+                       break;
+               }
+               case RESOURCE_ROCKETS:
+               {
+                       limit = g_pickup_rockets_max;
+                       break;
+               }
+               case RESOURCE_CELLS:
+               {
+                       limit = g_pickup_cells_max;
+                       break;
+               }
+               case RESOURCE_PLASMA:
+               {
+                       limit = g_pickup_plasma_max;
+                       break;
+               }
+               case RESOURCE_FUEL:
+               {
+                       limit = autocvar_g_balance_fuel_limit;
+                       break;
+               }
+               default:
+               {
+                       error("GetResourceLimit: Invalid resource type.");
+                       return 0;
+               }
+       }
+       MUTATOR_CALLHOOK(GetResourceLimit, e, resource_type, limit);
+       limit = M_ARGV(2, float);
+       if (limit > RESOURCE_AMOUNT_HARD_LIMIT)
+       {
+               limit = RESOURCE_AMOUNT_HARD_LIMIT;
+       }
+       return limit;
+}
+
+float GetResourceAmount(entity e, int resource_type)
+{
+       .float resource_field = GetResourceField(resource_type);
+       return e.(resource_field);
+}
+
+void SetResourceAmount(entity e, int resource_type, float amount)
+{
+       bool forbid = MUTATOR_CALLHOOK(SetResourceAmount, e, resource_type, amount);
+       if (forbid)
+       {
+               return;
+       }
+       resource_type = M_ARGV(1, int);
+       amount = M_ARGV(2, float);
+       .float resource_field = GetResourceField(resource_type);
+       if (e.(resource_field) == amount)
+       {
+               return;
+       }
+       float max_amount = GetResourceLimit(e, resource_type);
+       if (amount > max_amount)
+       {
+               amount = max_amount;
+       }
+       e.(resource_field) = amount;
+}
+
+void GiveResource(entity receiver, int resource_type, float amount)
+{
+       if (amount == 0)
+       {
+               return;
+       }
+       bool forbid = MUTATOR_CALLHOOK(GiveResource, receiver, resource_type,
+               amount);
+       if (forbid)
+       {
+               return;
+       }
+       resource_type = M_ARGV(1, int);
+       amount = M_ARGV(2, float);
+       if (amount <= 0)
+       {
+               return;
+       }
+       SetResourceAmount(receiver, resource_type,
+               GetResourceAmount(receiver, resource_type) + amount);
+       switch (resource_type)
+       {
+               case RESOURCE_HEALTH:
+               {
+                       receiver.pauserothealth_finished =
+                               max(receiver.pauserothealth_finished, time +
+                               autocvar_g_balance_pause_health_rot);
+                       return;
+               }
+               case RESOURCE_ARMOR:
+               {
+                       receiver.pauserotarmor_finished =
+                               max(receiver.pauserotarmor_finished, time +
+                               autocvar_g_balance_pause_armor_rot);
+                       return;
+               }
+               case RESOURCE_FUEL:
+               {
+                       receiver.pauserotfuel_finished = max(receiver.pauserotfuel_finished,
+                               time + autocvar_g_balance_pause_fuel_rot);
+                       return;
+               }
+       }
+}
+
+void GiveResourceWithLimit(entity receiver, int resource_type, float amount,
+       float limit)
+{
+       if (amount == 0)
+       {
+               return;
+       }
+       float current_amount = GetResourceAmount(receiver, resource_type);
+       if (current_amount + amount > limit)
+       {
+               amount = limit - current_amount;
+       }
+       GiveResource(receiver, resource_type, amount);
+}
+
+int GetResourceType(.float resource_field)
+{
+       switch (resource_field)
+       {
+               case health: { return RESOURCE_HEALTH; }
+               case armorvalue: { return RESOURCE_ARMOR; }
+               case ammo_shells: { return RESOURCE_SHELLS; }
+               case ammo_nails: { return RESOURCE_BULLETS; }
+               case ammo_rockets: { return RESOURCE_ROCKETS; }
+               case ammo_cells: { return RESOURCE_CELLS; }
+               case ammo_plasma: { return RESOURCE_PLASMA; }
+               case ammo_fuel: { return RESOURCE_FUEL; }
+       }
+       error("GetResourceType: Invalid field.");
+       return 0;
+}
+
+.float GetResourceField(int resource_type)
+{
+       switch (resource_type)
+       {
+               case RESOURCE_HEALTH: { return health; }
+               case RESOURCE_ARMOR: { return armorvalue; }
+               case RESOURCE_SHELLS: { return ammo_shells; }
+               case RESOURCE_BULLETS: { return ammo_nails; }
+               case RESOURCE_ROCKETS: { return ammo_rockets; }
+               case RESOURCE_CELLS: { return ammo_cells; }
+               case RESOURCE_PLASMA: { return ammo_plasma; }
+               case RESOURCE_FUEL: { return ammo_fuel; }
+       }
+       error("GetResourceField: Invalid resource type.");
+       return health;
+}
diff --git a/qcsrc/server/resources.qh b/qcsrc/server/resources.qh
new file mode 100644 (file)
index 0000000..ce8e1e8
--- /dev/null
@@ -0,0 +1,70 @@
+#pragma once
+/// \file
+/// \brief Header file that describes the resource system.
+/// \author Lyberta
+/// \copyright GNU GPLv2 or any later version.
+
+/// \brief Unconditional maximum amount of resources the entity can have.
+const int RESOURCE_AMOUNT_HARD_LIMIT = 999;
+
+/// \brief Describes the available resource types.
+enum
+{
+       RESOURCE_HEALTH = 1, ///< Health.
+       RESOURCE_ARMOR, ///< Armor.
+       RESOURCE_SHELLS, ///< Shells (used by shotgun).
+       RESOURCE_BULLETS, ///< Bullets (used by machinegun and rifle)
+       RESOURCE_ROCKETS, ///< Rockets (used by mortar, hagar, devastator, etc).
+       RESOURCE_CELLS, ///< Cells (used by electro, crylink, vortex, etc)
+       RESOURCE_PLASMA, ///< Plasma (unused).
+       RESOURCE_FUEL ///< Fuel (used by jetpack).
+};
+
+// ============================ Public API ====================================
+
+/// \brief Returns the maximum amount of the given resource.
+/// \param[in] e Entity to check.
+/// \param[in] resource_type Type of the resource (a RESOURCE_* constant).
+/// \return Maximum amount of the given resource.
+float GetResourceLimit(entity e, int resource_type);
+
+/// \brief Returns the current amount of resource the given entity has.
+/// \param[in] e Entity to check.
+/// \param[in] resource_type Type of the resource (a RESOURCE_* constant).
+/// \return Current amount of resource the given entity has.
+float GetResourceAmount(entity e, int resource_type);
+
+/// \brief Sets the current amount of resource the given entity will have.
+/// \param[in,out] e Entity to adjust.
+/// \param[in] resource_type Type of the resource (a RESOURCE_* constant).
+/// \param[in] amount Amount of resource to set.
+/// \return No return.
+void SetResourceAmount(entity e, int resource_type, float amount);
+
+/// \brief Gives an entity some resource.
+/// \param[in,out] receiver Entity to give resource to.
+/// \param[in] resource_type Type of the resource (a RESOURCE_* constant).
+/// \param[in] amount Amount of resource to give.
+/// \return No return.
+void GiveResource(entity receiver, int resource_type, float amount);
+
+/// \brief Gives an entity some resource but not more than a limit.
+/// \param[in,out] receiver Entity to give resource to.
+/// \param[in] resource_type Type of the resource (a RESOURCE_* constant).
+/// \param[in] amount Amount of resource to give.
+/// \param[in] limit Limit of resources to give.
+/// \return No return.
+void GiveResourceWithLimit(entity receiver, int resource_type, float amount,
+       float limit);
+
+// ===================== Legacy and/or internal API ===========================
+
+/// \brief Converts an entity field to resource type.
+/// \param[in] resource_field Entity field to convert.
+/// \return Resource type (a RESOURCE_* constant).
+int GetResourceType(.float resource_field);
+
+/// \brief Converts resource type (a RESOURCE_* constant) to entity field.
+/// \param[in] resource_type Type of the resource.
+/// \return Entity field for that resource.
+.float GetResourceField(int resource_type);
index 266f773..11bc602 100644 (file)
@@ -54,7 +54,7 @@ vector ScoreField_Compare(entity t1, entity t2, .float field, float fieldflags,
 
 bool TeamScore_SendEntity(entity this, entity to, float sendflags)
 {
-       float i, p, longflags;
+       float i, longflags;
 
        WriteHeader(MSG_ENTITY, ENT_CLIENT_TEAMSCORES);
        int t = this.team - 1;
@@ -62,9 +62,9 @@ bool TeamScore_SendEntity(entity this, entity to, float sendflags)
        WriteByte(MSG_ENTITY, t);
 
        longflags = 0;
-       for(i = 0, p = 1; i < MAX_TEAMSCORE; ++i, p *= 2)
+       for(i = 0; i < MAX_TEAMSCORE; ++i)
                if(this.(teamscores(i)) > 127 || this.(teamscores(i)) <= -128)
-                       longflags |= p;
+                       longflags |= BIT(i);
 
 #if MAX_TEAMSCORE <= 8
        WriteByte(MSG_ENTITY, sendflags);
@@ -73,10 +73,10 @@ bool TeamScore_SendEntity(entity this, entity to, float sendflags)
        WriteShort(MSG_ENTITY, sendflags);
        WriteShort(MSG_ENTITY, longflags);
 #endif
-       for(i = 0, p = 1; i < MAX_TEAMSCORE; ++i, p *= 2)
-               if(sendflags & p)
+       for(i = 0; i < MAX_TEAMSCORE; ++i)
+               if(sendflags & BIT(i))
                {
-                       if(longflags & p)
+                       if(longflags & BIT(i))
                                WriteInt24_t(MSG_ENTITY, this.(teamscores(i)));
                        else
                                WriteChar(MSG_ENTITY, this.(teamscores(i)));
index 2b539c9..d46d95b 100644 (file)
@@ -5,6 +5,7 @@
 #include "client.qh"
 #include "scores.qh"
 #include <common/gamemodes/rules.qh>
+#include "teamplay.qh"
 
 int ScoreRules_teams;
 
index fcae79e..34f3cdc 100644 (file)
@@ -245,6 +245,77 @@ void StartFrame()
 .string gametypefilter;
 .string cvarfilter;
 bool DoesQ3ARemoveThisEntity(entity this);
+
+/**
+ * Evaluate an expression of the form: [+ | -]? [var[op]val | [op]var | val | var] ...
+ * +: all must match. this is the default
+ * -: one must NOT match
+ *
+ * var>x
+ * var<x
+ * var>=x
+ * var<=x
+ * var==x
+ * var!=x
+ * var===x
+ * var!==x
+ */
+bool expr_evaluate(string s)
+{
+    bool ret = false;
+    if (str2chr(s, 0) == '+') {
+        s = substring(s, 1, -1);
+    } else if (str2chr(s, 0) == '-') {
+        ret = true;
+        s = substring(s, 1, -1);
+    }
+    bool expr_fail = false;
+    for (int i = 0, n = tokenize_console(s); i < n; ++i) {
+        int o;
+        string k, v;
+        s = argv(i);
+        #define X(expr) \
+            if (expr) { \
+                continue; \
+            } else { \
+                expr_fail = true; \
+                break; \
+            }
+        #define BINOP(op, len, expr) \
+            if ((o = strstrofs(s, op, 0)) >= 0) { \
+                k = substring(s, 0, o); \
+                v = substring(s, o + len, -1); \
+                X(expr); \
+            }
+        BINOP(">=", 2, cvar(k) >= stof(v));
+        BINOP("<=", 2, cvar(k) <= stof(v));
+        BINOP(">",  1, cvar(k) >  stof(v));
+        BINOP("<",  1, cvar(k) <  stof(v));
+        BINOP("==", 2, cvar(k) == stof(v));
+        BINOP("!=", 2, cvar(k) != stof(v));
+        BINOP("===", 3, cvar_string(k) == v);
+        BINOP("!==", 3, cvar_string(k) != v);
+        {
+            k = s;
+            bool b = true;
+            if (str2chr(k, 0) == '!') {
+                k = substring(s, 1, -1);
+                b = false;
+            }
+            float f = stof(k);
+            bool isnum = ftos(f) == k;
+            X(boolean(isnum ? f : cvar(k)) == b);
+        }
+        #undef BINOP
+        #undef X
+    }
+    if (!expr_fail) {
+        ret = !ret;
+    }
+    // now ret is true if we want to keep the item, and false if we want to get rid of it
+    return ret;
+}
+
 void SV_OnEntityPreSpawnFunction(entity this)
 {
        __spawnfunc_expecting = true;
@@ -253,160 +324,44 @@ void SV_OnEntityPreSpawnFunction(entity this)
        if (this.gametypefilter != "")
        if (!isGametypeInFilter(MapInfo_LoadedGametype, teamplay, have_team_spawns, this.gametypefilter))
        {
-               delete(this);
-               __spawnfunc_expecting = false;
-               return;
+               goto cleanup;
        }
-       if(this.cvarfilter != "")
-       {
-               float n, i, o, inv;
-               string s, k, v;
-               inv = 0;
-
-               s = this.cvarfilter;
-               if(substring(s, 0, 1) == "+")
-               {
-                       s = substring(s, 1, -1);
-               }
-               else if(substring(s, 0, 1) == "-")
-               {
-                       inv = 1;
-                       s = substring(s, 1, -1);
-               }
-
-               n = tokenize_console(s);
-               for(i = 0; i < n; ++i)
-               {
-                       s = argv(i);
-                       // syntax:
-                       // var>x
-                       // var<x
-                       // var>=x
-                       // var<=x
-                       // var==x
-                       // var!=x
-                       // var===x
-                       // var!==x
-                       if((o = strstrofs(s, ">=", 0)) >= 0)
-                       {
-                               k = substring(s, 0, o);
-                               v = substring(s, o+2, -1);
-                               if(cvar(k) < stof(v))
-                                       goto cvar_fail;
-                       }
-                       else if((o = strstrofs(s, "<=", 0)) >= 0)
-                       {
-                               k = substring(s, 0, o);
-                               v = substring(s, o+2, -1);
-                               if(cvar(k) > stof(v))
-                                       goto cvar_fail;
-                       }
-                       else if((o = strstrofs(s, ">", 0)) >= 0)
-                       {
-                               k = substring(s, 0, o);
-                               v = substring(s, o+1, -1);
-                               if(cvar(k) <= stof(v))
-                                       goto cvar_fail;
-                       }
-                       else if((o = strstrofs(s, "<", 0)) >= 0)
-                       {
-                               k = substring(s, 0, o);
-                               v = substring(s, o+1, -1);
-                               if(cvar(k) >= stof(v))
-                                       goto cvar_fail;
-                       }
-                       else if((o = strstrofs(s, "==", 0)) >= 0)
-                       {
-                               k = substring(s, 0, o);
-                               v = substring(s, o+2, -1);
-                               if(cvar(k) != stof(v))
-                                       goto cvar_fail;
-                       }
-                       else if((o = strstrofs(s, "!=", 0)) >= 0)
-                       {
-                               k = substring(s, 0, o);
-                               v = substring(s, o+2, -1);
-                               if(cvar(k) == stof(v))
-                                       goto cvar_fail;
-                       }
-                       else if((o = strstrofs(s, "===", 0)) >= 0)
-                       {
-                               k = substring(s, 0, o);
-                               v = substring(s, o+2, -1);
-                               if(cvar_string(k) != v)
-                                       goto cvar_fail;
-                       }
-                       else if((o = strstrofs(s, "!==", 0)) >= 0)
-                       {
-                               k = substring(s, 0, o);
-                               v = substring(s, o+2, -1);
-                               if(cvar_string(k) == v)
-                                       goto cvar_fail;
-                       }
-                       else if(substring(s, 0, 1) == "!")
-                       {
-                               k = substring(s, 1, -1);
-                               if(cvar(k))
-                                       goto cvar_fail;
-                       }
-                       else
-                       {
-                               k = s;
-                               if (!cvar(k))
-                                       goto cvar_fail;
-                       }
-               }
-               inv = !inv;
-LABEL(cvar_fail)
-               // now inv is 1 if we want to keep the item, and 0 if we want to get rid of it
-               if (!inv)
-               {
-                       //print("cvarfilter fail\n");
-                       delete(this);
-                       __spawnfunc_expecting = false;
-                       return;
-               }
+       if (this.cvarfilter != "" && !expr_evaluate(this.cvarfilter)) {
+        goto cleanup;
        }
 
-       if(DoesQ3ARemoveThisEntity(this))
-       {
-               delete(this);
-               __spawnfunc_expecting = false;
-               return;
+       if (DoesQ3ARemoveThisEntity(this)) {
+               goto cleanup;
        }
 
        set_movetype(this, this.movetype);
 
-       if(this.monster_attack)
+       if (this.monster_attack) {
                IL_PUSH(g_monster_targets, this);
+    }
 
        // support special -1 and -2 angle from radiant
-       if (this.angles == '0 -1 0')
+       if (this.angles == '0 -1 0') {
                this.angles = '-90 0 0';
-       else if (this.angles == '0 -2 0')
+       } else if (this.angles == '0 -2 0') {
                this.angles = '+90 0 0';
-
-       if(this.originjitter.x != 0)
-               this.origin_x = this.origin.x + (random() * 2 - 1) * this.originjitter.x;
-       if(this.originjitter.y != 0)
-               this.origin_y = this.origin.y + (random() * 2 - 1) * this.originjitter.y;
-       if(this.originjitter.z != 0)
-               this.origin_z = this.origin.z + (random() * 2 - 1) * this.originjitter.z;
-       if(this.anglesjitter.x != 0)
-               this.angles_x = this.angles.x + (random() * 2 - 1) * this.anglesjitter.x;
-       if(this.anglesjitter.y != 0)
-               this.angles_y = this.angles.y + (random() * 2 - 1) * this.anglesjitter.y;
-       if(this.anglesjitter.z != 0)
-               this.angles_z = this.angles.z + (random() * 2 - 1) * this.anglesjitter.z;
-       if(this.anglejitter != 0)
-               this.angles_y = this.angles.y + (random() * 2 - 1) * this.anglejitter;
-
-       if(MUTATOR_CALLHOOK(OnEntityPreSpawn, this))
-       {
-               delete(this);
-               __spawnfunc_expecting = false;
-               return;
+    }
+
+    #define X(out, in) MACRO_BEGIN \
+        if (in != 0) { out = out + (random() * 2 - 1) * in; } \
+    MACRO_END
+    X(this.origin.x, this.originjitter.x); X(this.origin.y, this.originjitter.y); X(this.origin.z, this.originjitter.z);
+    X(this.angles.x, this.anglesjitter.x); X(this.angles.y, this.anglesjitter.y); X(this.angles.z, this.anglesjitter.z);
+    X(this.angles.y, this.anglejitter);
+    #undef X
+
+       if (MUTATOR_CALLHOOK(OnEntityPreSpawn, this)) {
+               goto cleanup;
        }
+       return;
+LABEL(cleanup)
+    builtin_remove(this);
+    __spawnfunc_expecting = false;
 }
 
 void WarpZone_PostInitialize_Callback()
index 6f70f09..7f86d19 100644 (file)
@@ -1 +1,3 @@
 #pragma once
+
+bool expr_evaluate(string s);
index e93a032..24e9a52 100644 (file)
@@ -43,8 +43,14 @@ void InitGameplayMode()
 
        // find out good world mins/maxs bounds, either the static bounds found by looking for solid, or the mapinfo specified bounds
        get_mi_min_max(1);
-       world.mins = mi_min;
-       world.maxs = mi_max;
+       // assign reflectively to avoid "assignment to world" warning
+       int done = 0; for (int i = 0, n = numentityfields(); i < n; ++i) {
+           string k = entityfieldname(i); vector v = (k == "mins") ? mi_min : (k == "maxs") ? mi_max : '0 0 0';
+           if (v) {
+            putentityfieldstring(i, world, sprintf("%d %d %d", v));
+            if (++done == 2) break;
+        }
+       }
        // currently, NetRadiant's limit is 131072 qu for each side
        // distance from one corner of a 131072qu cube to the opposite corner is approx. 227023 qu
        // set the distance according to map size but don't go over the limit to avoid issues with float precision
@@ -160,58 +166,79 @@ void setcolor(entity this, int clr)
 #endif
 }
 
-void SetPlayerColors(entity pl, float _color)
+void SetPlayerColors(entity player, float _color)
 {
-       /*string s;
-       s = ftos(cl);
-       stuffcmd(pl, strcat("color ", s, " ", s, "\n")  );
-       pl.team = cl + 1;
-       //pl.clientcolors = pl.clientcolors - (pl.clientcolors & 15) + cl;
-       pl.clientcolors = 16*cl + cl;*/
-
-       float pants, shirt;
-       pants = _color & 0x0F;
-       shirt = _color & 0xF0;
-
-
-       if(teamplay) {
-               setcolor(pl, 16*pants + pants);
-       } else {
-               setcolor(pl, shirt + pants);
+       float pants = _color & 0x0F;
+       float shirt = _color & 0xF0;
+       if (teamplay)
+       {
+               setcolor(player, 16 * pants + pants);
+       }
+       else
+       {
+               setcolor(player, shirt + pants);
        }
 }
 
-void SetPlayerTeam(entity pl, float t, float s, float noprint)
+void KillPlayerForTeamChange(entity player)
 {
-       float _color;
-
-       if(t == 4)
-               _color = NUM_TEAM_4 - 1;
-       else if(t == 3)
-               _color = NUM_TEAM_3 - 1;
-       else if(t == 2)
-               _color = NUM_TEAM_2 - 1;
-       else
-               _color = NUM_TEAM_1 - 1;
-
-       SetPlayerColors(pl,_color);
-
-       if(t != s) {
-               LogTeamchange(pl.playerid, pl.team, 3);  // log manual team join
+       if (IS_DEAD(player))
+       {
+               return;
+       }
+       if (MUTATOR_CALLHOOK(Player_ChangeTeamKill, player) == true)
+       {
+               return;
+       }
+       Damage(player, player, player, 100000, DEATH_TEAMCHANGE.m_id, player.origin,
+               '0 0 0');
+}
 
-               if(!noprint)
-                       bprint(playername(pl, false), "^7 has changed from ", Team_NumberToColoredFullName(s), "^7 to ", Team_NumberToColoredFullName(t), "\n");
+bool SetPlayerTeamSimple(entity player, int team_num)
+{
+       if (player.team == team_num)
+       {
+               // This is important when players join the game and one of their color
+               // matches the team color while other doesn't. For example [BOT]Lion.
+               SetPlayerColors(player, team_num - 1);
+               return true;
        }
+       if (MUTATOR_CALLHOOK(Player_ChangeTeam, player, Team_TeamToNumber(
+               player.team), Team_TeamToNumber(team_num)) == true)
+       {
+               // Mutator has blocked team change.
+               return false;
+       }
+       int old_team = player.team;
+       SetPlayerColors(player, team_num - 1);
+       MUTATOR_CALLHOOK(Player_ChangedTeam, player, old_team, player.team);
+       return true;
+}
 
+bool SetPlayerTeam(entity player, int destination_team, int source_team,
+       bool no_print)
+{
+       int team_num = Team_NumberToTeam(destination_team);
+       if (!SetPlayerTeamSimple(player, team_num))
+       {
+               return false;
+       }
+       LogTeamchange(player.playerid, player.team, 3);  // log manual team join
+       if (no_print)
+       {
+               return true;
+       }
+       bprint(playername(player, false), "^7 has changed from ", Team_NumberToColoredFullName(source_team), "^7 to ", Team_NumberToColoredFullName(destination_team), "\n");
+       return true;
 }
 
 // set c1...c4 to show what teams are allowed
-void CheckAllowedTeams (entity for_whom)
+void CheckAllowedTeams(entity for_whom)
 {
        int teams_mask = 0;
 
        c1 = c2 = c3 = c4 = -1;
-       cb1 = cb2 = cb3 = cb4 = 0;
+       num_bots_team1 = num_bots_team2 = num_bots_team3 = num_bots_team4 = 0;
 
        string teament_name = string_null;
 
@@ -313,284 +340,570 @@ float PlayerValue(entity p)
 // teams that are allowed will now have their player counts stored in c1...c4
 void GetTeamCounts(entity ignore)
 {
-       float value, bvalue;
-       // now count how many players are on each team already
-
-       // FIXME: also find and memorize the lowest-scoring bot on each team (in case players must be shuffled around)
-       // also remember the lowest-scoring player
-
-       FOREACH_CLIENT(true, {
-               float t;
-               if(IS_PLAYER(it) || it.caplayer)
-                       t = it.team;
-               else if(it.team_forced > 0)
-                       t = it.team_forced; // reserve the spot
-               else
-                       continue;
-               if(it != ignore)// && it.netname != "")
+       if (MUTATOR_CALLHOOK(GetTeamCounts) == true)
+       {
+               if (c1 >= 0)
                {
+                       MUTATOR_CALLHOOK(GetTeamCount, NUM_TEAM_1, ignore, c1,
+                               num_bots_team1, lowest_human_team1, lowest_bot_team1);
+                       c1 = M_ARGV(2, float);
+                       num_bots_team1 = M_ARGV(3, float);
+                       lowest_human_team1 = M_ARGV(4, entity);
+                       lowest_bot_team1 = M_ARGV(5, entity);
+               }
+               if (c2 >= 0)
+               {
+                       MUTATOR_CALLHOOK(GetTeamCount, NUM_TEAM_2, ignore, c2,
+                               num_bots_team2, lowest_human_team2, lowest_bot_team2);
+                       c2 = M_ARGV(2, float);
+                       num_bots_team2 = M_ARGV(3, float);
+                       lowest_human_team2 = M_ARGV(4, entity);
+                       lowest_bot_team2 = M_ARGV(5, entity);
+               }
+               if (c3 >= 0)
+               {
+                       MUTATOR_CALLHOOK(GetTeamCount, NUM_TEAM_3, ignore, c3,
+                               num_bots_team3, lowest_human_team3, lowest_bot_team3);
+                       c3 = M_ARGV(2, float);
+                       num_bots_team3 = M_ARGV(3, float);
+                       lowest_human_team3 = M_ARGV(4, entity);
+                       lowest_bot_team3 = M_ARGV(5, entity);
+               }
+               if (c4 >= 0)
+               {
+                       MUTATOR_CALLHOOK(GetTeamCount, NUM_TEAM_4, ignore,
+                               c4, num_bots_team4, lowest_human_team4, lowest_bot_team4);
+                       c4 = M_ARGV(2, float);
+                       num_bots_team4 = M_ARGV(3, float);
+                       lowest_human_team4 = M_ARGV(4, entity);
+                       lowest_bot_team4 = M_ARGV(5, entity);
+               }
+       }
+       else
+       {
+               float value, bvalue;
+               // now count how many players are on each team already
+               float lowest_human_score1 = FLOAT_MAX;
+               float lowest_bot_score1 = FLOAT_MAX;
+               float lowest_human_score2 = FLOAT_MAX;
+               float lowest_bot_score2 = FLOAT_MAX;
+               float lowest_human_score3 = FLOAT_MAX;
+               float lowest_bot_score3 = FLOAT_MAX;
+               float lowest_human_score4 = FLOAT_MAX;
+               float lowest_bot_score4 = FLOAT_MAX;
+               FOREACH_CLIENT(true,
+               {
+                       float t;
+                       if (IS_PLAYER(it) || it.caplayer)
+                       {
+                               t = it.team;
+                       }
+                       else if (it.team_forced > 0)
+                       {
+                               t = it.team_forced; // reserve the spot
+                       }
+                       else
+                       {
+                               continue;
+                       }
+                       if (it == ignore)
+                       {
+                               continue;
+                       }
                        value = PlayerValue(it);
-                       if(IS_BOT_CLIENT(it))
+                       if (IS_BOT_CLIENT(it))
+                       {
                                bvalue = value;
+                       }
                        else
+                       {
                                bvalue = 0;
-                       if(t == NUM_TEAM_1)
+                       }
+                       if (value == 0)
                        {
-                               if(c1 >= 0)
-                               {
-                                       c1 = c1 + value;
-                                       cb1 = cb1 + bvalue;
-                               }
+                               continue;
                        }
-                       else if(t == NUM_TEAM_2)
+                       switch (t)
                        {
-                               if(c2 >= 0)
+                               case NUM_TEAM_1:
                                {
-                                       c2 = c2 + value;
-                                       cb2 = cb2 + bvalue;
+                                       if (c1 < 0)
+                                       {
+                                               break;
+                                       }
+                                       c1 += value;
+                                       num_bots_team1 += bvalue;
+                                       float temp_score = PlayerScore_Get(it, SP_SCORE);
+                                       if (!bvalue)
+                                       {
+                                               if (temp_score < lowest_human_score1)
+                                               {
+                                                       lowest_human_team1 = it;
+                                                       lowest_human_score1 = temp_score;
+                                               }
+                                               break;
+                                       }
+                                       if (temp_score < lowest_bot_score1)
+                                       {
+                                               lowest_bot_team1 = it;
+                                               lowest_bot_score1 = temp_score;
+                                       }
+                                       break;
                                }
-                       }
-                       else if(t == NUM_TEAM_3)
-                       {
-                               if(c3 >= 0)
+                               case NUM_TEAM_2:
                                {
-                                       c3 = c3 + value;
-                                       cb3 = cb3 + bvalue;
+                                       if (c2 < 0)
+                                       {
+                                               break;
+                                       }
+                                       c2 += value;
+                                       num_bots_team2 += bvalue;
+                                       float temp_score = PlayerScore_Get(it, SP_SCORE);
+                                       if (!bvalue)
+                                       {
+                                               if (temp_score < lowest_human_score2)
+                                               {
+                                                       lowest_human_team2 = it;
+                                                       lowest_human_score2 = temp_score;
+                                               }
+                                               break;
+                                       }
+                                       if (temp_score < lowest_bot_score2)
+                                       {
+                                               lowest_bot_team2 = it;
+                                               lowest_bot_score2 = temp_score;
+                                       }
+                                       break;
                                }
-                       }
-                       else if(t == NUM_TEAM_4)
-                       {
-                               if(c4 >= 0)
+                               case NUM_TEAM_3:
+                               {
+                                       if (c3 < 0)
+                                       {
+                                               break;
+                                       }
+                                       c3 += value;
+                                       num_bots_team3 += bvalue;
+                                       float temp_score = PlayerScore_Get(it, SP_SCORE);
+                                       if (!bvalue)
+                                       {
+                                               if (temp_score < lowest_human_score3)
+                                               {
+                                                       lowest_human_team3 = it;
+                                                       lowest_human_score3 = temp_score;
+                                               }
+                                               break;
+                                       }
+                                       if (temp_score < lowest_bot_score3)
+                                       {
+                                               lowest_bot_team3 = it;
+                                               lowest_bot_score3 = temp_score;
+                                       }
+                                       break;
+                               }
+                               case NUM_TEAM_4:
                                {
-                                       c4 = c4 + value;
-                                       cb4 = cb4 + bvalue;
+                                       if (c4 < 0)
+                                       {
+                                               break;
+                                       }
+                                       c4 += value;
+                                       num_bots_team4 += bvalue;
+                                       float temp_score = PlayerScore_Get(it, SP_SCORE);
+                                       if (!bvalue)
+                                       {
+                                               if (temp_score < lowest_human_score4)
+                                               {
+                                                       lowest_human_team4 = it;
+                                                       lowest_human_score4 = temp_score;
+                                               }
+                                               break;
+                                       }
+                                       if (temp_score < lowest_bot_score4)
+                                       {
+                                               lowest_bot_team4 = it;
+                                               lowest_bot_score4 = temp_score;
+                                       }
+                                       break;
                                }
                        }
-               }
-       });
+               });
+       }
 
        // if the player who has a forced team has not joined yet, reserve the spot
        if(autocvar_g_campaign)
        {
                switch(autocvar_g_campaign_forceteam)
                {
-                       case 1: if(c1 == cb1) ++c1; break;
-                       case 2: if(c2 == cb2) ++c2; break;
-                       case 3: if(c3 == cb3) ++c3; break;
-                       case 4: if(c4 == cb4) ++c4; break;
+                       case 1: if(c1 == num_bots_team1) ++c1; break;
+                       case 2: if(c2 == num_bots_team2) ++c2; break;
+                       case 3: if(c3 == num_bots_team3) ++c3; break;
+                       case 4: if(c4 == num_bots_team4) ++c4; break;
                }
        }
 }
 
-float TeamSmallerEqThanTeam(float ta, float tb, entity e)
+bool IsTeamSmallerThanTeam(int team_a, int team_b, entity player,
+       bool use_score)
 {
+       if (team_a == team_b)
+       {
+               return false;
+       }
        // we assume that CheckAllowedTeams and GetTeamCounts have already been called
-       float f;
-       float ca = -1, cb = -1, cba = 0, cbb = 0, sa = 0, sb = 0;
-
-       switch(ta)
+       int num_players_team_a = -1, num_players_team_b = -1;
+       int num_bots_team_a = 0, num_bots_team_b = 0;
+       float score_team_a = 0, score_team_b = 0;
+       switch (team_a)
        {
-               case 1: ca = c1; cba = cb1; sa = team1_score; break;
-               case 2: ca = c2; cba = cb2; sa = team2_score; break;
-               case 3: ca = c3; cba = cb3; sa = team3_score; break;
-               case 4: ca = c4; cba = cb4; sa = team4_score; break;
+               case 1:
+               {
+                       num_players_team_a = c1;
+                       num_bots_team_a = num_bots_team1;
+                       score_team_a = team1_score;
+                       break;
+               }
+               case 2:
+               {
+                       num_players_team_a = c2;
+                       num_bots_team_a = num_bots_team2;
+                       score_team_a = team2_score;
+                       break;
+               }
+               case 3:
+               {
+                       num_players_team_a = c3;
+                       num_bots_team_a = num_bots_team3;
+                       score_team_a = team3_score;
+                       break;
+               }
+               case 4:
+               {
+                       num_players_team_a = c4;
+                       num_bots_team_a = num_bots_team4;
+                       score_team_a = team4_score;
+                       break;
+               }
        }
-       switch(tb)
+       switch (team_b)
        {
-               case 1: cb = c1; cbb = cb1; sb = team1_score; break;
-               case 2: cb = c2; cbb = cb2; sb = team2_score; break;
-               case 3: cb = c3; cbb = cb3; sb = team3_score; break;
-               case 4: cb = c4; cbb = cb4; sb = team4_score; break;
+               case 1:
+               {
+                       num_players_team_b = c1;
+                       num_bots_team_b = num_bots_team1;
+                       score_team_b = team1_score;
+                       break;
+               }
+               case 2:
+               {
+                       num_players_team_b = c2;
+                       num_bots_team_b = num_bots_team2;
+                       score_team_b = team2_score;
+                       break;
+               }
+               case 3:
+               {
+                       num_players_team_b = c3;
+                       num_bots_team_b = num_bots_team3;
+                       score_team_b = team3_score;
+                       break;
+               }
+               case 4:
+               {
+                       num_players_team_b = c4;
+                       num_bots_team_b = num_bots_team4;
+                       score_team_b = team4_score;
+                       break;
+               }
        }
-
        // invalid
-       if(ca < 0 || cb < 0)
+       if (num_players_team_a < 0 || num_players_team_b < 0)
+       {
                return false;
-
-       // equal
-       if(ta == tb)
+       }
+       if (IS_REAL_CLIENT(player) && bots_would_leave)
+       {
+               num_players_team_a -= num_bots_team_a;
+               num_players_team_b -= num_bots_team_b;
+       }
+       if (!use_score)
+       {
+               return num_players_team_a < num_players_team_b;
+       }
+       if (num_players_team_a < num_players_team_b)
+       {
                return true;
+       }
+       if (num_players_team_a > num_players_team_b)
+       {
+               return false;
+       }
+       return score_team_a < score_team_b;
+}
 
-       if(IS_REAL_CLIENT(e))
+bool IsTeamEqualToTeam(int team_a, int team_b, entity player, bool use_score)
+{
+       if (team_a == team_b)
+       {
+               return true;
+       }
+       // we assume that CheckAllowedTeams and GetTeamCounts have already been called
+       int num_players_team_a = -1, num_players_team_b = -1;
+       int num_bots_team_a = 0, num_bots_team_b = 0;
+       float score_team_a = 0, score_team_b = 0;
+       switch (team_a)
        {
-               if(bots_would_leave)
+               case 1:
+               {
+                       num_players_team_a = c1;
+                       num_bots_team_a = num_bots_team1;
+                       score_team_a = team1_score;
+                       break;
+               }
+               case 2:
+               {
+                       num_players_team_a = c2;
+                       num_bots_team_a = num_bots_team2;
+                       score_team_a = team2_score;
+                       break;
+               }
+               case 3:
+               {
+                       num_players_team_a = c3;
+                       num_bots_team_a = num_bots_team3;
+                       score_team_a = team3_score;
+                       break;
+               }
+               case 4:
                {
-                       ca -= cba * 0.999;
-                       cb -= cbb * 0.999;
+                       num_players_team_a = c4;
+                       num_bots_team_a = num_bots_team4;
+                       score_team_a = team4_score;
+                       break;
                }
        }
+       switch (team_b)
+       {
+               case 1:
+               {
+                       num_players_team_b = c1;
+                       num_bots_team_b = num_bots_team1;
+                       score_team_b = team1_score;
+                       break;
+               }
+               case 2:
+               {
+                       num_players_team_b = c2;
+                       num_bots_team_b = num_bots_team2;
+                       score_team_b = team2_score;
+                       break;
+               }
+               case 3:
+               {
+                       num_players_team_b = c3;
+                       num_bots_team_b = num_bots_team3;
+                       score_team_b = team3_score;
+                       break;
+               }
+               case 4:
+               {
+                       num_players_team_b = c4;
+                       num_bots_team_b = num_bots_team4;
+                       score_team_b = team4_score;
+                       break;
+               }
+       }
+       // invalid
+       if (num_players_team_a < 0 || num_players_team_b < 0)
+               return false;
 
-       // keep teams alive (teams of size 0 always count as smaller, ignoring score)
-       if(ca < 1)
-               if(cb >= 1)
-                       return true;
-       if(ca >= 1)
-               if(cb < 1)
-                       return false;
-
-       // first, normalize
-       f = max(ca, cb, 1);
-       ca /= f;
-       cb /= f;
-       f = max(sa, sb, 1);
-       sa /= f;
-       sb /= f;
-
-       // the more we're at the end of the match, the more take scores into account
-       f = bound(0, game_completion_ratio * autocvar_g_balance_teams_scorefactor, 1);
-       ca += (sa - ca) * f;
-       cb += (sb - cb) * f;
+       if (IS_REAL_CLIENT(player) && bots_would_leave)
+       {
+               num_players_team_a -= num_bots_team_a;
+               num_players_team_b -= num_bots_team_b;
+       }
+       if (!use_score)
+       {
+               return num_players_team_a == num_players_team_b;
+       }
+       if (num_players_team_a != num_players_team_b)
+       {
+               return false;
+       }
+       return score_team_a == score_team_b;
+}
 
-       return ca <= cb;
+int FindBestTeams(entity player, bool use_score)
+{
+       if (MUTATOR_CALLHOOK(FindBestTeams, player) == true)
+       {
+               return M_ARGV(1, float);
+       }
+       int team_bits = 0;
+       int previous_team = 0;
+       if (c1 >= 0)
+       {
+               team_bits = BIT(0);
+               previous_team = 1;
+       }
+       if (c2 >= 0)
+       {
+               if (previous_team == 0)
+               {
+                       team_bits = BIT(1);
+                       previous_team = 2;
+               }
+               else if (IsTeamSmallerThanTeam(2, previous_team, player, use_score))
+               {
+                       team_bits = BIT(1);
+                       previous_team = 2;
+               }
+               else if (IsTeamEqualToTeam(2, previous_team, player, use_score))
+               {
+                       team_bits |= BIT(1);
+                       previous_team = 2;
+               }
+       }
+       if (c3 >= 0)
+       {
+               if (previous_team == 0)
+               {
+                       team_bits = BIT(2);
+                       previous_team = 3;
+               }
+               else if (IsTeamSmallerThanTeam(3, previous_team, player, use_score))
+               {
+                       team_bits = BIT(2);
+                       previous_team = 3;
+               }
+               else if (IsTeamEqualToTeam(3, previous_team, player, use_score))
+               {
+                       team_bits |= BIT(2);
+                       previous_team = 3;
+               }
+       }
+       if (c4 >= 0)
+       {
+               if (previous_team == 0)
+               {
+                       team_bits = BIT(3);
+               }
+               else if (IsTeamSmallerThanTeam(4, previous_team, player, use_score))
+               {
+                       team_bits = BIT(3);
+               }
+               else if (IsTeamEqualToTeam(4, previous_team, player, use_score))
+               {
+                       team_bits |= BIT(3);
+               }
+       }
+       return team_bits;
 }
 
 // returns # of smallest team (1, 2, 3, 4)
 // NOTE: Assumes CheckAllowedTeams has already been called!
-float FindSmallestTeam(entity pl, float ignore_pl)
+int FindSmallestTeam(entity player, float ignore_player)
 {
-       int totalteams = 0;
-       int t = 1; // initialize with a random team?
-       if(c4 >= 0) t = 4;
-       if(c3 >= 0) t = 3;
-       if(c2 >= 0) t = 2;
-       if(c1 >= 0) t = 1;
-
-       // find out what teams are available
-       //CheckAllowedTeams();
-
-       // make sure there are at least 2 teams to join
-       if(c1 >= 0)
-               totalteams = totalteams + 1;
-       if(c2 >= 0)
-               totalteams = totalteams + 1;
-       if(c3 >= 0)
-               totalteams = totalteams + 1;
-       if(c4 >= 0)
-               totalteams = totalteams + 1;
-
-       if((autocvar_bot_vs_human || pl.team_forced > 0) && totalteams == 1)
-               totalteams += 1;
-
-       if(totalteams <= 1)
+       // count how many players are in each team
+       if (ignore_player)
        {
-               if(autocvar_g_campaign && pl && IS_REAL_CLIENT(pl))
-                       return 1; // special case for campaign and player joining
-               else if(totalteams == 1) // single team
-                       LOG_TRACEF("Only 1 team available for %s, you may need to fix your map", MapInfo_Type_ToString(MapInfo_CurrentGametype()));
-               else // no teams, major no no
-                       error(sprintf("No teams available for %s\n", MapInfo_Type_ToString(MapInfo_CurrentGametype())));
+               GetTeamCounts(player);
        }
-
-       // count how many players are in each team
-       if(ignore_pl)
-               GetTeamCounts(pl);
        else
+       {
                GetTeamCounts(NULL);
-
+       }
+       int team_bits = FindBestTeams(player, true);
+       if (team_bits == 0)
+       {
+               error(sprintf("No teams available for %s\n", MapInfo_Type_ToString(MapInfo_CurrentGametype())));
+       }
        RandomSelection_Init();
-
-       if(TeamSmallerEqThanTeam(1, t, pl))
-               t = 1;
-       if(TeamSmallerEqThanTeam(2, t, pl))
-               t = 2;
-       if(TeamSmallerEqThanTeam(3, t, pl))
-               t = 3;
-       if(TeamSmallerEqThanTeam(4, t, pl))
-               t = 4;
-
-       // now t is the minimum, or A minimum!
-       if(t == 1 || TeamSmallerEqThanTeam(1, t, pl))
+       if ((team_bits & BIT(0)) != 0)
+       {
                RandomSelection_AddFloat(1, 1, 1);
-       if(t == 2 || TeamSmallerEqThanTeam(2, t, pl))
+       }
+       if ((team_bits & BIT(1)) != 0)
+       {
                RandomSelection_AddFloat(2, 1, 1);
-       if(t == 3 || TeamSmallerEqThanTeam(3, t, pl))
+       }
+       if ((team_bits & BIT(2)) != 0)
+       {
                RandomSelection_AddFloat(3, 1, 1);
-       if(t == 4 || TeamSmallerEqThanTeam(4, t, pl))
+       }
+       if ((team_bits & BIT(3)) != 0)
+       {
                RandomSelection_AddFloat(4, 1, 1);
-
+       }
        return RandomSelection_chosen_float;
 }
 
-int JoinBestTeam(entity this, bool only_return_best, bool forcebestteam)
+int JoinBestTeam(entity this, bool only_return_best, bool force_best_team)
 {
-       float smallest, selectedteam;
-
        // don't join a team if we're not playing a team game
-       if(!teamplay)
+       if (!teamplay)
+       {
                return 0;
+       }
 
        // find out what teams are available
        CheckAllowedTeams(this);
 
        // if we don't care what team he ends up on, put him on whatever team he entered as.
        // if he's not on a valid team, then let other code put him on the smallest team
-       if(!forcebestteam)
+       if (!force_best_team)
        {
+               int selected_team;
                if(     c1 >= 0 && this.team == NUM_TEAM_1)
-                       selectedteam = this.team;
+                       selected_team = this.team;
                else if(c2 >= 0 && this.team == NUM_TEAM_2)
-                       selectedteam = this.team;
+                       selected_team = this.team;
                else if(c3 >= 0 && this.team == NUM_TEAM_3)
-                       selectedteam = this.team;
+                       selected_team = this.team;
                else if(c4 >= 0 && this.team == NUM_TEAM_4)
-                       selectedteam = this.team;
+                       selected_team = this.team;
                else
-                       selectedteam = -1;
+                       selected_team = -1;
 
-               if(selectedteam > 0)
+               if (selected_team > 0)
                {
-                       if(!only_return_best)
+                       if (!only_return_best)
                        {
-                               SetPlayerColors(this, selectedteam - 1);
+                               SetPlayerTeamSimple(this, selected_team);
 
                                // when JoinBestTeam is called by client.qc/ClientKill_Now_TeamChange the players team is -1 and thus skipped
                                // when JoinBestTeam is called by client.qc/ClientConnect the player_id is 0 the log attempt is rejected
                                LogTeamchange(this.playerid, this.team, 99);
                        }
-                       return selectedteam;
+                       return selected_team;
                }
                // otherwise end up on the smallest team (handled below)
        }
 
-       smallest = FindSmallestTeam(this, true);
-
-       if(!only_return_best && !this.bot_forced_team)
+       int best_team = FindSmallestTeam(this, true);
+       if (only_return_best || this.bot_forced_team)
        {
-               TeamchangeFrags(this);
-               if(smallest == 1)
-               {
-                       SetPlayerColors(this, NUM_TEAM_1 - 1);
-               }
-               else if(smallest == 2)
-               {
-                       SetPlayerColors(this, NUM_TEAM_2 - 1);
-               }
-               else if(smallest == 3)
-               {
-                       SetPlayerColors(this, NUM_TEAM_3 - 1);
-               }
-               else if(smallest == 4)
-               {
-                       SetPlayerColors(this, NUM_TEAM_4 - 1);
-               }
-               else
-               {
-                       error("smallest team: invalid team\n");
-               }
-
-               LogTeamchange(this.playerid, this.team, 2); // log auto join
-
-               if(!IS_DEAD(this))
-                       Damage(this, this, this, 100000, DEATH_TEAMCHANGE.m_id, this.origin, '0 0 0');
+               return best_team;
        }
-
-       return smallest;
+       best_team = Team_NumberToTeam(best_team);
+       if (best_team == -1)
+       {
+               error("JoinBestTeam: invalid team\n");
+       }
+       int old_team = Team_TeamToNumber(this.team);
+       TeamchangeFrags(this);
+       SetPlayerTeamSimple(this, best_team);
+       LogTeamchange(this.playerid, this.team, 2); // log auto join
+       if (!IS_BOT_CLIENT(this))
+       {
+               AutoBalanceBots(old_team, Team_TeamToNumber(best_team));
+       }
+       KillPlayerForTeamChange(this);
+       return best_team;
 }
 
-//void() ctf_playerchanged;
 void SV_ChangeTeam(entity this, float _color)
 {
-       float scolor, dcolor, steam, dteam; //, dbotcount, scount, dcount;
+       float source_color, destination_color, source_team, destination_team;
 
        // in normal deathmatch we can just apply the color and we're done
        if(!teamplay)
@@ -606,38 +919,23 @@ void SV_ChangeTeam(entity this, float _color)
        if(!teamplay)
                return;
 
-       scolor = this.clientcolors & 0x0F;
-       dcolor = _color & 0x0F;
-
-       if(scolor == NUM_TEAM_1 - 1)
-               steam = 1;
-       else if(scolor == NUM_TEAM_2 - 1)
-               steam = 2;
-       else if(scolor == NUM_TEAM_3 - 1)
-               steam = 3;
-       else // if(scolor == NUM_TEAM_4 - 1)
-               steam = 4;
-       if(dcolor == NUM_TEAM_1 - 1)
-               dteam = 1;
-       else if(dcolor == NUM_TEAM_2 - 1)
-               dteam = 2;
-       else if(dcolor == NUM_TEAM_3 - 1)
-               dteam = 3;
-       else // if(dcolor == NUM_TEAM_4 - 1)
-               dteam = 4;
+       source_color = this.clientcolors & 0x0F;
+       destination_color = _color & 0x0F;
+
+       source_team = Team_TeamToNumber(source_color + 1);
+       destination_team = Team_TeamToNumber(destination_color + 1);
 
        CheckAllowedTeams(this);
 
-       if(dteam == 1 && c1 < 0) dteam = 4;
-       if(dteam == 4 && c4 < 0) dteam = 3;
-       if(dteam == 3 && c3 < 0) dteam = 2;
-       if(dteam == 2 && c2 < 0) dteam = 1;
+       if (destination_team == 1 && c1 < 0) destination_team = 4;
+       if (destination_team == 4 && c4 < 0) destination_team = 3;
+       if (destination_team == 3 && c3 < 0) destination_team = 2;
+       if (destination_team == 2 && c2 < 0) destination_team = 1;
 
        // not changing teams
-       if(scolor == dcolor)
+       if (source_color == destination_color)
        {
-               //bprint("same team change\n");
-               SetPlayerTeam(this, dteam, steam, true);
+               SetPlayerTeam(this, destination_team, source_team, true);
                return;
        }
 
@@ -647,169 +945,102 @@ void SV_ChangeTeam(entity this, float _color)
        }
 
        // autocvar_g_balance_teams_prevent_imbalance only makes sense if autocvar_g_balance_teams is on, as it makes the team selection dialog pointless
-       if(autocvar_g_balance_teams && autocvar_g_balance_teams_prevent_imbalance)
+       if (autocvar_g_balance_teams && autocvar_g_balance_teams_prevent_imbalance)
        {
                GetTeamCounts(this);
-               if(!TeamSmallerEqThanTeam(dteam, steam, this))
+               if ((BIT(destination_team - 1) & FindBestTeams(this, false)) == 0)
                {
                        Send_Notification(NOTIF_ONE, this, MSG_INFO, INFO_TEAMCHANGE_LARGERTEAM);
                        return;
                }
        }
-
-//     bprint("allow change teams from ", ftos(steam), " to ", ftos(dteam), "\n");
-
-       if(IS_PLAYER(this) && steam != dteam)
+       if(IS_PLAYER(this) && source_team != destination_team)
        {
                // reduce frags during a team change
                TeamchangeFrags(this);
        }
-
-       MUTATOR_CALLHOOK(Player_ChangeTeam, this, steam, dteam);
-
-       SetPlayerTeam(this, dteam, steam, !IS_CLIENT(this));
-
-       if(IS_PLAYER(this) && steam != dteam)
+       if (!SetPlayerTeam(this, destination_team, source_team, !IS_CLIENT(this)))
+       {
+               return;
+       }
+       AutoBalanceBots(source_team, destination_team);
+       if (!IS_PLAYER(this) || (source_team == destination_team))
        {
-               // kill player when changing teams
-               if(!IS_DEAD(this))
-                       Damage(this, this, this, 100000, DEATH_TEAMCHANGE.m_id, this.origin, '0 0 0');
+               return;
        }
+       KillPlayerForTeamChange(this);
 }
 
-void ShufflePlayerOutOfTeam (float source_team)
+void AutoBalanceBots(int source_team, int destination_team)
 {
-       float smallestteam, smallestteam_count, steam;
-       float lowest_bot_score, lowest_player_score;
-       entity lowest_bot, lowest_player, selected;
-
-       smallestteam = 0;
-       smallestteam_count = 999999999;
-
-       if(c1 >= 0 && c1 < smallestteam_count)
-       {
-               smallestteam = 1;
-               smallestteam_count = c1;
-       }
-       if(c2 >= 0 && c2 < smallestteam_count)
-       {
-               smallestteam = 2;
-               smallestteam_count = c2;
-       }
-       if(c3 >= 0 && c3 < smallestteam_count)
+       if ((source_team == -1) || (destination_team == -1))
        {
-               smallestteam = 3;
-               smallestteam_count = c3;
+               return;
        }
-       if(c4 >= 0 && c4 < smallestteam_count)
+       if (!autocvar_g_balance_teams ||
+               !autocvar_g_balance_teams_prevent_imbalance)
        {
-               smallestteam = 4;
-               smallestteam_count = c4;
-       }
-
-       if(!smallestteam)
-       {
-               bprint("warning: no smallest team\n");
                return;
        }
-
-       if(source_team == 1)
-               steam = NUM_TEAM_1;
-       else if(source_team == 2)
-               steam = NUM_TEAM_2;
-       else if(source_team == 3)
-               steam = NUM_TEAM_3;
-       else // if(source_team == 4)
-               steam = NUM_TEAM_4;
-
-       lowest_bot = NULL;
-       lowest_bot_score = 999999999;
-       lowest_player = NULL;
-       lowest_player_score = 999999999;
-
-       // find the lowest-scoring player & bot of that team
-       FOREACH_CLIENT(IS_PLAYER(it) && it.team == steam, {
-               if(it.isbot)
+       int num_players_source_team = 0;
+       int num_players_destination_team = 0;
+       entity lowest_bot_destination_team = NULL;
+       switch (source_team)
+       {
+               case 1:
                {
-                       if(it.totalfrags < lowest_bot_score)
-                       {
-                               lowest_bot = it;
-                               lowest_bot_score = it.totalfrags;
-                       }
+                       num_players_source_team = c1;
+                       break;
                }
-               else
+               case 2:
                {
-                       if(it.totalfrags < lowest_player_score)
-                       {
-                               lowest_player = it;
-                               lowest_player_score = it.totalfrags;
-                       }
+                       num_players_source_team = c2;
+                       break;
+               }
+               case 3:
+               {
+                       num_players_source_team = c3;
+                       break;
+               }
+               case 4:
+               {
+                       num_players_source_team = c4;
+                       break;
                }
-       });
-
-       // prefers to move a bot...
-       if(lowest_bot != NULL)
-               selected = lowest_bot;
-       // but it will move a player if it has to
-       else
-               selected = lowest_player;
-       // don't do anything if it couldn't find anyone
-       if(!selected)
-       {
-               bprint("warning: couldn't find a player to move from team\n");
-               return;
-       }
-
-       // smallest team gains a member
-       if(smallestteam == 1)
-       {
-               c1 = c1 + 1;
-       }
-       else if(smallestteam == 2)
-       {
-               c2 = c2 + 1;
-       }
-       else if(smallestteam == 3)
-       {
-               c3 = c3 + 1;
-       }
-       else if(smallestteam == 4)
-       {
-               c4 = c4 + 1;
-       }
-       else
-       {
-               bprint("warning: destination team invalid\n");
-               return;
-       }
-       // source team loses a member
-       if(source_team == 1)
-       {
-               c1 = c1 + 1;
-       }
-       else if(source_team == 2)
-       {
-               c2 = c2 + 2;
-       }
-       else if(source_team == 3)
-       {
-               c3 = c3 + 3;
        }
-       else if(source_team == 4)
+       switch (destination_team)
        {
-               c4 = c4 + 4;
+               case 1:
+               {
+                       num_players_destination_team = c1;
+                       lowest_bot_destination_team = lowest_bot_team1;
+                       break;
+               }
+               case 2:
+               {
+                       num_players_destination_team = c2;
+                       lowest_bot_destination_team = lowest_bot_team2;
+                       break;
+               }
+               case 3:
+               {
+                       num_players_destination_team = c3;
+                       lowest_bot_destination_team = lowest_bot_team3;
+                       break;
+               }
+               case 4:
+               {
+                       num_players_destination_team = c4;
+                       lowest_bot_destination_team = lowest_bot_team4;
+                       break;
+               }
        }
-       else
+       if ((num_players_destination_team <= num_players_source_team) ||
+               (lowest_bot_destination_team == NULL))
        {
-               bprint("warning: source team invalid\n");
                return;
        }
-
-       // move the player to the new team
-       TeamchangeFrags(selected);
-       SetPlayerTeam(selected, smallestteam, source_team, false);
-
-       if(!IS_DEAD(selected))
-               Damage(selected, selected, selected, 100000, DEATH_AUTOTEAMCHANGE.m_id, selected.origin, '0 0 0');
-       Send_Notification(NOTIF_ONE, selected, MSG_CENTER, CENTER_DEATH_SELF_AUTOTEAMCHANGE, selected.team);
+       SetPlayerTeamSimple(lowest_bot_destination_team,
+               Team_NumberToTeam(source_team));
+       KillPlayerForTeamChange(lowest_bot_destination_team);
 }
index 8d0ea9c..1813db0 100644 (file)
@@ -3,10 +3,29 @@
 string cache_mutatormsg;
 string cache_lastmutatormsg;
 
-// client counts for each team
-//float c1, c2, c3, c4;
-// # of bots on those teams
-float cb1, cb2, cb3, cb4;
+// The following variables are used for balancing. They are not updated
+// automatically. You need to call CheckAllowedTeams and GetTeamCounts to get
+// proper values.
+
+// These four have 2 different states. If they are equal to -1, it means that
+// the player can't join the team. Zero or positive value means that player can
+// join the team and means the number of players on that team.
+float c1;
+float c2;
+float c3;
+float c4;
+float num_bots_team1; ///< Number of bots in the first team.
+float num_bots_team2; ///< Number of bots in the second team.
+float num_bots_team3; ///< Number of bots in the third team.
+float num_bots_team4; ///< Number of bots in the fourth team.
+entity lowest_human_team1; ///< Human with the lowest score in the first team.
+entity lowest_human_team2; ///< Human with the lowest score in the second team.
+entity lowest_human_team3; ///< Human with the lowest score in the third team.
+entity lowest_human_team4; ///< Human with the lowest score in the fourth team.
+entity lowest_bot_team1; ///< Bot with the lowest score in the first team.
+entity lowest_bot_team2; ///< Bot with the lowest score in the second team.
+entity lowest_bot_team3; ///< Bot with the lowest score in the third team.
+entity lowest_bot_team4; ///< Bot with the lowest score in the fourth team.
 
 int redowned, blueowned, yellowowned, pinkowned;
 
@@ -24,12 +43,30 @@ string GetClientVersionMessage(entity this);
 
 string getwelcomemessage(entity this);
 
-void SetPlayerColors(entity pl, float _color);
+void SetPlayerColors(entity player, float _color);
 
-void SetPlayerTeam(entity pl, float t, float s, float noprint);
+/// \brief Kills player as a result of team change.
+/// \param[in,out] player Player to kill.
+/// \return No return.
+void KillPlayerForTeamChange(entity player);
+
+/// \brief Sets the team of the player.
+/// \param[in,out] player Player to adjust.
+/// \param[in] team_num Team number to set. See TEAM_NUM constants.
+/// \return True if team switch was successful, false otherwise.
+bool SetPlayerTeamSimple(entity player, int team_num);
+
+/// \brief Sets the team of the player.
+/// \param[in,out] player Player to adjust.
+/// \param[in] destination_team Team to set.
+/// \param[in] source_team Previous team of the player.
+/// \param[in] no_print Whether to print this event to players' console.
+/// \return True if team switch was successful, false otherwise.
+bool SetPlayerTeam(entity player, int destination_team, int source_team,
+       bool no_print);
 
 // set c1...c4 to show what teams are allowed
-void CheckAllowedTeams (entity for_whom);
+void CheckAllowedTeams(entity for_whom);
 
 float PlayerValue(entity p);
 
@@ -37,16 +74,47 @@ float PlayerValue(entity p);
 // teams that are allowed will now have their player counts stored in c1...c4
 void GetTeamCounts(entity ignore);
 
-float TeamSmallerEqThanTeam(float ta, float tb, entity e);
+/// \brief Returns whether one team is smaller than the other.
+/// \param[in] team_a First team.
+/// \param[in] team_b Second team.
+/// \param[in] player Player to check.
+/// \param[in] use_score Whether to take into account team scores.
+/// \return True if first team is smaller than the second one, false otherwise.
+/// \note This function assumes that CheckAllowedTeams and GetTeamCounts have
+/// been called.
+bool IsTeamSmallerThanTeam(int team_a, int team_b, entity player,
+       bool use_score);
+
+/// \brief Returns whether one team is equal to the other.
+/// \param[in] team_a First team.
+/// \param[in] team_b Second team.
+/// \param[in] player Player to check.
+/// \param[in] use_score Whether to take into account team scores.
+/// \return True if first team is equal to the second one, false otherwise.
+/// \note This function assumes that CheckAllowedTeams and GetTeamCounts have
+/// been called.
+bool IsTeamEqualToTeam(int team_a, int team_b, entity player, bool use_score);
+
+/// \brief Returns the bitmask of the best teams for the player to join.
+/// \param[in] player Player to check.
+/// \param[in] use_score Whether to take into account team scores.
+/// \return Bitmask of the best teams for the player to join.
+/// \note This function assumes that CheckAllowedTeams and GetTeamCounts have
+/// been called.
+int FindBestTeams(entity player, bool use_score);
 
 // returns # of smallest team (1, 2, 3, 4)
 // NOTE: Assumes CheckAllowedTeams has already been called!
-float FindSmallestTeam(entity pl, float ignore_pl);
-
-int JoinBestTeam(entity this, bool only_return_best, bool forcebestteam);
+int FindSmallestTeam(entity player, float ignore_player);
 
-//void() ctf_playerchanged;
+int JoinBestTeam(entity this, bool only_return_best, bool force_best_team);
 
-void ShufflePlayerOutOfTeam (float source_team);
+/// \brief Auto balances bots in teams after the player has changed team.
+/// \param[in] source_team Previous team of the player (1, 2, 3, 4).
+/// \param[in] destination_team Current team of the player (1, 2, 3, 4).
+/// \return No return.
+/// \note This function assumes that CheckAllowedTeams and GetTeamCounts have
+/// been called.
+void AutoBalanceBots(int source_team, int destination_team);
 
 void setcolor(entity this, int clr);
index da5c7a5..4a603c0 100644 (file)
@@ -41,7 +41,7 @@ const string STR_OBSERVER = "observer";
                } \
        } MACRO_END
 
-#define FOREACH_CLIENT(cond, body) FOREACH_CLIENTSLOT(IS_CLIENT(it) && (cond), body)
+#define FOREACH_CLIENT(cond, body) FOREACH_CLIENTSLOT(IS_CLIENT(it) && (cond), LAMBDA(body))
 
 // using the "inside out" version of knuth-fisher-yates shuffle
 // https://en.wikipedia.org/wiki/Fisher–Yates_shuffle
index e835fa6..e3d1eb0 100755 (executable)
@@ -74,6 +74,12 @@ function check() {
     done
 }
 
-check client
-check server
-check menu
+if [ ${#@} -eq 0 ]; then
+    check client
+    check server
+    check menu
+else
+    for var in ${@}; do
+        check ${var}
+    done
+fi