]> git.rm.cloudns.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Added some SMB features, all players can wear hats, ride and stomp!
authorLegendaryGuard <rootuser999@gmail.com>
Fri, 21 May 2021 20:58:55 +0000 (22:58 +0200)
committerLegendaryGuard <rootuser999@gmail.com>
Fri, 21 May 2021 20:58:55 +0000 (22:58 +0200)
20 files changed:
qcsrc/common/_all.inc
qcsrc/common/cvars.qh [new file with mode: 0644]
qcsrc/common/mutators/mutator/_mod.inc
qcsrc/common/mutators/mutator/_mod.qh
qcsrc/common/mutators/mutator/hats/_mod.inc [new file with mode: 0644]
qcsrc/common/mutators/mutator/hats/_mod.qh [new file with mode: 0644]
qcsrc/common/mutators/mutator/hats/hats.qc [new file with mode: 0644]
qcsrc/common/mutators/mutator/hats/hats.qh [new file with mode: 0644]
qcsrc/common/mutators/mutator/player_crush/_mod.inc [new file with mode: 0644]
qcsrc/common/mutators/mutator/player_crush/_mod.qh [new file with mode: 0644]
qcsrc/common/mutators/mutator/player_crush/player_crush.qc [new file with mode: 0644]
qcsrc/common/mutators/mutator/player_crush/player_crush.qh [new file with mode: 0644]
qcsrc/common/notifications/all.inc
qcsrc/common/notifications/all.qh
qcsrc/server/_mod.inc
qcsrc/server/mutators/_mod.inc
qcsrc/server/mutators/akimbo.qc [new file with mode: 0644]
qcsrc/server/mutators/piggyback.qc [new file with mode: 0644]
qcsrc/server/mutators/zombie_apocalypse.qc [new file with mode: 0644]
qcsrc/server/mute.qc [new file with mode: 0644]

index bb090b99c40e527d0b09b2e47cce7ee3521f8a79..232de12effbd6e9522a5a7543cd321d08220dc9b 100644 (file)
@@ -1,6 +1,8 @@
 noref float autocvar_net_connecttimeout = 30;
 
+//SMB mods
 #include "announcer.qc"
+#include "cvars.qh"
 
 #ifdef GAMEQC
 #include "anim.qc"
diff --git a/qcsrc/common/cvars.qh b/qcsrc/common/cvars.qh
new file mode 100644 (file)
index 0000000..d572de1
--- /dev/null
@@ -0,0 +1,17 @@
+// the mod relies on AUTOCVAR() to define cvars for use in a special dumping command, so admins know which cvars are from the mod
+// but, many of these don't have any direct references, so we need to use this custom copy of the AUTOCVAR()
+
+#define __AUTOCVAR_NOREF(file, archive, var, type, desc, default) \
+       [[accumulate]] void RegisterCvars(void(string, string, string, bool, string) f) \
+       { \
+               f( #var, repr_cvar_##type(default), desc, archive, file); \
+       } \
+       noref type autocvar_##var = default
+#define AUTOCVAR_NOREF_5(file, archive, var, type, desc) \
+       __AUTOCVAR_NOREF(file, archive, var, type, desc, default_##type)
+#define AUTOCVAR_NOREF_6(file, archive, var, type, default, desc) \
+       __AUTOCVAR_NOREF(file, archive, var, type, desc, default)
+#define _AUTOCVAR_NOREF(...) EVAL__AUTOCVAR_NOREF(OVERLOAD(AUTOCVAR_NOREF, __FILE__, __VA_ARGS__))
+#define EVAL__AUTOCVAR_NOREF(...) __VA_ARGS__
+#define AUTOCVAR_NOREF_SAVE(...) _AUTOCVAR_NOREF(true, __VA_ARGS__)
+#define AUTOCVAR_NOREF(...) _AUTOCVAR_NOREF(false, __VA_ARGS__)
index 1479a1dfa1f97ac27d196434e54882ffd6029168..1d8061419c4e390e4de7b839e251d68531ae56ae 100644 (file)
 #include <common/mutators/mutator/doublejump/_mod.inc>
 #include <common/mutators/mutator/dynamic_handicap/_mod.inc>
 #include <common/mutators/mutator/globalforces/_mod.inc>
+
+//SMB mod feature Hats:
+#include <common/mutators/mutator/hats/_mod.inc>
+
 #include <common/mutators/mutator/hook/_mod.inc>
 #include <common/mutators/mutator/instagib/_mod.inc>
 #include <common/mutators/mutator/invincibleproj/_mod.inc>
 #include <common/mutators/mutator/overkill/_mod.inc>
 #include <common/mutators/mutator/physical_items/_mod.inc>
 #include <common/mutators/mutator/pinata/_mod.inc>
+
+//SMB mod feature Player crush:
+#include <common/mutators/mutator/player_crush/_mod.inc>
+
 #include <common/mutators/mutator/random_gravity/_mod.inc>
 #include <common/mutators/mutator/random_items/_mod.inc>
 #include <common/mutators/mutator/rocketflying/_mod.inc>
index 4654958287e9ffb570ec90fe05dc7be92d3cbdfd..172b4a351e87507a75fb7a660228288d0ad9c9db 100644 (file)
 #include <common/mutators/mutator/doublejump/_mod.qh>
 #include <common/mutators/mutator/dynamic_handicap/_mod.qh>
 #include <common/mutators/mutator/globalforces/_mod.qh>
+
+//SMB mod feature Hats:
+#include <common/mutators/mutator/hats/_mod.qh>
+
 #include <common/mutators/mutator/hook/_mod.qh>
 #include <common/mutators/mutator/instagib/_mod.qh>
 #include <common/mutators/mutator/invincibleproj/_mod.qh>
 #include <common/mutators/mutator/overkill/_mod.qh>
 #include <common/mutators/mutator/physical_items/_mod.qh>
 #include <common/mutators/mutator/pinata/_mod.qh>
+
+//SMB mod feature Player crush:
+#include <common/mutators/mutator/player_crush/_mod.qh>
+
 #include <common/mutators/mutator/random_gravity/_mod.qh>
 #include <common/mutators/mutator/random_items/_mod.qh>
 #include <common/mutators/mutator/rocketflying/_mod.qh>
diff --git a/qcsrc/common/mutators/mutator/hats/_mod.inc b/qcsrc/common/mutators/mutator/hats/_mod.inc
new file mode 100644 (file)
index 0000000..46ca9fb
--- /dev/null
@@ -0,0 +1,2 @@
+// generated file; do not modify
+#include <common/mutators/mutator/hats/hats.qc>
\ No newline at end of file
diff --git a/qcsrc/common/mutators/mutator/hats/_mod.qh b/qcsrc/common/mutators/mutator/hats/_mod.qh
new file mode 100644 (file)
index 0000000..996095a
--- /dev/null
@@ -0,0 +1,2 @@
+// generated file; do not modify
+#include <common/mutators/mutator/hats/hats.qh>
\ No newline at end of file
diff --git a/qcsrc/common/mutators/mutator/hats/hats.qc b/qcsrc/common/mutators/mutator/hats/hats.qc
new file mode 100644 (file)
index 0000000..359f6fc
--- /dev/null
@@ -0,0 +1,330 @@
+//FEATURE: Hats!
+#include "hats.qh"
+
+#ifdef SVQC
+.string cvar_cl_hat;
+.bool cvar_cl_nohats;
+#elif defined(CSQC)
+string cvar_cl_hat;
+bool cvar_cl_nohats;
+#endif
+#ifdef GAMEQC
+vector get_model_parameters_hat_height;
+float get_model_parameters_hat_scale;
+vector get_model_parameters_hat_angles;
+
+REPLICATE(cvar_cl_hat, string, "cl_magical_hax");
+REPLICATE(cvar_cl_nohats, bool, "cl_nohats");
+#endif
+
+#ifdef SVQC
+void hats_Precache(string pattern);
+
+AUTOCVAR(g_hats, bool, false, "Allow clients to use hats");
+AUTOCVAR(g_hats_default, string, "", "Default hat when player has none on");
+AUTOCVAR(g_hats_nolegacy, bool, false, "Use new default offsets (not recommended yet)");
+
+REGISTER_MUTATOR(hats, autocvar_g_hats && !cvar("g_overkill"))
+{
+       MUTATOR_ONADD
+       {
+               hats_Precache("models/hats/*.md3");
+               hats_Precache("models/hats/*.obj");
+               hats_Precache("models/hats/*.iqm");
+       }
+}
+
+.entity hatentity;
+.string hatname; // only update when the player spawns
+
+//ATTRIB(Client, cvar_cl_hat, string, this.cvar_cl_hat);
+//ATTRIB(Client, cvar_cl_nohats, bool, this.cvar_cl_nohats);
+
+void hats_Precache(string pattern)
+{
+       float globhandle, i, n;
+       string f;
+
+       globhandle = search_begin(pattern, true, false);
+       if (globhandle < 0)
+               return;
+       n = search_getsize(globhandle);
+       for (i = 0; i < n; ++i)
+       {
+               f = search_getfilename(globhandle, i);
+               precache_model(f);
+       }
+       search_end(globhandle);
+}
+
+float hats_getscale(entity e)
+{
+       float s;
+       get_model_parameters(e.model, e.skin);
+       s = get_model_parameters_hat_scale;
+       get_model_parameters(string_null, 0);
+       
+       return s;
+}
+
+vector hats_getheight(entity e)
+{
+       vector s, hat_offset = '0 0 0';
+       float myscale;
+       get_model_parameters(e.model, e.skin);
+       s = get_model_parameters_hat_height;
+       myscale = get_model_parameters_hat_scale;
+       get_model_parameters(string_null, 0);
+
+       if(autocvar_g_hats_nolegacy)
+       {
+               // attempt to get default height
+               hat_offset = '0 10 -1' * ((myscale) ? myscale : 1); // legacy models are offset by a margin that varies depending on their size
+       }
+
+       return hat_offset + s;
+}
+
+vector hats_getangles(entity e)
+{
+       vector s, hat_offset = '0 0 0';
+       get_model_parameters(e.model, e.skin);
+       s = get_model_parameters_hat_angles;
+       get_model_parameters(string_null, 0);
+
+       if(autocvar_g_hats_nolegacy)
+       {
+               // attempt to get default angle
+               hat_offset = '90 0 0';
+       }
+
+       return hat_offset + s;
+}
+
+string hat_exists(string pattern)
+{
+       string try;
+       try = strcat(pattern, ".md3");
+       if(fexists(try))
+               return try;
+       try = strcat(pattern, ".obj");
+       if(fexists(try))
+               return try;
+       try = strcat(pattern, ".iqm");
+       if(fexists(try))
+               return try;
+
+       return string_null;
+}
+
+void hat_Think(entity this)
+{
+       entity player = this.owner;
+       float tag_found;
+       this.nextthink = time;
+       if (player.hatentity != this || !player)
+       {
+               delete(this);
+               return;
+       }
+       if(STAT(FROZEN, player) || IS_DEAD(player))
+       {
+               this.model = "";
+               return;
+       }
+       if (this.hatname != player.hatname || this.dmg != player.modelindex || this.deadflag != player.deadflag)
+       {
+               this.hatname = player.hatname;
+               this.dmg = player.modelindex;
+               this.deadflag = player.deadflag;
+               string exists = ((player.hatname && player.hatname != "") ? hat_exists(strcat("models/hats/", player.hatname)) : string_null);
+               if (player.hatname && player.hatname != "" && exists)
+                       _setmodel(this, exists); // precision set below
+               else
+                       this.model = "";
+
+               if((tag_found = gettagindex(player, "tag_head")))
+               {
+                       this.tag_index = tag_found;
+                       this.tag_entity = player;
+                       setorigin(this, hats_getheight(player));
+                       this.scale = hats_getscale(player);
+                       this.angles = hats_getangles(player);
+               }
+               else
+               {
+                       setattachment(this, player, "head");
+                       setorigin(this, hats_getheight(player));
+                       this.scale = hats_getscale(player);
+                       this.angles = hats_getangles(player);
+               }
+       }
+       this.effects = player.effects;
+       this.effects |= EF_LOWPRECISION;
+       this.effects = this.effects & EFMASK_CHEAP; // eat performance
+       if(this.scale < -1)
+               this.alpha = -1;
+       else if(player.alpha == default_player_alpha)
+               this.alpha = default_weapon_alpha;
+       else if(player.alpha != 0)
+               this.alpha = player.alpha;
+       else
+               this.alpha = 1;
+
+       this.glowmod = player.glowmod;
+       this.colormap = player.colormap;
+
+       CSQCMODEL_AUTOUPDATE(this);
+}
+
+bool hats_Customize(entity this, entity client)
+{
+       if(CS_CVAR(client).cvar_cl_nohats) { return false; }
+       return true;
+}
+
+void hats_SpawnHat(entity this)
+{
+       this.hatentity = new(hatentity);
+       this.hatentity.solid = SOLID_NOT;
+       this.hatentity.owner = this;
+       this.hatentity.hatentity = this.hatentity;
+       setorigin(this.hatentity, '0 0 0');
+       this.hatentity.angles = '0 0 0';
+       setthink(this.hatentity, hat_Think);
+       this.hatentity.nextthink = time;
+       setcefc(this.hatentity, hats_Customize);
+
+       CSQCMODEL_AUTOINIT(this.hatentity);
+}
+
+MUTATOR_HOOKFUNCTION(hats, PutClientInServer)
+{
+       entity player = M_ARGV(0, entity);
+
+       hats_SpawnHat(player);
+       player.hatentity.alpha = default_weapon_alpha;
+       if(CS_CVAR(player).cvar_cl_hat != "")
+               player.hatname = strzone(CS_CVAR(player).cvar_cl_hat);
+       else
+               player.hatname = strzone(autocvar_g_hats_default);
+}
+
+MUTATOR_HOOKFUNCTION(hats, ClientDisconnect)
+{
+       entity player = M_ARGV(0, entity);
+
+       player.hatentity = NULL;
+       player.hatname = "";
+}
+
+MUTATOR_HOOKFUNCTION(hats, MakePlayerObserver)
+{
+       entity player = M_ARGV(0, entity);
+
+       player.hatentity = NULL;
+       player.hatname = "";
+}
+
+MUTATOR_HOOKFUNCTION(hats, ClearModelParams)
+{
+       get_model_parameters_hat_height = '0 0 0';
+       get_model_parameters_hat_scale = 0;
+       get_model_parameters_hat_angles = '0 0 0';
+}
+
+MUTATOR_HOOKFUNCTION(hats, GetModelParams)
+{
+       string checkmodel_input = M_ARGV(0, string);
+       string checkmodel_command = M_ARGV(1, string);
+
+       if(checkmodel_input == "hat_height")
+               get_model_parameters_hat_height = stov(checkmodel_command);
+       if(checkmodel_input == "hat_scale")
+               get_model_parameters_hat_scale = stof(checkmodel_command);
+       if(checkmodel_input == "hat_angles")
+               get_model_parameters_hat_angles = stov(checkmodel_command);
+}
+
+#elif defined(CSQC)
+
+REGISTER_MUTATOR(mod_hats, true);
+
+AUTOCVAR_NOREF_SAVE(cl_nohats,                    bool,   false,      "Disable hats completely, requires reconnect or sendcvar");
+AUTOCVAR_NOREF_SAVE(cl_magical_hax,               string, "",          "Magical hax, use at your own risk");
+
+string get_model_parameters_bone_head;
+
+classfield(Skeleton) .int bone_hat;
+classfield(Skeleton) .vector hat_height;
+classfield(Skeleton) .float hat_scale;
+
+MUTATOR_HOOKFUNCTION(mod_hats, TagIndex_Apply)
+{
+       entity ent = M_ARGV(0, entity);
+
+       if(substring(ent.model, 0, 12) == "models/hats/")
+       {
+               if(substring(ent.tag_entity.model, 0, 12) == "models/hats/")
+               {
+                       ent.tag_index = gettagindex(ent.tag_entity, "tag_head");
+                       if(!ent.tag_index)
+                               ent.tag_index = gettagindex(ent.tag_entity, "head");
+                       if(!ent.tag_index)
+                       {
+                               // we need to prevent this from 'appening
+                               ent.tag_entity = NULL;
+                               ent.drawmask = 0;
+                       }
+               }
+               else if(ent.tag_entity.isplayermodel)
+               {
+                       skeleton_loadinfo(ent.tag_entity);
+                       ent.tag_index = ent.tag_entity.bone_hat;
+               }
+       }
+}
+
+MUTATOR_HOOKFUNCTION(mod_hats, ClearModelParams)
+{
+       get_model_parameters_bone_head = string_null;
+       get_model_parameters_hat_height = '0 0 0';
+       get_model_parameters_hat_scale = 0;
+       get_model_parameters_hat_angles = '0 0 0';
+}
+
+MUTATOR_HOOKFUNCTION(mod_hats, GetModelParams)
+{
+       string checkmodel_input = M_ARGV(0, string);
+       string checkmodel_command = M_ARGV(1, string);
+
+       if(checkmodel_input == "bone_head")
+               get_model_parameters_bone_head = checkmodel_command;
+       if(checkmodel_input == "hat_height")
+               get_model_parameters_hat_height = stov(checkmodel_command);
+       if(checkmodel_input == "hat_scale")
+               get_model_parameters_hat_scale = stof(checkmodel_command);
+       if(checkmodel_input == "hat_angles")
+               get_model_parameters_hat_angles = stov(checkmodel_command);
+}
+
+MUTATOR_HOOKFUNCTION(mod_hats, Skeleton_CheckBones)
+{
+       entity ent = M_ARGV(0, entity);
+
+       ent.bone_hat = gettagindex(ent, "head");
+       if(!ent.bone_hat)
+               ent.bone_hat = gettagindex(ent, "tag_head");
+       if(!ent.bone_hat)
+               ent.bone_hat = gettagindex(ent, "bip01 head");
+}
+
+MUTATOR_HOOKFUNCTION(mod_hats, Skeleton_CheckModel)
+{
+       entity ent = M_ARGV(0, entity);
+
+       if(get_model_parameters_bone_head)
+               ent.bone_hat = gettagindex(ent, get_model_parameters_bone_head);
+}
+
+#endif
diff --git a/qcsrc/common/mutators/mutator/hats/hats.qh b/qcsrc/common/mutators/mutator/hats/hats.qh
new file mode 100644 (file)
index 0000000..6f70f09
--- /dev/null
@@ -0,0 +1 @@
+#pragma once
diff --git a/qcsrc/common/mutators/mutator/player_crush/_mod.inc b/qcsrc/common/mutators/mutator/player_crush/_mod.inc
new file mode 100644 (file)
index 0000000..f0fe6e8
--- /dev/null
@@ -0,0 +1,2 @@
+// generated file; do not modify
+#include <common/mutators/mutator/player_crush/player_crush.qc>
\ No newline at end of file
diff --git a/qcsrc/common/mutators/mutator/player_crush/_mod.qh b/qcsrc/common/mutators/mutator/player_crush/_mod.qh
new file mode 100644 (file)
index 0000000..fbb0852
--- /dev/null
@@ -0,0 +1,2 @@
+// generated file; do not modify
+#include <common/mutators/mutator/player_crush/player_crush.qh>
\ No newline at end of file
diff --git a/qcsrc/common/mutators/mutator/player_crush/player_crush.qc b/qcsrc/common/mutators/mutator/player_crush/player_crush.qc
new file mode 100644 (file)
index 0000000..af78244
--- /dev/null
@@ -0,0 +1,140 @@
+//FEATURE: Crushing, for all your goomba stomping needs
+#include "player_crush.qh"
+
+REGISTER_MUTATOR(pc, true);
+
+REGISTER_STAT(PLAYER_CRUSH, bool, autocvar_g_player_crush)
+REGISTER_STAT(PLAYER_CRUSH_SIMPLE, int, autocvar_g_player_crush_simple)
+REGISTER_STAT(PLAYER_CRUSH_BOUNCE, float, autocvar_g_player_crush_bounce)
+REGISTER_STAT(PLAYER_CRUSH_BOUNCE_JUMP, float, autocvar_g_player_crush_bounce_jump)
+
+#ifdef GAMEQC
+void pc_PlayerTouch(entity this, entity toucher)
+{
+       if(toucher == NULL)
+               return;
+#endif
+
+#ifdef SVQC
+       bool and_monster = IS_MONSTER(toucher) && (this.monsterdef.spawnflags & MON_FLAG_CRUSH);
+
+       if(!autocvar_g_player_crush && !and_monster)
+               return;
+
+       if(!IS_PLAYER(this))
+               return;
+
+       if(!IS_PLAYER(toucher) && !and_monster)
+               return;
+
+       if(IS_DEAD(this) || IS_DEAD(toucher))
+               return;
+
+       if(!this.iscreature || !toucher.iscreature)
+               return;
+
+       if(weaponLocked(this))
+               return;
+#elif defined(CSQC)
+       if(STAT(PLAYER_CRUSH_SIMPLE) != -1)
+               return;
+       if(!IS_PLAYER(toucher))
+               return;
+       if(!STAT(PLAYER_CRUSH))
+               return;
+#endif
+
+#ifdef GAMEQC
+       if(STAT(PLAYER_CRUSH_SIMPLE, this) == 1 && IS_PLAYER(toucher))
+       {
+#endif
+
+#ifdef SVQC
+               vector vdir = normalize(toucher.origin - this.origin);
+
+               if(vdir_z > autocvar_g_player_crush_headheight) // adjust this to set how sharp from above players need to hit the player to crush them. 
+                       Damage (this, toucher, toucher, autocvar_g_player_crush_damage, DEATH_VH_CRUSH.m_id, DMG_NOWEP, this.origin, '0 0 0');
+#endif
+
+#ifdef GAMEQC
+       }
+       else if(STAT(PLAYER_CRUSH_SIMPLE, this) == 0)
+       {
+               tracebox(this.origin, this.mins, this.maxs, this.origin - ('0 0 1' * (this.maxs_z + 5)), MOVE_NORMAL, this);
+
+               if(trace_ent == toucher)
+               {
+                       float mjumpheight = STAT(PLAYER_CRUSH_BOUNCE, this);
+
+                       setorigin(this, this.origin + '0 0 2');
+
+                       if(PHYS_INPUT_BUTTON_JUMP(this))
+                       {
+                               mjumpheight = STAT(PLAYER_CRUSH_BOUNCE_JUMP, this);
+                               SET_JUMP_HELD(this);
+                       }
+
+                       UNSET_ONGROUND(this);
+
+                       this.velocity_z = mjumpheight;
+#endif
+       #ifdef SVQC
+                       this.oldvelocity_z = this.velocity_z;
+
+                       animdecide_setaction(this, ANIMACTION_JUMP, true);
+
+                       Damage (toucher, this, this, autocvar_g_player_crush_damage, DEATH_VH_CRUSH.m_id, DMG_NOWEP, toucher.origin, '0 0 0');
+       #endif
+#ifdef GAMEQC
+               }
+       }
+       else if(STAT(PLAYER_CRUSH_SIMPLE, this) == -1)
+       {
+               if(tracebox_hits_box(this.origin, this.mins, this.maxs, this.origin - ('0 0 1' * (this.maxs_z + 5)), toucher.origin + toucher.mins, toucher.origin + toucher.maxs))
+               {
+                       float mjumpheight = STAT(PLAYER_CRUSH_BOUNCE, this);
+
+                       setorigin(this, this.origin + '0 0 2');
+
+                       if(PHYS_INPUT_BUTTON_JUMP(this))
+                       {
+                               mjumpheight = STAT(PLAYER_CRUSH_BOUNCE_JUMP, this);
+                               SET_JUMP_HELD(this);
+                       }
+
+                       UNSET_ONGROUND(this);
+
+                       this.velocity_z = mjumpheight;
+#endif
+       #ifdef SVQC
+                       this.oldvelocity_z = this.velocity_z;
+
+                       animdecide_setaction(this, ANIMACTION_JUMP, true);
+
+                       Damage (toucher, this, this, autocvar_g_player_crush_damage, DEATH_VH_CRUSH.m_id, DMG_NOWEP, toucher.origin, '0 0 0');
+       #endif
+#ifdef GAMEQC
+               }
+       }
+}
+#endif
+
+#ifdef SVQC
+MUTATOR_HOOKFUNCTION(pc, PlayerSpawn)
+{
+       entity player = M_ARGV(0, entity);
+
+       settouch(player, pc_PlayerTouch);
+}
+#elif defined(CSQC)
+MUTATOR_HOOKFUNCTION(pc, PlayerPhysics)
+{
+       if(!STAT(PLAYER_CRUSH)) return;
+       if(STAT(PLAYER_CRUSH_SIMPLE) != -1) return;
+
+       entity player = M_ARGV(0, entity);
+       if(gettouch(player) == pc_PlayerTouch) return; // getting is cheaper than setting
+
+       settouch(player, pc_PlayerTouch);
+}
+#endif
diff --git a/qcsrc/common/mutators/mutator/player_crush/player_crush.qh b/qcsrc/common/mutators/mutator/player_crush/player_crush.qh
new file mode 100644 (file)
index 0000000..28d11a9
--- /dev/null
@@ -0,0 +1,10 @@
+#pragma once
+
+#ifdef SVQC
+AUTOCVAR(g_player_crush, bool, false, "Allow crushing players by jumping on their head");
+AUTOCVAR(g_player_crush_simple, int, 1, "Use simple height checking");
+AUTOCVAR(g_player_crush_damage, float, 200, "");
+AUTOCVAR(g_player_crush_headheight, float, 0.9, "");
+AUTOCVAR(g_player_crush_bounce, float, 300, "Bounce height in advanced trace mode");
+AUTOCVAR(g_player_crush_bounce_jump, float, 600, "Bounce height while holding jump in advanced trace mode");
+#endif
\ No newline at end of file
index 2d81dafffa29e01b70ef95845ee397c363fbbadc..2d59a677763f8ebeda519a04754b093c82af0242 100644 (file)
@@ -821,6 +821,10 @@ string multiteam_info_sprintf(string input, string teamname) { return ((input !=
     MSG_CENTER_NOTIF(OVERTIME_CONTROLPOINT,             N_ENABLE,    0, 0, "",               CPID_OVERTIME,          "5 0",  _("^F2Now playing ^F4OVERTIME^F2!\n\nGenerators are now decaying.\nThe more control points your team holds,\nthe faster the enemy generator decays"), "")
     MSG_CENTER_NOTIF(OVERTIME_TIME,                     N_ENABLE,    0, 1, "f1time",         CPID_OVERTIME,          "0 0",  _("^F2Now playing ^F4OVERTIME^F2!\n^BGAdded ^F4%s^BG to the game!"), "")
 
+    //LegendGuard adds piggyback notifications, a feature from SMB mod 21-05-2021
+    MSG_CENTER_NOTIF(PIGGYBACK_CARRYING,                N_ENABLE,    1, 0, "s1",             CPID_PIGGYBACK,         "0 0", "^BGYou are now carrying %s^BG!", "")
+    MSG_CENTER_NOTIF(PIGGYBACK_RIDING,                  N_ENABLE,    1, 0, "s1",             CPID_PIGGYBACK,         "0 0", "^BGYou are now riding %s^BG!", "")
+
     MSG_CENTER_NOTIF(PORTO_CREATED_IN,                  N_ENABLE,    0, 0, "",               CPID_Null,              "0 0",  _("^K1In^BG-portal created"), "")
     MSG_CENTER_NOTIF(PORTO_CREATED_OUT,                 N_ENABLE,    0, 0, "",               CPID_Null,              "0 0",  _("^F3Out^BG-portal created"), "")
     MSG_CENTER_NOTIF(PORTO_FAILED,                      N_ENABLE,    0, 0, "",               CPID_Null,              "0 0",  _("^F1Portal creation failed"), "")
index 809c1fc9217a80aabfc6f258b68e07e17c0fed62..45f63b3f6311d0c688d0d5f27479dc41752619b8 100644 (file)
@@ -45,6 +45,7 @@ string Get_Notif_TypeName(MSG net_type)
        return "";
 }
 //LegendGuard adds CASE(CPID, MMM) after TIMEIN for MMM 20-02-2021
+//LegendGuard adds CASE(CPID, PIGGYBACK) after OVERTIME for Piggyback 21-05-2021
 ENUMCLASS(CPID)
        CASE(CPID, ASSAULT_ROLE)
        CASE(CPID, ROUND)
@@ -72,6 +73,7 @@ ENUMCLASS(CPID)
        CASE(CPID, ONSLAUGHT)
        CASE(CPID, ONS_CAPSHIELD)
        CASE(CPID, OVERTIME)
+       CASE(CPID, PIGGYBACK)
        CASE(CPID, POWERUP)
        CASE(CPID, RACE_FINISHLAP)
        CASE(CPID, TEAMCHANGE)
index 34ca1e2c85d664ee07c2ce53177c03945a39f22e..588aa92bd9b6da31bf2f606969f540629008d53a 100644 (file)
 #include <server/main.qc>
 #include <server/mapvoting.qc>
 #include <server/matrix.qc>
+
+//SMB mods
+#include <server/mute.qc>
+//
+
 #include <server/player.qc>
 #include <server/portals.qc>
 #include <server/race.qc>
index 7b7cdf33dffba200b23b9b5ebcfb4c5fb04803d6..bc8569325e603cad34a89d093c26e7345befa2e5 100644 (file)
@@ -1,3 +1,8 @@
 // generated file; do not modify
 #include <server/mutators/events.qc>
 #include <server/mutators/loader.qc>
+
+//SMB mods
+#include <server/mutators/akimbo.qc>
+#include <server/mutators/piggyback.qc>
+#include <server/mutators/zombie_apocalypse.qc>
\ No newline at end of file
diff --git a/qcsrc/server/mutators/akimbo.qc b/qcsrc/server/mutators/akimbo.qc
new file mode 100644 (file)
index 0000000..d07cab5
--- /dev/null
@@ -0,0 +1,27 @@
+//FEATURE: Akimbo mutator for all your dual wielding needs
+
+AUTOCVAR(g_akimbo, bool, false, "Enable Akimbo mutator (requires g_weaponswitch_debug 2)");
+AUTOCVAR(g_akimbo_atspawns, bool, false, "Allow Akimbo items to be collected from item spawnpoints, not just dropped items");
+
+REGISTER_MUTATOR(akimbo, autocvar_g_akimbo && !g_nexball && autocvar_g_weaponswitch_debug == 2);
+
+MUTATOR_HOOKFUNCTION(akimbo, ItemTouch)
+{
+       if(MUTATOR_RETURNVALUE == MUT_ITEMTOUCH_RETURN)
+               return false; // already handled, probably didn't pick it up
+
+       entity item = M_ARGV(0, entity);
+       entity player = M_ARGV(1, entity);
+
+       if((Item_IsLoot(item) || autocvar_g_akimbo_atspawns) && item.weapon && 
+                                                       (STAT(WEAPONS, player) & item.itemdef.m_weapon.m_wepset) && item.owner != player && !(item.itemdef.m_weapon.spawnflags & WEP_FLAG_NODUAL))
+               PS(player).dual_weapons |= item.itemdef.m_weapon.m_wepset; // dual it up!
+}
+
+MUTATOR_HOOKFUNCTION(akimbo, FilterItem)
+{
+       entity item = M_ARGV(0, entity);
+
+       if((Item_IsLoot(item) || autocvar_g_akimbo_atspawns) && item.weapon && item.owner && IS_PLAYER(item.owner))
+               PS(item.owner).dual_weapons &= ~item.itemdef.m_weapon.m_wepset; // no longer akimbo'd (we don't check blacklist here, no need)
+}
diff --git a/qcsrc/server/mutators/piggyback.qc b/qcsrc/server/mutators/piggyback.qc
new file mode 100644 (file)
index 0000000..f9b171c
--- /dev/null
@@ -0,0 +1,479 @@
+//FEATURE: Piggy-backing lets you ride your teammates to victory
+
+AUTOCVAR(g_piggyback, bool, false, "Enable piggyback mutator (riding teammates)");
+REGISTER_MUTATOR(piggyback, autocvar_g_piggyback);
+
+AUTOCVAR(g_piggyback_ride_enemies, bool, false, "Allow riding enemies");
+
+.bool cvar_cl_nocarry;
+.bool cvar_cl_noride;
+
+REPLICATE(cvar_cl_nocarry, bool, "cl_nocarry");
+REPLICATE(cvar_cl_noride, bool, "cl_noride");
+
+//ATTRIB(Client, cvar_cl_nocarry, bool, this.cvar_cl_nocarry);
+//ATTRIB(Client, cvar_cl_noride, bool, this.cvar_cl_noride);
+
+.entity piggybacker;
+.entity pbent;
+.entity pbhost;
+.float pb_canattach;
+//.float pb_attachblock;
+.float pb_oldsolid;
+
+#define BROKEN_PBREF(e) ((e).piggybacker && ((e).piggybacker.pbhost != (e) || (e).piggybacker.move_movetype != MOVETYPE_FOLLOW))
+
+void pb_Attach(entity host, entity slave);
+void pb_Detach(entity host);
+
+entity pb_TailOf(entity p);
+entity pb_RootOf(entity p);
+
+entity pb_TailOf(entity p)
+{
+       while(p.piggybacker)
+               p = p.piggybacker;
+       return p;
+}
+
+entity pb_RootOf(entity p)
+{
+       while(p.pbhost)
+               p = p.pbhost;
+       return p;
+}
+
+.float pb_oldmoveflags;
+.float pb_oldverticalfly;
+void pb_Attach(entity host, entity slave)
+{
+       entity root = pb_RootOf(host);
+       host = pb_TailOf(host);
+       
+       if(host == slave || root == slave || host == slave.piggybacker || slave == host.piggybacker)
+               return;
+       
+       host.piggybacker = slave;
+       set_movetype(slave, MOVETYPE_FOLLOW);
+       slave.aiment = host.pbent;
+       slave.pbhost = host;
+       slave.pb_oldsolid = slave.solid;
+       slave.solid = SOLID_CORPSE;
+       slave.pb_canattach = 0;
+
+       if(IS_MONSTER(host))
+       {
+               host.pb_oldmoveflags = host.monster_moveflags;
+               host.pb_oldverticalfly = (host.spawnflags & MONSTERFLAG_FLY_VERTICAL);
+               host.spawnflags |= MONSTERFLAG_FLY_VERTICAL;
+               host.monster_moveflags = MONSTER_MOVE_NOMOVE;
+       }
+       
+       RemoveGrapplingHooks(slave);
+       
+       Send_Notification(NOTIF_ONE, slave, MSG_CENTER, CENTER_PIGGYBACK_RIDING, (IS_MONSTER(host)) ? host.monster_name : host.netname);
+       if(IS_PLAYER(host))
+               Send_Notification(NOTIF_ONE, host, MSG_CENTER, CENTER_PIGGYBACK_CARRYING, slave.netname);
+}
+
+void pb_Detach(entity host)
+{
+       entity slave = host.piggybacker;
+       
+       if(!slave)
+               return;
+       
+       slave.aiment = NULL;
+       slave.pbhost = NULL;
+
+       if(IS_MONSTER(host))
+       {
+               host.monster_moveto = '0 0 0';
+               host.monster_moveflags = host.pb_oldmoveflags;
+               if(!host.pb_oldverticalfly) { host.spawnflags &= ~MONSTERFLAG_FLY_VERTICAL; }
+       }
+       
+       if(IS_PLAYER(slave))
+       {
+               // this doesn't happen when we're fixing a broken reference
+               
+               if(slave.move_movetype == MOVETYPE_FOLLOW) // don't reset if player was killed
+                       set_movetype(slave, MOVETYPE_WALK);
+               slave.velocity = '0 0 0';
+               slave.solid = slave.pb_oldsolid;
+               
+               tracebox(host.origin, slave.mins, slave.maxs, slave.origin, MOVE_NOMONSTERS, slave);
+               
+               if(trace_fraction < 1)
+                       setorigin(slave, trace_endpos); // fix player glitching out of the NULL
+               slave.oldorigin = slave.origin; // no fall damage!
+               
+               Kill_Notification(NOTIF_ONE, slave, MSG_CENTER, CPID_PIGGYBACK);
+               if(IS_PLAYER(host))
+                       Kill_Notification(NOTIF_ONE, host, MSG_CENTER, CPID_PIGGYBACK);
+       }
+       
+       host.piggybacker = NULL;
+}
+
+void pb_PBEntThink(entity this)
+{
+       setorigin(this, this.owner.origin + '0 0 0.82' * this.owner.maxs_z);
+       this.nextthink = time;
+}
+
+void pb_FixPBEnt(entity p)
+{
+       entity e = new(pb_ent);
+       e.owner = p;
+       setthink(e, pb_PBEntThink);
+       e.nextthink = time;
+       p.pbent = e;
+}
+
+bool pb_GiveItem(entity to, entity item)
+{
+       if (Item_IsExpiring(item))
+       {
+               item.strength_finished = max(0, item.strength_finished - time);
+               item.invincible_finished = max(0, item.invincible_finished - time);
+               item.superweapons_finished = max(0, item.superweapons_finished - time);
+       }
+
+       entity it = item.itemdef;
+       bool gave = (it && it.instanceOfPickup) ? ITEM_HANDLE(Pickup, it, item, to) : Item_GiveTo(item, to);
+       if (!gave)
+       {
+               if (Item_IsExpiring(item))
+               {
+                       // undo what we did above
+                       item.strength_finished += time;
+                       item.invincible_finished += time;
+                       item.superweapons_finished += time;
+               }
+               if(!to.pbhost)
+                       to = to.piggybacker; // increase it later
+               return false;
+       }
+
+       if (Item_IsExpiring(item))
+       {
+               // undo it anyway
+               item.strength_finished += time;
+               item.invincible_finished += time;
+               item.superweapons_finished += time;
+       }
+
+       STAT(LAST_PICKUP, to) = time;
+
+       _sound (to, CH_TRIGGER, item.item_pickupsound, VOL_BASE, ATTEN_NORM);
+       return true;
+}
+
+MUTATOR_HOOKFUNCTION(piggyback, MatchEnd)
+{
+       FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(pb_Detach(it)));
+       IL_EACH(g_monsters, true,
+       {
+               pb_Detach(it);
+       });
+       return false;
+}
+
+MUTATOR_HOOKFUNCTION(piggyback, PlayerUseKey)
+{
+       if(MUTATOR_RETURNVALUE || game_stopped) { return; }
+
+       entity player = M_ARGV(0, entity);
+       
+       if(player.pbhost)
+       {
+               pb_Detach(player.pbhost);
+               return true;
+       }
+       if(player.piggybacker)
+       {
+               pb_Detach(player);
+               return true;
+       }
+
+#define LIMBO(v) (IS_DEAD(v) || STAT(FROZEN, v))
+
+       if(!CS_CVAR(player).cvar_cl_noride)
+       if(!player.vehicle && !IS_DEAD(player) && !STAT(FROZEN, player))
+       {
+               entity head, closest_target = NULL;
+
+               head = WarpZone_FindRadius(player.origin, autocvar_g_vehicles_enter_radius, true);
+               while(head)
+               {
+                       if(IS_PLAYER(head) || (IS_MONSTER(head) && (head.monsterdef.spawnflags & MON_FLAG_RIDE)))
+                       if(SAME_TEAM(head, player) || autocvar_g_piggyback_ride_enemies || (IS_MONSTER(head) && head.realowner == player))
+                       if(head != player)
+                       if(!CS_CVAR(head).cvar_cl_nocarry)
+                       if(!IS_DEAD(head)) // we check for trapped status here, but better to be safe than sorry, considering the madness happening
+                       if(!head.vehicle)
+                       if((!LIMBO(head) && !LIMBO(player)) || (LIMBO(head) || LIMBO(player))) // your guess is as good as mine
+                       {
+                               if(closest_target)
+                               {
+                                       if(vlen2(player.origin - head.origin) < vlen2(player.origin - closest_target.origin))
+                                       { closest_target = head; }
+                               }
+                               else { closest_target = head; }
+                       }
+                       
+                       head = head.chain;
+               }
+
+               if(closest_target)
+               if(IS_BOT_CLIENT(closest_target)) { pb_Attach(player, closest_target); return true; }
+               else { pb_Attach(closest_target, player); return true; }
+       }
+
+#undef LIMBO
+}
+
+MUTATOR_HOOKFUNCTION(piggyback, PlayerPreThink)
+{
+       entity player = M_ARGV(0, entity);
+
+       if(BROKEN_PBREF(player)) { pb_Detach(player); }
+       
+       if(!player.pbent)
+               pb_FixPBEnt(player);
+
+       if(IS_MONSTER(player.pbhost))
+       if(CS(player).movement || PHYS_INPUT_BUTTON_JUMP(player) || PHYS_INPUT_BUTTON_CROUCH(player))
+       //if(!player.pbhost.enemy) // if player is trying to move, don't override
+       {
+               float forw, rit, updown = 0;
+               vector wishvel = '0 0 0';
+
+               vector forward, right, up;
+               MAKE_VECTORS(player.angles, forward, right, up);
+               if(PHYS_INPUT_BUTTON_JUMP(player))
+                       updown = 300;
+               else if(PHYS_INPUT_BUTTON_CROUCH(player))
+                       updown = -300;
+
+               if(CS(player).movement)
+               {
+                       forw = CS(player).movement_x * 500;
+                       rit = CS(player).movement_y * 500;
+                       //updown = CS(player).movement_z * 100;
+                       
+                       wishvel = forward * forw + right * rit;
+               }
+               
+               //vector wishvel = normalize(('10 0 0' + v_forward * CS(player).movement_x) + ('0 10 0' + v_right * CS(player).movement_y) + ('0 0 1' * CS(player).movement_z));
+               //print(vtos(player.origin), vtos(player.origin + wishvel), "\n");
+               player.pbhost.monster_moveto = player.origin + wishvel;
+               player.pbhost.monster_moveto_z = up.z * updown;
+       }
+}
+
+MUTATOR_HOOKFUNCTION(piggyback, MonsterMove)
+{
+       entity mon = M_ARGV(0, entity);
+
+       if(BROKEN_PBREF(mon)) { pb_Detach(mon); }
+       
+       if(!mon.pbent)
+               pb_FixPBEnt(mon);
+
+       if(mon.piggybacker)
+       {
+               mon.flags |= FL_PICKUPITEMS;
+               mon.last_trace = time;
+       }
+       else
+               mon.flags &= ~FL_PICKUPITEMS;
+}
+
+MUTATOR_HOOKFUNCTION(piggyback, reset_map_players)
+{
+       FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(pb_Detach(it)));
+}
+
+void pb_RemovePlayer(entity this)
+{
+       if(this.piggybacker)
+       {
+               this.piggybacker.pb_canattach = 1;
+               pb_Detach(this);
+       }
+       
+       if(this.pbhost)
+               pb_Detach(this.pbhost);
+       this.pb_canattach = 0;
+
+       if(this.pbent)
+       {
+               delete(this.pbent);
+               this.pbent = NULL;
+       }
+}
+
+MUTATOR_HOOKFUNCTION(piggyback, MakePlayerObserver)
+{
+       entity player = M_ARGV(0, entity);
+
+       pb_RemovePlayer(player);
+}
+
+MUTATOR_HOOKFUNCTION(piggyback, ClientDisconnect)
+{
+       entity player = M_ARGV(0, entity);
+
+       pb_RemovePlayer(player);
+}
+
+void pb_PlayerDies(entity this)
+{
+       if(!this.pbhost && this.pb_canattach)
+               this.pb_canattach = 0;
+               
+       if(this.piggybacker)
+       {
+               this.piggybacker.pb_canattach = 1;
+               pb_Detach(this);
+       }
+       
+       if(this.pbhost)
+               pb_Detach(this.pbhost);
+       this.pb_canattach = 0;
+}
+
+MUTATOR_HOOKFUNCTION(piggyback, MonsterDies)
+{
+       entity frag_target = M_ARGV(0, entity);
+
+       pb_PlayerDies(frag_target);
+}
+
+MUTATOR_HOOKFUNCTION(piggyback, PlayerDies)
+{
+       entity frag_target = M_ARGV(2, entity);
+
+       pb_PlayerDies(frag_target);
+}
+
+MUTATOR_HOOKFUNCTION(piggyback, FixPlayermodel)
+{
+       entity player = M_ARGV(2, entity);
+
+       if(!player.pbhost) { return false; }
+
+       // this was the nearest hook to the original code
+       bool do_crouch = PHYS_INPUT_BUTTON_CROUCH(player);
+       if(player.hook_state
+       || player.vehicle
+       || STAT(FROZEN, player)
+       ) { do_crouch = false; }
+
+       if(!do_crouch && IS_DUCKED(player))
+       {
+               tracebox(player.origin, STAT(PL_MIN, player), STAT(PL_MAX, player), player.origin, false, player);
+               if (!trace_startsolid || player.pbhost)
+               {
+                       UNSET_DUCKED(player);
+                       player.view_ofs = STAT(PL_VIEW_OFS, player);
+                       setsize (player, STAT(PL_MIN, player), STAT(PL_MAX, player));
+               }
+       }
+}
+
+MUTATOR_HOOKFUNCTION(piggyback, GrappleHookThink)
+{
+       entity hook_pullentity = M_ARGV(2, entity);
+
+       if(hook_pullentity.pbhost)
+       {
+               hook_pullentity = pb_RootOf(hook_pullentity);
+               M_ARGV(1, int) = 2; // enforce tarzan
+       }
+
+       M_ARGV(2, entity) = hook_pullentity;
+}
+
+MUTATOR_HOOKFUNCTION(piggyback, BuffModel_Customize)
+{
+       entity buff = M_ARGV(0, entity);
+       entity player = M_ARGV(1, entity);
+
+       if(player.pbhost == buff.owner || buff.owner.piggybacker)
+               return true; // don't show to piggybacker's carrier, and don't show if carrier is carrying someone else
+}
+
+
+MUTATOR_HOOKFUNCTION(piggyback, ItemTouch)
+{
+       entity item = M_ARGV(0, entity);
+       entity toucher = M_ARGV(1, entity);
+
+       if(IS_MONSTER(toucher))
+       {
+               entity p = toucher;
+               while(p.piggybacker)
+               {
+                       p = p.piggybacker;
+                       if(IS_PLAYER(p))
+                       {
+                               toucher = p;
+                               break;
+                       }
+               }
+
+               if(IS_MONSTER(toucher))
+                       return MUT_ITEMTOUCH_RETURN; // make sure we're not allowing something nasty here
+       }
+
+       // give items to all piggybackers
+       // if we get here, the above stuff is out of the way
+
+       // we use pb_RootOf here, as magnet buff can give riders items, and it wouldn't be very nice if they hogged them
+       entity p = pb_RootOf(toucher);
+       if(IS_PLAYER(p))
+               toucher = p;
+       while(p.piggybacker)
+       {
+               p = p.piggybacker;
+               if(IS_PLAYER(p))
+               {
+                       if(!pb_GiveItem(p, item))
+                               continue;
+               }
+       }
+
+       M_ARGV(1, entity) = toucher;
+
+       return MUT_ITEMTOUCH_CONTINUE;
+}
+
+MUTATOR_HOOKFUNCTION(piggyback, BuffTouch)
+{
+       entity buff = M_ARGV(0, entity);
+       entity toucher = M_ARGV(1, entity);
+
+       if(IS_MONSTER(toucher))
+       if(toucher.piggybacker)
+               toucher = toucher.piggybacker;
+
+       if(STAT(BUFFS, toucher))
+       if(STAT(BUFFS, toucher) == STAT(BUFFS, buff) || !CS_CVAR(toucher).cvar_cl_buffs_autoreplace)
+       {
+               entity p = toucher;
+               while(p.piggybacker)
+               {
+                       p = p.piggybacker;
+                       if(!STAT(BUFFS, p) || (!p.piggybacker && (CS_CVAR(p).cvar_cl_buffs_autoreplace || STAT(BUFFS, p) != STAT(BUFFS, buff))))
+                       {
+                               toucher = p;
+                               break;
+                       }
+               }
+       }
+
+       M_ARGV(1, entity) = toucher;
+}
diff --git a/qcsrc/server/mutators/zombie_apocalypse.qc b/qcsrc/server/mutators/zombie_apocalypse.qc
new file mode 100644 (file)
index 0000000..059b86f
--- /dev/null
@@ -0,0 +1,51 @@
+//FEATURE: Zombie Apocalypse mutator, which spawns random monsters around the map constantly
+
+float za_spawn_delay;
+AUTOCVAR(g_za, bool, false, "Enable zombie apocalypse mutator");
+
+REGISTER_MUTATOR(za, autocvar_g_za && autocvar_g_monsters && !g_nexball && !g_invasion && !g_cts && !g_race)
+{
+       MUTATOR_ONADD
+       {
+               za_spawn_delay = time + game_starttime;
+       }
+}
+
+AUTOCVAR(g_za_max_monsters, int, 20, "");
+AUTOCVAR(g_za_spawnmonster, string, "zombie", "Spawn this type of monster, can be random or any of the mobs in 'spawnmob list'");
+AUTOCVAR(g_za_spawn_delay, float, 5, "");
+
+void za_SpawnMonster()
+{
+       if(game_stopped) { return; }
+
+       entity e = spawn();
+
+       if(MoveToRandomMapLocation(e, DPCONTENTS_SOLID | DPCONTENTS_CORPSE | DPCONTENTS_PLAYERCLIP, DPCONTENTS_SLIME | DPCONTENTS_LAVA | DPCONTENTS_SKY | DPCONTENTS_BODY | DPCONTENTS_DONOTENTER, Q3SURFACEFLAG_SKY, 10, 1024, 256))
+       {
+               e.angles = '0 0 0';
+               spawnmonster(e, autocvar_g_za_spawnmonster, MON_Null, NULL, NULL, e.origin, false, false, 2);
+       }
+}
+
+MUTATOR_HOOKFUNCTION(za, SV_StartFrame)
+{
+       if(time < za_spawn_delay || autocvar_g_za_max_monsters <= 0 || !autocvar_g_za)
+               return;
+
+       int n_monsters = 0, maxmon = autocvar_g_za_max_monsters;
+
+       // count dead monsters too (zombies)
+       IL_EACH(g_monsters, true,
+       {
+               ++n_monsters;
+       });
+
+       while(n_monsters < maxmon)
+       {
+               ++n_monsters;
+               za_SpawnMonster();
+       }
+
+       za_spawn_delay = time + autocvar_g_za_spawn_delay;
+}
diff --git a/qcsrc/server/mute.qc b/qcsrc/server/mute.qc
new file mode 100644 (file)
index 0000000..5c81d6b
--- /dev/null
@@ -0,0 +1,81 @@
+//FEATURE: Command to permanently mute troublesome players
+
+REGISTER_MUTATOR(muteban, true);
+
+AUTOCVAR(sv_muteban_list, string, "", "");
+
+MUTATOR_HOOKFUNCTION(muteban, ClientConnect)
+{
+       entity player = M_ARGV(0, entity);
+
+       if(PlayerInList(player, autocvar_sv_muteban_list))
+               CS(player).muted = true;
+}
+
+MUTATOR_HOOKFUNCTION(muteban, SV_ParseServerCommand)
+{
+       if(MUTATOR_RETURNVALUE) // command was already handled?
+               return false;
+
+       string cmd_name = M_ARGV(0, string);
+       int cmd_argc = M_ARGV(1, int);
+
+       if(cmd_name == "muteban")
+       {
+               entity client = GetIndexedEntity(cmd_argc, 1);
+               bool accepted = VerifyClientEntity(client, true, false);
+
+               if (accepted > 0)
+               {
+                       string theid = "";
+                       if(!PlayerInIPList(client, autocvar_sv_muteban_list))
+                               theid = cons(theid, client.netaddress);
+                       if(!PlayerInIDList(client, autocvar_sv_muteban_list))
+                               theid = cons(theid, client.crypto_idfp);
+                       CS(client).muted = true;
+                       LOG_INFO(strcat("Mute-banning player ", GetCallerName(client), " (", argv(1), ")."));
+                       cvar_set("sv_muteban_list", cons(autocvar_sv_muteban_list, theid));
+               }
+               else
+               {
+                       LOG_INFO("mute-ban failed: ", GetClientErrorString(accepted, argv(1)), ".");
+               }
+
+               return true;
+       }
+
+       if(cmd_name == "unmuteban")
+       {
+               entity client = GetIndexedEntity(cmd_argc, 1);
+               bool accepted = VerifyClientEntity(client, true, false);
+               string original_arg = argv(1);
+
+               if (accepted > 0)
+               {
+                       string tmp_string = "";
+                       FOREACH_WORD(autocvar_sv_muteban_list, it != client.netaddress,
+                       {
+                               if(client.crypto_idfp && it == substring(client.crypto_idfp, 0, strlen(it)))
+                                       continue;
+                               tmp_string = cons(tmp_string, it);
+                       });
+
+                       cvar_set("sv_muteban_list", tmp_string);
+                       LOG_INFO(strcat("Unmuting player ", GetCallerName(client), " (", original_arg, ")."));
+                       CS(client).muted = false;
+               }
+               else
+               {
+                       LOG_INFO("mute-ban failed: ", GetClientErrorString(accepted, original_arg), ", attempting to unban by string.");
+
+                       string tmp_string = "";
+                       FOREACH_WORD(autocvar_sv_muteban_list, it != original_arg,
+                       {
+                               tmp_string = cons(tmp_string, it);
+                       });
+
+                       cvar_set("sv_muteban_list", tmp_string);
+               }
+               return true;
+       }
+}