diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c809116..f465a92a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -107,6 +107,7 @@ set(SRC_COMMON "${DIR_SRC}/route_lookup.c" "${DIR_SRC}/runes.c" "${DIR_SRC}/server.c" + "${DIR_SRC}/sprays.c" "${DIR_SRC}/sp_ai.c" "${DIR_SRC}/sp_boss.c" "${DIR_SRC}/sp_client.c" diff --git a/include/g_local.h b/include/g_local.h index 3032ba9f..cec24f1d 100644 --- a/include/g_local.h +++ b/include/g_local.h @@ -222,6 +222,8 @@ enum G_SETEXTFIELDPTR, G_GETEXTFIELDPTR, G_SETSENDNEEDED, + G_SPRAYCLEAR = G_EXTENSIONS_FIRST + 14, + G_SPRAYCLEARALL, G_EXTENSIONS_LAST }; extern qbool haveextensiontab[G_EXTENSIONS_LAST-G_EXTENSIONS_FIRST]; @@ -245,6 +247,11 @@ float g_random(void); float crandom(void); int i_rnd(int from, int to); float dist_random(float minValue, float maxValue, float spreadFactor); +void KTX_SpraysClearAll(void); +void KTX_SpraysClearPlayer(gedict_t *player); +void KTX_SpraysForgetPlayer(gedict_t *player); +qbool KTX_CanSpray(void); +void KTX_SprayPlaced(int spray_id); float next_frame(void); gedict_t* spawn(void); void ent_remove(gedict_t *t); @@ -911,6 +918,7 @@ void ra_break(void); // clan_arena.c qbool isCA(void); +qbool CA_CanSpray(void); qbool CA_CheckAlive(gedict_t *p); int CA_wins_required(void); int CA_count_ready_players(void); diff --git a/include/g_public.h b/include/g_public.h index af27bfa3..bd42eb3e 100644 --- a/include/g_public.h +++ b/include/g_public.h @@ -189,6 +189,8 @@ typedef enum GAME_CLIENT_SAY, // ( int isTeamSay ); GAME_PAUSED_TIC, // ( int duration_msec ); // duration is in msecs GAME_CLEAR_EDICT, // (self) + GAME_CAN_SPRAY, // self + GAME_SPRAY_PLACED, // self, spray_id GAME_EDICT_CSQCSEND = 200, //entrypoint, called when using SendEntity } gameExport_t; diff --git a/include/g_syscalls.h b/include/g_syscalls.h index 420d0075..8df91093 100644 --- a/include/g_syscalls.h +++ b/include/g_syscalls.h @@ -130,6 +130,8 @@ intptr_t trap_SetBotCMD(intptr_t edn, intptr_t msec, float angles_x, float angle intptr_t impulse); void trap_setpause(intptr_t pause); +intptr_t trap_SprayClear(intptr_t id); +intptr_t trap_SprayClearAll(void); intptr_t QVMstrftime(char *valbuff, intptr_t sizebuff, const char *fmt, intptr_t offset); diff --git a/src/clan_arena.c b/src/clan_arena.c index f94b67fe..e26f9ea0 100644 --- a/src/clan_arena.c +++ b/src/clan_arena.c @@ -295,6 +295,8 @@ void SM_PrepareCA(void) WO_InitializeSpawns(); // init wipeout spawns } + KTX_SpraysClearAll(); + team1_score = team2_score = 0; round_num = 1; @@ -336,6 +338,17 @@ qbool isCA(void) return (isTeam() && cvar("k_clan_arena")); } +qbool CA_CanSpray(void) +{ + // Allow sprays during prewar + if (!match_in_progress) { + return true; + } + + // During match, sprays are only allowed between rounds + return ca_round_pause != 0; +} + // Used to determine value of ca_alive when PutClientInServer() is called qbool CA_CheckAlive(gedict_t *p) { diff --git a/src/g_main.c b/src/g_main.c index 02beaef1..ae397f09 100644 --- a/src/g_main.c +++ b/src/g_main.c @@ -273,6 +273,11 @@ intptr_t VISIBILITY_VISIBLE vmMain( ClearGlobals(); self = PROG_TO_EDICT(g_globalvars.self); RemoveMOTD(); // remove MOTD entitys + + // clear the bookkeeping but keep the disconnected player's sprays + // use KTX_SpraysClearPlayer() instead to clear sprays on disconnect + KTX_SpraysForgetPlayer(self); + s_lr_clear(self); if (arg0) { @@ -437,6 +442,19 @@ intptr_t VISIBILITY_VISIBLE vmMain( return 0; + case GAME_CAN_SPRAY: + ClearGlobals(); + self = PROG_TO_EDICT(g_globalvars.self); + + return KTX_CanSpray(); + + case GAME_SPRAY_PLACED: + ClearGlobals(); + self = PROG_TO_EDICT(g_globalvars.self); + KTX_SprayPlaced(arg0); + + return 0; + case GAME_EDICT_CSQCSEND: self = PROG_TO_EDICT(g_globalvars.self); other = PROG_TO_EDICT(g_globalvars.other); @@ -671,6 +689,8 @@ static qbool G_InitExtensions(void) {"SetExtFieldPtr", G_SETEXTFIELDPTR}, {"GetExtFieldPtr", G_GETEXTFIELDPTR}, {"setsendneeded", G_SETSENDNEEDED}, + {"sprayclear", G_SPRAYCLEAR}, + {"sprayclearall", G_SPRAYCLEARALL}, }; int i; for (i = 0; i < sizeof(exttraps)/sizeof(exttraps[0]); i++) diff --git a/src/g_syscalls.asm b/src/g_syscalls.asm index d74b8ed7..2bfcf8b9 100644 --- a/src/g_syscalls.asm +++ b/src/g_syscalls.asm @@ -120,3 +120,5 @@ equ trap_pointerstat -265 equ trap_MapExtFieldPtr -266 equ trap_SetExtFieldPtr -267 equ trap_GetExtFieldPtr -268 +equ trap_SprayClear -270 +equ trap_SprayClearAll -271 diff --git a/src/g_syscalls.c b/src/g_syscalls.c index 45f8f0ad..a52f9583 100644 --- a/src/g_syscalls.c +++ b/src/g_syscalls.c @@ -434,6 +434,16 @@ void trap_setpause(intptr_t pause) syscall(G_SETPAUSE, pause); } +intptr_t trap_SprayClear(intptr_t id) +{ + return syscall(G_SPRAYCLEAR, id); +} + +intptr_t trap_SprayClearAll(void) +{ + return syscall(G_SPRAYCLEARALL); +} + intptr_t QVMstrftime(char *valbuff, intptr_t sizebuff, const char *fmt, intptr_t offset) { return syscall(G_QVMstrftime, (intptr_t) valbuff, sizebuff, (intptr_t) fmt, offset); diff --git a/src/sprays.c b/src/sprays.c new file mode 100644 index 00000000..d980d678 --- /dev/null +++ b/src/sprays.c @@ -0,0 +1,136 @@ +/* + * Server-authoritative spray policy for KTX. + * + * MVDSV owns the network protocol, pixel storage, and demo messages. KTX only + * decides when a player may spray and which previously placed spray should be + * removed when that player exceeds the current game-mode limit. + */ + +#include "g_local.h" + +// global limit - modes should specify a lower value +#define KTX_MAX_SPRAYS_PER_PLAYER 10 + +typedef struct +{ + int ids[KTX_MAX_SPRAYS_PER_PLAYER]; + int count; +} ktx_player_sprays_t; + +static ktx_player_sprays_t ktx_player_sprays[MAX_CLIENTS]; + +// k_player_spray_limit is a per-player policy value; clamp it to the fixed size of +// each player's tracked spray-id queue. +static int KTX_SprayLimit(void) +{ + int limit = (int)cvar("k_player_spray_limit"); + + limit = bound(0, limit, KTX_MAX_SPRAYS_PER_PLAYER); + + return limit; +} + +static int KTX_SprayPlayerIndex(gedict_t *player) +{ + int index; + + if (!player) { + return -1; + } + + index = NUM_FOR_EDICT(player) - 1; + if (index < 0 || index >= MAX_CLIENTS) { + return -1; + } + + return index; +} + +void KTX_SpraysClearAll(void) +{ + memset(ktx_player_sprays, 0, sizeof(ktx_player_sprays)); + + if (HAVEEXTENSION(G_SPRAYCLEARALL)) { + trap_SprayClearAll(); + } +} + +// Clear every visible spray KTX has tracked for this player. +// Use this only when the sprays should be removed from the map. +void KTX_SpraysClearPlayer(gedict_t *player) +{ + ktx_player_sprays_t *sprays; + int index = KTX_SprayPlayerIndex(player); + int i; + + if (index < 0) { + return; + } + + sprays = &ktx_player_sprays[index]; + if (HAVEEXTENSION(G_SPRAYCLEAR)) { + for (i = 0; i < sprays->count; ++i) { + trap_SprayClear(sprays->ids[i]); + } + } + memset(sprays, 0, sizeof(*sprays)); +} + +// Drop only this client slot's spray bookkeeping. +// Use this on disconnect so visible sprays stay on the map. +void KTX_SpraysForgetPlayer(gedict_t *player) +{ + int index = KTX_SprayPlayerIndex(player); + + if (index < 0) { + return; + } + + memset(&ktx_player_sprays[index], 0, sizeof(ktx_player_sprays[index])); +} + +qbool KTX_CanSpray(void) +{ + if (KTX_SprayPlayerIndex(self) < 0 || self->ct != ctPlayer) { + return false; + } + + if (isCA()) { + return CA_CanSpray(); + } + + return false; +} + +void KTX_SprayPlaced(int spray_id) +{ + ktx_player_sprays_t *sprays; + int index = KTX_SprayPlayerIndex(self); + int limit = KTX_SprayLimit(); + int i; + + if (index < 0 || spray_id <= 0) { + return; + } + + sprays = &ktx_player_sprays[index]; + if (limit <= 0) { + if (HAVEEXTENSION(G_SPRAYCLEAR)) { + trap_SprayClear(spray_id); + } + return; + } + + while (sprays->count >= limit) { + if (HAVEEXTENSION(G_SPRAYCLEAR)) { + trap_SprayClear(sprays->ids[0]); + } + + for (i = 1; i < sprays->count; ++i) { + sprays->ids[i - 1] = sprays->ids[i]; + } + --sprays->count; + } + + sprays->ids[sprays->count++] = spray_id; +} diff --git a/src/world.c b/src/world.c index 59d10a30..cc64f896 100644 --- a/src/world.c +++ b/src/world.c @@ -988,6 +988,7 @@ void FirstFrame(void) RegisterCvarEx("k_clan_arena", "0"); RegisterCvarEx("k_clan_arena_rounds", "9"); RegisterCvarEx("k_clan_arena_max_respawns", "0"); + RegisterCvarEx("k_player_spray_limit", "5"); // } // { upplayers/upspecs RegisterCvar("k_allowcountchange");