]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Merge remote-tracking branch 'origin/Juhu/strafehud-server' into morosophos/server...
authorNick S <nick@teichisma.info>
Thu, 30 Mar 2023 18:45:43 +0000 (21:45 +0300)
committerNick S <nick@teichisma.info>
Thu, 30 Mar 2023 18:45:43 +0000 (21:45 +0300)
34 files changed:
bal-wep-mario.cfg
bal-wep-nexuiz25.cfg
bal-wep-samual.cfg
bal-wep-xdf.cfg
bal-wep-xonotic.cfg
qcsrc/client/hud/panel/scoreboard.qc
qcsrc/common/command/generic.qc
qcsrc/common/constants.qh
qcsrc/common/gamemodes/gamemode/ctf/sv_ctf.qc
qcsrc/common/gamemodes/gamemode/cts/sv_cts.qc
qcsrc/common/mapobjects/target/_mod.inc
qcsrc/common/mapobjects/target/_mod.qh
qcsrc/common/mapobjects/target/speed.qc [new file with mode: 0644]
qcsrc/common/mapobjects/target/speed.qh [new file with mode: 0644]
qcsrc/common/mapobjects/teleporters.qc
qcsrc/common/mapobjects/trigger/jumppads.qc
qcsrc/common/mapobjects/trigger/jumppads.qh
qcsrc/common/mapobjects/trigger/teleport.qc
qcsrc/common/mapobjects/trigger/teleport.qh
qcsrc/common/scores.qh
qcsrc/common/stats.qh
qcsrc/common/util.qc
qcsrc/common/weapons/weapon/crylink.qc
qcsrc/common/weapons/weapon/crylink.qh
qcsrc/server/_mod.inc
qcsrc/server/_mod.qh
qcsrc/server/command/sv_cmd.qc
qcsrc/server/compat/quake3.qc
qcsrc/server/main.qc
qcsrc/server/race.qc
qcsrc/server/race.qh
qcsrc/server/strafe.qc [new file with mode: 0644]
qcsrc/server/strafe.qh [new file with mode: 0644]
xonotic-server.cfg

index adb7cc654fa3565e82441a1ca9115936191ebfc1..526fd0e544a72ea742526216b454c5f49e55ba4d 100644 (file)
@@ -287,6 +287,7 @@ set g_balance_crylink_secondary_shots 5
 set g_balance_crylink_secondary_speed 4000
 set g_balance_crylink_secondary_spread 0.08
 set g_balance_crylink_secondary_spreadtype 0
+set g_balance_crylink_swap_attacks 0
 set g_balance_crylink_switchdelay_drop 0.2
 set g_balance_crylink_switchdelay_raise 0.2
 set g_balance_crylink_weaponreplace ""
index 17f5b31cab3b29b313e70f27606e6cf42bd7e082..e91d40b42e531892bbea48070e810c5f48dd98a9 100644 (file)
@@ -287,6 +287,7 @@ set g_balance_crylink_secondary_shots 7
 set g_balance_crylink_secondary_speed 7000
 set g_balance_crylink_secondary_spread 0.08
 set g_balance_crylink_secondary_spreadtype 0
+set g_balance_crylink_swap_attacks 0
 set g_balance_crylink_switchdelay_drop 0.15
 set g_balance_crylink_switchdelay_raise 0.15
 set g_balance_crylink_weaponreplace ""
index 2799a2496b47fb4debea904aa6b051b95c59a621..e7e94f1e6ab3392e357e7469faa32c5d47d918a3 100644 (file)
@@ -287,6 +287,7 @@ set g_balance_crylink_secondary_shots 5
 set g_balance_crylink_secondary_speed 3000
 set g_balance_crylink_secondary_spread 0.01
 set g_balance_crylink_secondary_spreadtype 1
+set g_balance_crylink_swap_attacks 0
 set g_balance_crylink_switchdelay_drop 0.2
 set g_balance_crylink_switchdelay_raise 0.2
 set g_balance_crylink_weaponreplace ""
index 09466acce81bcb3949c256133dce2a9675c89acc..ac85665d49fd9b3179c662d5ed2ed81fa4fd93dc 100644 (file)
@@ -287,6 +287,7 @@ set g_balance_crylink_secondary_shots 1
 set g_balance_crylink_secondary_speed 2000
 set g_balance_crylink_secondary_spread 0
 set g_balance_crylink_secondary_spreadtype 1
+set g_balance_crylink_swap_attacks 0
 set g_balance_crylink_switchdelay_drop 0
 set g_balance_crylink_switchdelay_raise 0
 set g_balance_crylink_weaponreplace ""
index 319d77923035e150b23bf90fa1ca4321d5564e55..6b4c25b97ac310e3757bbab11b67c93047489940 100644 (file)
@@ -287,6 +287,7 @@ set g_balance_crylink_secondary_shots 5
 set g_balance_crylink_secondary_speed 3000
 set g_balance_crylink_secondary_spread 0.01
 set g_balance_crylink_secondary_spreadtype 1
+set g_balance_crylink_swap_attacks 0
 set g_balance_crylink_switchdelay_drop 0.2
 set g_balance_crylink_switchdelay_raise 0.2
 set g_balance_crylink_weaponreplace ""
index 484968a9c39a20ebf3b380c1d90e98d6b87dbb29..28ca9bd31dfab23ad9a6de10c4936c96899bc432 100644 (file)
@@ -154,6 +154,10 @@ string Label_getInfo(string label, int mode)
                case "revivals":     if (!mode) return CTX(_("SCO^revivals"));     else LOG_HELP(strcat("^3", "revivals", "           ^7", _("Number of revivals")));
                case "rounds":       if (!mode) return CTX(_("SCO^rounds won"));   else LOG_HELP(strcat("^3", "rounds", "             ^7", _("Number of rounds won")));
                case "score":        if (!mode) return CTX(_("SCO^score"));        else LOG_HELP(strcat("^3", "score", "              ^7", _("Total score")));
+               case "avgspeed":     if (!mode) return CTX(_("SCO^average speed"));else LOG_HELP(strcat("^3", "avgspeed", "           ^7", _("Average speed (CTS)")));
+               case "topspeed":     if (!mode) return CTX(_("SCO^top speed"));    else LOG_HELP(strcat("^3", "topspeed", "           ^7", _("Top speed (CTS)")));
+               case "startspeed":   if (!mode) return CTX(_("SCO^start speed"));  else LOG_HELP(strcat("^3", "startspeed", "         ^7", _("Start speed (CTS)")));
+               case "strafe":       if (!mode) return CTX(_("SCO^strafe"));       else LOG_HELP(strcat("^3", "strafe", "             ^7", _("Strafe efficiency (CTS)")));
                case "suicides":     if (!mode) return CTX(_("SCO^suicides"));     else LOG_HELP(strcat("^3", "suicides", "           ^7", _("Number of suicides")));
                case "sum":          if (!mode) return CTX(_("SCO^sum"));          else LOG_HELP(strcat("^3", "sum", "                ^7", _("Number of kills minus deaths")));
                case "takes":        if (!mode) return CTX(_("SCO^takes"));        else LOG_HELP(strcat("^3", "takes", "              ^7", _("Number of domination points taken (Domination)")));
@@ -728,7 +732,7 @@ void Cmd_Scoreboard_Help()
 " +ctf/pickups +ctf/fckills +ctf/returns +ctf/caps +ons/takes +ons/caps" \
 " +lms/lives +lms/rank" \
 " +kh/kckills +kh/losses +kh/caps" \
-" ?+rc/laps ?+rc/time +rc,cts/fastest" \
+" ?+rc/laps ?+rc/time ?+cts/strafe ?+cts/startspeed ?+cts/avgspeed ?+cts/topspeed +rc,cts/fastest" \
 " +as/objectives +nb/faults +nb/goals" \
 " +ka/pickups +ka/bckills +ka/bctime +ft/revivals" \
 " +dom/ticks +dom/takes" \
@@ -1070,6 +1074,23 @@ string Scoreboard_GetField(entity pl, PlayerScoreField field)
                case SP_DMG: case SP_DMGTAKEN:
                        return sprintf("%.1f k", pl.(scores(field)) / 1000);
 
+               case SP_CTS_STRAFE:
+               {
+                       float strafe_efficiency = pl.(scores(field)) / 1000;
+                       if(strafe_efficiency < -1) return "";
+                       sbt_field_rgb = '1 1 1' - (strafe_efficiency > 0 ? '1 0 1' : '0 1 1') * fabs(strafe_efficiency);
+                       return sprintf("%.1f%%", strafe_efficiency * 100);
+               }
+
+               case SP_CTS_STARTSPEED:
+               case SP_CTS_AVGSPEED:
+               case SP_CTS_TOPSPEED:
+               {
+                       float speed = pl.(scores(field)) * GetSpeedUnitFactor(autocvar_hud_panel_physics_speed_unit);
+                       if(speed < 0) return "";
+                       return sprintf("%d%s", speed, GetSpeedUnit(autocvar_hud_panel_physics_speed_unit));
+               }
+
                default: case SP_SCORE:
                        tmp = pl.(scores(field));
                        f = scores_flags(field);
index 929d3765345084e1e85917705e498417e46563c0..ec6894a6a9de23c50a3eecad790facb1e45fcdcb 100644 (file)
@@ -270,6 +270,12 @@ void GenericCommand_maplist(int request, int argc)
                                        return;
                                }
 
+                        case "rebuild": // rebuilds maplist to include available maps, useful after doing fs_rescan
+                          {
+                            cvar_set("g_maplist", MapInfo_ListAllowedMaps(MapInfo_CurrentGametype(), MapInfo_RequiredFlags(), MapInfo_ForbiddenFlags()));
+                            return;
+                          }
+
                                case "remove": // scans maplist and only adds back whatever maps were not provided in argv(2)
                                {
                                        if(argc == 3)
index e0c17a7a128a0e623be23e7048a6afd4bec0e108..50d7b0ba57c9d5058f823ccae2defabd14ef1237 100644 (file)
@@ -28,7 +28,7 @@ const int SPECIES_ROBOT_SHINY = 5;
 const int SPECIES_RESERVED = 15;
 
 #ifdef GAMEQC
-const int RANKINGS_CNT = 99;
+const int RANKINGS_CNT = 256;
 
 ///////////////////////////
 // keys pressed
index f8933bcb06afd22034aa155a1461af68fff2aa44..bd3cf8a03b5b9083c64d1053d726552c2a2a975c 100644 (file)
@@ -131,7 +131,7 @@ void ctf_CaptureRecord(entity flag, entity player)
                ctf_captimerecord = cap_time;
                db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
                db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
-               write_recordmarker(player, flag.ctf_pickuptime, cap_time);
+               write_recordmarker(player, 1, flag.ctf_pickuptime, cap_time);
        }
 
        if(autocvar_g_ctf_leaderboard && !ctf_oneflag)
index 0fbab7ae51f54159e1913b7fe723bd4795b77ed4..defeb686345f41da4ff1825d169d64beb61a0f1d 100644 (file)
@@ -57,6 +57,10 @@ void cts_ScoreRules()
     GameRules_score_enabled(false);
     GameRules_scoring(0, 0, 0, {
         if (g_race_qualifying) {
+            field(SP_CTS_STRAFE, "strafe", 0);
+            field(SP_CTS_STARTSPEED, "startspeed", 0);
+            field(SP_CTS_AVGSPEED, "avgspeed", 0);
+            field(SP_CTS_TOPSPEED, "topspeed", 0);
             field(SP_RACE_FASTEST, "fastest", SFL_SORT_PRIO_PRIMARY | SFL_LOWER_IS_BETTER | SFL_TIME);
         } else {
             field(SP_RACE_LAPS, "laps", SFL_SORT_PRIO_PRIMARY);
@@ -141,6 +145,21 @@ MUTATOR_HOOKFUNCTION(cts, PlayerPhysics)
                                CS(player).movement_y = -M_SQRT1_2 * wishspeed;
                }
        }
+       player.strafe_efficiency_sum += calculate_strafe_efficiency(player, CS(player).movement, dt) * dt;
+       if(player.race_started)
+       {
+               float current_speed = vlen(vec2(player.velocity));
+               if(player.race_topspeed < current_speed)
+               {
+                       player.race_topspeed = current_speed;
+               }
+               player.race_avgspeed_sum += current_speed * dt;
+               player.race_avgspeed_time += dt;
+       }
+       else
+       {
+               player.race_startspeed = player.race_avgspeed_sum = player.race_avgspeed_time = player.race_topspeed = 0;
+       }
 }
 
 MUTATOR_HOOKFUNCTION(cts, reset_map_global)
@@ -153,6 +172,15 @@ MUTATOR_HOOKFUNCTION(cts, reset_map_global)
        PlayerScore_Sort(race_place, 0, true, false);
 
        FOREACH_CLIENT(true, {
+               it.strafe_efficiency_best = -2;
+               it.strafe_efficiency_sum = it.strafe_efficiency_time = 0;
+               PlayerScore_Set(it, SP_CTS_STRAFE, -20000);
+               it.race_startspeed_best = it.race_avgspeed_best = it.race_topspeed_best = -1;
+               it.race_startspeed = it.race_avgspeed_sum = it.race_avgspeed_time = it.race_topspeed = 0;
+               PlayerScore_Set(it, SP_CTS_STARTSPEED, -1);
+               PlayerScore_Set(it, SP_CTS_AVGSPEED, -1);
+               PlayerScore_Set(it, SP_CTS_TOPSPEED, -1);
+
                if(it.race_place)
                {
                        s = GameRules_scoring_add(it, RACE_FASTEST, 0);
@@ -179,6 +207,14 @@ MUTATOR_HOOKFUNCTION(cts, ClientConnect)
 
        race_PreparePlayer(player);
        player.race_checkpoint = -1;
+       player.strafe_efficiency_best = -2;
+       player.strafe_efficiency_sum = player.strafe_efficiency_time = 0;
+       PlayerScore_Set(player, SP_CTS_STRAFE, -20000);
+       player.race_startspeed_best = player.race_avgspeed_best = player.race_topspeed_best = -1;
+       player.race_startspeed = player.race_avgspeed_sum = player.race_avgspeed_time = player.race_topspeed = 0;
+       PlayerScore_Set(player, SP_CTS_STARTSPEED, -1);
+       PlayerScore_Set(player, SP_CTS_AVGSPEED, -1);
+       PlayerScore_Set(player, SP_CTS_TOPSPEED, -1);
 
        race_SendAll(player, false);
 }
@@ -248,6 +284,9 @@ MUTATOR_HOOKFUNCTION(cts, PlayerDies)
        frag_target.respawn_flags |= RESPAWN_FORCE;
        race_AbandonRaceCheck(frag_target);
 
+       frag_target.strafe_efficiency_sum = frag_target.strafe_efficiency_time = 0;
+       frag_target.race_startspeed = frag_target.race_avgspeed_sum = frag_target.race_avgspeed_time = frag_target.race_topspeed = 0;
+
        if(autocvar_g_cts_removeprojectiles)
        {
                IL_EACH(g_projectiles, it.owner == frag_target && (it.flags & FL_PROJECTILE),
index f9cc536eda728e8e402121af6c5fe33a702cc179..9d97d66101451fa042925a756b13015da9fede91 100644 (file)
@@ -7,4 +7,5 @@
 #include <common/mapobjects/target/spawn.qc>
 #include <common/mapobjects/target/spawnpoint.qc>
 #include <common/mapobjects/target/speaker.qc>
+#include <common/mapobjects/target/speed.qc>
 #include <common/mapobjects/target/voicescript.qc>
index e3f4cede9912314c03bbd2cef0d9a5cc494e20b4..48c57c42d9f12b0090ece1df715229177569591f 100644 (file)
@@ -7,4 +7,5 @@
 #include <common/mapobjects/target/spawn.qh>
 #include <common/mapobjects/target/spawnpoint.qh>
 #include <common/mapobjects/target/speaker.qh>
+#include <common/mapobjects/target/speed.qh>
 #include <common/mapobjects/target/voicescript.qh>
diff --git a/qcsrc/common/mapobjects/target/speed.qc b/qcsrc/common/mapobjects/target/speed.qc
new file mode 100644 (file)
index 0000000..fb7da58
--- /dev/null
@@ -0,0 +1,209 @@
+#include "speed.qh"
+
+#define XYZ_ARRAY(name) float name[3]
+#define ARRAY_AS_VECTOR(a) ((a)[0] * '1 0 0' + (a)[1] * '0 1 0' + (a)[2] * '0 0 1')
+#define VECTOR_TO_ARRAY(a, e) { vector v = (e); (a)[0] = v.x; (a)[1] = v.y; (a)[2] = v.z; }
+#define FOR_XYZ(idx) for(int idx = 0; idx < 3; ++idx)
+vector target_speed_calculatevelocity(entity this, float speed, entity pushed_entity)
+{
+       bool is_percentage = boolean(this.spawnflags & SPEED_PERCENTAGE);
+       bool is_add = boolean(this.spawnflags & SPEED_ADD);
+       bool is_launcher = boolean(this.spawnflags & SPEED_LAUNCHER);
+
+       bool is_positive[3];
+       is_positive[0] = boolean(this.spawnflags & SPEED_POSITIVE_X);
+       is_positive[1] = boolean(this.spawnflags & SPEED_POSITIVE_Y);
+       is_positive[2] = boolean(this.spawnflags & SPEED_POSITIVE_Z);
+
+       bool is_negative[3];
+       is_negative[0] = boolean(this.spawnflags & SPEED_NEGATIVE_X);
+       is_negative[1] = boolean(this.spawnflags & SPEED_NEGATIVE_Y);
+       is_negative[2] = boolean(this.spawnflags & SPEED_NEGATIVE_Z);
+
+       // speed cannot be negative except when subtracting
+       if(!is_add)
+       {
+               speed = max(speed, 0);
+       }
+
+       XYZ_ARRAY(pushvel);
+       VECTOR_TO_ARRAY(pushvel, pushed_entity.velocity);
+
+       FOR_XYZ(i)
+       {
+               // launcher can only be either positive or negative not both
+               if(is_launcher && is_positive[i] && is_negative[i])
+               {
+                       is_positive[i] = is_negative[i] = false;
+               }
+
+               // ignore this direction
+               if(!is_positive[i] && !is_negative[i])
+               {
+                       pushvel[i] = 0;
+               }
+       }
+
+       float oldspeed = vlen(ARRAY_AS_VECTOR(pushvel));
+
+       // the speed field is used to specify the percentage of the current speed
+       if(is_percentage)
+       {
+               speed = oldspeed * speed / 100;
+       }
+
+       float launcherspeed = 0;
+
+       // do this properly when not playing a Q3 map, do not put this in the loop
+       if(!STAT(Q3COMPAT, pushed_entity))
+       {
+               launcherspeed += speed;
+
+               // add the add speed in the same variable
+               // as it goes in the same direction
+               if(is_add) launcherspeed += oldspeed;
+       }
+
+       FOR_XYZ(i)
+       {
+               if(((pushvel[i] != 0) || is_launcher) && (is_positive[i] != is_negative[i]))
+               {
+                       if(is_launcher)
+                       {
+                               // every direction weighs the same amount on launchers
+                               // movedir does not matter
+                               pushvel[i] = 1;
+
+                               // this does not belong inside the loop
+                               // only simulate this bug when playing a Q3 map
+                               if(STAT(Q3COMPAT, pushed_entity))
+                               {
+                                       launcherspeed += speed;
+
+                                       // add the add speed in the same variable
+                                       // as it goes in the same direction
+                                       if(is_add) launcherspeed += oldspeed;
+                               }
+                       }
+
+                       if(is_positive[i])
+                       {
+                               pushvel[i] = copysign(pushvel[i], 1);
+                       }
+                       else if(is_negative[i])
+                       {
+                               pushvel[i] = copysign(pushvel[i], -1);
+                       }
+               }
+       }
+
+       XYZ_ARRAY(oldvel);
+       VECTOR_TO_ARRAY(oldvel, pushed_entity.velocity);
+
+       if(is_launcher)
+       {
+               // launcher will always launch you in the correct direction
+               // even if speed is set to a negative value, fabs() is correct
+               VECTOR_TO_ARRAY(pushvel, normalize(ARRAY_AS_VECTOR(pushvel)) * fabs(launcherspeed));
+       }
+       else
+       {
+               VECTOR_TO_ARRAY(pushvel, normalize(ARRAY_AS_VECTOR(pushvel)) * speed)
+
+               if(is_add)
+               {
+                       VECTOR_TO_ARRAY(pushvel, ARRAY_AS_VECTOR(pushvel) + ARRAY_AS_VECTOR(oldvel));
+               }
+       }
+
+       FOR_XYZ(i)
+       {
+               // preserve unaffected directions
+               if(!is_positive[i] && !is_negative[i])
+               {
+                       pushvel[i] = oldvel[i];
+               }
+       }
+
+       return ARRAY_AS_VECTOR(pushvel);
+}
+#undef XYZ_ARRAY
+#undef ARRAY_AS_VECTOR
+#undef VECTOR_TO_ARRAY
+#undef FOR_XYZ
+
+REGISTER_NET_LINKED(ENT_CLIENT_TARGET_SPEED)
+
+void target_speed_use(entity this, entity actor, entity trigger)
+{
+       if(this.active != ACTIVE_ACTIVE)
+               return;
+
+       actor.velocity = target_speed_calculatevelocity(this, this.speed, actor);
+}
+
+void target_speed_reset(entity this)
+{
+       this.active = ACTIVE_ACTIVE;
+}
+
+#ifdef SVQC
+void target_speed_link(entity this);
+
+/*
+ * ENTITY PARAMETERS:
+ *
+ *   targetname:  Activating trigger points to this.
+ *   speed:       Speed value to set (default: 100).
+ */
+spawnfunc(target_speed)
+{
+       this.active = ACTIVE_ACTIVE;
+       this.setactive = generic_netlinked_setactive;
+       this.use = target_speed_use;
+       this.reset = target_speed_reset;
+
+       // FIXME: zero and unset cannot be disambiguated in xonotic
+       //if (!this.speed)
+       //      this.speed = 100;
+
+       target_speed_link(this);
+}
+
+bool target_speed_send(entity this, entity to, float sf)
+{
+       WriteHeader(MSG_ENTITY, ENT_CLIENT_TARGET_SPEED);
+
+       WriteInt24_t(MSG_ENTITY, this.spawnflags);
+       WriteByte(MSG_ENTITY, this.active);
+       WriteString(MSG_ENTITY, this.targetname);
+       WriteCoord(MSG_ENTITY, this.speed);
+
+       return true;
+}
+
+void target_speed_link(entity this)
+{
+       Net_LinkEntity(this, false, 0, target_speed_send);
+}
+
+#elif defined(CSQC)
+
+void target_speed_remove(entity this)
+{
+       strfree(this.targetname);
+}
+
+NET_HANDLE(ENT_CLIENT_TARGET_SPEED, bool isnew)
+{
+       this.spawnflags = ReadInt24_t();
+       this.active = ReadByte();
+       this.targetname = strzone(ReadString());
+       this.speed = ReadCoord();
+
+       this.use = target_speed_use;
+       this.entremove = target_speed_remove;
+
+       return true;
+}
+#endif
diff --git a/qcsrc/common/mapobjects/target/speed.qh b/qcsrc/common/mapobjects/target/speed.qh
new file mode 100644 (file)
index 0000000..6e85444
--- /dev/null
@@ -0,0 +1,12 @@
+#pragma once
+
+
+#define SPEED_PERCENTAGE BIT(0)
+#define SPEED_ADD        BIT(1)
+#define SPEED_POSITIVE_X BIT(2)
+#define SPEED_NEGATIVE_X BIT(3)
+#define SPEED_POSITIVE_Y BIT(4)
+#define SPEED_NEGATIVE_Y BIT(5)
+#define SPEED_POSITIVE_Z BIT(6)
+#define SPEED_NEGATIVE_Z BIT(7)
+#define SPEED_LAUNCHER   BIT(8)
index c8f9ad245d0ca7c24a7311de92005e62dc9acb16..7a725dfe155f9bf52c3cb2f6c05f592b3de05e6f 100644 (file)
@@ -223,9 +223,11 @@ entity Simple_TeleportPlayer(entity teleporter, entity player)
                if(vdist(player.velocity, >, e.speed))
                        player.velocity = normalize(player.velocity) * max(0, e.speed);
 
-       if(STAT(TELEPORT_MAXSPEED, player))
-               if(vdist(player.velocity, >, STAT(TELEPORT_MAXSPEED, player)))
-                       player.velocity = normalize(player.velocity) * max(0, STAT(TELEPORT_MAXSPEED, player));
+       if(!(teleporter.classname == "trigger_teleport" && teleporter.spawnflags & TELEPORT_KEEP_SPEED) &&
+          !(teleporter.classname == "target_teleporter" && teleporter.spawnflags & TELEPORTER_KEEP_SPEED))
+               if(STAT(TELEPORT_MAXSPEED, player))
+                       if(vdist(player.velocity, >, STAT(TELEPORT_MAXSPEED, player)))
+                               player.velocity = normalize(player.velocity) * max(0, STAT(TELEPORT_MAXSPEED, player));
 
        locout = e.origin + '0 0 1' * (1 - player.mins.z - 24);
 
index 4754213bf3dd6e37efd961219ed9e44cb66c91a2..b18c95ec23335d14021efaf3d194f3dddff2d01a 100644 (file)
@@ -14,6 +14,7 @@ void trigger_push_use(entity this, entity actor, entity trigger)
 #endif
 
 REGISTER_NET_LINKED(ENT_CLIENT_TRIGGER_PUSH)
+REGISTER_NET_LINKED(ENT_CLIENT_TRIGGER_PUSH_VELOCITY)
 REGISTER_NET_LINKED(ENT_CLIENT_TARGET_PUSH)
 
 /*
@@ -136,19 +137,146 @@ vector trigger_push_calculatevelocity(vector org, entity tgt, float ht, entity p
        return sdir * vs + '0 0 1' * vz;
 }
 
-bool jumppad_push(entity this, entity targ)
+vector trigger_push_velocity_calculatevelocity(entity this, vector org, entity tgt, float speed, float count, entity pushed_entity, bool already_pushed)
+{
+       bool is_playerdir_xy = boolean(this.spawnflags & PUSH_VELOCITY_PLAYERDIR_XY);
+       bool is_add_xy = boolean(this.spawnflags & PUSH_VELOCITY_ADD_XY);
+       bool is_playerdir_z = boolean(this.spawnflags & PUSH_VELOCITY_PLAYERDIR_Z);
+       bool is_add_z = boolean(this.spawnflags & PUSH_VELOCITY_ADD_Z);
+       bool is_bidirectional_xy = boolean(this.spawnflags & PUSH_VELOCITY_BIDIRECTIONAL_XY);
+       bool is_bidirectional_z = boolean(this.spawnflags & PUSH_VELOCITY_BIDIRECTIONAL_Z);
+       bool is_clamp_negative_adds = boolean(this.spawnflags & PUSH_VELOCITY_CLAMP_NEGATIVE_ADDS);
+
+       vector sdir = normalize(vec2(pushed_entity.velocity));
+       float zdir = pushed_entity.velocity.z;
+       if(zdir != 0) zdir = copysign(1, zdir);
+
+       vector vs_tgt = '0 0 0';
+       float vz_tgt = 0;
+       if (!is_playerdir_xy || !is_playerdir_z)
+       {
+               vector vel_tgt = trigger_push_calculatevelocity(org, tgt, 0, pushed_entity);
+               vs_tgt = vec2(vel_tgt);
+               vz_tgt = vel_tgt.z;
+
+               // bidirectional jump pads do not play nicely with xonotic's jump pad targets
+               if (is_bidirectional_xy)
+               {
+                       if (normalize(vs_tgt) * sdir < 0)
+                       {
+                               vs_tgt *= -1;
+                       }
+               }
+
+               if (is_bidirectional_z)
+               {
+                       if (signbit(vz_tgt) != signbit(zdir))
+                       {
+                               vz_tgt *= -1;
+                       }
+               }
+       }
+
+       vector vs;
+       if (is_playerdir_xy)
+       {
+               vs = sdir * speed;
+       }
+       else
+       {
+               vs = vs_tgt;
+       }
+
+       float vz;
+       if (is_playerdir_z)
+       {
+               vz = zdir * count;
+       }
+       else
+       {
+               vz = vz_tgt;
+       }
+
+       if (is_add_xy)
+       {
+               vector vs_add = vec2(pushed_entity.velocity);
+               if (already_pushed)
+               {
+                       vs = vs_add;
+               }
+               else
+               {
+                       vs += vs_add;
+
+                       if (is_clamp_negative_adds)
+                       {
+                               if ((normalize(vs) * sdir) < 0)
+                               {
+                                       vs = '0 0 0';
+                               }
+                       }
+               }
+       }
+
+       if (is_add_z)
+       {
+               float vz_add = pushed_entity.velocity.z;
+               if (already_pushed)
+               {
+                       vz = vz_add;
+               }
+               else
+               {
+                       vz += vz_add;
+
+                       if (is_clamp_negative_adds)
+                       {
+                               if (signbit(vz) != signbit(zdir))
+                               {
+                                       vz = 0;
+                               }
+                       }
+               }
+       }
+
+       return vs + '0 0 1' * vz;
+}
+
+bool jumppad_push(entity this, entity targ, bool is_velocity_pad)
 {
        if (!isPushable(targ))
                return false;
 
        vector org = targ.origin;
 
-       if(Q3COMPAT_COMMON || this.spawnflags & PUSH_STATIC)
+       if(STAT(Q3COMPAT, targ) || this.spawnflags & PUSH_STATIC)
+       {
                org = (this.absmin + this.absmax) * 0.5;
+       }
+
+       bool already_pushed = false;
+       if(is_velocity_pad) // remember velocity jump pads
+       {
+               if(this == targ.last_pushed || (targ.last_pushed && !STAT(Q3COMPAT, targ))) // if q3compat is active overwrite last stored jump pad, otherwise ignore
+               {
+                       already_pushed = true;
+               }
+               else
+               {
+                       targ.last_pushed = this; // may be briefly out of sync between client and server if client prediction is toggled
+               }
+       }
 
        if(this.enemy)
        {
-               targ.velocity = trigger_push_calculatevelocity(org, this.enemy, this.height, targ);
+               if(!is_velocity_pad)
+               {
+                       targ.velocity = trigger_push_calculatevelocity(org, this.enemy, this.height, targ);
+               }
+               else
+               {
+                       targ.velocity = trigger_push_velocity_calculatevelocity(this, org, this.enemy, this.speed, this.count, targ, already_pushed);
+               }
        }
        else if(this.target && this.target != "")
        {
@@ -161,14 +289,31 @@ bool jumppad_push(entity this, entity targ)
                        else
                                RandomSelection_AddEnt(e, 1, 1);
                }
-               targ.velocity = trigger_push_calculatevelocity(org, RandomSelection_chosen_ent, this.height, targ);
+               if(!is_velocity_pad)
+               {
+                       targ.velocity = trigger_push_calculatevelocity(org, RandomSelection_chosen_ent, this.height, targ);
+               }
+               else
+               {
+                       targ.velocity = trigger_push_velocity_calculatevelocity(this, org, RandomSelection_chosen_ent, this.speed, this.count, targ, already_pushed);
+               }
        }
        else
        {
-               targ.velocity = this.movedir;
+               if(!is_velocity_pad)
+               {
+                       targ.velocity = this.movedir;
+               }
+               else
+               {
+#ifdef SVQC
+                       objerror (this, "Jumppad with no target");
+#endif
+                       return false;
+               }
        }
 
-       UNSET_ONGROUND(targ);
+       if(!is_velocity_pad) UNSET_ONGROUND(targ);
 
 #ifdef CSQC
        if (targ.flags & FL_PROJECTILE)
@@ -196,7 +341,7 @@ bool jumppad_push(entity this, entity targ)
 
                // prevent sound spam when a player hits the jumppad more than once
                // or when a dead player gets stuck in the jumppad for some reason
-               if(this.pushltime < time && !(IS_DEAD(targ) && targ.velocity == '0 0 0'))
+               if(!already_pushed && this.pushltime < time && !(IS_DEAD(targ) && targ.velocity == '0 0 0'))
                {
                        // flash when activated
                        Send_Effect(EFFECT_JUMPPAD, targ.origin, targ.velocity, 1);
@@ -273,7 +418,7 @@ void trigger_push_touch(entity this, entity toucher)
 
        EXACTTRIGGER_TOUCH(this, toucher);
 
-       noref bool success = jumppad_push(this, toucher);
+       noref bool success = jumppad_push(this, toucher, false);
 
 #ifdef SVQC
        if (success && (this.spawnflags & PUSH_ONCE))
@@ -285,6 +430,19 @@ void trigger_push_touch(entity this, entity toucher)
 #endif
 }
 
+void trigger_push_velocity_touch(entity this, entity toucher)
+{
+       if (this.active == ACTIVE_NOT)
+               return;
+
+       if(this.team && DIFF_TEAM(this, toucher))
+               return;
+
+       EXACTTRIGGER_TOUCH(this, toucher);
+
+       jumppad_push(this, toucher, true);
+}
+
 #ifdef SVQC
 void trigger_push_link(entity this);
 void trigger_push_updatelink(entity this);
@@ -580,6 +738,21 @@ float trigger_push_send(entity this, entity to, float sf)
        return true;
 }
 
+float trigger_push_velocity_send(entity this, entity to, float sf)
+{
+       WriteHeader(MSG_ENTITY, ENT_CLIENT_TRIGGER_PUSH_VELOCITY);
+
+       WriteByte(MSG_ENTITY, this.team);
+       WriteInt24_t(MSG_ENTITY, this.spawnflags);
+       WriteByte(MSG_ENTITY, this.active);
+       WriteCoord(MSG_ENTITY, this.speed);
+       WriteCoord(MSG_ENTITY, this.count);
+
+       trigger_common_write(this, true);
+
+       return true;
+}
+
 void trigger_push_updatelink(entity this)
 {
        this.SendFlags |= SF_TRIGGER_INIT;
@@ -590,6 +763,11 @@ void trigger_push_link(entity this)
        trigger_link(this, trigger_push_send);
 }
 
+void trigger_push_velocity_link(entity this)
+{
+       trigger_link(this, trigger_push_velocity_send);
+}
+
 /*
  * ENTITY PARAMETERS:
  *
@@ -629,6 +807,29 @@ spawnfunc(trigger_push)
        InitializeEntity(this, trigger_push_findtarget, INITPRIO_FINDTARGET);
 }
 
+/*
+ * ENTITY PARAMETERS:
+ *
+ *   target:  this points to the target_position to which the player will jump.
+ *   speed:   XY speed for player-directional velocity pads - either sets or adds to the player's horizontal velocity.
+ *   count:   Z speed for player-directional velocity pads - either sets or adds to the player's vertical velocity.
+ */
+spawnfunc(trigger_push_velocity)
+{
+       trigger_init(this);
+
+       this.active = ACTIVE_ACTIVE;
+       this.use = trigger_push_use;
+       settouch(this, trigger_push_velocity_touch);
+
+       // normal push setup
+       if (!this.noise)
+               this.noise = "misc/jumppad.wav";
+       precache_sound (this.noise);
+
+       trigger_push_velocity_link(this); // link it now
+}
+
 
 bool target_push_send(entity this, entity to, float sf)
 {
@@ -648,7 +849,7 @@ void target_push_use(entity this, entity actor, entity trigger)
        if(trigger.classname == "trigger_push" || trigger == this)
                return; // WTF, why is this a thing
 
-       jumppad_push(this, actor);
+       jumppad_push(this, actor, false);
 }
 
 void target_push_link(entity this)
@@ -710,6 +911,24 @@ NET_HANDLE(ENT_CLIENT_TRIGGER_PUSH, bool isnew)
        return true;
 }
 
+NET_HANDLE(ENT_CLIENT_TRIGGER_PUSH_VELOCITY, bool isnew)
+{
+       int mytm = ReadByte(); if(mytm) { this.team = mytm - 1; }
+       this.spawnflags = ReadInt24_t();
+       this.active = ReadByte();
+       this.speed = ReadCoord();
+       this.count = ReadCoord();
+
+       trigger_common_read(this, true);
+
+       this.entremove = trigger_remove_generic;
+       this.solid = SOLID_TRIGGER;
+       settouch(this, trigger_push_velocity_touch);
+       this.move_time = time;
+
+       return true;
+}
+
 void target_push_remove(entity this)
 {
        // strfree(this.classname);
index c994bc61085b5971ee3c32aa7ea19e9b8d81e56a..0ef42018a965efc6a23589ac0b5c15a065be4171 100644 (file)
@@ -3,7 +3,15 @@
 
 const int PUSH_ONCE = BIT(0); // legacy, deactivate with relay instead
 const int PUSH_SILENT = BIT(1); // not used?
-const int PUSH_STATIC = BIT(12); // xonotic-only, Q3 already behaves like this by default
+#define PUSH_STATIC BIT(12) // xonotic-only, Q3 already behaves like this by default
+
+#define PUSH_VELOCITY_PLAYERDIR_XY        BIT(0)
+#define PUSH_VELOCITY_ADD_XY              BIT(1)
+#define PUSH_VELOCITY_PLAYERDIR_Z         BIT(2)
+#define PUSH_VELOCITY_ADD_Z               BIT(3)
+#define PUSH_VELOCITY_BIDIRECTIONAL_XY    BIT(4)
+#define PUSH_VELOCITY_BIDIRECTIONAL_Z     BIT(5)
+#define PUSH_VELOCITY_CLAMP_NEGATIVE_ADDS BIT(6)
 
 IntrusiveList g_jumppads;
 STATIC_INIT(g_jumppads) { g_jumppads = IL_NEW(); }
@@ -12,6 +20,8 @@ STATIC_INIT(g_jumppads) { g_jumppads = IL_NEW(); }
 .bool istypefrag;
 .float height;
 
+.entity last_pushed;
+
 const int NUM_JUMPPADSUSED = 3;
 .float jumppadcount;
 .entity jumppadsused[NUM_JUMPPADSUSED];
@@ -44,7 +54,7 @@ bool trigger_push_test(entity this, entity item);
 void trigger_push_findtarget(entity this);
 
 /*
- * ENTITY PARAMETERS:
+ * ENTITY PARAMETERS trigger_push:
  *
  *   target:  target of jump
  *   height:  the absolute value is the height of the highest point of the jump
@@ -55,8 +65,17 @@ void trigger_push_findtarget(entity this);
  *            values to target a point on the ceiling.
  *   movedir: if target is not set, this * speed * 10 is the velocity to be reached.
  */
+
+/*
+ * ENTITY PARAMETERS trigger_push_velocity:
+ *
+ *   target:  this points to the target_position to which the player will jump.
+ *   speed:   XY speed for player-directional velocity pads - either sets or adds to the player's horizontal velocity.
+ *   count:   Z speed for player-directional velocity pads - either sets or adds to the player's vertical velocity.
+ */
 #ifdef SVQC
 spawnfunc(trigger_push);
+spawnfunc(trigger_push_velocity);
 
 spawnfunc(target_push);
 spawnfunc(info_notnull);
index ae82a82c1003a8b5da7f8e4e084143a919d5e770..e6da12a34f12ef3f492da45f4a79dfe114ea09ca 100644 (file)
@@ -27,6 +27,9 @@ bool Teleport_Active(entity this, entity player)
 
        if(IS_TURRET(player))
                return false;
+
+       if(this.classname == "trigger_teleport" && this.spawnflags & TELEPORT_SPECTATOR && !IS_SPEC(player))
+               return false;
 #elif defined(CSQC)
        if(!IS_PLAYER(player))
                return false;
index 6f70f09beec2219624baeca92e2cd7deaa104fb4..7197d57c23c13e5dadd36aae9c376e6354311f02 100644 (file)
@@ -1 +1,11 @@
 #pragma once
+
+
+// q3df compat spawnflags, they may also be useful in xonotic
+
+// trigger_teleport
+#define TELEPORT_SPECTATOR BIT(0) // exists in q3 but is only documented in q3df
+#define TELEPORT_KEEP_SPEED BIT(1)
+
+// target_teleporter
+#define TELEPORTER_KEEP_SPEED BIT(0)
index 3449f2b53ee5b950b2d2916d8e593509decdece3..8ff5304d5fba965c628df1c827b5dcc7471304b4 100644 (file)
@@ -26,6 +26,11 @@ REGISTER_SP(RACE_LAPS);
 REGISTER_SP(RACE_TIME);
 REGISTER_SP(RACE_FASTEST);
 
+REGISTER_SP(CTS_STRAFE);
+REGISTER_SP(CTS_STARTSPEED);
+REGISTER_SP(CTS_AVGSPEED);
+REGISTER_SP(CTS_TOPSPEED);
+
 REGISTER_SP(ASSAULT_OBJECTIVES);
 
 REGISTER_SP(CTF_CAPS);
index c093b62826d020269e45b7792c642bf56bc65341..641aec73981e2c9b889caeb1062d2df1264631ea 100644 (file)
@@ -354,7 +354,11 @@ bool autocvar_sv_slick_applygravity;
 #endif
 REGISTER_STAT(SLICK_APPLYGRAVITY, bool, autocvar_sv_slick_applygravity)
 
+#ifdef SVQC
+int autocvar_sv_q3compat_jumppads;
+#endif
 REGISTER_STAT(Q3COMPAT, int, q3compat)
+REGISTER_STAT(Q3COMPAT_JUMPPADS, int, autocvar_sv_q3compat_jumppads)
 // FIXME: workaround for https://gitlab.com/xonotic/xonotic-data.pk3dir/-/issues/2812
 #ifdef SVQC
        #define Q3COMPAT_COMMON q3compat
index 98418767a15f373428d860b51612a2d445ecf44c..368ffd73fe3133726205c7bf609ec23314af6318 100644 (file)
@@ -1675,6 +1675,21 @@ void Skeleton_SetBones(entity e)
 string to_execute_next_frame;
 void execute_next_frame()
 {
+#ifdef SVQC
+       IL_EACH(g_moveables, it.last_pushed,
+       {
+               if(WarpZoneLib_ExactTrigger_Touch(it.last_pushed, it))
+               {
+                       it.last_pushed = NULL;
+               }
+       });
+#elif defined(CSQC)
+       if(csqcplayer.last_pushed && WarpZoneLib_ExactTrigger_Touch(csqcplayer.last_pushed, csqcplayer))
+       {
+               csqcplayer.last_pushed = NULL;
+       }
+#endif
+
        if(to_execute_next_frame)
        {
                localcmd("\n", to_execute_next_frame, "\n");
index ae1eae28459adfb50c468fc37571feb1000e9a69..228718ca72ca8080b0063a02671807c4b199e0fd 100644 (file)
@@ -532,7 +532,11 @@ METHOD(Crylink, wr_think, void(entity thiswep, entity actor, .entity weaponentit
         thiswep.wr_reload(thiswep, actor, weaponentity);
     }
 
-    if(fire & 1)
+    // attack swapping is useful for emulating BFG behavior in XDF
+    int primary_fire = autocvar_g_balance_crylink_swap_attacks ? fire & 2 : fire & 1;
+    int secondary_fire = autocvar_g_balance_crylink_swap_attacks ? fire & 1 : fire & 2;
+
+    if(primary_fire)
     {
         if(actor.(weaponentity).crylink_waitrelease != 1)
         if(weapon_prepareattack(thiswep, actor, weaponentity, false, WEP_CVAR_PRI(crylink, refire)))
@@ -542,7 +546,7 @@ METHOD(Crylink, wr_think, void(entity thiswep, entity actor, .entity weaponentit
         }
     }
 
-    if((fire & 2) && autocvar_g_balance_crylink_secondary)
+    if((secondary_fire) && autocvar_g_balance_crylink_secondary)
     {
         if(actor.(weaponentity).crylink_waitrelease != 2)
         if(weapon_prepareattack(thiswep, actor, weaponentity, true, WEP_CVAR_SEC(crylink, refire)))
@@ -552,7 +556,7 @@ METHOD(Crylink, wr_think, void(entity thiswep, entity actor, .entity weaponentit
         }
     }
 
-    if((actor.(weaponentity).crylink_waitrelease == 1 && !(fire & 1)) || (actor.(weaponentity).crylink_waitrelease == 2 && !(fire & 2)))
+    if((actor.(weaponentity).crylink_waitrelease == 1 && !(primary_fire)) || (actor.(weaponentity).crylink_waitrelease == 2 && !(secondary_fire)))
     {
         if(!actor.(weaponentity).crylink_lastgroup || time > actor.(weaponentity).crylink_lastgroup.teleport_time)
         {
index 2329c364e2998f16d425856292da6c6431b181e3..243407a00122e35a79a86f68ca60587da0d96c81 100644 (file)
@@ -49,6 +49,7 @@ CLASS(Crylink, Weapon)
         P(class, prefix, speed, float, BOTH) \
         P(class, prefix, spreadtype, float, SEC) \
         P(class, prefix, spread, float, BOTH) \
+        P(class, prefix, swap_attacks, float, NONE) \
         P(class, prefix, switchdelay_drop, float, NONE) \
         P(class, prefix, switchdelay_raise, float, NONE) \
         P(class, prefix, weaponreplace, string, NONE) \
index c82e892f721815fdeff9c6b46d0260eee26fe86d..c62dbad50db98188ae950d32014101f13df51319 100644 (file)
@@ -25,6 +25,7 @@
 #include <server/scores_rules.qc>
 #include <server/spawnpoints.qc>
 #include <server/steerlib.qc>
+#include <server/strafe.qc>
 #include <server/teamplay.qc>
 #include <server/tests.qc>
 #include <server/world.qc>
index 52574efecc1ac689a2d9a341cc9a10a7d40c6943..cc41db238f1e65908a7a3c68f3a6957251d0e8ae 100644 (file)
@@ -25,6 +25,7 @@
 #include <server/scores_rules.qh>
 #include <server/spawnpoints.qh>
 #include <server/steerlib.qh>
+#include <server/strafe.qh>
 #include <server/teamplay.qh>
 #include <server/tests.qh>
 #include <server/world.qh>
index 7627e177b83e432e14d8bf44d58deba0ad49219d..a52b5252aa31fe0a837474b231be03df21fe11ca 100644 (file)
@@ -26,6 +26,7 @@
 #include <server/scores_rules.qh>
 #include <server/teamplay.qh>
 #include <server/world.qh>
+#include <lib/misc.qh>
 
 //  used by GameCommand_make_mapinfo()
 void make_mapinfo_Think(entity this)
@@ -1210,6 +1211,38 @@ void GameCommand_nospectators(int request)
        }
 }
 
+void GameCommand_printplayer(int request, int argc)
+{
+       switch (request)
+        {
+               case CMD_REQUEST_COMMAND:
+                {
+                       entity player = GetIndexedEntity(argc, 1);
+                        if (player.playerid)
+                        {
+                               GameLogEcho(strcat(
+                                                   strcat(
+                                                          ":playerinfo:", ftos(player.playerid),
+                                                          ":", ftos(etof(player)),
+                                                          ":", ftos(CS_CVAR(player).cvar_cl_allow_uidtracking),
+                                                          ":", ftos(CS_CVAR(player).cvar_cl_allow_uid2name)),
+                                                   strcat(
+                                                          ":", ftos(CS_CVAR(player).cvar_cl_allow_uidranking),
+                                                          ":", ((IS_REAL_CLIENT(player)) ? GameLog_ProcessIP(player.netaddress) : "bot"),
+                                                          ":", player.crypto_idfp,
+                                                          ":", playername(player.netname, player.team, false))));
+                        }
+                       return;
+                }
+               default:
+                case CMD_REQUEST_USAGE:
+                {
+                       LOG_HELP("Usage:^3 sv_cmd printplayer <player_entity_id>");
+                        return;
+                }
+        }
+}
+
 void GameCommand_printstats(int request)
 {
        switch (request)
@@ -1717,6 +1750,36 @@ void GameCommand_warp(int request, int argc)
        }
 }
 
+void IRCSay(string msgstr)
+{
+       if(msgstr == "")
+               return;
+
+       string prefix;
+       if(substring(msgstr, 0, 3) == "^4*") // actions
+               prefix = "\{3}";
+       else
+               prefix = "\{1}";
+
+       msgstr = strcat(prefix, strreplace("\n", " ", msgstr), "\n"); // newlines only are good for centerprint
+
+       FOREACH_CLIENTSLOT(true,
+       {
+               if(!intermission_running)
+               if((autocvar_g_chat_nospectators == 1) || (autocvar_g_chat_nospectators == 2 && !(warmup_stage || game_stopped)))
+               if(IS_PLAYER(it))
+                       continue;
+               if(IS_REAL_CLIENT(it))
+                       sprint(it, msgstr);
+       });
+}
+
+void GameCommand_ircmsg(int request, int argc, string command)
+{
+       IRCSay(substring(command, strlen(argv(0))+1, strlen(command)));
+        return;
+}
+
 /* use this when creating a new command, making sure to place it in alphabetical order... also,
 ** ADD ALL NEW COMMANDS TO commands.cfg WITH PROPER ALIASES IN THE SAME FASHION!
 void GameCommand_(int request)
@@ -1768,10 +1831,12 @@ SERVER_COMMAND(extendmatchtime, "Increase the timelimit value incrementally") {
 SERVER_COMMAND(gametype, "Simple command to change the active gametype") { GameCommand_gametype(request, arguments); }
 SERVER_COMMAND(gettaginfo, "Get specific information about a weapon model") { GameCommand_gettaginfo(request, arguments); }
 SERVER_COMMAND(gotomap, "Simple command to switch to another map") { GameCommand_gotomap(request, arguments); }
+SERVER_COMMAND(ircmsg, "Utility function to forward chat messages from IRC/discord/whatever") { GameCommand_ircmsg(request, arguments, command); }
 SERVER_COMMAND(lockteams, "Disable the ability for players to switch or enter teams") { GameCommand_lockteams(request); }
 SERVER_COMMAND(make_mapinfo, "Automatically rebuild mapinfo files") { GameCommand_make_mapinfo(request); }
 SERVER_COMMAND(moveplayer, "Change the team/status of a player") { GameCommand_moveplayer(request, arguments); }
 SERVER_COMMAND(nospectators, "Automatically remove spectators from a match") { GameCommand_nospectators(request); }
+SERVER_COMMAND(printplayer, "Print information about a player") { GameCommand_printplayer(request, arguments); }
 SERVER_COMMAND(printstats, "Dump eventlog player stats and other score information") { GameCommand_printstats(request); }
 SERVER_COMMAND(radarmap, "Generate a radar image of the map") { GameCommand_radarmap(request, arguments); }
 SERVER_COMMAND(reducematchtime, "Decrease the timelimit value incrementally") { GameCommand_reducematchtime(request); }
index 9333ebfed50973872e0c0111aba93b4886cde5eb..666598d02545e1afa39d8c1d474e39af5116a846 100644 (file)
@@ -256,12 +256,29 @@ spawnfunc(target_score)
        this.use = score_use;
 }
 
+#define FRAGSFILTER_REMOVER BIT(0)
+#define FRAGSFILTER_RUNONCE BIT(1) // unused
+#define FRAGSFILTER_SILENT  BIT(2)
+#define FRAGSFILTER_RESET   BIT(3)
+
 void fragsfilter_use(entity this, entity actor, entity trigger)
 {
        if(!IS_PLAYER(actor))
                return;
        if(actor.fragsfilter_cnt >= this.frags)
+       {
+               if(this.spawnflags & FRAGSFILTER_RESET)
+                       actor.fragsfilter_cnt = 0;
+               else if(this.spawnflags & FRAGSFILTER_REMOVER)
+                       actor.fragsfilter_cnt -= this.frags;
                SUB_UseTargets(this, actor, trigger);
+       }
+       else if(!(this.spawnflags & FRAGSFILTER_SILENT))
+       {
+               int req_frags = this.frags - actor.fragsfilter_cnt;
+               centerprint(actor, sprintf("%d more frag%s needed", req_frags, req_frags > 1 ? "s" : ""));
+               play2(actor, SND(TALK));
+       }
 }
 spawnfunc(target_fragsFilter)
 {
index 2b29422b8969362356b6a552106158c9b881d0e7..5f560022eecdcefaa4eb37ecc752cebc1f4fd648 100644 (file)
@@ -248,7 +248,7 @@ void remove_except_protected(entity e)
 void remove_unsafely(entity e)
 {
     if(e.classname == "spike")
-        error("Removing spikes is forbidden (crylink bug), please report");
+        LOG_WARN("Removing spikes is forbidden (crylink bug), please report");
     builtin_remove(e);
 }
 
index f8af3fc5f716c5ca12eb604c6f32ecd55c64d496..9f95a5a6b6b472544c007ef15e4de961f606c74c 100644 (file)
 #include <server/spawnpoints.qh>
 #include <server/weapons/common.qh>
 #include <server/world.qh>
+#include <server/strafe.qh>
+
 
 .string stored_netname; // TODO: store this information independently of race-based gamemodes
 
+.float race_startspeed;
+.float race_startspeed_best;
+.float race_avgspeed_sum;
+.float race_avgspeed_time;
+.float race_avgspeed_best;
+.float race_topspeed;
+.float race_topspeed_best;
+
 string uid2name(string myuid)
 {
        string s = db_get(ServerProgsDB, strcat("/uid2name/", myuid));
@@ -54,15 +64,23 @@ string uid2name(string myuid)
        return s;
 }
 
-void write_recordmarker(entity pl, float tstart, float dt)
+void write_recordmarker(entity pl, float newpos, float tstart, float dt)
 {
-    GameLogEcho(strcat(":recordset:", ftos(pl.playerid), ":", ftos(dt)));
+    GameLogEcho(strcat(":recordset:", ftos(newpos), ":", ftos(pl.playerid), ":", ftos(etof(pl)), ":", ftos(dt)));
 
     // also write a marker into demo files for demotc-race-record-extractor to find
-    stuffcmd(pl,
-             strcat(
-                 strcat("//", strconv(2, 0, 0, GetGametype()), " RECORD SET ", TIME_ENCODED_TOSTRING(TIME_ENCODE(dt), false)),
-                 " ", ftos(tstart), " ", ftos(dt), "\n"));
+    if (pl.crypto_idfp != "") {
+      stuffcmd(pl,
+               strcat(
+                   strcat("//", strconv(2, 0, 0, GetGametype()), " RECORD SET ", TIME_ENCODED_TOSTRING(TIME_ENCODE(dt), false)),
+                   strcat(" ", ftos(tstart), " ", ftos(dt), " ", ftos(newpos), " "),
+                   strcat(pl.crypto_idfp, "\n")));
+    } else {
+      stuffcmd(pl,
+               strcat(
+                   strcat("//", strconv(2, 0, 0, GetGametype()), " RECORD SET ", TIME_ENCODED_TOSTRING(TIME_ENCODE(dt), false)),
+                   strcat(" ", ftos(tstart), " ", ftos(dt), " ", ftos(newpos), " ANONYMOUS\n")));
+    }
 }
 
 IntrusiveList g_race_targets;
@@ -172,6 +190,7 @@ float race_checkpoint_lastlaps[MAX_CHECKPOINTS];
 entity race_checkpoint_lastplayers[MAX_CHECKPOINTS];
 
 .float race_checkpoint_record[MAX_CHECKPOINTS];
+.float current_checkpoint_record[MAX_CHECKPOINTS];
 
 float race_highest_checkpoint;
 float race_timed_checkpoint;
@@ -219,11 +238,19 @@ void race_SendNextCheckpoint(entity e, float spec) // qualifying only
                return;
 
        int cp = e.race_checkpoint;
-       float recordtime = race_checkpoint_records[cp];
-       float myrecordtime = e.race_checkpoint_record[cp];
-       string recordholder = race_checkpoint_recordholders[cp];
-       if(recordholder == e.netname)
+        float myrecordtime = e.race_checkpoint_record[cp];
+        float recordtime;
+        string recordholder;
+        if (autocvar_g_cts_cptimes_onlyself && (race_CheckpointNetworkID(cp) < 254)) {  // cp 254 - start line, cp 255 - finish line
+          recordtime = myrecordtime;
+          recordholder = "";
+        } else {
+          recordtime = race_checkpoint_records[cp];
+
+         recordholder = race_checkpoint_recordholders[cp];
+         if(recordholder == e.netname)
                recordholder = "";
+        }
 
        if(!IS_REAL_CLIENT(e))
                return;
@@ -366,9 +393,9 @@ void race_setTime(string map, float t, string myuid, string mynetname, entity e,
 {
        // netname only used TEMPORARILY for printing
        int newpos = race_readPos(map, t);
-
+  int i;
        int player_prevpos = 0;
-       for(int i = 1; i <= RANKINGS_CNT; ++i)
+       for(i = 1; i <= RANKINGS_CNT; ++i)
        {
                if(race_readUID(map, i) == myuid)
                        player_prevpos = i;
@@ -394,6 +421,29 @@ void race_setTime(string map, float t, string myuid, string mynetname, entity e,
                return;
        }
 
+       string body = sprintf("RECORDv1\n%s\n%s\n%s\n%s\n%d\n", strftime(false, "%Y-%m-%dT%H:%M:%SZ"), map, e.crypto_idfp, e.netaddress, t);
+        bool first_cp = true;
+        for (i=0; i < MAX_CHECKPOINTS; i++) {
+               if (e.current_checkpoint_record[i] > 0) {
+                        if (first_cp) {
+                            body = strcat(body, sprintf("%d %d", i, TIME_ENCODE(e.current_checkpoint_record[i])));
+                            first_cp = false;
+                        } else {
+                          body = strcat(body, sprintf(";%d %d", i, TIME_ENCODE(e.current_checkpoint_record[i])));
+                        }
+               }
+               }
+       body = strcat(body, sprintf("\n%f\n%f\n%f\n%f\n%s", e.race_topspeed_best, e.race_avgspeed_best, e.race_startspeed_best, e.strafe_efficiency_best, e.netname));
+        float r;
+        float buf = buf_create();
+        bufstr_set(buf, 0, body);
+        r = crypto_uri_postbuf(autocvar_sv_checkpoint_house_url, URI_GET_CURL + curl_uri_get_pos, "text/plain", "&", buf, 0);
+        if (r) {
+            curl_uri_get_pos = (curl_uri_get_pos + 1) % (URI_GET_CURL_END - URI_GET_CURL + 1);
+          }
+        buf_del(buf);
+        write_recordmarker(e, newpos, time - TIME_DECODE(t), TIME_DECODE(t));
+
        // if we didn't hit a return yet, we have a new record!
 
        // if the player does not have a UID we can unfortunately not store the record, as the rankings system relies on UIDs
@@ -417,9 +467,8 @@ void race_setTime(string map, float t, string myuid, string mynetname, entity e,
        // store new ranking
        race_writeTime(GetMapname(), t, myuid);
 
-       if (newpos == 1 && showmessage)
+       if (showmessage)
        {
-               write_recordmarker(e, time - TIME_DECODE(t), TIME_DECODE(t));
                race_send_recordtime(MSG_ALL);
        }
 
@@ -486,7 +535,21 @@ void race_SendTime(entity e, float cp, float t, float tvalid)
        {
                int s = GameRules_scoring_add(e, RACE_FASTEST, 0);
                if(!s || t < s)
+               {
                        GameRules_scoring_add(e, RACE_FASTEST, t - s);
+
+                       e.strafe_efficiency_best = e.strafe_efficiency_sum / e.strafe_efficiency_time;
+                       PlayerScore_Set(e, SP_CTS_STRAFE, floor(e.strafe_efficiency_best * 1000 + .5));
+
+                       e.race_startspeed_best = e.race_startspeed;
+                       PlayerScore_Set(e, SP_CTS_STARTSPEED, floor(e.race_startspeed_best + .5));
+
+                       e.race_avgspeed_best = e.race_avgspeed_sum / e.race_avgspeed_time;
+                       PlayerScore_Set(e, SP_CTS_AVGSPEED, floor(e.race_avgspeed_best + .5));
+
+                       e.race_topspeed_best = e.race_topspeed;
+                       PlayerScore_Set(e, SP_CTS_TOPSPEED, floor(e.race_topspeed_best + .5));
+               }
                if(!g_race_qualifying)
                {
                        s = GameRules_scoring_add(e, RACE_TIME, 0);
@@ -518,12 +581,17 @@ void race_SendTime(entity e, float cp, float t, float tvalid)
 
                if(tvalid)
                {
-                       recordtime = race_checkpoint_records[cp];
-                       float myrecordtime = e.race_checkpoint_record[cp];
-                       recordholder = strcat1(race_checkpoint_recordholders[cp]); // make a tempstring copy, as we'll possibly strunzone it!
-                       if(recordholder == e.netname)
+                        float myrecordtime = e.race_checkpoint_record[cp];
+                        if (autocvar_g_cts_cptimes_onlyself && (race_CheckpointNetworkID(cp) < 254)) {  // cp 254 - start line, cp 255 - finish line
+                          recordtime = myrecordtime;
+                          recordholder = "";
+                        } else {
+                          recordtime = race_checkpoint_records[cp];
+                          recordholder = strcat1(race_checkpoint_recordholders[cp]); // make a tempstring copy, as we'll possibly strunzone it
+                          if(recordholder == e.netname)
                                recordholder = "";
 
+                        }
                        if(t != 0)
                        {
                                if(cp == race_timed_checkpoint)
@@ -771,11 +839,17 @@ void checkpoint_passed(entity this, entity player)
 
                if(!this.race_checkpoint) // start line
                {
+                       player.race_startspeed = vlen(vec2(player.velocity));
                        player.race_laptime = time;
                        player.race_movetime = player.race_movetime_frac = player.race_movetime_count = 0;
                        player.race_penalty_accumulator = 0;
                        player.race_lastpenalty = NULL;
-               }
+                 for (int i=0; i < MAX_CHECKPOINTS; i++) {
+                               player.current_checkpoint_record[i] = 0;
+                       }
+               } else {
+                   player.current_checkpoint_record[this.race_checkpoint] = player.race_movetime; 
+       }
 
                if(g_race_qualifying)
                        race_SendNextCheckpoint(player, 0);
index 1e851ee3041e7fd671397c917571292b5a5ef863..fade8f795fa7939351fbf8f5ae65fb1b06ed53c0 100644 (file)
@@ -1,6 +1,7 @@
 #pragma once
 
 bool autocvar_g_allow_checkpoints;
+string autocvar_sv_checkpoint_house_url;
 
 float race_teams;
 
@@ -8,6 +9,7 @@ float race_teams;
 const float ST_RACE_LAPS = 1;
 
 int autocvar_g_cts_send_rankings_cnt = 15;
+bool autocvar_g_cts_cptimes_onlyself = false;
 
 int g_race_qualifying;
 
@@ -34,7 +36,7 @@ float race_completing;
 .entity race_respawn_spotref; // try THIS spawn in case you respawn
 
 // definitions for functions used outside race.qc
-void write_recordmarker(entity pl, float tstart, float dt);
+void write_recordmarker(entity pl, float newpos, float tstart, float dt);
 
 float race_PreviousCheckpoint(float f);
 float race_NextCheckpoint(float f);
diff --git a/qcsrc/server/strafe.qc b/qcsrc/server/strafe.qc
new file mode 100644 (file)
index 0000000..c25b1ef
--- /dev/null
@@ -0,0 +1,188 @@
+#include "strafe.qh"
+
+#include <common/physics/movetypes/movetypes.qh>
+#include <common/physics/player.qh>
+#include <common/stats.qh>
+
+.float race_started;
+
+float calculate_strafe_efficiency(entity strafeplayer, vector movement, float dt)
+{
+    if(!strafeplayer) return 0;
+
+    bool swimming = strafeplayer.waterlevel >= WATERLEVEL_SWIMMING;
+    float speed = vlen(vec2(strafeplayer.velocity));
+
+    if(speed <= 0 || swimming || !strafeplayer.race_started) return 0; // only calculate the efficiency if all conditions are met
+    strafeplayer.strafe_efficiency_time += dt;
+
+    // physics
+    bool   onground                      = IS_ONGROUND(strafeplayer) && !(PHYS_INPUT_BUTTON_JUMP(strafeplayer) || PHYS_INPUT_BUTTON_JETPACK(strafeplayer));
+    bool   onslick                       = IS_ONSLICK(strafeplayer);
+    bool   strafekeys;
+    float  maxspeed_mod                  = IS_DUCKED(strafeplayer) ? .5 : 1;
+    float  maxspeed_phys                 = onground ? PHYS_MAXSPEED(strafeplayer) : PHYS_MAXAIRSPEED(strafeplayer);
+    float  maxspeed                      = maxspeed_phys * maxspeed_mod;
+    float  movespeed;
+    float  bestspeed;
+    float  maxaccel_phys                 = onground ? PHYS_ACCELERATE(strafeplayer) : PHYS_AIRACCELERATE(strafeplayer);
+    float  maxaccel                      = maxaccel_phys;
+    float  vel_angle                     = vectoangles(strafeplayer.velocity).y - (vectoangles(strafeplayer.velocity).y > 180 ? 360 : 0); // change the range from 0° - 360° to -180° - 180° to match how view_angle represents angles
+    float  view_angle                    = PHYS_INPUT_ANGLES(strafeplayer).y;
+    float  angle;
+    int    keys_fwd;
+    float  wishangle;
+    bool   fwd                           = true;
+
+    // determine whether the player is pressing forwards or backwards keys
+    if(movement.x > 0)
+    {
+        keys_fwd = 1;
+    }
+    else if(movement.x < 0)
+    {
+        keys_fwd = -1;
+    }
+    else
+    {
+        keys_fwd = 0;
+    }
+
+    // determine player wishdir
+    if(movement.x == 0)
+    {
+        if(movement.y < 0)
+        {
+            wishangle = -90;
+        }
+        else if(movement.y > 0)
+        {
+            wishangle = 90;
+        }
+        else
+        {
+            wishangle = 0;
+        }
+    }
+    else
+    {
+        if(movement.y == 0)
+        {
+            wishangle = 0;
+        }
+        else
+        {
+            wishangle = RAD2DEG * atan2(movement.y, movement.x);
+            // wrap the wish angle if it exceeds ±90°
+            if(fabs(wishangle) > 90)
+            {
+                if(wishangle < 0) wishangle += 180;
+                else wishangle -= 180;
+                wishangle = -wishangle;
+            }
+        }
+    }
+
+    strafekeys = fabs(wishangle) == 90;
+
+    if(strafekeys && !onground && !swimming)
+    {
+        if(PHYS_MAXAIRSTRAFESPEED(strafeplayer) != 0)
+            maxspeed = min(PHYS_MAXAIRSTRAFESPEED(strafeplayer), PHYS_MAXAIRSPEED(strafeplayer) * maxspeed_mod);
+        if(PHYS_AIRSTRAFEACCELERATE(strafeplayer) != 0)
+            maxaccel = PHYS_AIRSTRAFEACCELERATE(strafeplayer);
+    }
+
+    movespeed = min(vlen(vec2(movement)), maxspeed);
+
+    maxaccel *= dt * movespeed;
+    bestspeed = max(movespeed - maxaccel, 0);
+
+    float strafespeed = speed; // speed minus friction
+
+    if((strafespeed > 0) && onground){
+        float strafefriction = onslick ? PHYS_FRICTION_SLICK(strafeplayer) : PHYS_FRICTION(strafeplayer);
+        float f = 1 - dt * strafefriction * max(PHYS_STOPSPEED(strafeplayer) / strafespeed, 1);
+
+        if(f <= 0)
+            strafespeed = 0;
+        else
+            strafespeed *= f;
+    }
+
+    // get current strafing angle ranging from -180° to +180°
+    // calculate view angle relative to the players current velocity direction
+    angle = vel_angle - view_angle;
+
+    // if the angle goes above 180° or below -180° wrap it to the opposite side since we want the interior angle
+    if (angle > 180) angle -= 360;
+    else if(angle < -180) angle += 360;
+
+    // determine whether the player is strafing forwards or backwards
+    // if the player isn't strafe turning use forwards/backwards keys to determine direction
+    if(!strafekeys)
+    {
+        if(keys_fwd > 0)
+        {
+            fwd = true;
+        }
+        else if(keys_fwd < 0)
+        {
+            fwd = false;
+        }
+        else
+        {
+            fwd = fabs(angle) <= 90;
+        }
+    }
+    // otherwise determine by examining the strafe angle
+    else
+    {
+        if(wishangle < 0) // detect direction since the direction is not yet set
+        {
+            fwd = angle <= -wishangle;
+        }
+        else
+        {
+            fwd = angle >= -wishangle;
+        }
+    }
+
+    // shift the strafe angle by 180° when strafing backwards
+    if(!fwd)
+    {
+        if(angle < 0) angle += 180;
+        else angle -= 180;
+    }
+
+    // invert the wish angle when strafing backwards
+    if(!fwd)
+    {
+        wishangle = -wishangle;
+    }
+
+    // note about accuracy: a few ticks after dying do still have race_started set to true causing minimal interference in the efficiency total
+    float efficiency = 0;
+    float moveangle = fabs(angle + wishangle);
+    float bestangle = (strafespeed > bestspeed ? acos(bestspeed / strafespeed) : 0) * RAD2DEG;
+    float prebestangle = (strafespeed > movespeed ? acos(movespeed / strafespeed) : 0) * RAD2DEG;
+
+    if(fabs(vlen(vec2(movement))) > 0)
+    {
+        if(moveangle >= 90)
+        {
+            efficiency = (moveangle - 90) / 90;
+            if(efficiency > 1) efficiency = 2 - efficiency;
+            efficiency *= -1;
+        }
+        else if(moveangle >= bestangle)
+        {
+            efficiency = (90 - moveangle) / (90 - bestangle);
+        }
+        else if(moveangle >= prebestangle)
+        {
+            efficiency = (moveangle - prebestangle) / (bestangle - prebestangle);
+        }
+    }
+    return efficiency;
+}
diff --git a/qcsrc/server/strafe.qh b/qcsrc/server/strafe.qh
new file mode 100644 (file)
index 0000000..ceecbb5
--- /dev/null
@@ -0,0 +1,7 @@
+#pragma once
+
+.float strafe_efficiency_sum;
+.float strafe_efficiency_time;
+.float strafe_efficiency_best;
+
+float calculate_strafe_efficiency(entity, vector, float);
index 82cccae0f3b324fdcf0ce8ee999962c54772f483..bd1869e24a82f374e4aa0250b1bfab5a1b096dfb 100644 (file)
@@ -503,6 +503,7 @@ sv_gameplayfix_gravityunaffectedbyticrate 1
 sv_gameplayfix_nogravityonground 1
 
 set sv_q3compat_changehitbox 0 "use Q3 player hitbox dimensions and camera height on Q3 maps (maps with an entry in a .arena or .defi file)"
+set sv_q3compat_jumppads 1 "calculate the jump pad trajectory starting at the center of the push trigger instead of the player origin, \"0\" = never, \"1\" = on Q3 maps, \"2\" = on all maps"
 
 set g_movement_highspeed 1 "multiplier scale for movement speed (applies to sv_maxspeed and sv_maxairspeed, also applies to air acceleration when g_movement_highspeed_q3_compat is set to 0)"
 set g_movement_highspeed_q3_compat 0 "apply speed modifiers to air movement in a more Q3-compatible way (only apply speed buffs and g_movement_highspeed to max air speed, not to acceleration)"
@@ -598,3 +599,5 @@ set sv_warpzone_allow_selftarget 0 "do not touch"
 sv_disablenotify 1
 
 set sv_quickmenu_file "" "filename of a custom server's quickmenu that will be selectable from the default client's quickmenu. This file must be sent in a pk3 archive and should have an unique name (e.g. quickmenu-servername.txt) to prevent overriding existing quickmenus"
+
+set sv_checkpoint_house_url "http://127.0.0.1:10876/new-record"