diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index da37c1b..c20a89c 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -7,7 +7,10 @@ jobs:
strategy:
matrix:
os: [windows-latest, ubuntu-latest]
- arch: [x86, x86_64]
+ arch: [x86, x86_64, arm64]
+ exclude:
+ - os: windows-latest
+ arch: arm64
include:
# Windows
- os: windows-latest
@@ -23,6 +26,9 @@ jobs:
- os: ubuntu-latest
arch: x86_64
platform: linux
+ - os: ubuntu-latest
+ arch: arm64
+ platform: linux
steps:
- uses: actions/checkout@v7
@@ -32,7 +38,29 @@ jobs:
shell: bash
run: |
sudo apt-get update # && sudo apt-get upgrade -y
- sudo apt-get install -y build-essential libc6-dev-i386 g++-multilib gcc-mingw-w64 p7zip-full
+ sudo apt-get install -y build-essential p7zip-full
+ # 32-bit and multilib support only exists/is needed on x86 hosts;
+ # there's no i386 or multilib package on the arm64 runner
+ if [ "${{ matrix.arch }}" == "x86" ]; then
+ sudo apt-get install -y libc6-dev-i386 g++-multilib
+ fi
+ if [ "${{ matrix.arch }}" != "arm64" ]; then
+ sudo apt-get install -y gcc-mingw-w64
+ fi
+ if [ "${{ matrix.arch }}" == "arm64" ]; then
+ sudo apt-get install -y gcc-aarch64-linux-gnu
+ fi
+
+ - name: Install llvm-mingw (arm64 Windows cross-compile)
+ if: ${{ matrix.os != 'windows-latest' && matrix.arch == 'arm64' }}
+ shell: bash
+ run: |
+ LLVM_MINGW_VERSION=20241203
+ HOST_ARCH=$(uname -m)
+ curl -L -o llvm-mingw.tar.xz \
+ "https://github.com/mstorsjo/llvm-mingw/releases/download/${LLVM_MINGW_VERSION}/llvm-mingw-${LLVM_MINGW_VERSION}-ucrt-ubuntu-20.04-${HOST_ARCH}.tar.xz"
+ tar -xf llvm-mingw.tar.xz
+ echo "$PWD/llvm-mingw-${LLVM_MINGW_VERSION}-ucrt-ubuntu-20.04-${HOST_ARCH}/bin" >> "$GITHUB_PATH"
- name: Build for ${{ matrix.os }} ${{ matrix.arch }}
shell: bash
@@ -65,8 +93,8 @@ jobs:
ARCHIVE: 1
- name: Store QVM artifacts
- # store only with Linux x86_64
- if: ${{ matrix.os != 'windows-latest' && matrix.arch != 'x86' }}
+ # store only once, from Linux x86_64
+ if: ${{ matrix.os != 'windows-latest' && matrix.arch == 'x86_64' }}
uses: actions/upload-artifact@v7
with:
name: QVMs
@@ -114,4 +142,40 @@ jobs:
path: |
binaries/debug-windows-${{ matrix.arch }}
if-no-files-found: error
+ retention-days: 5
+
+ build-macos:
+ runs-on: macos-latest
+ strategy:
+ matrix:
+ arch: [x86_64, arm64]
+ steps:
+ - uses: actions/checkout@v7
+
+ - name: Build native macOS
+ shell: bash
+ run: |
+ mkdir -p binaries/release-darwin-${{ matrix.arch }}
+ make -j$(sysctl -n hw.ncpu) install ARCH="${{ matrix.arch }}" DESTDIR=./binaries/release-darwin-${{ matrix.arch }}
+ mkdir -p binaries/debug-darwin-${{ matrix.arch }}
+ make -j$(sysctl -n hw.ncpu) install_debug ARCH="${{ matrix.arch }}" DESTDIR=./binaries/debug-darwin-${{ matrix.arch }}
+ env:
+ ARCHIVE: 1
+
+ - name: Store macOS release ${{ matrix.arch }} .dylib artifacts
+ uses: actions/upload-artifact@v7
+ with:
+ name: release-darwin-${{ matrix.arch }}
+ path: |
+ binaries/release-darwin-${{ matrix.arch }}
+ if-no-files-found: error
+ retention-days: 5
+
+ - name: Store macOS debug ${{ matrix.arch }} .dylib artifacts
+ uses: actions/upload-artifact@v7
+ with:
+ name: debug-darwin-${{ matrix.arch }}
+ path: |
+ binaries/debug-darwin-${{ matrix.arch }}
+ if-no-files-found: error
retention-days: 5
\ No newline at end of file
diff --git a/Makefile b/Makefile
index f6b19cf..9299a25 100644
--- a/Makefile
+++ b/Makefile
@@ -55,6 +55,10 @@ ARCH=$(COMPILE_ARCH)
endif
export ARCH
+ifeq ($(ARCH),arm64)
+ override ARCH=aarch64
+endif
+
ifneq ($(PLATFORM),$(COMPILE_PLATFORM))
CROSS_COMPILING=1
else
@@ -127,9 +131,54 @@ ifeq ($(PLATFORM),linux)
BASE_CFLAGS += -m32
endif
+ ifeq ($(ARCH),aarch64)
+ LIB=lib64
+ ifeq ($(CROSS_COMPILING),1)
+ ifneq ($(filter $(CC),cc gcc),)
+ CC=aarch64-linux-gnu-gcc
+ endif
+ endif
+
+ CC_TARGET_MACHINE := $(shell $(CC) -dumpmachine 2>/dev/null)
+ ifeq (,$(findstring aarch64,$(CC_TARGET_MACHINE)))
+ $(error CC='$(CC)' does not target aarch64 (reports '$(CC_TARGET_MACHINE)'). \
+ Install an aarch64 cross-toolchain (e.g. 'apt install gcc-aarch64-linux-gnu' \
+ on Debian/Ubuntu) and/or pass CC=aarch64-linux-gnu-gcc explicitly, e.g.: \
+ make ARCH=aarch64 CC=aarch64-linux-gnu-gcc)
+ endif
+ endif
+
endif #Linux
+ifeq ($(PLATFORM),darwin)
+
+ BASE_CFLAGS = -Wall -fno-strict-aliasing -Wimplicit -Wstrict-prototypes -pipe
+
+ OPTIMIZE = -O3 -fomit-frame-pointer -ffast-math
+
+ SHLIBEXT=dylib
+ SHLIBCFLAGS=-fPIC -fvisibility=hidden
+ SHLIBLDFLAGS=-bundle -flat_namespace -undefined suppress $(LDFLAGS)
+
+ LIBS= -lm
+
+ ifeq ($(ARCH),x86_64)
+ BASE_CFLAGS += -arch x86_64
+ SHLIBLDFLAGS += -arch x86_64
+ endif
+ ifeq ($(ARCH),aarch64)
+ BASE_CFLAGS += -march=armv8-a -arch arm64
+ SHLIBLDFLAGS += -arch arm64
+ endif
+ ifeq ($(ARCH),x86)
+ BASE_CFLAGS += -arch i386
+ SHLIBLDFLAGS += -arch i386
+ endif
+
+endif #Darwin
+
+
ifdef MINGW
ifeq ($(CROSS_COMPILING),1)
@@ -147,12 +196,25 @@ ifdef MINGW
ifeq ($(ARCH),x86)
MINGW_PREFIXES=i686-w64-mingw32 i586-mingw32msvc i686-pc-mingw32
endif
+ ifeq ($(ARCH),aarch64)
+ MINGW_PREFIXES=aarch64-w64-mingw32
+ STRIP=aarch64-w64-mingw32-strip
+ endif
ifndef CC
CC=$(firstword $(strip $(foreach MINGW_PREFIX, $(MINGW_PREFIXES), \
$(call bin_path, $(MINGW_PREFIX)-gcc))))
endif
+ ifeq ($(ARCH),aarch64)
+ ifeq ($(strip $(CC)),)
+ $(error No aarch64-w64-mingw32-gcc found on PATH for ARCH=aarch64 \
+ Windows cross-compile. Install llvm-mingw (provides the \
+ aarch64-w64-mingw32 toolchain) and ensure its bin/ directory is \
+ on PATH, then retry.)
+ endif
+ endif
+
# STRIP=$(MINGW_PREFIX)-strip -g
ifndef WINDRES
@@ -201,11 +263,24 @@ ifeq ($(PLATFORM),windows)
ifeq ($(ARCH),x86)
MINGW_PREFIXES=i686-w64-mingw32 i586-mingw32msvc i686-pc-mingw32
endif
+ ifeq ($(ARCH),aarch64)
+ MINGW_PREFIXES=aarch64-w64-mingw32
+ STRIP=aarch64-w64-mingw32-strip
+ endif
ifndef CC
CC=$(firstword $(strip $(foreach MINGW_PREFIX, $(MINGW_PREFIXES), \
$(call bin_path, $(MINGW_PREFIX)-gcc))))
endif
+
+ ifeq ($(ARCH),aarch64)
+ ifeq ($(strip $(CC)),)
+ $(error No aarch64-w64-mingw32-gcc found on PATH for ARCH=aarch64 \
+ Windows cross-compile. Install llvm-mingw (provides the \
+ aarch64-w64-mingw32 toolchain) and ensure its bin/ directory is \
+ on PATH, then retry.)
+ endif
+ endif
STRIP=$(MINGW_PREFIX)-strip -g
diff --git a/README.md b/README.md
index aa5016b..374d545 100644
--- a/README.md
+++ b/README.md
@@ -30,7 +30,10 @@ A legendary Quake 3 Arena mod from the late 90s to early 2000s.
> > > 3.3.2. [Building QVM (using .bat)](#building-qvm-using-bat-1)
> > > 3.3.3. [Building shared libraries (.so)](#building-shared-libraries-so)
> >
-> > 3.4. [Optional](#optional)
+> > 3.4. [macOS](#macos)
+> > > 3.4.1. [Building dynamic libraries (.dylib)](#building-dynamic-libraries-dylib)
+> >
+> > 3.5. [Optional](#optional)
> 4. [Notes](#notes)
> 5. [Legal](#legal)
> 6. [Credits](#credits)
@@ -140,6 +143,13 @@ You'll notice some differences and things that the original Bid For Power didn't
- `missileArch [0/1]`: the projectile, with missile or rdmissile attack type, moves like an arch across the crosshair
- `missileDlightRainbow [0/1]` to see rainbow dynamic rainbow light effect, and `missileTrailRainbow [0/1]` to see rainbow trail effect, in the skin config
- new confetti particle, use `explosionConfetti ` in the skin config for the explosion
+- free-lock death camera, the player, as corpse, can see around their position
+- moderation features:
+ - player list: list player entity ids, names, IPs (server admin side only), bot info (if there's) and teams, usage: `playerlist`
+ - ignore: (client side only) ignores a client in the chat and voice, usage: `ignore `; to remove from ignore list: `unignore `; to clear all ignored clients: `clear_ignores`
+ - muteban: (server side only) stores a ban to mute completely a client in the server, like ignore but in server permanently, usage: `mute `; to remove from mute ban list: `unmute `; to check mute ban list: `mutebans`
+ - voteban: (server side only) stores a ban to avoid calling a vote and voting in the server, usage: `voteban `; to remove from vote ban list: `unvoteban `; to check vote ban list: `votebans`
+ - playban: (server side only) stores a ban and forces to spectate a client in the server and cannot join in the matches, usage: `playban `; to remove from play ban list: `unplayban `; to check play ban list: `playbans`
# How to build
@@ -198,7 +208,7 @@ The information in the map file can be useful for debugging and performance anal
To build, follow these instructions:
- 1. Install msys2 from https://msys2.github.io/, following the instructions there.It doesn’t matter which version you download, just get one appropriate for your OS.
+ 1. Install msys2 from https://msys2.github.io/, following the instructions there. It doesn't matter which version you download, just get one appropriate for your OS.
2. Start "MSYS2 MinGW 64-bit" from the Start Menu. If you're using 32-bit system, use "MSYS2 MinGW 32-bit".
@@ -269,7 +279,7 @@ The information in the map file can be useful for debugging and performance anal
2. Build the solution
In the *Solution Configurations* tab, you can switch `Debug` or `Release` and `Win32` or `x64`.
- Right-click to the solution in the *Solution Explorer* and click `Build Solution`, check the log until the build is succeded. Go to `Win32` or `x64` folder in `win32-msvc`, look inside `Debug` or `Release` and get the dlls you built.
+ Right-click to the solution in the *Solution Explorer* and click `Build Solution`, check the log until the build succeeds. Go to `Win32` or `x64` folder in `win32-msvc`, look inside `Debug` or `Release` and get the dlls you built.
3. Debugging
@@ -341,6 +351,27 @@ The information in the map file can be useful for debugging and performance anal
```
3. And find .so files in `build/release-linux-x86_64`, for 32-bit: `build/release-linux-x86`.
+- ### macOS:
+
+ * #### _Building dynamic libraries (.dylib)_:
+
+ Requires Xcode Command Line Tools (or full Xcode) to be installed. If you don't have them, install with:
+ ```sh
+ xcode-select --install
+ ```
+
+ 1. Keep in mind you must be in the repository directory. Simply execute (`-j4` is the number of parallel jobs you want to run during the compilation, in that case is set to 4):
+ ```sh
+ make -j4
+ ```
+ By default, this builds for whatever architecture your Mac is running (`x86_64` on Intel Macs, `arm64` on Apple Silicon).
+
+ 2. And find .dylib files in `build/release-darwin-x86_64` or `build/release-darwin-arm64` depending on the architecture you built.
+
+ > [!NOTE]
+ > **About 32-bit (`ARCH=x86`) on macOS**: this is *not* supported by any current Xcode/Clang toolchain, and it isn't something this Makefile can work around with flags — Apple removed the i386 SDK (including `libSystem`, the most basic system library) starting with Xcode 10 in 2018.
+ >
+ > The Makefile still has an `ARCH=x86` code path under `PLATFORM=darwin` for the rare case where someone has an old Xcode 9 (or earlier) toolchain lying around, paired with an old macOS SDK (10.13/10.14-era). If you do get a working i386 build this way, keep in mind the resulting binary will only *run* on macOS 10.14 (Mojave) or earlier — Catalina (10.15) and every version after it refuse to execute 32-bit binaries at all, regardless of how they were compiled.
- ### Optional:
@@ -348,6 +379,7 @@ The information in the map file can be useful for debugging and performance anal
- [MSYS2 (mingw) (Building dynamic libraries (.dll))](#msys2-mingw-building-dynamic-libraries-dll)
- [Cygwin (mingw) (Building dynamic libraries (.dll))](#cygwin-mingw-building-dynamic-libraries-dll)
- [Building shared libraries (.so)](#building-shared-libraries-so)
+ - [macOS (Building dynamic libraries (.dylib))](#building-dynamic-libraries-dylib)
You can execute optionally the parameters using the following ways:
@@ -361,7 +393,12 @@ The information in the map file can be useful for debugging and performance anal
make ARCH=x86 PLATFORM=windows # compiles release x86 .dll builds (creates "release-windows-x86" directory inside "build")
```
- ... Optionally, you can play the parameters like `ARCH=x86_64` (compiles 64-bits builds), `PLATFORM=windows` (compiles dlls), `PLATFORM=linux` (compiles shared libraries (.so files)) ...
+ * To compile release arm64 (aarch64) .dylib builds on macOS:
+ ```sh
+ make ARCH=arm64 PLATFORM=darwin # compiles release arm64 .dylib builds (creates "release-darwin-arm64" directory inside "build")
+ ```
+
+ ... Optionally, you can play the parameters like `ARCH=x86_64` (compiles 64-bits builds), `ARCH=arm64` (compiles arm64(aarch64) builds, only for `PLATFORM=darwin` or Linux), `PLATFORM=windows` (compiles dlls), `PLATFORM=linux` (compiles shared libraries (.so files)), `PLATFORM=darwin` (compiles dynamic libraries (.dylib files), macOS only) ...
* To compile and copy release builds at the destination directory, `DESTDIR` parameter is mandatory:
```sh
@@ -370,7 +407,7 @@ The information in the map file can be useful for debugging and performance anal
* To compile and copy debug builds at the destination directory, `DESTDIR` parameter is mandatory:
```sh
- make install DESTDIR=/your/path/q3/baseq3mod # compiles debug builds and copy the builds to the destination directory (you can also put ARCH=x86 PLATFORM=windows if you want)
+ make install_debug DESTDIR=/your/path/q3/baseq3mod # compiles debug builds and copy the builds to the destination directory (you can also put ARCH=x86 PLATFORM=windows if you want)
```
diff --git a/source/cgame/cg_local.h b/source/cgame/cg_local.h
index 18b4d35..8f156c5 100644
--- a/source/cgame/cg_local.h
+++ b/source/cgame/cg_local.h
@@ -138,6 +138,10 @@ typedef struct {
typedef struct {
lerpFrame_t legs, torso, flag;
+ // BFPR - Head to avoid player model look deformed when dead while camera can move freely
+#ifdef BFPR_DEAD_CAMERA_FREE_MOVE
+ lerpFrame_t head;
+#endif
int painTime;
int painDirection; // flip from 0 to 1
//int lightningFiring;
@@ -155,6 +159,13 @@ typedef struct {
qboolean constantFireAtkPlayed; // BFP - To play constantFireAttack fire sound once
int lastChargeVoiceLevel; // BFP - To play charge voice in one charge count once
+
+ // BFPR - For dead player angles
+#ifdef BFPR_DEAD_CAMERA_FREE_MOVE
+ qboolean deadAnglesFrozen;
+ int deadAnglesClientNum;
+ vec3_t deadAnglesOrigin;
+#endif
} playerEntity_t;
//=================================================
diff --git a/source/cgame/cg_players.c b/source/cgame/cg_players.c
index d9d91e3..a89bca3 100644
--- a/source/cgame/cg_players.c
+++ b/source/cgame/cg_players.c
@@ -1102,6 +1102,46 @@ static void CG_PlayerAngles( centity_t *cent, vec3_t legs[3], vec3_t torso[3], v
int dir, clientNum;
clientInfo_t *ci;
+ // BFPR - Make the camera move freely when the player is dead,
+ // keep player model angles, otherwise looks deformed
+#if BFPR_DEAD_CAMERA_FREE_MOVE
+ qboolean isNewCorpseInstance;
+ if ( cent->currentState.eFlags & EF_DEAD ) {
+ isNewCorpseInstance = !cent->pe.deadAnglesFrozen
+ || cent->pe.deadAnglesClientNum != cent->currentState.clientNum
+ || !VectorCompare( cent->pe.deadAnglesOrigin, cent->lerpOrigin );
+
+ if ( isNewCorpseInstance ) {
+ cent->pe.head.yawAngle = AngleMod( cent->pe.head.yawAngle );
+ cent->pe.head.pitchAngle = 0;
+ cent->pe.legs.yawAngle = cent->pe.head.yawAngle;
+ cent->pe.legs.pitchAngle = 0;
+ cent->pe.torso.yawAngle = cent->pe.head.yawAngle;
+ cent->pe.torso.pitchAngle = 0;
+ cent->pe.deadAnglesFrozen = qtrue;
+ cent->pe.deadAnglesClientNum = cent->currentState.clientNum;
+ VectorCopy( cent->lerpOrigin, cent->pe.deadAnglesOrigin );
+ }
+
+ headAngles[YAW] = cent->pe.head.yawAngle;
+ headAngles[PITCH] = cent->pe.head.pitchAngle;
+ legsAngles[YAW] = cent->pe.legs.yawAngle;
+ legsAngles[PITCH] = cent->pe.legs.pitchAngle;
+ torsoAngles[YAW] = cent->pe.torso.yawAngle;
+ torsoAngles[PITCH] = cent->pe.torso.pitchAngle;
+
+ // pull the angles back out of the hierarchial chain
+ AnglesSubtract( headAngles, torsoAngles, headAngles );
+ AnglesSubtract( torsoAngles, legsAngles, torsoAngles );
+ AnglesToAxis( legsAngles, legs );
+ AnglesToAxis( torsoAngles, torso );
+ AnglesToAxis( headAngles, head );
+ return;
+ } else if ( cent->pe.deadAnglesFrozen ) {
+ cent->pe.deadAnglesFrozen = qfalse;
+ }
+#endif
+
VectorCopy( cent->lerpAngles, headAngles );
headAngles[YAW] = AngleMod( headAngles[YAW] );
VectorClear( legsAngles );
@@ -1212,6 +1252,12 @@ static void CG_PlayerAngles( centity_t *cent, vec3_t legs[3], vec3_t torso[3], v
}
}
+ // BFPR - Head angles to avoid player model look deformed
+#if BFPR_DEAD_CAMERA_FREE_MOVE
+ cent->pe.head.yawAngle = headAngles[YAW];
+ cent->pe.head.pitchAngle = headAngles[PITCH];
+#endif
+
// pain twitch
CG_AddPainTwitch( cent, torsoAngles );
@@ -2535,6 +2581,16 @@ void CG_ResetPlayerEntity( centity_t *cent ) {
cent->pe.torso.pitchAngle = cent->rawAngles[PITCH];
cent->pe.torso.pitching = qfalse;
+ // BFPR - Reset head angles too, otherwise a freshly-spawned corpse entity
+#if BFPR_DEAD_CAMERA_FREE_MOVE
+ memset( ¢->pe.head, 0, sizeof( cent->pe.torso ) );
+ cent->pe.head.yawAngle = cent->rawAngles[YAW];
+ cent->pe.head.yawing = qfalse;
+ cent->pe.head.pitchAngle = 0;
+ cent->pe.head.pitching = qfalse;
+ cent->pe.deadAnglesFrozen = qfalse;
+#endif
+
if ( cg_debugPosition.integer ) {
CG_Printf("%i ResetPlayerEntity yaw=%i\n", cent->currentState.number, cent->pe.torso.yawAngle );
}
diff --git a/source/cgame/cg_view.c b/source/cgame/cg_view.c
index 3db0790..5bee870 100644
--- a/source/cgame/cg_view.c
+++ b/source/cgame/cg_view.c
@@ -258,12 +258,15 @@ static void CG_OffsetThirdPersonView( void ) {
VectorCopy( cg.refdefViewAngles, focusAngles );
+ // BFPR - Make the camera move freely when the player is dead
+#if !BFPR_DEAD_CAMERA_FREE_MOVE
// if dead, look at killer
if ( cg.predictedPlayerState.stats[STAT_HEALTH] <= 0 ) {
int totalYaw = cg.predictedPlayerState.damageYaw + cg.predictedPlayerState.damagePitch;
focusAngles[YAW] = totalYaw;
cg.refdefViewAngles[YAW] = totalYaw;
}
+#endif
AngleVectors( focusAngles, forward, NULL, NULL );
@@ -608,9 +611,7 @@ static int CG_CalcFov( void ) {
}
} else {
f = ( cg.time - cg.zoomTime ) / (float)ZOOM_TIME;
- if ( f > 1.0 ) {
- fov_x = fov_x;
- } else {
+ if ( f <= 1.0 ) {
fov_x = zoomFov + f * ( fov_x - zoomFov );
}
}
diff --git a/source/game/bg_lib.c b/source/game/bg_lib.c
index 1fccfd7..daf1f5a 100644
--- a/source/game/bg_lib.c
+++ b/source/game/bg_lib.c
@@ -67,10 +67,7 @@ static void swapfunc(char *, char *, int, int);
#define SWAPINIT(a, es) swaptype = ((char *)a - (char *)0) % sizeof(long) || \
es % sizeof(long) ? 2 : es == sizeof(long)? 0 : 1;
-static void
-swapfunc(a, b, n, swaptype)
- char *a, *b;
- int n, swaptype;
+static void swapfunc(char *a, char *b, int n, int swaptype)
{
if(swaptype <= 1)
swapcode(long, a, b, n)
@@ -88,21 +85,14 @@ swapfunc(a, b, n, swaptype)
#define vecswap(a, b, n) if ((n) > 0) swapfunc(a, b, n, swaptype)
-static char *
-med3(a, b, c, cmp)
- char *a, *b, *c;
- cmp_t *cmp;
+static char *med3(char *a, char *b, char *c, cmp_t *cmp)
{
return cmp(a, b) < 0 ?
(cmp(b, c) < 0 ? b : (cmp(a, c) < 0 ? c : a ))
:(cmp(b, c) > 0 ? b : (cmp(a, c) < 0 ? a : c ));
}
-void
-qsort(a, n, es, cmp)
- void *a;
- size_t n, es;
- cmp_t *cmp;
+void qsort(void *a, size_t n, size_t es, cmp_t *cmp)
{
char *pa, *pb, *pc, *pd, *pl, *pm, *pn;
int d, r, swaptype, swap_cnt;
diff --git a/source/game/bg_pmove.c b/source/game/bg_pmove.c
index 1f0c51e..b5d4d31 100644
--- a/source/game/bg_pmove.c
+++ b/source/game/bg_pmove.c
@@ -3078,9 +3078,12 @@ void PM_UpdateViewAngles( playerState_t *ps, const usercmd_t *cmd ) {
return; // no view changes at all
}
+ // BFPR - Make the camera move freely when the player is dead
+#if !BFPR_DEAD_CAMERA_FREE_MOVE
if ( ps->pm_type != PM_SPECTATOR && ps->stats[STAT_HEALTH] <= 0 ) {
return; // no view changes at all
}
+#endif
// circularly clamp the angles with deltas
for (i=0 ; i<3 ; i++) {
diff --git a/source/game/bg_public.h b/source/game/bg_public.h
index 4c41de8..c501543 100644
--- a/source/game/bg_public.h
+++ b/source/game/bg_public.h
@@ -237,6 +237,9 @@ typedef struct {
int (*pointcontents)( const vec3_t point, int passEntityNum );
} pmove_t;
+// BFPR - A macro to enable/disable the camera to make freely move while the player is dead
+#define BFPR_DEAD_CAMERA_FREE_MOVE 1
+
// if a full pmove isn't done on the client, you can just update the angles
void PM_UpdateViewAngles( playerState_t *ps, const usercmd_t *cmd );
void Pmove (pmove_t *pmove);
diff --git a/source/game/g_active.c b/source/game/g_active.c
index cb83bec..ee203ed 100644
--- a/source/game/g_active.c
+++ b/source/game/g_active.c
@@ -1351,11 +1351,11 @@ static void Client_Weapon( gentity_t *ent, usercmd_t *ucmd, pmove_t *pm ) { // B
}
if ( wpCfg->attackType == ATK_HITSCAN ) {
if ( ucmd->buttons & BUTTON_ATTACK ) {
- Client_KiConsumption( client, weaponTime, kiCost );
if ( client->ps.stats[STAT_KI] >= kiCost ) {
client->ps.eFlags |= EF_FIRING;
BG_AddPredictableEventToPlayerstate( EV_FIRE_WEAPON, 0, &ent->client->ps, -1 );
}
+ Client_KiConsumption( client, weaponTime, kiCost );
client->ps.weaponstate = WEAPON_READY;
} else {
client->ps.weaponstate = WEAPON_READY;
diff --git a/source/game/g_client.c b/source/game/g_client.c
index 4227702..5cfedc9 100644
--- a/source/game/g_client.c
+++ b/source/game/g_client.c
@@ -415,6 +415,15 @@ void CopyToBodyQue( gentity_t *ent ) {
body->timestamp = level.time;
body->physicsObject = qtrue;
body->physicsBounce = 0; // don't bounce
+
+ // BFPR - Use the angles frozen at the moment of death
+#if BFPR_DEAD_CAMERA_FREE_MOVE
+ if ( ent->client->hasDeathAngles ) {
+ VectorCopy( ent->client->deathAngles, body->s.angles );
+ VectorCopy( ent->client->deathAngles, body->s.apos.trBase );
+ }
+#endif
+
if ( body->s.groundEntityNum == ENTITYNUM_NONE ) {
body->s.pos.trType = TR_GRAVITY;
body->s.pos.trTime = level.time;
@@ -662,6 +671,10 @@ qboolean ClientUserinfoChanged( int clientNum ) {
client->pers.localClient = qtrue;
}
+ // BFPR - Resolve and persist the client's GUID
+ s = Info_ValueForKey( userinfo, "cl_guid" );
+ Q_strncpyz( client->pers.guid, s, sizeof(client->pers.guid) );
+
// check the item prediction
s = Info_ValueForKey( userinfo, "cg_predictItems" );
if ( !atoi( s ) ) {
@@ -1501,6 +1514,11 @@ void ClientSpawn(gentity_t *ent) {
// health will count down towards max_health
ent->health = client->ps.stats[STAT_HEALTH] = client->ps.stats[STAT_MAX_HEALTH]; // BFP - Before Q3: + 25
+ // BFPR - Player is alive again; let the next death capture a fresh yaw
+#if BFPR_DEAD_CAMERA_FREE_MOVE
+ client->hasDeathAngles = qfalse;
+#endif
+
// BFP - Monster gamemode
if ( g_gametype.integer == GT_MONSTER && client->ps.clientNum == level.monsterClientNum
&& ent->client->sess.sessionTeam != TEAM_SPECTATOR ) {
diff --git a/source/game/g_cmds.c b/source/game/g_cmds.c
index 4d02aee..aefb894 100644
--- a/source/game/g_cmds.c
+++ b/source/game/g_cmds.c
@@ -237,6 +237,154 @@ int ClientNumberFromString( gentity_t *to, char *s ) {
return -1;
}
+/*
+=================
+Cmd_Ignore_f
+=================
+*/
+static void Cmd_Ignore_f( gentity_t *ent ) { // BFPR - ignore command
+ int targetNum;
+ char arg[MAX_TOKEN_CHARS];
+
+ if ( trap_Argc() != 2 ) {
+ trap_SendServerCommand( ent-g_entities, "print \"usage: ignore \n\"" );
+ return;
+ }
+
+ trap_Argv( 1, arg, sizeof( arg ) );
+ targetNum = ClientNumberFromString( ent, arg );
+ if ( targetNum == -1 ) {
+ return;
+ }
+
+ if ( targetNum == ent->client - level.clients ) {
+ trap_SendServerCommand( ent-g_entities, "print \"You cannot ignore yourself.\n\"" );
+ return;
+ }
+
+ ent->client->pers.ignoredClients[targetNum] = qtrue;
+ trap_SendServerCommand( ent-g_entities, va( "print \"Ignoring %s\n\"",
+ level.clients[targetNum].pers.netname ) );
+}
+
+/*
+=================
+Cmd_Unignore_f
+=================
+*/
+static void Cmd_Unignore_f( gentity_t *ent ) { // BFPR - unignore command
+ int targetNum;
+ char arg[MAX_TOKEN_CHARS];
+
+ if ( trap_Argc() != 2 ) {
+ trap_SendServerCommand( ent-g_entities, "print \"usage: unignore \n\"" );
+ return;
+ }
+
+ trap_Argv( 1, arg, sizeof( arg ) );
+ targetNum = ClientNumberFromString( ent, arg );
+ if ( targetNum == -1 ) {
+ return;
+ }
+
+ ent->client->pers.ignoredClients[targetNum] = qfalse;
+ trap_SendServerCommand( ent-g_entities, va( "print \"No longer ignoring %s\n\"",
+ level.clients[targetNum].pers.netname ) );
+}
+
+/*
+=================
+Cmd_ClearIgnores_f
+=================
+*/
+static void Cmd_ClearIgnores_f( gentity_t *ent ) { // BFPR - clear_ignores command
+ memset( ent->client->pers.ignoredClients, 0, sizeof( ent->client->pers.ignoredClients ) );
+ trap_SendServerCommand( ent-g_entities, "print \"Cleared all ignores.\n\"" );
+}
+
+/*
+=================
+Cmd_PlayerList_f
+=================
+*/
+static void Cmd_PlayerList_f( gentity_t *ent ) { // BFPR - playerlist command (client, no IPs)
+ int i;
+ gclient_t *cl;
+ char list[MAX_STRING_CHARS];
+ char line[128];
+ char userinfo[MAX_INFO_STRING];
+ char *skillStr;
+ char skillDisp[8];
+ qboolean any = qfalse, hasBots = qfalse;
+ qboolean isBot;
+
+ // only show the bot/skill columns if at least one bot is connected
+ for ( i = 0, cl = level.clients ; i < level.maxclients ; i++, cl++ ) {
+ if ( cl->pers.connected == CON_DISCONNECTED ) {
+ continue;
+ }
+ if ( g_entities[i].r.svFlags & SVF_BOT ) {
+ hasBots = qtrue;
+ break;
+ }
+ }
+
+ if ( hasBots ) {
+ Q_strncpyz( list, "id name bot skill team\n"
+ "--- -------------------------------- --- ----- ----------\n", sizeof( list ) );
+ } else {
+ Q_strncpyz( list, "id name team\n"
+ "--- -------------------------------- ----------\n", sizeof( list ) );
+ }
+
+ for ( i = 0, cl = level.clients ; i < level.maxclients ; i++, cl++ ) {
+ if ( cl->pers.connected == CON_DISCONNECTED ) {
+ continue;
+ }
+ any = qtrue;
+
+ isBot = ( g_entities[i].r.svFlags & SVF_BOT );
+
+ if ( hasBots ) {
+ if ( isBot ) {
+ trap_GetUserinfo( i, userinfo, sizeof( userinfo ) );
+ skillStr = Info_ValueForKey( userinfo, "skill" );
+ if ( !skillStr[0] ) {
+ skillStr = "?";
+ } else { // strip the float string
+ Com_sprintf( skillDisp, sizeof( skillDisp ), "%i", (int)( atof( skillStr ) + 0.5f ) );
+ skillStr = skillDisp;
+ }
+ } else {
+ skillStr = "-";
+ }
+
+ Com_sprintf( line, sizeof( line ), "%-3i %-32s %-3s %-5s %s\n", i, cl->pers.netname,
+ isBot ? "^3yes^7" : "no", skillStr,
+ cl->sess.sessionTeam == TEAM_SPECTATOR ? "spectator" :
+ cl->sess.sessionTeam == TEAM_RED ? "red" :
+ cl->sess.sessionTeam == TEAM_BLUE ? "blue" : "free" );
+ } else {
+ Com_sprintf( line, sizeof( line ), "%-3i %-32s %s\n", i, cl->pers.netname,
+ cl->sess.sessionTeam == TEAM_SPECTATOR ? "spectator" :
+ cl->sess.sessionTeam == TEAM_RED ? "red" :
+ cl->sess.sessionTeam == TEAM_BLUE ? "blue" : "free" );
+ }
+
+ if ( strlen( list ) + strlen( line ) >= sizeof( list ) ) {
+ break; // don't overflow MAX_STRING_CHARS
+ }
+ Q_strcat( list, sizeof( list ), line );
+ }
+
+ if ( !any ) {
+ Q_strncpyz( list, "No players connected.\n", sizeof( list ) );
+ }
+
+ trap_SendServerCommand( ent-g_entities, va( "print \"%s\"", list ) );
+}
+
+
/*
==================
Cmd_Give_f
@@ -609,6 +757,11 @@ void SetTeam( gentity_t *ent, char *s ) {
team = TEAM_SPECTATOR;
}
+ // BFPR - Play-banned players can only spectate
+ if ( team != TEAM_SPECTATOR && G_IsSenderPlaybanned( ent ) ) {
+ team = TEAM_SPECTATOR;
+ }
+
// BFP - Monster gamemode, check if the player monster is changing teams
if ( g_gametype.integer == GT_MONSTER
&& ( ent->client->ps.eFlags & EF_MONSTER )
@@ -730,6 +883,8 @@ Cmd_Team_f
void Cmd_Team_f( gentity_t *ent ) {
int oldTeam;
char s[MAX_TOKEN_CHARS];
+ // BFPR - Ban message display
+ char banMsg[96];
if ( trap_Argc() != 2 ) {
oldTeam = ent->client->sess.sessionTeam;
@@ -795,6 +950,21 @@ void Cmd_Team_f( gentity_t *ent ) {
}
}
+ // BFPR - Play-banned players can only spectate
+ if ( Q_stricmp( s, "spectator" ) && Q_stricmp( s, "s" )
+ && Q_stricmp( s, "scoreboard" ) && Q_stricmp( s, "score" )
+ && G_IsSenderPlaybanned( ent ) ) {
+ trap_SendServerCommand( ent-g_entities,
+ va( "cp \"^1You are banned from \n^1playing on this server.\n%s\n\"",
+ G_BanMessageForSender( qtrue, ent, &g_playban_list, banMsg, sizeof(banMsg) ) ? banMsg : "" )
+ );
+ trap_SendServerCommand( ent-g_entities,
+ va( "print \"You are banned from playing on this server. %s\n\"",
+ G_BanMessageForSender( qfalse, ent, &g_playban_list, banMsg, sizeof(banMsg) ) ? banMsg : "" )
+ );
+ return;
+ }
+
// BFP - Team Last Man Standing, show a centerprint message to switching teams when the player were fragged and forced to spectate
if ( g_gametype.integer == GT_TLMS
&& ent->client->forceToSpectate ) {
@@ -954,6 +1124,10 @@ static void G_SayTo( gentity_t *ent, gentity_t *other, int mode, int color, cons
if ( other->client->pers.connected != CON_CONNECTED ) {
return;
}
+ // BFPR - Ignore client in ignore list
+ if ( other->client->pers.ignoredClients[ ent - g_entities ] ) {
+ return;
+ }
if ( mode == SAY_TEAM && !OnSameTeam(ent, other) ) {
return;
}
@@ -982,6 +1156,8 @@ void G_Say( gentity_t *ent, gentity_t *target, int mode, const char *chatText )
// don't let text be too long for malicious reasons
char text[MAX_SAY_TEXT];
char location[64];
+ // BFPR - Ban message display
+ char banMsg[96];
// BFP - Allow spectator chat
if ( ent->client->ps.pm_type == PM_SPECTATOR
@@ -989,6 +1165,19 @@ void G_Say( gentity_t *ent, gentity_t *target, int mode, const char *chatText )
return;
}
+ // BFPR - Muted players cannot send chat
+ if ( G_IsSenderMuted( ent ) ) {
+ trap_SendServerCommand( ent-g_entities,
+ va( "cp \"^1You are muted, \n^1because you are banned on this server.\n%s\n\"",
+ G_BanMessageForSender( qtrue, ent, &g_muteban_list, banMsg, sizeof(banMsg) ) ? banMsg : "" )
+ );
+ trap_SendServerCommand( ent-g_entities,
+ va( "print \"You are muted, because you are banned on this server. %s\n\"",
+ G_BanMessageForSender( qfalse, ent, &g_muteban_list, banMsg, sizeof(banMsg) ) ? banMsg : "" )
+ );
+ return;
+ }
+
if ( g_gametype.integer < GT_TEAM && mode == SAY_TEAM ) {
mode = SAY_ALL;
}
@@ -1116,6 +1305,14 @@ static void G_VoiceTo( gentity_t *ent, gentity_t *other, int mode, const char *i
if (!other->client) {
return;
}
+ // BFPR - Muted players cannot send voice chat
+ if ( G_IsSenderMuted( ent ) ) {
+ return;
+ }
+ // BFPR - Ignore client in ignore list
+ if ( other->client->pers.ignoredClients[ ent - g_entities ] ) {
+ return;
+ }
if ( mode == SAY_TEAM && !OnSameTeam(ent, other) ) {
return;
}
@@ -1363,6 +1560,7 @@ void Cmd_CallVote_f( gentity_t *ent ) {
int i;
char arg1[MAX_STRING_TOKENS];
char arg2[MAX_STRING_TOKENS];
+ char banMsg[96];
if ( !g_allowVote.integer ) {
trap_SendServerCommand( ent-g_entities, "print \"Voting not allowed here.\n\"" );
@@ -1454,6 +1652,19 @@ void Cmd_CallVote_f( gentity_t *ent ) {
Com_sprintf( level.voteDisplayString, sizeof( level.voteDisplayString ), "%s", level.voteString );
}
+ // BFPR - Vote-banned players cannot call votes
+ if ( G_IsSenderVotebanned( ent ) ) {
+ trap_SendServerCommand( ent-g_entities,
+ va( "cp \"^1You are banned from \n^1calling votes on this server.\n%s\n\"",
+ G_BanMessageForSender( qtrue, ent, &g_voteban_list, banMsg, sizeof(banMsg) ) ? banMsg : "" )
+ );
+ trap_SendServerCommand( ent-g_entities,
+ va( "print \"You are banned from calling votes on this server. %s\n\"",
+ G_BanMessageForSender( qfalse, ent, &g_voteban_list, banMsg, sizeof(banMsg) ) ? banMsg : "" )
+ );
+ return;
+ }
+
trap_SendServerCommand( -1, va("print \"%s called a vote.\n\"", ent->client->pers.netname ) );
// start the voting, the caller autoamtically votes yes
@@ -1479,6 +1690,7 @@ Cmd_Vote_f
*/
void Cmd_Vote_f( gentity_t *ent ) {
char msg[64];
+ char banMsg[96];
if ( !level.voteTime ) {
trap_SendServerCommand( ent-g_entities, "print \"No vote in progress.\n\"" );
@@ -1492,6 +1704,18 @@ void Cmd_Vote_f( gentity_t *ent ) {
trap_SendServerCommand( ent-g_entities, "print \"Not allowed to vote as spectator.\n\"" );
return;
}
+ // BFPR - votebanned players cannot cast votes
+ if ( G_IsSenderVotebanned( ent ) ) {
+ trap_SendServerCommand( ent-g_entities,
+ va( "cp \"^1You are banned from \n^1voting on this server.\n%s\n\"",
+ G_BanMessageForSender( qtrue, ent, &g_voteban_list, banMsg, sizeof(banMsg) ) ? banMsg : "" )
+ );
+ trap_SendServerCommand( ent-g_entities,
+ va( "print \"You are banned from voting on this server. %s\n\"",
+ G_BanMessageForSender( qfalse, ent, &g_voteban_list, banMsg, sizeof(banMsg) ) ? banMsg : "" )
+ );
+ return;
+ }
trap_SendServerCommand( ent-g_entities, "print \"Vote cast.\n\"" );
@@ -1775,26 +1999,6 @@ static void Cmd_BFP_KiUseToggle_f( gentity_t* ent ) { // BFP - Ki use toggle
}
}
-/*
-=====================
-Cmd_BFP_SelectCharacter_f
-=====================
-*/
-static void Cmd_BFP_SelectCharacter_f( gentity_t* ent ) { // BFP - Select character
- char characterselected[MAX_TOKEN_CHARS];
-
- // BFP - NOTE: That command was left without finishing the implementation to change the character of this way
- // What could it be?
-
- if ( trap_Argc() != 2 ) {
- return;
- }
- trap_Argv( 1, characterselected, sizeof( characterselected ) );
-
- // that prints the info in the server
- G_Printf( "Character: %s\n", characterselected );
-}
-
/*
=================
Cmd_BFP_Block_f
@@ -1892,6 +2096,14 @@ void ClientCommand( int clientNum ) {
Cmd_TeamTask_f (ent);
else if (Q_stricmp (cmd, "levelshot") == 0)
Cmd_LevelShot_f (ent);
+ else if (Q_stricmp (cmd, "ignore") == 0) // BFPR - ignore command
+ Cmd_Ignore_f( ent );
+ else if (Q_stricmp (cmd, "unignore") == 0) // BFPR - unignore command
+ Cmd_Unignore_f( ent );
+ else if (Q_stricmp (cmd, "clear_ignores") == 0) // BFPR - clear_ignores command
+ Cmd_ClearIgnores_f( ent );
+ else if (Q_stricmp (cmd, "playerlist") == 0) // BFPR - playerlist command (client, no IPs)
+ Cmd_PlayerList_f( ent );
else if (Q_stricmp (cmd, "follow") == 0)
Cmd_Follow_f (ent);
else if (Q_stricmp (cmd, "follownext") == 0)
@@ -1920,8 +2132,6 @@ void ClientCommand( int clientNum ) {
Cmd_BFP_Fly_f( ent );
else if (Q_stricmp (cmd, "kiusetoggle") == 0) // BFP - Ki use toggle
Cmd_BFP_KiUseToggle_f( ent );
- else if (Q_stricmp (cmd, "selectcharacter") == 0) // BFP - Select character
- Cmd_BFP_SelectCharacter_f( ent );
else if (Q_stricmp (cmd, "block") == 0) // BFP - Block
Cmd_BFP_Block_f( ent );
else
diff --git a/source/game/g_combat.c b/source/game/g_combat.c
index 59f15b6..b219dd4 100644
--- a/source/game/g_combat.c
+++ b/source/game/g_combat.c
@@ -884,6 +884,12 @@ void player_die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int
VectorCopy( self->s.angles, self->client->ps.viewangles );
+ // BFPR - Snapshot the real death angles
+#if BFPR_DEAD_CAMERA_FREE_MOVE
+ VectorCopy( self->s.angles, self->client->deathAngles );
+ self->client->hasDeathAngles = qtrue;
+#endif
+
self->s.loopSound = 0;
self->r.maxs[2] = -8;
@@ -1208,9 +1214,10 @@ void G_Damage( gentity_t *targ, gentity_t *inflictor, gentity_t *attacker,
float mass;
mass = 200;
- // BFP - Using hitscan weapons, the mass is different
+ // BFP - Using hitscan weapons without rail trail and splash damage, the mass is different
if ( targ->client != attacker->client
- && inflictor && inflictor->bfpWeapon && inflictor->bfpWeapon->attackType == ATK_HITSCAN ) {
+ && inflictor && inflictor->bfpWeapon && inflictor->bfpWeapon->attackType == ATK_HITSCAN
+ && inflictor->splashDamage <= 0 && !inflictor->bfpWeapon->railTrail ) {
mass = 50;
}
diff --git a/source/game/g_cvar.h b/source/game/g_cvar.h
index cecaeea..a62df12 100644
--- a/source/game/g_cvar.h
+++ b/source/game/g_cvar.h
@@ -98,6 +98,9 @@ G_CVAR( g_podiumDrop, "g_podiumDrop", "70", 0, 0, qfalse )
G_CVAR( g_allowVote, "g_allowVote", "1", CVAR_ARCHIVE, 0, qfalse )
G_CVAR( g_listEntity, "g_listEntity", "0", 0, 0, qfalse )
+G_CVAR( g_muteban_list, "g_muteban_list", "", CVAR_ARCHIVE, 0, qfalse ) // BFPR - Mute ban list
+G_CVAR( g_playban_list, "g_playban_list", "", CVAR_ARCHIVE, 0, qfalse ) // BFPR - Play ban list
+G_CVAR( g_voteban_list, "g_voteban_list", "", CVAR_ARCHIVE, 0, qfalse ) // BFPR - Vote ban list
G_CVAR( g_smoothClients, "g_smoothClients", "1", 0, 0, qfalse )
G_CVAR( pmove_fixed, "pmove_fixed", "0", CVAR_SYSTEMINFO, 0, qfalse )
diff --git a/source/game/g_local.h b/source/game/g_local.h
index fe7b690..6fdafe2 100644
--- a/source/game/g_local.h
+++ b/source/game/g_local.h
@@ -290,6 +290,8 @@ typedef struct {
int voteCount; // to prevent people from constantly calling votes
int teamVoteCount; // to prevent people from constantly calling votes
qboolean teamInfo; // send team overlay updates?
+ qboolean ignoredClients[MAX_CLIENTS]; // BFPR - Clients this player is ignoring (chat only)
+ char guid[33]; // BFPR - Client's cl_guid
} clientPersistant_t;
// BFP - A macro to enable/disable switch team time delay
@@ -390,6 +392,12 @@ struct gclient_s {
int zanzokenDelay;
int zanzokenLastUsed;
+ // BFPR - Angles frozen at the exact moment of death
+#if BFPR_DEAD_CAMERA_FREE_MOVE
+ vec3_t deathAngles;
+ qboolean hasDeathAngles;
+#endif
+
char *areabits;
};
@@ -514,6 +522,7 @@ char *G_NewString( const char *string );
// g_cmds.c
//
void Cmd_Score_f (gentity_t *ent);
+char *ConcatArgs( int start );
void StopFollowing( gentity_t *ent );
void BroadcastTeamChange( gclient_t *client, int oldTeam );
void SetTeam( gentity_t *ent, char *s );
@@ -712,6 +721,10 @@ qboolean SpotWouldTelefrag( gentity_t *spot );
qboolean ConsoleCommand( void );
void G_ProcessIPBans(void);
qboolean G_FilterPacket (char *from);
+qboolean G_IsSenderMuted( gentity_t *ent ); // BFPR - Mute the sender
+qboolean G_IsSenderPlaybanned( gentity_t *ent ); // BFPR - Play-ban the sender
+qboolean G_IsSenderVotebanned( gentity_t *ent ); // BFPR - Vote-ban the sender
+qboolean G_BanMessageForSender( qboolean cp, gentity_t *ent, vmCvar_t *list, char *out, int outSize ); // BFPR - Format ban expiration and reason for a client
//
// g_weapon.c
@@ -885,6 +898,8 @@ qboolean trap_GetEntityToken( char *buffer, int bufferSize );
int trap_DebugPolygonCreate(int color, int numPoints, vec3_t *points);
void trap_DebugPolygonDelete(int id);
+int trap_RealTime( qtime_t *qtime ); // BFPR - Real (wall-clock) time in seconds since epoch, for ban expirations
+
int trap_BotLibSetup( void );
int trap_BotLibShutdown( void );
int trap_BotLibVarSet(char *var_name, char *value);
diff --git a/source/game/g_session.c b/source/game/g_session.c
index f76d0ed..26db118 100644
--- a/source/game/g_session.c
+++ b/source/game/g_session.c
@@ -93,6 +93,13 @@ void G_ReadClientSessionData( gclient_t *client ) {
if ( (unsigned)client->sess.sessionTeam >= TEAM_NUM_TEAMS ) {
client->sess.sessionTeam = TEAM_SPECTATOR;
}
+
+ // BFPR - Play-banned players mustn't be restored to an active team from
+ // a session saved before the ban was applied (or while disconnected)
+ if ( client->sess.sessionTeam != TEAM_SPECTATOR
+ && G_IsSenderPlaybanned( &g_entities[ client - level.clients ] ) ) {
+ client->sess.sessionTeam = TEAM_SPECTATOR;
+ }
}
@@ -105,12 +112,16 @@ Called on a first-time connect
*/
void G_InitSessionData( gclient_t *client, const char *team, qboolean isBot ) {
clientSession_t *sess;
+ // BFPR - Check for play-banned players
+ qboolean playbanned = !isBot && G_IsSenderPlaybanned( &g_entities[ client - level.clients ] );
sess = &client->sess;
// initial team determination
if ( g_gametype.integer >= GT_TEAM ) {
- if ( team[0] == 's' || team[0] == 'S' ) {
+ if ( playbanned ) { // BFPR - Force to spectate while play-banned
+ sess->sessionTeam = TEAM_SPECTATOR;
+ } else if ( team[0] == 's' || team[0] == 'S' ) {
// a willing spectator, not a waiting-in-line
sess->sessionTeam = TEAM_SPECTATOR;
} else {
@@ -136,7 +147,9 @@ void G_InitSessionData( gclient_t *client, const char *team, qboolean isBot ) {
// BFP - Team Last Man Standing, keep selected team
client->selectedTeam = sess->sessionTeam;
} else {
- if ( team[0] == 's' || team[0] == 'S' ) {
+ if ( playbanned ) { // BFPR - Force to spectate while play-banned
+ sess->sessionTeam = TEAM_SPECTATOR;
+ } else if ( team[0] == 's' || team[0] == 'S' ) {
// a willing spectator, not a waiting-in-line
sess->sessionTeam = TEAM_SPECTATOR;
} else {
diff --git a/source/game/g_svcmds.c b/source/game/g_svcmds.c
index 4b0370b..ebf3f65 100644
--- a/source/game/g_svcmds.c
+++ b/source/game/g_svcmds.c
@@ -25,6 +25,22 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#include "g_local.h"
+// BFPR - Max length of a ban reason string stored in a ban-list entry
+#define MAX_ADMIN_BAN_REASON_LEN 64
+
+/*
+==================
+G_RealTimeSeconds
+
+Wall-clock time in seconds since epoch, via trap_RealTime.
+Used for ban expirations
+==================
+*/
+static int G_RealTimeSeconds( void ) { // BFPR - Wall-clock time for ban expirations
+ qtime_t qt;
+ return trap_RealTime( &qt );
+}
+
/*
==============================================================================
@@ -256,6 +272,587 @@ void G_ProcessIPBans(void)
}
+/*
+==============================================================================
+
+BFPR BAN FEATURES
+
+==============================================================================
+*/
+
+/*
+==================
+G_BanListParseEntry
+
+Splits a single "ip|guid|expires|reason" list entry into its parts
+==================
+*/
+static void G_BanListParseEntry( char *buf, char **ip, char **guid, int *expires, char **reason ) { // BFPR - Parse ban entry
+ char *p;
+
+ *ip = buf;
+ *guid = "";
+ *expires = 0;
+ *reason = "";
+
+ p = strchr( buf, '|' );
+ if ( !p ) {
+ return;
+ }
+ *p = '\0';
+ *guid = p + 1;
+
+ p = strchr( *guid, '|' );
+ if ( !p ) {
+ return;
+ }
+ *p = '\0';
+ *expires = atoi( p + 1 );
+
+ p = strchr( p + 1, '|' );
+ if ( !p ) {
+ return;
+ }
+ *p = '\0';
+ *reason = p + 1;
+}
+
+/*
+==================
+G_BanListIsMatch
+
+Checks a single "ip|guid|expires|reason" list entry against
+the IP/GUID being tested. An expired entry never matches
+==================
+*/
+static qboolean G_BanListIsMatch( const char *entry, const char *ip, const char *guid ) { // BFPR - Ban entry match
+ char buf[256];
+ char *entryIp, *entryGuid, *entryReason;
+ int entryExpires;
+
+ Q_strncpyz( buf, entry, sizeof( buf ) );
+ G_BanListParseEntry( buf, &entryIp, &entryGuid, &entryExpires, &entryReason );
+
+ if ( entryExpires != 0 && entryExpires <= G_RealTimeSeconds() ) {
+ return qfalse; // expired
+ }
+
+ if ( ip && ip[0] && entryIp[0] && !Q_stricmp( entryIp, ip ) ) {
+ return qtrue;
+ }
+ if ( guid && guid[0] && entryGuid[0] && !Q_stricmp( entryGuid, guid ) ) {
+ return qtrue;
+ }
+ return qfalse;
+}
+
+/*
+==================
+G_BanListContains
+
+Checks whether IP or GUID matches any non-expired entry of the given ban-list cvar
+==================
+*/
+static qboolean G_BanListContains( vmCvar_t *list, const char *ip, const char *guid ) { // BFPR - Check IP/GUID in ban list
+ char buf[MAX_CVAR_VALUE_STRING];
+ char *token, *p;
+
+ if ( !list->string[0] ) {
+ return qfalse;
+ }
+
+ Q_strncpyz( buf, list->string, sizeof( buf ) );
+ p = buf;
+ while ( ( token = strchr( p, ';' ) ) ) {
+ *token = '\0';
+ if ( p[0] && G_BanListIsMatch( p, ip, guid ) ) {
+ return qtrue;
+ }
+ p = token + 1;
+ }
+ if ( p[0] && G_BanListIsMatch( p, ip, guid ) ) {
+ return qtrue;
+ }
+ return qfalse;
+}
+
+/*
+==================
+G_BanListFind
+==================
+*/
+static qboolean G_BanListFind( vmCvar_t *list, const char *ip, const char *guid, int *expiresOut, char *reasonOut, int reasonOutSize ) { // BFPR - Find matching ban entry
+ char buf[MAX_CVAR_VALUE_STRING];
+ char parseBuf[256];
+ char *token, *p;
+ char *entryIp, *entryGuid, *entryReason;
+ int entryExpires;
+
+ if ( !list->string[0] ) {
+ return qfalse;
+ }
+
+ Q_strncpyz( buf, list->string, sizeof( buf ) );
+ p = buf;
+ while ( ( token = strchr( p, ';' ) ) ) {
+ *token = '\0';
+ if ( p[0] && G_BanListIsMatch( p, ip, guid ) ) {
+ Q_strncpyz( parseBuf, p, sizeof( parseBuf ) );
+ G_BanListParseEntry( parseBuf, &entryIp, &entryGuid, &entryExpires, &entryReason );
+ if ( expiresOut ) {
+ *expiresOut = entryExpires;
+ }
+ if ( reasonOut ) {
+ Q_strncpyz( reasonOut, entryReason[0] ? entryReason : "No_reason_given", reasonOutSize );
+ }
+ return qtrue;
+ }
+ p = token + 1;
+ }
+ if ( p[0] && G_BanListIsMatch( p, ip, guid ) ) {
+ Q_strncpyz( parseBuf, p, sizeof( parseBuf ) );
+ G_BanListParseEntry( parseBuf, &entryIp, &entryGuid, &entryExpires, &entryReason );
+ if ( expiresOut ) {
+ *expiresOut = entryExpires;
+ }
+ if ( reasonOut ) {
+ Q_strncpyz( reasonOut, entryReason[0] ? entryReason : "No_reason_given", reasonOutSize );
+ }
+ return qtrue;
+ }
+ return qfalse;
+}
+
+/*
+==================
+G_BanListPrune
+
+Rebuilds the given ban-list cvar dropping any expired entry
+==================
+*/
+static void G_BanListPrune( vmCvar_t *list, const char *cvarName ) { // BFPR - Drop expired ban entries
+ char buf[MAX_CVAR_VALUE_STRING];
+ char newList[MAX_CVAR_VALUE_STRING];
+ char parseBuf[256];
+ char *token, *p;
+ char *entryIp, *entryGuid, *entryReason;
+ int entryExpires;
+ qboolean first = qtrue;
+
+ if ( !list->string[0] ) {
+ return;
+ }
+
+ Q_strncpyz( buf, list->string, sizeof( buf ) );
+ newList[0] = '\0';
+ p = buf;
+ while ( ( token = strchr( p, ';' ) ) ) {
+ *token = '\0';
+ if ( p[0] ) {
+ Q_strncpyz( parseBuf, p, sizeof( parseBuf ) );
+ G_BanListParseEntry( parseBuf, &entryIp, &entryGuid, &entryExpires, &entryReason );
+ if ( !( entryExpires != 0 && entryExpires <= G_RealTimeSeconds() ) ) {
+ if ( !first ) {
+ Q_strcat( newList, sizeof( newList ), ";" );
+ }
+ Q_strcat( newList, sizeof( newList ), p );
+ first = qfalse;
+ }
+ }
+ p = token + 1;
+ }
+
+ if ( p[0] ) {
+ Q_strncpyz( parseBuf, p, sizeof( parseBuf ) );
+ G_BanListParseEntry( parseBuf, &entryIp, &entryGuid, &entryExpires, &entryReason );
+ if ( !( entryExpires != 0 && entryExpires <= G_RealTimeSeconds() ) ) {
+ if ( !first ) {
+ Q_strcat( newList, sizeof( newList ), ";" );
+ }
+ Q_strcat( newList, sizeof( newList ), p );
+ }
+ }
+ trap_Cvar_Set( cvarName, newList );
+ trap_Cvar_Update( list );
+}
+
+/*
+==================
+G_BanListAdd
+
+Adds an "ip|guid|expires|reason" entry to the given ban-list cvar, unless
+a non-expired entry already matches this IP or GUID.
+expires is a Unix timestamp (0 = permanent)
+==================
+*/
+static void G_BanListAdd( vmCvar_t *list, const char *cvarName, const char *ip, const char *guid, int expires, const char *reason ) { // BFPR - Add entry to ban list
+ char newList[MAX_CVAR_VALUE_STRING];
+ char entry[256];
+ char cleanReason[MAX_ADMIN_BAN_REASON_LEN];
+ int i;
+
+ G_BanListPrune( list, cvarName ); // sweep expired entries before checking/adding
+
+ if ( G_BanListContains( list, ip, guid ) ) {
+ return; // already banned by this IP or GUID
+ }
+
+ if ( !reason || !reason[0] ) {
+ reason = "No reason given";
+ }
+ Q_strncpyz( cleanReason, reason, sizeof( cleanReason ) );
+ for ( i = 0; cleanReason[i]; i++ ) {
+ if ( cleanReason[i] == '|' || cleanReason[i] == ';'
+ || cleanReason[i] == ' ' || cleanReason[i] == '\t' || cleanReason[i] == '\n' ) {
+ cleanReason[i] = '_';
+ }
+ }
+
+ Com_sprintf( entry, sizeof( entry ), "%s|%s|%i|%s", ip ? ip : "", guid ? guid : "", expires, cleanReason );
+
+ if ( list->string[0] ) {
+ Com_sprintf( newList, sizeof( newList ), "%s;%s", list->string, entry );
+ } else {
+ Q_strncpyz( newList, entry, sizeof( newList ) );
+ }
+ trap_Cvar_Set( cvarName, newList );
+ trap_Cvar_Update( list );
+}
+
+/*
+==================
+G_BanListRemove
+
+Removes every entry matching this IP or GUID from the given ban-list cvar
+==================
+*/
+static void G_BanListRemove( vmCvar_t *list, const char *cvarName, const char *ip, const char *guid ) { // BFPR - Remove IP/GUID from ban list
+ char buf[MAX_CVAR_VALUE_STRING], newList[MAX_CVAR_VALUE_STRING];
+ char *token, *p;
+ qboolean first = qtrue;
+
+ if ( !list->string[0] ) {
+ return;
+ }
+
+ Q_strncpyz( buf, list->string, sizeof( buf ) );
+ newList[0] = '\0';
+ p = buf;
+ while ( ( token = strchr( p, ';' ) ) ) {
+ *token = '\0';
+ if ( p[0] && !G_BanListIsMatch( p, ip, guid ) ) {
+ if ( !first ) Q_strcat( newList, sizeof( newList ), ";" );
+ Q_strcat( newList, sizeof( newList ), p );
+ first = qfalse;
+ }
+ p = token + 1;
+ }
+ if ( p[0] && !G_BanListIsMatch( p, ip, guid ) ) {
+ if ( !first ) Q_strcat( newList, sizeof( newList ), ";" );
+ Q_strcat( newList, sizeof( newList ), p );
+ }
+ trap_Cvar_Set( cvarName, newList );
+ trap_Cvar_Update( list );
+}
+
+/*
+==================
+G_BanListRemoveByIndex
+
+Remove the entry at position 'index' (1‑based) from the ban-list cvar
+==================
+*/
+static void G_BanListRemoveByIndex( vmCvar_t *list, const char *cvarName, int index ) { // BFPR - Remove ban from ban list index
+ char buf[MAX_CVAR_VALUE_STRING], newList[MAX_CVAR_VALUE_STRING];
+ char *p, *token;
+ int currentIndex = 0;
+ qboolean first = qtrue;
+
+ if ( !list->string[0] ) {
+ G_Printf( "This ban list is empty\n" );
+ return;
+ }
+
+ Q_strncpyz( buf, list->string, sizeof(buf) );
+ newList[0] = '\0';
+ p = buf;
+
+ while ( ( token = strchr( p, ';' ) ) ) {
+ *token = '\0';
+ if ( p[0] ) {
+ currentIndex++;
+ if ( currentIndex != index ) {
+ if ( !first ) {
+ Q_strcat( newList, sizeof(newList), ";" );
+ }
+ Q_strcat( newList, sizeof(newList), p );
+ first = qfalse;
+ }
+ }
+ p = token + 1;
+ }
+
+ if ( p[0] ) {
+ currentIndex++;
+ if ( currentIndex != index ) {
+ if ( !first ) {
+ Q_strcat( newList, sizeof(newList), ";" );
+ }
+ Q_strcat( newList, sizeof(newList), p );
+ }
+ }
+
+ if ( currentIndex < index ) {
+ G_Printf( "Index %d not found in list\n", index );
+ return;
+ }
+
+ trap_Cvar_Set( cvarName, newList );
+ trap_Cvar_Update( list );
+ G_Printf( "Removed entry %d from %s\n", index, cvarName );
+}
+
+
+/*
+==================
+G_FormatDuration
+
+Formats a duration in seconds as "Xd Xhr Xmin Xsec"
+==================
+*/
+static void G_FormatDuration( int totalSeconds, char *out, int outSize ) { // BFPR - Format seconds as Xd Xhr Xmin Xsec
+ int days, hours, minutes, seconds;
+ char part[16];
+
+ if ( totalSeconds < 0 ) {
+ totalSeconds = 0;
+ }
+
+ days = totalSeconds / 86400;
+ hours = ( totalSeconds % 86400 ) / 3600;
+ minutes = ( totalSeconds % 3600 ) / 60;
+ seconds = totalSeconds % 60;
+
+ out[0] = '\0';
+
+ if ( days > 0 ) {
+ Com_sprintf( part, sizeof(part), "%id ", days );
+ Q_strcat( out, outSize, part );
+ }
+ if ( hours > 0 || days > 0 ) {
+ Com_sprintf( part, sizeof(part), "%ihr ", hours );
+ Q_strcat( out, outSize, part );
+ }
+ if ( minutes > 0 || hours > 0 || days > 0 ) {
+ Com_sprintf( part, sizeof(part), "%imin ", minutes );
+ Q_strcat( out, outSize, part );
+ }
+ // seconds always shown, so the string is never empty
+ Com_sprintf( part, sizeof(part), "%isec", seconds );
+ Q_strcat( out, outSize, part );
+}
+
+/*
+==================
+G_BanListPrintf
+
+Prints every entry of the given ban-list cvar:
+ban_id: ip | guid | expires (or "permanent") | reason
+==================
+*/
+static void G_BanListPrintf( vmCvar_t *list, const char *label ) { // BFPR - Print ban list entries
+ char buf[MAX_CVAR_VALUE_STRING];
+ char displayReason[MAX_ADMIN_BAN_REASON_LEN];
+ char durationStr[32];
+ char parseBuf[256];
+ char *token, *p;
+ char *entryIp, *entryGuid, *entryReason;
+ int i, entryExpires, id = 1;
+
+ trap_Cvar_VariableStringBuffer( list->string, buf, sizeof(buf) );
+ if ( !list->string[0] ) {
+ G_Printf( "%s: none\n", label );
+ return;
+ }
+
+ G_Printf( "%s:\n", label );
+ Q_strncpyz( buf, list->string, sizeof( buf ) );
+ p = buf;
+ while ( ( token = strchr( p, ';' ) ) ) {
+ *token = '\0';
+ if ( p[0] ) {
+ Q_strncpyz( parseBuf, p, sizeof( parseBuf ) );
+ G_BanListParseEntry( parseBuf, &entryIp, &entryGuid, &entryExpires, &entryReason );
+ Q_strncpyz( displayReason, entryReason, sizeof( displayReason ) );
+ for ( i = 0; displayReason[i]; i++ ) {
+ if ( displayReason[i] == '_' ) displayReason[i] = ' ';
+ }
+ if ( entryExpires == 0 ) {
+ G_Printf( " ban_id=%d: ip=%s guid=%s expires=permanent reason=%s\n",
+ id, entryIp[0] ? entryIp : "-", entryGuid[0] ? entryGuid : "-",
+ displayReason[0] ? displayReason : "No reason given" );
+ } else {
+ G_FormatDuration( entryExpires - G_RealTimeSeconds(), durationStr, sizeof(durationStr) );
+ G_Printf( " ban_id=%d: ip=%s guid=%s expires_in=%s reason=%s\n",
+ id, entryIp[0] ? entryIp : "-", entryGuid[0] ? entryGuid : "-",
+ durationStr,
+ displayReason[0] ? displayReason : "No reason given" );
+ }
+ ++id;
+ }
+ p = token + 1;
+ }
+ if ( p[0] ) {
+ Q_strncpyz( parseBuf, p, sizeof( parseBuf ) );
+ G_BanListParseEntry( parseBuf, &entryIp, &entryGuid, &entryExpires, &entryReason );
+ Q_strncpyz( displayReason, entryReason, sizeof( displayReason ) );
+ for ( i = 0; displayReason[i]; i++ ) {
+ if ( displayReason[i] == '_' ) displayReason[i] = ' ';
+ }
+ if ( entryExpires == 0 ) {
+ G_Printf( " ban_id=%d: ip=%s guid=%s expires=permanent reason=%s\n",
+ id, entryIp[0] ? entryIp : "-", entryGuid[0] ? entryGuid : "-",
+ displayReason[0] ? displayReason : "No reason given" );
+ } else {
+ G_FormatDuration( entryExpires - G_RealTimeSeconds(), durationStr, sizeof(durationStr) );
+ G_Printf( " ban_id=%d: ip=%s guid=%s expires_in=%s reason=%s\n",
+ id, entryIp[0] ? entryIp : "-", entryGuid[0] ? entryGuid : "-",
+ durationStr,
+ displayReason[0] ? displayReason : "No reason given" );
+ }
+ ++id;
+ }
+}
+
+/*
+==================
+G_ResolveSenderIdentity
+
+Resolves this client's current IP (port stripped) and GUID
+==================
+*/
+static void G_ResolveSenderIdentity( gentity_t *ent, char *ipOut, int ipSize, char *guidOut, int guidSize ) { // BFPR - Resolve IP + GUID for a client
+ char userinfo[MAX_INFO_STRING];
+ char *ip, *colon;
+
+ ipOut[0] = '\0';
+ guidOut[0] = '\0';
+
+ if ( !ent || !ent->client ) {
+ return;
+ }
+
+ trap_GetUserinfo( ent - g_entities, userinfo, sizeof( userinfo ) );
+ ip = Info_ValueForKey( userinfo, "ip" );
+ colon = strchr( ip, ':' );
+ if ( colon ) {
+ *colon = '\0';
+ }
+ Q_strncpyz( ipOut, ip, ipSize );
+ Q_strncpyz( guidOut, ent->client->pers.guid, guidSize );
+}
+
+/*
+==================
+G_BanMessageForSender
+==================
+*/
+qboolean G_BanMessageForSender( qboolean cp, gentity_t *ent, vmCvar_t *list, char *out, int outSize ) { // BFPR - Format ban expiration and reason for a client
+ char ip[64], guid[33];
+ char reason[MAX_ADMIN_BAN_REASON_LEN];
+ char durationStr[32];
+ int i, expires, remaining;
+
+ if ( !ent || !ent->client ) {
+ return qfalse;
+ }
+
+ G_ResolveSenderIdentity( ent, ip, sizeof(ip), guid, sizeof(guid) );
+ if ( !G_BanListFind( list, ip, guid, &expires, reason, sizeof(reason) ) ) {
+ return qfalse;
+ }
+
+ // stored reason uses '_' in place of spaces - convert back for display
+ for ( i = 0; reason[i]; i++ ) {
+ if ( reason[i] == '_' ) {
+ reason[i] = ' ';
+ }
+ }
+
+ if ( expires == 0 ) {
+ if ( cp ) {
+ Com_sprintf( out, outSize, "\nBan: permanent\nReason:\n%s", reason );
+ } else {
+ Com_sprintf( out, outSize, "Ban: permanent | Reason: %s", reason );
+ }
+ } else {
+ remaining = expires - G_RealTimeSeconds();
+ G_FormatDuration( remaining, durationStr, sizeof(durationStr) );
+ if ( cp ) {
+ Com_sprintf( out, outSize, "\nYour ban expires in:\n^3%s^7\nBan reason:\n%s",
+ durationStr, reason );
+ } else {
+ Com_sprintf( out, outSize, "Expires in: %s | Ban reason: %s",
+ durationStr, reason );
+ }
+ }
+ return qtrue;
+}
+
+/*
+==================
+G_IsSenderMuted
+
+Resolves this client's current IP/GUID and checks them against g_muteban_list
+==================
+*/
+qboolean G_IsSenderMuted( gentity_t *ent ) { // BFPR - Mute the sender
+ char ip[64], guid[33];
+
+ if ( !ent || !ent->client ) {
+ return qfalse;
+ }
+
+ G_ResolveSenderIdentity( ent, ip, sizeof( ip ), guid, sizeof( guid ) );
+ return G_BanListContains( &g_muteban_list, ip, guid );
+}
+
+/*
+==================
+G_IsSenderVotebanned
+
+Resolves this client's current IP/GUID and checks them against g_voteban_list
+==================
+*/
+qboolean G_IsSenderVotebanned( gentity_t *ent ) { // BFPR - Vote-ban the sender
+ char ip[64], guid[33];
+
+ if ( !ent || !ent->client ) {
+ return qfalse;
+ }
+
+ G_ResolveSenderIdentity( ent, ip, sizeof( ip ), guid, sizeof( guid ) );
+ return G_BanListContains( &g_voteban_list, ip, guid );
+}
+
+/*
+==================
+G_IsSenderPlaybanned
+
+Resolves this client's current IP/GUID and checks them against g_playban_list
+==================
+*/
+qboolean G_IsSenderPlaybanned( gentity_t *ent ) { // BFPR - Play-ban the sender
+ char ip[64], guid[33];
+
+ if ( !ent || !ent->client ) {
+ return qfalse;
+ }
+
+ G_ResolveSenderIdentity( ent, ip, sizeof( ip ), guid, sizeof( guid ) );
+ return G_BanListContains( &g_playban_list, ip, guid );
+}
+
/*
=================
Svcmd_AddIP_f
@@ -378,6 +975,428 @@ void Svcmd_EntityList_f (void) {
}
}
+/*
+===================
+Svcmd_PlayerList_f
+
+Lists every connected client's slot number, netname and IP,
+so the admin knows which id to pass to mute/unmute, voteban/unvoteban,
+playban/unplayban, forceteam, etc
+===================
+*/
+void Svcmd_PlayerList_f ( void ) { // BFPR - playerlist command
+ int i;
+ gclient_t *cl;
+ char userinfo[MAX_INFO_STRING];
+ char *ip, *colon, *skillStr;
+ char skillDisp[8];
+ qboolean any = qfalse, hasBots = qfalse;
+ qboolean isBot;
+
+ // only show the bot/skill columns if at least one bot is connected
+ for ( i = 0, cl = level.clients ; i < level.maxclients ; i++, cl++ ) {
+ if ( cl->pers.connected == CON_DISCONNECTED ) {
+ continue;
+ }
+ if ( g_entities[i].r.svFlags & SVF_BOT ) {
+ hasBots = qtrue;
+ break;
+ }
+ }
+
+ if ( hasBots ) {
+ G_Printf( "id name ip bot skill team\n" );
+ G_Printf( "--- -------------------------------- ---------------- --- ----- ----------\n" );
+ } else {
+ G_Printf( "id name ip team\n" );
+ G_Printf( "--- -------------------------------- ---------------- ----------\n" );
+ }
+
+ for ( i = 0, cl = level.clients ; i < level.maxclients ; i++, cl++ ) {
+ if ( cl->pers.connected == CON_DISCONNECTED ) {
+ continue;
+ }
+ any = qtrue;
+
+ isBot = ( g_entities[i].r.svFlags & SVF_BOT );
+
+ if ( isBot ) {
+ ip = "---";
+ } else {
+ trap_GetUserinfo( i, userinfo, sizeof( userinfo ) );
+ ip = Info_ValueForKey( userinfo, "ip" );
+ // strip the port if present, e.g. "1.2.3.4:27960"
+ colon = strchr( ip, ':' );
+ if ( colon ) {
+ *colon = '\0';
+ }
+ }
+
+ if ( hasBots ) {
+ if ( isBot ) {
+ trap_GetUserinfo( i, userinfo, sizeof( userinfo ) );
+ skillStr = Info_ValueForKey( userinfo, "skill" );
+ if ( !skillStr[0] ) {
+ skillStr = "?";
+ } else { // strip the float string
+ Com_sprintf( skillDisp, sizeof( skillDisp ), "%i", (int)( atof( skillStr ) + 0.5f ) );
+ skillStr = skillDisp;
+ }
+ } else {
+ skillStr = "-";
+ }
+
+ G_Printf( "%-3i %-32s %-16s %-3s %-5s %s\n", i, cl->pers.netname, ip,
+ isBot ? "^3yes^7" : "no", skillStr,
+ cl->sess.sessionTeam == TEAM_SPECTATOR ? "spectator" :
+ cl->sess.sessionTeam == TEAM_RED ? "red" :
+ cl->sess.sessionTeam == TEAM_BLUE ? "blue" : "free" );
+ } else {
+ G_Printf( "%-3i %-32s %-16s %s\n", i, cl->pers.netname, ip,
+ cl->sess.sessionTeam == TEAM_SPECTATOR ? "spectator" :
+ cl->sess.sessionTeam == TEAM_RED ? "red" :
+ cl->sess.sessionTeam == TEAM_BLUE ? "blue" : "free" );
+ }
+ }
+
+ if ( !any ) {
+ G_Printf( "No players connected.\n" );
+ }
+}
+
+/*
+==================
+Svcmd_Mute_f
+
+Mutes a currently-connected client by IP and GUID, persists in g_muteban_list.
+usage: mute [minutes] [reason...]
+minutes omitted or 0 = permanent. reason omitted = "No reason given"
+==================
+*/
+static void Svcmd_Mute_f( void ) { // BFPR - mute [minutes] [reason...] command
+ char arg[MAX_TOKEN_CHARS];
+ int targetNum;
+ char userinfo[MAX_INFO_STRING];
+ char *ip, *colon;
+ char *guid;
+ int minutes, expires;
+ char *reason;
+
+ if ( trap_Argc() < 2 ) {
+ G_Printf( "usage: mute [minutes] [reason...]\n" );
+ return;
+ }
+
+ trap_Argv( 1, arg, sizeof( arg ) );
+ targetNum = atoi( arg );
+ if ( targetNum < 0 || targetNum >= level.maxclients
+ || level.clients[targetNum].pers.connected != CON_CONNECTED ) {
+ G_Printf( "Client %i is not active\n", targetNum );
+ return;
+ }
+
+ // bots share the "localhost" IP and have no GUID, so muting one would mute all of them
+ if ( g_entities[targetNum].r.svFlags & SVF_BOT ) {
+ G_Printf( "Client %i is a bot, mute cannot be applied.\n", targetNum );
+ return;
+ }
+
+ minutes = 0;
+ if ( trap_Argc() >= 3 ) {
+ trap_Argv( 2, arg, sizeof( arg ) );
+ minutes = atoi( arg );
+ if ( minutes < 0 ) {
+ minutes = 0;
+ }
+ }
+ expires = minutes > 0 ? G_RealTimeSeconds() + minutes * 60 : 0;
+ reason = trap_Argc() >= 4 ? ConcatArgs( 3 ) : "";
+
+ trap_GetUserinfo( targetNum, userinfo, sizeof( userinfo ) );
+ ip = Info_ValueForKey( userinfo, "ip" );
+ // strip the port if present, e.g. "1.2.3.4:27960"
+ colon = strchr( ip, ':' );
+ if ( colon ) {
+ *colon = '\0';
+ }
+ guid = level.clients[targetNum].pers.guid;
+
+ G_BanListAdd( &g_muteban_list, "g_muteban_list", ip, guid, expires, reason );
+ G_Printf( "Muted %s (%s)%s\n", level.clients[targetNum].pers.netname, ip,
+ minutes > 0 ? va( " for %i minutes", minutes ) : " permanently" );
+ G_LogPrintf( "mute: %s (%s|%s) expires=%i reason=%s\n",
+ level.clients[targetNum].pers.netname, ip, guid, expires, reason[0] ? reason : "No reason given" );
+}
+
+/*
+==================
+Svcmd_Unmute_f
+==================
+*/
+static void Svcmd_Unmute_f( void ) { // BFPR - unmute command
+ char arg[MAX_TOKEN_CHARS];
+
+ if ( trap_Argc() != 2 ) {
+ G_Printf( "usage: unmute \n" );
+ return;
+ }
+
+ trap_Argv( 1, arg, sizeof( arg ) );
+ if ( arg[0] ) {
+ int i = atoi( arg );
+ if ( i > 0 ) {
+ G_BanListRemoveByIndex( &g_muteban_list, "g_muteban_list", i );
+ G_LogPrintf( "unmute: %s\n", arg );
+ } else {
+ G_Printf( "Index must be positive.\n" );
+ }
+ } else {
+ G_BanListRemove( &g_muteban_list, "g_muteban_list", arg, arg );
+ G_Printf( "Removed %s from mute list\n", arg );
+ G_LogPrintf( "unmute: %s\n", arg );
+ }
+}
+
+/*
+==================
+Svcmd_MuteBans_f
+==================
+*/
+static void Svcmd_MuteBans_f( void ) { // BFPR - mutebans command
+ G_BanListPrune( &g_muteban_list, "g_muteban_list" );
+ G_BanListPrintf( &g_muteban_list, "Muted" );
+}
+
+/*
+==================
+Svcmd_Playban_f
+
+Bans a currently-connected client from playing (forced to spectate), by IP
+and GUID, persists in g_playban_list. If the client is on an active team
+right now, force them to spectator immediately.
+usage: playban [minutes] [reason...]
+minutes omitted or 0 = permanent. reason omitted = "No reason given"
+==================
+*/
+static void Svcmd_Playban_f( void ) { // BFPR - playban [minutes] [reason...] command
+ char arg[MAX_TOKEN_CHARS];
+ int targetNum;
+ char userinfo[MAX_INFO_STRING];
+ char *ip, *colon;
+ char *guid;
+ int minutes, expires;
+ char *reason;
+ gentity_t *target;
+
+ if ( trap_Argc() < 2 ) {
+ G_Printf( "usage: playban [minutes] [reason...]\n" );
+ return;
+ }
+
+ trap_Argv( 1, arg, sizeof( arg ) );
+ targetNum = atoi( arg );
+ if ( targetNum < 0 || targetNum >= level.maxclients
+ || level.clients[targetNum].pers.connected != CON_CONNECTED ) {
+ G_Printf( "Client %i is not active\n", targetNum );
+ return;
+ }
+
+ // bots share the "localhost" IP and have no GUID, so playbanning one would playban all of them
+ if ( g_entities[targetNum].r.svFlags & SVF_BOT ) {
+ G_Printf( "Client %i is a bot, playban cannot be applied.\n", targetNum );
+ return;
+ }
+
+ minutes = 0;
+ if ( trap_Argc() >= 3 ) {
+ trap_Argv( 2, arg, sizeof( arg ) );
+ minutes = atoi( arg );
+ if ( minutes < 0 ) {
+ minutes = 0;
+ }
+ }
+ expires = minutes > 0 ? G_RealTimeSeconds() + minutes * 60 : 0;
+ reason = trap_Argc() >= 4 ? ConcatArgs( 3 ) : "";
+
+ trap_GetUserinfo( targetNum, userinfo, sizeof( userinfo ) );
+ ip = Info_ValueForKey( userinfo, "ip" );
+ // strip the port if present, e.g. "1.2.3.4:27960"
+ colon = strchr( ip, ':' );
+ if ( colon ) {
+ *colon = '\0';
+ }
+ guid = level.clients[targetNum].pers.guid;
+
+ G_BanListAdd( &g_playban_list, "g_playban_list", ip, guid, expires, reason );
+ G_Printf( "Play-banned %s (%s)%s\n", level.clients[targetNum].pers.netname, ip,
+ minutes > 0 ? va( " for %i minutes", minutes ) : " permanently" );
+ G_LogPrintf( "playban: %s (%s|%s) expires=%i reason=%s\n",
+ level.clients[targetNum].pers.netname, ip, guid, expires, reason[0] ? reason : "No reason given" );
+
+ // if they are on an active team right now, force them to spectator immediately
+ target = &g_entities[targetNum];
+ if ( target->client && target->client->sess.sessionTeam != TEAM_SPECTATOR ) {
+ char durationStr[32];
+ int remaining = expires - G_RealTimeSeconds();
+ G_FormatDuration( remaining, durationStr, sizeof(durationStr) );
+
+ SetTeam( target, "spectator" );
+ ClientBegin( targetNum );
+ trap_SendServerCommand( targetNum,
+ va( "cp \"^1You are banned from \n^1playing on this server.\n\nYour ban expires in:\n^3%s^7\nBan reason:\n%s\n\"",
+ durationStr, reason[0] ? reason : "No reason given" )
+ );
+ trap_SendServerCommand( targetNum,
+ va( "print \"You are banned from playing on this server. Expires in: %s | Ban reason: %s\n\"",
+ durationStr, reason[0] ? reason : "No reason given" )
+ );
+ }
+}
+
+/*
+==================
+Svcmd_Unplayban_f
+==================
+*/
+static void Svcmd_Unplayban_f( void ) { // BFPR - unplayban command
+ char arg[MAX_TOKEN_CHARS];
+
+ if ( trap_Argc() != 2 ) {
+ G_Printf( "usage: unplayban \n" );
+ return;
+ }
+
+ trap_Argv( 1, arg, sizeof( arg ) );
+ if ( arg[0] ) {
+ int i = atoi( arg );
+ if ( i > 0 ) {
+ G_BanListRemoveByIndex( &g_playban_list, "g_playban_list", i );
+ G_LogPrintf( "unplayban: %s\n", arg );
+ } else {
+ G_Printf( "Index must be positive.\n" );
+ }
+ } else {
+ G_BanListRemove( &g_playban_list, "g_playban_list", arg, arg );
+ G_Printf( "Removed %s from playban list\n", arg );
+ G_LogPrintf( "unplayban: %s\n", arg );
+ }
+}
+
+/*
+==================
+Svcmd_Playbans_f
+==================
+*/
+static void Svcmd_Playbans_f( void ) { // BFPR - playbans command
+ G_BanListPrune( &g_playban_list, "g_playban_list" );
+ G_BanListPrintf( &g_playban_list, "Play-banned" );
+}
+
+/*
+==================
+Svcmd_Voteban_f
+
+Prevents a currently-connected client from calling or casting votes,
+by IP and GUID, persists in g_voteban_list.
+usage: voteban [minutes] [reason...]
+minutes omitted or 0 = permanent. reason omitted = "No reason given"
+==================
+*/
+static void Svcmd_Voteban_f( void ) { // BFPR - voteban [minutes] [reason...] command
+ char arg[MAX_TOKEN_CHARS];
+ int targetNum;
+ char userinfo[MAX_INFO_STRING];
+ char *ip, *colon;
+ char *guid;
+ int minutes, expires;
+ char *reason;
+
+ if ( trap_Argc() < 2 ) {
+ G_Printf( "usage: voteban [minutes] [reason...]\n" );
+ return;
+ }
+
+ trap_Argv( 1, arg, sizeof( arg ) );
+ targetNum = atoi( arg );
+ if ( targetNum < 0 || targetNum >= level.maxclients
+ || level.clients[targetNum].pers.connected != CON_CONNECTED ) {
+ G_Printf( "Client %i is not active\n", targetNum );
+ return;
+ }
+
+ // bots share the "localhost" IP and have no GUID, so votebanning one would voteban all of them
+ if ( g_entities[targetNum].r.svFlags & SVF_BOT ) {
+ G_Printf( "Client %i is a bot, voteban cannot be applied.\n", targetNum );
+ return;
+ }
+
+ minutes = 0;
+ if ( trap_Argc() >= 3 ) {
+ trap_Argv( 2, arg, sizeof( arg ) );
+ minutes = atoi( arg );
+ if ( minutes < 0 ) {
+ minutes = 0;
+ }
+ }
+ expires = minutes > 0 ? G_RealTimeSeconds() + minutes * 60 : 0;
+ reason = trap_Argc() >= 4 ? ConcatArgs( 3 ) : "";
+
+ trap_GetUserinfo( targetNum, userinfo, sizeof( userinfo ) );
+ ip = Info_ValueForKey( userinfo, "ip" );
+ // strip the port if present, e.g. "1.2.3.4:27960"
+ colon = strchr( ip, ':' );
+ if ( colon ) {
+ *colon = '\0';
+ }
+ guid = level.clients[targetNum].pers.guid;
+
+ G_BanListAdd( &g_voteban_list, "g_voteban_list", ip, guid, expires, reason );
+ G_Printf( "Vote-banned %s (%s)%s\n", level.clients[targetNum].pers.netname, ip,
+ minutes > 0 ? va( " for %i minutes", minutes ) : " permanently" );
+ G_LogPrintf( "voteban: %s (%s|%s) expires=%i reason=%s\n",
+ level.clients[targetNum].pers.netname, ip, guid, expires, reason[0] ? reason : "No reason given" );
+}
+
+/*
+==================
+Svcmd_Unvoteban_f
+==================
+*/
+static void Svcmd_Unvoteban_f( void ) { // BFPR - unvoteban command
+ char arg[MAX_TOKEN_CHARS];
+
+ if ( trap_Argc() != 2 ) {
+ G_Printf( "usage: unvoteban \n" );
+ return;
+ }
+
+ trap_Argv( 1, arg, sizeof( arg ) );
+ if ( arg[0] ) {
+ int i = atoi( arg );
+ if ( i > 0 ) {
+ G_BanListRemoveByIndex( &g_voteban_list, "g_voteban_list", i );
+ G_LogPrintf( "unvoteban: %s\n", arg );
+ } else {
+ G_Printf( "Index must be positive.\n" );
+ }
+ } else {
+ G_BanListRemove( &g_voteban_list, "g_voteban_list", arg, arg );
+ G_Printf( "Removed %s from voteban list\n", arg );
+ G_LogPrintf( "unvoteban: %s\n", arg );
+ }
+}
+
+/*
+==================
+Svcmd_Votebans_f
+==================
+*/
+static void Svcmd_Votebans_f( void ) { // BFPR - votebans command
+ G_BanListPrune( &g_voteban_list, "g_voteban_list" );
+ G_BanListPrintf( &g_voteban_list, "Vote-banned" );
+}
+
+
gclient_t *ClientForString( const char *s ) {
gclient_t *cl;
int i;
@@ -438,7 +1457,6 @@ void Svcmd_ForceTeam_f( void ) {
SetTeam( &g_entities[cl - level.clients], str );
}
-char *ConcatArgs( int start );
/*
=================
@@ -456,6 +1474,11 @@ qboolean ConsoleCommand( void ) {
return qtrue;
}
+ if ( Q_stricmp ( cmd, "playerlist" ) == 0 ) { // BFPR - playerlist command
+ Svcmd_PlayerList_f();
+ return qtrue;
+ }
+
if ( Q_stricmp (cmd, "forceteam") == 0 ) {
Svcmd_ForceTeam_f();
return qtrue;
@@ -496,6 +1519,51 @@ qboolean ConsoleCommand( void ) {
return qtrue;
}
+ if ( Q_stricmp ( cmd, "mute" ) == 0 ) { // BFPR - mute command
+ Svcmd_Mute_f();
+ return qtrue;
+ }
+
+ if ( Q_stricmp ( cmd, "unmute" ) == 0 ) { // BFPR - unmute command
+ Svcmd_Unmute_f();
+ return qtrue;
+ }
+
+ if ( Q_stricmp ( cmd, "mutebans" ) == 0 ) { // BFPR - mutebans command
+ Svcmd_MuteBans_f();
+ return qtrue;
+ }
+
+ if ( Q_stricmp ( cmd, "playban" ) == 0 ) { // BFPR - playban command
+ Svcmd_Playban_f();
+ return qtrue;
+ }
+
+ if ( Q_stricmp ( cmd, "unplayban" ) == 0 ) { // BFPR - unplayban command
+ Svcmd_Unplayban_f();
+ return qtrue;
+ }
+
+ if ( Q_stricmp ( cmd, "playbans" ) == 0 ) { // BFPR - playbans command
+ Svcmd_Playbans_f();
+ return qtrue;
+ }
+
+ if ( Q_stricmp ( cmd, "voteban" ) == 0 ) { // BFPR - voteban command
+ Svcmd_Voteban_f();
+ return qtrue;
+ }
+
+ if ( Q_stricmp ( cmd, "unvoteban" ) == 0 ) { // BFPR - unvoteban command
+ Svcmd_Unvoteban_f();
+ return qtrue;
+ }
+
+ if ( Q_stricmp ( cmd, "votebans" ) == 0 ) { // BFPR - votebans command
+ Svcmd_Votebans_f();
+ return qtrue;
+ }
+
if (g_dedicated.integer) {
if (Q_stricmp (cmd, "say") == 0) {
trap_SendServerCommand( -1, va("print \"server: %s\"", ConcatArgs(1) ) );
diff --git a/source/q3_ui/ui_controls2.c b/source/q3_ui/ui_controls2.c
index 81d88ca..dd3d27a 100644
--- a/source/q3_ui/ui_controls2.c
+++ b/source/q3_ui/ui_controls2.c
@@ -363,15 +363,10 @@ Controls_InitCvars
*/
static void Controls_InitCvars( void )
{
- int i;
configcvar_t* cvarptr;
- cvarptr = g_configcvars;
- for (i=0; ;i++,cvarptr++)
+ for (cvarptr = g_configcvars; cvarptr->name; cvarptr++)
{
- if (!cvarptr->name)
- break;
-
// get current value
cvarptr->value = trap_Cvar_VariableValue( cvarptr->name );
@@ -392,18 +387,16 @@ Controls_GetCvarDefault
static float Controls_GetCvarDefault( char* name )
{
configcvar_t* cvarptr;
- int i;
- cvarptr = g_configcvars;
- for (i=0; ;i++,cvarptr++)
+ for (cvarptr = g_configcvars; cvarptr->name; cvarptr++)
{
- if (!cvarptr->name)
- return (0);
-
if (!strcmp(cvarptr->name,name))
break;
}
+ if (!cvarptr->name)
+ return (0);
+
return (cvarptr->defaultvalue);
}
@@ -415,18 +408,16 @@ Controls_GetCvarValue
static float Controls_GetCvarValue( char* name )
{
configcvar_t* cvarptr;
- int i;
- cvarptr = g_configcvars;
- for (i=0; ;i++,cvarptr++)
+ for (cvarptr = g_configcvars; cvarptr->name; cvarptr++)
{
- if (!cvarptr->name)
- return (0);
-
if (!strcmp(cvarptr->name,name))
break;
}
+ if (!cvarptr->name)
+ return (0);
+
return (cvarptr->value);
}
@@ -802,19 +793,12 @@ Controls_GetConfig
*/
static void Controls_GetConfig( void )
{
- int i;
int twokeys[2];
bind_t* bindptr;
- // put the bindings into a local store
- bindptr = g_bindings;
-
// iterate each command, get its numeric binding
- for (i=0; ;i++,bindptr++)
+ for (bindptr = g_bindings; bindptr->label; bindptr++)
{
- if (!bindptr->label)
- break;
-
Controls_GetKeyAssignment(bindptr->command, twokeys);
bindptr->bind1 = twokeys[0];
@@ -838,18 +822,11 @@ Controls_SetConfig
*/
static void Controls_SetConfig( void )
{
- int i;
bind_t* bindptr;
- // set the bindings from the local store
- bindptr = g_bindings;
-
// iterate each command, get its numeric binding
- for (i=0; ;i++,bindptr++)
+ for (bindptr = g_bindings; bindptr->label; bindptr++)
{
- if (!bindptr->label)
- break;
-
if (bindptr->bind1 != -1)
{
trap_Key_SetBinding( bindptr->bind1, bindptr->command );
@@ -881,18 +858,11 @@ Controls_SetDefaults
*/
static void Controls_SetDefaults( void )
{
- int i;
bind_t* bindptr;
- // set the bindings from the local store
- bindptr = g_bindings;
-
// iterate each command, set its default binding
- for (i=0; ;i++,bindptr++)
+ for (bindptr = g_bindings; bindptr->label; bindptr++)
{
- if (!bindptr->label)
- break;
-
bindptr->bind1 = bindptr->defaultbind1;
bindptr->bind2 = bindptr->defaultbind2;
}
@@ -915,7 +885,6 @@ Controls_MenuKey
static sfxHandle_t Controls_MenuKey( int key )
{
int id;
- int i;
qboolean found;
bind_t* bindptr;
found = qfalse;
@@ -962,12 +931,8 @@ static sfxHandle_t Controls_MenuKey( int key )
if (key != -1)
{
// remove from any other bind
- bindptr = g_bindings;
- for (i=0; ;i++,bindptr++)
+ for (bindptr = g_bindings; bindptr->label; bindptr++)
{
- if (!bindptr->label)
- break;
-
if (bindptr->bind2 == key)
bindptr->bind2 = -1;
@@ -981,12 +946,8 @@ static sfxHandle_t Controls_MenuKey( int key )
// assign key to local store
id = ((menucommon_s*)(s_controls.menu.items[s_controls.menu.cursor]))->id;
- bindptr = g_bindings;
- for (i=0; ;i++,bindptr++)
+ for (bindptr = g_bindings; bindptr->label; bindptr++)
{
- if (!bindptr->label)
- break;
-
if (bindptr->id == id)
{
found = qtrue;