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;