diff --git a/build/msvc_2019/game.vcxproj b/build/msvc_2019/game.vcxproj
index ca35ae6..0fc4702 100644
--- a/build/msvc_2019/game.vcxproj
+++ b/build/msvc_2019/game.vcxproj
@@ -255,6 +255,7 @@
+
diff --git a/build/msvc_2019/game.vcxproj.filters b/build/msvc_2019/game.vcxproj.filters
index 0fa8cda..3251ada 100644
--- a/build/msvc_2019/game.vcxproj.filters
+++ b/build/msvc_2019/game.vcxproj.filters
@@ -245,6 +245,9 @@
game\mods\features
+
+ game\mods\features
+
game\mods\features\pingcomp
diff --git a/build/qvm_build/build_game.bat b/build/qvm_build/build_game.bat
index 60b719c..af46d52 100644
--- a/build/qvm_build/build_game.bat
+++ b/build/qvm_build/build_game.bat
@@ -65,6 +65,7 @@ set src=game\mods&set name=g_mod_stubs&call :compile
set src=game\mods&set name=g_mod_utils&call :compile
set src=game\mods\features&set name=feature_altswap_handler&call :compile
set src=game\mods\features&set name=feature_player_move&call :compile
+set src=game\mods\features&set name=feature_spect_passthrough&call :compile
set src=game\mods\features\pingcomp&set name=pc_client_predict&call :compile
set src=game\mods\features\pingcomp&set name=pc_dead_move&call :compile
set src=game\mods\features\pingcomp&set name=pc_instant_weapons&call :compile
diff --git a/code/game/g_mover.c b/code/game/g_mover.c
index 9e13810..b873c40 100644
--- a/code/game/g_mover.c
+++ b/code/game/g_mover.c
@@ -755,6 +755,10 @@ static void Touch_DoorTriggerSpectator( gentity_t *ent, gentity_t *other, trace_
int i, axis;
vec3_t origin, dir, angles;
+ if ( modfn.AdjustGeneralConstant( GC_SKIP_SPECTATOR_DOOR_TELEPORT, 0 ) ) {
+ return;
+ }
+
axis = ent->count;
VectorClear(dir);
if (fabs(other->s.origin[axis] - ent->r.absmax[axis]) <
diff --git a/code/game/mods/features/feature_spect_passthrough.c b/code/game/mods/features/feature_spect_passthrough.c
new file mode 100644
index 0000000..0e5f87c
--- /dev/null
+++ b/code/game/mods/features/feature_spect_passthrough.c
@@ -0,0 +1,97 @@
+/*
+* Spectator Pass-Through
+*
+* This module changes free-roaming spectator behavior to allow clipping through all dynamic
+* entities. It replaces the standard method of auto-teleporting through doors, which is very
+* unreliable and prone to getting stuck.
+*/
+
+#include "mods/g_mod_local.h"
+
+#define PREFIX( x ) ModSpectPassThrough_##x
+#define MOD_STATE PREFIX( state )
+
+static struct {
+ void ( *Prev_Trace )( trace_t *results, const vec3_t start, const vec3_t mins, const vec3_t maxs,
+ const vec3_t end, int passEntityNum, int contentMask );
+
+ // For mod function stacking
+ ModFNType_PmoveInit Prev_PmoveInit;
+ ModFNType_AdjustGeneralConstant Prev_AdjustGeneralConstant;
+} *MOD_STATE;
+
+/*
+================
+ModSpectPassThrough_Trace
+
+Trace with entities ignored so everything except the main map is pass-through.
+================
+*/
+static void ModSpectPassThrough_Trace( trace_t *results, const vec3_t start, const vec3_t mins, const vec3_t maxs,
+ const vec3_t end, int passEntityNum, int contentMask ) {
+ int entityNum;
+ MOD_STATE->Prev_Trace( results, start, mins, maxs, end, passEntityNum, contentMask );
+ entityNum = results->entityNum;
+
+ if ( entityNum >= 0 && entityNum < ENTITYNUM_MAX_NORMAL && EF_WARN_ASSERT( g_entities[entityNum].r.contents ) ) {
+ // Repeat trace with contacted entity temporarily set to empty contents.
+ int oldContents = g_entities[entityNum].r.contents;
+ g_entities[entityNum].r.contents = 0;
+ ModSpectPassThrough_Trace( results, start, mins, maxs, end, passEntityNum, contentMask );
+ g_entities[entityNum].r.contents = oldContents;
+ }
+}
+
+/*
+================
+(ModFN) PmoveInit
+
+Override trace function for spectators.
+================
+*/
+LOGFUNCTION_SVOID( PREFIX(PmoveInit), ( int clientNum, pmove_t *pmove ),
+ ( clientNum, pmove ), "G_MODFN_PMOVEINIT" ) {
+ const gclient_t *client = &level.clients[clientNum];
+
+ MOD_STATE->Prev_PmoveInit( clientNum, pmove );
+
+ if ( client->sess.sessionTeam == TEAM_SPECTATOR || ( client->ps.eFlags & EF_ELIMINATED ) ) {
+ MOD_STATE->Prev_Trace = pmove->trace;
+ pmove->trace = ModSpectPassThrough_Trace;
+ }
+}
+
+/*
+================
+(ModFN) AdjustGeneralConstant
+================
+*/
+int PREFIX(AdjustGeneralConstant)( generalConstant_t gcType, int defaultValue ) {
+ if ( gcType == GC_SKIP_SPECTATOR_DOOR_TELEPORT ) {
+ return 1;
+ }
+
+ return MOD_STATE->Prev_AdjustGeneralConstant( gcType, defaultValue );
+}
+
+/*
+================
+ModSpectPassThrough_Init
+================
+*/
+
+#define INIT_FN_STACKABLE( name ) \
+ MOD_STATE->Prev_##name = modfn.name; \
+ modfn.name = PREFIX(name);
+
+#define INIT_FN_OVERRIDE( name ) \
+ modfn.name = PREFIX(name);
+
+LOGFUNCTION_VOID( ModSpectPassThrough_Init, ( void ), (), "G_MOD_INIT" ) {
+ if ( !MOD_STATE ) {
+ MOD_STATE = G_Alloc( sizeof( *MOD_STATE ) );
+
+ INIT_FN_STACKABLE( PmoveInit );
+ INIT_FN_STACKABLE( AdjustGeneralConstant );
+ }
+}
diff --git a/code/game/mods/g_mod_local.h b/code/game/mods/g_mod_local.h
index d8be7f0..d931ac6 100644
--- a/code/game/mods/g_mod_local.h
+++ b/code/game/mods/g_mod_local.h
@@ -18,6 +18,7 @@ int G_ModUtils_GetLatchedValue( const char *cvar_name, const char *default_value
void ModAltSwapHandler_Init( void );
void ModPingcomp_Init( void );
void ModPlayerMove_Init( void );
+void ModSpectPassThrough_Init( void );
//
// Ping Compensation (pc_main.c)
diff --git a/code/game/mods/g_mod_main.c b/code/game/mods/g_mod_main.c
index 9a88787..44cc202 100644
--- a/code/game/mods/g_mod_main.c
+++ b/code/game/mods/g_mod_main.c
@@ -32,5 +32,6 @@ LOGFUNCTION_VOID( G_ModsInit, ( void ), (), "G_MOD_INIT" ) {
ModPlayerMove_Init();
ModAltSwapHandler_Init();
ModPingcomp_Init();
+ ModSpectPassThrough_Init();
}
}
diff --git a/code/game/mods/g_mod_public.h b/code/game/mods/g_mod_public.h
index 4586ffe..e24b02f 100644
--- a/code/game/mods/g_mod_public.h
+++ b/code/game/mods/g_mod_public.h
@@ -11,6 +11,7 @@ typedef enum {
GC_NONE,
GC_SKIP_RUN_MISSILE,
GC_EVENT_TIME_OFFSET,
+ GC_SKIP_SPECTATOR_DOOR_TELEPORT,
} generalConstant_t;
typedef enum {