diff --git a/README.md b/README.md index 3f22a02..2ca2725 100644 --- a/README.md +++ b/README.md @@ -1,459 +1,191 @@ -# Welcome to Muff Mode BETA! - -## What is Muff Mode? -Muff Mode is a server-side mod for [QUAKE II Remastered](https://github.com/id-Software/quake2-rerelease-dll) providing overall enhances functionality and refinements. - -### It is for Server Hosts -With a focus on multiplayer, it provides refined match handling and an extensive set of new capabilities for server owners to configure the game in a host of new ways. - -### It is for Level Designers -New creative possibilities are unlocked for level designers, with an array of new map entities and keys and a range of added gametypes to design for. - -### It is for the Players(tm) -Enhanced HUD info and a number of changable settings. + + + + +
+ Muff Mode Logo +

Muff Mode BETA

+

+ Muff Mode is a server-side mod for QUAKE II Rerelease + providing an enhanced multiplayer experience. +

+
+ +--- + +## Overview +- Purpose-built for multiplayer server hosts, level designers, and players who want modern conveniences. +- Bundles refined match handling, new game entities and gametypes, and quality-of-life tweaks. +- Ships with configuration, bot, and map overrides ready to go out of the box. + +## Audience Snapshot +| Server Hosts | Level Designers | Players™ | +| --- | --- | --- | +| Refined match handling, warmups, countdowns, and flexible admin controls. | Expanded map entities/keys and added gametypes to design around. | Enhanced HUD info and configurable settings. | ## Installation -1. Locate your installation. For Steam, this is normally "C:\Program Files (x86)\Steam\steamapps\common\Quake 2\rerelease". -2. Back up your baseq2/game_x64.dll. +1. Locate your installation. For Steam, this is normally `C:\Program Files (x86)\Steam\steamapps\common\Quake 2\rerelease`. +2. Back up your `baseq2/game_x64.dll`. 3. Download the latest [Muff Mode release](https://github.com/themuffinator/muffmode/releases/latest). -4. Extract the entire zip to your "Quake 2" folder (not rerelease), allow file replacements (unless you already have Muff Mode, this should only replace the .dll). +4. Extract the entire zip to your `Quake 2` folder (not `rerelease`); allow file replacements (unless you already have Muff Mode, this should only replace the .dll). 5. Load the game up as normal. A range of configs can be executed to apply settings once a game has been set up. -6. Once a lobby has been set up, you can execute the included server config via ``exec muff-sv.cfg``. +6. Once a lobby has been set up, execute the included server config via `exec muff-sv.cfg`. ## What's in the Bag? -Muff Mode includes the game logic, a server config, bot files and some map entity overrides all straight out of the bag! +Muff Mode includes the game logic, a server config, bot files, and map entity overrides all straight out of the bag. ## Feature Overview -- Refined HUD and scoreboard, more purpose-built for what information is needed and some extra features: - * Frag messages (also showing position in match) - * Dynamic miniscores with scorelimit - * Timer and match state - * Help texts - * Message of the Day - * Mini scoreboard -- A game menu for joining a match, changing for or voting on settings and viewing mod and server info. -- A whole host of controls for admins, voting and more. -- Refined match handling with conditional progression, including: warmups, readying, countdowns, post-match delays, sudden death, overtime and more. -- Enhanced teamplay with team auto-balancing, forced balancing rules, improved team handling, communicating joined team to players, major item pickup and weapon drop POI's, and friendly fire warnings. -- Extensive controls over specific map item spawns and entity string overrides. -- EyeCam spectating, smooth and with aim prediction (mostly!) -- MyMap map queing system inspired by tastyspleen.net. -- A number of bug fixes, minor refinements, balance tweaks and many new server settings added. -- Muff Maps: official maps under development included utilizing Muff Mode's enhanced capabilities -- Many more features, see release changelogs for more info! +- **HUD & Scoreboard:** Frag messages (with position), dynamic miniscores with scorelimit, timers, match state, help texts, message of the day, mini scoreboard. +- **Game Menu:** Join matches, change or vote on settings, and view mod/server info. +- **Admin & Voting Controls:** Broad controls for admins plus improved voting workflows. +- **Match Handling:** Conditional progression with warmups, readying, countdowns, post-match delays, sudden death, overtime, and more. +- **Teamplay Enhancements:** Auto-balancing, forced balancing rules, improved team handling, team join communication, POIs for major items and weapon drops, friendly-fire warnings. +- **Map & Entity Control:** Extensive control over map item spawns and entity string overrides. +- **Spectating:** EyeCam spectating with smooth movement and aim prediction (mostly!). +- **Queues & Content:** MyMap map queuing inspired by tastyspleen.net and official **Muff Maps** leveraging enhanced capabilities. +- **Fixes & Tweaks:** Bug fixes, balance updates, and a variety of new server settings. ### Muff Maps -- Almost Lost [ALpha v1] (mm-almostlost-a1) - Left out from Quake III: Arena's release, this level was eventually finished off and officially released as a seperate map as pro-q3tourney7. Revised in Quake Live, this map is streamlined for fast-paced FFAs and Duels. -- Arena of Death [Alpha v3] (mm-arena-a3) - A small and simple gem from Quake III: Arena. -- Hidden Fortress [Alpha v4] (mm-fortress-a4) - A small-to-medium-sized level connected via two teleporters. Originally created by Raster Productions for Quake III for Dreamcast, this offers the revised layout found in Quake Live. -- Longest Yard [Beta v2] (mm-longestyard-b2) - The quintessential space map from Quake III: Arena. -- Proving Grounds [Alpha v4] (mm-proving-a4) - A small Duel map from Quake III: Arena. -- Vertical Vengeance [Alpha v2] (mm-vengeance-a2) - A small Duel map from Quake III: Arena. +- **Almost Lost [Alpha v1] (mm-almostlost-a1):** Left out from Quake III: Arena's release, later finished and released as pro-q3tourney7. Streamlined for fast-paced FFAs and Duels. +- **Arena of Death [Alpha v3] (mm-arena-a3):** A small and simple gem from Quake III: Arena. +- **Hidden Fortress [Alpha v4] (mm-fortress-a4):** Small-to-medium-sized level connected via two teleporters. Originally created by Raster Productions for Quake III for Dreamcast, offering the revised layout found in Quake Live. +- **Longest Yard [Beta v2] (mm-longestyard-b2):** The quintessential space map from Quake III: Arena. +- **Proving Grounds [Alpha v4] (mm-proving-a4):** A small Duel map from Quake III: Arena. +- **Vertical Vengeance [Alpha v2] (mm-vengeance-a2):** A small Duel map from Quake III: Arena. ### New Gametypes -- Horde: Battle waves of monsters, stay on top of the scoreboard while defeating up to 16 waves to be victorious! Note: currently does not handle limited lives. -- Duel: Go head-to-head with an opponent. The victor goes on to face their next opponent in the queue. -- Clan Arena: Rocket Arena's famous round-based team elimination mode - no item spawns, no self-damage and a full arsenal of weapons. -- CaptureStrike: A Threewave classic, combines Clan Arena, CTF and Counter Strike. Teams take turns attacking or defending and battle until one team is dead, or the attacking team captures the flag. -- Red Rover: Clan Arena style where teams are changed on death. When a team has been eliminated, the round ends. +- **Horde:** Battle waves of monsters; stay on top of the scoreboard while defeating up to 16 waves to be victorious! Note: currently does not handle limited lives. +- **Duel:** Go head-to-head with an opponent; the victor faces the next opponent in the queue. +- **Clan Arena:** Rocket Arena's famous round-based team elimination mode—no item spawns, no self-damage, and a full arsenal of weapons. +- **CaptureStrike:** A Threewave classic combining Clan Arena, CTF, and Counter Strike. Teams alternate attacking/defending and battle until one team is dead or the attackers capture the flag. +- **Red Rover:** Clan Arena style where teams change on death. When a team is eliminated, the round ends. ### New Game Modifications -- Vampiric Damage: Gain health by inflicting damage on your foes! No health pickups and a draining health value means the pressure is on! -- Nade Fest: Grenade-only mode. -- Weapons Frenzy: Intensified combat! Faster rates of fire, faster rockets, regenerating ammo, faster weapon switching. -- Quad Hog: Find the Quad Damage to become the Quad Hog! +- **Vampiric Damage:** Gain health by inflicting damage. No health pickups; a draining health value keeps pressure high. +- **Nade Fest:** Grenade-only mode. +- **Weapons Frenzy:** Intensified combat with faster fire rates, faster rockets, regenerating ammo, and faster weapon switching. +- **Quad Hog:** Find the Quad Damage to become the Quad Hog! ### Deathmatch Refinements -- Intermission pre-delay: a one second intermission pre-delay means you can see your winning frag or capture before final scores (no damage is taken or additional scoring during this delay). -- Minimum respawn delay: a short respawn delay helps avoid accidental respawning and creates a smoother transition. +- Intermission pre-delay: a one-second intermission pre-delay shows your final frag or capture before scores tally (no damage or additional scoring during this delay). +- Minimum respawn delay: a short delay helps avoid accidental respawns and creates a smoother transition. - Kill beeps and frag messages highlight your frags and rank. - + ### Offhand Hook -- Added 'hook' and 'unhook' commands to use off-hand hook. Use `g_grapple_offhand 1` to enable this. -- Players can use ``alias +hook hook; alias -hook unhook; bind mouse2 +hook`` to use it as a button command +- Added `hook` and `unhook` commands to use off-hand hook. Use `g_grapple_offhand 1` to enable. +- Players can use `alias +hook hook; alias -hook unhook; bind mouse2 +hook` to use it as a button command. ### Gameplay Tweaks and Fixes - - Instagib and Nade Fest now give players regeneration to recover from environmental damage, falling damage etc. - - Quad and Protection player color shells don't change depending on team, avoids confusion - - func_rotating: Rotating map entities now explode non-player ents such as dropped items, practically this means no more blocked rotator in dm5. - - Player Feedback: - * Added Fragging Spree award - broadcasted message "x is on a fragging spree with x frags" per every 10 frags achieved without dying or killing a team mate - - Techs: fixed not being able to pick up your dropped tech - - Blaster and Grapple now both droppable and can spawn in world - - Current weapon is now droppable - - Smart weapon auto-switch: now switches to SSG from SG, CG from MG, never auto-switches to chainfist. - - Instant gametype changing (eg: from FFA to TDM) - - DuelFire Damage has been changed to Haste: 50% faster movement, 50% faster weapon rate of fire. - - Many more! +- Instagib and Nade Fest now give players regeneration to recover from environmental damage, falling damage, etc. +- Quad and Protection player color shells no longer change depending on team, avoiding confusion. +- `func_rotating`: Rotating map entities now explode non-player entities such as dropped items (no more blocked rotator in dm5). +- Player Feedback: Added Fragging Spree award—broadcasted message "x is on a fragging spree with x frags" per every 10 frags achieved without dying or killing a team mate. +- Techs: fixed not being able to pick up your dropped tech. +- Blaster and Grapple are now droppable and can spawn in world. +- Current weapon is now droppable. +- Smart weapon auto-switch now moves to SSG from SG, CG from MG, and never auto-switches to chainfist. +- Instant gametype changing (e.g., from FFA to TDM). +- DuelFire Damage has been changed to Haste: 50% faster movement, 50% faster weapon rate of fire. +- Many more! ## Rulesets Alter the gameplay balance by changing the ruleset. -### Quake II Rerelease (g_ruleset 1) +### Quake II Rerelease (`g_ruleset 1`) Everything remains as is. -### Muff Mode (g_ruleset 2) -This ruleset aims to tackle a few significant imbalances in the original game: - - **Plasma Beam** DM damage reduced from 15 to 10, maximum range limited to 768 units (same as LG in Q3) - - **Railgun**: restored to 150 damage in campaigns, rail knockback is now equal to damage*2 (no difference in DM). - - **Slugs/Railgun**: reduced slugs quantity from 10 to 5, should force more dynamic and challenging gameplay instead of an instagib approach to some matches. - - **Rockets**: removed randomised direct rocket damage value (rand 100-120), now a consistent 120. - - **Invulnerability** powerup has been replaced by Protection - player receives no splash damage, full protection from slime damage, third protection from lava, half direct damage after armor protection. - - **Adrenaline**: item now also increases max health by 5 during deathmatch. - - **Rebreather**: increased holding time from 30 to 45 seconds. - - **Auto Doc**: regen time increased from 500 ms to 1 sec, only regens either health or armor at a time - - **Power Armor**: CTF's 1 damage per cell now applies across deathmatch (originally 2 damage per cell in all DM bar CTF) -- this means same protection but consumes roughly twice the cells. - - Powerup spawn rules: 120 sec respawn default, 30-45 (randomised) initial spawn delay, global spawn and pickup sounds, spawn and pickup messages. +### Muff Mode (`g_ruleset 2`) +This ruleset tackles several imbalances in the original game: +- **Plasma Beam** DM damage reduced from 15 to 10; maximum range limited to 768 units (same as LG in Q3). +- **Railgun:** restored to 150 damage in campaigns; rail knockback equals damage*2 (no difference in DM). +- **Slugs/Railgun:** reduced slug quantity from 10 to 5 to encourage dynamic gameplay. +- **Rockets:** removed randomized direct rocket damage (rand 100-120); now a consistent 120. +- **Invulnerability** powerup replaced by Protection—no splash damage, full protection from slime damage, one-third protection from lava, half direct damage after armor protection. +- **Adrenaline:** item now also increases max health by 5 during deathmatch. +- **Rebreather:** increased holding time from 30 to 45 seconds. +- **Auto Doc:** regen time increased from 500 ms to 1 sec; only regens either health or armor at a time. +- **Power Armor:** CTF's 1 damage per cell now applies across deathmatch (originally 2 damage per cell in all DM bar CTF)—same protection but roughly twice the cell consumption. +- Powerup spawn rules: 120 sec respawn default, 30–45 (randomized) initial spawn delay, global spawn and pickup sounds, spawn and pickup messages. + +### Quake III Arena style (`g_ruleset 3`) +Inspired by Quake III Arena, this ruleset replicates key differences: +- Start with Machinegun and Rip Saw. +- Super Shotgun replaced by Shotgun. +- Weapon stats altered, including projectile velocity, spread, and damage. +- Ammo stats altered; ammo max is 200 for each type. +- Weapon pickup rule: +1 ammo if weapon is already held. +- Armor system: no tiers, +5 shard value, armor always provides 66% protection. +- Health and armor count down to max health. +- Spawning health bonus of 25. +- Removed Mega timer rule; Mega Health respawns after 60 seconds. +- **Invulnerability** powerup replaced by Protection—no splash damage, full protection from slime damage, one-third protection from lava, half direct damage after armor protection. +- Powerup spawn rules: 120 sec respawn default, 30–45 (randomized) initial spawn delay, global spawn and pickup sounds. -### Quake III Arena style (g_ruleset 3) -Inspired by Quake III Arena, this ruleset aims to replicate some of the differences: - - Start with Machinegun and Rip Saw - - Super Shotgun replaced by Shotgun - - Weapon stats altered, including projectile velocity, spread and damage - - Ammo stats altered, ammo max is 200 for each type. - - Weapon pickup rule: +1 ammo if weapon is already held. - - Armor system: no tiers, +5 shard value, armor always provides 66% protection - - Health and armor counts down to max health - - Spawning health bonus of 25. - - Removed Mega timer rule, Mega Health respawns after 60 seconds - - **Invulnerability** powerup has been replaced by Protection - player receives no splash damage, full protection from slime damage, third protection from lava, half direct damage after armor protection. - - Powerup spawn rules: 120 sec respawn default, 30-45 (randomised) initial spawn delay, global spawn and pickup sounds. - ## Commands and Variables ### Admin Commands -Use **[command] [arg]** for the below listed admin commands: - - **startmatch**: force the match to start, requires warmup. - - **endmatch**: force the match to end, requires a match in progress. - - **resetmatch**: force the match to reset to warmup, requires a match in progress. - - **lockteam [red/blue]**: locks a team from being joined - - **unlockteam [red/blue]**: unlocks a locked team so players can join - - **setteam [clientnum/name]**: forces a team change for a client - - **shuffle**: shuffles and balances the teams, resets the match. Requires a team gametype. - - **balance**: balances the teams without a shuffle, this switches last joined players from stacked team. Requires a team gametype. - - **vote [yes/no]**: passes or fails a voting in progress - - **spawn [entity_name] [spawn_args]**: spawns an entity, works the same as normal spawn server command but allows admins to do this without cheats enabled - - **map**: changes the level to the specified map, map needs to be a part of the map list. - - **nextmap**: forces level change to the next map. - - **map_restart**: restarts current level and session, applies latches cvar changes - - **gametype [gametype_name]**: changes gametype to selected option, then resets the level - - **ruleset **: changes gameplay style - - **readyall**: force all players to ready status (during readying warmup status) - - **unreadyall**: force all players to NOT ready status (during readying warmup status) - -### Client Commands - Player Configuration -Use **[command] [arg]** for the below listed client commands: - - **announcer**: toggles support of QL match announcer events (uses vo_evil set, needs converting to 22KHz PCM WAV) - - **fm**: toggle frag messages - - **help**: toggle help text drawing - - **id**: toggle crosshair ID drawing - - **kb**: toggle kill beeps - - **timer**: toggle match timer drawing - -### Client Commands - Gameplay - - **hook/unhook**: hook/unhook off-hand grapple - - **followkiller** : auto-follow killers when spectating (disabled by default) - - **followleader** : when spectating, auto-follows leading player - - **followpowerup** : auto-follows player picking up powerups when spectating (disabled by default) - - **forfeit**: forfeits a match (currently only in duels, requires g_allow_forfeit 1). - - **ready/notready**: sets ready status. - - **readyup**: toggles ready status. - - **callvote/cv**: calls a vote (use vote commands). - - **vote [yes/no]**: vote or veto a callvote. - - **maplist**: show server map list. - - **motd"": print the message of the day. - - **mymap**: add a map to the queue, must be a valid map from map list. - - **team [arg]**: selects a team, args: - - **blue/b**: select blue team - - **red/r**: select red team - - **auto/a**: auto-select team - - **free/f**: join free team (non-team games) - - **spectator/s**: spectate - - **time-in** : cuts a time out short - - **time-out** : call a time out, only 1 allowed per player and lasts for value set by g_dm_timeout_length (in seconds). **g_dm_timeout_length 0** disables time outs - - **follow [clientname/clientnum]**: follow a specific player. +Use **[command] [arg]** for the commands below: +- **startmatch**: force the match to start; requires warmup. +- **endmatch**: force the match to end; requires a match in progress. +- **resetmatch**: reset to warmup; requires a match in progress. +- **lockteam [red/blue]**: lock a team from being joined. +- **unlockteam [red/blue]**: unlock a locked team. +- **setteam [clientnum/name]**: force a team change for a client. +- **shuffle**: shuffle and balance teams, then reset the match (requires a team gametype). +- **balance**: balance teams without a shuffle; switches last-joined players from stacked team (requires a team gametype). +- **vote [yes/no]**: pass or fail a vote in progress. +- **spawn [entity_name] [spawn_args]**: spawn an entity (same as normal spawn server command) without requiring cheats. +- **map**: change the level to a specified map; map must be in the map list. +- **nextmap**: force level change to the next map. +- **map_restart**: restart current level and session; applies latched cvar changes. +- **gametype [gametype_name]**: change gametype, then reset the level. +- **ruleset **: change gameplay style. +- **readyall**: force all players to ready status (during readying warmup status). +- **unreadyall**: force all players to NOT ready status (during readying warmup status). + +### Client Commands – Player Configuration +Use **[command] [arg]**: +- **announcer**: toggle support of QL match announcer events (uses vo_evil set, needs converting to 22KHz PCM WAV). +- **fm**: toggle frag messages. +- **help**: toggle help text drawing. +- **id**: toggle crosshair ID drawing. +- **kb**: toggle kill beeps. +- **timer**: toggle match timer drawing. + +### Client Commands – Gameplay +- **hook/unhook**: hook/unhook off-hand grapple. +- **followkiller**: auto-follow killers when spectating (disabled by default). +- **followleader**: when spectating, auto-follow leading player. +- **followpowerup**: auto-follow player picking up powerups when spectating (disabled by default). +- **forfeit**: forfeits a match (currently only in duels, requires `g_allow_forfeit 1`). +- **ready/notready**: set ready status. +- **readyup**: toggle ready status. +- **callvote/cv**: call a vote (use vote commands). +- **vote [yes/no]**: vote or veto a callvote. +- **maplist**: show server map list. +- **motd""**: print the message of the day. +- **mymap**: add a map to the queue (must be a valid map from map list). +- **team [arg]**: select a team. Args: + - **blue/b**: select blue team + - **red/r**: select red team + - **auto/a**: auto-select team + - **free/f**: join free team (non-team games) + - **spectator/s**: spectate +- **time-in**: cut a time out short. +- **time-out**: call a time out; only 1 allowed per player and lasts for value set by `g_dm_timeout_length` (in seconds). `g_dm_timeout_length 0` disables time outs. +- **follow [clientname/clientnum]**: follow a specific player. ### Vote Commands -Use **callvote [command] [arg]** for the below listed vote commands: - - **map**: changes the level to the specified map, map needs to be a part of the map list. - - **nextmap**: forces level change to the next map. - - **restart**: force the match to reset to warmup, requires a match in progress. - - **gametype**: changes gametype to the specified type (ffa|duel|tdm|ctf|ca|ft|rr|strike|lms|horde) - - **timelimit**: changes timelimit to the minutes specified. - - **scorelimit**: changes scorelimit to the value specified. - - **shuffle**: shuffles and balances the teams, resets the match. Requires a team gametype. - - **balance**: balances the teams without a shuffle, this switches last joined players from stacked team. Requires a team gametype. - - **unlagged**: enables or disables lag compensation. - - **cointoss**: randomly returns either HEADS or TAILS. - - **random**: randomly returns a number from 2 to argument value, 100 max. - - **ruleset **: changes gameplay style - -### Cvar Changes - - g_dm_spawn_farthest: added an option, valid values are as follows: - - 0: high random (selects random spawn point except the 2 nearest) - - 1: half farthest (selects random spawn point from the furthest 50% of spawn points - - 2: spawn farthest to current position - - g_teamplay_force_join: renamed to g_dm_force_join - - sv_*: all mod-based sv_* cvars renamed g_* - - g_teleporter_nofreeze: renamed to g_teleporter_freeze, values do opposite effect (value of 1 freezes player), default is 0 (no freeze) - - deathmatch: default changed to 1 - -### New Cvars - - **bot_name_prefix**: allows changing bot name prefixes (blank to remove) (default "B|") - - **g_allow_admin**: allows administrative powers (default 1) - - **g_allow_custom_skins**: when set to 0, reverts any custom player models or skins to stock replacements (default: 0) - - **g_allow_forfeit**: Allows a player to forfeit the match, currently only for Duels (default 1) - - **g_allow_kill**: enables use of 'kill' suicide command (default 1) - - **g_allow_mymap**: allow mymap (map queuing function) (default 1) - - **g_allow_spec_vote**: Allows/prohibits voting from spectators. (default 1) - - **g_allow_vote_midgame**: Allows/prohibits voting during a match. (default 0) - - **g_allow_voting**: General control over voting, 0 prohibits any voting. (default 0) - - **g_arena_start_armor**: sets starting armor value in arena modes, range from 1-999, value affects armor tier (default 200) - - **g_arena_start_health**: sets starting health value in arena modes, range from 1-999 (default 200) - - **g_corpse_sink_time**: sets time in seconds for corpses to sink and disappear (default: 60) - - **g_dm_allow_no_humans**: when set to 1, allows matches to start or continue with only bots (default 1) - - **g_dm_do_readyup**: Enforce players to ready up to progress from match warmup stage (requires g_dm_do_warmup 1). (default 0) - - **g_dm_do_warmup**: Allow match warmup stage. (default 1) - - **g_dm_force_join**: replaces g_teamplay_force_join, the menu forces the cvar change so this gets around that, it now applies to regular DM too so the change makes sense. - - **g_dm_holdable_adrenaline** : when set to 1, allows holdable Adrenaline during deathmatch (default 1) - - **g_dm_no_self_damage**: when set to 1, disables any self damage after calculating knockback (default: 0) - - **g_dm_overtime**: Set stoppage time for each overtime session in seconds. Currently only applies to Duels. (default 120) - - **g_dm_powerup_drop**: when set to 1, drops carried powerups upon death (default: 1) - - **g_dm_powerups_minplayers**: Sets minimum current player load to allow powerup pickups, 0 to disable (default 0) - - **g_dm_respawn_delay_min**: the counterpart to g_dm_force_respawn_time, this sets a minimum respawn delay after dying (default: 1) - - **g_dm_respawn_point_min_dist**: sets minimum distance to respawn away from previous spawn point (default: 256, max = 512, 0 = disabled) - - **g_dm_respawn_point_min_dist_debug**: when set to 1, prints avoiding spawn points when g_dm_respawn_point_min_dist is used (default: 0) - - **g_dm_spawnpads**: Controls spawning of deathmatch spawn pads, removes pads when set to 0, 1 only removes in no item game modes, 2 forces pads in all dm matches. (default: 1) - - **g_drop_cmds**: bitflag operator, allows dropping of item types (default 7): - &1: allow dropping CTF flags - &2: allow dropping powerups - &4: allow dropping weapons and ammo - - **g_fast_doors**: When set to 1, doubles the default speed of standard and rotating doors (default 1) - - **g_frag_messages**: draw frag messages (default 1) - - **g_gametype**: cvar sets gametype by index number, this is the current list: - 0: Campaign (not used at present, use deathmatch 0 as usual) - 1. Free for All - 2. Duel - 3. Team Deathmatch - 4. Capture the Flag - 5. Clan Arena - 6. Freeze Tag (WIP) - 7. CaptureStrike - 8. Red Rover - 9. Last Man Standing - 10. Horde - 11. Race (WIP) - - **g_inactivity**: Values above 0 enables an inactivity timer for players, specifying number of seconds since last input to point of flagging the player as inactive. A warning is sent to the player 10 seconds before triggering and once triggered, the player is moved to spectators. Inactive clients are noted as such using the 'players' command. (default: 120) - - **g_instagib_splash**: enables a non-damaging explosion from railgun shots in instagib, allows for rail jumping or knocking foes about (default 0) - - **g_knockback_scale**: scales all knockback resulting from damage received (default 1.0) - - **g_ladder_steps**: Allow ladder step sounds, 1 = only in campaigns, 2 = always on (default 1) - - **g_match_lock**: when set to 1, prohibits joining the match while in progress (default 0) - - **g_motd_filename**: points to filename of message of the day file, reverts to default when blank (default motd.txt) - - **g_mover_speed_scale**: sets speed scaling factor for all movers in maps (doors, rotators, lifts etc.) (default: 1.0f) - - **g_no_powerups**: disable powerup pickups (Quad, Protection, Double, Haste, Invisibility, etc.) - - **g_owner_auto_join**: when set to 0, avoids auto-joining a match as lobby owner (default 1) - - **g_round_countdown**: sets round countdown time (in seconds) in round-based gametypes (default 10) - - **g_ruleset**: gameplay rules (default 2): - 1. Quake II Rerelease - 2. Muff Mode (rebalanced Q2Re) - 3. Quake III Arena style - - **g_showhelp**: when set to 1, prints a quick explanation about game modifications to players. (default: 1) - - **g_starting_armor**: sets starting armor for players on spawn (0-999) (default 0) - - **g_starting_health**: sets starting health for players on spawn (1-999) (default 100) - - **g_teamplay_allow_team_pick**: When set to 0, denies the ability to pick a specific team during teamplay. This changes the join menu accordingly. (default 0) - - **g_teamplay_auto_balance**: Set to 1, enforces team rebalancing during a match. The last joined player(s) of the stacked team switches teams but retain their scores. (default 1) - - **g_teamplay_force_balance**: When set to 1, prohibits joining a team with too many players. (default: 0) - - **g_teamplay_item_drop_notice**: When set to 1, sends team notice of item drops. (default 1) - - **g_teleporter_freeze**: When set to 0, does not freeze player velocity when teleporting. (default: 0) - - **g_vampiric_exp_min**: with vampiric damage enabled, sets expiration minimum health value (default 0) - - **g_vampiric_health_max**: sets maximum health cap from vampiric damage (default 999) - - **g_vampiric_percentile**: set health percentile bonus for vampiric damage (default 0.67f) - - **g_vote_flags**: Bitmask to disable specific vote options. (default 0) - - **g_vote_limit**: Sets maximum number of votes per match per client, 0 for no limit. (default 3) - - **g_warmup_ready_percentage**: in match mode, sets percentile of ready players out of total players required to start the match. Set to 0 to disable readying up. (default: 0.51f) - - **g_weapon_projection**: changes weapon projection offset. 0 = normal, 1 = always force central handedness, 2 = force central view projection. looks strange with view weapons. (default: 0) - - **hostname**: set string for server name, this gets printed at top of game menu for all to see. Limit this to 26 chars max. - - **maxplayers**: Set max number of players in the game (ie: non-spectators), it is capped to maxclients. In team games, team max size will be maxplayers/2 and rounded down. - - **mercylimit**: Sets score gap limit to end match, 0 to disable (default 0) - - **noplayerstime**: Sets time in minutes in which there have been no players to force a change of map, 0 to disable (default 0) - - **roundlimit**: sets number of round wins to win the match in round-based gametypes (default 8) - - **roundtimelimit**: sets round time limit (in minutes) in round-based gametypes (default 2) - -## Level Controls - -### New Items -- Personal Teleporter (item_teleporter): holdable item for deathmatch, teleports the players to a spawn point upon activation. -- Small ammo items for shells, bullets, rockets, cells and slugs (ie: ammo_bullets_small) -- Large ammo items for shells, bullets and cells (ie: ammo_bullets_large) -- Regeneration (item_regen): 30 second powerup regenerates your health up to 2x max health - -### Map Tweaks -Some entity overrides are included which add some subtle ambient sounds, mover sounds, intermission cams and gametype-specific item tweaks. - -### Map Entity Controls - * Map Item Replacement Control: - - **_[classname]** and **[mapname]__[classname]** user cvars to remove or replace specific DM map items (by classname) or only in specific maps if desired - * Save and load .ent files to override entire map entity string, located in baseq2/$g_entity_override_dir$/[mapname].ent: - - **g_entity_override_dir**: overrides entity override file subdir within baseq2 (default: maps) - - **g_entity_override_save**: when set to 1, will save entity override file upon map load (should one not already be loaded) (default: 0) - - **g_entity_override_load**: when set to 1, will load entity override file upon map load (default: 1) - * New entity keys**: "gametype" and "not_gametype": set conditional list of gametypes to respectively spawn or not spawn the entity in. The list can be comma or space separated. The following values correspond to a particular gametype: - campaign: Campaigns - ffa: Deathmatch - tournament: Duel - team: Team Deathmatch - ctf: Capture the Flag - ca: Clan Arena - ft: Freeze Tag - rr: Red Rover - lms: Last Man Standing - horde: Horde Mode - Example: "gametype" "ffa tournament" - this will spawn the entity only in deathmatches and duels. - * New entity keys**: "**notteam**" and "**notfree**": removes an entity from team gametypes or non-team gametypes respectively. - Example: "**notteam**" "1" - the entity will not spawn in team gametypes such as TDM, CTF, FreezeTag and Clan Arena. - * misc_teleporter: **"mins"/"maxs" "x y z"** entity keys to override teleport trigger size, removes teleporter pad if either keys are set - * new item spawnflag & 8: item spawns in suspended state (does not drop to floor) - * Hacky Map Fixes: - * bunk1: button for lift to ware2 now has a wait of -1 (never returns), stops co-op players from pushing the button again and toggling the lift! - * "nobots" and "nohumans": keys for info_player_deathmatch to avoid using for bots or humans respectively - -### Entity Keys -* SPAWNFLAGS: - - spawnflags & 8: suspends items (don't fall to ground) -* Worldspawn: - - **author** and **author2**: sets level author information, this can be seen in the server info submenu. - -### Entity Changes - - misc_nuke: now applies nuke effect (screen flash, earthquake) - - trigger_push: target a target_position/info_notnull to set a direction and apogee like in Q3, no target reverts to original behaviour - - trigger_key: does not remove inventory item in deathmatch, deathmatch or spawnflags 1 now allows multiple uses. - - trigger_coop_relay: annoying "all players must be present" feature in co-op has been removed as it proves a game breaker in games with afk players, always treated like a trigger_relay now - -### New Entities -- target_remove_powerups: - Takes away all the activator's powerups, techs, held items, keys and CTF flags. - -- target_remove_weapons: - Takes away all the activator's weapons and ammo (except blaster). - BLASTER : also remove blaster - -- target_give: - Gives the activator the targetted item. - -- target_delay: - Sets a delay before firing its targets. - "wait" seconds to pause before firing targets. - "random" delay variance, total delay = delay +/- random seconds - -- target_print: - Sends a center-printed message to clients. - "message" text to print - spawnflags: REDTEAM BLUETEAM PRIVATE - If "PRIVATE", only the activator gets the message. If no checks, all clients get the message. - -- target_setskill: - Set skill level. - "message" : skill level to set to (0-3) - Skill levels are: - 0 = Easy - 1 = Medium - 2 = Hard - 3 = Nightmare/Hard+ - -- target_score: - "count" number of points to adjust by, default 1. - The activator is given this many points. - spawnflags: TEAM - TEAM : also adjust team score - -- target_teleporter: - The activator will be teleported to the targetted destination. - If no target set, it will find a player spawn point instead. - -- target_relay: - Correctly named trigger_relay. - -- target_kill: - Kills the activator. - -- target_cvar: - When targetted sets a cvar to a value. - "cvar" : name of cvar to set - "cvarValue" : value to set cvar to - -- target_shooter_grenade: - Fires a grenade in the set direction when triggered. - dmg default is 120 - speed default is 600 - -- target_shooter_rocket: - Fires a rocket in the set direction when triggered. - dmg default is 120 - speed default is 600 - -- target_shooter_bfg: - Fires a BFG projectile in the set direction when triggered. - dmg default is 200 in DM, 500 in campaigns - speed default is 400 - -- target_shooter_prox: - Fires a prox mine in the set direction when triggered. - dmg default is 90 - speed default is 600 - -- target_shooter_ionripper: - Fires an ionripper projectile in the set direction when triggered. - dmg default is 20 in DM and 50 in campaigns - speed default is 800 - -- target_shooter_phalanx: - Fires a phalanx projectile in the set direction when triggered. - dmg default is 80 - speed default is 725 - -- target_shooter_flechette: - Fires a flechette in the set direction when triggered. - dmg default is 10 - speed default is 1150 - -- target_position: - Alias for info_notnull. - -- trigger_deathcount: - Fires targets only if minimum death count has been achieved in the level. - Deaths considered are monsters during campaigns and players during deathmatch. - "count" minimum number of deaths required (default 10) - spawnflags: REPEAT - REPEAT : repeats per every 'count' deaths - -- trigger_no_monsters: - Fires targets only if all monsters have been killed or none are present. - Auto-removed in deathmatch (except horde mode). - ONCE : will be removed after firing once - -- trigger_monsters: - Fires targets only if monsters are present in the level. - Auto-removed in deathmatch (except horde mode). - ONCE : will be removed after firing once - -## TODO: -- tastyspleen.net's mymap system: add support for dm flags -- gametype: Freeze Tag (WIP) -- Server-side player configs, stats, Elo, ranked matches, Elo team balancing (WIP) -- Gladiator bots -- Menu overhaul, adding voting, full admin controls, mymap, player config -- Quake II Santuary community for testing. A shout out particularly to Sata, TurboPtys_drk and Jobe for their help. - -## Credits: -- The Stingy Hat Games YouTube channel for their excellent modding tutorial, without it I would never be able to compile the damned source! -- Nightdive team for the impressive remaster, also some on the team who patiently answered all my annoying modding questions (particularly Paril, sponge, Edward850) -- Paril for some of the Horde Mode code (really just the spawn code), [link to Paril's mod](https://github.com/Paril/q2horde) -- id Software, both for Quake II and Quake III Arena (some code ported from the latter) -- ceeeKay for the eyecam code from Q2Eaks -- The Q2Re player community for bug spotting and general feedback +Use **callvote [command] [arg]**: +- **map**: change the level to the specified map; map must be in the map list. +- **nextmap**: force level change to the next map. +- **restart**: force the match to reset to warmup; requires a match in progress. +- **gametype**: change gametype to the specified type (ffa|duel|tdm|ctf|ca|ft|rr|strike|lms|horde). +- **timelimit**: change timelimit to the minutes specified. +- **scorelimit**: change scorelimit to the value specified. +- **shuffle**: shuffle and balance the teams, then reset the match (requires a team gametype). +- **balance**: balance the teams without a shuffle; switches last-joined players from stacked team (requires a team gametype). +- **unlagged**: enable or disable lag compensation. +- **cointoss**: randomly returns either HEADS or TAILS. +- **random**: randomly returns a number from 2 to argument value, 100 max. +- **ruleset **: change gameplay style. diff --git a/assets/img/muffmode.png b/assets/img/muffmode.png new file mode 100644 index 0000000..aad5648 Binary files /dev/null and b/assets/img/muffmode.png differ diff --git a/docs/custom_gravity_tests.md b/docs/custom_gravity_tests.md new file mode 100644 index 0000000..a3e53f2 --- /dev/null +++ b/docs/custom_gravity_tests.md @@ -0,0 +1,8 @@ +# Custom Gravity Push Coverage + +A manual check to ensure custom gravity values survive push interactions: + +1. Spawn a movable entity (e.g., a crate) and set its `gravity` to a non-default value such as `0.5`. +2. Position the entity on top of a moving platform so it will be pushed during the simulation. +3. Run a push movement tick and confirm the entity's `gravity` remains `0.5` after the push completes. +4. Verify triggers along the movement path still fire for the pushed entity. diff --git a/docs/intermission_flow_test.md b/docs/intermission_flow_test.md new file mode 100644 index 0000000..9199ef1 --- /dev/null +++ b/docs/intermission_flow_test.md @@ -0,0 +1,17 @@ +# Intermission Flow Test: Scoreboard Persistence + +## Objective +Confirm that deathmatch players remain on the scoreboard view throughout intermission instead of being forced back to other HUD layouts. + +## Preconditions +- Start a multiplayer deathmatch session with at least one human or bot participant. +- Reach a state where fraglimit or timelimit is close to completion so intermission can be triggered quickly. + +## Steps +1. Play until the match ends naturally (or use an admin command to end the match) to enter intermission. +2. Observe the client HUD as intermission begins. +3. Remain idle during the entire intermission countdown without pressing the score key or toggling menus. + +## Expected Result +- The scoreboard remains visible for the entire intermission without reverting to another HUD state. +- Player input is not required to keep the scoreboard displayed during intermission. diff --git a/docs/regression_tests.md b/docs/regression_tests.md new file mode 100644 index 0000000..126f947 --- /dev/null +++ b/docs/regression_tests.md @@ -0,0 +1,12 @@ +# Regression Tests + +## Mover collision resolution +- **Purpose:** Prevent movers from disappearing when collision traces reference freed or invalid entities during ground detection. +- **Setup:** + - Create a simple map with a platform mover intersecting with another entity that can be removed mid-move. + - Instrument the mover so that a collision resolution pass occurs immediately after the other entity is freed. +- **Steps:** + 1. Trigger the mover so it begins translating through the space occupied by the soon-to-be-removed entity. + 2. Remove the obstructing entity during the mover's travel to force a collision trace against an invalid reference. + 3. Allow the mover to complete its motion. +- **Expected result:** The mover finishes its path without being unlinked or lost from the world after the collision trace references an invalid entity. diff --git a/src/game.sln b/src/MuffMode.sln similarity index 100% rename from src/game.sln rename to src/MuffMode.sln diff --git a/src/bots/bot_utils.cpp b/src/bots/bot_utils.cpp index 8c7048a..c6764a2 100644 --- a/src/bots/bot_utils.cpp +++ b/src/bots/bot_utils.cpp @@ -16,6 +16,7 @@ static void Player_UpdateState(gentity_t *player) { const client_persistant_t &persistant = player->client->pers; player->sv.ent_flags = SVFL_NONE; + player->sv.objective_state = objective_state_t::None; if (player->groundentity != nullptr || (player->flags & FL_PARTIALGROUND) != 0) { player->sv.ent_flags |= SVFL_ONGROUND; } else { @@ -171,6 +172,7 @@ Monster_UpdateState */ static void Monster_UpdateState(gentity_t *monster) { monster->sv.ent_flags = SVFL_NONE; + monster->sv.objective_state = objective_state_t::None; if (monster->groundentity != nullptr) { monster->sv.ent_flags |= SVFL_ONGROUND; } @@ -229,6 +231,7 @@ Item_UpdateState static void Item_UpdateState(gentity_t *item) { item->sv.ent_flags = SVFL_IS_ITEM; item->sv.respawntime = 0; + item->sv.objective_state = objective_state_t::None; if (item->team != nullptr) { item->sv.ent_flags |= SVFL_IN_TEAM; @@ -251,7 +254,37 @@ static void Item_UpdateState(gentity_t *item) { const item_id_t itemID = item->item->id; if (itemID == IT_FLAG_RED || itemID == IT_FLAG_BLUE) { item->sv.ent_flags |= SVFL_IS_OBJECTIVE; - // TODO: figure out if the objective is dropped/carried/home... + item->sv.team = (itemID == IT_FLAG_RED) ? TEAM_RED : TEAM_BLUE; + + if (!item->sv.init) { + item->sv.start_origin = item->s.origin; + } + + item->sv.end_origin = item->s.origin; + + const bool isDroppedFlag = item->spawnflags.has(SPAWNFLAG_ITEM_DROPPED) || item->owner != nullptr; + const bool isHiddenFlag = (item->solid == SOLID_NOT) || ((item->svflags & SVF_NOCLIENT) != 0); + const bool isRespawning = ((item->svflags & SVF_RESPAWNING) != 0) || ((item->flags & FL_RESPAWN) != 0); + + if (isDroppedFlag) { + item->sv.ent_flags |= SVFL_OBJECTIVE_DROPPED; + item->sv.objective_state = objective_state_t::Dropped; + + if (item->nextthink.milliseconds() > 0) { + const gtime_t pendingRespawnTime = (item->nextthink - level.time); + item->sv.respawntime = static_cast(pendingRespawnTime.milliseconds()); + } + } else if (isHiddenFlag && isRespawning) { + item->sv.ent_flags |= SVFL_OBJECTIVE_CARRIED; + item->sv.objective_state = objective_state_t::Carried; + + if (item->sv.respawntime == 0) { + item->sv.respawntime = Item_UnknownRespawnTime; + } + } else { + item->sv.ent_flags |= SVFL_OBJECTIVE_AT_BASE; + item->sv.objective_state = objective_state_t::AtBase; + } } // always need to update these for items, since random item spawning @@ -275,6 +308,7 @@ Trap_UpdateState static void Trap_UpdateState(gentity_t *danger) { danger->sv.ent_flags = SVFL_TRAP_DANGER; danger->sv.velocity = danger->velocity; + danger->sv.objective_state = objective_state_t::None; if (danger->owner != nullptr && danger->owner->client != nullptr) { player_skinnum_t pl_skinnum; @@ -314,6 +348,7 @@ Mover_UpdateState static void Mover_UpdateState(gentity_t *entity) { entity->sv.ent_flags = SVFL_NONE; entity->sv.health = entity->health; + entity->sv.objective_state = objective_state_t::None; if (entity->takedamage) { entity->sv.ent_flags |= SVFL_TAKES_DAMAGE; diff --git a/src/cg_screen.cpp b/src/cg_screen.cpp index 5657789..ef8e74c 100644 --- a/src/cg_screen.cpp +++ b/src/cg_screen.cpp @@ -112,14 +112,29 @@ void CG_ClearNotify(int32_t isplit) { // if the top one is expired, cycle the ones ahead backwards (since // the times are always increasing) +/* +============= +CG_Notify_CheckExpire + +Expire stale notify entries with bounded swapping to prevent corrupted times +from triggering excessive iterations. +============= +*/ static void CG_Notify_CheckExpire(hud_data_t &data) { - while (data.notify[0].is_active && data.notify[0].time < cgi.CL_ClientTime()) { + size_t iterations = 0; + + while (data.notify[0].is_active && data.notify[0].time < cgi.CL_ClientTime() && iterations < MAX_NOTIFY) { data.notify[0].is_active = false; for (size_t i = 1; i < MAX_NOTIFY; i++) if (data.notify[i].is_active) std::swap(data.notify[i], data.notify[i - 1]); + + iterations++; } + + if (iterations >= MAX_NOTIFY && data.notify[0].is_active && data.notify[0].time < cgi.CL_ClientTime()) + data.notify[0].is_active = false; } // add notify to list @@ -694,11 +709,14 @@ static void CG_DrawTable(int x, int y, uint32_t width, uint32_t height, int32_t } /* -================= +============= CG_TimeStringMs -================= + +Format a client-visible timer string with millisecond precision. +============= */ static const char *CG_TimeStringMs(const int msec) { + static char buffer[32]; int hours, mins, seconds, ms = msec; seconds = ms / 1000; @@ -709,10 +727,12 @@ static const char *CG_TimeStringMs(const int msec) { mins -= hours * 60; if (hours > 0) { - return G_Fmt("{}:{:02}:{:02}.{}", hours, mins, seconds, ms).data(); + G_FmtTo(buffer, "{}:{:02}:{:02}.{}", hours, mins, seconds, ms); } else { - return G_Fmt("{:02}:{:02}.{}", mins, seconds, ms).data(); + G_FmtTo(buffer, "{:02}:{:02}.{}", mins, seconds, ms); } + + return buffer; } /* diff --git a/src/freeze.c b/src/freeze.c deleted file mode 100644 index e23c58c..0000000 --- a/src/freeze.c +++ /dev/null @@ -1,829 +0,0 @@ -#include "g_local.h" -#include "m_player.h" -//#include "stdlog.h" -//#include "gslog.h" - -#define nteam 5 -#define game_loop for (i = 0; i < maxclients->value; i++) -#define team_loop for (i = red; i < none; i++) -#define _team_loop for (i = red; i <= none; i++) -#define map_loop for (i = 0; i < 64; i++) -#define far_off 100000000 - -#define stat_identify 18 -#define stat_red 19 -#define stat_red_arrow 23 - -#define _shotgun 0x00000001 // 1 -#define _supershotgun 0x00000002 // 2 -#define _machinegun 0x00000004 // 4 -#define _chaingun 0x00000008 // 8 -#define _grenadelauncher 0x00000010 // 16 -#define _rocketlauncher 0x00000020 // 32 -#define _hyperblaster 0x00000040 // 64 -#define _railgun 0x00000080 // 128 - -#define ready_help 0x00000001 -#define thaw_help 0x00000002 -#define frozen_help 0x00000004 -#define chase_help 0x00000008 - -#define is_motd 0x00000001 -#define end_vote 0x00000002 -#define mapnohook 0x00000004 -#define everyone_ready 0x00000008 - -cvar_t *item_respawn_time; -cvar_t *hook_max_len; -cvar_t *hook_rpf; -cvar_t *hook_min_len; -cvar_t *hook_speed; -cvar_t *point_limit; -cvar_t *new_team_count; -cvar_t *frozen_time; -cvar_t *start_weapon; -cvar_t *start_armor; -cvar_t *random_map; -cvar_t *vote_percent; -cvar_t *use_ready; -cvar_t *grapple_wall; -static int gib_queue; -static int team_max_count; -static int moan[8]; -static int lame_hack; -static float ready_time; - -qboolean playerDamage(edict_t *targ, edict_t *attacker, int damage) { - if (!targ->client) - return false; - if (meansOfDeath == MOD_TELEFRAG) - return false; - if (!attacker->client) - return false; - if (targ->client->hookstate && random() < 0.2) - targ->client->hookstate = 0; - if (targ->health > 0) { - if (!(lame_hack & everyone_ready)) { - if (!(attacker->client->resp.help & ready_help)) { - attacker->client->showscores = false; - attacker->client->resp.help |= ready_help; - gi.centerprintf(attacker, "Waiting for everyone to be ready."); - gi.sound(attacker, CHAN_AUTO, gi.soundindex("misc/talk1.wav"), 1, ATTN_STATIC, 0); - } - return true; - } - if (targ == attacker) - return false; - if (targ->client->resp.team != attacker->client->resp.team && targ->client->respawn_time + 3 < level.time) - return false; - } else { - if (targ->client->frozen) { - if (random() < 0.1) - ThrowGib(targ, "models/objects/debris2/tris.md2", damage, GIB_ORGANIC); - return true; - } else - return false; - } - if ((int)(dmflags->value) & DF_NO_FRIENDLY_FIRE) - return true; - meansOfDeath |= MOD_FRIENDLY_FIRE; - return false; -} - -qboolean freezeCheck(edict_t *ent) { - if (ent->deadflag) - return false; - if (meansOfDeath & MOD_FRIENDLY_FIRE) - return false; - switch (meansOfDeath) { - case MOD_FALLING: - case MOD_SLIME: - case MOD_LAVA: - if (random() < 0.08) - break; - case MOD_SUICIDE: - case MOD_CRUSH: - case MOD_WATER: - case MOD_EXIT: - case MOD_TRIGGER_HURT: - case MOD_BFG_LASER: - case MOD_BFG_EFFECT: - case MOD_TELEFRAG: - return false; - } - return true; -} - -void freezeAnim(edict_t *ent) { - ent->client->anim_priority = ANIM_DEATH; - if (ent->client->ps.pmove.pm_flags & PMF_DUCKED) { - if (rand() & 1) { - ent->s.frame = FRAME_crpain1 - 1; - ent->client->anim_end = FRAME_crpain1 + rand() % 4; - } else { - ent->s.frame = FRAME_crdeath1 - 1; - ent->client->anim_end = FRAME_crdeath1 + rand() % 5; - } - } else { - switch (rand() % 8) { - case 0: - ent->s.frame = FRAME_run1 - 1; - ent->client->anim_end = FRAME_run1 + rand() % 6; - break; - case 1: - ent->s.frame = FRAME_pain101 - 1; - ent->client->anim_end = FRAME_pain101 + rand() % 4; - break; - case 2: - ent->s.frame = FRAME_pain201 - 1; - ent->client->anim_end = FRAME_pain201 + rand() % 4; - break; - case 3: - ent->s.frame = FRAME_pain301 - 1; - ent->client->anim_end = FRAME_pain301 + rand() % 4; - break; - case 4: - ent->s.frame = FRAME_jump1 - 1; - ent->client->anim_end = FRAME_jump1 + rand() % 6; - break; - case 5: - ent->s.frame = FRAME_death101 - 1; - ent->client->anim_end = FRAME_death101 + rand() % 6; - break; - case 6: - ent->s.frame = FRAME_death201 - 1; - ent->client->anim_end = FRAME_death201 + rand() % 6; - break; - case 7: - ent->s.frame = FRAME_death301 - 1; - ent->client->anim_end = FRAME_death301 + rand() % 6; - break; - } - } - - if (random() < 0.2 && !IsFemale(ent)) - gi.sound(ent, CHAN_BODY, gi.soundindex("player/lava2.wav"), 1, ATTN_NORM, 0); - else - gi.sound(ent, CHAN_BODY, gi.soundindex("boss3/d_hit.wav"), 1, ATTN_NORM, 0); - ent->client->frozen = true; - ent->client->frozen_time = level.time + frozen_time->value; - ent->client->resp.thawer = NULL; - ent->client->thaw_time = far_off; - if (random() > 0.3) - ent->client->hookstate -= ent->client->hookstate & (grow_on | shrink_on); - ent->deadflag = DEAD_DEAD; - gi.linkentity(ent); -} - -qboolean gibCheck() { - if (gib_queue > 35) - return true; - else { - gib_queue++; - return false; - } -} - -void gibThink(edict_t *ent) { - gib_queue--; - G_FreeEdict(ent); -} - -static void playerView(edict_t *ent) { - int i; - edict_t *other; - vec3_t ent_origin; - vec3_t forward; - vec3_t other_origin; - vec3_t dist; - trace_t trace; - float dot; - float other_dot; - edict_t *best_other; - - if (level.framenum & 7) - return; - - other_dot = 0.3; - best_other = NULL; - VectorCopy(ent->s.origin, ent_origin); - ent_origin[2] += ent->viewheight; - AngleVectors(ent->s.angles, forward, NULL, NULL); - - game_loop - { - other = g_edicts + 1 + i; - if (!other->inuse) - continue; - if (other->client->resp.spectator) - continue; - if (other == ent) - continue; - if (other->light_level < 10) - continue; - if (other->health <= 0 && !other->client->frozen) - continue; - VectorCopy(other->s.origin, other_origin); - other_origin[2] += other->viewheight; - VectorSubtract(other_origin, ent_origin, dist); - if (VectorLength(dist) > 800) - continue; - trace = gi.trace(ent_origin, vec3_origin, vec3_origin, other_origin, ent, MASK_OPAQUE); - if (trace.fraction != 1) - continue; - VectorNormalize(dist); - dot = DotProduct(dist, forward); - if (dot > other_dot) { - other_dot = dot; - best_other = other; - } - } - if (best_other) - ent->client->viewed = best_other; - else - ent->client->viewed = NULL; -} - -static void playerThaw(edict_t *ent) { - int i; - edict_t *other; - int j; - vec3_t eorg; - - game_loop - { - other = g_edicts + 1 + i; - if (!other->inuse) - continue; - if (other->client->resp.spectator) - continue; - if (other == ent) - continue; - if (other->health <= 0) - continue; - if (other->client->resp.team != ent->client->resp.team) - continue; - for (j = 0; j < 3; j++) - eorg[j] = ent->s.origin[j] - (other->s.origin[j] + (other->mins[j] + other->maxs[j]) * 0.5); - if (VectorLength(eorg) > MELEE_DISTANCE) - continue; - if (!(other->client->resp.help & thaw_help)) { - other->client->showscores = false; - other->client->resp.help |= thaw_help; - gi.centerprintf(other, "Wait here a second to free them."); - gi.sound(other, CHAN_AUTO, gi.soundindex("misc/talk1.wav"), 1, ATTN_STATIC, 0); - } - ent->client->resp.thawer = other; - if (ent->client->thaw_time == far_off) { - ent->client->thaw_time = level.time + 3; - gi.sound(ent, CHAN_BODY, gi.soundindex("world/steam3.wav"), 1, ATTN_NORM, 0); - } - return; - } - ent->client->resp.thawer = NULL; - ent->client->thaw_time = far_off; -} - -static void playerBreak(edict_t *ent, int force) { - int n; - - ent->client->respawn_time = level.time + 1; - if (ent->waterlevel == 3) - gi.sound(ent, CHAN_BODY, gi.soundindex("misc/fhit3.wav"), 1, ATTN_NORM, 0); - else - gi.sound(ent, CHAN_BODY, gi.soundindex("world/brkglas.wav"), 1, ATTN_NORM, 0); - n = rand() % (gib_queue > 10 ? 5 : 3); - if (rand() & 1) { - switch (n) { - case 0: - ThrowGib(ent, "models/objects/gibs/arm/tris.md2", force, GIB_ORGANIC); - break; - case 1: - ThrowGib(ent, "models/objects/gibs/bone/tris.md2", force, GIB_ORGANIC); - break; - case 2: - ThrowGib(ent, "models/objects/gibs/bone2/tris.md2", force, GIB_ORGANIC); - break; - case 3: - ThrowGib(ent, "models/objects/gibs/chest/tris.md2", force, GIB_ORGANIC); - break; - case 4: - ThrowGib(ent, "models/objects/gibs/leg/tris.md2", force, GIB_ORGANIC); - break; - } - } - while (n--) - ThrowGib(ent, "models/objects/debris1/tris.md2", force, GIB_ORGANIC); - ent->takedamage = DAMAGE_NO; - ent->movetype = MOVETYPE_TOSS; - ThrowClientHead(ent, force); - ent->client->frozen = false; - freeze[ent->client->resp.team].update = true; - ent->client->ps.stats[STAT_CHASE] = 0; -} - -static void playerUnfreeze(edict_t *ent) { - if (level.time > ent->client->frozen_time && level.time > ent->client->respawn_time) { - playerBreak(ent, 50); - return; - } - if (ent->waterlevel == 3 && !(level.framenum & 3)) - ent->client->frozen_time -= 0.15; - if (level.time > ent->client->thaw_time) { - if (!ent->client->resp.thawer || !ent->client->resp.thawer->inuse) { - ent->client->resp.thawer = NULL; - ent->client->thaw_time = far_off; - } else { - ent->client->resp.thawer->client->resp.score++; - ent->client->resp.thawer->client->resp.thawed++; - sl_LogScore(&gi, ent->client->resp.thawer->client->pers.netname, NULL, "Thaw", NULL, 1, level.time, ent->client->resp.thawer->client->ping); - freeze[ent->client->resp.team].thawed++; - if (rand() & 1) - gi.bprintf(PRINT_HIGH, "%s thaws %s like a package of frozen peas.\n", ent->client->resp.thawer->client->pers.netname, ent->client->pers.netname); - else - gi.bprintf(PRINT_HIGH, "%s evicts %s from their igloo.\n", ent->client->resp.thawer->client->pers.netname, ent->client->pers.netname); - playerBreak(ent, 100); - } - } -} - -static void playerMove(edict_t *ent) { - int i; - edict_t *other; - vec3_t forward; - float dist; - int j; - vec3_t eorg; - - if (ent->client->hookstate) - return; - AngleVectors(ent->s.angles, forward, NULL, NULL); - game_loop - { - other = g_edicts + 1 + i; - if (!other->inuse) - continue; - if (other->client->resp.spectator) - continue; - if (other == ent) - continue; - if (!other->client->frozen) - continue; - if (other->client->resp.team == ent->client->resp.team) - continue; - if (other->client->hookstate) - continue; - for (j = 0; j < 3; j++) - eorg[j] = ent->s.origin[j] - (other->s.origin[j] + (other->mins[j] + other->maxs[j]) * 0.5); - dist = VectorLength(eorg); - if (dist > MELEE_DISTANCE) - continue; - VectorScale(forward, 600, other->velocity); - other->velocity[2] = 200; - gi.linkentity(other); - } -} - -void freezeMain(edict_t *ent) { - if (!ent->inuse) - return; - playerView(ent); - if (ent->client->resp.spectator) - return; - if (ent->client->frozen) { - playerThaw(ent); - playerUnfreeze(ent); - } else if (ent->health > 0) - playerMove(ent); -} - -void freezeScore(edict_t *ent, edict_t *killer) { - int i, j, k; - edict_t *other; - int team, score; - int total[nteam]; - int sorted[nteam][MAX_CLIENTS]; - int sortedscores[nteam][MAX_CLIENTS]; - int count, best_total, best_team; - int x, y; - int move_over; - char string[1400]; - int stringlength; - char *tag; - char entry[1024]; - gclient_t *cl; - - _team_loop - total[i] = 0; - game_loop - { - other = g_edicts + 1 + i; - if (!other->inuse) - continue; - if (other->client->resp.spectator) - team = none; - else - team = other->client->resp.team; - score = other->client->resp.score; - for (j = 0; j < total[team]; j++) { - if (score > sortedscores[team][j]) - break; - } - for (k = total[team]; k > j; k--) { - sorted[team][k] = sorted[team][k - 1]; - sortedscores[team][k] = sortedscores[team][k - 1]; - } - sorted[team][j] = i; - sortedscores[team][j] = score; - total[team]++; - } - - for (;;) { - count = 0; - team_loop - count += 2 + total[i]; - if (count <= 48) - break; - best_total = 0; - team_loop - if (total[i] > best_total) { - best_total = total[i]; - best_team = i; - } - if (best_total) - total[best_team]--; - } - - x = 0; - y = 32; - - count = 4; - _team_loop - if (total[i]) - count += 3 + total[i]; - move_over = (int)(count / 2) * 8; - - string[0] = 0; - stringlength = strlen(string); - - _team_loop - { - if (i == red) - tag = "k_redkey"; - else if (i == blue) - tag = "k_bluekey"; - else if (i == green) - tag = "k_security"; - else - tag = "k_powercube"; - - if (i == none) - Com_sprintf(entry, sizeof(entry), "xv %d yv %d string \"%6.6s\" ", x, y, freeze_team_[i]); - else - Com_sprintf(entry, sizeof(entry), "xv %d yv %d if %d picn %s endif string \"%6.6s Sco%3d Tha%3d\" ", x, y, 19 + i, tag, freeze_team_[i], freeze[i].score, freeze[i].thawed); - k = strlen(entry); - if (stringlength + k > 1024) - break; - if (total[i]) { - strcpy(string + stringlength, entry); - stringlength += k; - y += 16; - } else - continue; - for (j = 0; j < total[i]; j++) { - if (y >= 224) { - if (x == 0) - x = 160; - else - break; - y = 32; - } - cl = &game.clients[sorted[i][j]]; - Com_sprintf(entry, sizeof(entry), "ctf %d %d %d %d %d ", x, y, sorted[i][j], cl->resp.score, level.intermissiontime ? cl->resp.thawed : (cl->ping > 999 ? 999 : cl->ping)); - if (cl->frozen) - sprintf(entry + strlen(entry), "xv %d yv %d string2 \"/\" ", x + 56, y); - k = strlen(entry); - if (stringlength + k > 1024) - break; - strcpy(string + stringlength, entry); - stringlength += k; - y += 8; - } - Com_sprintf(entry, sizeof(entry), "xv %d yv %d string \"--------------------\" ", x, y); - k = strlen(entry); - if (stringlength + k > 1024) - break; - strcpy(string + stringlength, entry); - stringlength += k; - if (y >= 208 || (y >= move_over && x == 0)) { - if (x == 0) - x = 160; - else - break; - y = 32; - } else - y += 8; - } - - gi.WriteByte(svc_layout); - gi.WriteString(string); -} - -void freezeIntermission(void) { - int i, j, k; - int team; - - i = j = k = 0; - team_loop - if (freeze[i].score > j) - j = freeze[i].score; - - team_loop - if (freeze[i].score == j) { - k++; - team = i; - } - - if (k > 1) { - i = j = k = 0; - team_loop - if (freeze[i].thawed > j) - j = freeze[i].thawed; - - team_loop - if (freeze[i].thawed == j) { - k++; - team = i; - } - } - if (k != 1) { - gi.bprintf(PRINT_HIGH, "Stalemate!\n"); - return; - } - gi.bprintf(PRINT_HIGH, "%s team is the winner!\n", freeze_team[team]); - team_loop - freeze[i].win_time = level.time; - freeze[team].win_time = far_off; -} - -char *makeGreen(char *s) { - static char string[16]; - int i; - - if (!*s) - return ""; - for (i = 0; i < 15 && *s; i++, s++) { - string[i] = *s; - string[i] |= 0x80; - } - string[i] = 0; - return string; -} - -static void playerHealth(edict_t *ent) { - int n; - - for (n = 0; n < game.num_items; n++) - ent->client->pers.inventory[n] = 0; - - ent->client->quad_framenum = 0; - ent->client->invincible_framenum = 0; - ent->flags &= ~FL_POWER_ARMOR; - - ent->health = ent->client->pers.max_health; - - ent->s.sound = 0; - ent->client->weapon_sound = 0; -} - -static void breakTeam(int team) { - int i; - edict_t *ent; - float break_time; - - break_time = level.time; - game_loop - { - ent = g_edicts + 1 + i; - if (!ent->inuse) - continue; - if (ent->client->frozen) { - if (ent->client->resp.team != team && team_max_count >= 3) - continue; - ent->client->frozen_time = break_time; - break_time += 0.25; - continue; - } - if (ent->health > 0 && team_max_count < 3) { - playerHealth(ent); - playerWeapon(ent); - } - } - freeze[team].break_time = break_time + 1; - if (rand() & 1) - gi.bprintf(PRINT_HIGH, "%s team was run circles around by their foe.\n", freeze_team[team]); - else - gi.bprintf(PRINT_HIGH, "%s team was less than a match for their foe.\n", freeze_team[team]); -} - -static void updateTeam(int team) { - int i; - edict_t *ent; - int frozen, alive; - char small[32]; - int play_sound = 0; - - frozen = alive = 0; - game_loop - { - ent = g_edicts + 1 + i; - if (!ent->inuse) - continue; - if (ent->client->resp.spectator) - continue; - if (ent->client->resp.team != team) - continue; - if (ent->client->frozen) - frozen++; - if (ent->health > 0) - alive++; - } - freeze[team].frozen = frozen; - freeze[team].alive = alive; - - if (frozen && !alive) { - team_loop - { - if (freeze[i].alive) { - play_sound++; - freeze[i].score++; - freeze[i].win_time = level.time + 5; - freeze[i].update = true; - } - } - breakTeam(team); - - if (play_sound <= 1) - gi.positioned_sound(vec3_origin, world, CHAN_VOICE | CHAN_RELIABLE, gi.soundindex("world/xian1.wav"), 1, ATTN_NONE, 0); - } - - Com_sprintf(small, sizeof(small), " %s%3d/%3d", freeze_team__[team], freeze[team].score, freeze[team].alive); - // if (!(freeze[team].alive == 1 && freeze[team].frozen)) - // makeGreen(small); - gi.configstring(CS_GENERAL + team, small); -} - -qboolean endCheck() { - int i; - - if (!(level.framenum & 31)) { - if (new_team_count->value) { - int _new_team_count = new_team_count->value; - int total[nteam]; - - _team_loop - total[i] = freeze[i].alive + freeze[i].frozen; - - if (total[yellow]) - team_max_count = 4; - else if (total[red] >= _new_team_count && total[blue] >= _new_team_count) { - if (total[green] >= _new_team_count) - team_max_count = 4; - else - team_max_count = 3; - } else if (total[green]) - team_max_count = 3; - else - team_max_count = 0; - } else - team_max_count = 0; - } - - if (use_ready->value && !(lame_hack & everyone_ready)) { - switch ((int)(ready_time / FRAMETIME) - level.framenum) { - case 150: - case 100: - case 50: - case 40: - case 30: - case 20: - gi.bprintf(PRINT_HIGH, "Begin in %d seconds!\n", (int)(((ready_time / FRAMETIME) - level.framenum) * FRAMETIME)); - } - if (level.time > ready_time) { - edict_t *ent; - - lame_hack |= everyone_ready; - gi.bprintf(PRINT_HIGH, "Begin!\n"); - game_loop - { - ent = g_edicts + 1 + i; - if (!ent->inuse) - continue; - if (ent->client->resp.spectator) - continue; - if (ent->health > 0) { - playerHealth(ent); - playerWeapon(ent); - } - } - } - } else - lame_hack |= everyone_ready; - - team_loop - if (freeze[i].update && level.time > freeze[i].last_update) { - updateTeam(i); - freeze[i].update = false; - freeze[i].last_update = level.time + 3; - } - - if (point_limit->value) { - int _point_limit; - - _point_limit = point_limit->value; - if (team_max_count >= 3) - _point_limit *= 3; - team_loop - if (freeze[i].score >= _point_limit) - return true; - } - if (lame_hack & end_vote) - return true; - - return false; -} - -void freezeRespawn(edict_t *ent, float delay) { - if (item_respawn_time->value) - SetRespawn(ent, item_respawn_time->value); - else - SetRespawn(ent, delay); -} - -void playerShell(edict_t *ent, int team) { - ent->s.effects |= EF_COLOR_SHELL; - ent->s.renderfx |= RF_SHELL_RED | RF_SHELL_GREEN | RF_SHELL_BLUE; - -} - -void freezeEffects(edict_t *ent) { - if (level.intermissiontime) - return; - if (!ent->client->frozen) - return; - if (!ent->client->resp.thawer || level.framenum & 8) - playerShell(ent, ent->client->resp.team); -} - -void playerStat(edict_t *ent) { - int i; - - if (ent->client->viewed && ent->client->viewed->inuse) { - int playernum = ent->client->viewed - g_edicts - 1; - - ent->client->ps.stats[stat_identify] = CS_PLAYERSKINS + playernum; - } else - ent->client->ps.stats[stat_identify] = 0; - - team_loop - { - if (((i == green && team_max_count < 3) || (i == yellow && team_max_count < 4)) || - (freeze[i].win_time > level.time && !(level.framenum & 8))) { - ent->client->ps.stats[stat_red + i] = 0; - ent->client->ps.stats[stat_red_arrow + i] = 0; - continue; - } - - ent->client->ps.stats[stat_red + i] = CS_GENERAL + i; - if (ent->client->resp.team == i && !ent->client->resp.spectator) - ent->client->ps.stats[stat_red_arrow + i] = CS_GENERAL + 5; - else - ent->client->ps.stats[stat_red_arrow + i] = 0; - } -} - -void freezeSpawn() { - int i; - - loadMessage(); - loadMap(); - - memset(freeze, 0, sizeof(freeze)); - team_loop - freeze[i].update = true; - lame_hack &= ~everyone_ready; - ready_time = far_off; - gib_queue = 0; - - moan[0] = gi.soundindex("insane/insane1.wav"); - moan[1] = gi.soundindex("insane/insane2.wav"); - moan[2] = gi.soundindex("insane/insane3.wav"); - moan[3] = gi.soundindex("insane/insane4.wav"); - moan[4] = gi.soundindex("insane/insane6.wav"); - moan[5] = gi.soundindex("insane/insane8.wav"); - moan[6] = gi.soundindex("insane/insane9.wav"); - moan[7] = gi.soundindex("insane/insane10.wav"); - - mapLight(); - gi.configstring(CS_GENERAL + 5, ">"); -} diff --git a/src/g_activation.cpp b/src/g_activation.cpp new file mode 100644 index 0000000..389b487 --- /dev/null +++ b/src/g_activation.cpp @@ -0,0 +1,32 @@ +#include "g_activation.h" + +/* +============= +BuildActivationMessagePlan + +Creates an activation message plan for the provided context. +============= +*/ +activation_message_plan_t BuildActivationMessagePlan(bool has_message, bool has_activator, bool activator_is_monster, bool coop_global, bool coop_enabled, int noise_index) +{ + activation_message_plan_t plan{}; + + if (!has_message) + return plan; + + if (coop_global && coop_enabled) + plan.broadcast_global = true; + + if (!has_activator || activator_is_monster) + return plan; + + plan.center_on_activator = true; + + if (noise_index >= 0) + { + plan.play_sound = true; + plan.sound_index = noise_index; + } + + return plan; +} diff --git a/src/g_activation.h b/src/g_activation.h new file mode 100644 index 0000000..72665b4 --- /dev/null +++ b/src/g_activation.h @@ -0,0 +1,25 @@ +/* +============= +BuildActivationMessagePlan + +Defines the information needed to safely emit activation messaging and sounds. +============= +*/ +#pragma once + +struct activation_message_plan_t +{ + bool broadcast_global = false; + bool center_on_activator = false; + bool play_sound = false; + int sound_index = -1; +}; + +/* +============= +BuildActivationMessagePlan + +Creates an activation message plan for the provided context. +============= +*/ +activation_message_plan_t BuildActivationMessagePlan(bool has_message, bool has_activator, bool activator_is_monster, bool coop_global, bool coop_enabled, int noise_index); diff --git a/src/g_ai.cpp b/src/g_ai.cpp index bab350a..a346b5b 100644 --- a/src/g_ai.cpp +++ b/src/g_ai.cpp @@ -3,6 +3,7 @@ // g_ai.c #include "g_local.h" +#include bool FindTarget(gentity_t *self); bool ai_checkattack(gentity_t *self, float dist); @@ -28,8 +29,8 @@ gentity_t *AI_GetSightClient(gentity_t *self) { if (level.intermission_time) return nullptr; - gentity_t **visible_players = (gentity_t **)alloca(sizeof(gentity_t *) * game.maxclients); - size_t num_visible = 0; + std::vector visible_players; + visible_players.reserve(game.maxclients); for (auto player : active_clients()) { if (player->health <= 0 || player->deadflag || !player->solid) @@ -43,13 +44,13 @@ gentity_t *AI_GetSightClient(gentity_t *self) { continue; } - visible_players[num_visible++] = player; // got one + visible_players.push_back(player); // got one } - if (!num_visible) + if (visible_players.empty()) return nullptr; - return visible_players[irandom(num_visible)]; + return visible_players[irandom(visible_players.size())]; } //============================================================================ diff --git a/src/g_ai_new.cpp b/src/g_ai_new.cpp index ca565d1..adc1211 100644 --- a/src/g_ai_new.cpp +++ b/src/g_ai_new.cpp @@ -363,20 +363,59 @@ void hintpath_stop(gentity_t *self) { } // ============= -// monsterlost_checkhint - the monster (self) will check around for valid hintpaths. -// a valid hintpath is one where the two endpoints can see both the monster -// and the monster's enemy. if only one person is visible from the endpoints, -// it will not go for it. -// ============= +static bool hint_path_chain_dirty = true; +static int32_t cached_num_hint_paths; +static std::vector cached_hint_path_chain; + +/* +============= +InvalidateHintPathChains + +Marks the cached hint path chains as dirty so they will be rebuilt on next use. +============= +*/ +static void InvalidateHintPathChains() { + hint_path_chain_dirty = true; +} + +/* +============= +BuildCachedHintPathChain + +Populates the flattened hint path chain cache when the cache is dirty or the path layout changed. +============= +*/ +static void BuildCachedHintPathChain() { + if (!hint_path_chain_dirty && cached_num_hint_paths == num_hint_paths) + return; + + cached_hint_path_chain.clear(); + cached_num_hint_paths = num_hint_paths; + + for (int i = 0; i < num_hint_paths; i++) { + for (gentity_t *node = hint_path_start[i]; node; node = node->hint_chain) + cached_hint_path_chain.push_back(node); + } + + hint_path_chain_dirty = false; +} + +/* +============= +monsterlost_checkhint + +The monster (self) will check around for valid hintpaths. A valid hintpath is one where the two endpoints can see both the monster +and the monster's enemy. If only one person is visible from the endpoints, it will not go for it. +============= +*/ bool monsterlost_checkhint(gentity_t *self) { - gentity_t *e, *monster_pathchain, *target_pathchain, *checkpoint = nullptr; - gentity_t *closest; - float closest_range = 1000000; + std::vector monster_pathchain; + std::vector target_pathchain; + float closest_range = 1000000; gentity_t *start, *destination; - int count5 = 0; - float r; - int i; - bool hint_path_represented[MAX_HINT_CHAINS]; + float r; + int i; + bool hint_path_represented[MAX_HINT_CHAINS]; // if there are no hint paths on this map, exit immediately. if (!hint_paths_present) @@ -392,75 +431,17 @@ bool monsterlost_checkhint(gentity_t *self) { if (!strcmp(self->classname, "monster_turret")) return false; - monster_pathchain = nullptr; + BuildCachedHintPathChain(); - // find all the hint_paths. - // FIXME - can we not do this every time? - for (i = 0; i < num_hint_paths; i++) { - e = hint_path_start[i]; - while (e) { - if (e->monster_hint_chain) - e->monster_hint_chain = nullptr; - - if (monster_pathchain) { - checkpoint->monster_hint_chain = e; - checkpoint = e; - } else { - monster_pathchain = e; - checkpoint = e; - } - e = e->hint_chain; - } - } + for (gentity_t *node : cached_hint_path_chain) { + r = realrange(self, node); - // filter them by distance and visibility to the monster - e = monster_pathchain; - checkpoint = nullptr; - while (e) { - r = realrange(self, e); - - if (r > 512) { - if (checkpoint) { - checkpoint->monster_hint_chain = e->monster_hint_chain; - e->monster_hint_chain = nullptr; - e = checkpoint->monster_hint_chain; - continue; - } else { - // use checkpoint as temp pointer - checkpoint = e; - e = e->monster_hint_chain; - checkpoint->monster_hint_chain = nullptr; - // and clear it again - checkpoint = nullptr; - // since we have yet to find a valid one (or else checkpoint would be set) move the - // start of monster_pathchain - monster_pathchain = e; - continue; - } - } - if (!visible(self, e)) { - if (checkpoint) { - checkpoint->monster_hint_chain = e->monster_hint_chain; - e->monster_hint_chain = nullptr; - e = checkpoint->monster_hint_chain; - continue; - } else { - // use checkpoint as temp pointer - checkpoint = e; - e = e->monster_hint_chain; - checkpoint->monster_hint_chain = nullptr; - // and clear it again - checkpoint = nullptr; - // since we have yet to find a valid one (or else checkpoint would be set) move the - // start of monster_pathchain - monster_pathchain = e; - continue; - } - } + if (r > 512) + continue; + if (!visible(self, node)) + continue; - count5++; - checkpoint = e; - e = e->monster_hint_chain; + monster_pathchain.push_back(node); } // at this point, we have a list of all of the eligible hint nodes for the monster @@ -468,95 +449,41 @@ bool monsterlost_checkhint(gentity_t *self) { // seeing whether any can see the player // // first, we figure out which hint chains we have represented in monster_pathchain - if (count5 == 0) + if (monster_pathchain.empty()) return false; for (i = 0; i < num_hint_paths; i++) hint_path_represented[i] = false; - e = monster_pathchain; - checkpoint = nullptr; - while (e) { - if ((e->hint_chain_id < 0) || (e->hint_chain_id > num_hint_paths)) + for (gentity_t *node : monster_pathchain) { + if ((node->hint_chain_id < 0) || (node->hint_chain_id > num_hint_paths)) return false; - hint_path_represented[e->hint_chain_id] = true; - e = e->monster_hint_chain; + hint_path_represented[node->hint_chain_id] = true; } - count5 = 0; - // now, build the target_pathchain which contains all of the hint_path nodes we need to check for // validity (within range, visibility) - target_pathchain = nullptr; - checkpoint = nullptr; for (i = 0; i < num_hint_paths; i++) { // if this hint chain is represented in the monster_hint_chain, add all of it's nodes to the target_pathchain // for validity checking if (hint_path_represented[i]) { - e = hint_path_start[i]; - while (e) { - if (target_pathchain) { - checkpoint->target_hint_chain = e; - checkpoint = e; - } else { - target_pathchain = e; - checkpoint = e; - } - e = e->hint_chain; - } - } - } + for (gentity_t *node = hint_path_start[i]; node; node = node->hint_chain) { + r = realrange(self->enemy, node); - // target_pathchain is a list of all of the hint_path nodes we need to check for validity relative to the target - e = target_pathchain; - checkpoint = nullptr; - while (e) { - r = realrange(self->enemy, e); - - if (r > 512) { - if (checkpoint) { - checkpoint->target_hint_chain = e->target_hint_chain; - e->target_hint_chain = nullptr; - e = checkpoint->target_hint_chain; - continue; - } else { - // use checkpoint as temp pointer - checkpoint = e; - e = e->target_hint_chain; - checkpoint->target_hint_chain = nullptr; - // and clear it again - checkpoint = nullptr; - target_pathchain = e; - continue; - } - } - if (!visible(self->enemy, e)) { - if (checkpoint) { - checkpoint->target_hint_chain = e->target_hint_chain; - e->target_hint_chain = nullptr; - e = checkpoint->target_hint_chain; - continue; - } else { - // use checkpoint as temp pointer - checkpoint = e; - e = e->target_hint_chain; - checkpoint->target_hint_chain = nullptr; - // and clear it again - checkpoint = nullptr; - target_pathchain = e; - continue; + if (r > 512) + continue; + if (!visible(self->enemy, node)) + continue; + + target_pathchain.push_back(node); } } - - count5++; - checkpoint = e; - e = e->target_hint_chain; } // at this point we should have: - // monster_pathchain - a list of "monster valid" hint_path nodes linked together by monster_hint_chain - // target_pathcain - a list of "target valid" hint_path nodes linked together by target_hint_chain. these + // monster_pathchain - a list of "monster valid" hint_path nodes + // target_pathcain - a list of "target valid" hint_path nodes. these // are filtered such that only nodes which are on the same chain as "monster valid" nodes // // Now, we figure out which "monster valid" node we want to use @@ -568,40 +495,27 @@ bool monsterlost_checkhint(gentity_t *self) { // // Once this filter is finished, we select the closest "monster valid" node, and go to it. - if (count5 == 0) + if (target_pathchain.empty()) return false; // reuse the hint_chain_represented array, this time to see which chains are represented by the target for (i = 0; i < num_hint_paths; i++) hint_path_represented[i] = false; - e = target_pathchain; - checkpoint = nullptr; - while (e) { - if ((e->hint_chain_id < 0) || (e->hint_chain_id > num_hint_paths)) + for (gentity_t *node : target_pathchain) { + if ((node->hint_chain_id < 0) || (node->hint_chain_id > num_hint_paths)) return false; - hint_path_represented[e->hint_chain_id] = true; - e = e->target_hint_chain; + hint_path_represented[node->hint_chain_id] = true; } - // traverse the monster_pathchain - if the hint_node isn't represented in the "target valid" chain list, - // remove it - // if it is on the list, check it for range from the monster. If the range is the closest, keep it - // - closest = nullptr; - e = monster_pathchain; - while (e) { - if (!(hint_path_represented[e->hint_chain_id])) { - checkpoint = e->monster_hint_chain; - e->monster_hint_chain = nullptr; - e = checkpoint; + gentity_t *closest = nullptr; + for (gentity_t *node : monster_pathchain) { + if (!hint_path_represented[node->hint_chain_id]) continue; - } - r = realrange(self, e); + r = realrange(self, node); if (r < closest_range) - closest = e; - e = e->monster_hint_chain; + closest = node; } if (!closest) @@ -614,14 +528,12 @@ bool monsterlost_checkhint(gentity_t *self) { closest = nullptr; closest_range = 10000000; - e = target_pathchain; - while (e) { - if (start->hint_chain_id == e->hint_chain_id) { - r = realrange(self, e); + for (gentity_t *node : target_pathchain) { + if (start->hint_chain_id == node->hint_chain_id) { + r = realrange(self, node); if (r < closest_range) - closest = e; + closest = node; } - e = e->target_hint_chain; } if (!closest) @@ -634,7 +546,6 @@ bool monsterlost_checkhint(gentity_t *self) { return true; } - // // Path code // @@ -726,6 +637,7 @@ void InitHintPaths() { int i; hint_paths_present = 0; + InvalidateHintPathChains(); // check all the hint_paths. e = G_FindByString<&gentity_t::classname>(nullptr, "hint_path"); diff --git a/src/g_chase.cpp b/src/g_chase.cpp index 1390814..8ee1c9e 100644 --- a/src/g_chase.cpp +++ b/src/g_chase.cpp @@ -2,6 +2,13 @@ // Licensed under the GNU General Public License 2.0. #include "g_local.h" +/* +============= +FreeFollower + +Release a client's current follow target and reset chase state. +============= +*/ void FreeFollower(gentity_t *ent) { if (!ent) return; @@ -22,8 +29,17 @@ void FreeFollower(gentity_t *ent) { ent->client->ps.screen_blend = {}; ent->client->ps.damage_blend = {}; ent->client->ps.rdflags = RDF_NONE; + ent->s.effects = EF_NONE; + ent->s.renderfx = RF_NONE; } +/* +============= +FreeClientFollowers + +Release all clients following the specified entity. +============= +*/ void FreeClientFollowers(gentity_t *ent) { if (!ent) return; @@ -36,6 +52,13 @@ void FreeClientFollowers(gentity_t *ent) { } } +/* +============= +UpdateChaseCam + +Update the chase camera position and visual state to mirror the target. +============= +*/ void UpdateChaseCam(gentity_t *ent) { vec3_t o, ownerv, goal; gentity_t *targ = ent->client->follow_target; @@ -54,6 +77,17 @@ void UpdateChaseCam(gentity_t *ent) { ownerv = targ->s.origin; oldgoal = ent->s.origin; + // ensure the spectator inherits the target's visual blends and render effects + ent->client->ps.screen_blend = targ->client->ps.screen_blend; + ent->client->ps.damage_blend = targ->client->ps.damage_blend; + ent->client->ps.rdflags = targ->client->ps.rdflags; + std::copy(targ->client->ps.stats.begin() + STAT_POWERUP_INFO_START, targ->client->ps.stats.begin() + STAT_POWERUP_INFO_END + 1, ent->client->ps.stats.begin() + STAT_POWERUP_INFO_START); + ent->client->ps.stats[STAT_FLASHES] = targ->client->ps.stats[STAT_FLASHES]; + ent->client->ps.stats[STAT_POWERUP_ICON] = targ->client->ps.stats[STAT_POWERUP_ICON]; + ent->client->ps.stats[STAT_POWERUP_TIME] = targ->client->ps.stats[STAT_POWERUP_TIME]; + ent->s.effects = targ->s.effects; + ent->s.renderfx = targ->s.renderfx; + // Q2Eaks eyecam handling if (g_eyecam->integer) { // mark the chased player as instanced so we can disable their model's visibility diff --git a/src/g_cmds.cpp b/src/g_cmds.cpp index a4ebe3f..3aafdc7 100644 --- a/src/g_cmds.cpp +++ b/src/g_cmds.cpp @@ -199,7 +199,8 @@ Give items to a client ================== */ static void Cmd_Give_f(gentity_t *ent) { - const char *name = gi.args(); + int argc = gi.argc(); + const char *name = (argc >= 2) ? gi.args() : ""; gitem_t *it; size_t i; bool give_all; @@ -210,8 +211,8 @@ static void Cmd_Give_f(gentity_t *ent) { else give_all = false; - if (give_all || Q_strcasecmp(gi.argv(1), "health") == 0) { - if (gi.argc() == 3) + if (give_all || (argc >= 2 && Q_strcasecmp(gi.argv(1), "health") == 0)) { + if (argc == 3) ent->health = atoi(gi.argv(2)); else ent->health = ent->max_health; @@ -299,7 +300,7 @@ static void Cmd_Give_f(gentity_t *ent) { } it = FindItem(name); - if (!it) { + if (!it && argc >= 2) { name = gi.argv(1); it = FindItem(name); } @@ -324,7 +325,7 @@ static void Cmd_Give_f(gentity_t *ent) { it_ent = G_Spawn(); it_ent->classname = it->classname; SpawnItem(it_ent, it); - if (it->flags & IF_AMMO && gi.argc() == 3) + if (it->flags & IF_AMMO && argc == 3) it_ent->count = atoi(gi.argv(2)); // since some items don't actually spawn when you say to .. @@ -1889,6 +1890,14 @@ bool AllowClientTeamSwitch(gentity_t *ent) { return true; } +struct team_balance_request_t { + int client_index; + team_t team; +}; + +static team_balance_request_t balance_queue[MAX_CLIENTS_KEX]; +static size_t balance_queue_count = 0; + /* ================ TeamBalance @@ -1897,6 +1906,50 @@ Balance the teams without shuffling. Switch last joined player(s) from stacked team. ================ */ +/* +============= +FindBalanceRequest + +Finds an existing queued balance request for the supplied client index +============= +*/ +static team_balance_request_t *FindBalanceRequest(int client_index) { + for (size_t i = 0; i < balance_queue_count; i++) { + if (balance_queue[i].client_index == client_index) + return &balance_queue[i]; + } + + return nullptr; +} + +/* +============= +EnqueueBalanceRequest + +Queues a balance change for the supplied client index +============= +*/ +static void EnqueueBalanceRequest(int client_index, team_t team) { + team_balance_request_t *existing_request = FindBalanceRequest(client_index); + + if (existing_request) { + existing_request->team = team; + return; + } + + if (balance_queue_count >= MAX_CLIENTS_KEX) + return; + + balance_queue[balance_queue_count].client_index = client_index; + balance_queue[balance_queue_count].team = team; + balance_queue_count++; +} + +/* +============= +TeamBalance +============= +*/ int TeamBalance(bool force) { if (!Teams()) return 0; @@ -1904,15 +1957,48 @@ int TeamBalance(bool force) { if (GT(GT_RR)) return 0; - int delta = abs(level.num_playing_red - level.num_playing_blue); + bool queue_changes = GTF(GTF_ROUNDS) && level.round_state == roundst_t::ROUND_IN_PROGRESS; + + int red_count = level.num_playing_red; + int blue_count = level.num_playing_blue; + + if (queue_changes) { + for (size_t i = 0; i < balance_queue_count; i++) { + gclient_t *queued_client = &game.clients[balance_queue[i].client_index]; + + switch (queued_client->sess.team) { + case TEAM_RED: + red_count--; + break; + case TEAM_BLUE: + blue_count--; + break; + default: + break; + } + + switch (balance_queue[i].team) { + case TEAM_RED: + red_count++; + break; + case TEAM_BLUE: + blue_count++; + break; + default: + break; + } + } + } + + int delta = abs(red_count - blue_count); if (delta < 2) return level.num_playing_red - level.num_playing_blue; - team_t stack_team = level.num_playing_red > level.num_playing_blue ? TEAM_RED : TEAM_BLUE; + team_t stack_team = red_count > blue_count ? TEAM_RED : TEAM_BLUE; - size_t count = 0; - int index[MAX_CLIENTS_KEX/2]; + size_t count = 0; + int index[MAX_CLIENTS_KEX/2]; memset(index, 0, sizeof(index)); // assemble list of client nums of everyone on stacked team @@ -1927,11 +2013,16 @@ int TeamBalance(bool force) { qsort(index, count, sizeof(index[0]), PlayerSortByJoinTime); //run through sort list, switching from stack_team until teams are even + if (!count) { + gi.LocBroadcast_Print(PRINT_HIGH, "Team balance skipped: no stacked players available.\n"); + return 0; + } + if (count) { - size_t i; + size_t i; int switched = 0; gclient_t *cl = nullptr; - for (i = 0; i < count, delta > 1; i++) { + for (i = 0; i < count && delta > 1; i++) { cl = &game.clients[index[i]]; if (!cl) @@ -1943,24 +2034,80 @@ int TeamBalance(bool force) { if (cl->sess.team != stack_team) continue; - cl->sess.team = stack_team == TEAM_RED ? TEAM_BLUE : TEAM_RED; + if (queue_changes) { + team_balance_request_t *queued_request = FindBalanceRequest(cl - game.clients); + + if (queued_request && queued_request->team != stack_team) + continue; + } + + team_t target_team = stack_team == TEAM_RED ? TEAM_BLUE : TEAM_RED; + + if (queue_changes) { + EnqueueBalanceRequest(cl - game.clients, target_team); + gi.LocClient_Print(&g_entities[cl - game.clients + 1], PRINT_CENTER, "You will change teams after this round ends to rebalance the game.\n"); + } else { + cl->sess.team = target_team; - //TODO: queue this change in round-based games - ClientRespawn(&g_entities[cl - game.clients + 1]); - gi.LocClient_Print(&g_entities[cl - game.clients + 1], PRINT_CENTER, "You have changed teams to rebalance the game.\n"); + ClientRespawn(&g_entities[cl - game.clients + 1]); + gi.LocClient_Print(&g_entities[cl - game.clients + 1], PRINT_CENTER, "You have changed teams to rebalance the game.\n"); + } delta--; switched++; } if (switched) { - gi.LocBroadcast_Print(PRINT_HIGH, "Teams have been balanced.\n"); + gi.LocBroadcast_Print(PRINT_HIGH, queue_changes ? "Team balance queued for end of round.\n" : "Teams have been balanced.\n"); return switched; } } return 0; } +/* +============= +ProcessBalanceQueue + +Applies queued team balance changes once the round has ended +============= +*/ +void ProcessBalanceQueue(void) { + if (!balance_queue_count) + return; + + if (GTF(GTF_ROUNDS) && level.round_state == roundst_t::ROUND_IN_PROGRESS) + return; + + size_t applied = 0; + + for (size_t i = 0; i < balance_queue_count; i++) { + if (balance_queue[i].client_index < 0 || balance_queue[i].client_index >= MAX_CLIENTS_KEX) + continue; + + gclient_t *cl = &game.clients[balance_queue[i].client_index]; + gentity_t *ent = &g_entities[balance_queue[i].client_index + 1]; + + if (!cl->pers.connected || !ent->inuse || !ent->client) + continue; + + if (cl->sess.team == balance_queue[i].team) + continue; + + cl->sess.team = balance_queue[i].team; + + ClientRespawn(ent); + gi.LocClient_Print(ent, PRINT_CENTER, "You have changed teams to rebalance the game.\n"); + + applied++; + } + + balance_queue_count = 0; + + if (applied) + gi.LocBroadcast_Print(PRINT_HIGH, "Teams have been balanced.\n"); +} + /* ================ TeamShuffle @@ -2702,7 +2849,8 @@ static bool ValidVoteCommand(gentity_t *ent) { return false; level.vote = cc; - level.vote_arg = std::string(gi.argv(2)); + const char *raw_arg = gi.argc() > 2 ? gi.argv(2) : ""; + level.vote_arg = std::string(raw_arg); //gi.Com_PrintFmt("argv={} vote_arg={}\n", gi.argv(2), level.vote_arg); return true; } @@ -2765,11 +2913,15 @@ static void Cmd_CallVote_f(gentity_t *ent) { for (size_t i = 0; i < ARRAY_LEN(vote_cmds); i++, cc++) { if (!cc->name) continue; - + if (g_vote_flags->integer & cc->flag) continue; - - strcat(vstr, G_Fmt("{} ", cc->name).data()); + + std::string option = std::string(G_Fmt("{} ", cc->name)); + if (Q_strlcat(vstr, option.c_str(), sizeof(vstr)) >= sizeof(vstr)) { + vstr[sizeof(vstr) - 1] = '\0'; + break; + } } if (!g_allow_voting->integer || strlen(vstr) <= 1) { @@ -2870,11 +3022,9 @@ void G_RevertVote(gclient_t *client) { if (client->pers.voted == 1) { level.vote_yes--; client->pers.voted = 0; - //trap_SetConfigstring(CS_VOTE_YES, va("%i", level.vote_yes)); } else if (client->pers.voted == -1) { level.vote_no--; client->pers.voted = 0; - //trap_SetConfigstring(CS_VOTE_NO, va("%i", level.vote_no)); } } @@ -2900,7 +3050,7 @@ static void Cmd_Follow_f(gentity_t *ent) { return; } - if (ClientIsPlaying(follow_ent->client)) { + if (!ClientIsPlaying(follow_ent->client)) { gi.Client_Print(ent, PRINT_HIGH, "Specified client is not playing.\n"); return; } @@ -2926,8 +3076,18 @@ Cmd_FollowLeader_f ================= */ static void Cmd_FollowLeader_f(gentity_t *ent) { - gentity_t *leader = &g_entities[level.sorted_clients[0] + 1]; ent->client->sess.pc.follow_leader ^= true; + + if (ent->client->sess.pc.follow_leader) { + if (!level.num_playing_clients || level.sorted_clients[0] < 0) { + ent->client->sess.pc.follow_leader = false; + gi.Client_Print(ent, PRINT_HIGH, "No leader available to follow.\n"); + gi.LocClient_Print(ent, PRINT_HIGH, "Auto-follow leader: OFF\n"); + return; + } + } + + gentity_t *leader = &g_entities[level.sorted_clients[0] + 1]; gi.LocClient_Print(ent, PRINT_HIGH, "Auto-follow leader: {}\n", ent->client->sess.pc.follow_leader ? "ON" : "OFF"); if (!ClientIsPlaying(ent->client) && ent->client->sess.pc.follow_leader && ent->client->follow_target != leader) { @@ -3191,13 +3351,50 @@ static void Cmd_Ruleset_f(gentity_t *ent) { gi.cvar_forceset("g_ruleset", G_Fmt("{}", (int)rs).data()); } +/* +============= +G_MapListContains + +Checks if the provided map name is present in g_map_list as a discrete entry. +============= +*/ +static bool G_MapListContains(const char *mapname) { + if (!mapname || !mapname[0]) + return false; + + if (!g_map_list->string[0]) + return true; + + const char *map_list = g_map_list->string; + char *token; + + while (true) { + token = COM_ParseEx(&map_list, " "); + + if (!*token) + break; + + if (!strcmp(token, mapname) || !Q_strcasecmp(token, mapname)) + return true; + } + + return false; +} + +/* +============= +Cmd_SetMap_f + +Changes to a map within the map list. +============= +*/ static void Cmd_SetMap_f(gentity_t *ent) { if (gi.argc() < 2) { gi.LocClient_Print(ent, PRINT_HIGH, "Usage: {} [mapname]\nChanges to a map within the map list.", gi.argv(0)); return; } - if (g_map_list->string[0] && !strstr(g_map_list->string, gi.argv(1))) { + if (!G_MapListContains(gi.argv(1))) { gi.Client_Print(ent, PRINT_HIGH, "Map name is not valid.\n"); return; } @@ -3206,14 +3403,22 @@ static void Cmd_SetMap_f(gentity_t *ent) { ExitLevel(); } -extern void ClearWorldEntities(); +/* +============= +Cmd_MapRestart_f + +Reset the match and world state before reloading the current map. +============= +*/ static void Cmd_MapRestart_f(gentity_t *ent) { gi.Broadcast_Print(PRINT_HIGH, "[ADMIN]: Session reset.\n"); - //TODO: reset match variables, clear world entities, reload world entities - //SpawnEntities(level.mapname, level.entstring.c_str(), nullptr); - //Match_Reset(); - //ClearWorldEntities(); + G_SaveLevelEntstring(); + Match_Reset(); + + if (G_ResetLevelFromSavedEntstring()) + return; + gi.AddCommandString(G_Fmt("gamemap {}\n", level.mapname).data()); } @@ -3611,15 +3816,26 @@ void ClientCommand(gentity_t *ent) { cmds_t *cc; const char *cmd; + if (!ent->inuse) + return; // not fully in game yet + if (!ent->client) return; // not fully in game yet - // check if client is 888, print what is being sent and prevent any further processing - if (ent->client->sess.is_888) { - gi.Com_PrintFmt("Sneaky little snake Dalude/888 (%s) sent the following command:\n{}\n", ent->client->pers.netname, gi.args()); + if (ent->client->sess.is_banned && level.time > ent->client->sess.ban_msg_debounce_time) { + gi.Client_Print(ent, PRINT_HIGH, "You are banned from this mod, you naughty little sausage.\nYou should reflect on your behaviour towards other players.\n"); + + // ban message debounce time + ent->client->sess.ban_msg_debounce_time = level.time + 5_sec; return; } + // check if client is 888, print what is being sent and prevent any further processing + //if (ent->client->sess.is_888) { + // gi.Com_PrintFmt("Sneaky little snake Dalude/888 ({}) sent the following command:\n{}\n", ent->client->pers.netname, gi.args()); + // return; + //} + cmd = gi.argv(0); cc = FindClientCmdByName(cmd); diff --git a/src/g_combat.cpp b/src/g_combat.cpp index da1a365..aec749a 100644 --- a/src/g_combat.cpp +++ b/src/g_combat.cpp @@ -717,7 +717,7 @@ void T_Damage(gentity_t *targ, gentity_t *inflictor, gentity_t *attacker, const } } - if (targ != attacker && attacker->client && targ->health > 0) { + if (targ != attacker && targ->client && attacker->client && targ->health > 0 && !targ->client->sess.is_banned) { int stat_take = take; if (stat_take > targ->health) stat_take = targ->health; diff --git a/src/g_items.cpp b/src/g_items.cpp index d268c4a..087b1dd 100644 --- a/src/g_items.cpp +++ b/src/g_items.cpp @@ -3,46 +3,47 @@ #include "g_local.h" #include "bots/bot_includes.h" #include "monsters/m_player.h" //doppelganger - -bool Pickup_Weapon(gentity_t *ent, gentity_t *other); -void Use_Weapon(gentity_t *ent, gitem_t *inv); -void Drop_Weapon(gentity_t *ent, gitem_t *inv); - -void Weapon_Blaster(gentity_t *ent); -void Weapon_Shotgun(gentity_t *ent); -void Weapon_SuperShotgun(gentity_t *ent); -void Weapon_Machinegun(gentity_t *ent); -void Weapon_Chaingun(gentity_t *ent); -void Weapon_HyperBlaster(gentity_t *ent); -void Weapon_RocketLauncher(gentity_t *ent); -void Weapon_HandGrenade(gentity_t *ent); -void Weapon_GrenadeLauncher(gentity_t *ent); -void Weapon_Railgun(gentity_t *ent); -void Weapon_BFG(gentity_t *ent); -void Weapon_IonRipper(gentity_t *ent); -void Weapon_Phalanx(gentity_t *ent); -void Weapon_Trap(gentity_t *ent); -void Weapon_ChainFist(gentity_t *ent); -void Weapon_Disruptor(gentity_t *ent); -void Weapon_ETF_Rifle(gentity_t *ent); -void Weapon_PlasmaBeam(gentity_t *ent); -void Weapon_Tesla(gentity_t *ent); -void Weapon_ProxLauncher(gentity_t *ent); - -void Use_Quad(gentity_t *ent, gitem_t *item); +#include "g_items_limits.h" + +bool Pickup_Weapon(gentity_t* ent, gentity_t* other); +void Use_Weapon(gentity_t* ent, gitem_t* inv); +void Drop_Weapon(gentity_t* ent, gitem_t* inv); + +void Weapon_Blaster(gentity_t* ent); +void Weapon_Shotgun(gentity_t* ent); +void Weapon_SuperShotgun(gentity_t* ent); +void Weapon_Machinegun(gentity_t* ent); +void Weapon_Chaingun(gentity_t* ent); +void Weapon_HyperBlaster(gentity_t* ent); +void Weapon_RocketLauncher(gentity_t* ent); +void Weapon_HandGrenade(gentity_t* ent); +void Weapon_GrenadeLauncher(gentity_t* ent); +void Weapon_Railgun(gentity_t* ent); +void Weapon_BFG(gentity_t* ent); +void Weapon_IonRipper(gentity_t* ent); +void Weapon_Phalanx(gentity_t* ent); +void Weapon_Trap(gentity_t* ent); +void Weapon_ChainFist(gentity_t* ent); +void Weapon_Disruptor(gentity_t* ent); +void Weapon_ETF_Rifle(gentity_t* ent); +void Weapon_PlasmaBeam(gentity_t* ent); +void Weapon_Tesla(gentity_t* ent); +void Weapon_ProxLauncher(gentity_t* ent); + +void Use_Quad(gentity_t* ent, gitem_t* item); static gtime_t quad_drop_timeout_hack; -void Use_Haste(gentity_t *ent, gitem_t *item); +void Use_Haste(gentity_t* ent, gitem_t* item); static gtime_t haste_drop_timeout_hack; -void Use_Double(gentity_t *ent, gitem_t *item); +void Use_Double(gentity_t* ent, gitem_t* item); static gtime_t double_drop_timeout_hack; -void Use_Invisibility(gentity_t *ent, gitem_t *item); +void Use_Invisibility(gentity_t* ent, gitem_t* item); static gtime_t invisibility_drop_timeout_hack; -void Use_Protection(gentity_t *ent, gitem_t *item); +void Use_Protection(gentity_t* ent, gitem_t* item); static gtime_t protection_drop_timeout_hack; -void Use_Regeneration(gentity_t *ent, gitem_t *item); +void Use_Regeneration(gentity_t* ent, gitem_t* item); static gtime_t regeneration_drop_timeout_hack; -static void UsedMessage(gentity_t *ent, gitem_t *item) { +static void UsedMessage(gentity_t* ent, gitem_t* item) { if (!ent || !item) return; @@ -56,10 +57,10 @@ static void UsedMessage(gentity_t *ent, gitem_t *item) { // DOPPELGANGER // *************************** -gentity_t *Sphere_Spawn(gentity_t *owner, spawnflags_t spawnflags); +gentity_t* Sphere_Spawn(gentity_t* owner, spawnflags_t spawnflags); -static DIE(doppelganger_die) (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, const vec3_t &point, const mod_t &mod) -> void { - gentity_t *sphere; +static DIE(doppelganger_die) (gentity_t* self, gentity_t* inflictor, gentity_t* attacker, int damage, const vec3_t& point, const mod_t& mod) -> void { + gentity_t* sphere; float dist; vec3_t dir; @@ -71,7 +72,8 @@ static DIE(doppelganger_die) (gentity_t *self, gentity_t *inflictor, gentity_t * if (dist > 768) { sphere = Sphere_Spawn(self, SF_SPHERE_HUNTER | SF_DOPPELGANGER); sphere->pain(sphere, attacker, 0, 0, mod); - } else { + } + else { sphere = Sphere_Spawn(self, SF_SPHERE_VENGEANCE | SF_DOPPELGANGER); sphere->pain(sphere, attacker, 0, 0, mod); } @@ -88,15 +90,15 @@ static DIE(doppelganger_die) (gentity_t *self, gentity_t *inflictor, gentity_t * BecomeExplosion1(self); } -static PAIN(doppelganger_pain) (gentity_t *self, gentity_t *other, float kick, int damage, const mod_t &mod) -> void { +static PAIN(doppelganger_pain) (gentity_t* self, gentity_t* other, float kick, int damage, const mod_t& mod) -> void { self->enemy = other; } -static THINK(doppelganger_timeout) (gentity_t *self) -> void { +static THINK(doppelganger_timeout) (gentity_t* self) -> void { doppelganger_die(self, self, self, 9999, self->s.origin, MOD_UNKNOWN); } -static THINK(body_think) (gentity_t *self) -> void { +static THINK(body_think) (gentity_t* self) -> void { float r; if (fabsf(self->ideal_yaw - anglemod(self->s.angles[YAW])) < 2) { @@ -107,7 +109,8 @@ static THINK(body_think) (gentity_t *self) -> void { self->timestamp = level.time + 1_sec; } } - } else + } + else M_ChangeYaw(self); if (self->teleport_time <= level.time) { @@ -121,9 +124,9 @@ static THINK(body_think) (gentity_t *self) -> void { self->nextthink = level.time + FRAME_TIME_MS; } -void fire_doppelganger(gentity_t *ent, const vec3_t &start, const vec3_t &aimdir) { - gentity_t *base; - gentity_t *body; +void fire_doppelganger(gentity_t* ent, const vec3_t& start, const vec3_t& aimdir) { + gentity_t* base; + gentity_t* body; vec3_t dir; vec3_t forward, right, up; int number; @@ -181,15 +184,15 @@ void fire_doppelganger(gentity_t *ent, const vec3_t &start, const vec3_t &aimdir //====================================================================== -constexpr gtime_t DEFENDER_LIFESPAN = 10_sec; //30_sec; -constexpr gtime_t HUNTER_LIFESPAN = 10_sec; //30_sec; -constexpr gtime_t VENGEANCE_LIFESPAN = 10_sec; //30_sec; -constexpr gtime_t MINIMUM_FLY_TIME = 10_sec; //15_sec; +constexpr gtime_t DEFENDER_LIFESPAN = 10_sec; //30_sec; +constexpr gtime_t HUNTER_LIFESPAN = 10_sec; //30_sec; +constexpr gtime_t VENGEANCE_LIFESPAN = 10_sec; //30_sec; +constexpr gtime_t MINIMUM_FLY_TIME = 10_sec; //15_sec; -void LookAtKiller(gentity_t *self, gentity_t *inflictor, gentity_t *attacker); +void LookAtKiller(gentity_t* self, gentity_t* inflictor, gentity_t* attacker); -void vengeance_touch(gentity_t *self, gentity_t *other, const trace_t &tr, bool other_touching_self); -void hunter_touch(gentity_t *self, gentity_t *other, const trace_t &tr, bool other_touching_self); +void vengeance_touch(gentity_t* self, gentity_t* other, const trace_t& tr, bool other_touching_self); +void hunter_touch(gentity_t* self, gentity_t* other, const trace_t& tr, bool other_touching_self); // ************************* // General Sphere Code @@ -197,7 +200,7 @@ void hunter_touch(gentity_t *self, gentity_t *other, const trace_t &tr, bool oth // ================= // ================= -static THINK(sphere_think_explode) (gentity_t *self) -> void { +static THINK(sphere_think_explode) (gentity_t* self) -> void { if (self->owner && self->owner->client && !(self->spawnflags & SF_DOPPELGANGER)) { self->owner->client->owned_sphere = nullptr; } @@ -207,14 +210,14 @@ static THINK(sphere_think_explode) (gentity_t *self) -> void { // ================= // sphere_explode // ================= -static DIE(sphere_explode) (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, const vec3_t &point, const mod_t &mod) -> void { +static DIE(sphere_explode) (gentity_t* self, gentity_t* inflictor, gentity_t* attacker, int damage, const vec3_t& point, const mod_t& mod) -> void { sphere_think_explode(self); } // ================= // sphere_if_idle_die - if the sphere is not currently attacking, blow up. // ================= -static DIE(sphere_if_idle_die) (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, const vec3_t &point, const mod_t &mod) -> void { +static DIE(sphere_if_idle_die) (gentity_t* self, gentity_t* inflictor, gentity_t* attacker, int damage, const vec3_t& point, const mod_t& mod) -> void { if (!self->enemy) sphere_think_explode(self); } @@ -223,7 +226,7 @@ static DIE(sphere_if_idle_die) (gentity_t *self, gentity_t *inflictor, gentity_t // Sphere Movement // ************************* -static void sphere_fly(gentity_t *self) { +static void sphere_fly(gentity_t* self) { vec3_t dest, dir; if (level.time >= gtime_t::from_sec(self->wait)) { @@ -246,7 +249,7 @@ static void sphere_fly(gentity_t *self) { self->velocity = dir * 5; } -static void sphere_chase(gentity_t *self, int stupidChase) { +static void sphere_chase(gentity_t* self, int stupidChase) { vec3_t dest; vec3_t dir; float dist; @@ -270,7 +273,8 @@ static void sphere_chase(gentity_t *self, int stupidChase) { self->s.angles = vectoangles(dir); self->velocity = dir * 300; // 500; self->monsterinfo.saved_goal = dest; - } else if (!self->monsterinfo.saved_goal) { + } + else if (!self->monsterinfo.saved_goal) { dir = self->enemy->s.origin - self->s.origin; dist = dir.normalize(); self->s.angles = vectoangles(dir); @@ -278,7 +282,8 @@ static void sphere_chase(gentity_t *self, int stupidChase) { // if lurking, hunter sphere uses lurking sound self->s.sound = gi.soundindex("spheres/h_lurk.wav"); self->velocity = {}; - } else { + } + else { dir = self->monsterinfo.saved_goal - self->s.origin; dist = dir.normalize(); @@ -295,7 +300,8 @@ static void sphere_chase(gentity_t *self, int stupidChase) { // if moving, hunter sphere uses active sound if (!stupidChase) self->s.sound = gi.soundindex("spheres/h_active.wav"); - } else { + } + else { dir = self->enemy->s.origin - self->s.origin; dist = dir.normalize(); self->s.angles = vectoangles(dir); @@ -313,7 +319,7 @@ static void sphere_chase(gentity_t *self, int stupidChase) { // Attack related stuff // ************************* -static void sphere_fire(gentity_t *self, gentity_t *enemy) { +static void sphere_fire(gentity_t* self, gentity_t* enemy) { vec3_t dest; vec3_t dir; @@ -335,7 +341,7 @@ static void sphere_fire(gentity_t *self, gentity_t *enemy) { self->nextthink = gtime_t::from_sec(self->wait); } -static void sphere_touch(gentity_t *self, gentity_t *other, const trace_t &tr, mod_t mod) { +static void sphere_touch(gentity_t* self, gentity_t* other, const trace_t& tr, mod_t mod) { if (self->spawnflags.has(SF_DOPPELGANGER)) { if (other == self->teammaster) return; @@ -343,7 +349,8 @@ static void sphere_touch(gentity_t *self, gentity_t *other, const trace_t &tr, m self->takedamage = false; self->owner = self->teammaster; self->teammaster = nullptr; - } else { + } + else { if (other == self->owner) return; // PMM - don't blow up on bodies @@ -360,7 +367,8 @@ static void sphere_touch(gentity_t *self, gentity_t *other, const trace_t &tr, m if (other->takedamage) { T_Damage(other, self, self->owner, self->velocity, self->s.origin, tr.plane.normal, 10000, 1, DAMAGE_DESTROY_ARMOR, mod); - } else { + } + else { T_RadiusDamage(self, self->owner, 512, self->owner, 256, DAMAGE_NONE, mod); } } @@ -368,15 +376,15 @@ static void sphere_touch(gentity_t *self, gentity_t *other, const trace_t &tr, m sphere_think_explode(self); } -TOUCH(vengeance_touch) (gentity_t *self, gentity_t *other, const trace_t &tr, bool other_touching_self) -> void { +TOUCH(vengeance_touch) (gentity_t* self, gentity_t* other, const trace_t& tr, bool other_touching_self) -> void { if (self->spawnflags.has(SF_DOPPELGANGER)) sphere_touch(self, other, tr, MOD_DOPPEL_VENGEANCE); else sphere_touch(self, other, tr, MOD_VENGEANCE_SPHERE); } -TOUCH(hunter_touch) (gentity_t *self, gentity_t *other, const trace_t &tr, bool other_touching_self) -> void { - gentity_t *owner; +TOUCH(hunter_touch) (gentity_t* self, gentity_t* other, const trace_t& tr, bool other_touching_self) -> void { + gentity_t* owner; // don't blow up if you hit the world.... sheesh. if (other == world) return; @@ -397,7 +405,7 @@ TOUCH(hunter_touch) (gentity_t *self, gentity_t *other, const trace_t &tr, bool sphere_touch(self, other, tr, MOD_HUNTER_SPHERE); } -static void defender_shoot(gentity_t *self, gentity_t *enemy) { +static void defender_shoot(gentity_t* self, gentity_t* enemy) { vec3_t dir; vec3_t start; @@ -430,7 +438,7 @@ static void defender_shoot(gentity_t *self, gentity_t *enemy) { // Activation Related Stuff // ************************* -static void body_gib(gentity_t *self) { +static void body_gib(gentity_t* self) { gi.sound(self, CHAN_BODY, gi.soundindex("misc/udeath.wav"), 1, ATTN_NORM, 0); ThrowGibs(self, 50, { { 4, "models/objects/gibs/sm_meat/tris.md2" }, @@ -438,8 +446,8 @@ static void body_gib(gentity_t *self) { }); } -static PAIN(hunter_pain) (gentity_t *self, gentity_t *other, float kick, int damage, const mod_t &mod) -> void { - gentity_t *owner; +static PAIN(hunter_pain) (gentity_t* self, gentity_t* other, float kick, int damage, const mod_t& mod) -> void { + gentity_t* owner; float dist; vec3_t dir; @@ -454,7 +462,8 @@ static PAIN(hunter_pain) (gentity_t *self, gentity_t *other, float kick, int dam if (other == owner) return; - } else { + } + else { // if fired by a doppelganger, set it to 10 second timeout self->wait = (level.time + MINIMUM_FLY_TIME).seconds(); } @@ -508,14 +517,14 @@ static PAIN(hunter_pain) (gentity_t *self, gentity_t *other, float kick, int dam } } -static PAIN(defender_pain) (gentity_t *self, gentity_t *other, float kick, int damage, const mod_t &mod) -> void { +static PAIN(defender_pain) (gentity_t* self, gentity_t* other, float kick, int damage, const mod_t& mod) -> void { if (other == self->owner) return; self->enemy = other; } -static PAIN(vengeance_pain) (gentity_t *self, gentity_t *other, float kick, int damage, const mod_t &mod) -> void { +static PAIN(vengeance_pain) (gentity_t* self, gentity_t* other, float kick, int damage, const mod_t& mod) -> void { if (self->enemy) return; @@ -525,7 +534,8 @@ static PAIN(vengeance_pain) (gentity_t *self, gentity_t *other, float kick, int if (other == self->owner) return; - } else { + } + else { self->wait = (level.time + MINIMUM_FLY_TIME).seconds(); } @@ -540,7 +550,7 @@ static PAIN(vengeance_pain) (gentity_t *self, gentity_t *other, float kick, int // Think Functions // ************************* -static THINK(defender_think) (gentity_t *self) -> void { +static THINK(defender_think) (gentity_t* self) -> void { if (!self->owner) { G_FreeEntity(self); return; @@ -574,14 +584,14 @@ static THINK(defender_think) (gentity_t *self) -> void { self->nextthink = level.time + 10_hz; } -static THINK(hunter_think) (gentity_t *self) -> void { +static THINK(hunter_think) (gentity_t* self) -> void { // if we've exited the level, just remove ourselves. if (level.intermission_time) { sphere_think_explode(self); return; } - gentity_t *owner = self->owner; + gentity_t* owner = self->owner; if (!owner && !(self->spawnflags & SF_DOPPELGANGER)) { G_FreeEntity(self); @@ -614,21 +624,23 @@ static THINK(hunter_think) (gentity_t *self) -> void { owner->mins = {}; owner->maxs = {}; gi.linkentity(owner); - } else // sphere timed out + } + else // sphere timed out { owner->velocity = {}; owner->movetype = MOVETYPE_NONE; gi.linkentity(owner); } } - } else + } + else sphere_fly(self); if (self->inuse) self->nextthink = level.time + 10_hz; } -static THINK(vengeance_think) (gentity_t *self) -> void { +static THINK(vengeance_think) (gentity_t* self) -> void { // if we've exited the level, just remove ourselves. if (level.intermission_time) { sphere_think_explode(self); @@ -650,8 +662,8 @@ static THINK(vengeance_think) (gentity_t *self) -> void { } // ================= -gentity_t *Sphere_Spawn(gentity_t *owner, spawnflags_t spawnflags) { - gentity_t *sphere; +gentity_t* Sphere_Spawn(gentity_t* owner, spawnflags_t spawnflags) { + gentity_t* sphere; sphere = G_Spawn(); sphere->s.origin = owner->s.origin; @@ -718,7 +730,7 @@ gentity_t *Sphere_Spawn(gentity_t *owner, spawnflags_t spawnflags) { // Own_Sphere - attach the sphere to the client so we can // directly access it later // ================= -static void Own_Sphere(gentity_t *self, gentity_t *sphere) { +static void Own_Sphere(gentity_t* self, gentity_t* sphere) { if (!sphere) return; @@ -733,29 +745,30 @@ static void Own_Sphere(gentity_t *self, gentity_t *sphere) { if (self->client->owned_sphere->inuse) { G_FreeEntity(self->client->owned_sphere); self->client->owned_sphere = sphere; - } else { + } + else { self->client->owned_sphere = sphere; } } } } -void Defender_Launch(gentity_t *self) { - gentity_t *sphere; +void Defender_Launch(gentity_t* self) { + gentity_t* sphere; sphere = Sphere_Spawn(self, SF_SPHERE_DEFENDER); Own_Sphere(self, sphere); } -void Hunter_Launch(gentity_t *self) { - gentity_t *sphere; +void Hunter_Launch(gentity_t* self) { + gentity_t* sphere; sphere = Sphere_Spawn(self, SF_SPHERE_HUNTER); Own_Sphere(self, sphere); } -void Vengeance_Launch(gentity_t *self) { - gentity_t *sphere; +void Vengeance_Launch(gentity_t* self) { + gentity_t* sphere; sphere = Sphere_Spawn(self, SF_SPHERE_VENGEANCE); Own_Sphere(self, sphere); @@ -763,12 +776,12 @@ void Vengeance_Launch(gentity_t *self) { //====================================================================== -static gentity_t *QuadHog_FindSpawn() { +static gentity_t* QuadHog_FindSpawn() { return SelectDeathmatchSpawnPoint(nullptr, vec3_origin, SPAWN_FAR_HALF, true, true, false, true).spot; } static void QuadHod_ClearAll() { - gentity_t *ent; + gentity_t* ent; for (ent = g_entities; ent < &g_entities[globals.num_entities]; ent++) { @@ -794,8 +807,8 @@ static void QuadHod_ClearAll() { } } -void QuadHog_Spawn(gitem_t *item, gentity_t *spot, bool reset) { - gentity_t *ent; +void QuadHog_Spawn(gitem_t* item, gentity_t* spot, bool reset) { + gentity_t* ent; vec3_t forward, right; vec3_t angles = vec3_origin; @@ -834,9 +847,9 @@ void QuadHog_Spawn(gitem_t *item, gentity_t *spot, bool reset) { gi.linkentity(ent); } -THINK(QuadHog_DoSpawn) (gentity_t *ent) -> void { - gentity_t *spot; - gitem_t *it = GetItemByIndex(IT_POWERUP_QUAD); +THINK(QuadHog_DoSpawn) (gentity_t* ent) -> void { + gentity_t* spot; + gitem_t* it = GetItemByIndex(IT_POWERUP_QUAD); if (!it) return; @@ -848,9 +861,9 @@ THINK(QuadHog_DoSpawn) (gentity_t *ent) -> void { G_FreeEntity(ent); } -THINK(QuadHog_DoReset) (gentity_t *ent) -> void { - gentity_t *spot; - gitem_t *it = GetItemByIndex(IT_POWERUP_QUAD); +THINK(QuadHog_DoReset) (gentity_t* ent) -> void { + gentity_t* spot; + gitem_t* it = GetItemByIndex(IT_POWERUP_QUAD); if (!it) return; @@ -863,7 +876,7 @@ THINK(QuadHog_DoReset) (gentity_t *ent) -> void { } void QuadHog_SetupSpawn(gtime_t delay) { - gentity_t *ent; + gentity_t* ent; if (!g_quadhog->integer) return; @@ -881,7 +894,7 @@ void QuadHog_SetupSpawn(gtime_t delay) { constexpr gtime_t TECH_TIMEOUT = 60_sec; // seconds before techs spawn again -static bool Tech_PlayerHasATech(gentity_t *ent) { +static bool Tech_PlayerHasATech(gentity_t* ent) { if (Tech_Held(ent) != nullptr) { if (level.time - ent->client->tech_last_message_time > 5_sec) { gi.LocCenter_Print(ent, "$g_already_have_tech"); @@ -892,7 +905,7 @@ static bool Tech_PlayerHasATech(gentity_t *ent) { return false; } -gitem_t *Tech_Held(gentity_t *ent) { +gitem_t* Tech_Held(gentity_t* ent) { for (size_t i = 0; i < q_countof(tech_ids); i++) { if (ent->client->pers.inventory[tech_ids[i]]) return GetItemByIndex(tech_ids[i]); @@ -900,7 +913,7 @@ gitem_t *Tech_Held(gentity_t *ent) { return nullptr; } -static bool Tech_Pickup(gentity_t *ent, gentity_t *other) { +static bool Tech_Pickup(gentity_t* ent, gentity_t* other) { // client only gets one tech if (Tech_PlayerHasATech(other)) return false; @@ -910,32 +923,33 @@ static bool Tech_Pickup(gentity_t *ent, gentity_t *other) { return true; } -static void Tech_Spawn(gitem_t *item, gentity_t *spot); +static void Tech_Spawn(gitem_t* item, gentity_t* spot); -static gentity_t *FindTechSpawn() { +static gentity_t* FindTechSpawn() { return SelectDeathmatchSpawnPoint(nullptr, vec3_origin, SPAWN_FAR_HALF, true, true, false, true).spot; } -static THINK(Tech_Think) (gentity_t *tech) -> void { - gentity_t *spot; +static THINK(Tech_Think) (gentity_t* tech) -> void { + gentity_t* spot; if ((spot = FindTechSpawn()) != nullptr) { Tech_Spawn(tech->item, spot); G_FreeEntity(tech); - } else { + } + else { tech->nextthink = level.time + TECH_TIMEOUT; tech->think = Tech_Think; } } -static THINK(Tech_Make_Touchable) (gentity_t *tech) -> void { +static THINK(Tech_Make_Touchable) (gentity_t* tech) -> void { tech->touch = Touch_Item; tech->nextthink = level.time + TECH_TIMEOUT; tech->think = Tech_Think; } -static void Tech_Drop(gentity_t *ent, gitem_t *item) { - gentity_t *tech; +static void Tech_Drop(gentity_t* ent, gitem_t* item) { + gentity_t* tech; tech = Drop_Item(ent, item); tech->nextthink = level.time + 1_sec; @@ -943,8 +957,8 @@ static void Tech_Drop(gentity_t *ent, gitem_t *item) { ent->client->pers.inventory[item->id] = 0; } -void Tech_DeadDrop(gentity_t *ent) { - gentity_t *dropped; +void Tech_DeadDrop(gentity_t* ent) { + gentity_t* dropped; int i; i = 0; @@ -962,8 +976,8 @@ void Tech_DeadDrop(gentity_t *ent) { } } -static void Tech_Spawn(gitem_t *item, gentity_t *spot) { - gentity_t *ent = G_Spawn(); +static void Tech_Spawn(gitem_t* item, gentity_t* spot) { + gentity_t* ent = G_Spawn(); vec3_t forward, right; vec3_t angles = { 0, (float)irandom(360), 0 }; @@ -999,8 +1013,8 @@ static bool AllowTechs() { return !!(g_allow_techs->integer && ItemSpawnsEnabled()); } -static THINK(Tech_SpawnAll) (gentity_t *ent) -> void { - gentity_t *spot; +static THINK(Tech_SpawnAll) (gentity_t* ent) -> void { + gentity_t* spot; if (!AllowTechs()) return; @@ -1014,7 +1028,7 @@ static THINK(Tech_SpawnAll) (gentity_t *ent) -> void { if (!num) return; - gitem_t *it = nullptr; + gitem_t* it = nullptr; for (size_t i = 0; i < q_countof(tech_ids); i++) { it = GetItemByIndex(tech_ids[i]); if (!it) @@ -1031,13 +1045,13 @@ void Tech_SetupSpawn() { if (!AllowTechs()) return; - gentity_t *ent = G_Spawn(); + gentity_t* ent = G_Spawn(); ent->nextthink = level.time + 2_sec; ent->think = Tech_SpawnAll; } void Tech_Reset() { - gentity_t *ent; + gentity_t* ent; uint32_t i; for (ent = g_entities + 1, i = 1; i < globals.num_entities; i++, ent++) { @@ -1049,7 +1063,7 @@ void Tech_Reset() { //Tech_SpawnAll(nullptr); } -int Tech_ApplyDisruptorShield(gentity_t *ent, int dmg) { +int Tech_ApplyDisruptorShield(gentity_t* ent, int dmg) { float volume = 1.0; if (ent->client && ent->client->silencer_shots) @@ -1063,14 +1077,14 @@ int Tech_ApplyDisruptorShield(gentity_t *ent, int dmg) { return dmg; } -int Tech_ApplyPowerAmp(gentity_t *ent, int dmg) { +int Tech_ApplyPowerAmp(gentity_t* ent, int dmg) { if (dmg && ent->client && ent->client->pers.inventory[IT_TECH_POWER_AMP]) { return dmg * 2; } return dmg; } -bool Tech_ApplyPowerAmpSound(gentity_t *ent) { +bool Tech_ApplyPowerAmpSound(gentity_t* ent) { float volume = 1.0; if (ent->client && ent->client->silencer_shots) @@ -1090,14 +1104,14 @@ bool Tech_ApplyPowerAmpSound(gentity_t *ent) { return false; } -bool Tech_ApplyTimeAccel(gentity_t *ent) { +bool Tech_ApplyTimeAccel(gentity_t* ent) { if (ent->client && ent->client->pers.inventory[IT_TECH_TIME_ACCEL]) return true; return false; } -void Tech_ApplyTimeAccelSound(gentity_t *ent) { +void Tech_ApplyTimeAccelSound(gentity_t* ent) { float volume = 1.0; if (ent->client && ent->client->silencer_shots) @@ -1111,14 +1125,22 @@ void Tech_ApplyTimeAccelSound(gentity_t *ent) { } } -void Tech_ApplyAutoDoc(gentity_t *ent) { +/* +============= +Tech_ApplyAutoDoc + +Applies regeneration benefits when the Autodoc tech is owned. +============= +*/ +void Tech_ApplyAutoDoc(gentity_t* ent) { bool noise = false; - gclient_t *cl; + gclient_t* cl; int index; float volume = 1.0; bool mod = g_instagib->integer || g_nadefest->integer; + bool has_autodoc = false; bool no_health = mod || GTF(GTF_ARENA) || g_no_health->integer; - int max = g_vampiric_damage->integer ? ceil(g_vampiric_health_max->integer/2) : mod ? 100 : 150; + int max = G_GetTechRegenMax(g_vampiric_health_max->integer, g_vampiric_damage->integer, mod); cl = ent->client; if (!cl) @@ -1130,14 +1152,16 @@ void Tech_ApplyAutoDoc(gentity_t *ent) { if (cl->silencer_shots) volume = 0.2f; + has_autodoc = cl->pers.inventory[IT_TECH_AUTODOC]; + + if (!has_autodoc) + return; + if (mod && !cl->tech_regen_time) { cl->tech_regen_time = level.time; return; } - if (!(cl->pers.inventory[IT_TECH_AUTODOC] || mod)) - return; - if (cl->tech_regen_time < level.time) { bool mm = !!(RS(RS_MM)); gtime_t delay = mm ? 1_sec : 500_ms; @@ -1170,12 +1194,21 @@ void Tech_ApplyAutoDoc(gentity_t *ent) { } } -bool Tech_HasRegeneration(gentity_t *ent) { - if (!ent->client) return false; - if (ent->client->pers.inventory[IT_TECH_AUTODOC]) return true; - if (g_instagib->integer) return true; - if (g_nadefest->integer) return true; - return false; +/* +============= +Tech_HasRegeneration + +Returns true if the entity currently benefits from a regeneration effect. +============= +*/ +bool Tech_HasRegeneration(gentity_t* ent) { + if (!ent || !ent->client) + return false; + + if (ent->client->pu_time_regeneration > level.time) + return true; + + return ent->client->pers.inventory[IT_TECH_AUTODOC]; } // =============================================== @@ -1185,22 +1218,22 @@ bool Tech_HasRegeneration(gentity_t *ent) { GetItemByIndex =============== */ -gitem_t *GetItemByIndex(item_id_t index) { +gitem_t* GetItemByIndex(item_id_t index) { if (index <= IT_NULL || index >= IT_TOTAL) return nullptr; return &itemlist[index]; } -static gitem_t *ammolist[AMMO_MAX]; +static gitem_t* ammolist[AMMO_MAX]; -gitem_t *GetItemByAmmo(ammo_t ammo) { +gitem_t* GetItemByAmmo(ammo_t ammo) { return ammolist[ammo]; } -static gitem_t *poweruplist[POWERUP_MAX]; +static gitem_t* poweruplist[POWERUP_MAX]; -gitem_t *GetItemByPowerup(powerup_t powerup) { +gitem_t* GetItemByPowerup(powerup_t powerup) { return poweruplist[powerup]; } @@ -1210,9 +1243,9 @@ FindItemByClassname =============== */ -gitem_t *FindItemByClassname(const char *classname) { +gitem_t* FindItemByClassname(const char* classname) { int i; - gitem_t *it; + gitem_t* it; it = itemlist; for (i = 0; i < IT_TOTAL; i++, it++) { @@ -1231,9 +1264,9 @@ FindItem =============== */ -gitem_t *FindItem(const char *pickup_name) { +gitem_t* FindItem(const char* pickup_name) { int i; - gitem_t *it; + gitem_t* it; it = itemlist; for (i = 0; i < IT_TOTAL; i++, it++) { @@ -1249,7 +1282,7 @@ gitem_t *FindItem(const char *pickup_name) { //====================================================================== static inline item_flags_t GetSubstituteItemFlags(item_id_t id) { - const gitem_t *item = GetItemByIndex(id); + const gitem_t* item = GetItemByIndex(id); // we want to stay within the item class item_flags_t flags = item->flags & IF_TYPE_MASK; @@ -1260,7 +1293,7 @@ static inline item_flags_t GetSubstituteItemFlags(item_id_t id) { return flags; } -static inline item_id_t FindSubstituteItem(gentity_t *ent) { +static inline item_id_t FindSubstituteItem(gentity_t* ent) { // never replace flags if (ent->item->id == IT_FLAG_RED || ent->item->id == IT_FLAG_BLUE || ent->item->id == IT_TAG_TOKEN) return IT_NULL; @@ -1323,26 +1356,31 @@ static inline item_id_t FindSubstituteItem(gentity_t *ent) { // gather matching items for (item_id_t i = static_cast(IT_NULL + 1); i < IT_TOTAL; i = static_cast(static_cast(i) + 1)) { - const gitem_t *it = GetItemByIndex(i); + const gitem_t* it = GetItemByIndex(i); item_flags_t itflags = it->flags; bool add = false, subtract = false; if (game.item_inhibit_pu && itflags & (IF_POWERUP | IF_SPHERE)) { add = game.item_inhibit_pu > 0 ? true : false; subtract = game.item_inhibit_pu < 0 ? true : false; - } else if (game.item_inhibit_pa && itflags & IF_POWER_ARMOR) { + } + else if (game.item_inhibit_pa && itflags & IF_POWER_ARMOR) { add = game.item_inhibit_pa > 0 ? true : false; subtract = game.item_inhibit_pa < 0 ? true : false; - } else if (game.item_inhibit_ht && itflags & IF_HEALTH) { + } + else if (game.item_inhibit_ht && itflags & IF_HEALTH) { add = game.item_inhibit_ht > 0 ? true : false; subtract = game.item_inhibit_ht < 0 ? true : false; - } else if (game.item_inhibit_ar && itflags & IF_ARMOR) { + } + else if (game.item_inhibit_ar && itflags & IF_ARMOR) { add = game.item_inhibit_ar > 0 ? true : false; subtract = game.item_inhibit_ar < 0 ? true : false; - } else if (game.item_inhibit_am && itflags & IF_AMMO) { + } + else if (game.item_inhibit_am && itflags & IF_AMMO) { add = game.item_inhibit_am > 0 ? true : false; subtract = game.item_inhibit_am < 0 ? true : false; - } else if (game.item_inhibit_wp && itflags & IF_WEAPON) { + } + else if (game.item_inhibit_wp && itflags & IF_WEAPON) { add = game.item_inhibit_wp > 0 ? true : false; subtract = game.item_inhibit_wp < 0 ? true : false; } @@ -1387,7 +1425,7 @@ static inline item_id_t FindSubstituteItem(gentity_t *ent) { return possible_items[irandom(possible_item_count)]; } -item_id_t DoRandomRespawn(gentity_t *ent) { +item_id_t DoRandomRespawn(gentity_t* ent) { if (!ent->item) return IT_NULL; // why @@ -1400,11 +1438,11 @@ item_id_t DoRandomRespawn(gentity_t *ent) { } // originally 'DoRespawn' -THINK(RespawnItem) (gentity_t *ent) -> void { +THINK(RespawnItem) (gentity_t* ent) -> void { if (ent->team) { - gentity_t *master, *current; + gentity_t* master, * current; int count, choice; - + if (!ent->teammaster) gi.Com_ErrorFmt("{}: {}: bad teammaster", __FUNCTION__, *ent); @@ -1428,13 +1466,14 @@ THINK(RespawnItem) (gentity_t *ent) -> void { if (ent == current) current_index = count; } - + if (RS(RS_MM)) { choice = (current_index + 1) % count; //gi.Com_PrintFmt("ci={} co={} ch={}\n", current_index, count, choice); for (count = 0, ent = master; count < choice; ent = ent->chain, count++) ; - } else { + } + else { choice = irandom(count); for (count = 0, ent = master; count < choice; ent = ent->chain, count++) ; @@ -1476,7 +1515,7 @@ THINK(RespawnItem) (gentity_t *ent) -> void { } } -void SetRespawn(gentity_t *ent, gtime_t delay, bool hide_self) { +void SetRespawn(gentity_t* ent, gtime_t delay, bool hide_self) { if (!deathmatch->integer) return; @@ -1511,15 +1550,15 @@ void SetRespawn(gentity_t *ent, gtime_t delay, bool hide_self) { // 4x longer delay in horde if (GT(GT_HORDE)) - ent->nextthink += delay*3; + ent->nextthink += delay * 3; ent->think = RespawnItem; } //====================================================================== -static void Use_Teleporter(gentity_t *ent, gitem_t *item) { - gentity_t *fx = G_Spawn(); +static void Use_Teleporter(gentity_t* ent, gitem_t* item) { + gentity_t* fx = G_Spawn(); fx->classname = "telefx"; fx->s.event = EV_PLAYER_TELEPORT; fx->s.origin = ent->s.origin; @@ -1535,7 +1574,7 @@ static void Use_Teleporter(gentity_t *ent, gitem_t *item) { UsedMessage(ent, item); } -static bool Pickup_Teleporter(gentity_t *ent, gentity_t *other) { +static bool Pickup_Teleporter(gentity_t* ent, gentity_t* other) { if (!deathmatch->integer) return false; if (other->client->pers.inventory[ent->item->id]) @@ -1558,7 +1597,7 @@ static bool IsInstantItemsEnabled() { return false; } -static bool Pickup_AllowPowerupPickup(gentity_t *ent, gentity_t *other) { +static bool Pickup_AllowPowerupPickup(gentity_t* ent, gentity_t* other) { int quantity = other->client->pers.inventory[ent->item->id]; if ((skill->integer == 0 && quantity >= 4) || (skill->integer == 1 && quantity >= 3) || @@ -1586,19 +1625,19 @@ static bool Pickup_AllowPowerupPickup(gentity_t *ent, gentity_t *other) { return true; } -static bool Pickup_Powerup(gentity_t *ent, gentity_t *other) { +static bool Pickup_Powerup(gentity_t* ent, gentity_t* other) { if (!Pickup_AllowPowerupPickup(ent, other)) return false; other->client->pers.inventory[ent->item->id]++; - + if (g_quadhog->integer && ent->item->id == IT_POWERUP_QUAD) { if (ent->item->use) ent->item->use(other, ent->item); G_FreeEntity(ent); return true; } - + bool is_dropped_from_death = ent->spawnflags.has(SPAWNFLAG_ITEM_DROPPED_PLAYER) && !ent->spawnflags.has(SPAWNFLAG_ITEM_DROPPED); if (IsInstantItemsEnabled() || is_dropped_from_death) { @@ -1665,7 +1704,7 @@ static bool Pickup_Powerup(gentity_t *ent, gentity_t *other) { return true; } -static bool Pickup_AllowTimedItemPickup(gentity_t *ent, gentity_t *other) { +static bool Pickup_AllowTimedItemPickup(gentity_t* ent, gentity_t* other) { int quantity = other->client->pers.inventory[ent->item->id]; if ((skill->integer == 0 && quantity >= 3) || (skill->integer == 1 && quantity >= 2) || @@ -1678,7 +1717,7 @@ static bool Pickup_AllowTimedItemPickup(gentity_t *ent, gentity_t *other) { return true; } -static bool Pickup_TimedItem(gentity_t *ent, gentity_t *other) { +static bool Pickup_TimedItem(gentity_t* ent, gentity_t* other) { if (!Pickup_AllowTimedItemPickup(ent, other)) return false; @@ -1692,9 +1731,11 @@ static bool Pickup_TimedItem(gentity_t *ent, gentity_t *other) { bool msg = false; if (ent->item->id == IT_ADRENALINE && !other->client->pers.holdable_item_msg_adren) { other->client->pers.holdable_item_msg_adren = msg = true; - } else if (ent->item->id == IT_TELEPORTER && !other->client->pers.holdable_item_msg_tele) { + } + else if (ent->item->id == IT_TELEPORTER && !other->client->pers.holdable_item_msg_tele) { other->client->pers.holdable_item_msg_tele = msg = true; - } else if (ent->item->id == IT_DOPPELGANGER && !other->client->pers.holdable_item_msg_doppel) { + } + else if (ent->item->id == IT_DOPPELGANGER && !other->client->pers.holdable_item_msg_doppel) { other->client->pers.holdable_item_msg_doppel = msg = true; } if (msg) @@ -1709,7 +1750,7 @@ static bool Pickup_TimedItem(gentity_t *ent, gentity_t *other) { //====================================================================== -static void Use_Defender(gentity_t *ent, gitem_t *item) { +static void Use_Defender(gentity_t* ent, gitem_t* item) { if (ent->client && ent->client->owned_sphere) { gi.LocClient_Print(ent, PRINT_HIGH, "$g_only_one_sphere_time"); return; @@ -1720,7 +1761,7 @@ static void Use_Defender(gentity_t *ent, gitem_t *item) { Defender_Launch(ent); } -static void Use_Hunter(gentity_t *ent, gitem_t *item) { +static void Use_Hunter(gentity_t* ent, gitem_t* item) { if (ent->client && ent->client->owned_sphere) { gi.LocClient_Print(ent, PRINT_HIGH, "$g_only_one_sphere_time"); return; @@ -1731,7 +1772,7 @@ static void Use_Hunter(gentity_t *ent, gitem_t *item) { Hunter_Launch(ent); } -static void Use_Vengeance(gentity_t *ent, gitem_t *item) { +static void Use_Vengeance(gentity_t* ent, gitem_t* item) { if (ent->client && ent->client->owned_sphere) { gi.LocClient_Print(ent, PRINT_HIGH, "$g_only_one_sphere_time"); return; @@ -1742,7 +1783,7 @@ static void Use_Vengeance(gentity_t *ent, gitem_t *item) { Vengeance_Launch(ent); } -static bool Pickup_Sphere(gentity_t *ent, gentity_t *other) { +static bool Pickup_Sphere(gentity_t* ent, gentity_t* other) { int quantity; if (other->client && other->client->owned_sphere) { @@ -1773,7 +1814,7 @@ static bool Pickup_Sphere(gentity_t *ent, gentity_t *other) { //====================================================================== -static void Use_IR(gentity_t *ent, gitem_t *item) { +static void Use_IR(gentity_t* ent, gitem_t* item) { ent->client->pers.inventory[item->id]--; ent->client->ir_time = max(level.time, ent->client->ir_time) + 60_sec; @@ -1783,7 +1824,7 @@ static void Use_IR(gentity_t *ent, gitem_t *item) { //====================================================================== -static void Use_Nuke(gentity_t *ent, gitem_t *item) { +static void Use_Nuke(gentity_t* ent, gitem_t* item) { vec3_t forward, right, start; ent->client->pers.inventory[item->id]--; @@ -1794,7 +1835,7 @@ static void Use_Nuke(gentity_t *ent, gitem_t *item) { fire_nuke(ent, start, forward, 100); } -static bool Pickup_Nuke(gentity_t *ent, gentity_t *other) { +static bool Pickup_Nuke(gentity_t* ent, gentity_t* other) { int quantity = other->client->pers.inventory[ent->item->id]; if (quantity >= 1) @@ -1812,7 +1853,14 @@ static bool Pickup_Nuke(gentity_t *ent, gentity_t *other) { //====================================================================== -static void Use_Doppelganger(gentity_t *ent, gitem_t *item) { +/* +============= +Use_Doppelganger + +Spawns a doppelganger at a nearby valid location and consumes the item. +============= +*/ +static void Use_Doppelganger(gentity_t* ent, gitem_t* item) { vec3_t forward, right; vec3_t createPt, spawnPt; vec3_t ang; @@ -1822,10 +1870,10 @@ static void Use_Doppelganger(gentity_t *ent, gitem_t *item) { createPt = ent->s.origin + (forward * 48); - if (!FindSpawnPoint(createPt, ent->mins, ent->maxs, spawnPt, 32)) + if (!FindSpawnPoint(createPt, ent->mins, ent->maxs, spawnPt, 32, true, ent->gravityVector)) return; - if (!CheckGroundSpawnPoint(spawnPt, ent->mins, ent->maxs, 64, -1)) + if (!CheckGroundSpawnPoint(spawnPt, ent->mins, ent->maxs, 64, ent->gravityVector)) return; ent->client->pers.inventory[item->id]--; @@ -1835,15 +1883,26 @@ static void Use_Doppelganger(gentity_t *ent, gitem_t *item) { fire_doppelganger(ent, spawnPt, forward); } -static bool Pickup_Doppelganger(gentity_t *ent, gentity_t *other) { - int quantity; +/* +============= +Pickup_Doppelganger + +Checks for doppelganger limits, granting the pickup when allowed. +============= +*/ +static bool Pickup_Doppelganger(gentity_t* ent, gentity_t* other) { + int quantity; + int max_allowed; if (!deathmatch->integer) return false; + max_allowed = G_GetHoldableMax(g_dm_holdable_doppel_max->integer, ent->item->quantity_max, 1); quantity = other->client->pers.inventory[ent->item->id]; - if (quantity >= 1) // FIXME - apply max to doppelgangers + if (quantity >= max_allowed) { + gi.LocClient_Print(other, PRINT_LOW, "You can only carry {} {}\n", max_allowed, ent->item->pickup_name); return false; + } other->client->pers.inventory[ent->item->id]++; @@ -1854,7 +1913,7 @@ static bool Pickup_Doppelganger(gentity_t *ent, gentity_t *other) { //====================================================================== -static bool Pickup_General(gentity_t *ent, gentity_t *other) { +static bool Pickup_General(gentity_t* ent, gentity_t* other) { if (other->client->pers.inventory[ent->item->id]) return false; @@ -1865,17 +1924,17 @@ static bool Pickup_General(gentity_t *ent, gentity_t *other) { return true; } -static bool Pickup_Ball(gentity_t *ent, gentity_t *other) { +static bool Pickup_Ball(gentity_t* ent, gentity_t* other) { other->client->pers.inventory[ent->item->id] = 1; return true; } -static void Drop_General(gentity_t *ent, gitem_t *item) { +static void Drop_General(gentity_t* ent, gitem_t* item) { if (g_quadhog->integer && item->id == IT_POWERUP_QUAD) return; - gentity_t *dropped = Drop_Item(ent, item); + gentity_t* dropped = Drop_Item(ent, item); dropped->spawnflags |= SPAWNFLAG_ITEM_DROPPED_PLAYER; dropped->svflags &= ~SVF_INSTANCED; ent->client->pers.inventory[item->id]--; @@ -1913,7 +1972,7 @@ static void Drop_General(gentity_t *ent, gitem_t *item) { //====================================================================== -static void Use_Adrenaline(gentity_t *ent, gitem_t *item) { +static void Use_Adrenaline(gentity_t* ent, gitem_t* item) { ent->max_health += deathmatch->integer ? ((RS(RS_MM)) ? 5 : 0) : 1; if (ent->health < ent->max_health) @@ -1927,7 +1986,7 @@ static void Use_Adrenaline(gentity_t *ent, gitem_t *item) { UsedMessage(ent, item); } -static bool Pickup_LegacyHead(gentity_t *ent, gentity_t *other) { +static bool Pickup_LegacyHead(gentity_t* ent, gentity_t* other) { other->max_health += 5; other->health += 5; @@ -1936,7 +1995,7 @@ static bool Pickup_LegacyHead(gentity_t *ent, gentity_t *other) { return true; } -void G_CheckPowerArmor(gentity_t *ent) { +void G_CheckPowerArmor(gentity_t* ent) { bool has_enough_cells; if (!ent->client->pers.inventory[IT_AMMO_CELLS]) @@ -1949,11 +2008,12 @@ void G_CheckPowerArmor(gentity_t *ent) { if (ent->flags & FL_POWER_ARMOR) { // ran out of cells for power armor / lost power armor if (!has_enough_cells || (!ent->client->pers.inventory[IT_POWER_SCREEN] && - !ent->client->pers.inventory[IT_POWER_SHIELD])) { + !ent->client->pers.inventory[IT_POWER_SHIELD])) { ent->flags &= ~FL_POWER_ARMOR; gi.sound(ent, CHAN_AUTO, gi.soundindex("misc/power2.wav"), 1, ATTN_NORM, 0); } - } else { + } + else { // special case for power armor, for auto-shields if (ent->client->pers.autoshield != AUTO_SHIELD_MANUAL && has_enough_cells && (ent->client->pers.inventory[IT_POWER_SCREEN] || @@ -1980,9 +2040,9 @@ static item_id_t AmmoConvertId(item_id_t original_id) { return new_id; } -static inline bool G_AddAmmoAndCap(gentity_t *other, item_id_t id, int32_t max, int32_t quantity) { +static inline bool G_AddAmmoAndCap(gentity_t* other, item_id_t id, int32_t max, int32_t quantity) { item_id_t new_id = AmmoConvertId(id); - + if (other->client->pers.inventory[new_id] == AMMO_INFINITE) return false; @@ -1991,7 +2051,8 @@ static inline bool G_AddAmmoAndCap(gentity_t *other, item_id_t id, int32_t max, if (quantity == AMMO_INFINITE) { other->client->pers.inventory[new_id] = AMMO_INFINITE; - } else { + } + else { other->client->pers.inventory[new_id] += quantity; if (other->client->pers.inventory[new_id] > max) other->client->pers.inventory[new_id] = max; @@ -2001,16 +2062,16 @@ static inline bool G_AddAmmoAndCap(gentity_t *other, item_id_t id, int32_t max, return true; } -static inline bool G_AddAmmoAndCapQuantity(gentity_t *other, ammo_t ammo) { - gitem_t *item = GetItemByAmmo(ammo); +static inline bool G_AddAmmoAndCapQuantity(gentity_t* other, ammo_t ammo) { + gitem_t* item = GetItemByAmmo(ammo); return G_AddAmmoAndCap(other, item->id, other->client->pers.max_ammo[ammo], item->quantity); } -static inline void G_AdjustAmmoCap(gentity_t *other, ammo_t ammo, int16_t new_max) { +static inline void G_AdjustAmmoCap(gentity_t* other, ammo_t ammo, int16_t new_max) { other->client->pers.max_ammo[ammo] = max(other->client->pers.max_ammo[ammo], new_max); } -static bool Pickup_Bandolier(gentity_t *ent, gentity_t *other) { +static bool Pickup_Bandolier(gentity_t* ent, gentity_t* other) { G_AdjustAmmoCap(other, AMMO_BULLETS, 250); G_AdjustAmmoCap(other, AMMO_SHELLS, 150); G_AdjustAmmoCap(other, AMMO_CELLS, 250); @@ -2028,8 +2089,8 @@ static bool Pickup_Bandolier(gentity_t *ent, gentity_t *other) { return true; } -void G_CheckAutoSwitch(gentity_t *ent, gitem_t *item, bool is_new); -static bool Pickup_Pack(gentity_t *ent, gentity_t *other) { +void G_CheckAutoSwitch(gentity_t* ent, gitem_t* item, bool is_new); +static bool Pickup_Pack(gentity_t* ent, gentity_t* other) { G_AdjustAmmoCap(other, AMMO_BULLETS, 300); G_AdjustAmmoCap(other, AMMO_SHELLS, 200); G_AdjustAmmoCap(other, AMMO_ROCKETS, 100); @@ -2049,8 +2110,8 @@ static bool Pickup_Pack(gentity_t *ent, gentity_t *other) { G_AddAmmoAndCapQuantity(other, AMMO_MAGSLUG); G_AddAmmoAndCapQuantity(other, AMMO_FLECHETTES); G_AddAmmoAndCapQuantity(other, AMMO_DISRUPTOR); - - gitem_t *it = GetItemByIndex(IT_AMMO_GRENADES); + + gitem_t* it = GetItemByIndex(IT_AMMO_GRENADES); if (it) G_CheckAutoSwitch(other, it, !other->client->pers.inventory[IT_AMMO_GRENADES]); @@ -2061,12 +2122,12 @@ static bool Pickup_Pack(gentity_t *ent, gentity_t *other) { //====================================================================== -static void Use_Powerup_BroadcastMsg(gentity_t *ent, gitem_t *item, const char *sound_name, const char *announcer_name) { +static void Use_Powerup_BroadcastMsg(gentity_t* ent, gitem_t* item, const char* sound_name, const char* announcer_name) { if (deathmatch->integer) { if (g_quadhog->integer && item->id == IT_POWERUP_QUAD) { gi.LocBroadcast_Print(PRINT_CENTER, "{} is the Quad Hog!\n", ent->client->resp.netname); - //} else { - // gi.LocBroadcast_Print(PRINT_HIGH, "{} got the {}!\n", ent->client->resp.netname, item->pickup_name); + //} else { + // gi.LocBroadcast_Print(PRINT_HIGH, "{} got the {}!\n", ent->client->resp.netname, item->pickup_name); } if (RS(RS_MM) || RS(RS_Q3A)) { gi.sound(ent, CHAN_RELIABLE | CHAN_NO_PHS_ADD | CHAN_AUX, gi.soundindex(sound_name), 1, ATTN_NONE, 0); @@ -2075,7 +2136,7 @@ static void Use_Powerup_BroadcastMsg(gentity_t *ent, gitem_t *item, const char * } } -void Use_Quad(gentity_t *ent, gitem_t *item) { +void Use_Quad(gentity_t* ent, gitem_t* item) { gtime_t timeout; ent->client->pers.inventory[item->id]--; @@ -2083,7 +2144,8 @@ void Use_Quad(gentity_t *ent, gitem_t *item) { if (quad_drop_timeout_hack) { timeout = quad_drop_timeout_hack; quad_drop_timeout_hack = 0_ms; - } else { + } + else { timeout = 30_sec; } @@ -2093,7 +2155,7 @@ void Use_Quad(gentity_t *ent, gitem_t *item) { } // ===================================================================== -void Use_Haste(gentity_t *ent, gitem_t *item) { +void Use_Haste(gentity_t* ent, gitem_t* item) { gtime_t timeout; ent->client->pers.inventory[item->id]--; @@ -2101,7 +2163,8 @@ void Use_Haste(gentity_t *ent, gitem_t *item) { if (haste_drop_timeout_hack) { timeout = haste_drop_timeout_hack; haste_drop_timeout_hack = 0_ms; - } else { + } + else { timeout = 30_sec; } @@ -2112,7 +2175,7 @@ void Use_Haste(gentity_t *ent, gitem_t *item) { //====================================================================== -static void Use_Double(gentity_t *ent, gitem_t *item) { +static void Use_Double(gentity_t* ent, gitem_t* item) { gtime_t timeout; ent->client->pers.inventory[item->id]--; @@ -2120,7 +2183,8 @@ static void Use_Double(gentity_t *ent, gitem_t *item) { if (double_drop_timeout_hack) { timeout = double_drop_timeout_hack; double_drop_timeout_hack = 0_ms; - } else { + } + else { timeout = 30_sec; } @@ -2131,21 +2195,21 @@ static void Use_Double(gentity_t *ent, gitem_t *item) { //====================================================================== -static void Use_Breather(gentity_t *ent, gitem_t *item) { +static void Use_Breather(gentity_t* ent, gitem_t* item) { ent->client->pers.inventory[item->id]--; ent->client->pu_time_rebreather = max(level.time, ent->client->pu_time_rebreather) + (RS(RS_MM) ? 45_sec : 30_sec); } //====================================================================== -static void Use_Envirosuit(gentity_t *ent, gitem_t *item) { +static void Use_Envirosuit(gentity_t* ent, gitem_t* item) { ent->client->pers.inventory[item->id]--; ent->client->pu_time_enviro = max(level.time, ent->client->pu_time_enviro) + 30_sec; } //====================================================================== -static void Use_Protection(gentity_t *ent, gitem_t *item) { +static void Use_Protection(gentity_t* ent, gitem_t* item) { gtime_t timeout; ent->client->pers.inventory[item->id]--; @@ -2153,7 +2217,8 @@ static void Use_Protection(gentity_t *ent, gitem_t *item) { if (protection_drop_timeout_hack) { timeout = protection_drop_timeout_hack; protection_drop_timeout_hack = 0_ms; - } else { + } + else { timeout = 30_sec; } @@ -2164,9 +2229,9 @@ static void Use_Protection(gentity_t *ent, gitem_t *item) { //====================================================================== -void Powerup_ApplyRegeneration(gentity_t *ent) { +void Powerup_ApplyRegeneration(gentity_t* ent) { bool noise = false; - gclient_t *cl; + gclient_t* cl; float volume = 1.0; bool mod = g_instagib->integer || g_nadefest->integer; bool no_health = mod || GTF(GTF_ARENA) || g_no_health->integer; @@ -2208,7 +2273,7 @@ void Powerup_ApplyRegeneration(gentity_t *ent) { } } -static void Use_Regeneration(gentity_t *ent, gitem_t *item) { +static void Use_Regeneration(gentity_t* ent, gitem_t* item) { gtime_t timeout; ent->client->pers.inventory[item->id]--; @@ -2216,7 +2281,8 @@ static void Use_Regeneration(gentity_t *ent, gitem_t *item) { if (regeneration_drop_timeout_hack) { timeout = regeneration_drop_timeout_hack; regeneration_drop_timeout_hack = 0_ms; - } else { + } + else { timeout = 30_sec; } @@ -2225,7 +2291,7 @@ static void Use_Regeneration(gentity_t *ent, gitem_t *item) { Use_Powerup_BroadcastMsg(ent, item, "items/protect.wav", "regeneration"); } -static void Use_Invisibility(gentity_t *ent, gitem_t *item) { +static void Use_Invisibility(gentity_t* ent, gitem_t* item) { gtime_t timeout; ent->client->pers.inventory[item->id]--; @@ -2233,7 +2299,8 @@ static void Use_Invisibility(gentity_t *ent, gitem_t *item) { if (invisibility_drop_timeout_hack) { timeout = invisibility_drop_timeout_hack; invisibility_drop_timeout_hack = 0_ms; - } else { + } + else { timeout = 30_sec; } @@ -2244,21 +2311,22 @@ static void Use_Invisibility(gentity_t *ent, gitem_t *item) { //====================================================================== -static void Use_Silencer(gentity_t *ent, gitem_t *item) { +static void Use_Silencer(gentity_t* ent, gitem_t* item) { ent->client->pers.inventory[item->id]--; ent->client->silencer_shots += 30; } //====================================================================== -static bool Pickup_Key(gentity_t *ent, gentity_t *other) { +static bool Pickup_Key(gentity_t* ent, gentity_t* other) { if (coop->integer) { if (ent->item->id == IT_KEY_POWER_CUBE || ent->item->id == IT_KEY_EXPLOSIVE_CHARGES) { if (other->client->pers.power_cubes & ((ent->spawnflags & SPAWNFLAG_EDITOR_MASK).value >> 8)) return false; other->client->pers.inventory[ent->item->id]++; other->client->pers.power_cubes |= ((ent->spawnflags & SPAWNFLAG_EDITOR_MASK).value >> 8); - } else { + } + else { if (other->client->pers.inventory[ent->item->id]) return false; other->client->pers.inventory[ent->item->id] = 1; @@ -2273,7 +2341,7 @@ static bool Pickup_Key(gentity_t *ent, gentity_t *other) { //====================================================================== -bool Add_Ammo(gentity_t *ent, gitem_t *item, int count) { +bool Add_Ammo(gentity_t* ent, gitem_t* item, int count) { if (!ent->client || item->tag < AMMO_BULLETS || item->tag >= AMMO_MAX) return false; @@ -2281,7 +2349,7 @@ bool Add_Ammo(gentity_t *ent, gitem_t *item, int count) { } // we just got weapon `item`, check if we should switch to it -void G_CheckAutoSwitch(gentity_t *ent, gitem_t *item, bool is_new) { +void G_CheckAutoSwitch(gentity_t* ent, gitem_t* item, bool is_new) { // already using or switching to if (ent->client->pers.weapon == item || ent->client->newweapon == item) @@ -2317,13 +2385,14 @@ void G_CheckAutoSwitch(gentity_t *ent, gitem_t *item, bool is_new) { break; case IT_WEAPON_SHOTGUN: // switch only to SSG - if (item->id != IT_WEAPON_SSHOTGUN) + if (item->id != IT_WEAPON_SSHOTGUN) return; break; case IT_WEAPON_MACHINEGUN: if (RS(RS_Q3A)) { // always switch from mg in Q3A - } else { + } + else { // switch only to CG if (item->id != IT_WEAPON_CHAINGUN) return; @@ -2344,7 +2413,7 @@ void G_CheckAutoSwitch(gentity_t *ent, gitem_t *item, bool is_new) { ent->client->newweapon = item; } -static bool Pickup_Ammo(gentity_t *ent, gentity_t *other) { +static bool Pickup_Ammo(gentity_t* ent, gentity_t* other) { bool weapon = !!(ent->item->flags & IF_WEAPON); int count, oldcount; @@ -2372,7 +2441,7 @@ static bool Pickup_Ammo(gentity_t *ent, gentity_t *other) { return true; } -static void Drop_Ammo(gentity_t *ent, gitem_t *item) { +static void Drop_Ammo(gentity_t* ent, gitem_t* item) { // [Paril-KEX] if (InfiniteAmmoOn(item)) return; @@ -2382,7 +2451,7 @@ static void Drop_Ammo(gentity_t *ent, gitem_t *item) { if (ent->client->pers.inventory[index] <= 0) return; - gentity_t *drop = Drop_Item(ent, item); + gentity_t* drop = Drop_Item(ent, item); drop->spawnflags |= SPAWNFLAG_ITEM_DROPPED_PLAYER; drop->svflags &= ~SVF_INSTANCED; @@ -2410,7 +2479,7 @@ static void Drop_Ammo(gentity_t *ent, gitem_t *item) { //====================================================================== -static THINK(MegaHealth_think) (gentity_t *self) -> void { +static THINK(MegaHealth_think) (gentity_t* self) -> void { int32_t health = self->max_health; if (health < self->owner->max_health) health = self->owner->max_health; @@ -2428,7 +2497,7 @@ static THINK(MegaHealth_think) (gentity_t *self) -> void { G_FreeEntity(self); } -static bool Pickup_Health(gentity_t *ent, gentity_t *other) { +static bool Pickup_Health(gentity_t* ent, gentity_t* other) { int health_flags = (ent->style ? ent->style : ent->item->tag); if (!(health_flags & HEALTH_IGNORE_MAX)) @@ -2474,7 +2543,8 @@ static bool Pickup_Health(gentity_t *ent, gentity_t *other) { // mega health doesn't need to be special in SP // since it never respawns. other->client->pers.megahealth_time = 5_sec; - } else { + } + else { ent->think = MegaHealth_think; ent->nextthink = level.time + 5_sec; ent->owner = other; @@ -2487,7 +2557,8 @@ static bool Pickup_Health(gentity_t *ent, gentity_t *other) { ent->max_health = ent->owner->health - count; } - } else { + } + else { SetRespawn(ent, RS(RS_Q3A) ? 60_sec : 30_sec); } @@ -2496,7 +2567,7 @@ static bool Pickup_Health(gentity_t *ent, gentity_t *other) { //====================================================================== -item_id_t ArmorIndex(gentity_t *ent) { +item_id_t ArmorIndex(gentity_t* ent) { if (ent->svflags & SVF_MONSTER) return ent->monsterinfo.armor_type; @@ -2505,8 +2576,9 @@ item_id_t ArmorIndex(gentity_t *ent) { if (ent->client->pers.inventory[IT_ARMOR_JACKET] > 0 || ent->client->pers.inventory[IT_ARMOR_COMBAT] > 0 || ent->client->pers.inventory[IT_ARMOR_BODY] > 0) - return IT_ARMOR_COMBAT; - } else { + return IT_ARMOR_COMBAT; + } + else { if (ent->client->pers.inventory[IT_ARMOR_JACKET] > 0) return IT_ARMOR_JACKET; else if (ent->client->pers.inventory[IT_ARMOR_COMBAT] > 0) @@ -2519,7 +2591,7 @@ item_id_t ArmorIndex(gentity_t *ent) { return IT_NULL; } -static bool Pickup_Armor_Q3(gentity_t *ent, gentity_t *other, int32_t base_count) { +static bool Pickup_Armor_Q3(gentity_t* ent, gentity_t* other, int32_t base_count) { if (other->client->pers.inventory[IT_ARMOR_COMBAT] >= other->client->pers.max_health * 2) return false; @@ -2539,10 +2611,10 @@ static bool Pickup_Armor_Q3(gentity_t *ent, gentity_t *other, int32_t base_count return true; } -static bool Pickup_Armor(gentity_t *ent, gentity_t *other) { +static bool Pickup_Armor(gentity_t* ent, gentity_t* other) { item_id_t old_armor_index; - const gitem_armor_t *oldinfo; - const gitem_armor_t *newinfo; + const gitem_armor_t* oldinfo; + const gitem_armor_t* newinfo; int newcount; float salvage; int salvagecount; @@ -2593,7 +2665,8 @@ static bool Pickup_Armor(gentity_t *ent, gentity_t *other) { // change armor to new item with computed value other->client->pers.inventory[ent->item->id] = newcount; - } else { + } + else { // calc new armor values salvage = newinfo->normal_protection / oldinfo->normal_protection; salvagecount = (int)(salvage * base_count); @@ -2620,7 +2693,7 @@ static bool Pickup_Armor(gentity_t *ent, gentity_t *other) { //====================================================================== -item_id_t PowerArmorType(gentity_t *ent) { +item_id_t PowerArmorType(gentity_t* ent) { if (!ent->client) return IT_NULL; @@ -2636,11 +2709,12 @@ item_id_t PowerArmorType(gentity_t *ent) { return IT_NULL; } -static void Use_PowerArmor(gentity_t *ent, gitem_t *item) { +static void Use_PowerArmor(gentity_t* ent, gitem_t* item) { if (ent->flags & FL_POWER_ARMOR) { ent->flags &= ~(FL_POWER_ARMOR | FL_WANTS_POWER_ARMOR); gi.sound(ent, CHAN_AUTO, gi.soundindex("misc/power2.wav"), 1, ATTN_NORM, 0); - } else { + } + else { if (!ent->client->pers.inventory[IT_AMMO_CELLS]) { gi.LocClient_Print(ent, PRINT_HIGH, "$g_no_cells_power_armor"); return; @@ -2656,7 +2730,7 @@ static void Use_PowerArmor(gentity_t *ent, gitem_t *item) { } } -static bool Pickup_PowerArmor(gentity_t *ent, gentity_t *other) { +static bool Pickup_PowerArmor(gentity_t* ent, gentity_t* other) { other->client->pers.inventory[ent->item->id]++; SetRespawn(ent, gtime_t::from_sec(ent->item->quantity)); @@ -2667,7 +2741,7 @@ static bool Pickup_PowerArmor(gentity_t *ent, gentity_t *other) { return true; } -static void Drop_PowerArmor(gentity_t *ent, gitem_t *item) { +static void Drop_PowerArmor(gentity_t* ent, gitem_t* item) { if ((ent->flags & FL_POWER_ARMOR) && (ent->client->pers.inventory[item->id] == 1)) Use_PowerArmor(ent, item); Drop_General(ent, item); @@ -2675,7 +2749,7 @@ static void Drop_PowerArmor(gentity_t *ent, gitem_t *item) { //====================================================================== -bool Entity_IsVisibleToPlayer(gentity_t *ent, gentity_t *player) { +bool Entity_IsVisibleToPlayer(gentity_t* ent, gentity_t* player) { // Q2Eaks make eyecam chase target invisible, but keep other client visible if (g_eyecam->integer && player->client->follow_target && ent == player->client->follow_target) return false; @@ -2690,7 +2764,7 @@ bool Entity_IsVisibleToPlayer(gentity_t *ent, gentity_t *player) { Touch_Item =============== */ -TOUCH(Touch_Item) (gentity_t *ent, gentity_t *other, const trace_t &tr, bool other_touching_self) -> void { +TOUCH(Touch_Item) (gentity_t* ent, gentity_t* other, const trace_t& tr, bool other_touching_self) -> void { bool taken; if (!other->client) @@ -2702,7 +2776,7 @@ TOUCH(Touch_Item) (gentity_t *ent, gentity_t *other, const trace_t &tr, bool oth if (!ent->item->pickup) return; // not a grabbable item? - gitem_t *it = ent->item; + gitem_t* it = ent->item; // already got this instanced item if (coop->integer && P_UseCoopInstancedItems()) { @@ -2757,7 +2831,7 @@ TOUCH(Touch_Item) (gentity_t *ent, gentity_t *other, const trace_t &tr, bool oth case IT_ARMOR_BODY: case IT_POWER_SCREEN: case IT_POWER_SHIELD: - case IT_ADRENALINE: + case IT_ADRENALINE: case IT_HEALTH_MEGA: case IT_POWERUP_QUAD: case IT_POWERUP_DOUBLE: @@ -2802,7 +2876,7 @@ TOUCH(Touch_Item) (gentity_t *ent, gentity_t *other, const trace_t &tr, bool oth // [Paril-KEX] see above msg; this also disables the message in DM // since there's no need to print pickup messages in DM (this wasn't // even a documented feature, relays were traditionally used for this) - const char *message_backup = nullptr; + const char* message_backup = nullptr; if (deathmatch->integer || (coop->integer && P_UseCoopInstancedItems())) std::swap(message_backup, ent->message); @@ -2827,7 +2901,8 @@ TOUCH(Touch_Item) (gentity_t *ent, gentity_t *other, const trace_t &tr, bool oth // if not dropped else should_remove = ent->spawnflags.has(SPAWNFLAG_ITEM_DROPPED | SPAWNFLAG_ITEM_DROPPED_PLAYER) || !(it->flags & IF_STAY_COOP); - } else + } + else should_remove = !deathmatch->integer || ent->spawnflags.has(SPAWNFLAG_ITEM_DROPPED | SPAWNFLAG_ITEM_DROPPED_PLAYER); if (should_remove) { @@ -2841,14 +2916,14 @@ TOUCH(Touch_Item) (gentity_t *ent, gentity_t *other, const trace_t &tr, bool oth //====================================================================== -static TOUCH(drop_temp_touch) (gentity_t *ent, gentity_t *other, const trace_t &tr, bool other_touching_self) -> void { +static TOUCH(drop_temp_touch) (gentity_t* ent, gentity_t* other, const trace_t& tr, bool other_touching_self) -> void { if (other == ent->owner) return; Touch_Item(ent, other, tr, other_touching_self); } -static THINK(drop_make_touchable) (gentity_t *ent) -> void { +static THINK(drop_make_touchable) (gentity_t* ent) -> void { ent->touch = Touch_Item; if (deathmatch->integer) { ent->nextthink = level.time + 29_sec; @@ -2856,8 +2931,8 @@ static THINK(drop_make_touchable) (gentity_t *ent) -> void { } } -gentity_t *Drop_Item(gentity_t *ent, gitem_t *item) { - gentity_t *dropped; +gentity_t* Drop_Item(gentity_t* ent, gitem_t* item) { + gentity_t* dropped; vec3_t forward, right; vec3_t offset; @@ -2884,7 +2959,8 @@ gentity_t *Drop_Item(gentity_t *ent, gitem_t *item) { dropped->s.origin = G_ProjectSource(ent->s.origin, offset, forward, right); trace = gi.trace(ent->s.origin, dropped->mins, dropped->maxs, dropped->s.origin, ent, CONTENTS_SOLID); dropped->s.origin = trace.endpos; - } else { + } + else { AngleVectors(ent->s.angles, forward, right, nullptr); dropped->s.origin = (ent->absmin + ent->absmax) / 2; } @@ -2904,14 +2980,15 @@ gentity_t *Drop_Item(gentity_t *ent, gitem_t *item) { return dropped; } -static USE(Use_Item) (gentity_t *ent, gentity_t *other, gentity_t *activator) -> void { +static USE(Use_Item) (gentity_t* ent, gentity_t* other, gentity_t* activator) -> void { ent->svflags &= ~SVF_NOCLIENT; ent->use = nullptr; if (ent->spawnflags.has(SPAWNFLAG_ITEM_NO_TOUCH)) { ent->solid = SOLID_BBOX; ent->touch = nullptr; - } else { + } + else { ent->solid = SOLID_TRIGGER; ent->touch = Touch_Item; } @@ -2928,12 +3005,13 @@ FinishSpawningItem previously 'droptofloor' ================ */ -static THINK(FinishSpawningItem) (gentity_t *ent) -> void { +static THINK(FinishSpawningItem) (gentity_t* ent) -> void { // [Paril-KEX] scale foodcube based on how much we ingested if (strcmp(ent->classname, "item_foodcube") == 0) { - ent->mins = vec3_t{ -8, -8, -8 } * ent->s.scale; - ent->maxs = vec3_t{ 8, 8, 8 } * ent->s.scale; - } else { + ent->mins = vec3_t{ -8, -8, -8 } *ent->s.scale; + ent->maxs = vec3_t{ 8, 8, 8 } *ent->s.scale; + } + else { ent->mins = { -15, -15, -15 }; ent->maxs = { 15, 15, 15 }; } @@ -2945,12 +3023,13 @@ static THINK(FinishSpawningItem) (gentity_t *ent) -> void { if (ent->spawnflags.has(SPAWNFLAG_ITEM_SUSPENDED)) { ent->movetype = MOVETYPE_NONE; - } else { + } + else { ent->movetype = MOVETYPE_TOSS; vec3_t dest = ent->s.origin + vec3_t{ 0, 0, -4096 }; trace_t tr = gi.trace(ent->s.origin, ent->mins, ent->maxs, dest, ent, MASK_SOLID); - + if (tr.startsolid) { if (G_FixStuckObject(ent, ent->s.origin) == stuck_result_t::NO_GOOD_POSITION) { if (strcmp(ent->classname, "item_foodcube") == 0) @@ -2961,7 +3040,8 @@ static THINK(FinishSpawningItem) (gentity_t *ent) -> void { return; } } - } else + } + else ent->s.origin = tr.endpos; } @@ -2972,12 +3052,13 @@ static THINK(FinishSpawningItem) (gentity_t *ent) -> void { ent->svflags |= SVF_NOCLIENT; ent->solid = SOLID_NOT; - + if (ent == ent->teammaster) { ent->nextthink = level.time + 10_hz; //if (!ent->think) - ent->think = RespawnItem; - } else + ent->think = RespawnItem; + } + else ent->nextthink = 0_sec; } @@ -3021,11 +3102,11 @@ This will be called for each item spawned in a level, and for each item in each client's inventory. =============== */ -void PrecacheItem(gitem_t *it) { - const char *s, *start; +void PrecacheItem(gitem_t* it) { + const char* s, * start; char data[MAX_QPATH]; ptrdiff_t len; - gitem_t *ammo; + gitem_t* ammo; if (!it) return; @@ -3038,7 +3119,7 @@ void PrecacheItem(gitem_t *it) { gi.modelindex(it->view_model); if (it->icon) gi.imageindex(it->icon); - + // parse everything for its ammo if (it->ammo) { ammo = GetItemByIndex(it->ammo); @@ -3081,8 +3162,8 @@ void PrecacheItem(gitem_t *it) { CheckItemEnabled ============ */ -bool CheckItemEnabled(gitem_t *item) { - cvar_t *cv; +bool CheckItemEnabled(gitem_t* item) { + cvar_t* cv; if (!deathmatch->integer) { if (item->pickup == Pickup_Doppelganger || item->pickup == Pickup_Nuke) @@ -3115,19 +3196,24 @@ bool CheckItemEnabled(gitem_t *item) { if (game.item_inhibit_pu && item->flags & (IF_POWERUP | IF_SPHERE)) { add = game.item_inhibit_pu > 0 ? true : false; subtract = game.item_inhibit_pu < 0 ? true : false; - } else if (game.item_inhibit_pa && item->flags & IF_POWER_ARMOR) { + } + else if (game.item_inhibit_pa && item->flags & IF_POWER_ARMOR) { add = game.item_inhibit_pa > 0 ? true : false; subtract = game.item_inhibit_pa < 0 ? true : false; - } else if (game.item_inhibit_ht && item->flags & IF_HEALTH) { + } + else if (game.item_inhibit_ht && item->flags & IF_HEALTH) { add = game.item_inhibit_ht > 0 ? true : false; subtract = game.item_inhibit_ht < 0 ? true : false; - } else if (game.item_inhibit_ar && item->flags & IF_ARMOR) { + } + else if (game.item_inhibit_ar && item->flags & IF_ARMOR) { add = game.item_inhibit_ar > 0 ? true : false; subtract = game.item_inhibit_ar < 0 ? true : false; - } else if (game.item_inhibit_am && item->flags & IF_AMMO) { + } + else if (game.item_inhibit_am && item->flags & IF_AMMO) { add = game.item_inhibit_am > 0 ? true : false; subtract = game.item_inhibit_am < 0 ? true : false; - } else if (game.item_inhibit_wp && item->flags & IF_WEAPON) { + } + else if (game.item_inhibit_wp && item->flags & IF_WEAPON) { add = game.item_inhibit_wp > 0 ? true : false; subtract = game.item_inhibit_wp < 0 ? true : false; } @@ -3180,18 +3266,18 @@ bool CheckItemEnabled(gitem_t *item) { CheckItemReplacements ============ */ -gitem_t *CheckItemReplacements(gitem_t *item) { - cvar_t *cv; +gitem_t* CheckItemReplacements(gitem_t* item) { + cvar_t* cv; cv = gi.cvar(G_Fmt("{}_replace_{}", level.mapname, item->classname).data(), "", CVAR_NOFLAGS); if (*cv->string) { - gitem_t *out = FindItemByClassname(cv->string); + gitem_t* out = FindItemByClassname(cv->string); return out ? out : item; } cv = gi.cvar(G_Fmt("replace_{}", item->classname).data(), "", CVAR_NOFLAGS); if (*cv->string) { - gitem_t *out = FindItemByClassname(cv->string); + gitem_t* out = FindItemByClassname(cv->string); return out ? out : item; } @@ -3215,7 +3301,7 @@ Item_TriggeredSpawn Create the item marked for spawn creation ============ */ -static USE(Item_TriggeredSpawn) (gentity_t *self, gentity_t *other, gentity_t *activator) -> void { +static USE(Item_TriggeredSpawn) (gentity_t* self, gentity_t* other, gentity_t* activator) -> void { self->svflags &= ~SVF_NOCLIENT; self->use = nullptr; @@ -3243,7 +3329,7 @@ SetTriggeredSpawn Sets up an item to spawn in later. ============ */ -static void SetTriggeredSpawn(gentity_t *ent) { +static void SetTriggeredSpawn(gentity_t* ent) { // don't do anything on key_power_cubes. if (ent->item->id == IT_KEY_POWER_CUBE || ent->item->id == IT_KEY_EXPLOSIVE_CHARGES) return; @@ -3265,7 +3351,7 @@ Items can't be immediately dropped to floor, because they might be on an entity that hasn't spawned yet. ============ */ -bool SpawnItem(gentity_t *ent, gitem_t *item) { +bool SpawnItem(gentity_t* ent, gitem_t* item) { // check for item replacements or disablements item = CheckItemReplacements(item); if (!CheckItemEnabled(item)) { @@ -3288,7 +3374,8 @@ bool SpawnItem(gentity_t *ent, gitem_t *item) { ent->s.effects &= ~(EF_ROTATE | EF_BOB); ent->s.renderfx &= ~RF_GLOW; } - } else if (ent->spawnflags.value >= SPAWNFLAG_ITEM_MAX.value) { + } + else if (ent->spawnflags.value >= SPAWNFLAG_ITEM_MAX.value) { ent->spawnflags = SPAWNFLAG_NONE; gi.Com_PrintFmt("{} has invalid spawnflags set\n", *ent); } @@ -3320,7 +3407,7 @@ bool SpawnItem(gentity_t *ent, gitem_t *item) { if (ent->spawnflags.has(SPAWNFLAG_ITEM_SUSPENDED)) ent->s.effects |= (EF_ROTATE | EF_BOB); - + if (ent->spawnflags.has(SPAWNFLAG_ITEM_TRIGGER_SPAWN)) SetTriggeredSpawn(ent); @@ -3337,7 +3424,7 @@ bool SpawnItem(gentity_t *ent, gitem_t *item) { ent->s.effects |= EF_COLOR_SHELL; } } - + if (!g_item_bobbing->integer && !ent->spawnflags.has(SPAWNFLAG_ITEM_SUSPENDED)) ent->s.effects &= ~EF_BOB; @@ -3356,7 +3443,7 @@ bool SpawnItem(gentity_t *ent, gitem_t *item) { return true; } -void P_ToggleFlashlight(gentity_t *ent, bool state) { +void P_ToggleFlashlight(gentity_t* ent, bool state) { if (!!(ent->flags & FL_FLASHLIGHT) == state) return; @@ -3365,14 +3452,14 @@ void P_ToggleFlashlight(gentity_t *ent, bool state) { gi.sound(ent, CHAN_AUTO, gi.soundindex(ent->flags & FL_FLASHLIGHT ? "items/flashlight_on.wav" : "items/flashlight_off.wav"), 1.f, ATTN_STATIC, 0); } -static void Use_Flashlight(gentity_t *ent, gitem_t *inv) { +static void Use_Flashlight(gentity_t* ent, gitem_t* inv) { P_ToggleFlashlight(ent, !(ent->flags & FL_FLASHLIGHT)); } constexpr size_t MAX_TEMP_POI_POINTS = 128; -void Compass_Update(gentity_t *ent, bool first) { - vec3_t *&points = level.poi_points[ent->s.number - 1]; +void Compass_Update(gentity_t* ent, bool first) { + vec3_t*& points = level.poi_points[ent->s.number - 1]; // deleted for some reason if (!points) @@ -3415,7 +3502,7 @@ void Compass_Update(gentity_t *ent, bool first) { ent->client->help_draw_time = level.time + 200_ms; } -static void Use_Compass(gentity_t *ent, gitem_t *inv) { +static void Use_Compass(gentity_t* ent, gitem_t* inv) { if (deathmatch->integer) { Cmd_ReadyUp_f(ent); return; @@ -3431,10 +3518,10 @@ static void Use_Compass(gentity_t *ent, gitem_t *inv) { ent->client->help_poi_location = level.current_poi; ent->client->help_poi_image = level.current_poi_image; - vec3_t *&points = level.poi_points[ent->s.number - 1]; + vec3_t*& points = level.poi_points[ent->s.number - 1]; if (!points) - points = (vec3_t *)gi.TagMalloc(sizeof(vec3_t) * (MAX_TEMP_POI_POINTS + 1), TAG_LEVEL); + points = (vec3_t*)gi.TagMalloc(sizeof(vec3_t) * (MAX_TEMP_POI_POINTS + 1), TAG_LEVEL); PathRequest request; request.start = ent->s.origin; @@ -3485,17 +3572,18 @@ static void Use_Compass(gentity_t *ent, gitem_t *inv) { ent->client->help_draw_time = 0_ms; Compass_Update(ent, true); - } else { + } + else { P_SendLevelPOI(ent); gi.local_sound(ent, CHAN_AUTO, gi.soundindex("misc/help_marker.wav"), 1.f, ATTN_NORM, 0, GetUnicastKey()); } } -static void Use_Ball(gentity_t *ent, gitem_t *item) { +static void Use_Ball(gentity_t* ent, gitem_t* item) { } -static void Drop_Ball(gentity_t *ent, gitem_t *item) { +static void Drop_Ball(gentity_t* ent, gitem_t* item) { } @@ -3537,132 +3625,132 @@ model="models/items/armor/body/tris.md2" /* armor_info */ &bodyarmor_info }, -/*QUAKED item_armor_combat (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -*/ - { - /* id */ IT_ARMOR_COMBAT, - /* classname */ "item_armor_combat", - /* pickup */ Pickup_Armor, - /* use */ nullptr, - /* drop */ nullptr, - /* weaponthink */ nullptr, - /* pickup_sound */ "misc/ar1_pkup.wav", - /* world_model */ "models/items/armor/combat/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "i_combatarmor", - /* use_name */ "Combat Armor", - /* pickup_name */ "$item_combat_armor", - /* pickup_name_definite */ "$item_combat_armor_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_ARMOR, - /* vwep_model */ nullptr, - /* armor_info */ &combatarmor_info - }, - -/*QUAKED item_armor_jacket (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -*/ - { - /* id */ IT_ARMOR_JACKET, - /* classname */ "item_armor_jacket", - /* pickup */ Pickup_Armor, - /* use */ nullptr, - /* drop */ nullptr, - /* weaponthink */ nullptr, - /* pickup_sound */ "misc/ar1_pkup.wav", - /* world_model */ "models/items/armor/jacket/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "i_jacketarmor", - /* use_name */ "Jacket Armor", - /* pickup_name */ "$item_jacket_armor", - /* pickup_name_definite */ "$item_jacket_armor_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_ARMOR, - /* vwep_model */ nullptr, - /* armor_info */ &jacketarmor_info - }, - -/*QUAKED item_armor_shard (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -*/ - { - /* id */ IT_ARMOR_SHARD, - /* classname */ "item_armor_shard", - /* pickup */ Pickup_Armor, - /* use */ nullptr, - /* drop */ nullptr, - /* weaponthink */ nullptr, - /* pickup_sound */ "misc/ar2_pkup.wav", - /* world_model */ "models/items/armor/shard/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "i_armor_shard", - /* use_name */ "Armor Shard", - /* pickup_name */ "$item_armor_shard", - /* pickup_name_definite */ "$item_armor_shard_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_ARMOR - }, - -/*QUAKED item_power_screen (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -*/ - { - /* id */ IT_POWER_SCREEN, - /* classname */ "item_power_screen", - /* pickup */ Pickup_PowerArmor, - /* use */ Use_PowerArmor, - /* drop */ Drop_PowerArmor, - /* weaponthink */ nullptr, - /* pickup_sound */ "misc/ar3_pkup.wav", - /* world_model */ "models/items/armor/screen/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "i_powerscreen", - /* use_name */ "Power Screen", - /* pickup_name */ "$item_power_screen", - /* pickup_name_definite */ "$item_power_screen_def", - /* quantity */ 60, - /* ammo */ IT_AMMO_CELLS, - /* chain */ IT_NULL, - /* flags */ IF_ARMOR | IF_POWERUP_WHEEL | IF_POWERUP_ONOFF, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_SCREEN, - /* precaches */ "misc/power2.wav misc/power1.wav" - }, - -/*QUAKED item_power_shield (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -*/ - { - /* id */ IT_POWER_SHIELD, - /* classname */ "item_power_shield", - /* pickup */ Pickup_PowerArmor, - /* use */ Use_PowerArmor, - /* drop */ Drop_PowerArmor, - /* weaponthink */ nullptr, - /* pickup_sound */ "misc/ar3_pkup.wav", - /* world_model */ "models/items/armor/shield/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "i_powershield", - /* use_name */ "Power Shield", - /* pickup_name */ "$item_power_shield", - /* pickup_name_definite */ "$item_power_shield_def", - /* quantity */ 60, - /* ammo */ IT_AMMO_CELLS, - /* chain */ IT_NULL, - /* flags */ IF_ARMOR | IF_POWERUP_WHEEL | IF_POWERUP_ONOFF, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_SHIELD, - /* precaches */ "misc/power2.wav misc/power1.wav" - }, + /*QUAKED item_armor_combat (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + */ + { + /* id */ IT_ARMOR_COMBAT, + /* classname */ "item_armor_combat", + /* pickup */ Pickup_Armor, + /* use */ nullptr, + /* drop */ nullptr, + /* weaponthink */ nullptr, + /* pickup_sound */ "misc/ar1_pkup.wav", + /* world_model */ "models/items/armor/combat/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "i_combatarmor", + /* use_name */ "Combat Armor", + /* pickup_name */ "$item_combat_armor", + /* pickup_name_definite */ "$item_combat_armor_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_ARMOR, + /* vwep_model */ nullptr, + /* armor_info */ &combatarmor_info + }, + + /*QUAKED item_armor_jacket (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + */ + { + /* id */ IT_ARMOR_JACKET, + /* classname */ "item_armor_jacket", + /* pickup */ Pickup_Armor, + /* use */ nullptr, + /* drop */ nullptr, + /* weaponthink */ nullptr, + /* pickup_sound */ "misc/ar1_pkup.wav", + /* world_model */ "models/items/armor/jacket/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "i_jacketarmor", + /* use_name */ "Jacket Armor", + /* pickup_name */ "$item_jacket_armor", + /* pickup_name_definite */ "$item_jacket_armor_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_ARMOR, + /* vwep_model */ nullptr, + /* armor_info */ &jacketarmor_info + }, + + /*QUAKED item_armor_shard (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + */ + { + /* id */ IT_ARMOR_SHARD, + /* classname */ "item_armor_shard", + /* pickup */ Pickup_Armor, + /* use */ nullptr, + /* drop */ nullptr, + /* weaponthink */ nullptr, + /* pickup_sound */ "misc/ar2_pkup.wav", + /* world_model */ "models/items/armor/shard/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "i_armor_shard", + /* use_name */ "Armor Shard", + /* pickup_name */ "$item_armor_shard", + /* pickup_name_definite */ "$item_armor_shard_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_ARMOR + }, + + /*QUAKED item_power_screen (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + */ + { + /* id */ IT_POWER_SCREEN, + /* classname */ "item_power_screen", + /* pickup */ Pickup_PowerArmor, + /* use */ Use_PowerArmor, + /* drop */ Drop_PowerArmor, + /* weaponthink */ nullptr, + /* pickup_sound */ "misc/ar3_pkup.wav", + /* world_model */ "models/items/armor/screen/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "i_powerscreen", + /* use_name */ "Power Screen", + /* pickup_name */ "$item_power_screen", + /* pickup_name_definite */ "$item_power_screen_def", + /* quantity */ 60, + /* ammo */ IT_AMMO_CELLS, + /* chain */ IT_NULL, + /* flags */ IF_ARMOR | IF_POWERUP_WHEEL | IF_POWERUP_ONOFF, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_SCREEN, + /* precaches */ "misc/power2.wav misc/power1.wav" + }, + + /*QUAKED item_power_shield (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + */ + { + /* id */ IT_POWER_SHIELD, + /* classname */ "item_power_shield", + /* pickup */ Pickup_PowerArmor, + /* use */ Use_PowerArmor, + /* drop */ Drop_PowerArmor, + /* weaponthink */ nullptr, + /* pickup_sound */ "misc/ar3_pkup.wav", + /* world_model */ "models/items/armor/shield/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "i_powershield", + /* use_name */ "Power Shield", + /* pickup_name */ "$item_power_shield", + /* pickup_name_definite */ "$item_power_shield_def", + /* quantity */ 60, + /* ammo */ IT_AMMO_CELLS, + /* chain */ IT_NULL, + /* flags */ IF_ARMOR | IF_POWERUP_WHEEL | IF_POWERUP_ONOFF, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_SHIELD, + /* precaches */ "misc/power2.wav misc/power1.wav" + }, // // WEAPONS @@ -3695,2463 +3783,2466 @@ model="models/items/armor/body/tris.md2" /* precaches */ "weapons/grapple/grfire.wav weapons/grapple/grpull.wav weapons/grapple/grhang.wav weapons/grapple/grreset.wav weapons/grapple/grhit.wav weapons/grapple/grfly.wav" }, -/* weapon_blaster (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -*/ - { - /* id */ IT_WEAPON_BLASTER, - /* classname */ "weapon_blaster", - /* pickup */ Pickup_Weapon, - /* use */ Use_Weapon, - /* drop */ Drop_Weapon, - /* weaponthink */ Weapon_Blaster, - /* pickup_sound */ "misc/w_pkup.wav", - /* world_model */ "models/weapons/g_blast/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ "models/weapons/v_blast/tris.md2", - /* icon */ "w_blaster", - /* use_name */ "Blaster", - /* pickup_name */ "$item_blaster", - /* pickup_name_definite */ "$item_blaster_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_WEAPON_BLASTER, - /* flags */ IF_WEAPON | IF_STAY_COOP | IF_NOT_RANDOM, - /* vwep_model */ "#w_blaster.md2", - /* armor_info */ nullptr, - /* tag */ 0, - /* precaches */ "weapons/blastf1a.wav misc/lasfly.wav" - }, + /* weapon_blaster (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + */ + { + /* id */ IT_WEAPON_BLASTER, + /* classname */ "weapon_blaster", + /* pickup */ Pickup_Weapon, + /* use */ Use_Weapon, + /* drop */ Drop_Weapon, + /* weaponthink */ Weapon_Blaster, + /* pickup_sound */ "misc/w_pkup.wav", + /* world_model */ "models/weapons/g_blast/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ "models/weapons/v_blast/tris.md2", + /* icon */ "w_blaster", + /* use_name */ "Blaster", + /* pickup_name */ "$item_blaster", + /* pickup_name_definite */ "$item_blaster_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_WEAPON_BLASTER, + /* flags */ IF_WEAPON | IF_STAY_COOP | IF_NOT_RANDOM, + /* vwep_model */ "#w_blaster.md2", + /* armor_info */ nullptr, + /* tag */ 0, + /* precaches */ "weapons/blastf1a.wav misc/lasfly.wav" + }, + + /*QUAKED weapon_chainfist (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/weapons/g_chainf/tris.md2" + */ + { + /* id */ IT_WEAPON_CHAINFIST, + /* classname */ "weapon_chainfist", + /* pickup */ Pickup_Weapon, + /* use */ Use_Weapon, + /* drop */ Drop_Weapon, + /* weaponthink */ Weapon_ChainFist, + /* pickup_sound */ "misc/w_pkup.wav", + /* world_model */ "models/weapons/g_chainf/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ "models/weapons/v_chainf/tris.md2", + /* icon */ "w_chainfist", + /* use_name */ "Chainfist", + /* pickup_name */ "$item_chainfist", + /* pickup_name_definite */ "$item_chainfist_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_WEAPON_BLASTER, + /* flags */ IF_WEAPON | IF_STAY_COOP | IF_NO_HASTE, + /* vwep_model */ "#w_chainfist.md2", + /* armor_info */ nullptr, + /* tag */ 0, + /* precaches */ "weapons/sawidle.wav weapons/sawhit.wav weapons/sawslice.wav", + }, + + /*QUAKED weapon_shotgun (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/weapons/g_shotg/tris.md2" + */ + { + /* id */ IT_WEAPON_SHOTGUN, + /* classname */ "weapon_shotgun", + /* pickup */ Pickup_Weapon, + /* use */ Use_Weapon, + /* drop */ Drop_Weapon, + /* weaponthink */ Weapon_Shotgun, + /* pickup_sound */ "misc/w_pkup.wav", + /* world_model */ "models/weapons/g_shotg/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ "models/weapons/v_shotg/tris.md2", + /* icon */ "w_shotgun", + /* use_name */ "Shotgun", + /* pickup_name */ "$item_shotgun", + /* pickup_name_definite */ "$item_shotgun_def", + /* quantity */ 1, + /* ammo */ IT_AMMO_SHELLS, + /* chain */ IT_NULL, + /* flags */ IF_WEAPON | IF_STAY_COOP, + /* vwep_model */ "#w_shotgun.md2", + /* armor_info */ nullptr, + /* tag */ AMMO_SHELLS, + /* precaches */ "weapons/shotgf1b.wav weapons/shotgr1b.wav" + }, + + /*QUAKED weapon_supershotgun (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/weapons/g_shotg2/tris.md2" + */ + { + /* id */ IT_WEAPON_SSHOTGUN, + /* classname */ "weapon_supershotgun", + /* pickup */ Pickup_Weapon, + /* use */ Use_Weapon, + /* drop */ Drop_Weapon, + /* weaponthink */ Weapon_SuperShotgun, + /* pickup_sound */ "misc/w_pkup.wav", + /* world_model */ "models/weapons/g_shotg2/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ "models/weapons/v_shotg2/tris.md2", + /* icon */ "w_sshotgun", + /* use_name */ "Super Shotgun", + /* pickup_name */ "$item_super_shotgun", + /* pickup_name_definite */ "$item_super_shotgun_def", + /* quantity */ 2, + /* ammo */ IT_AMMO_SHELLS, + /* chain */ IT_NULL, + /* flags */ IF_WEAPON | IF_STAY_COOP, + /* vwep_model */ "#w_sshotgun.md2", + /* armor_info */ nullptr, + /* tag */ AMMO_SHELLS, + /* precaches */ "weapons/sshotf1b.wav", + /* sort_id */ 0, + /* quantity_warn */ 10 + }, + + /*QUAKED weapon_machinegun (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/weapons/g_machn/tris.md2" + */ + { + /* id */ IT_WEAPON_MACHINEGUN, + /* classname */ "weapon_machinegun", + /* pickup */ Pickup_Weapon, + /* use */ Use_Weapon, + /* drop */ Drop_Weapon, + /* weaponthink */ Weapon_Machinegun, + /* pickup_sound */ "misc/w_pkup.wav", + /* world_model */ "models/weapons/g_machn/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ "models/weapons/v_machn/tris.md2", + /* icon */ "w_machinegun", + /* use_name */ "Machinegun", + /* pickup_name */ "$item_machinegun", + /* pickup_name_definite */ "$item_machinegun_def", + /* quantity */ 1, + /* ammo */ IT_AMMO_BULLETS, + /* chain */ IT_WEAPON_MACHINEGUN, + /* flags */ IF_WEAPON | IF_STAY_COOP, + /* vwep_model */ "#w_machinegun.md2", + /* armor_info */ nullptr, + /* tag */ AMMO_BULLETS, + /* precaches */ "weapons/machgf1b.wav weapons/machgf2b.wav weapons/machgf3b.wav weapons/machgf4b.wav weapons/machgf5b.wav", + /* sort_id */ 0, + /* quantity_warn */ 30 + }, + + /*QUAKED weapon_etf_rifle (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/weapons/g_etf_rifle/tris.md2" + */ + { + /* id */ IT_WEAPON_ETF_RIFLE, + /* classname */ "weapon_etf_rifle", + /* pickup */ Pickup_Weapon, + /* use */ Use_Weapon, + /* drop */ Drop_Weapon, + /* weaponthink */ Weapon_ETF_Rifle, + /* pickup_sound */ "misc/w_pkup.wav", + /* world_model */ "models/weapons/g_etf_rifle/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ "models/weapons/v_etf_rifle/tris.md2", + /* icon */ "w_etf_rifle", + /* use_name */ "ETF Rifle", + /* pickup_name */ "$item_etf_rifle", + /* pickup_name_definite */ "$item_etf_rifle_def", + /* quantity */ 1, + /* ammo */ IT_AMMO_FLECHETTES, + /* chain */ IT_WEAPON_MACHINEGUN, + /* flags */ IF_WEAPON | IF_STAY_COOP, + /* vwep_model */ "#w_etfrifle.md2", + /* armor_info */ nullptr, + /* tag */ AMMO_FLECHETTES, + /* precaches */ "weapons/nail1.wav models/proj/flechette/tris.md2", + /* sort_id */ 0, + /* quantity_warn */ 30 + }, + + /*QUAKED weapon_chaingun (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/weapons/g_chain/tris.md2" + */ + { + /* id */ IT_WEAPON_CHAINGUN, + /* classname */ "weapon_chaingun", + /* pickup */ Pickup_Weapon, + /* use */ Use_Weapon, + /* drop */ Drop_Weapon, + /* weaponthink */ Weapon_Chaingun, + /* pickup_sound */ "misc/w_pkup.wav", + /* world_model */ "models/weapons/g_chain/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ "models/weapons/v_chain/tris.md2", + /* icon */ "w_chaingun", + /* use_name */ "Chaingun", + /* pickup_name */ "$item_chaingun", + /* pickup_name_definite */ "$item_chaingun_def", + /* quantity */ 1, + /* ammo */ IT_AMMO_BULLETS, + /* chain */ IT_NULL, + /* flags */ IF_WEAPON | IF_STAY_COOP, + /* vwep_model */ "#w_chaingun.md2", + /* armor_info */ nullptr, + /* tag */ AMMO_BULLETS, + /* precaches */ "weapons/chngnu1a.wav weapons/chngnl1a.wav weapons/machgf3b.wav weapons/chngnd1a.wav", + /* sort_id */ 0, + /* quantity_warn */ 60 + }, + + /*QUAKED ammo_grenades (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + */ + { + /* id */ IT_AMMO_GRENADES, + /* classname */ "ammo_grenades", + /* pickup */ Pickup_Ammo, + /* use */ Use_Weapon, + /* drop */ Drop_Ammo, + /* weaponthink */ Weapon_HandGrenade, + /* pickup_sound */ "misc/am_pkup.wav", + /* world_model */ "models/items/ammo/grenades/medium/tris.md2", + /* world_model_flags */ EF_NONE, + /* view_model */ "models/weapons/v_handgr/tris.md2", + /* icon */ "a_grenades", + /* use_name */ "Grenades", + /* pickup_name */ "$item_grenades", + /* pickup_name_definite */ "$item_grenades_def", + /* quantity */ 5, + /* ammo */ IT_AMMO_GRENADES, + /* chain */ IT_AMMO_GRENADES, + /* flags */ IF_AMMO | IF_WEAPON, + /* vwep_model */ "#a_grenades.md2", + /* armor_info */ nullptr, + /* tag */ AMMO_GRENADES, + /* precaches */ "weapons/hgrent1a.wav weapons/hgrena1b.wav weapons/hgrenc1b.wav weapons/hgrenb1a.wav weapons/hgrenb2a.wav models/objects/grenade3/tris.md2", + /* sort_id */ 0, + /* quantity_warn */ 2 + }, + + /*QUAKED ammo_trap (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/weapons/g_trap/tris.md2" + */ + { + /* id */ IT_AMMO_TRAP, + /* classname */ "ammo_trap", + /* pickup */ Pickup_Ammo, + /* use */ Use_Weapon, + /* drop */ Drop_Ammo, + /* weaponthink */ Weapon_Trap, + /* pickup_sound */ "misc/am_pkup.wav", + /* world_model */ "models/weapons/g_trap/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ "models/weapons/v_trap/tris.md2", + /* icon */ "a_trap", + /* use_name */ "Trap", + /* pickup_name */ "$item_trap", + /* pickup_name_definite */ "$item_trap_def", + /* quantity */ 1, + /* ammo */ IT_AMMO_TRAP, + /* chain */ IT_AMMO_GRENADES, + /* flags */ IF_AMMO | IF_WEAPON | IF_NO_INFINITE_AMMO, + /* vwep_model */ "#a_trap.md2", + /* armor_info */ nullptr, + /* tag */ AMMO_TRAP, + /* precaches */ "misc/fhit3.wav weapons/trapcock.wav weapons/traploop.wav weapons/trapsuck.wav weapons/trapdown.wav items/s_health.wav items/n_health.wav items/l_health.wav items/m_health.wav models/weapons/z_trap/tris.md2", + /* sort_id */ 0, + /* quantity_warn */ 1 + }, + + /*QUAKED ammo_tesla (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/ammo/am_tesl/tris.md2" + */ + { + /* id */ IT_AMMO_TESLA, + /* classname */ "ammo_tesla", + /* pickup */ Pickup_Ammo, + /* use */ Use_Weapon, + /* drop */ Drop_Ammo, + /* weaponthink */ Weapon_Tesla, + /* pickup_sound */ "misc/am_pkup.wav", + /* world_model */ "models/ammo/am_tesl/tris.md2", + /* world_model_flags */ EF_NONE, + /* view_model */ "models/weapons/v_tesla/tris.md2", + /* icon */ "a_tesla", + /* use_name */ "Tesla", + /* pickup_name */ "$item_tesla", + /* pickup_name_definite */ "$item_tesla_def", + /* quantity */ 3, + /* ammo */ IT_AMMO_TESLA, + /* chain */ IT_AMMO_GRENADES, + /* flags */ IF_AMMO | IF_WEAPON | IF_NO_INFINITE_AMMO, + /* vwep_model */ "#a_tesla.md2", + /* armor_info */ nullptr, + /* tag */ AMMO_TESLA, + /* precaches */ "weapons/teslaopen.wav weapons/hgrenb1a.wav weapons/hgrenb2a.wav models/weapons/g_tesla/tris.md2", + /* sort_id */ 0, + /* quantity_warn */ 1 + }, + + /*QUAKED weapon_grenadelauncher (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/weapons/g_launch/tris.md2" + */ + { + /* id */ IT_WEAPON_GLAUNCHER, + /* classname */ "weapon_grenadelauncher", + /* pickup */ Pickup_Weapon, + /* use */ Use_Weapon, + /* drop */ Drop_Weapon, + /* weaponthink */ Weapon_GrenadeLauncher, + /* pickup_sound */ "misc/w_pkup.wav", + /* world_model */ "models/weapons/g_launch/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ "models/weapons/v_launch/tris.md2", + /* icon */ "w_glauncher", + /* use_name */ "Grenade Launcher", + /* pickup_name */ "$item_grenade_launcher", + /* pickup_name_definite */ "$item_grenade_launcher_def", + /* quantity */ 1, + /* ammo */ IT_AMMO_GRENADES, + /* chain */ IT_WEAPON_GLAUNCHER, + /* flags */ IF_WEAPON | IF_STAY_COOP, + /* vwep_model */ "#w_glauncher.md2", + /* armor_info */ nullptr, + /* tag */ AMMO_GRENADES, + /* precaches */ "models/objects/grenade4/tris.md2 weapons/grenlf1a.wav weapons/grenlr1b.wav weapons/grenlb1b.wav" + }, + + /*QUAKED weapon_proxlauncher (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/weapons/g_plaunch/tris.md2" + */ + { + /* id */ IT_WEAPON_PROXLAUNCHER, + /* classname */ "weapon_proxlauncher", + /* pickup */ Pickup_Weapon, + /* use */ Use_Weapon, + /* drop */ Drop_Weapon, + /* weaponthink */ Weapon_ProxLauncher, + /* pickup_sound */ "misc/w_pkup.wav", + /* world_model */ "models/weapons/g_plaunch/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ "models/weapons/v_plaunch/tris.md2", + /* icon */ "w_proxlaunch", + /* use_name */ "Prox Launcher", + /* pickup_name */ "$item_prox_launcher", + /* pickup_name_definite */ "$item_prox_launcher_def", + /* quantity */ 1, + /* ammo */ IT_AMMO_PROX, + /* chain */ IT_WEAPON_GLAUNCHER, + /* flags */ IF_WEAPON | IF_STAY_COOP, + /* vwep_model */ "#w_plauncher.md2", + /* armor_info */ nullptr, + /* tag */ AMMO_PROX, + /* precaches */ "weapons/grenlf1a.wav weapons/grenlr1b.wav weapons/grenlb1b.wav weapons/proxwarn.wav weapons/proxopen.wav", + }, + + /*QUAKED weapon_rocketlauncher (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/weapons/g_rocket/tris.md2" + */ + { + /* id */ IT_WEAPON_RLAUNCHER, + /* classname */ "weapon_rocketlauncher", + /* pickup */ Pickup_Weapon, + /* use */ Use_Weapon, + /* drop */ Drop_Weapon, + /* weaponthink */ Weapon_RocketLauncher, + /* pickup_sound */ "misc/w_pkup.wav", + /* world_model */ "models/weapons/g_rocket/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ "models/weapons/v_rocket/tris.md2", + /* icon */ "w_rlauncher", + /* use_name */ "Rocket Launcher", + /* pickup_name */ "$item_rocket_launcher", + /* pickup_name_definite */ "$item_rocket_launcher_def", + /* quantity */ 1, + /* ammo */ IT_AMMO_ROCKETS, + /* chain */ IT_NULL, + /* flags */ IF_WEAPON | IF_STAY_COOP, + /* vwep_model */ "#w_rlauncher.md2", + /* armor_info */ nullptr, + /* tag */ AMMO_ROCKETS, + /* precaches */ "models/objects/rocket/tris.md2 weapons/rockfly.wav weapons/rocklf1a.wav weapons/rocklr1b.wav models/objects/debris2/tris.md2" + }, + + /*QUAKED weapon_hyperblaster (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/weapons/g_hyperb/tris.md2" + */ + { + /* id */ IT_WEAPON_HYPERBLASTER, + /* classname */ "weapon_hyperblaster", + /* pickup */ Pickup_Weapon, + /* use */ Use_Weapon, + /* drop */ Drop_Weapon, + /* weaponthink */ Weapon_HyperBlaster, + /* pickup_sound */ "misc/w_pkup.wav", + /* world_model */ "models/weapons/g_hyperb/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ "models/weapons/v_hyperb/tris.md2", + /* icon */ "w_hyperblaster", + /* use_name */ "HyperBlaster", + /* pickup_name */ "$item_hyperblaster", + /* pickup_name_definite */ "$item_hyperblaster_def", + /* quantity */ 1, + /* ammo */ IT_AMMO_CELLS, + /* chain */ IT_WEAPON_HYPERBLASTER, + /* flags */ IF_WEAPON | IF_STAY_COOP, + /* vwep_model */ "#w_hyperblaster.md2", + /* armor_info */ nullptr, + /* tag */ AMMO_CELLS, + /* precaches */ "weapons/hyprbu1a.wav weapons/hyprbl1a.wav weapons/hyprbf1a.wav weapons/hyprbd1a.wav misc/lasfly.wav", + /* sort_id */ 0, + /* quantity_warn */ 30 + }, + + /*QUAKED weapon_boomer (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/weapons/g_boom/tris.md2" + */ + { + /* id */ IT_WEAPON_IONRIPPER, + /* classname */ "weapon_boomer", + /* pickup */ Pickup_Weapon, + /* use */ Use_Weapon, + /* drop */ Drop_Weapon, + /* weaponthink */ Weapon_IonRipper, + /* pickup_sound */ "misc/w_pkup.wav", + /* world_model */ "models/weapons/g_boom/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ "models/weapons/v_boomer/tris.md2", + /* icon */ "w_ripper", + /* use_name */ "Ionripper", + /* pickup_name */ "$item_ionripper", + /* pickup_name_definite */ "$item_ionripper_def", + /* quantity */ 2, + /* ammo */ IT_AMMO_CELLS, + /* chain */ IT_WEAPON_HYPERBLASTER, + /* flags */ IF_WEAPON | IF_STAY_COOP, + /* vwep_model */ "#w_ripper.md2", + /* armor_info */ nullptr, + /* tag */ AMMO_CELLS, + /* precaches */ "weapons/rippfire.wav models/objects/boomrang/tris.md2 misc/lasfly.wav", + /* sort_id */ 0, + /* quantity_warn */ 30 + }, + + /*QUAKED weapon_plasmabeam (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/weapons/g_beamer/tris.md2" + */ + { + /* id */ IT_WEAPON_PLASMABEAM, + /* classname */ "weapon_plasmabeam", + /* pickup */ Pickup_Weapon, + /* use */ Use_Weapon, + /* drop */ Drop_Weapon, + /* weaponthink */ Weapon_PlasmaBeam, + /* pickup_sound */ "misc/w_pkup.wav", + /* world_model */ "models/weapons/g_beamer/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ "models/weapons/v_beamer/tris.md2", + /* icon */ "w_heatbeam", + /* use_name */ "Plasma Beam", + /* pickup_name */ "$item_plasma_beam", + /* pickup_name_definite */ "$item_plasma_beam_def", + /* quantity */ 2, + /* ammo */ IT_AMMO_CELLS, + /* chain */ IT_WEAPON_HYPERBLASTER, + /* flags */ IF_WEAPON | IF_STAY_COOP, + /* vwep_model */ "#w_plasma.md2", + /* armor_info */ nullptr, + /* tag */ AMMO_CELLS, + /* precaches */ "weapons/bfg__l1a.wav weapons/bfg_hum.wav", + /* sort_id */ 0, + /* quantity_warn */ 50 + }, + + /*QUAKED weapon_railgun (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/weapons/g_rail/tris.md2" + */ + { + /* id */ IT_WEAPON_RAILGUN, + /* classname */ "weapon_railgun", + /* pickup */ Pickup_Weapon, + /* use */ Use_Weapon, + /* drop */ Drop_Weapon, + /* weaponthink */ Weapon_Railgun, + /* pickup_sound */ "misc/w_pkup.wav", + /* world_model */ "models/weapons/g_rail/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ "models/weapons/v_rail/tris.md2", + /* icon */ "w_railgun", + /* use_name */ "Railgun", + /* pickup_name */ "$item_railgun", + /* pickup_name_definite */ "$item_railgun_def", + /* quantity */ 1, + /* ammo */ IT_AMMO_SLUGS, + /* chain */ IT_WEAPON_RAILGUN, + /* flags */ IF_WEAPON | IF_STAY_COOP, + /* vwep_model */ "#w_railgun.md2", + /* armor_info */ nullptr, + /* tag */ AMMO_SLUGS, + /* precaches */ "weapons/rg_hum.wav" + }, + + /*QUAKED weapon_phalanx (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/weapons/g_shotx/tris.md2" + */ + { + /* id */ IT_WEAPON_PHALANX, + /* classname */ "weapon_phalanx", + /* pickup */ Pickup_Weapon, + /* use */ Use_Weapon, + /* drop */ Drop_Weapon, + /* weaponthink */ Weapon_Phalanx, + /* pickup_sound */ "misc/w_pkup.wav", + /* world_model */ "models/weapons/g_shotx/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ "models/weapons/v_shotx/tris.md2", + /* icon */ "w_phallanx", + /* use_name */ "Phalanx", + /* pickup_name */ "$item_phalanx", + /* pickup_name_definite */ "$item_phalanx_def", + /* quantity */ 1, + /* ammo */ IT_AMMO_MAGSLUG, + /* chain */ IT_WEAPON_RAILGUN, + /* flags */ IF_WEAPON | IF_STAY_COOP, + /* vwep_model */ "#w_phalanx.md2", + /* armor_info */ nullptr, + /* tag */ AMMO_MAGSLUG, + /* precaches */ "weapons/plasshot.wav sprites/s_photon.sp2 weapons/rockfly.wav" + }, + + /*QUAKED weapon_bfg (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/weapons/g_bfg/tris.md2" + */ + { + /* id */ IT_WEAPON_BFG, + /* classname */ "weapon_bfg", + /* pickup */ Pickup_Weapon, + /* use */ Use_Weapon, + /* drop */ Drop_Weapon, + /* weaponthink */ Weapon_BFG, + /* pickup_sound */ "misc/w_pkup.wav", + /* world_model */ "models/weapons/g_bfg/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ "models/weapons/v_bfg/tris.md2", + /* icon */ "w_bfg", + /* use_name */ "BFG10K", + /* pickup_name */ "$item_bfg10k", + /* pickup_name_definite */ "$item_bfg10k_def", + /* quantity */ 50, + /* ammo */ IT_AMMO_CELLS, + /* chain */ IT_WEAPON_BFG, + /* flags */ IF_WEAPON | IF_STAY_COOP, + /* vwep_model */ "#w_bfg.md2", + /* armor_info */ nullptr, + /* tag */ AMMO_CELLS, + /* precaches */ "sprites/s_bfg1.sp2 sprites/s_bfg2.sp2 sprites/s_bfg3.sp2 weapons/bfg__f1y.wav weapons/bfg__l1a.wav weapons/bfg__x1b.wav weapons/bfg_hum.wav", + /* sort_id */ 0, + /* quantity_warn */ 50 + }, + + /*QUAKED weapon_disintegrator (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/weapons/g_dist/tris.md2" + */ + { + /* id */ IT_WEAPON_DISRUPTOR, + /* classname */ "weapon_disintegrator", + /* pickup */ Pickup_Weapon, + /* use */ Use_Weapon, + /* drop */ Drop_Weapon, + /* weaponthink */ Weapon_Disruptor, + /* pickup_sound */ "misc/w_pkup.wav", + /* world_model */ "models/weapons/g_dist/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ "models/weapons/v_dist/tris.md2", + /* icon */ "w_disintegrator", + /* use_name */ "Disruptor", + /* pickup_name */ "$item_disruptor", + /* pickup_name_definite */ "$item_disruptor_def", + /* quantity */ 1, + /* ammo */ IT_AMMO_ROUNDS, + /* chain */ IT_WEAPON_BFG, + /* flags */ IF_WEAPON | IF_STAY_COOP, + /* vwep_model */ "#w_disrupt.md2", + /* armor_info */ nullptr, + /* tag */ AMMO_DISRUPTOR, + /* precaches */ "models/proj/disintegrator/tris.md2 weapons/disrupt.wav weapons/disint2.wav weapons/disrupthit.wav", + }, + + // + // AMMO ITEMS + // + + /*QUAKED ammo_shells (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/items/ammo/shells/medium/tris.md2" + */ + { + /* id */ IT_AMMO_SHELLS, + /* classname */ "ammo_shells", + /* pickup */ Pickup_Ammo, + /* use */ nullptr, + /* drop */ Drop_Ammo, + /* weaponthink */ nullptr, + /* pickup_sound */ "misc/am_pkup.wav", + /* world_model */ "models/items/ammo/shells/medium/tris.md2", + /* world_model_flags */ EF_NONE, + /* view_model */ nullptr, + /* icon */ "a_shells", + /* use_name */ "Shells", + /* pickup_name */ "$item_shells", + /* pickup_name_definite */ "$item_shells_def", + /* quantity */ 10, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_AMMO, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ AMMO_SHELLS + }, + + /*QUAKED ammo_bullets (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/items/ammo/bullets/medium/tris.md2" + */ + { + /* id */ IT_AMMO_BULLETS, + /* classname */ "ammo_bullets", + /* pickup */ Pickup_Ammo, + /* use */ nullptr, + /* drop */ Drop_Ammo, + /* weaponthink */ nullptr, + /* pickup_sound */ "misc/am_pkup.wav", + /* world_model */ "models/items/ammo/bullets/medium/tris.md2", + /* world_model_flags */ EF_NONE, + /* view_model */ nullptr, + /* icon */ "a_bullets", + /* use_name */ "Bullets", + /* pickup_name */ "$item_bullets", + /* pickup_name_definite */ "$item_bullets_def", + /* quantity */ 50, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_AMMO, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ AMMO_BULLETS + }, + + /*QUAKED ammo_cells (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/items/ammo/cells/medium/tris.md2" + */ + { + /* id */ IT_AMMO_CELLS, + /* classname */ "ammo_cells", + /* pickup */ Pickup_Ammo, + /* use */ nullptr, + /* drop */ Drop_Ammo, + /* weaponthink */ nullptr, + /* pickup_sound */ "misc/am_pkup.wav", + /* world_model */ "models/items/ammo/cells/medium/tris.md2", + /* world_model_flags */ EF_NONE, + /* view_model */ nullptr, + /* icon */ "a_cells", + /* use_name */ "Cells", + /* pickup_name */ "$item_cells", + /* pickup_name_definite */ "$item_cells_def", + /* quantity */ 50, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_AMMO, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ AMMO_CELLS + }, + + /*QUAKED ammo_rockets (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/items/ammo/rockets/medium/tris.md2" + */ + { + /* id */ IT_AMMO_ROCKETS, + /* classname */ "ammo_rockets", + /* pickup */ Pickup_Ammo, + /* use */ nullptr, + /* drop */ Drop_Ammo, + /* weaponthink */ nullptr, + /* pickup_sound */ "misc/am_pkup.wav", + /* world_model */ "models/items/ammo/rockets/medium/tris.md2", + /* world_model_flags */ EF_NONE, + /* view_model */ nullptr, + /* icon */ "a_rockets", + /* use_name */ "Rockets", + /* pickup_name */ "$item_rockets", + /* pickup_name_definite */ "$item_rockets_def", + /* quantity */ 5, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_AMMO, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ AMMO_ROCKETS + }, + + /*QUAKED ammo_slugs (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/items/ammo/slugs/medium/tris.md2" + */ + { + /* id */ IT_AMMO_SLUGS, + /* classname */ "ammo_slugs", + /* pickup */ Pickup_Ammo, + /* use */ nullptr, + /* drop */ Drop_Ammo, + /* weaponthink */ nullptr, + /* pickup_sound */ "misc/am_pkup.wav", + /* world_model */ "models/items/ammo/slugs/medium/tris.md2", + /* world_model_flags */ EF_NONE, + /* view_model */ nullptr, + /* icon */ "a_slugs", + /* use_name */ "Slugs", + /* pickup_name */ "$item_slugs", + /* pickup_name_definite */ "$item_slugs_def", + /* quantity */ 5, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_AMMO, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ AMMO_SLUGS + }, + + /*QUAKED ammo_magslug (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/objects/ammo/tris.md2" + */ + { + /* id */ IT_AMMO_MAGSLUG, + /* classname */ "ammo_magslug", + /* pickup */ Pickup_Ammo, + /* use */ nullptr, + /* drop */ Drop_Ammo, + /* weaponthink */ nullptr, + /* pickup_sound */ "misc/am_pkup.wav", + /* world_model */ "models/objects/ammo/tris.md2", + /* world_model_flags */ EF_NONE, + /* view_model */ nullptr, + /* icon */ "a_mslugs", + /* use_name */ "Mag Slug", + /* pickup_name */ "$item_mag_slug", + /* pickup_name_definite */ "$item_mag_slug_def", + /* quantity */ 10, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_AMMO, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ AMMO_MAGSLUG + }, + + /*QUAKED ammo_flechettes (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/ammo/am_flechette/tris.md2" + */ + { + /* id */ IT_AMMO_FLECHETTES, + /* classname */ "ammo_flechettes", + /* pickup */ Pickup_Ammo, + /* use */ nullptr, + /* drop */ Drop_Ammo, + /* weaponthink */ nullptr, + /* pickup_sound */ "misc/am_pkup.wav", + /* world_model */ "models/ammo/am_flechette/tris.md2", + /* world_model_flags */ EF_NONE, + /* view_model */ nullptr, + /* icon */ "a_flechettes", + /* use_name */ "Flechettes", + /* pickup_name */ "$item_flechettes", + /* pickup_name_definite */ "$item_flechettes_def", + /* quantity */ 50, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_AMMO, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ AMMO_FLECHETTES + }, + + /*QUAKED ammo_prox (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/ammo/am_prox/tris.md2" + */ + { + /* id */ IT_AMMO_PROX, + /* classname */ "ammo_prox", + /* pickup */ Pickup_Ammo, + /* use */ nullptr, + /* drop */ Drop_Ammo, + /* weaponthink */ nullptr, + /* pickup_sound */ "misc/am_pkup.wav", + /* world_model */ "models/ammo/am_prox/tris.md2", + /* world_model_flags */ EF_NONE, + /* view_model */ nullptr, + /* icon */ "a_prox", + /* use_name */ "Prox", + /* pickup_name */ "$item_prox", + /* pickup_name_definite */ "$item_prox_def", + /* quantity */ 5, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_AMMO, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ AMMO_PROX, + /* precaches */ "models/weapons/g_prox/tris.md2 weapons/proxwarn.wav" + }, + + /*QUAKED ammo_nuke (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/ammo/g_nuke/tris.md2" + */ + { + /* id */ IT_AMMO_NUKE, + /* classname */ "ammo_nuke", + /* pickup */ Pickup_Nuke, + /* use */ Use_Nuke, + /* drop */ Drop_Ammo, + /* weaponthink */ nullptr, + /* pickup_sound */ "misc/am_pkup.wav", + /* world_model */ "models/weapons/g_nuke/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "p_nuke", + /* use_name */ "A-M Bomb", + /* pickup_name */ "$item_am_bomb", + /* pickup_name_definite */ "$item_am_bomb_def", + /* quantity */ 300, + /* ammo */ IT_AMMO_NUKE, + /* chain */ IT_NULL, + /* flags */ IF_TIMED | IF_POWERUP_WHEEL, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_AM_BOMB, + /* precaches */ "weapons/nukewarn2.wav world/rumble.wav" + }, + + /*QUAKED ammo_disruptor (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/ammo/am_disr/tris.md2" + */ + { + /* id */ IT_AMMO_ROUNDS, + /* classname */ "ammo_disruptor", + /* pickup */ Pickup_Ammo, + /* use */ nullptr, + /* drop */ Drop_Ammo, + /* weaponthink */ nullptr, + /* pickup_sound */ "misc/am_pkup.wav", + /* world_model */ "models/ammo/am_disr/tris.md2", + /* world_model_flags */ EF_NONE, + /* view_model */ nullptr, + /* icon */ "a_disruptor", + /* use_name */ "Rounds", + /* pickup_name */ "$item_rounds", + /* pickup_name_definite */ "$item_rounds_def", + /* quantity */ 3, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_AMMO, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ AMMO_DISRUPTOR + }, + + // + // POWERUP ITEMS + // + /*QUAKED item_quad (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/items/quaddama/tris.md2" + */ + { + /* id */ IT_POWERUP_QUAD, + /* classname */ "item_quad", + /* pickup */ Pickup_Powerup, + /* use */ Use_Quad, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/quaddama/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "p_quad", + /* use_name */ "Quad Damage", + /* pickup_name */ "$item_quad_damage", + /* pickup_name_definite */ "$item_quad_damage_def", + /* quantity */ 60, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_POWERUP | IF_POWERUP_WHEEL, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_QUAD, + /* precaches */ "items/damage.wav items/damage2.wav items/damage3.wav ctf/tech2x.wav" + }, + + /*QUAKED item_quadfire (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/items/quadfire/tris.md2" + */ + { + /* id */ IT_POWERUP_HASTE, + /* classname */ "item_quadfire", + /* pickup */ Pickup_Powerup, + /* use */ Use_Haste, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/quadfire/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "p_quadfire", + /* use_name */ "Haste", + /* pickup_name */ "Haste", + /* pickup_name_definite */ "Haste", + /* quantity */ 60, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_POWERUP | IF_POWERUP_WHEEL, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_HASTE, + /* precaches */ "items/quadfire1.wav items/quadfire2.wav items/quadfire3.wav" + }, + + /*QUAKED item_invulnerability (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/items/invulner/tris.md2" + */ + { + /* id */ IT_POWERUP_PROTECTION, + /* classname */ "item_invulnerability", + /* pickup */ Pickup_Powerup, + /* use */ Use_Protection, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/invulner/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "p_invulnerability", + /* use_name */ "Protection", + /* pickup_name */ "Protection", + /* pickup_name_definite */ "Protection", + /* quantity */ 60, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_POWERUP | IF_POWERUP_WHEEL, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_PROTECTION, + /* precaches */ "items/protect.wav items/protect2.wav items/protect4.wav" + }, + + /*QUAKED item_invisibility (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/items/cloaker/tris.md2" + */ + { + /* id */ IT_POWERUP_INVISIBILITY, + /* classname */ "item_invisibility", + /* pickup */ Pickup_Powerup, + /* use */ Use_Invisibility, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/cloaker/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "p_cloaker", + /* use_name */ "Invisibility", + /* pickup_name */ "$item_invisibility", + /* pickup_name_definite */ "$item_invisibility_def", + /* quantity */ 60, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_POWERUP | IF_POWERUP_WHEEL, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_INVISIBILITY, + }, + + /*QUAKED item_silencer (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/items/silencer/tris.md2" + */ + { + /* id */ IT_POWERUP_SILENCER, + /* classname */ "item_silencer", + /* pickup */ Pickup_TimedItem, + /* use */ Use_Silencer, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/silencer/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "p_silencer", + /* use_name */ "Silencer", + /* pickup_name */ "$item_silencer", + /* pickup_name_definite */ "$item_silencer_def", + /* quantity */ 60, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_TIMED | IF_POWERUP_WHEEL, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_SILENCER, + }, + + /*QUAKED item_breather (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/items/breather/tris.md2" + */ + { + /* id */ IT_POWERUP_REBREATHER, + /* classname */ "item_breather", + /* pickup */ Pickup_TimedItem, + /* use */ Use_Breather, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/breather/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "p_rebreather", + /* use_name */ "Rebreather", + /* pickup_name */ "$item_rebreather", + /* pickup_name_definite */ "$item_rebreather_def", + /* quantity */ 60, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_STAY_COOP | IF_TIMED | IF_POWERUP_WHEEL, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_REBREATHER, + /* precaches */ "items/airout.wav" + }, + + /*QUAKED item_enviro (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/items/enviro/tris.md2" + */ + { + /* id */ IT_POWERUP_ENVIROSUIT, + /* classname */ "item_enviro", + /* pickup */ Pickup_TimedItem, + /* use */ Use_Envirosuit, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/enviro/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "p_envirosuit", + /* use_name */ "Environment Suit", + /* pickup_name */ "$item_environment_suit", + /* pickup_name_definite */ "$item_environment_suit_def", + /* quantity */ 60, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_STAY_COOP | IF_TIMED | IF_POWERUP_WHEEL, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_ENVIROSUIT, + /* precaches */ "items/airout.wav" + }, + + /*QUAKED item_ancient_head (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + Special item that gives +2 to maximum health + model="models/items/c_head/tris.md2" + */ + { + /* id */ IT_ANCIENT_HEAD, + /* classname */ "item_ancient_head", + /* pickup */ Pickup_LegacyHead, + /* use */ nullptr, + /* drop */ nullptr, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/c_head/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "i_fixme", + /* use_name */ "Ancient Head", + /* pickup_name */ "$item_ancient_head", + /* pickup_name_definite */ "$item_ancient_head_def", + /* quantity */ 60, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_HEALTH | IF_NOT_RANDOM, + }, + + /*QUAKED item_legacy_head (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + Special item that gives +5 to maximum health. + model="models/items/legacyhead/tris.md2" + */ + { + /* id */ IT_LEGACY_HEAD, + /* classname */ "item_legacy_head", + /* pickup */ Pickup_LegacyHead, + /* use */ nullptr, + /* drop */ nullptr, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/legacyhead/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "i_fixme", + /* use_name */ "Ranger's Head", + /* pickup_name */ "Ranger's Head", + /* pickup_name_definite */ "Ranger's Head", + /* quantity */ 60, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_HEALTH | IF_NOT_RANDOM, + }, + + /*QUAKED item_adrenaline (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + Gives +1 to maximum health, +5 in deathmatch. + model="models/items/adrenal/tris.md2" + */ + { + /* id */ IT_ADRENALINE, + /* classname */ "item_adrenaline", + /* pickup */ Pickup_TimedItem, + /* use */ Use_Adrenaline, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/adrenal/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "p_adrenaline", + /* use_name */ "Adrenaline", + /* pickup_name */ "$item_adrenaline", + /* pickup_name_definite */ "$item_adrenaline_def", + /* quantity */ 60, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_HEALTH | IF_POWERUP_WHEEL, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_ADRENALINE, + /* precache */ "items/n_health.wav" + }, + + /*QUAKED item_bandolier (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/items/band/tris.md2" + */ + { + /* id */ IT_BANDOLIER, + /* classname */ "item_bandolier", + /* pickup */ Pickup_Bandolier, + /* use */ nullptr, + /* drop */ nullptr, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/band/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "p_bandolier", + /* use_name */ "Bandolier", + /* pickup_name */ "$item_bandolier", + /* pickup_name_definite */ "$item_bandolier_def", + /* quantity */ 60, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_TIMED + }, + + /*QUAKED item_pack (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/items/pack/tris.md2" + */ + { + /* id */ IT_PACK, + /* classname */ "item_pack", + /* pickup */ Pickup_Pack, + /* use */ nullptr, + /* drop */ nullptr, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/pack/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "i_pack", + /* use_name */ "Ammo Pack", + /* pickup_name */ "$item_ammo_pack", + /* pickup_name_definite */ "$item_ammo_pack_def", + /* quantity */ 180, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_TIMED + }, + + /*QUAKED item_ir_goggles (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + Infrared vision. + model="models/items/goggles/tris.md2" + */ + { + /* id */ IT_IR_GOGGLES, + /* classname */ "item_ir_goggles", + /* pickup */ Pickup_TimedItem, + /* use */ Use_IR, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/goggles/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "p_ir", + /* use_name */ "IR Goggles", + /* pickup_name */ "$item_ir_goggles", + /* pickup_name_definite */ "$item_ir_goggles_def", + /* quantity */ 60, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_TIMED | IF_POWERUP_WHEEL, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_IR_GOGGLES, + /* precaches */ "misc/ir_start.wav" + }, + + /*QUAKED item_double (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/items/ddamage/tris.md2" + */ + { + /* id */ IT_POWERUP_DOUBLE, + /* classname */ "item_double", + /* pickup */ Pickup_Powerup, + /* use */ Use_Double, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/ddamage/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "p_double", + /* use_name */ "Double Damage", + /* pickup_name */ "$item_double_damage", + /* pickup_name_definite */ "$item_double_damage_def", + /* quantity */ 60, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_POWERUP | IF_POWERUP_WHEEL, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_DOUBLE, + /* precaches */ "misc/ddamage1.wav misc/ddamage2.wav misc/ddamage3.wav ctf/tech2x.wav" + }, + + /*QUAKED item_sphere_vengeance (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/items/vengnce/tris.md2" + */ + { + /* id */ IT_POWERUP_SPHERE_VENGEANCE, + /* classname */ "item_sphere_vengeance", + /* pickup */ Pickup_Sphere, + /* use */ Use_Vengeance, + /* drop */ nullptr, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/vengnce/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "p_vengeance", + /* use_name */ "vengeance sphere", + /* pickup_name */ "$item_vengeance_sphere", + /* pickup_name_definite */ "$item_vengeance_sphere_def", + /* quantity */ 60, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_SPHERE | IF_POWERUP_WHEEL, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_SPHERE_VENGEANCE, + /* precaches */ "spheres/v_idle.wav" + }, + + /*QUAKED item_sphere_hunter (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/items/hunter/tris.md2" + */ + { + /* id */ IT_POWERUP_SPHERE_HUNTER, + /* classname */ "item_sphere_hunter", + /* pickup */ Pickup_Sphere, + /* use */ Use_Hunter, + /* drop */ nullptr, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/hunter/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "p_hunter", + /* use_name */ "hunter sphere", + /* pickup_name */ "$item_hunter_sphere", + /* pickup_name_definite */ "$item_hunter_sphere_def", + /* quantity */ 120, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_SPHERE | IF_POWERUP_WHEEL, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_SPHERE_HUNTER, + /* precaches */ "spheres/h_idle.wav spheres/h_active.wav spheres/h_lurk.wav" + }, + + /*QUAKED item_sphere_defender (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/items/defender/tris.md2" + */ + { + /* id */ IT_POWERUP_SPHERE_DEFENDER, + /* classname */ "item_sphere_defender", + /* pickup */ Pickup_Sphere, + /* use */ Use_Defender, + /* drop */ nullptr, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/defender/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "p_defender", + /* use_name */ "defender sphere", + /* pickup_name */ "$item_defender_sphere", + /* pickup_name_definite */ "$item_defender_sphere_def", + /* quantity */ 60, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_SPHERE | IF_POWERUP_WHEEL, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_SPHERE_DEFENDER, + /* precaches */ "models/objects/laser/tris.md2 models/items/shell/tris.md2 spheres/d_idle.wav" + }, + + /*QUAKED item_doppleganger (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/items/dopple/tris.md2" + */ + { + /* id */ IT_DOPPELGANGER, + /* classname */ "item_doppleganger", + /* pickup */ Pickup_Doppelganger, + /* use */ Use_Doppelganger, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/dopple/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "p_doppleganger", + /* use_name */ "Doppelganger", + /* pickup_name */ "$item_doppleganger", + /* pickup_name_definite */ "$item_doppleganger_def", + /* quantity */ 90, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_TIMED | IF_POWERUP_WHEEL, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_DOPPELGANGER, + /* precaches */ "models/objects/dopplebase/tris.md2 models/items/spawngro3/tris.md2 medic_commander/monsterspawn1.wav models/items/hunter/tris.md2 models/items/vengnce/tris.md2", + /* sort_id */ 0, + /* quantity_warn */ 1, + /* quantity_max */ 1 + }, + + /* Tag Token */ + { + /* id */ IT_TAG_TOKEN, + /* classname */ nullptr, + /* pickup */ nullptr, + /* use */ nullptr, + /* drop */ nullptr, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/tagtoken/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB | EF_TAGTRAIL, + /* view_model */ nullptr, + /* icon */ "i_tagtoken", + /* use_name */ "Tag Token", + /* pickup_name */ "$item_tag_token", + /* pickup_name_definite */ "$item_tag_token_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_TIMED | IF_NOT_GIVEABLE + }, -/*QUAKED weapon_chainfist (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + // + // KEYS + // +/*QUAKED key_data_cd (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP +Key for computer centers. -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/weapons/g_chainf/tris.md2" +model="models/items/keys/data_cd/tris.md2" */ { - /* id */ IT_WEAPON_CHAINFIST, - /* classname */ "weapon_chainfist", - /* pickup */ Pickup_Weapon, - /* use */ Use_Weapon, - /* drop */ Drop_Weapon, - /* weaponthink */ Weapon_ChainFist, - /* pickup_sound */ "misc/w_pkup.wav", - /* world_model */ "models/weapons/g_chainf/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ "models/weapons/v_chainf/tris.md2", - /* icon */ "w_chainfist", - /* use_name */ "Chainfist", - /* pickup_name */ "$item_chainfist", - /* pickup_name_definite */ "$item_chainfist_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_WEAPON_BLASTER, - /* flags */ IF_WEAPON | IF_STAY_COOP | IF_NO_HASTE, - /* vwep_model */ "#w_chainfist.md2", - /* armor_info */ nullptr, - /* tag */ 0, - /* precaches */ "weapons/sawidle.wav weapons/sawhit.wav weapons/sawslice.wav", - }, - -/*QUAKED weapon_shotgun (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/weapons/g_shotg/tris.md2" -*/ - { - /* id */ IT_WEAPON_SHOTGUN, - /* classname */ "weapon_shotgun", - /* pickup */ Pickup_Weapon, - /* use */ Use_Weapon, - /* drop */ Drop_Weapon, - /* weaponthink */ Weapon_Shotgun, - /* pickup_sound */ "misc/w_pkup.wav", - /* world_model */ "models/weapons/g_shotg/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ "models/weapons/v_shotg/tris.md2", - /* icon */ "w_shotgun", - /* use_name */ "Shotgun", - /* pickup_name */ "$item_shotgun", - /* pickup_name_definite */ "$item_shotgun_def", - /* quantity */ 1, - /* ammo */ IT_AMMO_SHELLS, - /* chain */ IT_NULL, - /* flags */ IF_WEAPON | IF_STAY_COOP, - /* vwep_model */ "#w_shotgun.md2", - /* armor_info */ nullptr, - /* tag */ AMMO_SHELLS, - /* precaches */ "weapons/shotgf1b.wav weapons/shotgr1b.wav" - }, - -/*QUAKED weapon_supershotgun (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/weapons/g_shotg2/tris.md2" -*/ - { - /* id */ IT_WEAPON_SSHOTGUN, - /* classname */ "weapon_supershotgun", - /* pickup */ Pickup_Weapon, - /* use */ Use_Weapon, - /* drop */ Drop_Weapon, - /* weaponthink */ Weapon_SuperShotgun, - /* pickup_sound */ "misc/w_pkup.wav", - /* world_model */ "models/weapons/g_shotg2/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ "models/weapons/v_shotg2/tris.md2", - /* icon */ "w_sshotgun", - /* use_name */ "Super Shotgun", - /* pickup_name */ "$item_super_shotgun", - /* pickup_name_definite */ "$item_super_shotgun_def", - /* quantity */ 2, - /* ammo */ IT_AMMO_SHELLS, - /* chain */ IT_NULL, - /* flags */ IF_WEAPON | IF_STAY_COOP, - /* vwep_model */ "#w_sshotgun.md2", - /* armor_info */ nullptr, - /* tag */ AMMO_SHELLS, - /* precaches */ "weapons/sshotf1b.wav", - /* sort_id */ 0, - /* quantity_warn */ 10 - }, - -/*QUAKED weapon_machinegun (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/weapons/g_machn/tris.md2" -*/ - { - /* id */ IT_WEAPON_MACHINEGUN, - /* classname */ "weapon_machinegun", - /* pickup */ Pickup_Weapon, - /* use */ Use_Weapon, - /* drop */ Drop_Weapon, - /* weaponthink */ Weapon_Machinegun, - /* pickup_sound */ "misc/w_pkup.wav", - /* world_model */ "models/weapons/g_machn/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ "models/weapons/v_machn/tris.md2", - /* icon */ "w_machinegun", - /* use_name */ "Machinegun", - /* pickup_name */ "$item_machinegun", - /* pickup_name_definite */ "$item_machinegun_def", - /* quantity */ 1, - /* ammo */ IT_AMMO_BULLETS, - /* chain */ IT_WEAPON_MACHINEGUN, - /* flags */ IF_WEAPON | IF_STAY_COOP, - /* vwep_model */ "#w_machinegun.md2", - /* armor_info */ nullptr, - /* tag */ AMMO_BULLETS, - /* precaches */ "weapons/machgf1b.wav weapons/machgf2b.wav weapons/machgf3b.wav weapons/machgf4b.wav weapons/machgf5b.wav", - /* sort_id */ 0, - /* quantity_warn */ 30 - }, - -/*QUAKED weapon_etf_rifle (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/weapons/g_etf_rifle/tris.md2" -*/ - { - /* id */ IT_WEAPON_ETF_RIFLE, - /* classname */ "weapon_etf_rifle", - /* pickup */ Pickup_Weapon, - /* use */ Use_Weapon, - /* drop */ Drop_Weapon, - /* weaponthink */ Weapon_ETF_Rifle, - /* pickup_sound */ "misc/w_pkup.wav", - /* world_model */ "models/weapons/g_etf_rifle/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ "models/weapons/v_etf_rifle/tris.md2", - /* icon */ "w_etf_rifle", - /* use_name */ "ETF Rifle", - /* pickup_name */ "$item_etf_rifle", - /* pickup_name_definite */ "$item_etf_rifle_def", - /* quantity */ 1, - /* ammo */ IT_AMMO_FLECHETTES, - /* chain */ IT_WEAPON_MACHINEGUN, - /* flags */ IF_WEAPON | IF_STAY_COOP, - /* vwep_model */ "#w_etfrifle.md2", - /* armor_info */ nullptr, - /* tag */ AMMO_FLECHETTES, - /* precaches */ "weapons/nail1.wav models/proj/flechette/tris.md2", - /* sort_id */ 0, - /* quantity_warn */ 30 - }, - -/*QUAKED weapon_chaingun (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/weapons/g_chain/tris.md2" -*/ - { - /* id */ IT_WEAPON_CHAINGUN, - /* classname */ "weapon_chaingun", - /* pickup */ Pickup_Weapon, - /* use */ Use_Weapon, - /* drop */ Drop_Weapon, - /* weaponthink */ Weapon_Chaingun, - /* pickup_sound */ "misc/w_pkup.wav", - /* world_model */ "models/weapons/g_chain/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ "models/weapons/v_chain/tris.md2", - /* icon */ "w_chaingun", - /* use_name */ "Chaingun", - /* pickup_name */ "$item_chaingun", - /* pickup_name_definite */ "$item_chaingun_def", - /* quantity */ 1, - /* ammo */ IT_AMMO_BULLETS, - /* chain */ IT_NULL, - /* flags */ IF_WEAPON | IF_STAY_COOP, - /* vwep_model */ "#w_chaingun.md2", - /* armor_info */ nullptr, - /* tag */ AMMO_BULLETS, - /* precaches */ "weapons/chngnu1a.wav weapons/chngnl1a.wav weapons/machgf3b.wav weapons/chngnd1a.wav", - /* sort_id */ 0, - /* quantity_warn */ 60 - }, - -/*QUAKED ammo_grenades (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -*/ - { - /* id */ IT_AMMO_GRENADES, - /* classname */ "ammo_grenades", - /* pickup */ Pickup_Ammo, - /* use */ Use_Weapon, - /* drop */ Drop_Ammo, - /* weaponthink */ Weapon_HandGrenade, - /* pickup_sound */ "misc/am_pkup.wav", - /* world_model */ "models/items/ammo/grenades/medium/tris.md2", - /* world_model_flags */ EF_NONE, - /* view_model */ "models/weapons/v_handgr/tris.md2", - /* icon */ "a_grenades", - /* use_name */ "Grenades", - /* pickup_name */ "$item_grenades", - /* pickup_name_definite */ "$item_grenades_def", - /* quantity */ 5, - /* ammo */ IT_AMMO_GRENADES, - /* chain */ IT_AMMO_GRENADES, - /* flags */ IF_AMMO | IF_WEAPON, - /* vwep_model */ "#a_grenades.md2", - /* armor_info */ nullptr, - /* tag */ AMMO_GRENADES, - /* precaches */ "weapons/hgrent1a.wav weapons/hgrena1b.wav weapons/hgrenc1b.wav weapons/hgrenb1a.wav weapons/hgrenb2a.wav models/objects/grenade3/tris.md2", - /* sort_id */ 0, - /* quantity_warn */ 2 - }, - -/*QUAKED ammo_trap (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/weapons/g_trap/tris.md2" -*/ - { - /* id */ IT_AMMO_TRAP, - /* classname */ "ammo_trap", - /* pickup */ Pickup_Ammo, - /* use */ Use_Weapon, - /* drop */ Drop_Ammo, - /* weaponthink */ Weapon_Trap, - /* pickup_sound */ "misc/am_pkup.wav", - /* world_model */ "models/weapons/g_trap/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ "models/weapons/v_trap/tris.md2", - /* icon */ "a_trap", - /* use_name */ "Trap", - /* pickup_name */ "$item_trap", - /* pickup_name_definite */ "$item_trap_def", - /* quantity */ 1, - /* ammo */ IT_AMMO_TRAP, - /* chain */ IT_AMMO_GRENADES, - /* flags */ IF_AMMO | IF_WEAPON | IF_NO_INFINITE_AMMO, - /* vwep_model */ "#a_trap.md2", - /* armor_info */ nullptr, - /* tag */ AMMO_TRAP, - /* precaches */ "misc/fhit3.wav weapons/trapcock.wav weapons/traploop.wav weapons/trapsuck.wav weapons/trapdown.wav items/s_health.wav items/n_health.wav items/l_health.wav items/m_health.wav models/weapons/z_trap/tris.md2", - /* sort_id */ 0, - /* quantity_warn */ 1 - }, - -/*QUAKED ammo_tesla (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/ammo/am_tesl/tris.md2" -*/ - { - /* id */ IT_AMMO_TESLA, - /* classname */ "ammo_tesla", - /* pickup */ Pickup_Ammo, - /* use */ Use_Weapon, - /* drop */ Drop_Ammo, - /* weaponthink */ Weapon_Tesla, - /* pickup_sound */ "misc/am_pkup.wav", - /* world_model */ "models/ammo/am_tesl/tris.md2", - /* world_model_flags */ EF_NONE, - /* view_model */ "models/weapons/v_tesla/tris.md2", - /* icon */ "a_tesla", - /* use_name */ "Tesla", - /* pickup_name */ "$item_tesla", - /* pickup_name_definite */ "$item_tesla_def", - /* quantity */ 3, - /* ammo */ IT_AMMO_TESLA, - /* chain */ IT_AMMO_GRENADES, - /* flags */ IF_AMMO | IF_WEAPON | IF_NO_INFINITE_AMMO, - /* vwep_model */ "#a_tesla.md2", - /* armor_info */ nullptr, - /* tag */ AMMO_TESLA, - /* precaches */ "weapons/teslaopen.wav weapons/hgrenb1a.wav weapons/hgrenb2a.wav models/weapons/g_tesla/tris.md2", - /* sort_id */ 0, - /* quantity_warn */ 1 - }, - -/*QUAKED weapon_grenadelauncher (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/weapons/g_launch/tris.md2" -*/ - { - /* id */ IT_WEAPON_GLAUNCHER, - /* classname */ "weapon_grenadelauncher", - /* pickup */ Pickup_Weapon, - /* use */ Use_Weapon, - /* drop */ Drop_Weapon, - /* weaponthink */ Weapon_GrenadeLauncher, - /* pickup_sound */ "misc/w_pkup.wav", - /* world_model */ "models/weapons/g_launch/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ "models/weapons/v_launch/tris.md2", - /* icon */ "w_glauncher", - /* use_name */ "Grenade Launcher", - /* pickup_name */ "$item_grenade_launcher", - /* pickup_name_definite */ "$item_grenade_launcher_def", - /* quantity */ 1, - /* ammo */ IT_AMMO_GRENADES, - /* chain */ IT_WEAPON_GLAUNCHER, - /* flags */ IF_WEAPON | IF_STAY_COOP, - /* vwep_model */ "#w_glauncher.md2", - /* armor_info */ nullptr, - /* tag */ AMMO_GRENADES, - /* precaches */ "models/objects/grenade4/tris.md2 weapons/grenlf1a.wav weapons/grenlr1b.wav weapons/grenlb1b.wav" - }, - -/*QUAKED weapon_proxlauncher (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/weapons/g_plaunch/tris.md2" -*/ - { - /* id */ IT_WEAPON_PROXLAUNCHER, - /* classname */ "weapon_proxlauncher", - /* pickup */ Pickup_Weapon, - /* use */ Use_Weapon, - /* drop */ Drop_Weapon, - /* weaponthink */ Weapon_ProxLauncher, - /* pickup_sound */ "misc/w_pkup.wav", - /* world_model */ "models/weapons/g_plaunch/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ "models/weapons/v_plaunch/tris.md2", - /* icon */ "w_proxlaunch", - /* use_name */ "Prox Launcher", - /* pickup_name */ "$item_prox_launcher", - /* pickup_name_definite */ "$item_prox_launcher_def", - /* quantity */ 1, - /* ammo */ IT_AMMO_PROX, - /* chain */ IT_WEAPON_GLAUNCHER, - /* flags */ IF_WEAPON | IF_STAY_COOP, - /* vwep_model */ "#w_plauncher.md2", - /* armor_info */ nullptr, - /* tag */ AMMO_PROX, - /* precaches */ "weapons/grenlf1a.wav weapons/grenlr1b.wav weapons/grenlb1b.wav weapons/proxwarn.wav weapons/proxopen.wav", - }, - -/*QUAKED weapon_rocketlauncher (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/weapons/g_rocket/tris.md2" -*/ - { - /* id */ IT_WEAPON_RLAUNCHER, - /* classname */ "weapon_rocketlauncher", - /* pickup */ Pickup_Weapon, - /* use */ Use_Weapon, - /* drop */ Drop_Weapon, - /* weaponthink */ Weapon_RocketLauncher, - /* pickup_sound */ "misc/w_pkup.wav", - /* world_model */ "models/weapons/g_rocket/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ "models/weapons/v_rocket/tris.md2", - /* icon */ "w_rlauncher", - /* use_name */ "Rocket Launcher", - /* pickup_name */ "$item_rocket_launcher", - /* pickup_name_definite */ "$item_rocket_launcher_def", - /* quantity */ 1, - /* ammo */ IT_AMMO_ROCKETS, - /* chain */ IT_NULL, - /* flags */ IF_WEAPON | IF_STAY_COOP, - /* vwep_model */ "#w_rlauncher.md2", - /* armor_info */ nullptr, - /* tag */ AMMO_ROCKETS, - /* precaches */ "models/objects/rocket/tris.md2 weapons/rockfly.wav weapons/rocklf1a.wav weapons/rocklr1b.wav models/objects/debris2/tris.md2" - }, - -/*QUAKED weapon_hyperblaster (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/weapons/g_hyperb/tris.md2" -*/ - { - /* id */ IT_WEAPON_HYPERBLASTER, - /* classname */ "weapon_hyperblaster", - /* pickup */ Pickup_Weapon, - /* use */ Use_Weapon, - /* drop */ Drop_Weapon, - /* weaponthink */ Weapon_HyperBlaster, - /* pickup_sound */ "misc/w_pkup.wav", - /* world_model */ "models/weapons/g_hyperb/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ "models/weapons/v_hyperb/tris.md2", - /* icon */ "w_hyperblaster", - /* use_name */ "HyperBlaster", - /* pickup_name */ "$item_hyperblaster", - /* pickup_name_definite */ "$item_hyperblaster_def", - /* quantity */ 1, - /* ammo */ IT_AMMO_CELLS, - /* chain */ IT_WEAPON_HYPERBLASTER, - /* flags */ IF_WEAPON | IF_STAY_COOP, - /* vwep_model */ "#w_hyperblaster.md2", - /* armor_info */ nullptr, - /* tag */ AMMO_CELLS, - /* precaches */ "weapons/hyprbu1a.wav weapons/hyprbl1a.wav weapons/hyprbf1a.wav weapons/hyprbd1a.wav misc/lasfly.wav", - /* sort_id */ 0, - /* quantity_warn */ 30 - }, - -/*QUAKED weapon_boomer (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/weapons/g_boom/tris.md2" -*/ - { - /* id */ IT_WEAPON_IONRIPPER, - /* classname */ "weapon_boomer", - /* pickup */ Pickup_Weapon, - /* use */ Use_Weapon, - /* drop */ Drop_Weapon, - /* weaponthink */ Weapon_IonRipper, - /* pickup_sound */ "misc/w_pkup.wav", - /* world_model */ "models/weapons/g_boom/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ "models/weapons/v_boomer/tris.md2", - /* icon */ "w_ripper", - /* use_name */ "Ionripper", - /* pickup_name */ "$item_ionripper", - /* pickup_name_definite */ "$item_ionripper_def", - /* quantity */ 2, - /* ammo */ IT_AMMO_CELLS, - /* chain */ IT_WEAPON_HYPERBLASTER, - /* flags */ IF_WEAPON | IF_STAY_COOP, - /* vwep_model */ "#w_ripper.md2", - /* armor_info */ nullptr, - /* tag */ AMMO_CELLS, - /* precaches */ "weapons/rippfire.wav models/objects/boomrang/tris.md2 misc/lasfly.wav", - /* sort_id */ 0, - /* quantity_warn */ 30 - }, - -/*QUAKED weapon_plasmabeam (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/weapons/g_beamer/tris.md2" -*/ - { - /* id */ IT_WEAPON_PLASMABEAM, - /* classname */ "weapon_plasmabeam", - /* pickup */ Pickup_Weapon, - /* use */ Use_Weapon, - /* drop */ Drop_Weapon, - /* weaponthink */ Weapon_PlasmaBeam, - /* pickup_sound */ "misc/w_pkup.wav", - /* world_model */ "models/weapons/g_beamer/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ "models/weapons/v_beamer/tris.md2", - /* icon */ "w_heatbeam", - /* use_name */ "Plasma Beam", - /* pickup_name */ "$item_plasma_beam", - /* pickup_name_definite */ "$item_plasma_beam_def", - /* quantity */ 2, - /* ammo */ IT_AMMO_CELLS, - /* chain */ IT_WEAPON_HYPERBLASTER, - /* flags */ IF_WEAPON | IF_STAY_COOP, - /* vwep_model */ "#w_plasma.md2", - /* armor_info */ nullptr, - /* tag */ AMMO_CELLS, - /* precaches */ "weapons/bfg__l1a.wav weapons/bfg_hum.wav", - /* sort_id */ 0, - /* quantity_warn */ 50 - }, - -/*QUAKED weapon_railgun (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/weapons/g_rail/tris.md2" -*/ - { - /* id */ IT_WEAPON_RAILGUN, - /* classname */ "weapon_railgun", - /* pickup */ Pickup_Weapon, - /* use */ Use_Weapon, - /* drop */ Drop_Weapon, - /* weaponthink */ Weapon_Railgun, - /* pickup_sound */ "misc/w_pkup.wav", - /* world_model */ "models/weapons/g_rail/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ "models/weapons/v_rail/tris.md2", - /* icon */ "w_railgun", - /* use_name */ "Railgun", - /* pickup_name */ "$item_railgun", - /* pickup_name_definite */ "$item_railgun_def", - /* quantity */ 1, - /* ammo */ IT_AMMO_SLUGS, - /* chain */ IT_WEAPON_RAILGUN, - /* flags */ IF_WEAPON | IF_STAY_COOP, - /* vwep_model */ "#w_railgun.md2", - /* armor_info */ nullptr, - /* tag */ AMMO_SLUGS, - /* precaches */ "weapons/rg_hum.wav" - }, - -/*QUAKED weapon_phalanx (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/weapons/g_shotx/tris.md2" -*/ - { - /* id */ IT_WEAPON_PHALANX, - /* classname */ "weapon_phalanx", - /* pickup */ Pickup_Weapon, - /* use */ Use_Weapon, - /* drop */ Drop_Weapon, - /* weaponthink */ Weapon_Phalanx, - /* pickup_sound */ "misc/w_pkup.wav", - /* world_model */ "models/weapons/g_shotx/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ "models/weapons/v_shotx/tris.md2", - /* icon */ "w_phallanx", - /* use_name */ "Phalanx", - /* pickup_name */ "$item_phalanx", - /* pickup_name_definite */ "$item_phalanx_def", - /* quantity */ 1, - /* ammo */ IT_AMMO_MAGSLUG, - /* chain */ IT_WEAPON_RAILGUN, - /* flags */ IF_WEAPON | IF_STAY_COOP, - /* vwep_model */ "#w_phalanx.md2", - /* armor_info */ nullptr, - /* tag */ AMMO_MAGSLUG, - /* precaches */ "weapons/plasshot.wav sprites/s_photon.sp2 weapons/rockfly.wav" - }, - -/*QUAKED weapon_bfg (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/weapons/g_bfg/tris.md2" -*/ - { - /* id */ IT_WEAPON_BFG, - /* classname */ "weapon_bfg", - /* pickup */ Pickup_Weapon, - /* use */ Use_Weapon, - /* drop */ Drop_Weapon, - /* weaponthink */ Weapon_BFG, - /* pickup_sound */ "misc/w_pkup.wav", - /* world_model */ "models/weapons/g_bfg/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ "models/weapons/v_bfg/tris.md2", - /* icon */ "w_bfg", - /* use_name */ "BFG10K", - /* pickup_name */ "$item_bfg10k", - /* pickup_name_definite */ "$item_bfg10k_def", - /* quantity */ 50, - /* ammo */ IT_AMMO_CELLS, - /* chain */ IT_WEAPON_BFG, - /* flags */ IF_WEAPON | IF_STAY_COOP, - /* vwep_model */ "#w_bfg.md2", - /* armor_info */ nullptr, - /* tag */ AMMO_CELLS, - /* precaches */ "sprites/s_bfg1.sp2 sprites/s_bfg2.sp2 sprites/s_bfg3.sp2 weapons/bfg__f1y.wav weapons/bfg__l1a.wav weapons/bfg__x1b.wav weapons/bfg_hum.wav", - /* sort_id */ 0, - /* quantity_warn */ 50 - }, - -/*QUAKED weapon_disintegrator (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/weapons/g_dist/tris.md2" -*/ - { - /* id */ IT_WEAPON_DISRUPTOR, - /* classname */ "weapon_disintegrator", - /* pickup */ Pickup_Weapon, - /* use */ Use_Weapon, - /* drop */ Drop_Weapon, - /* weaponthink */ Weapon_Disruptor, - /* pickup_sound */ "misc/w_pkup.wav", - /* world_model */ "models/weapons/g_dist/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ "models/weapons/v_dist/tris.md2", - /* icon */ "w_disintegrator", - /* use_name */ "Disruptor", - /* pickup_name */ "$item_disruptor", - /* pickup_name_definite */ "$item_disruptor_def", - /* quantity */ 1, - /* ammo */ IT_AMMO_ROUNDS, - /* chain */ IT_WEAPON_BFG, - /* flags */ IF_WEAPON | IF_STAY_COOP, - /* vwep_model */ "#w_disrupt.md2", - /* armor_info */ nullptr, - /* tag */ AMMO_DISRUPTOR, - /* precaches */ "models/proj/disintegrator/tris.md2 weapons/disrupt.wav weapons/disint2.wav weapons/disrupthit.wav", - }, - - // - // AMMO ITEMS - // - -/*QUAKED ammo_shells (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/items/ammo/shells/medium/tris.md2" -*/ - { - /* id */ IT_AMMO_SHELLS, - /* classname */ "ammo_shells", - /* pickup */ Pickup_Ammo, - /* use */ nullptr, - /* drop */ Drop_Ammo, - /* weaponthink */ nullptr, - /* pickup_sound */ "misc/am_pkup.wav", - /* world_model */ "models/items/ammo/shells/medium/tris.md2", - /* world_model_flags */ EF_NONE, - /* view_model */ nullptr, - /* icon */ "a_shells", - /* use_name */ "Shells", - /* pickup_name */ "$item_shells", - /* pickup_name_definite */ "$item_shells_def", - /* quantity */ 10, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_AMMO, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ AMMO_SHELLS - }, - -/*QUAKED ammo_bullets (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/items/ammo/bullets/medium/tris.md2" -*/ - { - /* id */ IT_AMMO_BULLETS, - /* classname */ "ammo_bullets", - /* pickup */ Pickup_Ammo, - /* use */ nullptr, - /* drop */ Drop_Ammo, - /* weaponthink */ nullptr, - /* pickup_sound */ "misc/am_pkup.wav", - /* world_model */ "models/items/ammo/bullets/medium/tris.md2", - /* world_model_flags */ EF_NONE, - /* view_model */ nullptr, - /* icon */ "a_bullets", - /* use_name */ "Bullets", - /* pickup_name */ "$item_bullets", - /* pickup_name_definite */ "$item_bullets_def", - /* quantity */ 50, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_AMMO, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ AMMO_BULLETS - }, - -/*QUAKED ammo_cells (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/items/ammo/cells/medium/tris.md2" -*/ - { - /* id */ IT_AMMO_CELLS, - /* classname */ "ammo_cells", - /* pickup */ Pickup_Ammo, - /* use */ nullptr, - /* drop */ Drop_Ammo, - /* weaponthink */ nullptr, - /* pickup_sound */ "misc/am_pkup.wav", - /* world_model */ "models/items/ammo/cells/medium/tris.md2", - /* world_model_flags */ EF_NONE, - /* view_model */ nullptr, - /* icon */ "a_cells", - /* use_name */ "Cells", - /* pickup_name */ "$item_cells", - /* pickup_name_definite */ "$item_cells_def", - /* quantity */ 50, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_AMMO, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ AMMO_CELLS - }, - -/*QUAKED ammo_rockets (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/items/ammo/rockets/medium/tris.md2" -*/ - { - /* id */ IT_AMMO_ROCKETS, - /* classname */ "ammo_rockets", - /* pickup */ Pickup_Ammo, - /* use */ nullptr, - /* drop */ Drop_Ammo, - /* weaponthink */ nullptr, - /* pickup_sound */ "misc/am_pkup.wav", - /* world_model */ "models/items/ammo/rockets/medium/tris.md2", - /* world_model_flags */ EF_NONE, - /* view_model */ nullptr, - /* icon */ "a_rockets", - /* use_name */ "Rockets", - /* pickup_name */ "$item_rockets", - /* pickup_name_definite */ "$item_rockets_def", - /* quantity */ 5, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_AMMO, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ AMMO_ROCKETS - }, - -/*QUAKED ammo_slugs (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/items/ammo/slugs/medium/tris.md2" -*/ - { - /* id */ IT_AMMO_SLUGS, - /* classname */ "ammo_slugs", - /* pickup */ Pickup_Ammo, - /* use */ nullptr, - /* drop */ Drop_Ammo, - /* weaponthink */ nullptr, - /* pickup_sound */ "misc/am_pkup.wav", - /* world_model */ "models/items/ammo/slugs/medium/tris.md2", - /* world_model_flags */ EF_NONE, - /* view_model */ nullptr, - /* icon */ "a_slugs", - /* use_name */ "Slugs", - /* pickup_name */ "$item_slugs", - /* pickup_name_definite */ "$item_slugs_def", - /* quantity */ 5, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_AMMO, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ AMMO_SLUGS - }, - -/*QUAKED ammo_magslug (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/objects/ammo/tris.md2" -*/ - { - /* id */ IT_AMMO_MAGSLUG, - /* classname */ "ammo_magslug", - /* pickup */ Pickup_Ammo, - /* use */ nullptr, - /* drop */ Drop_Ammo, - /* weaponthink */ nullptr, - /* pickup_sound */ "misc/am_pkup.wav", - /* world_model */ "models/objects/ammo/tris.md2", - /* world_model_flags */ EF_NONE, - /* view_model */ nullptr, - /* icon */ "a_mslugs", - /* use_name */ "Mag Slug", - /* pickup_name */ "$item_mag_slug", - /* pickup_name_definite */ "$item_mag_slug_def", - /* quantity */ 10, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_AMMO, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ AMMO_MAGSLUG - }, - -/*QUAKED ammo_flechettes (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/ammo/am_flechette/tris.md2" -*/ - { - /* id */ IT_AMMO_FLECHETTES, - /* classname */ "ammo_flechettes", - /* pickup */ Pickup_Ammo, - /* use */ nullptr, - /* drop */ Drop_Ammo, - /* weaponthink */ nullptr, - /* pickup_sound */ "misc/am_pkup.wav", - /* world_model */ "models/ammo/am_flechette/tris.md2", - /* world_model_flags */ EF_NONE, - /* view_model */ nullptr, - /* icon */ "a_flechettes", - /* use_name */ "Flechettes", - /* pickup_name */ "$item_flechettes", - /* pickup_name_definite */ "$item_flechettes_def", - /* quantity */ 50, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_AMMO, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ AMMO_FLECHETTES - }, - -/*QUAKED ammo_prox (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/ammo/am_prox/tris.md2" -*/ - { - /* id */ IT_AMMO_PROX, - /* classname */ "ammo_prox", - /* pickup */ Pickup_Ammo, - /* use */ nullptr, - /* drop */ Drop_Ammo, - /* weaponthink */ nullptr, - /* pickup_sound */ "misc/am_pkup.wav", - /* world_model */ "models/ammo/am_prox/tris.md2", - /* world_model_flags */ EF_NONE, - /* view_model */ nullptr, - /* icon */ "a_prox", - /* use_name */ "Prox", - /* pickup_name */ "$item_prox", - /* pickup_name_definite */ "$item_prox_def", - /* quantity */ 5, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_AMMO, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ AMMO_PROX, - /* precaches */ "models/weapons/g_prox/tris.md2 weapons/proxwarn.wav" - }, - -/*QUAKED ammo_nuke (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/ammo/g_nuke/tris.md2" -*/ - { - /* id */ IT_AMMO_NUKE, - /* classname */ "ammo_nuke", - /* pickup */ Pickup_Nuke, - /* use */ Use_Nuke, - /* drop */ Drop_Ammo, - /* weaponthink */ nullptr, - /* pickup_sound */ "misc/am_pkup.wav", - /* world_model */ "models/weapons/g_nuke/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "p_nuke", - /* use_name */ "A-M Bomb", - /* pickup_name */ "$item_am_bomb", - /* pickup_name_definite */ "$item_am_bomb_def", - /* quantity */ 300, - /* ammo */ IT_AMMO_NUKE, - /* chain */ IT_NULL, - /* flags */ IF_TIMED | IF_POWERUP_WHEEL, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_AM_BOMB, - /* precaches */ "weapons/nukewarn2.wav world/rumble.wav" - }, - -/*QUAKED ammo_disruptor (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/ammo/am_disr/tris.md2" -*/ - { - /* id */ IT_AMMO_ROUNDS, - /* classname */ "ammo_disruptor", - /* pickup */ Pickup_Ammo, - /* use */ nullptr, - /* drop */ Drop_Ammo, - /* weaponthink */ nullptr, - /* pickup_sound */ "misc/am_pkup.wav", - /* world_model */ "models/ammo/am_disr/tris.md2", - /* world_model_flags */ EF_NONE, - /* view_model */ nullptr, - /* icon */ "a_disruptor", - /* use_name */ "Rounds", - /* pickup_name */ "$item_rounds", - /* pickup_name_definite */ "$item_rounds_def", - /* quantity */ 3, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_AMMO, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ AMMO_DISRUPTOR - }, - -// -// POWERUP ITEMS -// -/*QUAKED item_quad (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/items/quaddama/tris.md2" -*/ - { - /* id */ IT_POWERUP_QUAD, - /* classname */ "item_quad", - /* pickup */ Pickup_Powerup, - /* use */ Use_Quad, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/quaddama/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "p_quad", - /* use_name */ "Quad Damage", - /* pickup_name */ "$item_quad_damage", - /* pickup_name_definite */ "$item_quad_damage_def", - /* quantity */ 60, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_POWERUP | IF_POWERUP_WHEEL, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_QUAD, - /* precaches */ "items/damage.wav items/damage2.wav items/damage3.wav ctf/tech2x.wav" - }, - -/*QUAKED item_quadfire (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/items/quadfire/tris.md2" -*/ - { - /* id */ IT_POWERUP_HASTE, - /* classname */ "item_quadfire", - /* pickup */ Pickup_Powerup, - /* use */ Use_Haste, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/quadfire/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "p_quadfire", - /* use_name */ "Haste", - /* pickup_name */ "Haste", - /* pickup_name_definite */ "Haste", - /* quantity */ 60, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_POWERUP | IF_POWERUP_WHEEL, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_HASTE, - /* precaches */ "items/quadfire1.wav items/quadfire2.wav items/quadfire3.wav" - }, - -/*QUAKED item_invulnerability (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/items/invulner/tris.md2" -*/ - { - /* id */ IT_POWERUP_PROTECTION, - /* classname */ "item_invulnerability", - /* pickup */ Pickup_Powerup, - /* use */ Use_Protection, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/invulner/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "p_invulnerability", - /* use_name */ "Protection", - /* pickup_name */ "Protection", - /* pickup_name_definite */ "Protection", - /* quantity */ 60, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_POWERUP | IF_POWERUP_WHEEL, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_PROTECTION, - /* precaches */ "items/protect.wav items/protect2.wav items/protect4.wav" - }, - -/*QUAKED item_invisibility (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/items/cloaker/tris.md2" -*/ - { - /* id */ IT_POWERUP_INVISIBILITY, - /* classname */ "item_invisibility", - /* pickup */ Pickup_Powerup, - /* use */ Use_Invisibility, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/cloaker/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "p_cloaker", - /* use_name */ "Invisibility", - /* pickup_name */ "$item_invisibility", - /* pickup_name_definite */ "$item_invisibility_def", - /* quantity */ 60, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_POWERUP | IF_POWERUP_WHEEL, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_INVISIBILITY, - }, - -/*QUAKED item_silencer (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/items/silencer/tris.md2" -*/ - { - /* id */ IT_POWERUP_SILENCER, - /* classname */ "item_silencer", - /* pickup */ Pickup_TimedItem, - /* use */ Use_Silencer, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/silencer/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "p_silencer", - /* use_name */ "Silencer", - /* pickup_name */ "$item_silencer", - /* pickup_name_definite */ "$item_silencer_def", - /* quantity */ 60, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_TIMED | IF_POWERUP_WHEEL, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_SILENCER, - }, - -/*QUAKED item_breather (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/items/breather/tris.md2" -*/ - { - /* id */ IT_POWERUP_REBREATHER, - /* classname */ "item_breather", - /* pickup */ Pickup_TimedItem, - /* use */ Use_Breather, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/breather/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "p_rebreather", - /* use_name */ "Rebreather", - /* pickup_name */ "$item_rebreather", - /* pickup_name_definite */ "$item_rebreather_def", - /* quantity */ 60, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_STAY_COOP | IF_TIMED | IF_POWERUP_WHEEL, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_REBREATHER, - /* precaches */ "items/airout.wav" - }, - -/*QUAKED item_enviro (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/items/enviro/tris.md2" -*/ - { - /* id */ IT_POWERUP_ENVIROSUIT, - /* classname */ "item_enviro", - /* pickup */ Pickup_TimedItem, - /* use */ Use_Envirosuit, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/enviro/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "p_envirosuit", - /* use_name */ "Environment Suit", - /* pickup_name */ "$item_environment_suit", - /* pickup_name_definite */ "$item_environment_suit_def", - /* quantity */ 60, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_STAY_COOP | IF_TIMED | IF_POWERUP_WHEEL, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_ENVIROSUIT, - /* precaches */ "items/airout.wav" - }, - -/*QUAKED item_ancient_head (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Special item that gives +2 to maximum health -model="models/items/c_head/tris.md2" -*/ - { - /* id */ IT_ANCIENT_HEAD, - /* classname */ "item_ancient_head", - /* pickup */ Pickup_LegacyHead, - /* use */ nullptr, - /* drop */ nullptr, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/c_head/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "i_fixme", - /* use_name */ "Ancient Head", - /* pickup_name */ "$item_ancient_head", - /* pickup_name_definite */ "$item_ancient_head_def", - /* quantity */ 60, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_HEALTH | IF_NOT_RANDOM, - }, - -/*QUAKED item_legacy_head (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Special item that gives +5 to maximum health. -model="models/items/legacyhead/tris.md2" -*/ - { - /* id */ IT_LEGACY_HEAD, - /* classname */ "item_legacy_head", - /* pickup */ Pickup_LegacyHead, - /* use */ nullptr, - /* drop */ nullptr, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/legacyhead/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "i_fixme", - /* use_name */ "Ranger's Head", - /* pickup_name */ "Ranger's Head", - /* pickup_name_definite */ "Ranger's Head", - /* quantity */ 60, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_HEALTH | IF_NOT_RANDOM, - }, - -/*QUAKED item_adrenaline (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Gives +1 to maximum health, +5 in deathmatch. -model="models/items/adrenal/tris.md2" -*/ - { - /* id */ IT_ADRENALINE, - /* classname */ "item_adrenaline", - /* pickup */ Pickup_TimedItem, - /* use */ Use_Adrenaline, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/adrenal/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "p_adrenaline", - /* use_name */ "Adrenaline", - /* pickup_name */ "$item_adrenaline", - /* pickup_name_definite */ "$item_adrenaline_def", - /* quantity */ 60, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_HEALTH | IF_POWERUP_WHEEL, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_ADRENALINE, - /* precache */ "items/n_health.wav" - }, - -/*QUAKED item_bandolier (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/items/band/tris.md2" -*/ - { - /* id */ IT_BANDOLIER, - /* classname */ "item_bandolier", - /* pickup */ Pickup_Bandolier, - /* use */ nullptr, - /* drop */ nullptr, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/band/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "p_bandolier", - /* use_name */ "Bandolier", - /* pickup_name */ "$item_bandolier", - /* pickup_name_definite */ "$item_bandolier_def", - /* quantity */ 60, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_TIMED - }, - -/*QUAKED item_pack (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/items/pack/tris.md2" -*/ - { - /* id */ IT_PACK, - /* classname */ "item_pack", - /* pickup */ Pickup_Pack, - /* use */ nullptr, - /* drop */ nullptr, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/pack/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "i_pack", - /* use_name */ "Ammo Pack", - /* pickup_name */ "$item_ammo_pack", - /* pickup_name_definite */ "$item_ammo_pack_def", - /* quantity */ 180, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_TIMED - }, - -/*QUAKED item_ir_goggles (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Infrared vision. -model="models/items/goggles/tris.md2" -*/ - { - /* id */ IT_IR_GOGGLES, - /* classname */ "item_ir_goggles", - /* pickup */ Pickup_TimedItem, - /* use */ Use_IR, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/goggles/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "p_ir", - /* use_name */ "IR Goggles", - /* pickup_name */ "$item_ir_goggles", - /* pickup_name_definite */ "$item_ir_goggles_def", - /* quantity */ 60, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_TIMED | IF_POWERUP_WHEEL, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_IR_GOGGLES, - /* precaches */ "misc/ir_start.wav" - }, - -/*QUAKED item_double (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/items/ddamage/tris.md2" -*/ - { - /* id */ IT_POWERUP_DOUBLE, - /* classname */ "item_double", - /* pickup */ Pickup_Powerup, - /* use */ Use_Double, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/ddamage/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "p_double", - /* use_name */ "Double Damage", - /* pickup_name */ "$item_double_damage", - /* pickup_name_definite */ "$item_double_damage_def", - /* quantity */ 60, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_POWERUP | IF_POWERUP_WHEEL, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_DOUBLE, - /* precaches */ "misc/ddamage1.wav misc/ddamage2.wav misc/ddamage3.wav ctf/tech2x.wav" - }, - -/*QUAKED item_sphere_vengeance (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/items/vengnce/tris.md2" -*/ - { - /* id */ IT_POWERUP_SPHERE_VENGEANCE, - /* classname */ "item_sphere_vengeance", - /* pickup */ Pickup_Sphere, - /* use */ Use_Vengeance, - /* drop */ nullptr, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/vengnce/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "p_vengeance", - /* use_name */ "vengeance sphere", - /* pickup_name */ "$item_vengeance_sphere", - /* pickup_name_definite */ "$item_vengeance_sphere_def", - /* quantity */ 60, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_SPHERE | IF_POWERUP_WHEEL, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_SPHERE_VENGEANCE, - /* precaches */ "spheres/v_idle.wav" - }, - -/*QUAKED item_sphere_hunter (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/items/hunter/tris.md2" -*/ - { - /* id */ IT_POWERUP_SPHERE_HUNTER, - /* classname */ "item_sphere_hunter", - /* pickup */ Pickup_Sphere, - /* use */ Use_Hunter, - /* drop */ nullptr, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/hunter/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "p_hunter", - /* use_name */ "hunter sphere", - /* pickup_name */ "$item_hunter_sphere", - /* pickup_name_definite */ "$item_hunter_sphere_def", - /* quantity */ 120, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_SPHERE | IF_POWERUP_WHEEL, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_SPHERE_HUNTER, - /* precaches */ "spheres/h_idle.wav spheres/h_active.wav spheres/h_lurk.wav" - }, - -/*QUAKED item_sphere_defender (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/items/defender/tris.md2" -*/ - { - /* id */ IT_POWERUP_SPHERE_DEFENDER, - /* classname */ "item_sphere_defender", - /* pickup */ Pickup_Sphere, - /* use */ Use_Defender, - /* drop */ nullptr, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/defender/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "p_defender", - /* use_name */ "defender sphere", - /* pickup_name */ "$item_defender_sphere", - /* pickup_name_definite */ "$item_defender_sphere_def", - /* quantity */ 60, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_SPHERE | IF_POWERUP_WHEEL, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_SPHERE_DEFENDER, - /* precaches */ "models/objects/laser/tris.md2 models/items/shell/tris.md2 spheres/d_idle.wav" - }, - -/*QUAKED item_doppleganger (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/items/dopple/tris.md2" -*/ - { - /* id */ IT_DOPPELGANGER, - /* classname */ "item_doppleganger", - /* pickup */ Pickup_Doppelganger, - /* use */ Use_Doppelganger, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/dopple/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "p_doppleganger", - /* use_name */ "Doppelganger", - /* pickup_name */ "$item_doppleganger", - /* pickup_name_definite */ "$item_doppleganger_def", - /* quantity */ 90, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_TIMED | IF_POWERUP_WHEEL, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_DOPPELGANGER, - /* precaches */ "models/objects/dopplebase/tris.md2 models/items/spawngro3/tris.md2 medic_commander/monsterspawn1.wav models/items/hunter/tris.md2 models/items/vengnce/tris.md2", - }, - -/* Tag Token */ - { - /* id */ IT_TAG_TOKEN, - /* classname */ nullptr, - /* pickup */ nullptr, - /* use */ nullptr, - /* drop */ nullptr, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/tagtoken/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB | EF_TAGTRAIL, - /* view_model */ nullptr, - /* icon */ "i_tagtoken", - /* use_name */ "Tag Token", - /* pickup_name */ "$item_tag_token", - /* pickup_name_definite */ "$item_tag_token_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_TIMED | IF_NOT_GIVEABLE - }, - - // - // KEYS - // -/*QUAKED key_data_cd (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Key for computer centers. --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/items/keys/data_cd/tris.md2" -*/ - { - /* id */ IT_KEY_DATA_CD, - /* classname */ "key_data_cd", - /* pickup */ Pickup_Key, - /* use */ nullptr, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/keys/data_cd/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "k_datacd", - /* use_name */ "Data CD", - /* pickup_name */ "$item_data_cd", - /* pickup_name_definite */ "$item_data_cd_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_STAY_COOP | IF_KEY - }, - -/*QUAKED key_power_cube (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN NO_TOUCH x x x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Power Cubes for warehouse. --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/items/keys/power/tris.md2" -*/ - { - /* id */ IT_KEY_POWER_CUBE, - /* classname */ "key_power_cube", - /* pickup */ Pickup_Key, - /* use */ nullptr, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/keys/power/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "k_powercube", - /* use_name */ "Power Cube", - /* pickup_name */ "$item_power_cube", - /* pickup_name_definite */ "$item_power_cube_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_STAY_COOP | IF_KEY - }, - -/*QUAKED key_explosive_charges (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN NO_TOUCH x x x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Explosive Charges - for N64. --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/items/n64/charge/tris.md2" -*/ - { - /* id */ IT_KEY_EXPLOSIVE_CHARGES, - /* classname */ "key_explosive_charges", - /* pickup */ Pickup_Key, - /* use */ nullptr, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/n64/charge/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "n64/i_charges", - /* use_name */ "Explosive Charges", - /* pickup_name */ "$item_explosive_charges", - /* pickup_name_definite */ "$item_explosive_charges_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_STAY_COOP | IF_KEY - }, - -/*QUAKED key_yellow_key (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Normal door key - Yellow - for N64. --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/items/n64/yellow_key/tris.md2" -*/ - { - /* id */ IT_KEY_YELLOW, - /* classname */ "key_yellow_key", - /* pickup */ Pickup_Key, - /* use */ nullptr, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/n64/yellow_key/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "n64/i_yellow_key", - /* use_name */ "Yellow Key", - /* pickup_name */ "$item_yellow_key", - /* pickup_name_definite */ "$item_yellow_key_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_STAY_COOP | IF_KEY - }, - -/*QUAKED key_power_core (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Power Core key - for N64. --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/items/n64/power_core/tris.md2" -*/ - { - /* id */ IT_KEY_POWER_CORE, - /* classname */ "key_power_core", - /* pickup */ Pickup_Key, - /* use */ nullptr, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/n64/power_core/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "k_pyramid", - /* use_name */ "Power Core", - /* pickup_name */ "$item_power_core", - /* pickup_name_definite */ "$item_power_core_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_STAY_COOP | IF_KEY - }, - -/*QUAKED key_pyramid (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Key for the entrance of jail3. --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/items/keys/pyramid/tris.md2" -*/ - { - /* id */ IT_KEY_PYRAMID, - /* classname */ "key_pyramid", - /* pickup */ Pickup_Key, - /* use */ nullptr, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/keys/pyramid/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "k_pyramid", - /* use_name */ "Pyramid Key", - /* pickup_name */ "$item_pyramid_key", - /* pickup_name_definite */ "$item_pyramid_key_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_STAY_COOP | IF_KEY - }, - -/*QUAKED key_data_spinner (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Key for the city computer. --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/items/keys/spinner/tris.md2" -*/ - { - /* id */ IT_KEY_DATA_SPINNER, - /* classname */ "key_data_spinner", - /* pickup */ Pickup_Key, - /* use */ nullptr, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/keys/spinner/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "k_dataspin", - /* use_name */ "Data Spinner", - /* pickup_name */ "$item_data_spinner", - /* pickup_name_definite */ "$item_data_spinner_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_STAY_COOP | IF_KEY - }, - -/*QUAKED key_pass (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Security pass for the security level. --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/items/keys/pass/tris.md2" -*/ - { - /* id */ IT_KEY_PASS, - /* classname */ "key_pass", - /* pickup */ Pickup_Key, - /* use */ nullptr, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/keys/pass/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "k_security", - /* use_name */ "Security Pass", - /* pickup_name */ "$item_security_pass", - /* pickup_name_definite */ "$item_security_pass_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_STAY_COOP | IF_KEY - }, - -/*QUAKED key_blue_key (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Normal door key - Blue. --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/items/keys/key/tris.md2" -*/ - { - /* id */ IT_KEY_BLUE_KEY, - /* classname */ "key_blue_key", - /* pickup */ Pickup_Key, - /* use */ nullptr, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/keys/key/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "k_bluekey", - /* use_name */ "Blue Key", - /* pickup_name */ "$item_blue_key", - /* pickup_name_definite */ "$item_blue_key_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_STAY_COOP | IF_KEY - }, - -/*QUAKED key_red_key (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Normal door key - Red. --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/items/keys/red_key/tris.md2" -*/ - { - /* id */ IT_KEY_RED_KEY, - /* classname */ "key_red_key", - /* pickup */ Pickup_Key, - /* use */ nullptr, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/keys/red_key/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "k_redkey", - /* use_name */ "Red Key", - /* pickup_name */ "$item_red_key", - /* pickup_name_definite */ "$item_red_key_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_STAY_COOP | IF_KEY - }, - -/*QUAKED key_green_key (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Normal door key - Green. --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/items/keys/green_key/tris.md2" -*/ - { - /* id */ IT_KEY_GREEN_KEY, - /* classname */ "key_green_key", - /* pickup */ Pickup_Key, - /* use */ nullptr, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/keys/green_key/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "k_green", - /* use_name */ "Green Key", - /* pickup_name */ "$item_green_key", - /* pickup_name_definite */ "$item_green_key_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_STAY_COOP | IF_KEY - }, - -/*QUAKED key_commander_head (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Key - Tank Commander's Head. -model="models/monsters/commandr/head/tris.md2" -*/ - { - /* id */ IT_KEY_COMMANDER_HEAD, - /* classname */ "key_commander_head", - /* pickup */ Pickup_Key, - /* use */ nullptr, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/monsters/commandr/head/tris.md2", - /* world_model_flags */ EF_GIB, - /* view_model */ nullptr, - /* icon */ "k_comhead", - /* use_name */ "Commander's Head", - /* pickup_name */ "$item_commanders_head", - /* pickup_name_definite */ "$item_commanders_head_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_STAY_COOP | IF_KEY - }, - -/*QUAKED key_airstrike_target (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Key - Airstrike Target for strike. -model="models/items/keys/target/tris.md2" -*/ - { - /* id */ IT_KEY_AIRSTRIKE, - /* classname */ "key_airstrike_target", - /* pickup */ Pickup_Key, - /* use */ nullptr, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/keys/target/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "i_airstrike", - /* use_name */ "Airstrike Marker", - /* pickup_name */ "$item_airstrike_marker", - /* pickup_name_definite */ "$item_airstrike_marker_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_STAY_COOP | IF_KEY - }, - -/*QUAKED key_nuke_container (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/weapons/g_nuke/tris.md2" -*/ - { - /* id */ IT_KEY_NUKE_CONTAINER, - /* classname */ "key_nuke_container", - /* pickup */ Pickup_Key, - /* use */ nullptr, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/weapons/g_nuke/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "i_contain", - /* use_name */ "Antimatter Pod", - /* pickup_name */ "$item_antimatter_pod", - /* pickup_name_definite */ "$item_antimatter_pod_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_STAY_COOP | IF_KEY, - }, - -/*QUAKED key_nuke (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/weapons/g_nuke/tris.md2" -*/ - { - /* id */ IT_KEY_NUKE, - /* classname */ "key_nuke", - /* pickup */ Pickup_Key, - /* use */ nullptr, - /* drop */ Drop_General, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/weapons/g_nuke/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "i_nuke", - /* use_name */ "Antimatter Bomb", - /* pickup_name */ "$item_antimatter_bomb", - /* pickup_name_definite */ "$item_antimatter_bomb_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_STAY_COOP | IF_KEY, - }, - -/*QUAKED item_health_small (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Health - Stimpack. --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/items/healing/stimpack/tris.md2" -*/ - // Paril: split the healths up so they are always valid classnames - { - /* id */ IT_HEALTH_SMALL, - /* classname */ "item_health_small", - /* pickup */ Pickup_Health, - /* use */ nullptr, - /* drop */ nullptr, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/s_health.wav", - /* world_model */ "models/items/healing/stimpack/tris.md2", - /* world_model_flags */ EF_NONE, - /* view_model */ nullptr, - /* icon */ "i_health", - /* use_name */ "Health", - /* pickup_name */ "$item_stimpack", - /* pickup_name_definite */ "$item_stimpack_def", - /* quantity */ 2, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_HEALTH, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ HEALTH_IGNORE_MAX - }, - -/*QUAKED item_health (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Health - First Aid. --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/items/healing/medium/tris.md2" -*/ - { - /* id */ IT_HEALTH_MEDIUM, - /* classname */ "item_health", - /* pickup */ Pickup_Health, - /* use */ nullptr, - /* drop */ nullptr, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/n_health.wav", - /* world_model */ "models/items/healing/medium/tris.md2", - /* world_model_flags */ EF_NONE, - /* view_model */ nullptr, - /* icon */ "i_health", - /* use_name */ "Health", - /* pickup_name */ "$item_small_medkit", - /* pickup_name_definite */ "$item_small_medkit_def", - /* quantity */ 10, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_HEALTH - }, - -/*QUAKED item_health_large (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Health - Medkit. --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/items/healing/large/tris.md2" -*/ - { - /* id */ IT_HEALTH_LARGE, - /* classname */ "item_health_large", - /* pickup */ Pickup_Health, - /* use */ nullptr, - /* drop */ nullptr, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/l_health.wav", - /* world_model */ "models/items/healing/large/tris.md2", - /* world_model_flags */ EF_NONE, - /* view_model */ nullptr, - /* icon */ "i_health", - /* use_name */ "Health", - /* pickup_name */ "$item_large_medkit", - /* pickup_name_definite */ "$item_large_medkit", - /* quantity */ 25, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_HEALTH - }, - -/*QUAKED item_health_mega (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Health - Mega Health. --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="models/items/mega_h/tris.md2" -*/ - { - /* id */ IT_HEALTH_MEGA, - /* classname */ "item_health_mega", - /* pickup */ Pickup_Health, - /* use */ nullptr, - /* drop */ nullptr, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/m_health.wav", - /* world_model */ "models/items/mega_h/tris.md2", - /* world_model_flags */ EF_NONE, - /* view_model */ nullptr, - /* icon */ "p_megahealth", - /* use_name */ "Mega Health", - /* pickup_name */ "$item_mega_health", - /* pickup_name_definite */ "$item_mega_health_def", - /* quantity */ 100, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_HEALTH, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ HEALTH_IGNORE_MAX | HEALTH_TIMED - }, - -/*QUAKED item_flag_team_red (1 0.2 0) (-16 -16 -24) (16 16 32) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Red Flag for CTF. --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="players/male/flag1.md2" -*/ - { - /* id */ IT_FLAG_RED, - /* classname */ ITEM_CTF_FLAG_RED, - /* pickup */ CTF_PickupFlag, - /* use */ nullptr, - /* drop */ CTF_DropFlag, //Should this be null if we don't want players to drop it manually? - /* weaponthink */ nullptr, - /* pickup_sound */ "ctf/flagtk.wav", - /* world_model */ "players/male/flag1.md2", - /* world_model_flags */ EF_FLAG_RED, - /* view_model */ nullptr, - /* icon */ "i_ctf1", - /* use_name */ "Red Flag", - /* pickup_name */ "$item_red_flag", - /* pickup_name_definite */ "$item_red_flag_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_NONE, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ 0, - /* precaches */ "ctf/flagcap.wav" - }, - -/*QUAKED item_flag_team_blue (1 0.2 0) (-16 -16 -24) (16 16 32) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Blue Flag for CTF. --------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- -model="players/male/flag2.md2" -*/ - { - /* id */ IT_FLAG_BLUE, - /* classname */ ITEM_CTF_FLAG_BLUE, - /* pickup */ CTF_PickupFlag, - /* use */ nullptr, - /* drop */ CTF_DropFlag, - /* weaponthink */ nullptr, - /* pickup_sound */ "ctf/flagtk.wav", - /* world_model */ "players/male/flag2.md2", - /* world_model_flags */ EF_FLAG_BLUE, - /* view_model */ nullptr, - /* icon */ "i_ctf2", - /* use_name */ "Blue Flag", - /* pickup_name */ "$item_blue_flag", - /* pickup_name_definite */ "$item_blue_flag_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_NONE, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ 0, - /* precaches */ "ctf/flagcap.wav" - }, - -/* Disruptor Shield Tech */ - { - /* id */ IT_TECH_DISRUPTOR_SHIELD, - /* classname */ "item_tech1", - /* pickup */ Tech_Pickup, - /* use */ nullptr, - /* drop */ Tech_Drop, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/ctf/resistance/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "tech1", - /* use_name */ "Disruptor Shield", - /* pickup_name */ "$item_disruptor_shield", - /* pickup_name_definite */ "$item_disruptor_shield_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_TECH | IF_POWERUP_WHEEL, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_TECH_DISRUPTOR_SHIELD, - /* precaches */ "ctf/tech1.wav" - }, - -/* Power Amplifier Tech */ - { - /* id */ IT_TECH_POWER_AMP, - /* classname */ "item_tech2", - /* pickup */ Tech_Pickup, - /* use */ nullptr, - /* drop */ Tech_Drop, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/ctf/strength/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "tech2", - /* use_name */ "Power Amplifier", - /* pickup_name */ "$item_power_amplifier", - /* pickup_name_definite */ "$item_power_amplifier_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_TECH | IF_POWERUP_WHEEL, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_TECH_POWER_AMP, - /* precaches */ "ctf/tech2.wav ctf/tech2x.wav" - }, - -/* Time Accel Tech */ - { - /* id */ IT_TECH_TIME_ACCEL, - /* classname */ "item_tech3", - /* pickup */ Tech_Pickup, - /* use */ nullptr, - /* drop */ Tech_Drop, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/ctf/haste/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "tech3", - /* use_name */ "Time Accel", - /* pickup_name */ "$item_time_accel", - /* pickup_name_definite */ "$item_time_accel_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_TECH | IF_POWERUP_WHEEL, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_TECH_TIME_ACCEL, - /* precaches */ "ctf/tech3.wav" - }, - -/* AutoDoc Tech */ - { - /* id */ IT_TECH_AUTODOC, - /* classname */ "item_tech4", - /* pickup */ Tech_Pickup, - /* use */ nullptr, - /* drop */ Tech_Drop, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/ctf/regeneration/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "tech4", - /* use_name */ "AutoDoc", - /* pickup_name */ "$item_autodoc", - /* pickup_name_definite */ "$item_autodoc_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_TECH | IF_POWERUP_WHEEL, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_TECH_AUTODOC, - /* precaches */ "ctf/tech4.wav" - }, - -/*QUAKED ammo_shells_large (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/vault/items/ammo/shells/large/tris.md2" -*/ - { - /* id */ IT_AMMO_SHELLS_LARGE , - /* classname */ "ammo_shells_large", - /* pickup */ Pickup_Ammo, - /* use */ nullptr, - /* drop */ Drop_Ammo, - /* weaponthink */ nullptr, - /* pickup_sound */ "misc/am_pkup.wav", - /* world_model */ "models/vault/items/ammo/shells/large/tris.md2", - /* world_model_flags */ EF_NONE, - /* view_model */ nullptr, - /* icon */ "a_shells", - /* use_name */ "Large Shells", - /* pickup_name */ "Large Shells", - /* pickup_name_definite */ "Large Shells", - /* quantity */ 20, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_AMMO, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ AMMO_SHELLS - }, - -/*QUAKED ammo_shells_small (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/vault/items/ammo/shells/small/tris.md2" -*/ - { - /* id */ IT_AMMO_SHELLS_SMALL, - /* classname */ "ammo_shells_small", - /* pickup */ Pickup_Ammo, - /* use */ nullptr, - /* drop */ Drop_Ammo, - /* weaponthink */ nullptr, - /* pickup_sound */ "misc/am_pkup.wav", - /* world_model */ "models/vault/items/ammo/shells/small/tris.md2", - /* world_model_flags */ EF_NONE, - /* view_model */ nullptr, - /* icon */ "a_shells", - /* use_name */ "Small Shells", - /* pickup_name */ "Small Shells", - /* pickup_name_definite */ "Small Shells", - /* quantity */ 6, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_AMMO, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ AMMO_SHELLS - }, - -/*QUAKED ammo_bullets_large (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/vault/items/ammo/bullets/large/tris.md2" -*/ - { - /* id */ IT_AMMO_BULLETS_LARGE, - /* classname */ "ammo_bullets_large", - /* pickup */ Pickup_Ammo, - /* use */ nullptr, - /* drop */ Drop_Ammo, - /* weaponthink */ nullptr, - /* pickup_sound */ "misc/am_pkup.wav", - /* world_model */ "models/vault/items/ammo/bullets/large/tris.md2", - /* world_model_flags */ EF_NONE, - /* view_model */ nullptr, - /* icon */ "a_bullets", - /* use_name */ "Large Bullets", - /* pickup_name */ "Large Bullets", - /* pickup_name_definite */ "Large Bullets", - /* quantity */ 100, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_AMMO, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ AMMO_BULLETS - }, - -/*QUAKED ammo_bullets_small (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/vault/items/ammo/bullets/small/tris.md2" -*/ - { - /* id */ IT_AMMO_BULLETS_SMALL, - /* classname */ "ammo_bullets_small", - /* pickup */ Pickup_Ammo, - /* use */ nullptr, - /* drop */ Drop_Ammo, - /* weaponthink */ nullptr, - /* pickup_sound */ "misc/am_pkup.wav", - /* world_model */ "models/vault/items/ammo/bullets/small/tris.md2", - /* world_model_flags */ EF_NONE, - /* view_model */ nullptr, - /* icon */ "a_bullets", - /* use_name */ "Small Bullets", - /* pickup_name */ "Small Bullets", - /* pickup_name_definite */ "Small Bullets", - /* quantity */ 16, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_AMMO, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ AMMO_BULLETS - }, - -/*QUAKED ammo_cells_large (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/vault/items/ammo/cells/large/tris.md2" -*/ - { - /* id */ IT_AMMO_CELLS_LARGE, - /* classname */ "ammo_cells_large", - /* pickup */ Pickup_Ammo, - /* use */ nullptr, - /* drop */ Drop_Ammo, - /* weaponthink */ nullptr, - /* pickup_sound */ "misc/am_pkup.wav", - /* world_model */ "models/vault/items/ammo/cells/large/tris.md2", - /* world_model_flags */ EF_NONE, - /* view_model */ nullptr, - /* icon */ "a_cells", - /* use_name */ "Large Cells", - /* pickup_name */ "Large Cells", - /* pickup_name_definite */ "Large Cells", - /* quantity */ 100, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_AMMO, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ AMMO_CELLS - }, - -/*QUAKED ammo_cells_small (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/vault/items/ammo/cells/small/tris.md2" -*/ - { - /* id */ IT_AMMO_CELLS_SMALL, - /* classname */ "ammo_cells_small", - /* pickup */ Pickup_Ammo, - /* use */ nullptr, - /* drop */ Drop_Ammo, - /* weaponthink */ nullptr, - /* pickup_sound */ "misc/am_pkup.wav", - /* world_model */ "models/vault/items/ammo/cells/small/tris.md2", - /* world_model_flags */ EF_NONE, - /* view_model */ nullptr, - /* icon */ "a_cells", - /* use_name */ "Small Cells", - /* pickup_name */ "Small Cells", - /* pickup_name_definite */ "Small Cells", - /* quantity */ 20, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_AMMO, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ AMMO_CELLS - }, - -/*QUAKED ammo_rockets_small (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/vault/items/ammo/rockets/small/tris.md2" -*/ - { - /* id */ IT_AMMO_ROCKETS_SMALL, - /* classname */ "ammo_rockets_small", - /* pickup */ Pickup_Ammo, - /* use */ nullptr, - /* drop */ Drop_Ammo, - /* weaponthink */ nullptr, - /* pickup_sound */ "misc/am_pkup.wav", - /* world_model */ "models/vault/items/ammo/rockets/small/tris.md2", - /* world_model_flags */ EF_NONE, - /* view_model */ nullptr, - /* icon */ "a_rockets", - /* use_name */ "Small Rockets", - /* pickup_name */ "Small Rockets", - /* pickup_name_definite */ "Small Rockets", - /* quantity */ 2, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_AMMO, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ AMMO_ROCKETS - }, - -/*QUAKED ammo_slugs_large (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/vault/items/ammo/slugs/large/tris.md2" -*/ - { - /* id */ IT_AMMO_SLUGS_LARGE, - /* classname */ "ammo_slugs_large", - /* pickup */ Pickup_Ammo, - /* use */ nullptr, - /* drop */ Drop_Ammo, - /* weaponthink */ nullptr, - /* pickup_sound */ "misc/am_pkup.wav", - /* world_model */ "models/vault/items/ammo/slugs/large/tris.md2", - /* world_model_flags */ EF_NONE, - /* view_model */ nullptr, - /* icon */ "a_slugs", - /* use_name */ "Large Slugs", - /* pickup_name */ "Large Slugs", - /* pickup_name_definite */ "Large Slugs", - /* quantity */ 20, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_AMMO, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ AMMO_SLUGS - }, - -/*QUAKED ammo_slugs_small (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/vault/items/ammo/slugs/small/tris.md2" -*/ - { - /* id */ IT_AMMO_SLUGS_SMALL, - /* classname */ "ammo_slugs_small", - /* pickup */ Pickup_Ammo, + /* id */ IT_KEY_DATA_CD, + /* classname */ "key_data_cd", + /* pickup */ Pickup_Key, /* use */ nullptr, - /* drop */ Drop_Ammo, - /* weaponthink */ nullptr, - /* pickup_sound */ "misc/am_pkup.wav", - /* world_model */ "models/vault/items/ammo/slugs/small/tris.md2", - /* world_model_flags */ EF_NONE, - /* view_model */ nullptr, - /* icon */ "a_slugs", - /* use_name */ "Small Slugs", - /* pickup_name */ "Small Slugs", - /* pickup_name_definite */ "Small Slugs", - /* quantity */ 3, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_AMMO, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ AMMO_SLUGS - }, - -/*QUAKED item_teleporter (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/vault/items/ammo/nuke/tris.md2" -*/ - { - /* id */ IT_TELEPORTER, - /* classname */ "item_teleporter", - /* pickup */ Pickup_Teleporter, - /* use */ Use_Teleporter, - /* drop */ nullptr, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/vault/items/ammo/nuke/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "i_fixme", - /* use_name */ "Personal Teleporter", - /* pickup_name */ "Personal Teleporter", - /* pickup_name_definite */ "Personal Teleporter", - /* quantity */ 120, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_TIMED | IF_POWERUP_WHEEL | IF_POWERUP_ONOFF - }, - -/*QUAKED item_regen (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -model="models/items/invulner/tris.md2" -*/ - { - /* id */ IT_POWERUP_REGEN, - /* classname */ "item_regen", - /* pickup */ Pickup_Powerup, - /* use */ Use_Regeneration, /* drop */ Drop_General, /* weaponthink */ nullptr, /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/invulner/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "i_fixme", - /* use_name */ "Regeneration", - /* pickup_name */ "Regeneration", - /* pickup_name_definite */ "Regeneration", - /* quantity */ 60, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_POWERUP | IF_POWERUP_WHEEL, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_REGEN, - /* precaches */ "items/protect.wav" - }, - -/*QUAKED item_foodcube (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Meaty cube o' health -model="models/objects/trapfx/tris.md2" -*/ - { - /* id */ IT_FOODCUBE, - /* classname */ "item_foodcube", - /* pickup */ Pickup_Health, - /* use */ nullptr, - /* drop */ nullptr, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/n_health.wav", - /* world_model */ "models/objects/trapfx/tris.md2", - /* world_model_flags */ EF_GIB, - /* view_model */ nullptr, - /* icon */ "i_health", - /* use_name */ "Meaty Cube", - /* pickup_name */ "Meaty Cube", - /* pickup_name_definite */ "Meaty Cube", - /* quantity */ 50, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_HEALTH, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ HEALTH_IGNORE_MAX - }, - -/*QUAKED item_ball (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP -Big ol' ball -models/items/ammo/grenades/medium/tris.md2" -*/ - { - /* id */ IT_BALL, - /* classname */ "item_ball", - /* pickup */ Pickup_Ball, - /* use */ Use_Ball, - /* drop */ Drop_Ball, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/ammo/grenades/medium/tris.md2", - /* world_model_flags */ EF_ROTATE | EF_BOB, - /* view_model */ nullptr, - /* icon */ "i_help", - /* use_name */ "Ball", - /* pickup_name */ "Ball", - /* pickup_name_definite */ "Ball", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_STAY_COOP | IF_POWERUP| IF_POWERUP_WHEEL | IF_NOT_RANDOM, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_BALL, - /* precaches */ "", - /* sort_id */ -1 - }, - -/* Flashlight */ - { - /* id */ IT_FLASHLIGHT, - /* classname */ "item_flashlight", - /* pickup */ Pickup_General, - /* use */ Use_Flashlight, - /* drop */ nullptr, - /* weaponthink */ nullptr, - /* pickup_sound */ "items/pkup.wav", - /* world_model */ "models/items/flashlight/tris.md2", + /* world_model */ "models/items/keys/data_cd/tris.md2", /* world_model_flags */ EF_ROTATE | EF_BOB, /* view_model */ nullptr, - /* icon */ "p_torch", - /* use_name */ "Flashlight", - /* pickup_name */ "$item_flashlight", - /* pickup_name_definite */ "$item_flashlight_def", + /* icon */ "k_datacd", + /* use_name */ "Data CD", + /* pickup_name */ "$item_data_cd", + /* pickup_name_definite */ "$item_data_cd_def", /* quantity */ 0, /* ammo */ IT_NULL, /* chain */ IT_NULL, - /* flags */ IF_STAY_COOP | IF_POWERUP_WHEEL | IF_POWERUP_ONOFF | IF_NOT_RANDOM, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_FLASHLIGHT, - /* precaches */ "items/flashlight_on.wav items/flashlight_off.wav", - /* sort_id */ -1 + /* flags */ IF_STAY_COOP | IF_KEY }, -/* Compass */ - { - /* id */ IT_COMPASS, - /* classname */ "item_compass", - /* pickup */ nullptr, - /* use */ Use_Compass, - /* drop */ nullptr, - /* weaponthink */ nullptr, - /* pickup_sound */ nullptr, - /* world_model */ nullptr, - /* world_model_flags */ EF_NONE, - /* view_model */ nullptr, - /* icon */ "p_compass", - /* use_name */ "Compass", - /* pickup_name */ "$item_compass", - /* pickup_name_definite */ "$item_compass_def", - /* quantity */ 0, - /* ammo */ IT_NULL, - /* chain */ IT_NULL, - /* flags */ IF_STAY_COOP | IF_POWERUP_WHEEL | IF_POWERUP_ONOFF, - /* vwep_model */ nullptr, - /* armor_info */ nullptr, - /* tag */ POWERUP_COMPASS, - /* precaches */ "misc/help_marker.wav", - /* sort_id */ -2 - }, + /*QUAKED key_power_cube (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN NO_TOUCH x x x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + Power Cubes for warehouse. + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/items/keys/power/tris.md2" + */ + { + /* id */ IT_KEY_POWER_CUBE, + /* classname */ "key_power_cube", + /* pickup */ Pickup_Key, + /* use */ nullptr, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/keys/power/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "k_powercube", + /* use_name */ "Power Cube", + /* pickup_name */ "$item_power_cube", + /* pickup_name_definite */ "$item_power_cube_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_STAY_COOP | IF_KEY + }, + + /*QUAKED key_explosive_charges (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN NO_TOUCH x x x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + Explosive Charges - for N64. + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/items/n64/charge/tris.md2" + */ + { + /* id */ IT_KEY_EXPLOSIVE_CHARGES, + /* classname */ "key_explosive_charges", + /* pickup */ Pickup_Key, + /* use */ nullptr, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/n64/charge/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "n64/i_charges", + /* use_name */ "Explosive Charges", + /* pickup_name */ "$item_explosive_charges", + /* pickup_name_definite */ "$item_explosive_charges_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_STAY_COOP | IF_KEY + }, + + /*QUAKED key_yellow_key (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + Normal door key - Yellow - for N64. + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/items/n64/yellow_key/tris.md2" + */ + { + /* id */ IT_KEY_YELLOW, + /* classname */ "key_yellow_key", + /* pickup */ Pickup_Key, + /* use */ nullptr, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/n64/yellow_key/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "n64/i_yellow_key", + /* use_name */ "Yellow Key", + /* pickup_name */ "$item_yellow_key", + /* pickup_name_definite */ "$item_yellow_key_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_STAY_COOP | IF_KEY + }, + + /*QUAKED key_power_core (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + Power Core key - for N64. + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/items/n64/power_core/tris.md2" + */ + { + /* id */ IT_KEY_POWER_CORE, + /* classname */ "key_power_core", + /* pickup */ Pickup_Key, + /* use */ nullptr, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/n64/power_core/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "k_pyramid", + /* use_name */ "Power Core", + /* pickup_name */ "$item_power_core", + /* pickup_name_definite */ "$item_power_core_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_STAY_COOP | IF_KEY + }, + + /*QUAKED key_pyramid (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + Key for the entrance of jail3. + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/items/keys/pyramid/tris.md2" + */ + { + /* id */ IT_KEY_PYRAMID, + /* classname */ "key_pyramid", + /* pickup */ Pickup_Key, + /* use */ nullptr, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/keys/pyramid/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "k_pyramid", + /* use_name */ "Pyramid Key", + /* pickup_name */ "$item_pyramid_key", + /* pickup_name_definite */ "$item_pyramid_key_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_STAY_COOP | IF_KEY + }, + + /*QUAKED key_data_spinner (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + Key for the city computer. + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/items/keys/spinner/tris.md2" + */ + { + /* id */ IT_KEY_DATA_SPINNER, + /* classname */ "key_data_spinner", + /* pickup */ Pickup_Key, + /* use */ nullptr, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/keys/spinner/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "k_dataspin", + /* use_name */ "Data Spinner", + /* pickup_name */ "$item_data_spinner", + /* pickup_name_definite */ "$item_data_spinner_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_STAY_COOP | IF_KEY + }, + + /*QUAKED key_pass (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + Security pass for the security level. + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/items/keys/pass/tris.md2" + */ + { + /* id */ IT_KEY_PASS, + /* classname */ "key_pass", + /* pickup */ Pickup_Key, + /* use */ nullptr, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/keys/pass/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "k_security", + /* use_name */ "Security Pass", + /* pickup_name */ "$item_security_pass", + /* pickup_name_definite */ "$item_security_pass_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_STAY_COOP | IF_KEY + }, + + /*QUAKED key_blue_key (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + Normal door key - Blue. + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/items/keys/key/tris.md2" + */ + { + /* id */ IT_KEY_BLUE_KEY, + /* classname */ "key_blue_key", + /* pickup */ Pickup_Key, + /* use */ nullptr, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/keys/key/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "k_bluekey", + /* use_name */ "Blue Key", + /* pickup_name */ "$item_blue_key", + /* pickup_name_definite */ "$item_blue_key_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_STAY_COOP | IF_KEY + }, + + /*QUAKED key_red_key (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + Normal door key - Red. + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/items/keys/red_key/tris.md2" + */ + { + /* id */ IT_KEY_RED_KEY, + /* classname */ "key_red_key", + /* pickup */ Pickup_Key, + /* use */ nullptr, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/keys/red_key/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "k_redkey", + /* use_name */ "Red Key", + /* pickup_name */ "$item_red_key", + /* pickup_name_definite */ "$item_red_key_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_STAY_COOP | IF_KEY + }, + + /*QUAKED key_green_key (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + Normal door key - Green. + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/items/keys/green_key/tris.md2" + */ + { + /* id */ IT_KEY_GREEN_KEY, + /* classname */ "key_green_key", + /* pickup */ Pickup_Key, + /* use */ nullptr, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/keys/green_key/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "k_green", + /* use_name */ "Green Key", + /* pickup_name */ "$item_green_key", + /* pickup_name_definite */ "$item_green_key_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_STAY_COOP | IF_KEY + }, + + /*QUAKED key_commander_head (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + Key - Tank Commander's Head. + model="models/monsters/commandr/head/tris.md2" + */ + { + /* id */ IT_KEY_COMMANDER_HEAD, + /* classname */ "key_commander_head", + /* pickup */ Pickup_Key, + /* use */ nullptr, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/monsters/commandr/head/tris.md2", + /* world_model_flags */ EF_GIB, + /* view_model */ nullptr, + /* icon */ "k_comhead", + /* use_name */ "Commander's Head", + /* pickup_name */ "$item_commanders_head", + /* pickup_name_definite */ "$item_commanders_head_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_STAY_COOP | IF_KEY + }, + + /*QUAKED key_airstrike_target (0 .5 .8) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + Key - Airstrike Target for strike. + model="models/items/keys/target/tris.md2" + */ + { + /* id */ IT_KEY_AIRSTRIKE, + /* classname */ "key_airstrike_target", + /* pickup */ Pickup_Key, + /* use */ nullptr, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/keys/target/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "i_airstrike", + /* use_name */ "Airstrike Marker", + /* pickup_name */ "$item_airstrike_marker", + /* pickup_name_definite */ "$item_airstrike_marker_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_STAY_COOP | IF_KEY + }, + + /*QUAKED key_nuke_container (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/weapons/g_nuke/tris.md2" + */ + { + /* id */ IT_KEY_NUKE_CONTAINER, + /* classname */ "key_nuke_container", + /* pickup */ Pickup_Key, + /* use */ nullptr, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/weapons/g_nuke/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "i_contain", + /* use_name */ "Antimatter Pod", + /* pickup_name */ "$item_antimatter_pod", + /* pickup_name_definite */ "$item_antimatter_pod_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_STAY_COOP | IF_KEY, + }, + + /*QUAKED key_nuke (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/weapons/g_nuke/tris.md2" + */ + { + /* id */ IT_KEY_NUKE, + /* classname */ "key_nuke", + /* pickup */ Pickup_Key, + /* use */ nullptr, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/weapons/g_nuke/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "i_nuke", + /* use_name */ "Antimatter Bomb", + /* pickup_name */ "$item_antimatter_bomb", + /* pickup_name_definite */ "$item_antimatter_bomb_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_STAY_COOP | IF_KEY, + }, + + /*QUAKED item_health_small (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + Health - Stimpack. + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/items/healing/stimpack/tris.md2" + */ + // Paril: split the healths up so they are always valid classnames + { + /* id */ IT_HEALTH_SMALL, + /* classname */ "item_health_small", + /* pickup */ Pickup_Health, + /* use */ nullptr, + /* drop */ nullptr, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/s_health.wav", + /* world_model */ "models/items/healing/stimpack/tris.md2", + /* world_model_flags */ EF_NONE, + /* view_model */ nullptr, + /* icon */ "i_health", + /* use_name */ "Health", + /* pickup_name */ "$item_stimpack", + /* pickup_name_definite */ "$item_stimpack_def", + /* quantity */ 2, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_HEALTH, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ HEALTH_IGNORE_MAX + }, + + /*QUAKED item_health (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + Health - First Aid. + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/items/healing/medium/tris.md2" + */ + { + /* id */ IT_HEALTH_MEDIUM, + /* classname */ "item_health", + /* pickup */ Pickup_Health, + /* use */ nullptr, + /* drop */ nullptr, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/n_health.wav", + /* world_model */ "models/items/healing/medium/tris.md2", + /* world_model_flags */ EF_NONE, + /* view_model */ nullptr, + /* icon */ "i_health", + /* use_name */ "Health", + /* pickup_name */ "$item_small_medkit", + /* pickup_name_definite */ "$item_small_medkit_def", + /* quantity */ 10, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_HEALTH + }, + + /*QUAKED item_health_large (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + Health - Medkit. + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/items/healing/large/tris.md2" + */ + { + /* id */ IT_HEALTH_LARGE, + /* classname */ "item_health_large", + /* pickup */ Pickup_Health, + /* use */ nullptr, + /* drop */ nullptr, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/l_health.wav", + /* world_model */ "models/items/healing/large/tris.md2", + /* world_model_flags */ EF_NONE, + /* view_model */ nullptr, + /* icon */ "i_health", + /* use_name */ "Health", + /* pickup_name */ "$item_large_medkit", + /* pickup_name_definite */ "$item_large_medkit", + /* quantity */ 25, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_HEALTH + }, + + /*QUAKED item_health_mega (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + Health - Mega Health. + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="models/items/mega_h/tris.md2" + */ + { + /* id */ IT_HEALTH_MEGA, + /* classname */ "item_health_mega", + /* pickup */ Pickup_Health, + /* use */ nullptr, + /* drop */ nullptr, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/m_health.wav", + /* world_model */ "models/items/mega_h/tris.md2", + /* world_model_flags */ EF_NONE, + /* view_model */ nullptr, + /* icon */ "p_megahealth", + /* use_name */ "Mega Health", + /* pickup_name */ "$item_mega_health", + /* pickup_name_definite */ "$item_mega_health_def", + /* quantity */ 100, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_HEALTH, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ HEALTH_IGNORE_MAX | HEALTH_TIMED + }, + + /*QUAKED item_flag_team_red (1 0.2 0) (-16 -16 -24) (16 16 32) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + Red Flag for CTF. + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="players/male/flag1.md2" + */ + { + /* id */ IT_FLAG_RED, + /* classname */ ITEM_CTF_FLAG_RED, + /* pickup */ CTF_PickupFlag, + /* use */ nullptr, + /* drop */ CTF_DropFlag, //Should this be null if we don't want players to drop it manually? + /* weaponthink */ nullptr, + /* pickup_sound */ "ctf/flagtk.wav", + /* world_model */ "players/male/flag1.md2", + /* world_model_flags */ EF_FLAG_RED, + /* view_model */ nullptr, + /* icon */ "i_ctf1", + /* use_name */ "Red Flag", + /* pickup_name */ "$item_red_flag", + /* pickup_name_definite */ "$item_red_flag_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_NONE, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ 0, + /* precaches */ "ctf/flagcap.wav" + }, + + /*QUAKED item_flag_team_blue (1 0.2 0) (-16 -16 -24) (16 16 32) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + Blue Flag for CTF. + -------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY -------- + model="players/male/flag2.md2" + */ + { + /* id */ IT_FLAG_BLUE, + /* classname */ ITEM_CTF_FLAG_BLUE, + /* pickup */ CTF_PickupFlag, + /* use */ nullptr, + /* drop */ CTF_DropFlag, + /* weaponthink */ nullptr, + /* pickup_sound */ "ctf/flagtk.wav", + /* world_model */ "players/male/flag2.md2", + /* world_model_flags */ EF_FLAG_BLUE, + /* view_model */ nullptr, + /* icon */ "i_ctf2", + /* use_name */ "Blue Flag", + /* pickup_name */ "$item_blue_flag", + /* pickup_name_definite */ "$item_blue_flag_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_NONE, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ 0, + /* precaches */ "ctf/flagcap.wav" + }, + + /* Disruptor Shield Tech */ + { + /* id */ IT_TECH_DISRUPTOR_SHIELD, + /* classname */ "item_tech1", + /* pickup */ Tech_Pickup, + /* use */ nullptr, + /* drop */ Tech_Drop, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/ctf/resistance/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "tech1", + /* use_name */ "Disruptor Shield", + /* pickup_name */ "$item_disruptor_shield", + /* pickup_name_definite */ "$item_disruptor_shield_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_TECH | IF_POWERUP_WHEEL, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_TECH_DISRUPTOR_SHIELD, + /* precaches */ "ctf/tech1.wav" + }, + + /* Power Amplifier Tech */ + { + /* id */ IT_TECH_POWER_AMP, + /* classname */ "item_tech2", + /* pickup */ Tech_Pickup, + /* use */ nullptr, + /* drop */ Tech_Drop, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/ctf/strength/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "tech2", + /* use_name */ "Power Amplifier", + /* pickup_name */ "$item_power_amplifier", + /* pickup_name_definite */ "$item_power_amplifier_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_TECH | IF_POWERUP_WHEEL, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_TECH_POWER_AMP, + /* precaches */ "ctf/tech2.wav ctf/tech2x.wav" + }, + + /* Time Accel Tech */ + { + /* id */ IT_TECH_TIME_ACCEL, + /* classname */ "item_tech3", + /* pickup */ Tech_Pickup, + /* use */ nullptr, + /* drop */ Tech_Drop, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/ctf/haste/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "tech3", + /* use_name */ "Time Accel", + /* pickup_name */ "$item_time_accel", + /* pickup_name_definite */ "$item_time_accel_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_TECH | IF_POWERUP_WHEEL, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_TECH_TIME_ACCEL, + /* precaches */ "ctf/tech3.wav" + }, + + /* AutoDoc Tech */ + { + /* id */ IT_TECH_AUTODOC, + /* classname */ "item_tech4", + /* pickup */ Tech_Pickup, + /* use */ nullptr, + /* drop */ Tech_Drop, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/ctf/regeneration/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "tech4", + /* use_name */ "AutoDoc", + /* pickup_name */ "$item_autodoc", + /* pickup_name_definite */ "$item_autodoc_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_TECH | IF_POWERUP_WHEEL, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_TECH_AUTODOC, + /* precaches */ "ctf/tech4.wav" + }, + + /*QUAKED ammo_shells_large (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/vault/items/ammo/shells/large/tris.md2" + */ + { + /* id */ IT_AMMO_SHELLS_LARGE , + /* classname */ "ammo_shells_large", + /* pickup */ Pickup_Ammo, + /* use */ nullptr, + /* drop */ Drop_Ammo, + /* weaponthink */ nullptr, + /* pickup_sound */ "misc/am_pkup.wav", + /* world_model */ "models/vault/items/ammo/shells/large/tris.md2", + /* world_model_flags */ EF_NONE, + /* view_model */ nullptr, + /* icon */ "a_shells", + /* use_name */ "Large Shells", + /* pickup_name */ "Large Shells", + /* pickup_name_definite */ "Large Shells", + /* quantity */ 20, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_AMMO, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ AMMO_SHELLS + }, + + /*QUAKED ammo_shells_small (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/vault/items/ammo/shells/small/tris.md2" + */ + { + /* id */ IT_AMMO_SHELLS_SMALL, + /* classname */ "ammo_shells_small", + /* pickup */ Pickup_Ammo, + /* use */ nullptr, + /* drop */ Drop_Ammo, + /* weaponthink */ nullptr, + /* pickup_sound */ "misc/am_pkup.wav", + /* world_model */ "models/vault/items/ammo/shells/small/tris.md2", + /* world_model_flags */ EF_NONE, + /* view_model */ nullptr, + /* icon */ "a_shells", + /* use_name */ "Small Shells", + /* pickup_name */ "Small Shells", + /* pickup_name_definite */ "Small Shells", + /* quantity */ 6, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_AMMO, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ AMMO_SHELLS + }, + + /*QUAKED ammo_bullets_large (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/vault/items/ammo/bullets/large/tris.md2" + */ + { + /* id */ IT_AMMO_BULLETS_LARGE, + /* classname */ "ammo_bullets_large", + /* pickup */ Pickup_Ammo, + /* use */ nullptr, + /* drop */ Drop_Ammo, + /* weaponthink */ nullptr, + /* pickup_sound */ "misc/am_pkup.wav", + /* world_model */ "models/vault/items/ammo/bullets/large/tris.md2", + /* world_model_flags */ EF_NONE, + /* view_model */ nullptr, + /* icon */ "a_bullets", + /* use_name */ "Large Bullets", + /* pickup_name */ "Large Bullets", + /* pickup_name_definite */ "Large Bullets", + /* quantity */ 100, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_AMMO, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ AMMO_BULLETS + }, + + /*QUAKED ammo_bullets_small (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/vault/items/ammo/bullets/small/tris.md2" + */ + { + /* id */ IT_AMMO_BULLETS_SMALL, + /* classname */ "ammo_bullets_small", + /* pickup */ Pickup_Ammo, + /* use */ nullptr, + /* drop */ Drop_Ammo, + /* weaponthink */ nullptr, + /* pickup_sound */ "misc/am_pkup.wav", + /* world_model */ "models/vault/items/ammo/bullets/small/tris.md2", + /* world_model_flags */ EF_NONE, + /* view_model */ nullptr, + /* icon */ "a_bullets", + /* use_name */ "Small Bullets", + /* pickup_name */ "Small Bullets", + /* pickup_name_definite */ "Small Bullets", + /* quantity */ 16, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_AMMO, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ AMMO_BULLETS + }, + + /*QUAKED ammo_cells_large (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/vault/items/ammo/cells/large/tris.md2" + */ + { + /* id */ IT_AMMO_CELLS_LARGE, + /* classname */ "ammo_cells_large", + /* pickup */ Pickup_Ammo, + /* use */ nullptr, + /* drop */ Drop_Ammo, + /* weaponthink */ nullptr, + /* pickup_sound */ "misc/am_pkup.wav", + /* world_model */ "models/vault/items/ammo/cells/large/tris.md2", + /* world_model_flags */ EF_NONE, + /* view_model */ nullptr, + /* icon */ "a_cells", + /* use_name */ "Large Cells", + /* pickup_name */ "Large Cells", + /* pickup_name_definite */ "Large Cells", + /* quantity */ 100, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_AMMO, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ AMMO_CELLS + }, + + /*QUAKED ammo_cells_small (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/vault/items/ammo/cells/small/tris.md2" + */ + { + /* id */ IT_AMMO_CELLS_SMALL, + /* classname */ "ammo_cells_small", + /* pickup */ Pickup_Ammo, + /* use */ nullptr, + /* drop */ Drop_Ammo, + /* weaponthink */ nullptr, + /* pickup_sound */ "misc/am_pkup.wav", + /* world_model */ "models/vault/items/ammo/cells/small/tris.md2", + /* world_model_flags */ EF_NONE, + /* view_model */ nullptr, + /* icon */ "a_cells", + /* use_name */ "Small Cells", + /* pickup_name */ "Small Cells", + /* pickup_name_definite */ "Small Cells", + /* quantity */ 20, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_AMMO, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ AMMO_CELLS + }, + + /*QUAKED ammo_rockets_small (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/vault/items/ammo/rockets/small/tris.md2" + */ + { + /* id */ IT_AMMO_ROCKETS_SMALL, + /* classname */ "ammo_rockets_small", + /* pickup */ Pickup_Ammo, + /* use */ nullptr, + /* drop */ Drop_Ammo, + /* weaponthink */ nullptr, + /* pickup_sound */ "misc/am_pkup.wav", + /* world_model */ "models/vault/items/ammo/rockets/small/tris.md2", + /* world_model_flags */ EF_NONE, + /* view_model */ nullptr, + /* icon */ "a_rockets", + /* use_name */ "Small Rockets", + /* pickup_name */ "Small Rockets", + /* pickup_name_definite */ "Small Rockets", + /* quantity */ 2, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_AMMO, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ AMMO_ROCKETS + }, + + /*QUAKED ammo_slugs_large (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/vault/items/ammo/slugs/large/tris.md2" + */ + { + /* id */ IT_AMMO_SLUGS_LARGE, + /* classname */ "ammo_slugs_large", + /* pickup */ Pickup_Ammo, + /* use */ nullptr, + /* drop */ Drop_Ammo, + /* weaponthink */ nullptr, + /* pickup_sound */ "misc/am_pkup.wav", + /* world_model */ "models/vault/items/ammo/slugs/large/tris.md2", + /* world_model_flags */ EF_NONE, + /* view_model */ nullptr, + /* icon */ "a_slugs", + /* use_name */ "Large Slugs", + /* pickup_name */ "Large Slugs", + /* pickup_name_definite */ "Large Slugs", + /* quantity */ 20, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_AMMO, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ AMMO_SLUGS + }, + + /*QUAKED ammo_slugs_small (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/vault/items/ammo/slugs/small/tris.md2" + */ + { + /* id */ IT_AMMO_SLUGS_SMALL, + /* classname */ "ammo_slugs_small", + /* pickup */ Pickup_Ammo, + /* use */ nullptr, + /* drop */ Drop_Ammo, + /* weaponthink */ nullptr, + /* pickup_sound */ "misc/am_pkup.wav", + /* world_model */ "models/vault/items/ammo/slugs/small/tris.md2", + /* world_model_flags */ EF_NONE, + /* view_model */ nullptr, + /* icon */ "a_slugs", + /* use_name */ "Small Slugs", + /* pickup_name */ "Small Slugs", + /* pickup_name_definite */ "Small Slugs", + /* quantity */ 3, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_AMMO, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ AMMO_SLUGS + }, + + /*QUAKED item_teleporter (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/vault/items/ammo/nuke/tris.md2" + */ + { + /* id */ IT_TELEPORTER, + /* classname */ "item_teleporter", + /* pickup */ Pickup_Teleporter, + /* use */ Use_Teleporter, + /* drop */ nullptr, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/vault/items/ammo/nuke/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "i_fixme", + /* use_name */ "Personal Teleporter", + /* pickup_name */ "Personal Teleporter", + /* pickup_name_definite */ "Personal Teleporter", + /* quantity */ 120, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_TIMED | IF_POWERUP_WHEEL | IF_POWERUP_ONOFF + }, + + /*QUAKED item_regen (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + model="models/items/invulner/tris.md2" + */ + { + /* id */ IT_POWERUP_REGEN, + /* classname */ "item_regen", + /* pickup */ Pickup_Powerup, + /* use */ Use_Regeneration, + /* drop */ Drop_General, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/invulner/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "i_fixme", + /* use_name */ "Regeneration", + /* pickup_name */ "Regeneration", + /* pickup_name_definite */ "Regeneration", + /* quantity */ 60, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_POWERUP | IF_POWERUP_WHEEL, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_REGEN, + /* precaches */ "items/protect.wav" + }, + + /*QUAKED item_foodcube (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + Meaty cube o' health + model="models/objects/trapfx/tris.md2" + */ + { + /* id */ IT_FOODCUBE, + /* classname */ "item_foodcube", + /* pickup */ Pickup_Health, + /* use */ nullptr, + /* drop */ nullptr, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/n_health.wav", + /* world_model */ "models/objects/trapfx/tris.md2", + /* world_model_flags */ EF_GIB, + /* view_model */ nullptr, + /* icon */ "i_health", + /* use_name */ "Meaty Cube", + /* pickup_name */ "Meaty Cube", + /* pickup_name_definite */ "Meaty Cube", + /* quantity */ 50, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_HEALTH, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ HEALTH_IGNORE_MAX + }, + + /*QUAKED item_ball (.3 .3 1) (-16 -16 -16) (16 16 16) TRIGGER_SPAWN x x SUSPENDED x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP + Big ol' ball + models/items/ammo/grenades/medium/tris.md2" + */ + { + /* id */ IT_BALL, + /* classname */ "item_ball", + /* pickup */ Pickup_Ball, + /* use */ Use_Ball, + /* drop */ Drop_Ball, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/ammo/grenades/medium/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "i_help", + /* use_name */ "Ball", + /* pickup_name */ "Ball", + /* pickup_name_definite */ "Ball", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_STAY_COOP | IF_POWERUP | IF_POWERUP_WHEEL | IF_NOT_RANDOM, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_BALL, + /* precaches */ "", + /* sort_id */ -1 + }, + + /* Flashlight */ + { + /* id */ IT_FLASHLIGHT, + /* classname */ "item_flashlight", + /* pickup */ Pickup_General, + /* use */ Use_Flashlight, + /* drop */ nullptr, + /* weaponthink */ nullptr, + /* pickup_sound */ "items/pkup.wav", + /* world_model */ "models/items/flashlight/tris.md2", + /* world_model_flags */ EF_ROTATE | EF_BOB, + /* view_model */ nullptr, + /* icon */ "p_torch", + /* use_name */ "Flashlight", + /* pickup_name */ "$item_flashlight", + /* pickup_name_definite */ "$item_flashlight_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_STAY_COOP | IF_POWERUP_WHEEL | IF_POWERUP_ONOFF | IF_NOT_RANDOM, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_FLASHLIGHT, + /* precaches */ "items/flashlight_on.wav items/flashlight_off.wav", + /* sort_id */ -1 + }, + + /* Compass */ + { + /* id */ IT_COMPASS, + /* classname */ "item_compass", + /* pickup */ nullptr, + /* use */ Use_Compass, + /* drop */ nullptr, + /* weaponthink */ nullptr, + /* pickup_sound */ nullptr, + /* world_model */ nullptr, + /* world_model_flags */ EF_NONE, + /* view_model */ nullptr, + /* icon */ "p_compass", + /* use_name */ "Compass", + /* pickup_name */ "$item_compass", + /* pickup_name_definite */ "$item_compass_def", + /* quantity */ 0, + /* ammo */ IT_NULL, + /* chain */ IT_NULL, + /* flags */ IF_STAY_COOP | IF_POWERUP_WHEEL | IF_POWERUP_ONOFF, + /* vwep_model */ nullptr, + /* armor_info */ nullptr, + /* tag */ POWERUP_COMPASS, + /* precaches */ "misc/help_marker.wav", + /* sort_id */ -2 + }, }; // clang-format on @@ -6166,13 +6257,13 @@ void InitItems() { if (!itemlist[i].chain) continue; - gitem_t *item = &itemlist[i]; + gitem_t* item = &itemlist[i]; // already initialized if (item->chain_next) continue; - gitem_t *chain_item = &itemlist[item->chain]; + gitem_t* chain_item = &itemlist[item->chain]; if (!chain_item) gi.Com_ErrorFmt("Invalid item chain {} for {}", (int32_t)item->chain, item->pickup_name); @@ -6183,7 +6274,7 @@ void InitItems() { // if we're not the first in chain, add us now if (chain_item != item) { - gitem_t *c; + gitem_t* c; // end of chain is one whose chain_next points to chain_item for (c = chain_item; c->chain_next != chain_item; c = c->chain_next) @@ -6196,7 +6287,7 @@ void InitItems() { } // set up ammo - for (auto &it : itemlist) { + for (auto& it : itemlist) { if ((it.flags & IF_AMMO) && it.tag >= AMMO_BULLETS && it.tag < AMMO_MAX) { if (it.id <= IT_AMMO_ROUNDS) ammolist[it.tag] = ⁢ @@ -6206,7 +6297,7 @@ void InitItems() { } // in coop or DM with Weapons' Stay, remove drop ptr - for (auto &it : itemlist) { + for (auto& it : itemlist) { if (coop->integer) if (!P_UseCoopInstancedItems() && (it.flags & IF_STAY_COOP)) it.drop = nullptr; @@ -6218,7 +6309,7 @@ void InitItems() { G_CanDropItem =============== */ -static inline bool G_CanDropItem(const gitem_t &item) { +static inline bool G_CanDropItem(const gitem_t& item) { if (!item.drop) return false; else if ((item.flags & IF_WEAPON) && !(item.flags & IF_AMMO) && deathmatch->integer && g_dm_weapons_stay->integer) diff --git a/src/g_items_limits.h b/src/g_items_limits.h new file mode 100644 index 0000000..7073b64 --- /dev/null +++ b/src/g_items_limits.h @@ -0,0 +1,35 @@ +#pragma once + +#include + +/* +============= +G_GetHoldableMax + +Returns the effective max for a holdable, preferring cvar overrides when set. +============= +*/ +constexpr int G_GetHoldableMax(int override_max, int item_max, int fallback) +{ + int max_value = override_max > 0 ? override_max : item_max; + return max_value > 0 ? max_value : fallback; +} + +/* +============= +G_GetTechRegenMax + +Returns the regeneration cap applied when the Autodoc tech is active. +============= +*/ +inline int G_GetTechRegenMax(int vampiric_health_max, bool vampiric_damage_enabled, bool mod_enabled) +{ + if (vampiric_damage_enabled) + return (int)ceil(static_cast(vampiric_health_max) / 2.0f); + + return mod_enabled ? 100 : 150; +} + +static_assert(G_GetHoldableMax(3, 1, 1) == 3, "Override max should win when positive."); +static_assert(G_GetHoldableMax(0, 2, 1) == 2, "Item max should win when override is unset."); +static_assert(G_GetHoldableMax(0, 0, 1) == 1, "Fallback should apply when no max is defined."); diff --git a/src/g_local.h b/src/g_local.h index fd9129c..32b52ec 100644 --- a/src/g_local.h +++ b/src/g_local.h @@ -6,11 +6,13 @@ #include "bg_local.h" +#include + // the "gameversion" client command will print this plus compile date constexpr const char *GAMEVERSION = "baseq2"; constexpr const char *GAMEMOD_TITLE = "Muff Mode BETA"; -constexpr const char *GAMEMOD_VERSION = "0.19.50.4"; +constexpr const char *GAMEMOD_VERSION = "0.21.01"; //================================================================== @@ -829,8 +831,9 @@ struct save_data_list_t { save_data_tag_t tag; const void *ptr; // pointer to raw data const save_data_list_t *next; // next in list + bool valid; - save_data_list_t(const char *name, save_data_tag_t tag, const void *ptr); + save_data_list_t(const char *name, save_data_tag_t tag, const void *ptr, bool link = true, bool valid = true); static const save_data_list_t *fetch(const void *link_ptr, save_data_tag_t tag); }; @@ -859,12 +862,16 @@ struct save_data_t { save_data_t() {} constexpr save_data_t(const save_data_list_t *list_in) : - value(list_in->ptr), + value(list_in && list_in->valid ? list_in->ptr : nullptr), list(list_in) {} inline save_data_t(value_type ptr_in) : value(ptr_in), - list(ptr_in ? save_data_list_t::fetch(reinterpret_cast(ptr_in), static_cast(Tag)) : nullptr) {} + list(ptr_in ? save_data_list_t::fetch(reinterpret_cast(ptr_in), static_cast(Tag)) : nullptr) { + if (list && !list->valid) + value = nullptr; + } + inline save_data_t(const save_data_t &ref_in) : save_data_t(ref_in.value) {} @@ -873,6 +880,9 @@ struct save_data_t { if (value != ptr_in) { value = ptr_in; list = value ? save_data_list_t::fetch(reinterpret_cast(ptr_in), static_cast(Tag)) : nullptr; + + if (list && !list->valid) + value = nullptr; } return *this; @@ -880,7 +890,7 @@ struct save_data_t { constexpr const value_type pointer() const { return value; } constexpr const save_data_list_t *save_list() const { return list; } - constexpr const char *name() const { return value ? list->name : "null"; } + constexpr const char *name() const { return list ? list->name : "null"; } constexpr const value_type operator->() const { return value; } constexpr explicit operator bool() const { return value; } constexpr bool operator==(value_type ptr_in) const { return value == ptr_in; } @@ -1293,6 +1303,7 @@ struct gitem_t { int32_t sort_id = 0; // used by some items to control their sorting int32_t quantity_warn = 5; // when to warn on low ammo + int32_t quantity_max = 0; // maximum quantity a holdable can stack to (0 = use fallback) // set in InitItems, don't set by hand // circular list of chained weapons @@ -1444,6 +1455,7 @@ struct game_locals_t { gametype_t gametype; std::string motd; + char *motd_buffer = nullptr; int motd_mod_count = 0; ruleset_t ruleset; @@ -1561,6 +1573,7 @@ struct level_locals_t { int32_t body_que; // dead bodies int32_t power_cubes; // ugly necessity for coop + int32_t steam_effect_next_id; gentity_t *disguise_violator; gtime_t disguise_violation_time; @@ -2382,6 +2395,7 @@ extern cvar_t *g_coop_health_scaling; extern cvar_t *g_coop_instanced_items; extern cvar_t *g_coop_num_lives; extern cvar_t *g_coop_player_collision; +extern cvar_t *g_horde_num_lives; extern cvar_t *g_coop_squad_respawn; extern cvar_t *g_corpse_sink_time; extern cvar_t *g_damage_scale; @@ -2400,6 +2414,7 @@ extern cvar_t *g_dm_force_join; extern cvar_t *g_dm_force_respawn; extern cvar_t *g_dm_force_respawn_time; extern cvar_t *g_dm_holdable_adrenaline; +extern cvar_t *g_dm_holdable_doppel_max; extern cvar_t *g_dm_instant_items; extern cvar_t *g_dm_intermission_shots; extern cvar_t *g_dm_item_respawn_rate; @@ -2531,6 +2546,7 @@ team_t PickTeam(int ignoreClientNum); void BroadcastTeamChange(gentity_t *ent, int old_team, bool inactive, bool silent); bool AllowClientTeamSwitch(gentity_t *ent); int TeamBalance(bool force); +void ProcessBalanceQueue(void); void Cmd_ReadyUp_f(gentity_t *ent); void VoteCommandStore(gentity_t *ent); @@ -2673,6 +2689,7 @@ void G_StuffCmd(gentity_t *e, const char *fmt, ...); // // g_spawn.cpp // +const std::vector &G_GetSpawnClassnameConstants(); void ED_CallSpawn(gentity_t *ent); char *ED_NewString(char *string); void GT_SetLongName(void); @@ -2775,7 +2792,7 @@ void monster_fire_bfg(gentity_t *self, const vec3_t &start, const vec3_t &aimdir bool M_CheckClearShot(gentity_t *self, const vec3_t &offset); bool M_CheckClearShot(gentity_t *self, const vec3_t &offset, vec3_t &start); vec3_t M_ProjectFlashSource(gentity_t *self, const vec3_t &offset, const vec3_t &forward, const vec3_t &right); -bool M_droptofloor_generic(vec3_t &origin, const vec3_t &mins, const vec3_t &maxs, bool ceiling, gentity_t *ignore, contents_t mask, bool allow_partial); +bool M_droptofloor_generic(vec3_t &origin, const vec3_t &mins, const vec3_t &maxs, const vec3_t &gravityVector, gentity_t *ignore, contents_t mask, bool allow_partial); bool M_droptofloor(gentity_t *ent); void monster_think(gentity_t *self); void monster_dead_think(gentity_t *self); @@ -2994,6 +3011,16 @@ void ClientBeginServerFrame(gentity_t *ent); void ClientUserinfoChanged(gentity_t *ent, const char *userinfo); void Match_Ghost_Assign(gentity_t *ent); void Match_Ghost_DoAssign(gentity_t *ent); + +struct player_life_state_t { + bool playing; + bool eliminated; + int32_t health; + int32_t lives; +}; + +bool Horde_LivesEnabled(); +bool Horde_NoLivesRemain(const std::vector &states); void P_AssignClientSkinnum(gentity_t *ent); void P_ForceFogTransition(gentity_t *ent, bool instant); void P_SendLevelPOI(gentity_t *ent); @@ -3098,8 +3125,8 @@ extern byte damage_multiplier; // // m_move.cpp // -bool M_CheckBottom_Fast_Generic(const vec3_t &absmins, const vec3_t &absmaxs, bool ceiling); -bool M_CheckBottom_Slow_Generic(const vec3_t &origin, const vec3_t &absmins, const vec3_t &absmaxs, gentity_t *ignore, contents_t mask, bool ceiling, bool allow_any_step_height); +bool M_CheckBottom_Fast_Generic(const vec3_t &absmins, const vec3_t &absmaxs, const vec3_t &gravityVector); +bool M_CheckBottom_Slow_Generic(const vec3_t &origin, const vec3_t &absmins, const vec3_t &absmaxs, gentity_t *ignore, contents_t mask, const vec3_t &gravityVector, bool allow_any_step_height); bool M_CheckBottom(gentity_t *ent); bool G_CloseEnough(gentity_t *ent, gentity_t *goal, float dist); bool M_walkmove(gentity_t *ent, float yaw, float dist); @@ -3152,7 +3179,11 @@ bool InAMatch(); void ChangeGametype(gametype_t gt); void GT_Changes(); void SpawnEntities(const char *mapname, const char *entities, const char *spawnpoint); +void ClearWorldEntities(); +void G_SaveLevelEntstring(); +bool G_ResetLevelFromSavedEntstring(); void G_LoadMOTD(); +bool G_IsAdminSocialId(const char *social_id); // // g_chase.cpp @@ -3214,11 +3245,12 @@ gentity_t *CreateFlyMonster(const vec3_t &origin, const vec3_t &angles, const ve const char *classname); gentity_t *CreateGroundMonster(const vec3_t &origin, const vec3_t &angles, const vec3_t &mins, const vec3_t &maxs, const char *classname, float height); -bool FindSpawnPoint(const vec3_t &startpoint, const vec3_t &mins, const vec3_t &maxs, vec3_t &spawnpoint, - float maxMoveUp, bool drop = true); -bool CheckSpawnPoint(const vec3_t &origin, const vec3_t &mins, const vec3_t &maxs); -bool CheckGroundSpawnPoint(const vec3_t &origin, const vec3_t &entMins, const vec3_t &entMaxs, float height, - float gravity); +bool FindSpawnPoint(const vec3_t &startpoint, const vec3_t &mins, const vec3_t &maxs, vec3_t &spawnpoint, + float maxMoveUp, bool drop = true, const vec3_t &gravityVector = { 0.0f, 0.0f, -1.0f }); +bool CheckSpawnPoint(const vec3_t &origin, const vec3_t &mins, const vec3_t &maxs, const vec3_t &gravityVector = { 0.0f, 0.0f, -1.0f }); +bool SpawnCheckAndDropToFloor(vec3_t &origin, const vec3_t &mins, const vec3_t &maxs, const vec3_t &gravityVector = { 0.0f, 0.0f, -1.0f }); +bool CheckGroundSpawnPoint(const vec3_t &origin, const vec3_t &entMins, const vec3_t &entMaxs, float height, + const vec3_t &gravityVector = { 0.0f, 0.0f, -1.0f }); void SpawnGrow_Spawn(const vec3_t &startpos, float start_size, float end_size); void Widowlegs_Spawn(const vec3_t &startpos, const vec3_t &angles); @@ -3434,6 +3466,8 @@ struct client_session_t { bool admin; bool is_888; bool is_a_bot; + bool is_banned = false; + gtime_t ban_msg_debounce_time = 0_sec; // inactivity timer bool inactive; @@ -3590,6 +3624,7 @@ struct gclient_t { bool anim_duck; bool anim_run; gtime_t anim_time; + int32_t pain_anim_index; // powerup timers gtime_t pu_time_quad; @@ -4236,6 +4271,30 @@ struct fmt::formatter { } }; +template<> +struct fmt::formatter : fmt::formatter { + using fmt::formatter::parse; + + template + auto format(gentity_t *const &p, FormatContext &ctx) -> decltype(ctx.out()) { + if (!p) + return fmt::format_to(ctx.out(), FMT_STRING("null gentity_t")); + return fmt::formatter::format(*p, ctx); + } +}; + +template<> +struct fmt::formatter : fmt::formatter { + using fmt::formatter::parse; + + template + auto format(const gentity_t *const &p, FormatContext &ctx) -> decltype(ctx.out()) { + if (!p) + return fmt::format_to(ctx.out(), FMT_STRING("null gentity_t")); + return fmt::formatter::format(*p, ctx); + } +}; + // POI tags used by this mod enum pois_t : uint16_t { POI_OBJECTIVE = MAX_ENTITIES, // current objective @@ -4324,4 +4383,4 @@ template<> cached_modelindex *cached_modelindex::head; template<> cached_imageindex *cached_imageindex::head; extern cached_modelindex sm_meat_index; -extern cached_soundindex snd_fry; \ No newline at end of file +extern cached_soundindex snd_fry; diff --git a/src/g_main.cpp b/src/g_main.cpp index 5ebcb1f..7fc7f72 100644 --- a/src/g_main.cpp +++ b/src/g_main.cpp @@ -4,6 +4,11 @@ #include "g_local.h" #include "bots/bot_includes.h" #include "monsters/m_player.h" // match starts +#include +#include +#include +#include +#include CHECK_GCLIENT_INTEGRITY; CHECK_ENTITY_INTEGRITY; @@ -33,6 +38,8 @@ gentity_t *g_entities; cvar_t *hostname; +static std::string saved_level_entstring; + cvar_t *deathmatch; cvar_t *ctf; cvar_t *teamplay; @@ -62,6 +69,8 @@ static cvar_t *maxentities; cvar_t *maxplayers; cvar_t *minplayers; +static std::unordered_set g_admin_social_ids; + cvar_t *ai_allow_dm_spawn; cvar_t *ai_damage_scale; cvar_t *ai_model_scale; @@ -98,6 +107,7 @@ cvar_t *g_coop_health_scaling; cvar_t *g_coop_instanced_items; cvar_t *g_coop_num_lives; cvar_t *g_coop_player_collision; +cvar_t *g_horde_num_lives; cvar_t *g_coop_squad_respawn; cvar_t *g_corpse_sink_time; cvar_t *g_damage_scale; @@ -116,6 +126,7 @@ cvar_t *g_dm_force_join; cvar_t *g_dm_force_respawn; cvar_t *g_dm_force_respawn_time; cvar_t *g_dm_holdable_adrenaline; +cvar_t *g_dm_holdable_doppel_max; cvar_t *g_dm_instant_items; cvar_t *g_dm_intermission_shots; cvar_t *g_dm_item_respawn_rate; @@ -528,6 +539,13 @@ static void Horde_Init() { */ } +/* +============= +Horde_AllMonstersDead + +Returns true when no live monsters remain in the level. +============= +*/ static bool Horde_AllMonstersDead() { for (size_t i = 0; i < globals.max_entities; i++) { if (!g_entities[i].inuse) @@ -541,10 +559,56 @@ static bool Horde_AllMonstersDead() { return true; } +/* +============= +Horde_LivesEnabled + +Returns true when Horde is configured to use limited lives. +============= +*/ +bool Horde_LivesEnabled() { + return GT(GT_HORDE) && g_horde_num_lives->integer > 0; +} + +/* +============= +Horde_NoLivesRemain + +Returns true when no active players are present or when every playing client is out of lives or eliminated. +============= +*/ +bool Horde_NoLivesRemain(const std::vector &states) { + for (const auto &state : states) { + if (!state.playing) + continue; + + if (state.health > 0) + return false; + + if (!state.eliminated && state.lives > 0) + return false; + } + + return true; +} // ================================================= +/* +============= +G_LoadMOTD + +Loads the server message of the day text into persistent memory. +============= +*/ void G_LoadMOTD() { + if (game.motd_buffer) { + gi.TagFree(game.motd_buffer); + game.motd_buffer = nullptr; + } + + game.motd.clear(); + // load up ent override const char *name = G_Fmt("baseq2/{}", g_motd_filename->string[0] ? g_motd_filename->string : "motd.txt").data(); FILE *f = fopen(name, "rb"); @@ -563,7 +627,7 @@ void G_LoadMOTD() { valid = false; } if (valid) { - buffer = (char *)gi.TagMalloc(length + 1, '\0'); + buffer = (char *)gi.TagMalloc(length + 1, TAG_GAME); if (length) { read_length = fread(buffer, 1, length, f); @@ -572,20 +636,29 @@ void G_LoadMOTD() { valid = false; } } + buffer[length] = '\0'; } fclose(f); - + if (valid) { - game.motd = (const char *)buffer; + game.motd_buffer = buffer; + game.motd.assign(buffer, length); game.motd_mod_count++; if (g_verbose->integer) gi.Com_PrintFmt("{}: MotD file verified and loaded: \"{}\"\n", __FUNCTION__, name); } else { gi.Com_PrintFmt("{}: MotD file load error for \"{}\", discarding.\n", __FUNCTION__, name); + if (buffer) { + gi.TagFree(buffer); + buffer = nullptr; + } + game.motd_buffer = nullptr; + game.motd.clear(); } } } + int check_ruleset = -1; static void CheckRuleset() { if (game.ruleset && check_ruleset == g_ruleset->modified_count) @@ -606,37 +679,142 @@ static void InitGametype() { bool force_dm = false; if (g_gametype->integer < 0 || g_gametype->integer >= GT_NUM_GAMETYPES) - gi.cvar_forceset("g_gametype", G_Fmt("{}", clamp(g_gametype->integer, (int)GT_FIRST, (int)GT_LAST)).data()); - + gi.cvar_forceset("g_gametype", G_Fmt("{}", clamp(g_gametype->integer, (int)GT_FIRST, (int)GT_LAST)).data()); + if (ctf->integer) { - force_dm = true; - // force coop off - if (coop->integer) - gi.cvar_set(COOP, "0"); - // force tdm off - if (teamplay->integer) - gi.cvar_set("teamplay", "0"); + force_dm = true; + // force coop off + if (coop->integer) + gi.cvar_set(COOP, "0"); + // force tdm off + if (teamplay->integer) + gi.cvar_set("teamplay", "0"); } if (teamplay->integer) { - force_dm = true; - // force coop off - if (coop->integer) - gi.cvar_set(COOP, "0"); + force_dm = true; + // force coop off + if (coop->integer) + gi.cvar_set(COOP, "0"); } if (force_dm && !deathmatch->integer) { - gi.Com_Print("Forcing deathmatch.\n"); - gi.cvar_forceset("deathmatch", "1"); + gi.Com_Print("Forcing deathmatch.\n"); + gi.cvar_forceset("deathmatch", "1"); } // force even maxplayers value during teamplay if (Teams()) { - int pmax = maxplayers->integer; + int pmax = maxplayers->integer; - if (pmax != floor(pmax / 2)) - gi.cvar_set("maxplayers", G_Fmt("{}", floor(pmax / 2) * 2).data()); + if (pmax != floor(pmax / 2)) + gi.cvar_set("maxplayers", G_Fmt("{}", floor(pmax / 2) * 2).data()); + } + } + +/* +============= +G_Admins_NormalizeId + +Converts a social ID to lowercase for comparison. +============= +*/ +static std::string G_Admins_NormalizeId(const std::string &input) { + std::string normalized; + normalized.reserve(input.size()); + + for (char ch : input) + normalized.push_back((char)std::tolower((uint8_t)ch)); + + return normalized; + } + +/* +============= +G_Admins_TrimLine + +Strips comments and whitespace from a config line. +============= +*/ +static std::string G_Admins_TrimLine(const std::string &line) { + std::string trimmed = line; + + size_t comment = trimmed.find_first_of("#;"); + if (comment != std::string::npos) + trimmed = trimmed.substr(0, comment); + + size_t start = trimmed.find_first_not_of(" \t\r\n"); + if (start == std::string::npos) + return {}; + + size_t end = trimmed.find_last_not_of(" \t\r\n"); + return trimmed.substr(start, end - start + 1); + } + +/* +============= +G_IsAdminSocialId + +Checks whether the provided social ID is in the admin list. +============= +*/ +bool G_IsAdminSocialId(const char *social_id) { + if (!social_id || !*social_id) + return false; + + std::string normalized = G_Admins_NormalizeId(social_id); + return g_admin_social_ids.find(normalized) != g_admin_social_ids.end(); + } + +/* +============= +G_LoadAdminList + +Loads admins from the admins.txt configuration file. +============= +*/ +static void G_LoadAdminList() { + g_admin_social_ids.clear(); + + const char *filename = "admins.txt"; + std::ifstream file(filename); + + if (!file.is_open()) { + gi.Com_PrintFmt("G_LoadAdminList: {} not found, skipping admin preload.\n", filename); + return; + } + + size_t line_number = 0; + std::string line; + + while (std::getline(file, line)) { + ++line_number; + + std::string trimmed = G_Admins_TrimLine(line); + if (trimmed.empty()) + continue; + + if (trimmed.size() >= MAX_INFO_VALUE) { + gi.Com_PrintFmt("G_LoadAdminList: line {} exceeds maximum length, ignoring.\n", line_number); + continue; + } + + if (trimmed.find_first_of(" \t") != std::string::npos) { + gi.Com_PrintFmt("G_LoadAdminList: unexpected whitespace on line {}, ignoring.\n", line_number); + continue; + } + + std::string normalized = G_Admins_NormalizeId(trimmed); + + if (normalized.empty()) { + gi.Com_PrintFmt("G_LoadAdminList: invalid entry on line {}, ignoring.\n", line_number); + continue; + } + + g_admin_social_ids.insert(normalized); + } + + gi.Com_PrintFmt("G_LoadAdminList: loaded {} admin entr{}.\n", g_admin_social_ids.size(), g_admin_social_ids.size() == 1 ? "y" : "ies"); } -} void ChangeGametype(gametype_t gt) { switch (gt) { @@ -670,6 +848,51 @@ int gt_ctf = 0; int gt_g_gametype = 0; bool gt_teams_on = false; gametype_t gt_check = GT_NONE; +static char *gt_saved_entstring = nullptr; + +/* +============= +G_SaveGametypeEntityString + +Make a copy of the current level entity string so the level can be +rebuilt without a full map load. +============= +*/ +static bool G_SaveGametypeEntityString() { + if (gt_saved_entstring) { + gi.TagFree(gt_saved_entstring); + gt_saved_entstring = nullptr; + } + + if (level.entstring.empty()) + return false; + + size_t length = level.entstring.length() + 1; + gt_saved_entstring = (char *)gi.TagMalloc(length, TAG_GAME); + if (!gt_saved_entstring) + return false; + + Q_strlcpy(gt_saved_entstring, level.entstring.c_str(), length); + return true; +} + +/* +============= +G_LoadGametypeEntityString + +Reload the saved entity string and clear its cached copy. +============= +*/ +static bool G_LoadGametypeEntityString() { + if (!gt_saved_entstring) + return false; + + SpawnEntities(level.mapname, gt_saved_entstring, game.spawnpoint); + gi.TagFree(gt_saved_entstring); + gt_saved_entstring = nullptr; + return true; +} + void GT_Changes() { if (!deathmatch->integer) return; @@ -750,7 +973,8 @@ void GT_Changes() { return; //gi.Com_PrintFmt("GAMETYPE = {}\n", (int)gt); - + bool saved_entstring = G_SaveGametypeEntityString(); + if (gt_teams_on != Teams()) { team_reset = true; gt_teams_on = Teams(); @@ -790,14 +1014,55 @@ void GT_Changes() { gt_check = (gametype_t)g_gametype->integer; } else return; - //TODO: save ent string so we can simply reload it and Match_Reset - //gi.AddCommandString("map_restart"); + if (saved_entstring && G_LoadGametypeEntityString()) { + Match_Reset(); + } else { + if (gt_saved_entstring) { + gi.TagFree(gt_saved_entstring); + gt_saved_entstring = nullptr; + } + gi.AddCommandString(G_Fmt("gamemap {}\n", level.mapname).data()); + } + + GT_PrecacheAssets(); + GT_SetLongName(); + gi.LocBroadcast_Print(PRINT_CENTER, "{}", level.gametype_name); +} + +/* +============= +G_SaveLevelEntstring + +Preserve the currently loaded entity string so it can be reused when rebuilding the level without a full map reload. +============= +*/ +void G_SaveLevelEntstring() { + if (!level.entstring.empty()) + saved_level_entstring = level.entstring; +} + +/* +============= +G_ResetLevelFromSavedEntstring + +Rebuild the level using the cached entity string if available, avoiding a full map reload. Returns true when the reset completed using the cached data. +============= +*/ +bool G_ResetLevelFromSavedEntstring() { + const char *entities = nullptr; + + if (!saved_level_entstring.empty()) + entities = saved_level_entstring.c_str(); + else if (!level.entstring.empty()) + entities = level.entstring.c_str(); + + if (!entities) + return false; - gi.AddCommandString(G_Fmt("gamemap {}\n", level.mapname).data()); + ClearWorldEntities(); + SpawnEntities(level.mapname, entities, nullptr); - GT_PrecacheAssets(); - GT_SetLongName(); - gi.LocBroadcast_Print(PRINT_CENTER, "{}", level.gametype_name); + return true; } /* @@ -877,10 +1142,11 @@ static void InitGame() { // [Paril-KEX] g_coop_player_collision = gi.cvar("g_coop_player_collision", "0", CVAR_LATCH); - g_coop_squad_respawn = gi.cvar("g_coop_squad_respawn", "1", CVAR_LATCH); - g_coop_enable_lives = gi.cvar("g_coop_enable_lives", "0", CVAR_LATCH); - g_coop_num_lives = gi.cvar("g_coop_num_lives", "2", CVAR_LATCH); - g_coop_instanced_items = gi.cvar("g_coop_instanced_items", "1", CVAR_LATCH); +g_coop_squad_respawn = gi.cvar("g_coop_squad_respawn", "1", CVAR_LATCH); +g_coop_enable_lives = gi.cvar("g_coop_enable_lives", "0", CVAR_LATCH); +g_coop_num_lives = gi.cvar("g_coop_num_lives", "2", CVAR_LATCH); +g_horde_num_lives = gi.cvar("g_horde_num_lives", "0", CVAR_SERVERINFO | CVAR_LATCH); +g_coop_instanced_items = gi.cvar("g_coop_instanced_items", "1", CVAR_LATCH); g_allow_grapple = gi.cvar("g_allow_grapple", "auto", CVAR_NOFLAGS); g_allow_kill = gi.cvar("g_allow_kill", "1", CVAR_NOFLAGS); g_grapple_offhand = gi.cvar("g_grapple_offhand", "0", CVAR_NOFLAGS); @@ -967,13 +1233,14 @@ static void InitGame() { g_dm_do_readyup = gi.cvar("g_dm_do_readyup", "0", CVAR_NOFLAGS); g_dm_do_warmup = gi.cvar("g_dm_do_warmup", "1", CVAR_NOFLAGS); g_dm_exec_level_cfg = gi.cvar("g_dm_exec_level_cfg", "0", CVAR_NOFLAGS); - g_dm_force_join = gi.cvar("g_dm_force_join", "0", CVAR_NOFLAGS); - g_dm_force_respawn = gi.cvar("g_dm_force_respawn", "1", CVAR_NOFLAGS); - g_dm_force_respawn_time = gi.cvar("g_dm_force_respawn_time", "3", CVAR_NOFLAGS); - g_dm_holdable_adrenaline = gi.cvar("g_dm_holdable_adrenaline", "1", CVAR_NOFLAGS); - g_dm_instant_items = gi.cvar("g_dm_instant_items", "1", CVAR_NOFLAGS); - g_dm_intermission_shots = gi.cvar("g_dm_intermission_shots", "0", CVAR_NOFLAGS); - g_dm_item_respawn_rate = gi.cvar("g_dm_item_respawn_rate", "1.0", CVAR_NOFLAGS); +g_dm_force_join = gi.cvar("g_dm_force_join", "0", CVAR_NOFLAGS); +g_dm_force_respawn = gi.cvar("g_dm_force_respawn", "1", CVAR_NOFLAGS); +g_dm_force_respawn_time = gi.cvar("g_dm_force_respawn_time", "3", CVAR_NOFLAGS); +g_dm_holdable_adrenaline = gi.cvar("g_dm_holdable_adrenaline", "1", CVAR_NOFLAGS); +g_dm_holdable_doppel_max = gi.cvar("g_dm_holdable_doppel_max", "0", CVAR_NOFLAGS); +g_dm_instant_items = gi.cvar("g_dm_instant_items", "1", CVAR_NOFLAGS); +g_dm_intermission_shots = gi.cvar("g_dm_intermission_shots", "0", CVAR_NOFLAGS); +g_dm_item_respawn_rate = gi.cvar("g_dm_item_respawn_rate", "1.0", CVAR_NOFLAGS); g_dm_no_fall_damage = gi.cvar("g_dm_no_fall_damage", "0", CVAR_NOFLAGS); g_dm_no_quad_drop = gi.cvar("g_dm_no_quad_drop", "0", CVAR_NOFLAGS); g_dm_no_self_damage = gi.cvar("g_dm_no_self_damage", "0", CVAR_NOFLAGS); @@ -1043,6 +1310,8 @@ static void InitGame() { g_weapon_projection = gi.cvar("g_weapon_projection", "0", CVAR_NOFLAGS); g_weapon_respawn_time = gi.cvar("g_weapon_respawn_time", "30", CVAR_NOFLAGS); + G_LoadAdminList(); + bot_name_prefix = gi.cvar("bot_name_prefix", "B|", CVAR_NOFLAGS); // ruleset @@ -1053,6 +1322,8 @@ static void InitGame() { game = {}; + const int32_t clamped_maxclients = std::clamp(maxclients->integer, 1, static_cast(MAX_CLIENTS)); + // initialize all entities for this game game.maxentities = maxentities->integer; g_entities = (gentity_t *)gi.TagMalloc(game.maxentities * sizeof(g_entities[0]), TAG_GAME); @@ -1060,13 +1331,13 @@ static void InitGame() { globals.max_entities = game.maxentities; // initialize all clients for this game - game.maxclients = maxclients->integer; - game.clients = (gclient_t *)gi.TagMalloc(game.maxclients * sizeof(game.clients[0]), TAG_GAME); - globals.num_entities = game.maxclients + 1; + game.maxclients = clamped_maxclients; + game.clients = (gclient_t *)gi.TagMalloc(clamped_maxclients * sizeof(game.clients[0]), TAG_GAME); + globals.num_entities = clamped_maxclients + 1; // how far back we should support lag origins for game.max_lag_origins = 20 * (0.1f / gi.frame_time_s); - game.lag_origins = (vec3_t *)gi.TagMalloc(game.maxclients * sizeof(vec3_t) * game.max_lag_origins, TAG_GAME); + game.lag_origins = (vec3_t *)gi.TagMalloc(clamped_maxclients * sizeof(vec3_t) * game.max_lag_origins, TAG_GAME); level.start_time = level.time; @@ -1455,6 +1726,8 @@ void Round_End() { level.round_state = roundst_t::ROUND_ENDED; level.round_state_timer = level.time + 3_sec; level.horde_all_spawned = false; + + ProcessBalanceQueue(); } /* @@ -1842,9 +2115,21 @@ static void CheckDMRoundState(void) { Round_End(); return; } - break; - } +break; +} case GT_HORDE: + if (Horde_LivesEnabled()) { + std::vector life_states; + + for (auto ec : active_clients()) + life_states.push_back({ ClientIsPlaying(ec->client), ec->client->eliminated, ec->health, ec->client->pers.lives }); + + if (Horde_NoLivesRemain(life_states)) { + gi.LocBroadcast_Print(PRINT_CENTER, "No lives remaining!\n"); + QueueIntermission("OUT OF LIVES", true, false); + return; + } + } Horde_RunSpawning(); //if (level.horde_all_spawned && Horde_AllMonstersDead()) { if (level.horde_all_spawned && !(level.total_monsters - level.killed_monsters)) { @@ -2246,8 +2531,7 @@ void FindIntermissionPoint(void) { if (target) { gi.Com_Print("FindIntermissionPoint target 2\n"); dir = (target->s.origin - level.intermission_origin).normalized(); - AngleVectors(dir); - level.intermission_angle = dir; + level.intermission_angle = vectoangles(dir); } } } @@ -2286,31 +2570,30 @@ void SetIntermissionPoint(void) { if (ent) { level.intermission_origin = ent->s.origin; level.spawn_spots[SPAWN_SPOT_INTERMISSION] = ent; - } - - // ugly hax! - if (!Q_strncasecmp(level.mapname, "campgrounds", 11)) { - gvec3_t v = { -320, -96, 503 }; - if (ent->s.origin == v) - level.intermission_angle[PITCH] = -30; - } else if (!Q_strncasecmp(level.mapname, "rdm10", 5)) { - gvec3_t v = { -1256, -1672, -136 }; - if (ent->s.origin == v) - level.intermission_angle = { 15, 135, 0 }; - } else { - // if it has a target, look towards it - if (ent && ent->target) { - gentity_t *target = G_PickTarget(ent->target); - if (target) { - //gi.Com_Print("HAS TARGET\n"); - vec3_t dir = (target->s.origin - level.intermission_origin).normalized(); - AngleVectors(dir); - level.intermission_angle = dir; + // ugly hax! + if (!Q_strncasecmp(level.mapname, "campgrounds", 11)) { + gvec3_t v = { -320, -96, 503 }; + if (ent->s.origin == v) + level.intermission_angle[PITCH] = -30; + } else if (!Q_strncasecmp(level.mapname, "rdm10", 5)) { + gvec3_t v = { -1256, -1672, -136 }; + if (ent->s.origin == v) + level.intermission_angle = { 15, 135, 0 }; + } else { + // if it has a target, look towards it + if (ent->target) { + gentity_t *target = G_PickTarget(ent->target); + + if (target) { + //gi.Com_Print("HAS TARGET\n"); + vec3_t dir = (target->s.origin - level.intermission_origin).normalized(); + level.intermission_angle = vectoangles(dir); + } } + if (!level.intermission_angle) + level.intermission_angle = ent->s.angles; } - if (ent && !level.intermission_angle) - level.intermission_angle = ent->s.angles; } //gi.Com_PrintFmt("{}: origin={} angles={}\n", __FUNCTION__, level.intermission_origin, level.intermission_angle); @@ -2994,7 +3277,8 @@ void Match_End() { if (values[0] == level.mapname) std::swap(values[0], values[values.size() - 1]); - gi.cvar_forceset("g_map_list", fmt::format("{}", join_strings(values, " ")).data()); + auto shuffled_map_list = join_strings(values, " "); + gi.cvar_forceset("g_map_list", shuffled_map_list.c_str()); BeginIntermission(CreateTargetChangeLevel(values[0].c_str())); return; @@ -3430,33 +3714,62 @@ ExitLevel */ void ExitLevel() { if (deathmatch->integer && g_dm_intermission_shots->integer && level.num_playing_human_clients > 0) { - struct tm *ltime; - time_t gmtime; + time_t current_time; + struct tm local_time {}; + bool have_time = time(¤t_time) != static_cast(-1); - time(&gmtime); - ltime = localtime(&gmtime); - time(&gmtime); - ltime = localtime(&gmtime); + if (have_time) { +#if defined(_WIN32) + have_time = localtime_s(&local_time, ¤t_time) == 0; +#else + have_time = localtime_r(¤t_time, &local_time) != nullptr; +#endif + } - const char *s = ""; + if (!have_time) { + gi.Com_Print("Failed to resolve local time for intermission screenshot.\n"); + } else { + std::string screenshot_command; + + const auto first_index = level.sorted_clients[0]; + const auto second_index = level.sorted_clients[1]; + const bool have_first = first_index >= 0 && first_index < static_cast(MAX_CLIENTS); + const bool have_second = second_index >= 0 && second_index < static_cast(MAX_CLIENTS); + + if (GT(GT_DUEL) && level.num_playing_human_clients > 1 && have_first && have_second) { + gentity_t *e1 = &g_entities[first_index + 1]; + gentity_t *e2 = &g_entities[second_index + 1]; + const char *n1 = (e1 && e1->client) ? e1->client->resp.netname : ""; + const char *n2 = (e2 && e2->client) ? e2->client->resp.netname : ""; + + screenshot_command = std::string(G_Fmt("screenshot {}-vs-{}-{}-{}_{:02}_{:02}-{:02}_{:02}_{:02}\n", + n1, n2, level.mapname, 1900 + local_time.tm_year, local_time.tm_mon + 1, local_time.tm_mday, local_time.tm_hour, local_time.tm_min, local_time.tm_sec)); + gi.Com_Print(screenshot_command.c_str()); + } else if (have_first) { + gentity_t *ent = &g_entities[first_index + 1]; + const bool has_follow_target = ent && ent->client && ent->client->follow_target && ent->client->follow_target->client; + const char *name = has_follow_target ? ent->client->follow_target->client->resp.netname : + (ent && ent->client ? ent->client->resp.netname : ""); + + screenshot_command = std::string(G_Fmt("screenshot {}-{}-{}-{}_{:02}_{:02}-{:02}_{:02}_{:02}\n", gt_short_name_upper[g_gametype->integer], + name, level.mapname, 1900 + local_time.tm_year, local_time.tm_mon + 1, local_time.tm_mday, local_time.tm_hour, local_time.tm_min, local_time.tm_sec)); + } else { + for (auto player : active_clients()) { + if (!player->client || !ClientIsPlaying(player->client)) + continue; - if (GT(GT_DUEL)) { - gentity_t *e1 = &g_entities[level.sorted_clients[0] + 1]; - gentity_t *e2 = &g_entities[level.sorted_clients[1] + 1]; - const char *n1 = e1 ? e1->client->resp.netname : ""; - const char *n2 = e2 ? e2->client->resp.netname : ""; + const bool has_follow_target = player->client->follow_target && player->client->follow_target->client; + const char *name = has_follow_target ? player->client->follow_target->client->resp.netname : player->client->resp.netname; - s = G_Fmt("screenshot {}-vs-{}-{}-{}_{:02}_{:02}-{:02}_{:02}_{:02}\n", - n1, n2, level.mapname, 1900 + ltime->tm_year, ltime->tm_mon + 1, ltime->tm_mday, ltime->tm_hour, ltime->tm_min, ltime->tm_sec).data(); - gi.Com_Print(s); - } else { - gentity_t *ent = &g_entities[1]; - const char *name = ent->client->follow_target ? ent->client->follow_target->client->resp.netname : ent->client->resp.netname; + screenshot_command = std::string(G_Fmt("screenshot {}-{}-{}-{}_{:02}_{:02}-{:02}_{:02}_{:02}\n", gt_short_name_upper[g_gametype->integer], + name, level.mapname, 1900 + local_time.tm_year, local_time.tm_mon + 1, local_time.tm_mday, local_time.tm_hour, local_time.tm_min, local_time.tm_sec)); + break; + } + } - s = G_Fmt("screenshot {}-{}-{}-{}_{:02}_{:02}-{:02}_{:02}_{:02}\n", gt_short_name_upper[g_gametype->integer], - name, level.mapname, 1900 + ltime->tm_year, ltime->tm_mon + 1, ltime->tm_mday, ltime->tm_hour, ltime->tm_min, ltime->tm_sec).data(); + if (!screenshot_command.empty()) + gi.AddCommandString(screenshot_command.c_str()); } - gi.AddCommandString(s); } // [Paril-KEX] N64 fade @@ -3623,7 +3936,17 @@ static void CheckCvars() { CheckMinMaxPlayers(); } -static bool G_AnyDeadPlayersWithoutLives() { +/* +============= +G_AnyDeadPlayersWithoutLives + +Checks for any dead players who have exhausted their lives and should remain eliminated. +============= +*/ +static bool G_AnyDeadPlayersWithoutLives(bool limited_lives) { + if (!limited_lives) + return false; + for (auto player : active_clients()) if (player->health <= 0 && (!player->client->pers.lives || player->client->eliminated)) return true; @@ -3631,6 +3954,8 @@ static bool G_AnyDeadPlayersWithoutLives() { return false; } + + /* ================ CheckDMEndFrame @@ -3727,18 +4052,20 @@ static inline void G_RunFrame_(bool main_loop) { // clear client coop respawn states; this is done // early since it may be set multiple times for different // players - if (InCoopStyle() && (g_coop_enable_lives->integer || g_coop_squad_respawn->integer)) { + if (InCoopStyle() && (g_coop_squad_respawn->integer || g_coop_enable_lives->integer || Horde_LivesEnabled())) { + const bool limited_lives = g_coop_enable_lives->integer || Horde_LivesEnabled(); + for (auto player : active_clients()) { if (player->client->respawn_time >= level.time) player->client->coop_respawn_state = COOP_RESPAWN_WAITING; - else if (g_coop_enable_lives->integer && player->health <= 0 && player->client->pers.lives == 0) + else if (limited_lives && player->health <= 0 && player->client->pers.lives == 0) player->client->coop_respawn_state = COOP_RESPAWN_NO_LIVES; - else if (g_coop_enable_lives->integer && G_AnyDeadPlayersWithoutLives()) + else if (G_AnyDeadPlayersWithoutLives(limited_lives)) player->client->coop_respawn_state = COOP_RESPAWN_NO_LIVES; else player->client->coop_respawn_state = COOP_RESPAWN_NONE; - } - } +} +} // // treat each object in turn diff --git a/src/g_menu.cpp b/src/g_menu.cpp index 23180e0..d28944f 100644 --- a/src/g_menu.cpp +++ b/src/g_menu.cpp @@ -228,7 +228,7 @@ static void G_Menu_Admin_Settings(gentity_t *ent, menu_hnd_t *p) { settings->instantweap = g_instant_weapon_switch->integer != 0; settings->match_lock = g_match_lock->integer != 0; - menu = P_Menu_Open(ent, def_setmenu, -1, sizeof(def_setmenu) / sizeof(menu_t), settings, nullptr); + menu = P_Menu_Open(ent, def_setmenu, -1, sizeof(def_setmenu) / sizeof(menu_t), settings, true, nullptr); G_Menu_Admin_UpdateSettings(ent, menu); } @@ -285,7 +285,7 @@ void G_Menu_Admin(gentity_t *ent, menu_hnd_t *p) { } P_Menu_Close(ent); - P_Menu_Open(ent, adminmenu, -1, sizeof(adminmenu) / sizeof(menu_t), nullptr, nullptr); + P_Menu_Open(ent, adminmenu, -1, sizeof(adminmenu) / sizeof(menu_t), nullptr, false, nullptr); } /*-----------------------------------------------------------------------*/ @@ -313,59 +313,93 @@ const menu_t pmstatsmenu[] = { static void G_Menu_PMStats_Update(gentity_t *ent) { - if (!g_matchstats->integer) return; + if (!g_matchstats->integer) + return; menu_t *entries = ent->client->menu->entries; client_match_stats_t *st = &ent->client->mstats; int i = 0; char value[MAX_INFO_VALUE] = { 0 }; - gi.Info_ValueForKey(g_entities[1].client->pers.userinfo, "name", value, sizeof(value)); + if (game.maxclients > 0 && g_entities[1].client) { + gi.Info_ValueForKey(g_entities[1].client->pers.userinfo, "name", value, sizeof(value)); + } - Q_strlcpy(entries[i].text, "Player Stats for Match", sizeof(entries[i].text)); + if (i < ent->client->menu->num) + Q_strlcpy(entries[i].text, "Player Stats for Match", sizeof(entries[i].text)); i++; if (value[0]) { - Q_strlcpy(entries[i].text, G_Fmt("{}", value).data(), sizeof(entries[i].text)); + if (i < ent->client->menu->num) + Q_strlcpy(entries[i].text, G_Fmt("{}", value).data(), sizeof(entries[i].text)); i++; } - Q_strlcpy(entries[i].text, BREAKER, sizeof(entries[i].text)); + if (i < ent->client->menu->num) + Q_strlcpy(entries[i].text, BREAKER, sizeof(entries[i].text)); i++; - Q_strlcpy(entries[i].text, G_Fmt("kills: {}", st->total_kills).data(), sizeof(entries[i].text)); + if (i < ent->client->menu->num) + Q_strlcpy(entries[i].text, G_Fmt("kills: {}", st->total_kills).data(), sizeof(entries[i].text)); i++; - Q_strlcpy(entries[i].text, G_Fmt("deaths: {}", st->total_deaths).data(), sizeof(entries[i].text)); + if (i < ent->client->menu->num) + Q_strlcpy(entries[i].text, G_Fmt("deaths: {}", st->total_deaths).data(), sizeof(entries[i].text)); i++; if (st->total_kills) { - float val = st->total_kills > 0 ? ((float)st->total_kills / (float)st->total_deaths) : 0; - Q_strlcpy(entries[i].text, G_Fmt("k/d ratio: {:2}", val).data(), sizeof(entries[i].text)); + if (i < ent->client->menu->num) { + if (st->total_deaths > 0) { + float val = (float)st->total_kills / (float)st->total_deaths; + Q_strlcpy(entries[i].text, G_Fmt("k/d ratio: {:2}", val).data(), sizeof(entries[i].text)); + } else { + Q_strlcpy(entries[i].text, "k/d ratio: N/A", sizeof(entries[i].text)); + } + } i++; } + if (i < ent->client->menu->num) + entries[i].text[0] = '\0'; i++; - Q_strlcpy(entries[i].text, G_Fmt("dmg dealt: {}", st->total_dmg_dealt).data(), sizeof(entries[i].text)); + if (i < ent->client->menu->num) + Q_strlcpy(entries[i].text, G_Fmt("dmg dealt: {}", st->total_dmg_dealt).data(), sizeof(entries[i].text)); i++; - Q_strlcpy(entries[i].text, G_Fmt("dmg received: {}", st->total_dmg_received).data(), sizeof(entries[i].text)); + if (i < ent->client->menu->num) + Q_strlcpy(entries[i].text, G_Fmt("dmg received: {}", st->total_dmg_received).data(), sizeof(entries[i].text)); i++; if (st->total_dmg_dealt) { - float val = st->total_dmg_dealt ? ((float)st->total_dmg_dealt / (float)st->total_dmg_received) : 0; - Q_strlcpy(entries[i].text, G_Fmt("dmg ratio: {:02}", val).data(), sizeof(entries[i].text)); + if (i < ent->client->menu->num) { + if (st->total_dmg_received > 0) { + float val = (float)st->total_dmg_dealt / (float)st->total_dmg_received; + Q_strlcpy(entries[i].text, G_Fmt("dmg ratio: {:02}", val).data(), sizeof(entries[i].text)); + } else { + Q_strlcpy(entries[i].text, "dmg ratio: N/A", sizeof(entries[i].text)); + } + } i++; } + if (i < ent->client->menu->num) + entries[i].text[0] = '\0'; i++; - Q_strlcpy(entries[i].text, G_Fmt("shots fired: {}", st->total_shots).data(), sizeof(entries[i].text)); + if (i < ent->client->menu->num) + Q_strlcpy(entries[i].text, G_Fmt("shots fired: {}", st->total_shots).data(), sizeof(entries[i].text)); i++; - Q_strlcpy(entries[i].text, G_Fmt("shots on target: {}", st->total_hits).data(), sizeof(entries[i].text)); + if (i < ent->client->menu->num) + Q_strlcpy(entries[i].text, G_Fmt("shots on target: {}", st->total_hits).data(), sizeof(entries[i].text)); i++; if (st->total_hits) { - int val = st->total_hits ? ((float)st->total_hits / (float)st->total_shots) * 100. : 0; - Q_strlcpy(entries[i].text, G_Fmt("total accuracy: {}%", val).data(), sizeof(entries[i].text)); + if (i < ent->client->menu->num) { + if (st->total_shots > 0) { + int val = (int)(((float)st->total_hits / (float)st->total_shots) * 100.f); + Q_strlcpy(entries[i].text, G_Fmt("total accuracy: {}%", val).data(), sizeof(entries[i].text)); + } else { + Q_strlcpy(entries[i].text, "total accuracy: N/A", sizeof(entries[i].text)); + } + } i++; } } static void G_Menu_PMStats(gentity_t *ent, menu_hnd_t *p) { P_Menu_Close(ent); - P_Menu_Open(ent, pmstatsmenu, -1, sizeof(pmstatsmenu) / sizeof(menu_t), nullptr, G_Menu_PMStats_Update); + P_Menu_Open(ent, pmstatsmenu, -1, sizeof(pmstatsmenu) / sizeof(menu_t), nullptr, false, G_Menu_PMStats_Update); } /*-----------------------------------------------------------------------*/ @@ -460,11 +494,24 @@ const menu_t pmcallvotemenu_timelimit[] = { }; void G_Menu_CallVote_Map_Selection(gentity_t *ent, menu_hnd_t *p) { - vcmds_t *cc = FindVoteCmdByName("map"); + if (!cc) { + gi.Com_PrintFmt("{}: missing map vote command.\n", __FUNCTION__); + return; + } + if (!p || !p->entries || p->cur < 0 || p->cur >= p->num) { + gi.Com_PrintFmt("{}: invalid map selection index.\n", __FUNCTION__); + return; + } + + const menu_t &selected = p->entries[p->cur]; + if (!selected.text[0]) { + gi.Com_PrintFmt("{}: no map selected.\n", __FUNCTION__); + return; + } level.vote = cc; - level.vote_arg = std::string("q2dm1"); //TODO: store selected map name for use here + level.vote_arg = selected.text; VoteCommandStore(ent); P_Menu_Close(ent); @@ -472,11 +519,21 @@ void G_Menu_CallVote_Map_Selection(gentity_t *ent, menu_hnd_t *p) { inline std::vector str_split(const std::string_view &str, char by) { std::vector out; - size_t start, end = 0; + size_t start = 0; + + while (true) { + start = str.find_first_not_of(by, start); + if (start == std::string_view::npos) + break; - while ((start = str.find_first_not_of(by, end)) != std::string_view::npos) { - end = str.find(by, start); - out.push_back(std::string{ str.substr(start, end - start) }); + size_t end = str.find(by, start); + if (end == std::string_view::npos) { + out.emplace_back(str.substr(start)); + break; + } + + out.emplace_back(str.substr(start, end - start)); + start = end + 1; } return out; @@ -493,10 +550,12 @@ static void G_Menu_CallVote_Map_Update(gentity_t *ent) { if (!values.size()) return; - for (i = 2; i < 15; i++) + for (i = 2; i < 15; i++) { entries[i].SelectFunc = nullptr; + entries[i].text[0] = '\0'; + } - for (num = 0, i = 2; num < values.size(), num < 15; num++, i++) { + for (num = 0, i = 2; num < values.size() && num < 15; num++, i++) { Q_strlcpy(entries[i].text, values[num].c_str(), sizeof(entries[i].text)); entries[i].SelectFunc = G_Menu_CallVote_Map_Selection; } @@ -504,19 +563,19 @@ static void G_Menu_CallVote_Map_Update(gentity_t *ent) { void G_Menu_CallVote_Map(gentity_t *ent, menu_hnd_t *p) { P_Menu_Close(ent); - P_Menu_Open(ent, pmcallvotemenu_map, -1, sizeof(pmcallvotemenu_map) / sizeof(menu_t), nullptr, G_Menu_CallVote_Map_Update); + P_Menu_Open(ent, pmcallvotemenu_map, -1, sizeof(pmcallvotemenu_map) / sizeof(menu_t), nullptr, false, G_Menu_CallVote_Map_Update); } void G_Menu_CallVote_NextMap(gentity_t *ent, menu_hnd_t *p) { level.vote = FindVoteCmdByName("nextmap"); - level.vote_arg = nullptr; + level.vote_arg.clear(); VoteCommandStore(ent); P_Menu_Close(ent); } void G_Menu_CallVote_Restart(gentity_t *ent, menu_hnd_t *p) { level.vote = FindVoteCmdByName("restart"); - level.vote_arg = nullptr; + level.vote_arg.clear(); VoteCommandStore(ent); P_Menu_Close(ent); } @@ -527,15 +586,15 @@ void G_Menu_CallVote_GameType(gentity_t *ent, menu_hnd_t *p) { void G_Menu_CallVote_TimeLimit_Update(gentity_t *ent) { - level.vote_arg = nullptr; + level.vote_arg.clear(); } void G_Menu_CallVote_TimeLimit(gentity_t *ent, menu_hnd_t *p) { //level.vote = FindVoteCmdByName("timelimit"); - //level.vote_arg = nullptr; + //level.vote_arg.clear(); //VoteCommandStore(ent); P_Menu_Close(ent); - P_Menu_Open(ent, pmcallvotemenu_timelimit, -1, sizeof(pmcallvotemenu_timelimit) / sizeof(menu_t), nullptr, G_Menu_CallVote_TimeLimit_Update); + P_Menu_Open(ent, pmcallvotemenu_timelimit, -1, sizeof(pmcallvotemenu_timelimit) / sizeof(menu_t), nullptr, false, G_Menu_CallVote_TimeLimit_Update); } void G_Menu_CallVote_ScoreLimit(gentity_t *ent, menu_hnd_t *p) { @@ -544,14 +603,14 @@ void G_Menu_CallVote_ScoreLimit(gentity_t *ent, menu_hnd_t *p) { void G_Menu_CallVote_ShuffleTeams(gentity_t *ent, menu_hnd_t *p) { level.vote = FindVoteCmdByName("shuffle"); - level.vote_arg = nullptr; + level.vote_arg.clear(); VoteCommandStore(ent); P_Menu_Close(ent); } void G_Menu_CallVote_BalanceTeams(gentity_t *ent, menu_hnd_t *p) { level.vote = FindVoteCmdByName("balance"); - level.vote_arg = nullptr; + level.vote_arg.clear(); VoteCommandStore(ent); P_Menu_Close(ent); @@ -563,7 +622,7 @@ void G_Menu_CallVote_Unlagged(gentity_t *ent, menu_hnd_t *p) { void G_Menu_CallVote_Cointoss(gentity_t *ent, menu_hnd_t *p) { level.vote = FindVoteCmdByName("cointoss"); - level.vote_arg = nullptr; + level.vote_arg.clear(); VoteCommandStore(ent); P_Menu_Close(ent); } @@ -606,7 +665,7 @@ static void G_Menu_CallVote_Update(gentity_t *ent) { static void G_Menu_CallVote(gentity_t *ent, menu_hnd_t *p) { P_Menu_Close(ent); - P_Menu_Open(ent, pmcallvotemenu, -1, sizeof(pmcallvotemenu) / sizeof(menu_t), nullptr, G_Menu_CallVote_Update); + P_Menu_Open(ent, pmcallvotemenu, -1, sizeof(pmcallvotemenu) / sizeof(menu_t), nullptr, false, G_Menu_CallVote_Update); } /*-----------------------------------------------------------------------*/ @@ -691,7 +750,7 @@ static void G_Menu_Vote_Update(gentity_t *ent) { } void G_Menu_Vote_Open(gentity_t *ent) { - P_Menu_Open(ent, votemenu, -1, sizeof(votemenu) / sizeof(menu_t), nullptr, G_Menu_Vote_Update); + P_Menu_Open(ent, votemenu, -1, sizeof(votemenu) / sizeof(menu_t), nullptr, false, G_Menu_Vote_Update); } @@ -866,7 +925,7 @@ void G_Menu_ChaseCam(gentity_t *ent, menu_hnd_t *p) { GetFollowTarget(ent); P_Menu_Close(ent); - P_Menu_Open(ent, nochasemenu, -1, sizeof(nochasemenu) / sizeof(menu_t), nullptr, G_Menu_NoChaseCamUpdate); + P_Menu_Open(ent, nochasemenu, -1, sizeof(nochasemenu) / sizeof(menu_t), nullptr, false, G_Menu_NoChaseCamUpdate); } void G_Menu_ReturnToMain(gentity_t *ent, menu_hnd_t *p) { @@ -914,7 +973,7 @@ static void G_Menu_HostInfo_Update(gentity_t *ent) { void G_Menu_HostInfo(gentity_t *ent, menu_hnd_t *p) { P_Menu_Close(ent); - P_Menu_Open(ent, hostinfomenu, -1, sizeof(hostinfomenu) / sizeof(menu_t), nullptr, G_Menu_HostInfo_Update); + P_Menu_Open(ent, hostinfomenu, -1, sizeof(hostinfomenu) / sizeof(menu_t), nullptr, false, G_Menu_HostInfo_Update); } static void G_Menu_ServerInfo_Update(gentity_t *ent) { @@ -1128,7 +1187,7 @@ static void G_Menu_ServerInfo_Update(gentity_t *ent) { void G_Menu_ServerInfo(gentity_t *ent, menu_hnd_t *p) { P_Menu_Close(ent); - P_Menu_Open(ent, svinfomenu, -1, sizeof(svinfomenu) / sizeof(menu_t), nullptr, G_Menu_ServerInfo_Update); + P_Menu_Open(ent, svinfomenu, -1, sizeof(svinfomenu) / sizeof(menu_t), nullptr, false, G_Menu_ServerInfo_Update); } static void G_Menu_GameRules_Update(gentity_t *ent) { @@ -1143,7 +1202,7 @@ static void G_Menu_GameRules_Update(gentity_t *ent) { static void G_Menu_GameRules(gentity_t *ent, menu_hnd_t *p) { P_Menu_Close(ent); - P_Menu_Open(ent, svinfomenu, -1, sizeof(svinfomenu) / sizeof(menu_t), nullptr, G_Menu_GameRules_Update); + P_Menu_Open(ent, svinfomenu, -1, sizeof(svinfomenu) / sizeof(menu_t), nullptr, false, G_Menu_GameRules_Update); } static void G_Menu_Join_Update(gentity_t *ent) { @@ -1308,8 +1367,8 @@ void G_Menu_Join_Open(gentity_t *ent) { else team = brandom() ? TEAM_RED : TEAM_BLUE; - P_Menu_Open(ent, teams_join_menu, team, sizeof(teams_join_menu) / sizeof(menu_t), nullptr, G_Menu_Join_Update); + P_Menu_Open(ent, teams_join_menu, team, sizeof(teams_join_menu) / sizeof(menu_t), nullptr, false, G_Menu_Join_Update); } else { - P_Menu_Open(ent, free_join_menu, TEAM_FREE, sizeof(free_join_menu) / sizeof(menu_t), nullptr, G_Menu_Join_Update); + P_Menu_Open(ent, free_join_menu, TEAM_FREE, sizeof(free_join_menu) / sizeof(menu_t), nullptr, false, G_Menu_Join_Update); } } diff --git a/src/g_misc.cpp b/src/g_misc.cpp index 7436f1c..652d0c8 100644 --- a/src/g_misc.cpp +++ b/src/g_misc.cpp @@ -494,6 +494,14 @@ struct shadow_light_info_t { static shadow_light_info_t shadowlightinfo[MAX_SHADOW_LIGHTS]; +/* +============= +GetShadowLightData + +Returns the shadow light data for the specified entity number or nullptr when +no matching entry exists. +============= +*/ const shadow_light_data_t *GetShadowLightData(int32_t entity_number) { for (size_t i = 0; i < level.shadow_light_count; i++) { if (shadowlightinfo[i].entity_number == entity_number) @@ -503,7 +511,20 @@ const shadow_light_data_t *GetShadowLightData(int32_t entity_number) { return nullptr; } +/* +============= +setup_shadow_lights + +Initializes shadow light data and configstrings while clamping to the maximum +allowed lights to prevent out-of-bounds access. +============= +*/ void setup_shadow_lights() { + if (level.shadow_light_count < 0) + level.shadow_light_count = 0; + else if (level.shadow_light_count > MAX_SHADOW_LIGHTS) + level.shadow_light_count = MAX_SHADOW_LIGHTS; + for (int i = 0; i < level.shadow_light_count; ++i) { gentity_t *self = &g_entities[shadowlightinfo[i].entity_number]; @@ -544,7 +565,20 @@ void setup_shadow_lights() { // lights to be ordered wrong on return levels // if the spawn functions are changed. // this will work without changing the save/load code. +/* +============= +G_LoadShadowLights + +Restores shadow light data from configstrings while ensuring the light count +stays within valid bounds. +============= +*/ void G_LoadShadowLights() { + if (level.shadow_light_count < 0) + level.shadow_light_count = 0; + else if (level.shadow_light_count > MAX_SHADOW_LIGHTS) + level.shadow_light_count = MAX_SHADOW_LIGHTS; + for (size_t i = 0; i < level.shadow_light_count; i++) { const char *cstr = gi.get_configstring(CS_SHADOWLIGHTS + i); const char *token = COM_ParseEx(&cstr, ";"); @@ -589,9 +623,21 @@ void G_LoadShadowLights() { } // --------------------------------------------------------------------------------- + +/* +============= +setup_dynamic_light + +Initializes a dynamic light entity and tracks it for shadow handling while +respecting the maximum supported light count. +============= +*/ static void setup_dynamic_light(gentity_t *self) { // [Sam-KEX] Shadow stuff if (st.sl.data.radius > 0) { + if (level.shadow_light_count >= MAX_SHADOW_LIGHTS) + return; + self->s.renderfx = RF_CASTSHADOW; self->itemtarget = st.sl.lightstyletarget; @@ -2609,3 +2655,105 @@ void SP_misc_nuke_core(gentity_t *ent) { ent->use = misc_nuke_core_use; } + +/* +================= +misc_prox +================= +*/ + +constexpr gtime_t PROX_TIME_DELAY = 500_ms; +constexpr float PROX_DAMAGE_RADIUS = 192; +constexpr int32_t PROX_HEALTH = 20; +constexpr int32_t PROX_DAMAGE = 60; + +void Prox_Explode(gentity_t* ent); +void prox_die(gentity_t* self, gentity_t* inflictor, gentity_t* attacker, int damage, const vec3_t& point, const mod_t& mod); + +THINK(misc_prox_seek) (gentity_t* ent) -> void +{ + gentity_t* target = nullptr; + gentity_t* best = nullptr; + vec3_t vec; + float len; + float oldlen = 8000; + + while ((target = findradius(target, ent->s.origin, PROX_DAMAGE_RADIUS)) != nullptr) { + if (target == ent) + continue; + + if (!target->client && !(target->monsterinfo.aiflags & AI_GOOD_GUY)) + continue; + + if (target->health <= 0) + continue; + + if (!visible(ent, target)) + continue; + + vec = ent->s.origin - target->s.origin; + len = vec.length(); + + if (!best) { + best = target; + oldlen = len; + continue; + } + if (len < oldlen) { + oldlen = len; + best = target; + } + } + + if (best) { + gi.sound(ent, CHAN_VOICE, gi.soundindex("weapons/proxwarn.wav"), 1, ATTN_NORM, 0); + ent->think = Prox_Explode; + ent->nextthink = level.time + PROX_TIME_DELAY; + return; + } + + ent->nextthink = level.time + 10_hz; +} + +THINK(misc_prox_activate) (gentity_t* ent) -> void +{ + gi.Com_Print("check 3!\n"); + ent->s.frame = 9; + ent->s.skinnum = 3; + ent->think = misc_prox_seek; + ent->nextthink = level.time + 10_hz; +} + +void SP_misc_prox(gentity_t* ent) +{ + gi.Com_Print("check 1!\n"); + if (deathmatch->integer) { + G_FreeEntity(ent); + return; + } + + if (!ent->health) + ent->health = PROX_HEALTH; + + if (!ent->dmg) + ent->dmg = PROX_DAMAGE; + + gi.Com_Print("check 2!\n"); + ent->s.modelindex = gi.modelindex("models/weapons/g_prox/tris.md2"); + ent->s.frame = 9; + ent->s.skinnum = 3; + ent->mins = { -6, -6, -6 }; + ent->maxs = { 6, 6, 6 }; + ent->movetype = MOVETYPE_NONE; + ent->solid = SOLID_BBOX; + ent->takedamage = true; + ent->die = prox_die; + ent->classname = "prox_mine"; + ent->flags |= (FL_DAMAGEABLE | FL_TRAP | FL_MECHANICAL); + ent->s.renderfx |= RF_IR_VISIBLE; + + ent->think = misc_prox_activate; + ent->nextthink = level.time + 1_sec; + + gi.linkentity(ent); +} diff --git a/src/g_monster.cpp b/src/g_monster.cpp index 882eab7..75a82e2 100644 --- a/src/g_monster.cpp +++ b/src/g_monster.cpp @@ -307,24 +307,26 @@ void M_WorldEffects(gentity_t *ent) { } } -bool M_droptofloor_generic(vec3_t &origin, const vec3_t &mins, const vec3_t &maxs, bool ceiling, gentity_t *ignore, contents_t mask, bool allow_partial) { - vec3_t end; - trace_t trace; +/* +============= +M_droptofloor_generic - if (gi.trace(origin, mins, maxs, origin, ignore, mask).startsolid) { - if (!ceiling) - origin[2] += 1; - else - origin[2] -= 1; - } +Drops an origin along the provided gravity vector until contact is made or a blocking +volume is found. +============= +*/ +bool M_droptofloor_generic(vec3_t &origin, const vec3_t &mins, const vec3_t &maxs, const vec3_t &gravityVector, gentity_t *ignore, contents_t mask, bool allow_partial) { + vec3_t gravity_dir = gravityVector.normalized(); - if (!ceiling) { - end = origin; - end[2] -= 256; - } else { - end = origin; - end[2] += 256; - } + if (!gravity_dir) + gravity_dir = { 0.0f, 0.0f, -1.0f }; + + trace_t trace = gi.trace(origin, mins, maxs, origin, ignore, mask); + + if (trace.startsolid) + origin -= gravity_dir; + + vec3_t end = origin + (gravity_dir * 256.0f); trace = gi.trace(origin, mins, maxs, end, ignore, mask); @@ -336,11 +338,12 @@ bool M_droptofloor_generic(vec3_t &origin, const vec3_t &mins, const vec3_t &max return true; } + bool M_droptofloor(gentity_t *ent) { contents_t mask = G_GetClipMask(ent); if (!ent->spawnflags.has(SPAWNFLAG_MONSTER_NO_DROP)) { - if (!M_droptofloor_generic(ent->s.origin, ent->mins, ent->maxs, ent->gravityVector[2] > 0, ent, mask, true)) + if (!M_droptofloor_generic(ent->s.origin, ent->mins, ent->maxs, ent->gravityVector, ent, mask, true)) return false; } else { if (gi.trace(ent->s.origin, ent->mins, ent->maxs, ent->s.origin, ent, mask).startsolid) @@ -354,6 +357,7 @@ bool M_droptofloor(gentity_t *ent) { return true; } + void M_SetEffects(gentity_t *ent) { ent->s.effects &= ~(EF_COLOR_SHELL | EF_POWERSCREEN | EF_DOUBLE | EF_QUAD | EF_PENT | EF_FLIES); ent->s.renderfx &= ~(RF_SHELL_RED | RF_SHELL_GREEN | RF_SHELL_BLUE | RF_SHELL_DOUBLE); @@ -948,12 +952,41 @@ USE(monster_use) (gentity_t *self, gentity_t *other, gentity_t *activator) -> vo void monster_start_go(gentity_t *self); +/* +============= +monster_clear_trigger_spawn_state + +Clears trigger-spawn state so the monster behaves like a standard spawn. +============= +*/ +static void monster_clear_trigger_spawn_state(gentity_t *self) { + self->svflags &= ~SVF_NOCLIENT; + + if (self->spawnflags.has(SPAWNFLAG_MONSTER_TRIGGER_SPAWN)) + self->spawnflags &= ~SPAWNFLAG_MONSTER_TRIGGER_SPAWN; + + if (self->monsterinfo.aiflags & AI_DO_NOT_COUNT) { + self->monsterinfo.aiflags &= ~AI_DO_NOT_COUNT; + + if (!self->spawnflags.has(SPAWNFLAG_MONSTER_DEAD)) { + if (g_debug_monster_kills->integer) + level.monsters_registered[level.total_monsters] = self; + level.total_monsters++; + } + } +} + +/* +============= +monster_triggered_spawn +============= +*/ static THINK(monster_triggered_spawn) (gentity_t *self) -> void { self->s.origin[2] += 1; self->solid = SOLID_BBOX; self->movetype = MOVETYPE_STEP; - self->svflags &= ~SVF_NOCLIENT; + monster_clear_trigger_spawn_state(self); self->air_finished = level.time + 12_sec; gi.linkentity(self); @@ -1620,18 +1653,22 @@ void monster_fire_heatbeam(gentity_t *self, const vec3_t &start, const vec3_t &d void stationarymonster_start_go(gentity_t *self); +/* +============= +stationarymonster_triggered_spawn + +Activates a trigger-spawned stationary monster and converts it to standard behavior. +============= +*/ static THINK(stationarymonster_triggered_spawn) (gentity_t *self) -> void { self->solid = SOLID_BBOX; self->movetype = MOVETYPE_NONE; - self->svflags &= ~SVF_NOCLIENT; + monster_clear_trigger_spawn_state(self); self->air_finished = level.time + 12_sec; gi.linkentity(self); KillBox(self, false); - // FIXME - why doesn't this happen with real monsters? - self->spawnflags &= ~SPAWNFLAG_MONSTER_TRIGGER_SPAWN; - stationarymonster_start_go(self); if (self->enemy && !(self->spawnflags & SPAWNFLAG_MONSTER_AMBUSH) && !(self->enemy->flags & FL_NOTARGET)) { @@ -1644,6 +1681,13 @@ static THINK(stationarymonster_triggered_spawn) (gentity_t *self) -> void { } } +/* +============= +stationarymonster_triggered_spawn_use + +Entry point when a trigger fires a stationary monster. +============= +*/ static USE(stationarymonster_triggered_spawn_use) (gentity_t *self, gentity_t *other, gentity_t *activator) -> void { // we have a one frame delay here so we don't telefrag the guy who activated us self->think = stationarymonster_triggered_spawn; diff --git a/src/g_monster_spawn.cpp b/src/g_monster_spawn.cpp index d47e7f8..299ad1e 100644 --- a/src/g_monster_spawn.cpp +++ b/src/g_monster_spawn.cpp @@ -21,20 +21,25 @@ // FIXME - for the black widow, if we want the stalkers coming in on the roof, we'll have to tweak some things -// -// CreateMonster -// +/* +============= +CreateMonster + +Spawns a monster entity with default downward gravity. +============= +*/ +#ifndef MONSTER_SPAWN_TESTS gentity_t *CreateMonster(const vec3_t &origin, const vec3_t &angles, const char *classname) { gentity_t *newEnt; - + newEnt = G_Spawn(); - + newEnt->s.origin = origin; newEnt->s.angles = angles; newEnt->classname = classname; newEnt->monsterinfo.aiflags |= AI_DO_NOT_COUNT; - + newEnt->gravityVector = { 0, 0, -1 }; ED_CallSpawn(newEnt); newEnt->s.renderfx |= RF_IR_VISIBLE; @@ -42,14 +47,44 @@ gentity_t *CreateMonster(const vec3_t &origin, const vec3_t &angles, const char return newEnt; } +#else +/* +============= +CreateMonster + +Provided by unit tests when MONSTER_SPAWN_TESTS is defined. +============= +*/ +gentity_t *CreateMonster(const vec3_t &origin, const vec3_t &angles, const char *classname); +#endif + +/* +============= +CreateFlyMonster + +Validates a spawn point for a flying monster and creates it if clear. +============= +*/ gentity_t *CreateFlyMonster(const vec3_t &origin, const vec3_t &angles, const vec3_t &mins, const vec3_t &maxs, const char *classname) { - if (!CheckSpawnPoint(origin, mins, maxs)) + if (!CheckSpawnPoint(origin, mins, maxs, { 0.0f, 0.0f, -1.0f })) + return nullptr; + + gentity_t *newEnt = CreateMonster(origin, angles, classname); + + if (!newEnt) return nullptr; - return (CreateMonster(origin, angles, classname)); + return newEnt; } +/* +============= +CreateGroundMonster + +Checks ground viability before creating a walking monster. +============= +*/ // This is just a wrapper for CreateMonster that looks down height # of CMUs and sees if there // are bad things down there or not @@ -58,7 +93,7 @@ gentity_t *CreateGroundMonster(const vec3_t &origin, const vec3_t &angles, const gentity_t *newEnt; // check the ground to make sure it's there, it's relatively flat, and it's not toxic - if (!CheckGroundSpawnPoint(origin, entMins, entMaxs, height, -1.f)) + if (!CheckGroundSpawnPoint(origin, entMins, entMaxs, height, { 0.0f, 0.0f, -1.0f })) return nullptr; newEnt = CreateMonster(origin, angles, classname); @@ -68,50 +103,113 @@ gentity_t *CreateGroundMonster(const vec3_t &origin, const vec3_t &angles, const return newEnt; } -// FindSpawnPoint -// PMM - this is used by the medic commander (possibly by the carrier) to find a good spawn point -// if the startpoint is bad, try above the startpoint for a bit -bool FindSpawnPoint(const vec3_t &startpoint, const vec3_t &mins, const vec3_t &maxs, vec3_t &spawnpoint, float maxMoveUp, bool drop) + +/* +============= +FindSpawnPoint + +PMM - this is used by the medic commander (possibly by the carrier) to find a good spawn point. +If the start point is bad, try above the start point for a bit. +============= +*/ +bool FindSpawnPoint(const vec3_t &startpoint, const vec3_t &mins, const vec3_t &maxs, vec3_t &spawnpoint, float maxMoveUp, bool drop, const vec3_t &gravityVector) { spawnpoint = startpoint; - // drop first - if (!drop || !M_droptofloor_generic(spawnpoint, mins, maxs, false, nullptr, MASK_MONSTERSOLID, false)) + vec3_t gravity_dir = gravityVector.normalized(); + + if (!gravity_dir) + gravity_dir = { 0.0f, 0.0f, -1.0f }; + + auto try_drop = [&] (vec3_t testpoint) -> bool { - spawnpoint = startpoint; + spawnpoint = testpoint; + + if (!CheckSpawnPoint(spawnpoint, mins, maxs, gravityVector)) + return false; + + if (!drop) + return true; + + if (M_droptofloor_generic(spawnpoint, mins, maxs, gravity_dir, nullptr, MASK_MONSTERSOLID, false)) + return true; + + spawnpoint = testpoint; - // fix stuck if we couldn't drop initially if (G_FixStuckObject_Generic(spawnpoint, mins, maxs, [] (const vec3_t &start, const vec3_t &mins, const vec3_t &maxs, const vec3_t &end) { return gi.trace(start, mins, maxs, end, nullptr, MASK_MONSTERSOLID); - }) == stuck_result_t::NO_GOOD_POSITION) - return false; + }) != stuck_result_t::NO_GOOD_POSITION) + { + return !drop || M_droptofloor_generic(spawnpoint, mins, maxs, gravity_dir, nullptr, MASK_MONSTERSOLID, false); + } + + spawnpoint = { 0.0f, 0.0f, 0.0f }; + return false; + }; + + // drop first + if (try_drop(startpoint)) + return true; + + vec3_t against_gravity = gravity_dir * -1.0f; + float move_amount = 16.0f; + + while (move_amount <= maxMoveUp) + { + vec3_t raised_start = startpoint + (against_gravity * move_amount); + + if (try_drop(raised_start)) + return true; - // fixed, so drop again - if (drop && !M_droptofloor_generic(spawnpoint, mins, maxs, false, nullptr, MASK_MONSTERSOLID, false)) - return false; // ??? + move_amount += 16.0f; } - return true; + return false; } + // FIXME - all of this needs to be tweaked to handle the new gravity rules // if we ever want to spawn stuff on the roof -// -// CheckSpawnPoint -// -// PMM - checks volume to make sure we can spawn a monster there (is it solid?) -// -// This is all fliers should need +/* +============= +BoxExtentAlongDirection + +Returns the projection of the bounding box extents along the specified direction. +============= +*/ +static float BoxExtentAlongDirection(const vec3_t &mins, const vec3_t &maxs, const vec3_t &dir) +{ + vec3_t corner { + dir[0] >= 0.0f ? maxs[0] : mins[0], + dir[1] >= 0.0f ? maxs[1] : mins[1], + dir[2] >= 0.0f ? maxs[2] : mins[2] + }; + + return DotProduct(corner, dir); +} + +/* +============= +CheckSpawnPoint -bool CheckSpawnPoint(const vec3_t &origin, const vec3_t &mins, const vec3_t &maxs) +PMM - checks volume to make sure we can spawn a monster there (is it solid?) +This is all fliers should need. +============= +*/ +bool CheckSpawnPoint(const vec3_t &origin, const vec3_t &mins, const vec3_t &maxs, const vec3_t &gravityVector) { trace_t tr; if (!mins || !maxs) return false; + vec3_t gravity_dir = gravityVector.normalized(); + + if (!gravity_dir) + gravity_dir = { 0.0f, 0.0f, -1.0f }; + tr = gi.trace(origin, mins, maxs, origin, nullptr, MASK_MONSTERSOLID); if (tr.startsolid || tr.allsolid) return false; @@ -119,33 +217,113 @@ bool CheckSpawnPoint(const vec3_t &origin, const vec3_t &mins, const vec3_t &max if (tr.ent != world) return false; + const float along_gravity_extent = BoxExtentAlongDirection(mins, maxs, gravity_dir); + const float against_gravity_extent = BoxExtentAlongDirection(mins, maxs, -gravity_dir); + + if (against_gravity_extent > 0.0f) + { + vec3_t upward_check = origin - (gravity_dir * against_gravity_extent); + + tr = gi.trace(origin, mins, maxs, upward_check, nullptr, MASK_MONSTERSOLID); + if (tr.startsolid || tr.allsolid) + return false; + } + + if (along_gravity_extent > 0.0f) + { + vec3_t downward_check = origin + (gravity_dir * along_gravity_extent); + + tr = gi.trace(origin, mins, maxs, downward_check, nullptr, MASK_MONSTERSOLID); + if (tr.startsolid || tr.allsolid) + return false; + } + return true; } -// -// CheckGroundSpawnPoint -// -// PMM - used for walking monsters -// checks: -// 1) is there a ground within the specified height of the origin? -// 2) is the ground non-water? -// 3) is the ground flat enough to walk on? -// +/* +============= +SpawnCheckAndDropToFloor -bool CheckGroundSpawnPoint(const vec3_t &origin, const vec3_t &entMins, const vec3_t &entMaxs, float height, float gravity) +Checks spawn clearance along the provided gravity vector and drops the origin to the nearest surface. +============= +*/ +bool SpawnCheckAndDropToFloor(vec3_t &origin, const vec3_t &mins, const vec3_t &maxs, const vec3_t &gravityVector) { - if (!CheckSpawnPoint(origin, entMins, entMaxs)) + vec3_t gravity_dir = gravityVector.normalized(); + + if (!gravity_dir) + gravity_dir = { 0.0f, 0.0f, -1.0f }; + + const float along_gravity_extent = BoxExtentAlongDirection(mins, maxs, gravity_dir); + const float against_gravity_extent = BoxExtentAlongDirection(mins, maxs, -gravity_dir); + + if (!CheckSpawnPoint(origin, mins, maxs, gravity_dir)) + return false; + + vec3_t trace_start = origin - (gravity_dir * against_gravity_extent); + vec3_t trace_end = origin + (gravity_dir * (along_gravity_extent + against_gravity_extent + 256.0f)); + + trace_t tr = gi.trace(trace_start, mins, maxs, trace_end, nullptr, MASK_MONSTERSOLID); + + if (tr.startsolid || tr.allsolid) return false; - if (M_CheckBottom_Fast_Generic(origin + entMins, origin + entMaxs, false)) + if (tr.fraction == 1.0f) + return false; + + origin = tr.endpos + (gravity_dir * against_gravity_extent); + + return CheckSpawnPoint(origin, mins, maxs, gravity_dir); +} + +/* +============= +CheckGroundSpawnPoint + +PMM - used for walking monsters +checks: + 1) is there a ground within the specified height of the origin? + 2) is the ground non-water? + 3) is the ground flat enough to walk on? +============= +*/ + +bool CheckGroundSpawnPoint(const vec3_t &origin, const vec3_t &entMins, const vec3_t &entMaxs, float height, const vec3_t &gravityVector) +{ + vec3_t gravity_dir = gravityVector.normalized(); + + if (!gravity_dir) + gravity_dir = { 0.0f, 0.0f, -1.0f }; + + // don't spawn in or above water + if (gi.pointcontents(origin) & MASK_WATER) + return false; + + trace_t contents_trace = gi.trace(origin, entMins, entMaxs, origin + (gravity_dir * height), nullptr, MASK_WATER); + + if ((contents_trace.contents & MASK_WATER) || contents_trace.startsolid || contents_trace.allsolid) + return false; + + if (!CheckSpawnPoint(origin, entMins, entMaxs, gravityVector)) + return false; + + trace_t trace = gi.trace(origin, entMins, entMaxs, origin + (gravity_dir * height), nullptr, MASK_MONSTERSOLID); + + if (trace.fraction == 1.0f) + return false; + + if (M_CheckBottom_Fast_Generic(origin + entMins, origin + entMaxs, gravityVector)) return true; - if (M_CheckBottom_Slow_Generic(origin, entMins, entMaxs, nullptr, MASK_MONSTERSOLID, false, false)) + if (M_CheckBottom_Slow_Generic(origin, entMins, entMaxs, nullptr, MASK_MONSTERSOLID, gravityVector, false)) return true; - return false; + return false; } + +#ifndef MONSTER_SPAWN_TESTS // **************************** // SPAWNGROW stuff // **************************** @@ -361,3 +539,4 @@ void Widowlegs_Spawn(const vec3_t &startpos, const vec3_t &angles) ent->nextthink = level.time + 10_hz; gi.linkentity(ent); } +#endif // MONSTER_SPAWN_TESTS diff --git a/src/g_phys.cpp b/src/g_phys.cpp index fe82964..c8a8e20 100644 --- a/src/g_phys.cpp +++ b/src/g_phys.cpp @@ -2,7 +2,9 @@ // Licensed under the GNU General Public License 2.0. // g_phys.c -#include "g_local.h" +#include + +#include "g_runthink.h" /* @@ -93,19 +95,16 @@ Runs thinking code for this frame if necessary ============= */ bool G_RunThink(gentity_t *ent) { - gtime_t thinktime = ent->nextthink; - if (thinktime <= 0_ms) - return true; - if (thinktime > level.time) - return true; - - ent->nextthink = 0_ms; - if (!ent->think) - //gi.Com_Error("nullptr ent->think"); - return false; //true; - ent->think(ent); - - return false; + return G_RunThinkImpl( + ent, + level.time, + [](gentity_t *warn_ent) { + const char *name = warn_ent->classname ? warn_ent->classname : ""; + gi.Com_PrintFmt("G_RunThink: null think function for entity \"{}\"\n", name); + }, + [](gentity_t *warn_ent) { + G_FreeEntity(warn_ent); + }); } /* @@ -134,6 +133,7 @@ The basic solid body movement clip that slides along multiple planes */ void G_FlyMove(gentity_t *ent, float time, contents_t mask) { ent->groundentity = nullptr; + ent->groundentity_linkcount = 0; touch_list_t touch; PM_StepSlideMove_Generic(ent->s.origin, ent->velocity, time, ent->mins, ent->maxs, touch, false, [&](const vec3_t &start, const vec3_t &mins, const vec3_t &maxs, const vec3_t &end) { @@ -144,8 +144,13 @@ void G_FlyMove(gentity_t *ent, float time, contents_t mask) { auto &trace = touch.traces[i]; if (trace.plane.normal[2] > 0.7f) { - ent->groundentity = trace.ent; - ent->groundentity_linkcount = trace.ent->linkcount; + if (trace.ent && trace.ent->inuse) { + ent->groundentity = trace.ent; + ent->groundentity_linkcount = trace.ent->linkcount; + } else { + ent->groundentity = nullptr; + ent->groundentity_linkcount = 0; + } } // @@ -189,6 +194,7 @@ Does not change the entities velocity at all static trace_t G_PushEntity(gentity_t *ent, const vec3_t &push) { vec3_t start = ent->s.origin; vec3_t end = start + push; + float saved_gravity = ent->gravity; trace_t trace = gi.trace(start, ent->mins, ent->maxs, end, ent, G_GetClipMask(ent)); @@ -207,8 +213,8 @@ static trace_t G_PushEntity(gentity_t *ent, const vec3_t &push) { } } - // FIXME - is this needed? - ent->gravity = 1.0; + if (ent->gravity != saved_gravity) + ent->gravity = saved_gravity; if (ent->inuse) G_TouchTriggers(ent); @@ -367,9 +373,30 @@ static bool G_Push(gentity_t *pusher, vec3_t &move, vec3_t &amove) { } // FIXME: is there a better way to handle this? - // see if anything we moved has touched a trigger - for (p = pushed_p - 1; p >= pushed; p--) + // see if anything we moved has touched a trigger, but avoid + // invoking callbacks multiple times for the same entity in a + // single push. + gentity_t *touched[MAX_ENTITIES]; + uint32_t num_touched = 0; + + for (p = pushed_p - 1; p >= pushed; p--) { + bool already_touched = false; + + for (uint32_t i = 0; i < num_touched; i++) { + if (touched[i] == p->ent) { + already_touched = true; + break; + } + } + + if (already_touched) + continue; + + if (num_touched < std::size(touched)) + touched[num_touched++] = p->ent; + G_TouchTriggers(p->ent); + } return true; } diff --git a/src/g_runthink.h b/src/g_runthink.h new file mode 100644 index 0000000..6f44b8c --- /dev/null +++ b/src/g_runthink.h @@ -0,0 +1,33 @@ +// Copyright (c) ZeniMax Media Inc. +// Licensed under the GNU General Public License 2.0. + +#pragma once + +#include "g_local.h" + +/* +============= +G_RunThinkImpl + +Shared implementation for running entity think functions +============= +*/ +template +bool G_RunThinkImpl(Entity *ent, const gtime_t ¤t_time, Logger &&log_warning, Fallback &&fallback_action) { + gtime_t thinktime = ent->nextthink; + if (thinktime <= 0_ms) + return true; + if (thinktime > current_time) + return true; + + ent->nextthink = 0_ms; + if (!ent->think) { + log_warning(ent); + fallback_action(ent); + return false; + } + + ent->think(ent); + + return false; +} diff --git a/src/g_save.cpp b/src/g_save.cpp index 268212c..8c8d9d1 100644 --- a/src/g_save.cpp +++ b/src/g_save.cpp @@ -24,7 +24,26 @@ // does have some C-isms in here. constexpr size_t SAVE_FORMAT_VERSION = 1; +/* +============= +GetEngineBuildString + +Fetches the engine build string from the version cvar. +============= +*/ +static const char *GetEngineBuildString() { + cvar_t *engine_version = gi.cvar("version", "", 0); + + if (!engine_version || !engine_version->string) + return ""; + + return engine_version->string; +} + #include +#include +#include +#include // Professor Daniel J. Bernstein; https://www.partow.net/programming/hashfunctions/#APHashFunction MIT struct cstring_hash { @@ -57,9 +76,38 @@ static const save_data_list_t *list_head = nullptr; static std::unordered_map list_hash; static std::unordered_map list_str_hash; static std::unordered_map, const save_data_list_t *, ptr_tag_hash> list_from_ptr_hash; +static std::vector> list_name_storage; + +/* +============= +StoreListNameCopy + +Duplicates a save data name for safe storage in the string hash map. +============= +*/ +static const char *StoreListNameCopy(const char *name) { + if (!name) + return nullptr; + + const size_t name_len = strlen(name) + 1; + std::unique_ptr stored_name = std::make_unique(name_len); + + memcpy(stored_name.get(), name, name_len); + + list_name_storage.emplace_back(std::move(stored_name)); + + return list_name_storage.back().get(); +} #include +/* +============= +InitSave + +Initializes the save data lists and validates for duplicates. +============= +*/ void InitSave() { if (save_data_initialized) return; @@ -67,91 +115,222 @@ void InitSave() { for (const save_data_list_t *link = list_head; link; link = link->next) { const void *link_ptr = link; - if (list_hash.find(link_ptr) != list_hash.end()) { - auto existing = *list_hash.find(link_ptr); - - // [0] is just to silence warning - assert(false || "invalid save pointer; break here to find which pointer it is"[0]); + auto existing_ptr = list_hash.find(link_ptr); + if (existing_ptr != list_hash.end()) { + const save_data_list_t *existing = existing_ptr->second; if (!deathmatch->integer) { if (g_strict_saves->integer) - gi.Com_ErrorFmt("link pointer {} already linked as {}; fatal error", link_ptr, existing.second->name); + gi.Com_ErrorFmt("duplicate save link pointer {} for type {} (tag {}) already mapped to {} (tag {})", link_ptr, link->name, (int32_t)link->tag, existing->name, (int32_t)existing->tag); else - gi.Com_PrintFmt("link pointer {} already linked as {}; fatal error", link_ptr, existing.second->name); + gi.Com_PrintFmt("duplicate save link pointer {} for type {} (tag {}) already mapped to {} (tag {})", link_ptr, link->name, (int32_t)link->tag, existing->name, (int32_t)existing->tag); } - } - if (list_str_hash.find(link->name) != list_str_hash.end()) { - auto existing = *list_str_hash.find(link->name); + continue; + } - // [0] is just to silence warning - assert(false || "invalid save pointer; break here to find which pointer it is"[0]); + auto existing_name = list_str_hash.find(link->name); + if (existing_name != list_str_hash.end()) { + const save_data_list_t *existing = existing_name->second; if (!deathmatch->integer) { + const void *existing_ptr = reinterpret_cast(existing); + if (g_strict_saves->integer) - gi.Com_ErrorFmt("link pointer {} already linked as {}; fatal error", link_ptr, existing.second->name); + gi.Com_ErrorFmt("duplicate save type name {} (tag {}) already linked to pointer {}, cannot add pointer {}", link->name, (int32_t)link->tag, existing_ptr, link_ptr); else - gi.Com_PrintFmt("link pointer {} already linked as {}; fatal error", link_ptr, existing.second->name); + gi.Com_PrintFmt("duplicate save type name {} (tag {}) already linked to pointer {}, cannot add pointer {}", link->name, (int32_t)link->tag, existing_ptr, link_ptr); } + + continue; } list_hash.emplace(link_ptr, link); - list_str_hash.emplace(link->name, link); + const char *stored_name = StoreListNameCopy(link->name); + list_str_hash.emplace(stored_name ? stored_name : link->name, link); list_from_ptr_hash.emplace(std::make_tuple(link->ptr, link->tag), link); } + save_data_initialized = true; } - // initializer for save data -save_data_list_t::save_data_list_t(const char *name_in, save_data_tag_t tag_in, const void *ptr_in) : +/* +============= +save_data_list_t::save_data_list_t + +Registers a save data entry and optionally links it into the global list used for lookups. +============= +*/ +save_data_list_t::save_data_list_t(const char *name_in, save_data_tag_t tag_in, const void *ptr_in, bool link, bool valid_in) : name(name_in), tag(tag_in), - ptr(ptr_in) { - if (save_data_initialized) + ptr(ptr_in), + next(nullptr), + valid(valid_in) { + assert(name_in); + + if (!name_in) + gi.Com_Error("save data entry name cannot be null"); + + if (save_data_initialized && link) gi.Com_Error("attempted to create save_data_list at runtime"); - next = list_head; - list_head = this; + if (link) { + next = list_head; + list_head = this; + } } +/* +============= +save_data_list_t::fetch + +Fetches a save data entry for a pointer/tag pair, returning a sentinel when missing. +============= +*/ const save_data_list_t *save_data_list_t::fetch(const void *ptr, save_data_tag_t tag) { auto link = list_from_ptr_hash.find(std::make_tuple(ptr, tag)); if (link != list_from_ptr_hash.end() && link->second->tag == tag) return link->second; - // [0] is just to silence warning - assert(false || "invalid save pointer; break here to find which pointer it is"[0]); + const char *tag_name = nullptr; + + for (const auto &entry : list_hash) { + if (entry.second && entry.second->tag == tag) { + tag_name = entry.second->name; + break; + } + } if (g_strict_saves->integer) - gi.Com_ErrorFmt("value pointer {} was not linked to save tag {}\n", ptr, (int32_t)tag); + gi.Com_ErrorFmt("value pointer {} was not linked to save tag {} ({})\n", ptr, (int32_t)tag, tag_name ? tag_name : ""); else - gi.Com_PrintFmt("value pointer {} was not linked to save tag {}\n", ptr, (int32_t)tag); + gi.Com_PrintFmt("value pointer {} was not linked to save tag {} ({})\n", ptr, (int32_t)tag, tag_name ? tag_name : ""); - return nullptr; + static save_data_list_t invalid_save_data("", SAVE_DATA_MMOVE, nullptr, false, false); + + invalid_save_data.tag = tag; + invalid_save_data.ptr = ptr; + + return &invalid_save_data; } std::string json_error_stack; +/* +============= +json_stack_segment + +Pushes a stack segment on construction and pops it on destruction to ensure +balanced JSON stack usage. +============= +*/ +class json_stack_segment { +public: + /* + ============= + json_stack_segment + + Pushes the provided stack segment when the guard is constructed. + ============= + */ + template + explicit json_stack_segment(const T &stack) { + json_push_stack(stack); + } + + /* + ============= + ~json_stack_segment + + Pops the active stack segment when the guard is destroyed. + ============= + */ + ~json_stack_segment() { + json_pop_stack(); + } +}; + +/* +============= +json_push_stack + +Pushes a new context onto the JSON error stack. +============= +*/ void json_push_stack(const std::string &stack) { json_error_stack += "::" + stack; } +/* +============= +json_pop_stack + +Removes the most recent context, including its preceding delimiter, from the JSON error stack. +============= +*/ void json_pop_stack() { - size_t o = json_error_stack.find_last_of("::"); + const size_t delimiter = json_error_stack.rfind("::"); - if (o != std::string::npos) - json_error_stack.resize(o - 1); + if (delimiter != std::string::npos) + json_error_stack.erase(delimiter); } +/* +============= +json_print_error + +Prints JSON load errors with the current stack context, optionally treating them as fatal. +============= +*/ void json_print_error(const char *field, const char *message, bool fatal) { - if (fatal || g_strict_saves->integer) + if (fatal || g_strict_saves->integer) { gi.Com_ErrorFmt("Error loading JSON\n{}.{}: {}", json_error_stack, field, message); + return; + } + gi.Com_PrintFmt("Warning loading JSON\n{}.{}: {}\n", json_error_stack, field, message); } +#ifndef NDEBUG +/* +============= +json_run_stack_tests + +Verifies nested JSON stack push/pop calls restore the stack without leaving delimiters behind. +============= +*/ +static void json_run_stack_tests() { + const std::string original_stack = json_error_stack; + + json_error_stack.clear(); + json_push_stack("outer"); + json_push_stack("inner"); + assert(json_error_stack == "::outer::inner"); + + json_pop_stack(); + assert(json_error_stack == "::outer"); + + json_pop_stack(); + assert(json_error_stack.empty()); + + json_error_stack = "::root"; + json_push_stack("child"); + json_push_stack("grandchild"); + json_pop_stack(); + assert(json_error_stack == "::root::child"); + json_pop_stack(); + assert(json_error_stack == "::root"); + json_pop_stack(); + assert(json_error_stack.empty()); + + json_error_stack = original_stack; +} +#endif + using save_void_t = save_data_t; enum save_type_id_t { @@ -208,8 +387,9 @@ struct save_type_t { bool never_empty = false; // this should be persisted even if all empty bool (*is_empty)(const void *data) = nullptr; // override default check - void (*read)(void *data, const Json::Value &json, const char *field) = nullptr; // for custom reading - bool (*write)(const void *data, bool null_for_empty, Json::Value &output) = nullptr; // for custom writing +void (*read)(void *data, const Json::Value &json, const char *field) = nullptr; // for custom reading +bool (*write)(const void *data, bool null_for_empty, Json::Value &output) = nullptr; // for custom writing +const char *(*string_resolver)(const char *value) = nullptr; }; struct save_field_t { @@ -225,10 +405,10 @@ struct save_field_t { }; struct save_struct_t { - const char *name; - const std::initializer_list fields; // field list +const char *name; +const std::initializer_list fields; // field list - std::string debug() const { +std::string debug() const { std::stringstream s; for (auto &field : fields) @@ -236,9 +416,54 @@ struct save_struct_t { << field.type.count << '\n'; return s.str(); - } +} }; +static std::unordered_map classname_constants; + +/* +============= +InitClassnameConstants + +Builds a mapping of known classnames to their canonical pointers. +============= +*/ +static void InitClassnameConstants() { + if (!classname_constants.empty()) + return; + + for (const char *classname : G_GetSpawnClassnameConstants()) + classname_constants.emplace(classname, classname); + + for (item_id_t i = static_cast(IT_NULL + 1); i < IT_TOTAL; i = static_cast(i + 1)) { + const gitem_t *item = GetItemByIndex(i); + + if (item && item->classname) + classname_constants.emplace(item->classname, item->classname); + } +} + +/* +============= +Save_ResolveClassname + +Returns the canonical classname pointer for persisted entities. +============= +*/ +static const char *Save_ResolveClassname(const char *classname) { + if (!classname) + return nullptr; + + InitClassnameConstants(); + + auto classname_it = classname_constants.find(classname); + + if (classname_it != classname_constants.end()) + return classname_it->second; + + return nullptr; +} + // field header macro #define SAVE_FIELD(n, f) #f, offsetof(n, f) @@ -560,6 +785,14 @@ struct save_type_deducer> { } \ } +#define FIELD_CLASSNAME(f) \ + { \ + FIELD(f), \ + { \ + ST_STRING, TAG_LEVEL, 0, nullptr, nullptr, false, nullptr, nullptr, nullptr, Save_ResolveClassname \ + } \ + } + // macro for creating save type deducer for // specified struct type #define MAKE_STRUCT_SAVE_DEDUCER(t) \ @@ -644,6 +877,7 @@ FIELD_AUTO(killed_monsters), FIELD_AUTO(body_que), FIELD_AUTO(power_cubes), +FIELD_AUTO(steam_effect_next_id), FIELD_AUTO(disguise_violator), FIELD_AUTO(disguise_violation_time), @@ -919,7 +1153,7 @@ FIELD_LEVEL_STRING(model), FIELD_AUTO(freetime), FIELD_LEVEL_STRING(message), -FIELD_LEVEL_STRING(classname), // FIXME: should allow loading from constants +FIELD_CLASSNAME(classname), FIELD_AUTO(spawnflags), FIELD_AUTO(timestamp), @@ -1453,9 +1687,19 @@ void read_save_type_json(const Json::Value &json, void *data, const save_type_t if (type->count && strlen(json.asCString()) >= type->count) json_print_error(field, "static-length dynamic string overrun", false); else { + if (type->string_resolver) { + const char *resolved = type->string_resolver(json.asCString()); + + if (resolved) { + *((const char **)data) = resolved; + return; + } + } + size_t len = strlen(json.asCString()); - char *str = *((char **)data) = (char *)gi.TagMalloc(type->count ? type->count : (len + 1), type->tag); - strcpy(str, json.asCString()); + size_t alloc_size = type->count ? type->count : (len + 1); + char *str = *((char **)data) = (char *)gi.TagMalloc(alloc_size, type->tag); + Q_strlcpy(str, json.asCString(), alloc_size); str[len] = 0; } } else if (json.isArray()) { @@ -1485,8 +1729,10 @@ void read_save_type_json(const Json::Value &json, void *data, const save_type_t if (json.isString()) { if (type->count && strlen(json.asCString()) >= type->count) json_print_error(field, "fixed length string overrun", false); - else - strcpy((char *)data, json.asCString()); + else { + size_t dest_size = type->count ? type->count : (strlen(json.asCString()) + 1); + Q_strlcpy((char *)data, json.asCString(), dest_size); + } } else if (json.isArray()) { if (type->count && json.size() >= type->count - 1) json_print_error(field, "fixed length string overrun", false); @@ -1569,9 +1815,8 @@ void read_save_type_json(const Json::Value &json, void *data, const save_type_t return; case ST_STRUCT: if (!json.isNull()) { - json_push_stack(field); + json_stack_segment stack_guard(field); read_save_struct_json(json, data, type->structure); - json_pop_stack(); } return; case ST_ENTITY: @@ -1646,18 +1891,16 @@ void read_save_type_json(const Json::Value &json, void *data, const save_type_t const Json::Value &value = *it; if (!value.isInt()) { - json_push_stack(classname); + json_stack_segment stack_guard(classname); json_print_error(field, "expected integer", false); - json_pop_stack(); continue; } gitem_t *item = FindItemByClassname(classname); if (!item) { - json_push_stack(classname); + json_stack_segment stack_guard(classname); json_print_error(field, G_Fmt("can't find item {}", classname).data(), false); - json_pop_stack(); continue; } @@ -1681,45 +1924,39 @@ void read_save_type_json(const Json::Value &json, void *data, const save_type_t const Json::Value &value = json[i]; if (!value.isObject()) { - json_push_stack(fmt::format("{}", i)); + json_stack_segment stack_guard(fmt::format("{}", i)); json_print_error(field, "expected object", false); - json_pop_stack(); continue; } // quick type checks if (!value["classname"].isString()) { - json_push_stack(fmt::format("{}.classname", i)); + json_stack_segment stack_guard(fmt::format("{}.classname", i)); json_print_error(field, "expected string", false); - json_pop_stack(); continue; } if (!value["mins"].isArray() || value["mins"].size() != 3) { - json_push_stack(fmt::format("{}.mins", i)); + json_stack_segment stack_guard(fmt::format("{}.mins", i)); json_print_error(field, "expected array[3]", false); - json_pop_stack(); continue; } if (!value["maxs"].isArray() || value["maxs"].size() != 3) { - json_push_stack(fmt::format("{}.maxs", i)); + json_stack_segment stack_guard(fmt::format("{}.maxs", i)); json_print_error(field, "expected array[3]", false); - json_pop_stack(); continue; } if (!value["strength"].isInt()) { - json_push_stack(fmt::format("{}.strength", i)); + json_stack_segment stack_guard(fmt::format("{}.strength", i)); json_print_error(field, "expected int", false); - json_pop_stack(); continue; } p->classname = G_CopyString(value["classname"].asCString(), TAG_LEVEL); p->strength = value["strength"].asInt(); - for (int32_t x = 0; x < 3; x++) { p->mins[x] = value["mins"][x].asInt(); p->maxs[x] = value["maxs"][x].asInt(); @@ -2168,6 +2405,13 @@ void read_save_struct_json(const Json::Value &json, void *data, const save_struc #include #include +/* +============= +parseJson + +Parses a JSON string into a Json::Value and errors on failure. +============= +*/ static Json::Value parseJson(const char *jsonString) { Json::CharReaderBuilder reader; reader["allowSpecialFloats"] = true; @@ -2184,12 +2428,19 @@ static Json::Value parseJson(const char *jsonString) { return json; } +/* +============= +saveJson + +Serializes a Json::Value to a TagMalloc'ed string buffer. +============= +*/ static char *saveJson(const Json::Value &json, size_t *out_size) { Json::StreamWriterBuilder builder; builder["indentation"] = "\t"; builder["useSpecialFloats"] = true; const std::unique_ptr writer(builder.newStreamWriter()); - std::stringstream ss(std::ios_base::out | std::ios_base::binary); + std::stringstream ss(std::ios_base::out | std::ios_base::binary); writer->write(json, &ss); *out_size = ss.tellp(); char *const out = static_cast(gi.TagMalloc(*out_size + 1, TAG_GAME)); @@ -2200,8 +2451,13 @@ static char *saveJson(const Json::Value &json, size_t *out_size) { return out; } -// new entry point for WriteGame. -// returns pointer to TagMalloc'd JSON string. +/* +============= +WriteGameJson + +Serializes the current game state to TagMalloc'd JSON data. +============= +*/ char *WriteGameJson(bool autosave, size_t *out_size) { if (!autosave) SaveClientData(); @@ -2209,7 +2465,7 @@ char *WriteGameJson(bool autosave, size_t *out_size) { Json::Value json(Json::objectValue); json["save_version"] = SAVE_FORMAT_VERSION; - // TODO: engine version ID? + json["engine_version"] = GetEngineBuildString(); // write game game.autosaved = autosave; @@ -2230,13 +2486,51 @@ char *WriteGameJson(bool autosave, size_t *out_size) { void PrecacheInventoryItems(); -// new entry point for ReadGame. -// takes in pointer to JSON data. does -// not store or modify it. +/* +============= +ValidateEngineVersion + +Validates the engine version recorded in the save JSON and warns or aborts on mismatches. +============= +*/ +static void ValidateEngineVersion(const Json::Value &json) { + const Json::Value &engine_version_json = json["engine_version"]; + const char *current_engine_version = GetEngineBuildString(); + + if (!engine_version_json.isString()) { + if (g_strict_saves->integer) + gi.Com_Error("expected \"engine_version\" to be string"); + else + gi.Com_Print("warning: save file missing engine version info; continuing load anyway.\n"); + + return; + } + + const std::string save_engine_version = engine_version_json.asString(); + const char *actual_engine_version = (current_engine_version && current_engine_version[0]) ? current_engine_version : ""; + + if (save_engine_version != actual_engine_version) { + const std::string warning = fmt::format("Save was created with engine version \"{}\" but current engine is \"{}\".", save_engine_version, actual_engine_version); + + if (g_strict_saves->integer) + gi.Com_Error(warning.c_str()); + else + gi.Com_PrintFmt("{}\n", warning); + } +} + +/* +============= +ReadGameJson + +Loads game state from JSON data and restores entities and clients. +============= +*/ void ReadGameJson(const char *jsonString) { gi.FreeTags(TAG_GAME); Json::Value json = parseJson(jsonString); + ValidateEngineVersion(json); uint32_t max_entities = game.maxentities; uint32_t max_clients = game.maxclients; @@ -2247,9 +2541,8 @@ void ReadGameJson(const char *jsonString) { globals.gentities = g_entities; // read game - json_push_stack("game"); + json_stack_segment game_stack("game"); read_save_struct_json(json["game"], &game, &game_locals_t_savestruct); - json_pop_stack(); // read clients const Json::Value &clients = json["clients"]; @@ -2262,16 +2555,20 @@ void ReadGameJson(const char *jsonString) { size_t i = 0; for (auto &v : clients) { - json_push_stack(fmt::format("clients[{}]", i)); + json_stack_segment stack_guard(fmt::format("clients[{}]", i)); read_save_struct_json(v, &game.clients[i++], &gclient_t_savestruct); - json_pop_stack(); } PrecacheInventoryItems(); } -// new entry point for WriteLevel. -// returns pointer to TagMalloc'd JSON string. +/* +============= +WriteLevelJson + +Serializes the current level state to TagMalloc'd JSON data. +============= +*/ char *WriteLevelJson(bool transition, size_t *out_size) { // update current level entry now, just so we can // use gamemap to test EOU @@ -2286,7 +2583,7 @@ char *WriteLevelJson(bool transition, size_t *out_size) { // write entities Json::Value entities(Json::objectValue); - char number[16]; + char number[16]; for (size_t i = 0; i < globals.num_entities; i++) { if (!globals.gentities[i].inuse) @@ -2312,9 +2609,13 @@ char *WriteLevelJson(bool transition, size_t *out_size) { return saveJson(json, out_size); } -// new entry point for ReadLevel. -// takes in pointer to JSON data. does -// not store or modify it. +/* +============= +ReadLevelJson + +Loads level state from JSON data and repopulates entities. +============= +*/ void ReadLevelJson(const char *jsonString) { // free any dynamic memory allocated by loading the level // base state @@ -2327,9 +2628,8 @@ void ReadLevelJson(const char *jsonString) { globals.num_entities = game.maxclients + 1; // read level - json_push_stack("level"); + json_stack_segment level_stack("level"); read_save_struct_json(json["level"], &level, &level_locals_t_savestruct); - json_pop_stack(); // read entities const Json::Value &entities = json["entities"]; @@ -2342,16 +2642,15 @@ void ReadLevelJson(const char *jsonString) { const char *dummy; const char *id = it.memberName(&dummy); const Json::Value &value = *it;//json[key]; - uint32_t number = strtoul(id, nullptr, 10); + uint32_t number = strtoul(id, nullptr, 10); if (number >= globals.num_entities) globals.num_entities = number + 1; gentity_t *ent = &g_entities[number]; G_InitGentity(ent); - json_push_stack(fmt::format("entities[{}]", number)); + json_stack_segment stack_guard(fmt::format("entities[{}]", number)); read_save_struct_json(value, ent, &gentity_t_savestruct); - json_pop_stack(); gi.linkentity(ent); } @@ -2404,4 +2703,4 @@ bool CanSave() { /*static*/ template<> cached_soundindex *cached_soundindex::head = nullptr; /*static*/ template<> cached_modelindex *cached_modelindex::head = nullptr; -/*static*/ template<> cached_imageindex *cached_imageindex::head = nullptr; \ No newline at end of file +/*static*/ template<> cached_imageindex *cached_imageindex::head = nullptr; diff --git a/src/g_save_test.cpp b/src/g_save_test.cpp new file mode 100644 index 0000000..73ae97d --- /dev/null +++ b/src/g_save_test.cpp @@ -0,0 +1,89 @@ +#include +#include + +#include "g_local.h" + +// Stub globals required by g_save.cpp +local_game_import_t gi{}; +g_fmt_data_t g_fmt_data{}; + +cvar_t strict_stub{ + const_cast("g_strict_saves"), + const_cast("0"), + nullptr, + static_cast(0), + 0, + 0.0f, + nullptr, + 0 +}; + +cvar_t deathmatch_stub{ + const_cast("deathmatch"), + const_cast("0"), + nullptr, + static_cast(0), + 0, + 0.0f, + nullptr, + 0 +}; + +cvar_t *g_strict_saves = &strict_stub; +cvar_t *deathmatch = &deathmatch_stub; + +/* +============= +StubComPrint + +Captures formatted error output for validation. +============= +*/ +static void StubComPrint(const char *message) { + (void)message; +} + +/* +============= +StubComError + +Captures fatal errors for validation. +============= +*/ +static void StubComError(const char *message) { + (void)message; +} + +#include "g_save.cpp" + +/* +============= +ValidateUnknownTagLookup + +Ensures save_data_list_t::fetch returns an invalid sentinel for unknown pointers. +============= +*/ +static void ValidateUnknownTagLookup() { + strict_stub.integer = 0; + deathmatch_stub.integer = 0; + gi.Com_Print = &StubComPrint; + gi.Com_Error = &StubComError; + + const void *unknown_ptr = reinterpret_cast(0x1234); + const save_data_list_t *result = save_data_list_t::fetch(unknown_ptr, SAVE_FUNC_THINK); + + assert(result != nullptr); + assert(!result->valid); + assert(result->ptr == unknown_ptr); + assert(result->tag == SAVE_FUNC_THINK); +} + +/* +============= +main +============= +*/ +int main() { + ValidateUnknownTagLookup(); + return 0; +} diff --git a/src/g_spawn.cpp b/src/g_spawn.cpp index 4d7d6c0..4a3ec9d 100644 --- a/src/g_spawn.cpp +++ b/src/g_spawn.cpp @@ -3,6 +3,8 @@ #include "g_local.h" +#include + struct spawn_t { const char *name; void (*spawn)(gentity_t *ent); @@ -172,6 +174,7 @@ void SP_misc_flare(gentity_t *ent); // [Sam-KEX] void SP_misc_hologram(gentity_t *ent); void SP_misc_lavaball(gentity_t *ent); void SP_misc_nuke_core(gentity_t *self); +void SP_misc_prox(gentity_t* self); void SP_monster_berserk(gentity_t *self); void SP_monster_gladiator(gentity_t *self); @@ -395,6 +398,7 @@ static const std::initializer_list spawns = { { "misc_transport", SP_misc_transport }, { "misc_nuke", SP_misc_nuke }, { "misc_nuke_core", SP_misc_nuke_core }, + { "misc_prox", SP_misc_prox }, { "monster_berserk", SP_monster_berserk }, { "monster_gladiator", SP_monster_gladiator }, @@ -455,6 +459,27 @@ static const std::initializer_list spawns = { // clang-format on +/* +============= +G_GetSpawnClassnameConstants + +Provides canonical spawn classname pointers. +============= +*/ +const std::vector &G_GetSpawnClassnameConstants() { + static std::vector classnames; + + if (!classnames.empty()) + return classnames; + + classnames.reserve(spawns.size()); + + for (const spawn_t &spawn : spawns) + classnames.push_back(spawn.name); + + return classnames; +} + static void SpawnEnt_MapFixes(gentity_t *ent) { if (!Q_strcasecmp(level.mapname, "bunk1")) { if (!Q_strcasecmp(ent->classname, "func_button") && !Q_strcasecmp(ent->model, "*36")) { @@ -596,7 +621,7 @@ void ED_CallSpawn(gentity_t *ent) { if (GT(GT_BALL)) { ent->s.effects |= EF_COLOR_SHELL; ent->s.renderfx |= RF_SHELL_RED | RF_SHELL_GREEN; - } else { + } else { G_FreeEntity(ent); } return; @@ -1223,7 +1248,7 @@ static inline bool G_InhibitEntity(gentity_t *ent) { ((skill->integer >= 2) && ent->spawnflags.has(SPAWNFLAG_NOT_HARD)); } -void setup_shadow_lights(); +void setup_shadow_lights(); // [Paril-KEX] void PrecacheInventoryItems() { @@ -1545,10 +1570,20 @@ static void G_LocateSpawnSpots(void) { level.num_spawn_spots = n; } +/* +============= + ParseWorldEntityString + +Loads the base entity string for the level and optionally overrides it with +an external .ent file. +============= +*/ static void ParseWorldEntityString(const char *mapname, bool try_q3) { bool ent_file_exists = false, ent_valid = true; const char *entities = level.entstring.c_str(); + (void)try_q3; + // load up ent override const char *name = G_Fmt("baseq2/{}/{}.ent", g_entity_override_dir->string[0] ? g_entity_override_dir->string : "maps", mapname).data(); FILE *f = fopen(name, "rb"); @@ -1566,7 +1601,7 @@ static void ParseWorldEntityString(const char *mapname, bool try_q3) { ent_valid = false; } if (ent_valid) { - buffer = (char *)gi.TagMalloc(length + 1, '\0'); + buffer = (char *)gi.TagMalloc(length + 1, TAG_LEVEL); if (length) { read_length = fread(buffer, 1, length, f); @@ -1575,6 +1610,7 @@ static void ParseWorldEntityString(const char *mapname, bool try_q3) { ent_valid = false; } } + buffer[length] = '\0'; } ent_file_exists = true; fclose(f); @@ -1587,7 +1623,7 @@ static void ParseWorldEntityString(const char *mapname, bool try_q3) { //gi.Com_PrintFmt("Entities override: \"{}\"\n", name); } } - } else { + } else { gi.Com_PrintFmt("{}: Entities override file load error for \"{}\", discarding.\n", __FUNCTION__, name); } } @@ -1602,7 +1638,7 @@ static void ParseWorldEntityString(const char *mapname, bool try_q3) { gi.Com_PrintFmt("{}: Entities override file written to: \"{}\"\n", __FUNCTION__, name); fclose(f); } - } else { + } else { if (g_verbose->integer) gi.Com_PrintFmt("{}: Entities override file not saved as file already exists: \"{}\"\n", __FUNCTION__, name); } @@ -1610,6 +1646,13 @@ static void ParseWorldEntityString(const char *mapname, bool try_q3) { level.entstring = entities; } +/* +============= +ParseWorldEntities + +Creates runtime entities from the currently loaded entity string. +============= +*/ static void ParseWorldEntities() { gentity_t *ent = nullptr; int inhibit = 0; @@ -1678,6 +1721,45 @@ void ClearWorldEntities() { } } +/* +============= +ResetLevelState + +Value-initializes the global level state and reapplies defaults for +non-trivial members. +============= +*/ +static void ResetLevelState() { + level = level_locals_t{}; + level.monsters_registered.fill(nullptr); + level.health_bar_entities.fill(nullptr); +} + +/* +============= +G_TestCTFSpawnPoints + +Ensure Capture the Flag spawn points remain linked and usable after +entity parsing. +============= +*/ +static void G_TestCTFSpawnPoints() { + if (!(GTF(GTF_CTF))) + return; + + auto ensure_linked = [](const char *classname) { + gentity_t *spot = nullptr; + + while ((spot = G_FindByString<&gentity_t::classname>(spot, classname)) != nullptr) { + if (!spot->linkcount) + gi.linkentity(spot); + } + }; + + ensure_linked("info_player_team_red"); + ensure_linked("info_player_team_blue"); +} + /* ============== SpawnEntities @@ -1687,8 +1769,16 @@ parsing textual entity definitions out of an ent file. ============== */ void SpawnEntities(const char *mapname, const char *entities, const char *spawnpoint) { + std::string new_entstring = entities ? entities : ""; bool ent_file_exists = false, ent_valid = true; //const char *entities = level.entstring.c_str(); + + Q_strlcpy(level.mapname, mapname, sizeof(level.mapname)); + // Paril: fixes a bug where autosaves will start you at + // the wrong spawnpoint if they happen to be non-empty + // (mine2 -> mine3) + if (!game.autosaved) + Q_strlcpy(game.spawnpoint, spawnpoint, sizeof(game.spawnpoint)); //#if 0 // load up ent override //const char *name = G_Fmt("baseq2/maps/{}.ent", mapname).data(); @@ -1708,7 +1798,7 @@ void SpawnEntities(const char *mapname, const char *entities, const char *spawnp ent_valid = false; } if (ent_valid) { - buffer = (char *)gi.TagMalloc(length + 1, '\0'); + buffer = (char *)gi.TagMalloc(length + 1, TAG_LEVEL); if (length) { read_length = fread(buffer, 1, length, f); @@ -1717,6 +1807,7 @@ void SpawnEntities(const char *mapname, const char *entities, const char *spawnp ent_valid = false; } } + buffer[length] = '\0'; } ent_file_exists = true; fclose(f); @@ -1730,7 +1821,7 @@ void SpawnEntities(const char *mapname, const char *entities, const char *spawnp gi.Com_PrintFmt("{}: Entities override file verified and loaded: \"{}\"\n", __FUNCTION__, name); } } - } else { + } else { gi.Com_PrintFmt("{}: Entities override file load error for \"{}\", discarding.\n", __FUNCTION__, name); } } @@ -1745,11 +1836,11 @@ void SpawnEntities(const char *mapname, const char *entities, const char *spawnp gi.Com_PrintFmt("{}: Entities override file written to: \"{}\"\n", __FUNCTION__, name); fclose(f); } - } else { + } else { //gi.Com_PrintFmt("{}: Entities override file not saved as file already exists: \"{}\"\n", __FUNCTION__, name); } } - level.entstring = entities; + std::string incoming_entstring = entities ? std::string(entities) : std::string(); //#endif //ParseWorldEntityString(mapname, RS(RS_Q3A)); @@ -1767,17 +1858,19 @@ void SpawnEntities(const char *mapname, const char *entities, const char *spawnp gi.FreeTags(TAG_LEVEL); memset(&level, 0, sizeof(level)); + level.steam_effect_next_id = 0; memset(g_entities, 0, game.maxentities * sizeof(g_entities[0])); + globals.num_entities = game.maxclients + 1; + level.entstring = incoming_entstring; + entities = level.entstring.c_str(); // all other flags are not important atm - globals.server_flags &= SERVER_FLAG_LOADING; + globals.server_flags |= SERVER_FLAG_LOADING; - Q_strlcpy(level.mapname, mapname, sizeof(level.mapname)); - // Paril: fixes a bug where autosaves will start you at - // the wrong spawnpoint if they happen to be non-empty - // (mine2 -> mine3) - if (!game.autosaved) - Q_strlcpy(game.spawnpoint, spawnpoint, sizeof(game.spawnpoint)); + level.entstring = new_entstring; + ParseWorldEntityString(mapname, RS(RS_Q3A)); + + G_SaveLevelEntstring(); level.is_n64 = strncmp(level.mapname, "q64/", 4) == 0; @@ -1796,57 +1889,7 @@ void SpawnEntities(const char *mapname, const char *entities, const char *spawnp // reserve some spots for dead player bodies for coop / deathmatch InitBodyQue(); - gentity_t *ent = nullptr; - int inhibit = 0; - const char *com_token; - //const char *entities = level.entstring.c_str(); - - // parse entities - while (1) { - // parse the opening brace - com_token = COM_Parse(&entities); - if (!entities) - break; - if (com_token[0] != '{') - gi.Com_ErrorFmt("{}: Found \"{}\" when expecting {{ in entity string.\n", __FUNCTION__, com_token); - - if (!ent) - ent = g_entities; - else - ent = G_Spawn(); - entities = ED_ParseEntity(entities, ent); - - // nasty hacks time! - if (!strcmp(level.mapname, "bunk1")) { - if (!strcmp(ent->classname, "func_button") && !Q_strcasecmp(ent->model, "*36")) { - ent->wait = -1; - } - } - - // remove things (except the world) from different skill levels or deathmatch - if (ent != g_entities) { - if (G_InhibitEntity(ent)) { - G_FreeEntity(ent); - inhibit++; - continue; - } - - ent->spawnflags &= ~SPAWNFLAG_EDITOR_MASK; - } - - if (!ent) - gi.Com_ErrorFmt("{}: Invalid or empty entity string.", __FUNCTION__); - - // do this before calling the spawn function so it can be overridden. - ent->gravityVector = { 0.0, 0.0, -1.0 }; - - ED_CallSpawn(ent); - - ent->s.renderfx |= RF_IR_VISIBLE; - } - - if (inhibit && g_verbose->integer) - gi.Com_PrintFmt("{} entities inhibited.\n", inhibit); + ParseWorldEntities(); // precache start_items PrecacheStartItems(); @@ -1859,6 +1902,8 @@ void SpawnEntities(const char *mapname, const char *entities, const char *spawnp QuadHog_SetupSpawn(5_sec); Tech_SetupSpawn(); + G_TestCTFSpawnPoints(); + if (deathmatch->integer) { if (g_dm_random_items->integer) PrecacheForRandomRespawn(); @@ -1871,7 +1916,7 @@ void SpawnEntities(const char *mapname, const char *entities, const char *spawnp game.item_inhibit_wp = 0; } else { InitHintPaths(); // if there aren't hintpaths on this map, enable quick aborts - } +} G_LocateSpawnSpots(); @@ -1936,8 +1981,9 @@ static void G_InitStatusbar() { // top of screen coop respawn display sb.ifstat(STAT_COOP_RESPAWN).xv(0).yt(0).loc_stat_cstring2(STAT_COOP_RESPAWN).endifstat(); - // coop lives - if (g_coop_enable_lives->integer && g_coop_num_lives->integer > 0) + // coop & horde lives +const bool limited_lives = (g_coop_enable_lives->integer && g_coop_num_lives->integer > 0) || Horde_LivesEnabled(); + if (limited_lives) sb.ifstat(STAT_LIVES).xr(-16).yt(y = 2).lives_num(STAT_LIVES).xr(0).yt(y += text_adj).loc_rstring("$g_lives").endifstat(); // total monsters @@ -1972,7 +2018,7 @@ static void G_InitStatusbar() { sb.ifstat(STAT_HEALTH_BARS).yt(24).health_bars().endifstat(); sb.story(); - } else { + } else { if (Teams()) { // flag carrier indicator if (GTF(GTF_CTF)) @@ -2014,7 +2060,7 @@ static void G_InitStatusbar() { void GT_SetLongName(void) { const char *s; - if (deathmatch->integer) { + if (deathmatch->integer) { if (GT(GT_CTF)) { if (g_instagib->integer) { s = "Insta-CTF"; @@ -2026,7 +2072,7 @@ void GT_SetLongName(void) { s = "NadeFest CTF"; } else if (g_quadhog->integer) { s = "Quad Hog CTF"; - } else { + } else { s = gt_long_name[GT_CTF]; } } else if (GT(GT_FREEZE)) { @@ -2040,7 +2086,7 @@ void GT_SetLongName(void) { s = "NadeFest Freeze"; } else if (g_quadhog->integer) { s = "Quad Hog Freeze"; - } else { + } else { s = gt_long_name[GT_FREEZE]; } } else if (GT(GT_CA)) { @@ -2054,7 +2100,7 @@ void GT_SetLongName(void) { s = "NadeFest CA"; } else if (g_quadhog->integer) { s = "Quad Hog CA"; - } else { + } else { s = gt_long_name[GT_CA]; } } else if (GT(GT_RR)) { @@ -2068,7 +2114,7 @@ void GT_SetLongName(void) { s = "NadeFest RR"; } else if (g_quadhog->integer) { s = "Quad Hog RR"; - } else { + } else { s = gt_long_name[GT_RR]; } } else if (GT(GT_STRIKE)) { @@ -2082,7 +2128,7 @@ void GT_SetLongName(void) { s = "NadeFest Strike"; } else if (g_quadhog->integer) { s = "Quad Hog Strike"; - } else { + } else { s = gt_long_name[GT_STRIKE]; } } else if (GT(GT_TDM)) { @@ -2096,7 +2142,7 @@ void GT_SetLongName(void) { s = "NadeFest TDM"; } else if (g_quadhog->integer) { s = "Quad Hog TDM"; - } else { + } else { s = gt_long_name[GT_TDM]; } } else if (GT(GT_DUEL)) { @@ -2110,7 +2156,7 @@ void GT_SetLongName(void) { s = "NadeFest Duel"; } else if (g_quadhog->integer) { s = "Quad Hog Duel"; - } else { + } else { s = gt_long_name[GT_DUEL]; } } else if (GT(GT_HORDE)) { @@ -2124,7 +2170,7 @@ void GT_SetLongName(void) { s = "NadeFest Horde"; } else if (g_quadhog->integer) { s = "Quad Hog Horde"; - } else { + } else { s = gt_long_name[GT_HORDE]; } } else if (GT(GT_BALL)) { @@ -2138,10 +2184,10 @@ void GT_SetLongName(void) { s = "NadeFest ProBall"; } else if (g_quadhog->integer) { s = "Quad Hog ProBall"; - } else { + } else { s = gt_long_name[GT_BALL]; } - } else if (deathmatch->integer) { + } else if (deathmatch->integer) { if (g_instagib->integer) { s = "InstaGib"; } else if (g_vampiric_damage->integer) { @@ -2152,16 +2198,16 @@ void GT_SetLongName(void) { s = "NadeFest"; } else if (g_quadhog->integer) { s = "Quad Hog"; - } else { + } else { s = gt_long_name[GT_FFA]; } - } else { + } else { s = "Unknown Gametype"; } - } else { + } else { if (coop->integer) { s = "Co-op"; - } else { + } else { s = "Single Player"; } } @@ -2253,7 +2299,7 @@ void SP_worldspawn(gentity_t *ent) { if (st.music && st.music[0]) { gi.configstring(CS_CDTRACK, st.music); - } else { + } else { gi.configstring(CS_CDTRACK, G_Fmt("{}", ent->sounds).data()); } @@ -2312,7 +2358,7 @@ void SP_worldspawn(gentity_t *ent) { if (!st.gravity) { level.gravity = 800.f; gi.cvar_set("g_gravity", "800"); - } else { + } else { level.gravity = atof(st.gravity); gi.cvar_set("g_gravity", st.gravity); } @@ -2430,4 +2476,6 @@ void SP_worldspawn(gentity_t *ent) { gi.configstring(CONFIG_COOP_RESPAWN_STRING + 3, "$g_coop_respawn_waiting"); gi.configstring(CONFIG_COOP_RESPAWN_STRING + 4, "$g_coop_respawn_no_lives"); } + + globals.server_flags &= ~SERVER_FLAG_LOADING; } diff --git a/src/g_svcmds.cpp b/src/g_svcmds.cpp index 4296fd6..9275f06 100644 --- a/src/g_svcmds.cpp +++ b/src/g_svcmds.cpp @@ -2,6 +2,26 @@ // Licensed under the GNU General Public License 2.0. #include "g_local.h" +#include +#include +#include +#include +#include + +/* +============= +PackBytesToUint32 + +Assembles four bytes into a uint32_t using little-endian order to maintain +consistent packet filter behavior across platforms. +============= +*/ +static uint32_t PackBytesToUint32(const byte bytes[4]) { + return (static_cast(bytes[0])) | + (static_cast(bytes[1]) << 8) | + (static_cast(bytes[2]) << 16) | + (static_cast(bytes[3]) << 24); +} static void Svcmd_Test_f() { gi.LocClient_Print(nullptr, PRINT_HIGH, "Svcmd_Test_f()\n"); @@ -44,8 +64,8 @@ only allows players from your local network. */ struct ipfilter_t { - unsigned mask; - unsigned compare; + uint32_t mask; + uint32_t compare; }; constexpr size_t MAX_IPFILTERS = 1024; @@ -60,7 +80,7 @@ StringToFilter */ static bool StringToFilter(const char *s, ipfilter_t *f) { char num[128]; - int i, j; + int i, j; byte b[4]; byte m[4]; @@ -89,8 +109,8 @@ static bool StringToFilter(const char *s, ipfilter_t *f) { s++; } - f->mask = *(unsigned *)m; - f->compare = *(unsigned *)b; + f->mask = PackBytesToUint32(m); + f->compare = PackBytesToUint32(b); return true; } @@ -101,9 +121,9 @@ G_FilterPacket ================= */ bool G_FilterPacket(const char *from) { - int i; - unsigned in; - byte m[4]; + int i; + uint32_t in; + byte m[4]; const char *p; i = 0; @@ -120,7 +140,7 @@ bool G_FilterPacket(const char *from) { p++; } - in = *(unsigned *)m; + in = PackBytesToUint32(m); for (i = 0; i < numipfilters; i++) if ((in & ipfilters[i].mask) == ipfilters[i].compare) @@ -209,45 +229,52 @@ static void SVCmd_NextMap_f() { /* ================= -G_WriteIP_f +SVCmd_WriteIP_f + +Serializes the current filterban value and IP filters to listip.cfg using +platform-aware file handling. ================= */ static void SVCmd_WriteIP_f(void) { - // KEX_FIXME: Sys_FOpen isn't available atm, just commenting this out since i don't think we even need this functionality - sponge - /* - FILE* f; + byte b[4]; + int i; + cvar_t *game; - byte b[4]; - int i; - cvar_t* game; + game = gi.cvar("game", "", static_cast(0)); - game = gi.cvar("game", "", 0); + std::filesystem::path listip_path = *game->string ? std::filesystem::path(game->string) : std::filesystem::path(GAMEVERSION); + listip_path /= "listip.cfg"; - std::string name; - if (!*game->string) - name = std::string(GAMEVERSION) + "/listip.cfg"; - else - name = std::string(game->string) + "/listip.cfg"; + gi.LocClient_Print(nullptr, PRINT_HIGH, "Writing {}.\n", listip_path.string().c_str()); - gi.LocClient_Print(nullptr, PRINT_HIGH, "Writing {}.\n", name.c_str()); + if (!listip_path.parent_path().empty()) { + std::error_code create_error; + std::filesystem::create_directories(listip_path.parent_path(), create_error); + if (create_error) { + gi.LocClient_Print(nullptr, PRINT_HIGH, "Couldn't write {} ({})\n", listip_path.string().c_str(), create_error.message().c_str()); + return; + } + } - f = Sys_FOpen(name.c_str(), "wb"); - if (!f) - { - gi.LocClient_Print(nullptr, PRINT_HIGH, "Couldn't open {}\n", name.c_str()); + std::ofstream file(listip_path, std::ios::out | std::ios::binary | std::ios::trunc); + if (!file.is_open()) { + const std::error_code open_error(errno, std::generic_category()); + gi.LocClient_Print(nullptr, PRINT_HIGH, "Couldn't write {} ({})\n", listip_path.string().c_str(), open_error.message().c_str()); return; } - fprintf(f, "set filterban %d\n", filterban->integer); + file << "set filterban " << filterban->integer << '\n'; - for (i = 0; i < numipfilters; i++) - { - *(unsigned*)b = ipfilters[i].compare; - fprintf(f, "sv addip %i.%i.%i.%i\n", b[0], b[1], b[2], b[3]); + for (i = 0; i < numipfilters; i++) { + *(unsigned *)b = ipfilters[i].compare; + file << "sv addip " << static_cast(b[0]) << '.' << static_cast(b[1]) << '.' << static_cast(b[2]) << '.' << static_cast(b[3]) << '\n'; } - fclose(f); - */ + file.close(); + if (file.fail()) { + const std::error_code close_error(errno, std::generic_category()); + gi.LocClient_Print(nullptr, PRINT_HIGH, "Couldn't write {} ({})\n", listip_path.string().c_str(), close_error.message().c_str()); + } } /* diff --git a/src/g_target.cpp b/src/g_target.cpp index 67b70ba..771415b 100644 --- a/src/g_target.cpp +++ b/src/g_target.cpp @@ -1979,15 +1979,34 @@ good colors to use: 232 - blood */ -static USE(use_target_steam) (gentity_t *self, gentity_t *other, gentity_t *activator) -> void { - // FIXME - this needs to be a global - static int nextid; - vec3_t point; - if (nextid > 20000) - nextid = nextid % 20000; +/* +============= +GetNextSteamEffectID + +Returns the next steam effect identifier for this level, wrapping to avoid +overflowing the protocol limit. +============= +*/ +static int GetNextSteamEffectID() { + if (level.steam_effect_next_id > 20000) + level.steam_effect_next_id %= 20000; + + level.steam_effect_next_id++; + + return level.steam_effect_next_id; +} - nextid++; +/* +============= +use_target_steam + +Activates the steam effect and sends the configured temp entity to clients. +============= +*/ +static USE(use_target_steam) (gentity_t *self, gentity_t *other, gentity_t *activator) -> void { + const int steam_id = GetNextSteamEffectID(); + vec3_t point; // automagically set wait from func_timer unless they set it already, or // default to 1000 if not called by a func_timer (eek!) @@ -2008,7 +2027,7 @@ static USE(use_target_steam) (gentity_t *self, gentity_t *other, gentity_t *acti if (self->wait > 100) { gi.WriteByte(svc_temp_entity); gi.WriteByte(TE_STEAM); - gi.WriteShort(nextid); + gi.WriteShort(steam_id); gi.WriteByte(self->count); gi.WritePosition(self->s.origin); gi.WriteDir(self->movedir); @@ -2029,6 +2048,13 @@ static USE(use_target_steam) (gentity_t *self, gentity_t *other, gentity_t *acti } } +/* +============= +target_steam_start + +Initializes steam parameters and prepares the entity for activation. +============= +*/ static THINK(target_steam_start) (gentity_t *self) -> void { gentity_t *ent; @@ -2061,6 +2087,13 @@ static THINK(target_steam_start) (gentity_t *self) -> void { gi.linkentity(self); } +/* +============= +SP_target_steam + +Spawns a steam target and defers initialization if linked to a target. +============= +*/ void SP_target_steam(gentity_t *self) { self->style = (int)self->speed; @@ -2450,11 +2483,13 @@ static USE(target_teleporter_use) (gentity_t *ent, gentity_t *other, gentity_t * } void SP_target_teleporter(gentity_t *ent) { - - if (!ent->target[0]) { - //gi.Com_PrintFmt("{}: Couldn't find teleporter destination, removing.\n", ent); - //G_FreeEntity(ent); - //return; + if (ent->target && ent->target[0]) { + ent->target_ent = G_PickTarget(ent->target); + if (!ent->target_ent) { + gi.Com_PrintFmt("{}: Couldn't find teleporter destination, removing.\n", *ent); + G_FreeEntity(ent); + return; + } } ent->target_ent = G_PickTarget(ent->target); diff --git a/src/g_utils.cpp b/src/g_utils.cpp index 5205bf8..f807bd5 100644 --- a/src/g_utils.cpp +++ b/src/g_utils.cpp @@ -2,7 +2,13 @@ // Licensed under the GNU General Public License 2.0. // g_utils.c -- misc utility functions for game module +#include "g_activation.h" #include "g_local.h" +#include "g_utils_friendly_message.h" +#include +#include +#include +#include "g_utils_target_selection.h" /* ============= @@ -14,17 +20,21 @@ Searches beginning at the entity after from, or the beginning if nullptr nullptr will be returned if the end of the list is reached. ============= */ -gentity_t *G_Find(gentity_t *from, std::function matcher) { - if (!from) - from = g_entities; - else - from++; +gentity_t* G_Find(gentity_t* from, std::function matcher) { + const std::span entities{ g_entities, static_cast(globals.num_entities) }; + size_t start_index = 0; - for (; from < &g_entities[globals.num_entities]; from++) { - if (!from->inuse) + if (from) + start_index = static_cast((from - g_entities) + 1); + + if (start_index >= entities.size()) + return nullptr; + + for (gentity_t& ent : entities.subspan(start_index)) { + if (!ent.inuse) continue; - if (matcher(from)) - return from; + if (matcher(&ent)) + return &ent; } return nullptr; @@ -39,10 +49,7 @@ Returns entities that have origins within a spherical area findradius (origin, radius) ================= */ -gentity_t *findradius(gentity_t *from, const vec3_t &org, float rad) { - vec3_t eorg; - int j; - +gentity_t* findradius(gentity_t* from, const vec3_t& org, float rad) { if (!from) from = g_entities; else @@ -52,8 +59,8 @@ gentity_t *findradius(gentity_t *from, const vec3_t &org, float rad) { continue; if (from->solid == SOLID_NOT) continue; - for (j = 0; j < 3; j++) - eorg[j] = org[j] - (from->s.origin[j] + (from->mins[j] + from->maxs[j]) * 0.5f); + const vec3_t entity_center = from->s.origin + (from->mins + from->maxs) * 0.5f; + const vec3_t eorg = org - entity_center; if (eorg.length() > rad) continue; return from; @@ -63,6 +70,8 @@ gentity_t *findradius(gentity_t *from, const vec3_t &org, float rad) { } + + /* ============= G_PickTarget @@ -75,12 +84,9 @@ nullptr will be returned if the end of the list is reached. ============= */ -constexpr size_t MAXCHOICES = 8; - -gentity_t *G_PickTarget(const char *targetname) { - gentity_t *choice[MAXCHOICES]; - gentity_t *ent = nullptr; - int num_choices = 0; +gentity_t* G_PickTarget(const char* targetname) { +std::vector choices; +gentity_t* ent = nullptr; if (!targetname) { gi.Com_PrintFmt("{}: called with nullptr targetname.\n", __FUNCTION__); @@ -91,54 +97,123 @@ gentity_t *G_PickTarget(const char *targetname) { ent = G_FindByString<&gentity_t::targetname>(ent, targetname); if (!ent) break; - choice[num_choices++] = ent; - if (num_choices == MAXCHOICES) - break; + choices.emplace_back(ent); } - if (!num_choices) { + if (choices.empty()) { gi.Com_PrintFmt("{}: target {} not found\n", __FUNCTION__, targetname); return nullptr; } - return choice[irandom(num_choices)]; + return G_SelectRandomTarget( + choices, + [](size_t max_index) { + return static_cast(irandom(static_cast(max_index))); + }); } -static THINK(Think_Delay) (gentity_t *ent) -> void { +/* +============= +Think_Delay + +Executes delayed target use and frees duplicated strings. +============= +*/ +static THINK(Think_Delay) (gentity_t* ent) -> void { G_UseTargets(ent, ent->activator); + + if (ent->message) + gi.TagFree((void*)ent->message); + + if (ent->target) + gi.TagFree((void*)ent->target); + + if (ent->killtarget) + gi.TagFree((void*)ent->killtarget); + G_FreeEntity(ent); } -void G_PrintActivationMessage(gentity_t *ent, gentity_t *activator, bool coop_global) { - // - // print the message - // - if ((ent->message) && !(activator->svflags & SVF_MONSTER)) { - if (coop_global && coop->integer) - gi.LocBroadcast_Print(PRINT_CENTER, "{}", ent->message); +/* +============= +G_PrintActivationMessage + +Prints activation messaging and plays optional sounds when an entity is used. +============= +*/ +void G_PrintActivationMessage(gentity_t* ent, gentity_t* activator, bool coop_global) { + if (!ent || !ent->message) + return; + + const bool has_activator = activator != nullptr; + const bool activator_is_monster = has_activator && (activator->svflags & SVF_MONSTER); + const activation_message_plan_t plan = BuildActivationMessagePlan(true, has_activator, activator_is_monster, coop_global, coop->integer, ent->noise_index); + + if (!plan.broadcast_global && !plan.center_on_activator) + return; + + if (plan.broadcast_global) + gi.LocBroadcast_Print(PRINT_CENTER, "{}", ent->message); + + if (!has_activator || !plan.center_on_activator) + return; + + gi.LocCenter_Print(activator, "{}", ent->message); + + // [Paril-KEX] allow non-noisy centerprints + if (plan.play_sound) { + if (plan.sound_index) + gi.sound(activator, CHAN_AUTO, plan.sound_index, 1, ATTN_NORM, 0); else - gi.LocCenter_Print(activator, "{}", ent->message); - - // [Paril-KEX] allow non-noisy centerprints - if (ent->noise_index >= 0) { - if (ent->noise_index) - gi.sound(activator, CHAN_AUTO, ent->noise_index, 1, ATTN_NORM, 0); - else - gi.sound(activator, CHAN_AUTO, gi.soundindex("misc/talk1.wav"), 1, ATTN_NORM, 0); - } + gi.sound(activator, CHAN_AUTO, gi.soundindex("misc/talk1.wav"), 1, ATTN_NORM, 0); } } -void BroadcastFriendlyMessage(team_t team, const char *msg) { +/* +============= +BroadcastFriendlyMessage + +Broadcast a friendly message to active teammates or, in non-team modes, all +active players. +============= +*/ +void BroadcastFriendlyMessage(team_t team, const char* msg) { + if (!FriendlyMessageHasText(msg)) + return; + for (auto ce : active_clients()) { - if (!ClientIsPlaying(ce->client) || (Teams() && ce->client->sess.team == team)) { - gi.LocClient_Print(ce, PRINT_HIGH, G_Fmt("{}{}", ce->client->sess.team != TEAM_SPECTATOR ? "[TEAM]: " : "", msg).data()); + const bool playing = ClientIsPlaying(ce->client); + bool following_team = false; + if (!playing) { + if (!Teams()) + continue; + gentity_t* follow = ce->client->follow_target; + if (!follow || !follow->client || follow->client->sess.team != team) + continue; + + following_team = true; + } + else if (Teams() && ce->client->sess.team != team) { + continue; } + + const bool is_team_player = playing && ce->client->sess.team == team && ce->client->sess.team != TEAM_SPECTATOR; + const bool prefix_team = FriendlyMessageShouldPrefixTeam(Teams(), team == TEAM_SPECTATOR, playing, is_team_player, following_team); + gi.LocClient_Print(ce, PRINT_HIGH, G_Fmt("{}{}", prefix_team ? "[TEAM]: " : "", msg).data()); } } -void BroadcastTeamMessage(team_t team, print_type_t level, const char *msg) { +/* +============= +BroadcastTeamMessage + +Broadcast a message to all clients actively playing for the specified team. +============= +*/ +void BroadcastTeamMessage(team_t team, print_type_t level, const char* msg) { for (auto ce : active_clients()) { + if (!ClientIsPlaying(ce->client)) + continue; if (ce->client->sess.team != team) continue; @@ -147,7 +222,7 @@ void BroadcastTeamMessage(team_t team, print_type_t level, const char *msg) { } } -void G_MonsterKilled(gentity_t *self); +void G_MonsterKilled(gentity_t* self); /* ============================== @@ -165,8 +240,8 @@ match (string)self.target and call their .use function ============================== */ -void G_UseTargets(gentity_t *ent, gentity_t *activator) { - gentity_t *t; +void G_UseTargets(gentity_t* ent, gentity_t* activator) { + gentity_t* t; if (!ent) return; @@ -186,9 +261,9 @@ void G_UseTargets(gentity_t *ent, gentity_t *activator) { t->activator = activator; if (!activator) gi.Com_PrintFmt("{}: {} with no activator.\n", __FUNCTION__, *t); - t->message = ent->message; - t->target = ent->target; - t->killtarget = ent->killtarget; + t->message = G_CopyString(ent->message, TAG_LEVEL); + t->target = G_CopyString(ent->target, TAG_LEVEL); + t->killtarget = G_CopyString(ent->killtarget, TAG_LEVEL); return; } @@ -201,49 +276,51 @@ void G_UseTargets(gentity_t *ent, gentity_t *activator) { // kill killtargets // if (ent->killtarget) { - t = nullptr; - while ((t = G_FindByString<&gentity_t::targetname>(t, ent->killtarget))) { - if (t->teammaster) { + for (gentity_t* cursor = G_FindByString<&gentity_t::targetname>(nullptr, ent->killtarget); cursor;) { + gentity_t* next = G_FindByString<&gentity_t::targetname>(cursor, ent->killtarget); + + if (cursor->teammaster) { // if this entity is part of a chain, cleanly remove it - if (t->flags & FL_TEAMSLAVE) { - for (gentity_t *master = t->teammaster; master; master = master->teamchain) { - if (master->teamchain == t) { - master->teamchain = t->teamchain; + if (cursor->flags & FL_TEAMSLAVE) { + for (gentity_t* master = cursor->teammaster; master; master = master->teamchain) { + if (master->teamchain == cursor) { + master->teamchain = cursor->teamchain; break; } } } // [Paril-KEX] remove teammaster too - else if (t->flags & FL_TEAMMASTER) { - t->teammaster->flags &= ~FL_TEAMMASTER; + else if (cursor->flags & FL_TEAMMASTER) { + cursor->teammaster->flags &= ~FL_TEAMMASTER; - gentity_t *new_master = t->teammaster->teamchain; + gentity_t* new_master = cursor->teammaster->teamchain; if (new_master) { new_master->flags |= FL_TEAMMASTER; new_master->flags &= ~FL_TEAMSLAVE; - for (gentity_t *m = new_master; m; m = m->teamchain) + for (gentity_t* m = new_master; m; m = m->teamchain) m->teammaster = new_master; } } } // [Paril-KEX] if we killtarget a monster, clean up properly - if (t->svflags & SVF_MONSTER) { - if (!t->deadflag && !(t->monsterinfo.aiflags & AI_DO_NOT_COUNT) && !(t->spawnflags & SPAWNFLAG_MONSTER_DEAD)) - G_MonsterKilled(t); + if (cursor->svflags & SVF_MONSTER) { + if (!cursor->deadflag && !(cursor->monsterinfo.aiflags & AI_DO_NOT_COUNT) && !(cursor->spawnflags & SPAWNFLAG_MONSTER_DEAD)) + G_MonsterKilled(cursor); } - G_FreeEntity(t); + G_FreeEntity(cursor); if (!ent->inuse) { gi.Com_PrintFmt("{}: Entity was removed while using killtargets.\n", __FUNCTION__); return; } + + cursor = next; } } - // // fire targets // @@ -258,7 +335,8 @@ void G_UseTargets(gentity_t *ent, gentity_t *activator) { if (t == ent) { gi.Com_PrintFmt("{}: WARNING: Entity used itself.\n", __FUNCTION__); - } else { + } + else { if (t->use) t->use(t, ent, activator); } @@ -275,33 +353,35 @@ void G_UseTargets(gentity_t *ent, gentity_t *activator) { G_SetMovedir =============== */ -void G_SetMovedir(vec3_t &angles, vec3_t &movedir) { - static vec3_t VEC_UP = { 0, -1, 0 }; - static vec3_t MOVEDIR_UP = { 0, 0, 1 }; - static vec3_t VEC_DOWN = { 0, -2, 0 }; - static vec3_t MOVEDIR_DOWN = { 0, 0, -1 }; +void G_SetMovedir(vec3_t& angles, vec3_t& movedir) { + static vec3_t VEC_UP = { 0, -1, 0 }; + static vec3_t MOVEDIR_UP = { 0, 0, 1 }; + static vec3_t VEC_DOWN = { 0, -2, 0 }; + static vec3_t MOVEDIR_DOWN = { 0, 0, -1 }; if (angles == VEC_UP) { movedir = MOVEDIR_UP; - } else if (angles == VEC_DOWN) { + } + else if (angles == VEC_DOWN) { movedir = MOVEDIR_DOWN; - } else { + } + else { AngleVectors(angles, movedir, nullptr, nullptr); } angles = {}; } -char *G_CopyString(const char *in, int32_t tag) { +char* G_CopyString(const char* in, int32_t tag) { if (!in) return nullptr; const size_t amt = strlen(in) + 1; - char *const out = static_cast(gi.TagMalloc(amt, tag)); + char* const out = static_cast(gi.TagMalloc(amt, tag)); Q_strlcpy(out, in, amt); return out; } -void G_InitGentity(gentity_t *e) { +void G_InitGentity(gentity_t* e) { // FIXME - // this fixes a bug somewhere that is setting "nextthink" for an entity that has // already been released. nextthink is being set to FRAME_TIME_S after level.time, @@ -330,8 +410,8 @@ instead of being removed and recreated, which can cause interpolated angles and bad trails. ================= */ -gentity_t *G_Spawn() { - gentity_t *e = &g_entities[game.maxclients + 1]; +gentity_t* G_Spawn() { + gentity_t* e = &g_entities[game.maxclients + 1]; size_t i; for (i = game.maxclients + 1; i < globals.num_entities; i++, e++) { @@ -359,7 +439,7 @@ G_FreeEntity Marks the entity as free ================= */ -THINK(G_FreeEntity) (gentity_t *ed) -> void { +THINK(G_FreeEntity) (gentity_t* ed) -> void { // already freed if (!ed->inuse) return; @@ -386,7 +466,7 @@ THINK(G_FreeEntity) (gentity_t *ed) -> void { ed->sv.init = false; } -BoxEntitiesResult_t G_TouchTriggers_BoxFilter(gentity_t *hit, void *) { +BoxEntitiesResult_t G_TouchTriggers_BoxFilter(gentity_t* hit, void*) { if (!hit->touch) return BoxEntitiesResult_t::Skip; @@ -399,10 +479,10 @@ G_TouchTriggers ============ */ -void G_TouchTriggers(gentity_t *ent) { +void G_TouchTriggers(gentity_t* ent) { int num; - static gentity_t *touch[MAX_ENTITIES]; - gentity_t *hit; + static gentity_t* touch[MAX_ENTITIES]; + gentity_t* hit; if (ent->client && ent->client->eliminated); else @@ -430,9 +510,9 @@ void G_TouchTriggers(gentity_t *ent) { // [Paril-KEX] scan for projectiles between our movement positions // to see if we need to collide against them -void G_TouchProjectiles(gentity_t *ent, vec3_t previous_origin) { +void G_TouchProjectiles(gentity_t* ent, vec3_t previous_origin) { struct skipped_projectile { - gentity_t *projectile; + gentity_t* projectile; int32_t spawn_count; }; // a bit ugly, but we'll store projectiles we are ignoring here. @@ -458,7 +538,7 @@ void G_TouchProjectiles(gentity_t *ent, vec3_t previous_origin) { G_Impact(ent, tr); } - for (auto &skip : skipped) + for (auto& skip : skipped) if (skip.projectile->inuse && skip.projectile->spawn_count == skip.spawn_count) skip.projectile->svflags |= SVF_PROJECTILE; @@ -482,14 +562,14 @@ of ent. ================= */ -BoxEntitiesResult_t KillBox_BoxFilter(gentity_t *hit, void *) { +BoxEntitiesResult_t KillBox_BoxFilter(gentity_t* hit, void*) { if (!hit->solid || !hit->takedamage || hit->solid == SOLID_TRIGGER) return BoxEntitiesResult_t::Skip; return BoxEntitiesResult_t::Keep; } -bool KillBox(gentity_t *ent, bool from_spawning, mod_id_t mod, bool bsp_clipping) { +bool KillBox(gentity_t* ent, bool from_spawning, mod_id_t mod, bool bsp_clipping) { // don't telefrag as spectator or noclip player... if (ent->movetype == MOVETYPE_NOCLIP || ent->movetype == MOVETYPE_FREECAM) return true; @@ -501,8 +581,8 @@ bool KillBox(gentity_t *ent, bool from_spawning, mod_id_t mod, bool bsp_clipping mask &= ~CONTENTS_PLAYER; int i, num; - static gentity_t *touch[MAX_ENTITIES]; - gentity_t *hit; + static gentity_t* touch[MAX_ENTITIES]; + gentity_t* hit; num = gi.BoxEntities(ent->absmin, ent->absmax, touch, MAX_ENTITIES, AREA_SOLID, KillBox_BoxFilter, nullptr); @@ -540,7 +620,7 @@ bool KillBox(gentity_t *ent, bool from_spawning, mod_id_t mod, bool bsp_clipping /*--------------------------------------------------------------------------*/ -const char *Teams_TeamName(team_t team) { +const char* Teams_TeamName(team_t team) { switch (team) { case TEAM_RED: return "RED"; @@ -554,7 +634,7 @@ const char *Teams_TeamName(team_t team) { return "NONE"; } -const char *Teams_OtherTeamName(team_t team) { +const char* Teams_OtherTeamName(team_t team) { switch (team) { case TEAM_RED: return "BLUE"; @@ -574,15 +654,15 @@ team_t Teams_OtherTeam(team_t team) { return TEAM_SPECTATOR; // invalid value } -constexpr const char *TEAM_RED_SKIN = "ctf_r"; -constexpr const char *TEAM_BLUE_SKIN = "ctf_b"; +constexpr const char* TEAM_RED_SKIN = "ctf_r"; +constexpr const char* TEAM_BLUE_SKIN = "ctf_b"; /* ================= G_AssignPlayerSkin ================= */ -void G_AssignPlayerSkin(gentity_t *ent, const char *s) { +void G_AssignPlayerSkin(gentity_t* ent, const char* s) { int playernum = ent - g_entities - 1; std::string_view t(s); @@ -613,9 +693,12 @@ void G_AssignPlayerSkin(gentity_t *ent, const char *s) { G_AdjustPlayerScore =================== */ -void G_AdjustPlayerScore(gclient_t *cl, int32_t offset, bool adjust_team, int32_t team_offset) { +void G_AdjustPlayerScore(gclient_t* cl, int32_t offset, bool adjust_team, int32_t team_offset) { if (!cl) return; + if (cl->sess.is_banned) + return; + if (IsScoringDisabled()) return; @@ -636,7 +719,7 @@ void G_AdjustPlayerScore(gclient_t *cl, int32_t offset, bool adjust_team, int32_ Horde_AdjustPlayerScore =================== */ -void Horde_AdjustPlayerScore(gclient_t *cl, int32_t offset) { +void Horde_AdjustPlayerScore(gclient_t* cl, int32_t offset) { if (notGT(GT_HORDE)) return; if (!cl || !cl->pers.connected) return; @@ -651,7 +734,7 @@ void Horde_AdjustPlayerScore(gclient_t *cl, int32_t offset) { G_SetPlayerScore =================== */ -void G_SetPlayerScore(gclient_t *cl, int32_t value) { +void G_SetPlayerScore(gclient_t* cl, int32_t value) { if (!cl) return; if (IsScoringDisabled()) @@ -718,36 +801,46 @@ G_PlaceString Adapted from Quake III =================== */ -const char *G_PlaceString(int rank) { +const char* G_PlaceString(int rank) { static char str[64]; - const char *s, *t; + const char* s, * t; if (rank & RANK_TIED_FLAG) { rank &= ~RANK_TIED_FLAG; t = "Tied for "; - } else { + } + else { t = ""; } if (rank == 1) { s = "1st"; - } else if (rank == 2) { + } + else if (rank == 2) { s = "2nd"; - } else if (rank == 3) { + } + else if (rank == 3) { s = "3rd"; - } else if (rank == 11) { + } + else if (rank == 11) { s = "11th"; - } else if (rank == 12) { + } + else if (rank == 12) { s = "12th"; - } else if (rank == 13) { + } + else if (rank == 13) { s = "13th"; - } else if (rank % 10 == 1) { + } + else if (rank % 10 == 1) { s = G_Fmt("{}st", rank).data(); - } else if (rank % 10 == 2) { + } + else if (rank % 10 == 2) { s = G_Fmt("{}nd", rank).data(); - } else if (rank % 10 == 3) { + } + else if (rank % 10 == 3) { s = G_Fmt("{}rd", rank).data(); - } else { + } + else { s = G_Fmt("{}th", rank).data(); } Q_strlcpy(str, G_Fmt("{}{}", t, s).data(), sizeof(str)); @@ -765,7 +858,7 @@ bool ItemSpawnsEnabled() { } -static void loc_buildboxpoints(vec3_t(&p)[8], const vec3_t &org, const vec3_t &mins, const vec3_t &maxs) { +static void loc_buildboxpoints(vec3_t(&p)[8], const vec3_t& org, const vec3_t& mins, const vec3_t& maxs) { p[0] = org + mins; p[1] = p[0]; p[1][0] -= mins[0]; @@ -784,7 +877,7 @@ static void loc_buildboxpoints(vec3_t(&p)[8], const vec3_t &org, const vec3_t &m p[7][1] -= maxs[1]; } -bool loc_CanSee(gentity_t *targ, gentity_t *inflictor) { +bool loc_CanSee(gentity_t* targ, gentity_t* inflictor) { trace_t trace; vec3_t targpoints[8]; int i; @@ -800,7 +893,7 @@ bool loc_CanSee(gentity_t *targ, gentity_t *inflictor) { viewpoint[2] += inflictor->viewheight; for (i = 0; i < 8; i++) { - trace = gi.traceline(viewpoint, targpoints[i], inflictor, CONTENTS_MIST|MASK_WATER|MASK_SOLID); + trace = gi.traceline(viewpoint, targpoints[i], inflictor, CONTENTS_MIST | MASK_WATER | MASK_SOLID); if (trace.fraction == 1.0f) return true; } @@ -814,11 +907,14 @@ bool Teams() { } /* -================= +============= G_TimeString -================= + +Format a match timer string with minute precision. +============= */ -const char *G_TimeString(const int msec, bool state) { +const char* G_TimeString(const int msec, bool state) { + static char buffer[32]; if (state) { if (level.match_state < matchst_t::MATCH_COUNTDOWN) return "WARMUP"; @@ -837,17 +933,23 @@ const char *G_TimeString(const int msec, bool state) { mins -= hours * 60; if (hours > 0) { - return G_Fmt("{}{}:{:02}:{:02}", msec < 1000 ? "-" : "", hours, mins, seconds).data(); - } else { - return G_Fmt("{}{:02}:{:02}", msec < 1000 ? "-" : "", mins, seconds).data(); + G_FmtTo(buffer, "{}{}:{:02}:{:02}", msec < 1000 ? "-" : "", hours, mins, seconds); + } + else { + G_FmtTo(buffer, "{}{:02}:{:02}", msec < 1000 ? "-" : "", mins, seconds); } + + return buffer; } /* -================= +============= G_TimeStringMs -================= + +Format a match timer string with millisecond precision. +============= */ -const char *G_TimeStringMs(const int msec, bool state) { +const char* G_TimeStringMs(const int msec, bool state) { + static char buffer[32]; if (state) { if (level.match_state < matchst_t::MATCH_COUNTDOWN) return "WARMUP"; @@ -866,23 +968,29 @@ const char *G_TimeStringMs(const int msec, bool state) { mins -= hours * 60; if (hours > 0) { - return G_Fmt("{}:{:02}:{:02}.{}", hours, mins, seconds, ms).data(); - } else { - return G_Fmt("{:02}:{:02}.{}", mins, seconds, ms).data(); + G_FmtTo(buffer, "{}:{:02}:{:02}.{}", hours, mins, seconds, ms); + } + else { + G_FmtTo(buffer, "{:02}:{:02}.{}", mins, seconds, ms); } + + return buffer; } -team_t StringToTeamNum(const char *in) { +team_t StringToTeamNum(const char* in) { if (!Q_strcasecmp(in, "spectator") || !Q_strcasecmp(in, "s")) { return TEAM_SPECTATOR; - } else if (!Q_strcasecmp(in, "auto") || !Q_strcasecmp(in, "a")) { + } + else if (!Q_strcasecmp(in, "auto") || !Q_strcasecmp(in, "a")) { return PickTeam(-1); - } else if (Teams()) { + } + else if (Teams()) { if (!Q_strcasecmp(in, "blue") || !Q_strcasecmp(in, "b")) return TEAM_BLUE; else if (!Q_strcasecmp(in, "red") || !Q_strcasecmp(in, "r")) return TEAM_RED; - } else { + } + else { if (!Q_strcasecmp(in, "free") || !Q_strcasecmp(in, "f")) return TEAM_FREE; } @@ -940,7 +1048,7 @@ bool IsScoringDisabled() { return false; } -gametype_t GT_IndexFromString(const char *in) { +gametype_t GT_IndexFromString(const char* in) { for (size_t i = 0; i < gametype_t::GT_NUM_GAMETYPES; i++) { if (!Q_strcasecmp(in, gt_short_name[i])) return (gametype_t)i; @@ -962,7 +1070,7 @@ void BroadcastReadyReminderMessage() { } } -void TeleportPlayerToRandomSpawnPoint(gentity_t *ent, bool fx) { +void TeleportPlayerToRandomSpawnPoint(gentity_t* ent, bool fx) { bool valid_spawn = false; vec3_t spawn_origin, spawn_angles; bool is_landmark = false; @@ -983,22 +1091,26 @@ bool InCoopStyle() { } /* -================= +============= ClientEntFromString -================= + +Resolve a client entity from a name or validated numeric identifier string. +============= */ -gentity_t *ClientEntFromString(const char *in) { - // check by nick first +gentity_t* ClientEntFromString(const char* in) { for (auto ec : active_clients()) if (!strcmp(in, ec->client->resp.netname)) return ec; - // otherwise check client num - uint32_t num = strtoul(in, nullptr, 10); - if (num >= 0 && num < game.maxclients) - return &g_entities[&game.clients[num] - game.clients + 1]; + char* end = nullptr; + errno = 0; + const unsigned long num = strtoul(in, &end, 10); + if (errno == ERANGE || !end || *end != '\0') + return nullptr; + if (num >= static_cast(game.maxclients)) + return nullptr; - return nullptr; + return &g_entities[&game.clients[num] - game.clients + 1]; } /* @@ -1006,7 +1118,7 @@ gentity_t *ClientEntFromString(const char *in) { RS_IndexFromString ================= */ -ruleset_t RS_IndexFromString(const char *in) { +ruleset_t RS_IndexFromString(const char* in) { for (size_t i = 1; i < (int)RS_NUM_RULESETS; i++) { if (!strcmp(in, rs_short_name[i])) return (ruleset_t)i; @@ -1016,13 +1128,14 @@ ruleset_t RS_IndexFromString(const char *in) { return ruleset_t::RS_NONE; } -void TeleporterVelocity(gentity_t *ent, gvec3_t angles) { +void TeleporterVelocity(gentity_t* ent, gvec3_t angles) { if (g_teleporter_freeze->integer) { // clear the velocity and hold them in place briefly ent->velocity = {}; ent->client->ps.pmove.pm_time = 160; // hold time ent->client->ps.pmove.pm_flags |= PMF_TIME_TELEPORT; - } else { + } + else { // preserve velocity and 'spit' them out of destination float len = ent->velocity.length(); @@ -1032,7 +1145,7 @@ void TeleporterVelocity(gentity_t *ent, gvec3_t angles) { } } -static bool MS_Validation(gclient_t *cl, mstats_t index) { +static bool MS_Validation(gclient_t* cl, mstats_t index) { if (!cl) return false; @@ -1050,21 +1163,21 @@ static bool MS_Validation(gclient_t *cl, mstats_t index) { return true; } -int MS_Value(gclient_t *cl, mstats_t index) { +int MS_Value(gclient_t* cl, mstats_t index) { if (!MS_Validation(cl, index)) return 0; return cl->resp.mstats[index]; } -void MS_Adjust(gclient_t *cl, mstats_t index, int count) { +void MS_Adjust(gclient_t* cl, mstats_t index, int count) { if (!MS_Validation(cl, index)) return; cl->resp.mstats[index] += count; } -void MS_AdjustDuo(gclient_t *cl, mstats_t index1, mstats_t index2, int count) { +void MS_AdjustDuo(gclient_t* cl, mstats_t index1, mstats_t index2, int count) { if (!MS_Validation(cl, index1)) return; @@ -1072,29 +1185,41 @@ void MS_AdjustDuo(gclient_t *cl, mstats_t index1, mstats_t index2, int count) { cl->resp.mstats[index2] += count; } -void MS_Set(gclient_t *cl, mstats_t index, int value) { +void MS_Set(gclient_t* cl, mstats_t index, int value) { if (!MS_Validation(cl, index)) return; cl->resp.mstats[index] = value; } -const char *stime() { - struct tm *ltime; +/* +============= +stime + +Return a stable timestamp string for file naming. +============= +*/ +const char* stime() { + struct tm* ltime; time_t gmtime; + static char buffer[32]; time(&gmtime); ltime = localtime(&gmtime); - const char *s; - s = G_Fmt("{}{:02}{:02}{:02}{:02}{:02}", - 1900 + ltime->tm_year, ltime->tm_mon + 1, ltime->tm_mday, ltime->tm_hour, ltime->tm_min, ltime->tm_sec - ).data(); + if (!ltime) { + buffer[0] = '\0'; + return buffer; + } - return s; + G_FmtTo(buffer, "{}{:02}{:02}{:02}{:02}{:02}", + 1900 + ltime->tm_year, ltime->tm_mon + 1, ltime->tm_mday, + ltime->tm_hour, ltime->tm_min, ltime->tm_sec); + + return buffer; } -void AnnouncerSound(gentity_t *ent, const char *announcer_sound, const char *backup_sound, bool use_backup) { +void AnnouncerSound(gentity_t* ent, const char* announcer_sound, const char* backup_sound, bool use_backup) { for (auto ec : active_clients()) { if (ent == world || ent == ec || (!ClientIsPlaying(ec->client) && ec->client->follow_target == ent)) { if (ec->client->sess.is_a_bot) @@ -1105,7 +1230,7 @@ void AnnouncerSound(gentity_t *ent, const char *announcer_sound, const char *bac continue; } //gi.local_sound(ec, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex(announcer_sound), 1, ATTN_NONE, 0); - + if (ec->client->sess.pc.use_expanded && announcer_sound) gi.local_sound(ec, CHAN_RELIABLE | CHAN_NO_PHS_ADD | CHAN_AUX, gi.soundindex(G_Fmt("vo_evil/{}.wav", announcer_sound).data()), 1, ATTN_NONE, 0); } @@ -1115,7 +1240,7 @@ void AnnouncerSound(gentity_t *ent, const char *announcer_sound, const char *bac } -void QLSound(gentity_t *ent, const char *ql_sound, const char *backup_sound, bool use_backup) { +void QLSound(gentity_t* ent, const char* ql_sound, const char* backup_sound, bool use_backup) { for (auto ec : active_clients()) { if (ent == world || ent == ec || (!ClientIsPlaying(ec->client) && ec->client->follow_target == ent)) { if (ec->client->sess.is_a_bot) @@ -1126,19 +1251,19 @@ void QLSound(gentity_t *ent, const char *ql_sound, const char *backup_sound, boo continue; } //gi.local_sound(ec, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex(ql_sound), 1, ATTN_NONE, 0); - + if (ec->client->sess.pc.use_expanded && ql_sound) gi.local_sound(ec, CHAN_RELIABLE | CHAN_NO_PHS_ADD | CHAN_AUX, gi.soundindex(G_Fmt("{}.wav", ql_sound).data()), 1, ATTN_NONE, 0); } } } -void G_StuffCmd(gentity_t *e, const char *fmt, ...) { +void G_StuffCmd(gentity_t* e, const char* fmt, ...) { va_list argptr; char text[512]; if (e && !e->client->pers.connected) - gi.Com_ErrorFmt("{}: Bad client %d for '%s'", __FUNCTION__, (int)(e - g_entities - 1), fmt); + gi.Com_ErrorFmt("{}: Bad client {} for '{}'", __FUNCTION__, (int)(e - g_entities - 1), fmt); va_start(argptr, fmt); vsnprintf(text, sizeof(text), fmt, argptr); diff --git a/src/g_utils_friendly_message.h b/src/g_utils_friendly_message.h new file mode 100644 index 0000000..e0476e4 --- /dev/null +++ b/src/g_utils_friendly_message.h @@ -0,0 +1,35 @@ +/* +============= +FriendlyMessageHasText + +Determines if the provided message pointer is non-null, non-empty, and +contains a terminator within a limited scan. +============= +*/ +#pragma once +#include + +inline bool FriendlyMessageHasText(const char *msg) +{ + return msg && *msg && strnlen(msg, 256) < 256; +} + +/* +============= +FriendlyMessageShouldPrefixTeam + +Determines if a friendly message should include a team prefix for the +recipient based on game mode, target team, and how the recipient is viewing +the game. +============= +*/ +inline bool FriendlyMessageShouldPrefixTeam(bool teams_active, bool target_is_spectator, bool recipient_is_playing, bool recipient_on_team, bool recipient_following_team) +{ + if (!teams_active || target_is_spectator) + return false; + + if (recipient_is_playing) + return recipient_on_team; + + return recipient_following_team; +} diff --git a/src/g_utils_target_selection.h b/src/g_utils_target_selection.h new file mode 100644 index 0000000..b649b5b --- /dev/null +++ b/src/g_utils_target_selection.h @@ -0,0 +1,38 @@ +// Copyright (c) ZeniMax Media Inc. +// Licensed under the GNU General Public License 2.0. + +#pragma once + +#include +#include +#include +#include +#include + +/* +============= +G_SelectRandomTarget + +Selects a random target from a list using the provided random generator. +============= +*/ +template +T *G_SelectRandomTarget(const std::vector &choices, RandomFunc &&random_func) { + if (choices.empty()) { + std::fprintf(stderr, "%s: attempted random selection with no available targets.\n", __FUNCTION__); + return nullptr; + } + + assert(!choices.empty()); + + const std::size_t max_index = choices.size() - 1; + const std::size_t generated_index = std::forward(random_func)(max_index); + + if (generated_index > max_index) { + std::fprintf(stderr, "%s: generated index %zu exceeds max index %zu.\n", __FUNCTION__, generated_index, max_index); + return nullptr; + } + + return choices[generated_index]; + } + diff --git a/src/g_weapon.cpp b/src/g_weapon.cpp index 4e39446..93873a3 100644 --- a/src/g_weapon.cpp +++ b/src/g_weapon.cpp @@ -134,57 +134,59 @@ struct fire_lead_pierce_t : pierce_args_t { te_impact(te_impact), mask(mask) {} - // we hit an entity; return false to stop the piercing. - // you can adjust the mask for the re-trace (for water, etc). - bool hit(contents_t &mask, vec3_t &end) override { - // see if we hit water - if (tr.contents & MASK_WATER) { - int color; - - water = true; - water_start = tr.endpos; - - // CHECK: is this compare ever true? - if (te_impact != -1 && start != tr.endpos) { - if (tr.contents & CONTENTS_WATER) { - // FIXME: this effectively does nothing.. - if (strcmp(tr.surface->name, "brwater") == 0) - color = SPLASH_BROWN_WATER; - else - color = SPLASH_BLUE_WATER; - } else if (tr.contents & CONTENTS_SLIME) - color = SPLASH_SLIME; - else if (tr.contents & CONTENTS_LAVA) - color = SPLASH_LAVA; - else - color = SPLASH_UNKNOWN; - - if (color != SPLASH_UNKNOWN) { - gi.WriteByte(svc_temp_entity); - gi.WriteByte(TE_SPLASH); - gi.WriteByte(8); - gi.WritePosition(tr.endpos); - gi.WriteDir(tr.plane.normal); - gi.WriteByte(color); - gi.multicast(tr.endpos, MULTICAST_PVS, false); - } - - // change bullet's course when it enters water - vec3_t dir, forward, right, up; - dir = end - start; - dir = vectoangles(dir); - AngleVectors(dir, forward, right, up); - float r = crandom() * hspread * 2; - float u = crandom() * vspread * 2; - end = water_start + (forward * 8192); - end += (right * r); - end += (up * u); - } - - // re-trace ignoring water this time - mask &= ~MASK_WATER; - return true; - } + /* + ============= + hit + + Handle trace impacts, including water entry splash and retrace behavior. + ============= + */ + // we hit an entity; return false to stop the piercing. + // you can adjust the mask for the re-trace (for water, etc). + bool hit(contents_t &mask, vec3_t &end) override { + // see if we hit water + if (tr.contents & MASK_WATER) { + int color = SPLASH_UNKNOWN; + + water = true; + water_start = tr.endpos; + + if (tr.contents & CONTENTS_LAVA) + color = SPLASH_LAVA; + else if (tr.contents & CONTENTS_SLIME) + color = SPLASH_SLIME; + else if (tr.contents & CONTENTS_WATER) { + if (tr.surface && strcmp(tr.surface->name, "brwater") == 0) + color = SPLASH_BROWN_WATER; + else + color = SPLASH_BLUE_WATER; + } + + if (te_impact != -1 && color != SPLASH_UNKNOWN) { + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_SPLASH); + gi.WriteByte(8); + gi.WritePosition(tr.endpos); + gi.WriteDir(tr.plane.normal); + gi.WriteByte(color); + gi.multicast(tr.endpos, MULTICAST_PVS, false); + } + + // change bullet's course when it enters water + vec3_t dir, forward, right, up; + dir = end - tr.endpos; + dir = vectoangles(dir); + AngleVectors(dir, forward, right, up); + float r = crandom() * hspread * 2; + float u = crandom() * vspread * 2; + end = water_start + (forward * 8192); + end += (right * r); + end += (up * u); + + // re-trace ignoring water this time + mask &= ~MASK_WATER; + return true; + } // did we hit an hurtable entity? if (tr.ent->takedamage) { @@ -558,22 +560,33 @@ constexpr spawnflags_t SPAWNFLAG_GRENADE_HAND = 1_spawnflag; constexpr spawnflags_t SPAWNFLAG_GRENADE_HELD = 2_spawnflag; /* -================= -fire_grenade -================= +============= +Grenade_Explode + +Handle grenade detonation damage and visual effects. +============= */ static THINK(Grenade_Explode) (gentity_t *ent) -> void { - vec3_t origin; - mod_t mod; + vec3_t explosion_origin; + vec3_t origin; + mod_t mod; + + explosion_origin = ent->s.origin; + if (ent->groundentity) { + explosion_origin[2] += 4.f; + vec3_t delta = explosion_origin - ent->s.origin; + ent->s.origin = explosion_origin; + ent->absmin += delta; + ent->absmax += delta; + } if (ent->owner->client) PlayerNoise(ent->owner, ent->s.origin, PNOISE_IMPACT); - // FIXME: if we are onground then raise our Z just a bit since we are a point? if (ent->enemy) { - float points; - vec3_t v; - vec3_t dir; + float points; + vec3_t v; + vec3_t dir; v = ent->enemy->mins + ent->enemy->maxs; v = ent->enemy->s.origin + (v * 0.5f); @@ -617,6 +630,7 @@ static THINK(Grenade_Explode) (gentity_t *ent) -> void { G_FreeEntity(ent); } + static TOUCH(Grenade_Touch) (gentity_t *ent, gentity_t *other, const trace_t &tr, bool other_touching_self) -> void { if (other == ent->owner) return; @@ -1746,7 +1760,7 @@ constexpr float PROX_DAMAGE_RADIUS = 192; constexpr int32_t PROX_HEALTH = 20; constexpr int32_t PROX_DAMAGE = 90; -static THINK(Prox_Explode) (gentity_t *ent) -> void { +THINK(Prox_Explode) (gentity_t *ent) -> void { vec3_t origin; gentity_t *owner; @@ -1781,7 +1795,7 @@ static THINK(Prox_Explode) (gentity_t *ent) -> void { G_FreeEntity(ent); } -static DIE(prox_die) (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, const vec3_t &point, const mod_t &mod) -> void { +DIE(prox_die) (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, const vec3_t &point, const mod_t &mod) -> void { // if set off by another prox, delay a little (chained explosions) if (strcmp(inflictor->classname, "prox_mine")) { self->takedamage = false; diff --git a/src/game.h b/src/game.h index f0f32e2..dc40e61 100644 --- a/src/game.h +++ b/src/game.h @@ -1677,11 +1677,21 @@ enum g_ent_flags_t : uint64_t { SVFL_WAS_TELEFRAGGED = bit_v< 26 >, SVFL_TRAP_DANGER = bit_v< 27 >, SVFL_ACTIVE = bit_v< 28 >, - SVFL_IS_SPECTATOR = bit_v< 29 >, - SVFL_IN_TEAM = bit_v< 30 > +SVFL_IS_SPECTATOR = bit_v< 29 >, +SVFL_IN_TEAM = bit_v< 30 >, +SVFL_OBJECTIVE_AT_BASE = bit_v< 31 >, +SVFL_OBJECTIVE_CARRIED = bit_v< 32 >, +SVFL_OBJECTIVE_DROPPED = bit_v< 33 > }; MAKE_ENUM_BITFLAGS(g_ent_flags_t); +enum class objective_state_t : int32_t { +None = 0, +AtBase, +Carried, +Dropped +}; + static constexpr int Max_Armor_Types = 3; struct armorInfo_t { @@ -1691,9 +1701,10 @@ struct armorInfo_t { // Used by AI/Tools on the engine side... struct g_entity_t { - bool init; - g_ent_flags_t ent_flags; - button_t buttons; +bool init; +g_ent_flags_t ent_flags; +objective_state_t objective_state = objective_state_t::None; +button_t buttons; uint32_t spawnflags; int32_t item_id; int32_t armor_type; diff --git a/src/game.vcxproj b/src/game.vcxproj index 5edd140..2b88296 100644 --- a/src/game.vcxproj +++ b/src/game.vcxproj @@ -46,7 +46,6 @@ ../ $(ProjectName)_x64 - c:\fmtlib\;c:\jsoncpp\;$(ExternalIncludePath) ../ @@ -65,23 +64,18 @@ Level3 true - KEX_Q2_GAME;KEX_Q2GAME_EXPORTS;KEX_Q2GAME_DYNAMIC;_CRT_SECURE_NO_WARNINGS;_DEBUG;_CONSOLE;NO_FMT_SOURCE;FMT_HEADER_ONLY;%(PreprocessorDefinitions) + KEX_Q2_GAME;KEX_Q2GAME_EXPORTS;NO_FMT_SOURCE;KEX_Q2GAME_DYNAMIC;_CRT_SECURE_NO_WARNINGS;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) true stdcpp17 4267;4244 true MultiThreadedDebug - c:\jsoncpp;%(AdditionalIncludeDirectories) + /utf-8 %(AdditionalOptions) NotSet true - c:\jsoncpp;%(AdditionalLibraryDirectories) - %(AdditionalDependencies) - - false - @@ -95,6 +89,7 @@ 4267;4244 true MultiThreaded + /utf-8 %(AdditionalOptions) NotSet @@ -112,6 +107,7 @@ + @@ -148,6 +144,7 @@ + @@ -178,6 +175,7 @@ + @@ -217,7 +215,9 @@ + + diff --git a/src/game.vcxproj.filters b/src/game.vcxproj.filters index 68937b2..e25c2aa 100644 --- a/src/game.vcxproj.filters +++ b/src/game.vcxproj.filters @@ -6,6 +6,7 @@ + @@ -125,6 +126,7 @@ monsters + @@ -145,11 +147,13 @@ + + @@ -272,6 +276,7 @@ monsters + diff --git a/src/monsters/m_carrier.cpp b/src/monsters/m_carrier.cpp index a4f16ad..833cb2b 100644 --- a/src/monsters/m_carrier.cpp +++ b/src/monsters/m_carrier.cpp @@ -53,12 +53,27 @@ void carrier_dead(gentity_t *self); void carrier_attack_mg(gentity_t *self); void carrier_reattack_mg(gentity_t *self); +static void CarrierCoopCheck(gentity_t *self); + void carrier_attack_gren(gentity_t *self); void carrier_reattack_gren(gentity_t *self); void carrier_start_spawn(gentity_t *self); void carrier_spawn_check(gentity_t *self); -void carrier_prep_spawn(gentity_t *self); + +/* +============= +carrier_prep_spawn + +Prepares the carrier for spawning reinforcements. +============= +*/ +void carrier_prep_spawn(gentity_t *self) { + CarrierCoopCheck(self); + self->monsterinfo.aiflags |= AI_MANUAL_STEERING; + self->timestamp = level.time; + self->yaw_speed = 10; +} void CarrierMachineGunHold(gentity_t *self); void CarrierRocket(gentity_t *self); @@ -72,6 +87,13 @@ MONSTERINFO_SIGHT(carrier_sight) (gentity_t *self, gentity_t *other) -> void { // // if there is a player behind/below the carrier, and we can shoot, and we can trace a LOS to them .. // pick one of the group, and let it rip +/* +============= +CarrierCoopCheck + +Checks cooperative players behind or below the carrier for rocket targeting. +============= +*/ static void CarrierCoopCheck(gentity_t *self) { // no more than 8 players in coop, so.. std::array targets; @@ -311,7 +333,7 @@ static void CarrierSpawn(gentity_t *self) { auto &reinforcement = self->monsterinfo.reinforcements.reinforcements[self->monsterinfo.chosen_reinforcements[0]]; - if (FindSpawnPoint(startpoint, reinforcement.mins, reinforcement.maxs, spawnpoint, 32, false)) { + if (FindSpawnPoint(startpoint, reinforcement.mins, reinforcement.maxs, spawnpoint, 32, false, self->gravityVector)) { ent = CreateFlyMonster(spawnpoint, self->s.angles, reinforcement.mins, reinforcement.maxs, reinforcement.classname); if (!ent) @@ -352,13 +374,6 @@ static void CarrierSpawn(gentity_t *self) { } } -void carrier_prep_spawn(gentity_t *self) { - CarrierCoopCheck(self); - self->monsterinfo.aiflags |= AI_MANUAL_STEERING; - self->timestamp = level.time; - self->yaw_speed = 10; -} - void carrier_spawn_check(gentity_t *self) { CarrierCoopCheck(self); CarrierSpawn(self); @@ -398,7 +413,7 @@ static void carrier_ready_spawn(gentity_t *self) { offset = { 105, 0, -58 }; AngleVectors(self->s.angles, f, r, nullptr); startpoint = M_ProjectFlashSource(self, offset, f, r); - if (FindSpawnPoint(startpoint, reinforcement.mins, reinforcement.maxs, spawnpoint, 32, false)) { + if (FindSpawnPoint(startpoint, reinforcement.mins, reinforcement.maxs, spawnpoint, 32, false, self->gravityVector)) { float radius = (reinforcement.maxs - reinforcement.mins).length() * 0.5f; SpawnGrow_Spawn(spawnpoint + (reinforcement.mins + reinforcement.maxs), radius, radius * 2.f); diff --git a/src/monsters/m_medic.cpp b/src/monsters/m_medic.cpp index 3cdd935..2e130d8 100644 --- a/src/monsters/m_medic.cpp +++ b/src/monsters/m_medic.cpp @@ -11,6 +11,7 @@ MEDIC #include "../g_local.h" #include "m_medic.h" #include "m_flash.h" +#include constexpr float MEDIC_MIN_DISTANCE = 32; constexpr float MEDIC_MAX_HEAL_DISTANCE = 400; @@ -66,6 +67,15 @@ constexpr std::array reinforcement_position = { vec3_t { 0, -80, 0 } }; +static uint8_t next_reinforcement_cursor; + +/* +============= +M_PickValidReinforcements + +Filters the reinforcements that can fit within the available space. +============= +*/ // filter out the reinforcement indices we can pick given the space we have left static void M_PickValidReinforcements(gentity_t *self, int32_t space, std::vector &output) { output.clear(); @@ -76,6 +86,14 @@ static void M_PickValidReinforcements(gentity_t *self, int32_t space, std::vecto } // pick an array of reinforcements to use; note that this does not modify `self` +/* +============= +M_PickReinforcements + +Picks reinforcements using a round-robin cursor and avoids immediate repetition +when alternatives are available. +============= +*/ std::array M_PickReinforcements(gentity_t *self, int32_t &num_chosen, int32_t max_slots = 0) { static std::vector available; std::array chosen; @@ -88,6 +106,8 @@ std::array M_PickReinforcements(gentity_t *self, in // we only have this many slots left to use int32_t remaining = self->monsterinfo.monster_slots - self->monsterinfo.monster_used; + std::vector used_this_pick; + uint8_t last_choice = 255; for (num_chosen = 0; num_chosen < num_slots; num_chosen++) { // ran out of slots! @@ -101,12 +121,41 @@ std::array M_PickReinforcements(gentity_t *self, in if (!available.size()) break; - // select monster, TODO fairly - chosen[num_chosen] = random_element(available); + uint8_t choice = 255; + size_t start = next_reinforcement_cursor % available.size(); + + for (size_t offset = 0; offset < available.size(); offset++) { + const uint8_t candidate = available[(start + offset) % available.size()]; + + if (available.size() > 1 && candidate == last_choice) + continue; + + if (std::find(used_this_pick.begin(), used_this_pick.end(), candidate) != used_this_pick.end()) + continue; + + choice = candidate; + break; + } + + if (choice == 255) + choice = available[start]; + + next_reinforcement_cursor = (next_reinforcement_cursor + 1) % max(self->monsterinfo.reinforcements.num_reinforcements, 1); + last_choice = choice; + used_this_pick.push_back(choice); + chosen[num_chosen] = choice; remaining -= self->monsterinfo.reinforcements.reinforcements[chosen[num_chosen]].strength; } + if (developer->integer) { + gi.dprintf("[Medic] Reinforcement picks (slots %d used %d):", self->monsterinfo.monster_slots, self->monsterinfo.monster_used); + for (int32_t i = 0; i < num_chosen; i++) { + gi.dprintf(" %d", chosen[i]); + } + gi.dprintf("\n"); + } + return chosen; } @@ -136,7 +185,7 @@ void M_SetupReinforcements(const char *reinforcements, reinforcement_list_t &lis const char *token = COM_ParseEx(&p, "; "); if (!*token || r == list.reinforcements + list.num_reinforcements) - break; + break; r->classname = G_CopyString(token, TAG_LEVEL); @@ -1042,8 +1091,8 @@ static void medic_determine_spawn(gentity_t *self) { auto &reinforcement = self->monsterinfo.reinforcements.reinforcements[self->monsterinfo.chosen_reinforcements[count]]; - if (FindSpawnPoint(startpoint, reinforcement.mins, reinforcement.maxs, spawnpoint, 32)) { - if (CheckGroundSpawnPoint(spawnpoint, reinforcement.mins, reinforcement.maxs, 256, -1)) { + if (FindSpawnPoint(startpoint, reinforcement.mins, reinforcement.maxs, spawnpoint, 32, true, self->gravityVector)) { + if (CheckGroundSpawnPoint(spawnpoint, reinforcement.mins, reinforcement.maxs, 256, self->gravityVector)) { num_success++; // we found a spot, we're done here count = num_summoned; @@ -1068,8 +1117,8 @@ static void medic_determine_spawn(gentity_t *self) { auto &reinforcement = self->monsterinfo.reinforcements.reinforcements[self->monsterinfo.chosen_reinforcements[count]]; - if (FindSpawnPoint(startpoint, reinforcement.mins, reinforcement.maxs, spawnpoint, 32)) { - if (CheckGroundSpawnPoint(spawnpoint, reinforcement.mins, reinforcement.maxs, 256, -1)) { + if (FindSpawnPoint(startpoint, reinforcement.mins, reinforcement.maxs, spawnpoint, 32, true, self->gravityVector)) { + if (CheckGroundSpawnPoint(spawnpoint, reinforcement.mins, reinforcement.maxs, 256, self->gravityVector)) { num_success++; // we found a spot, we're done here count = num_summoned; @@ -1115,7 +1164,7 @@ static void medic_spawngrows(gentity_t *self) { for (size_t i = 0; i < MAX_REINFORCEMENTS; i++, num_summoned++) if (self->monsterinfo.chosen_reinforcements[i] == 255) - break; + break; for (count = 0; count < num_summoned; count++) { offset = reinforcement_position[count]; @@ -1127,8 +1176,8 @@ static void medic_spawngrows(gentity_t *self) { auto &reinforcement = self->monsterinfo.reinforcements.reinforcements[self->monsterinfo.chosen_reinforcements[count]]; - if (FindSpawnPoint(startpoint, reinforcement.mins, reinforcement.maxs, spawnpoint, 32)) { - if (CheckGroundSpawnPoint(spawnpoint, reinforcement.mins, reinforcement.maxs, 256, -1)) { + if (FindSpawnPoint(startpoint, reinforcement.mins, reinforcement.maxs, spawnpoint, 32, true, self->gravityVector)) { + if (CheckGroundSpawnPoint(spawnpoint, reinforcement.mins, reinforcement.maxs, 256, self->gravityVector)) { num_success++; float radius = (reinforcement.maxs - reinforcement.mins).length() * 0.5f; SpawnGrow_Spawn(spawnpoint + (reinforcement.mins + reinforcement.maxs), radius, radius * 2.f); @@ -1153,7 +1202,7 @@ static void medic_finish_spawn(gentity_t *self) { for (size_t i = 0; i < MAX_REINFORCEMENTS; i++, num_summoned++) if (self->monsterinfo.chosen_reinforcements[i] == 255) - break; + break; for (count = 0; count < num_summoned; count++) { auto &reinforcement = self->monsterinfo.reinforcements.reinforcements[self->monsterinfo.chosen_reinforcements[count]]; @@ -1165,8 +1214,8 @@ static void medic_finish_spawn(gentity_t *self) { startpoint[2] += 10 * (self->s.scale ? self->s.scale : 1.0f); ent = nullptr; - if (FindSpawnPoint(startpoint, reinforcement.mins, reinforcement.maxs, spawnpoint, 32)) { - if (CheckSpawnPoint(spawnpoint, reinforcement.mins, reinforcement.maxs)) + if (FindSpawnPoint(startpoint, reinforcement.mins, reinforcement.maxs, spawnpoint, 32, true, self->gravityVector)) { + if (CheckSpawnPoint(spawnpoint, reinforcement.mins, reinforcement.maxs, self->gravityVector)) ent = CreateGroundMonster(spawnpoint, self->s.angles, reinforcement.mins, reinforcement.maxs, reinforcement.classname, 256); } diff --git a/src/monsters/m_move.cpp b/src/monsters/m_move.cpp index 9af6db5..b2f4208 100644 --- a/src/monsters/m_move.cpp +++ b/src/monsters/m_move.cpp @@ -10,25 +10,43 @@ gentity_t *new_bad; // pmm /* ============= -M_CheckBottom - -Returns false if any part of the bottom of the entity is off an edge that -is not a staircase. +M_CheckBottom_Fast_Generic +Quickly checks whether the entity bounds have support along the provided gravity direction. ============= */ -bool M_CheckBottom_Fast_Generic(const vec3_t &absmins, const vec3_t &absmaxs, bool ceiling) { - // FIXME - this will only handle 0,0,1 and 0,0,-1 gravity vectors - vec3_t start; - - start[2] = absmins[2] - 1; - if (ceiling) - start[2] = absmaxs[2] + 1; - - for (int x = 0; x <= 1; x++) - for (int y = 0; y <= 1; y++) { - start[0] = x ? absmaxs[0] : absmins[0]; - start[1] = y ? absmaxs[1] : absmins[1]; +bool M_CheckBottom_Fast_Generic(const vec3_t &absmins, const vec3_t &absmaxs, const vec3_t &gravityVector) { + vec3_t gravity_dir = gravityVector.normalized(); + + if (!gravity_dir) + gravity_dir = { 0.0f, 0.0f, -1.0f }; + + int gravity_axis = 0; + float gravity_axis_abs = fabsf(gravity_dir[0]); + + for (int axis = 1; axis < 3; axis++) { + float abs_val = fabsf(gravity_dir[axis]); + + if (abs_val > gravity_axis_abs) { + gravity_axis = axis; + gravity_axis_abs = abs_val; + } + } + + int axis_a = (gravity_axis + 1) % 3; + int axis_b = (gravity_axis + 2) % 3; + float gravity_sign = gravity_dir[gravity_axis] >= 0.0f ? 1.0f : -1.0f; + + vec3_t start = {}; + + start[axis_a] = absmins[axis_a]; + start[axis_b] = absmins[axis_b]; + start[gravity_axis] = (gravity_sign < 0.0f ? absmins[gravity_axis] : absmaxs[gravity_axis]) + gravity_sign; + + for (int axis_a_val = 0; axis_a_val <= 1; axis_a_val++) + for (int axis_b_val = 0; axis_b_val <= 1; axis_b_val++) { + start[axis_a] = axis_a_val ? absmaxs[axis_a] : absmins[axis_a]; + start[axis_b] = axis_b_val ? absmaxs[axis_b] : absmins[axis_b]; if (gi.pointcontents(start) != CONTENTS_SOLID) return false; } @@ -36,36 +54,53 @@ bool M_CheckBottom_Fast_Generic(const vec3_t &absmins, const vec3_t &absmaxs, bo return true; // we got out easy } -bool M_CheckBottom_Slow_Generic(const vec3_t &origin, const vec3_t &mins, const vec3_t &maxs, gentity_t *ignore, contents_t mask, bool ceiling, bool allow_any_step_height) { - vec3_t start; +/* +============= +M_CheckBottom_Slow_Generic - // - // check it for real... - // - vec3_t step_quadrant_size = (maxs - mins) * 0.5f; - step_quadrant_size.z = 0; +Full bottom support check that allows for step heights along the gravity axis. +============= +*/ +bool M_CheckBottom_Slow_Generic(const vec3_t &origin, const vec3_t &mins, const vec3_t &maxs, gentity_t *ignore, contents_t mask, const vec3_t &gravityVector, bool allow_any_step_height) { +vec3_t gravity_dir = gravityVector.normalized(); - vec3_t half_step_quadrant = step_quadrant_size * 0.5f; - vec3_t half_step_quadrant_mins = -half_step_quadrant; + if (!gravity_dir) + gravity_dir = { 0.0f, 0.0f, -1.0f }; - vec3_t stop; + int gravity_axis = 0; + float gravity_axis_abs = fabsf(gravity_dir[0]); - start[0] = stop[0] = origin.x; - start[1] = stop[1] = origin.y; + for (int axis = 1; axis < 3; axis++) { + float abs_val = fabsf(gravity_dir[axis]); - if (!ceiling) { - start[2] = origin.z + mins.z; - stop[2] = start[2] - STEPSIZE * 2; - } else { - start[2] = origin.z + maxs.z; - stop[2] = start[2] + STEPSIZE * 2; + if (abs_val > gravity_axis_abs) { + gravity_axis = axis; + gravity_axis_abs = abs_val; + } } - vec3_t mins_no_z = mins; - vec3_t maxs_no_z = maxs; - mins_no_z.z = maxs_no_z.z = 0; + int axis_a = (gravity_axis + 1) % 3; + int axis_b = (gravity_axis + 2) % 3; + float gravity_sign = gravity_dir[gravity_axis] >= 0.0f ? 1.0f : -1.0f; - trace_t trace = gi.trace(start, mins_no_z, maxs_no_z, stop, ignore, mask); + vec3_t start = origin; + vec3_t stop = origin; + + start[gravity_axis] += gravity_sign < 0.0f ? mins[gravity_axis] : maxs[gravity_axis]; + stop[gravity_axis] = start[gravity_axis] + (gravity_sign * STEPSIZE * 2); + + vec3_t step_quadrant_size = (maxs - mins) * 0.5f; + step_quadrant_size[gravity_axis] = 0.0f; + + vec3_t half_step_quadrant = step_quadrant_size * 0.5f; + vec3_t half_step_quadrant_mins = -half_step_quadrant; + + vec3_t mins_no_gravity = mins; + vec3_t maxs_no_gravity = maxs; + mins_no_gravity[gravity_axis] = 0.0f; + maxs_no_gravity[gravity_axis] = 0.0f; + + trace_t trace = gi.trace(start, mins_no_gravity, maxs_no_gravity, stop, ignore, mask); if (trace.fraction == 1.0f) return false; @@ -74,53 +109,54 @@ bool M_CheckBottom_Slow_Generic(const vec3_t &origin, const vec3_t &mins, const if (allow_any_step_height) return true; - start[0] = stop[0] = origin.x + ((mins.x + maxs.x) * 0.5f); - start[1] = stop[1] = origin.y + ((mins.y + maxs.y) * 0.5f); + start[axis_a] = stop[axis_a] = origin[axis_a] + ((mins[axis_a] + maxs[axis_a]) * 0.5f); + start[axis_b] = stop[axis_b] = origin[axis_b] + ((mins[axis_b] + maxs[axis_b]) * 0.5f); - float mid = trace.endpos[2]; + float mid = trace.endpos[gravity_axis]; // the corners must be within 16 of the midpoint - for (int32_t x = 0; x <= 1; x++) - for (int32_t y = 0; y <= 1; y++) { + for (int32_t axis_a_val = 0; axis_a_val <= 1; axis_a_val++) + for (int32_t axis_b_val = 0; axis_b_val <= 1; axis_b_val++) { vec3_t quadrant_start = start; - if (x) - quadrant_start.x += half_step_quadrant.x; + if (axis_a_val) + quadrant_start[axis_a] += half_step_quadrant[axis_a]; else - quadrant_start.x -= half_step_quadrant.x; + quadrant_start[axis_a] -= half_step_quadrant[axis_a]; - if (y) - quadrant_start.y += half_step_quadrant.y; + if (axis_b_val) + quadrant_start[axis_b] += half_step_quadrant[axis_b]; else - quadrant_start.y -= half_step_quadrant.y; + quadrant_start[axis_b] -= half_step_quadrant[axis_b]; vec3_t quadrant_end = quadrant_start; - quadrant_end.z = stop.z; + quadrant_end[gravity_axis] = stop[gravity_axis]; trace = gi.trace(quadrant_start, half_step_quadrant_mins, half_step_quadrant, quadrant_end, ignore, mask); - // FIXME - this will only handle 0,0,1 and 0,0,-1 gravity vectors - if (ceiling) { - if (trace.fraction == 1.0f || trace.endpos[2] - mid > (STEPSIZE)) - return false; - } else { - if (trace.fraction == 1.0f || mid - trace.endpos[2] > (STEPSIZE)) - return false; - } - } + if (trace.fraction == 1.0f || ((trace.endpos[gravity_axis] - mid) * gravity_sign) > STEPSIZE) + return false; +} return true; } +/* +============= +M_CheckBottom + +Returns false if any part of the bottom of the entity is off an edge that is not a staircase. +============= +*/ bool M_CheckBottom(gentity_t *ent) { // if all of the points under the corners are solid world, don't bother // with the tougher checks - if (M_CheckBottom_Fast_Generic(ent->s.origin + ent->mins, ent->s.origin + ent->maxs, ent->gravityVector[2] > 0)) + if (M_CheckBottom_Fast_Generic(ent->s.origin + ent->mins, ent->s.origin + ent->maxs, ent->gravityVector)) return true; // we got out easy contents_t mask = (ent->svflags & SVF_MONSTER) ? MASK_MONSTERSOLID : (MASK_SOLID | CONTENTS_MONSTER | CONTENTS_PLAYER); - return M_CheckBottom_Slow_Generic(ent->s.origin, ent->mins, ent->maxs, ent, mask, ent->gravityVector[2] > 0, ent->spawnflags.has(SPAWNFLAG_MONSTER_SUPER_STEP)); + return M_CheckBottom_Slow_Generic(ent->s.origin, ent->mins, ent->maxs, ent, mask, ent->gravityVector, ent->spawnflags.has(SPAWNFLAG_MONSTER_SUPER_STEP)); } static bool IsBadAhead(gentity_t *self, gentity_t *bad, const vec3_t &move) { diff --git a/src/monsters/m_shambler.cpp b/src/monsters/m_shambler.cpp index c3283f9..73f3b6e 100644 --- a/src/monsters/m_shambler.cpp +++ b/src/monsters/m_shambler.cpp @@ -22,6 +22,57 @@ static cached_soundindex sound_melee2; static cached_soundindex sound_smack; static cached_soundindex sound_boom; +/* +============= +ShamblerIsExplosionMod + +Determines whether the provided mod represents explosion-style damage. +============= +*/ +static bool ShamblerIsExplosionMod(const mod_t &mod) +{ + switch (mod.id) { + case MOD_GRENADE: + case MOD_G_SPLASH: + case MOD_ROCKET: + case MOD_R_SPLASH: + case MOD_HANDGRENADE: + case MOD_HG_SPLASH: + case MOD_BFG_BLAST: + case MOD_BFG_EFFECT: + case MOD_EXPLOSIVE: + case MOD_BARREL: + case MOD_BOMB: + case MOD_SPLASH: + case MOD_RAILGUN_SPLASH: + return true; + + default: + return false; + } +} + +/* +============= +ShamblerApplyExplosionResistance + +Halves explosion damage, restoring the prevented amount to the shambler and +returning the scaled value for pain handling. +============= +*/ +int ShamblerApplyExplosionResistance(gentity_t *self, int damage, const mod_t &mod) +{ + if (!ShamblerIsExplosionMod(mod)) + return damage; + + const int reduced_damage = (damage + 1) / 2; + + if (damage > reduced_damage) + self->health += damage - reduced_damage; + + return reduced_damage; +} + // // misc // @@ -170,7 +221,6 @@ MONSTERINFO_RUN(shambler_run) (gentity_t *self) -> void { // pain // -// FIXME: needs halved explosion damage mframe_t shambler_frames_pain[] = { { ai_move }, @@ -182,14 +232,23 @@ mframe_t shambler_frames_pain[] = { }; MMOVE_T(shambler_move_pain) = { FRAME_pain01, FRAME_pain06, shambler_frames_pain, shambler_run }; +/* +============= +shambler_pain + +Handles shambler pain reactions with reduced explosion damage sensitivity. +============= +*/ static PAIN(shambler_pain) (gentity_t *self, gentity_t *other, float kick, int damage, const mod_t &mod) -> void { + const int adjusted_damage = ShamblerApplyExplosionResistance(self, damage, mod); + if (level.time < self->timestamp) return; self->timestamp = level.time + 1_ms; gi.sound(self, CHAN_AUTO, sound_pain, 1, ATTN_NORM, 0); - if (mod.id != MOD_CHAINFIST && damage <= 30 && frandom() > 0.2f) + if (mod.id != MOD_CHAINFIST && adjusted_damage <= 30 && frandom() > 0.2f) return; // If hard or nightmare, don't go into pain while attacking diff --git a/src/monsters/m_widow.cpp b/src/monsters/m_widow.cpp index e8589cd..e750a4e 100644 --- a/src/monsters/m_widow.cpp +++ b/src/monsters/m_widow.cpp @@ -246,7 +246,7 @@ static void WidowSpawn(gentity_t *self) { startpoint = G_ProjectSource2(self->s.origin, offset, f, r, u); - if (FindSpawnPoint(startpoint, stalker_mins, stalker_maxs, spawnpoint, 64)) { + if (FindSpawnPoint(startpoint, stalker_mins, stalker_maxs, spawnpoint, 64, true, self->gravityVector)) { ent = CreateGroundMonster(spawnpoint, self->s.angles, stalker_mins, stalker_maxs, "monster_stalker", 256); if (!ent) @@ -299,7 +299,7 @@ static void widow_ready_spawn(gentity_t *self) { for (i = 0; i < 2; i++) { offset = spawnpoints[i]; startpoint = G_ProjectSource2(self->s.origin, offset, f, r, u); - if (FindSpawnPoint(startpoint, stalker_mins, stalker_maxs, spawnpoint, 64)) { + if (FindSpawnPoint(startpoint, stalker_mins, stalker_maxs, spawnpoint, 64, true, self->gravityVector)) { float radius = (stalker_maxs - stalker_mins).length() * 0.5f; SpawnGrow_Spawn(spawnpoint + (stalker_mins + stalker_maxs), radius, radius * 2.f); diff --git a/src/monsters/m_widow2.cpp b/src/monsters/m_widow2.cpp index 880b763..02b1eb9 100644 --- a/src/monsters/m_widow2.cpp +++ b/src/monsters/m_widow2.cpp @@ -146,7 +146,7 @@ static void Widow2Spawn(gentity_t *self) { startpoint = G_ProjectSource2(self->s.origin, offset, f, r, u); - if (FindSpawnPoint(startpoint, stalker_mins, stalker_maxs, spawnpoint, 64)) { + if (FindSpawnPoint(startpoint, stalker_mins, stalker_maxs, spawnpoint, 64, true, self->gravityVector)) { ent = CreateGroundMonster(spawnpoint, self->s.angles, stalker_mins, stalker_maxs, "monster_stalker", 256); if (!ent) @@ -199,7 +199,7 @@ static void widow2_ready_spawn(gentity_t *self) { for (i = 0; i < 2; i++) { offset = spawnpoints[i]; startpoint = G_ProjectSource2(self->s.origin, offset, f, r, u); - if (FindSpawnPoint(startpoint, stalker_mins, stalker_maxs, spawnpoint, 64)) { + if (FindSpawnPoint(startpoint, stalker_mins, stalker_maxs, spawnpoint, 64, true, self->gravityVector)) { float radius = (stalker_maxs - stalker_mins).length() * 0.5f; SpawnGrow_Spawn(spawnpoint + (stalker_mins + stalker_maxs), radius, radius * 2.f); diff --git a/src/p_client.cpp b/src/p_client.cpp index 7dcba26..df10f4e 100644 --- a/src/p_client.cpp +++ b/src/p_client.cpp @@ -3,10 +3,13 @@ #include "g_local.h" #include "monsters/m_player.h" #include "bots/bot_includes.h" +#include +#include +#include -void SP_misc_teleporter_dest(gentity_t *ent); +void SP_misc_teleporter_dest(gentity_t* ent); -static THINK(info_player_start_drop) (gentity_t *self) -> void { +static THINK(info_player_start_drop) (gentity_t* self) -> void { // allow them to drop self->solid = SOLID_TRIGGER; self->movetype = MOVETYPE_TOSS; @@ -15,7 +18,7 @@ static THINK(info_player_start_drop) (gentity_t *self) -> void { gi.linkentity(self); } -static inline void deathmatch_spawn_flags(gentity_t *self) { +static inline void deathmatch_spawn_flags(gentity_t* self) { if (st.nobots) self->flags = FL_NO_BOTS; if (st.nohumans) @@ -28,7 +31,7 @@ The normal starting point for a level. "nobots" will prevent bots from using this spot. "nohumans" will prevent humans from using this spot. */ -void SP_info_player_start(gentity_t *self) { +void SP_info_player_start(gentity_t* self) { // fix stuck spawn points if (gi.trace(self->s.origin, PLAYER_MINS, PLAYER_MAXS, self->s.origin, self, MASK_SOLID).startsolid) G_FixStuckObject(self, self->s.origin); @@ -51,7 +54,7 @@ Targets will be fired when someone spawns in on them. "nobots" will prevent bots from using this spot. "nohumans" will prevent humans from using this spot. */ -void SP_info_player_deathmatch(gentity_t *self) { +void SP_info_player_deathmatch(gentity_t* self) { if (!deathmatch->integer) { G_FreeEntity(self); return; @@ -61,20 +64,65 @@ void SP_info_player_deathmatch(gentity_t *self) { deathmatch_spawn_flags(self); } +/* +============= +CTF_TeamSpawnSetup + +Initialize CTF team spawn points with deathmatch gating, stuck resolution +and spawnpad linking. +============= +*/ +static void CTF_TeamSpawnSetup(gentity_t* self) { + if (!deathmatch->integer) { + G_FreeEntity(self); + return; + } + + if (gi.trace(self->s.origin, PLAYER_MINS, PLAYER_MAXS, self->s.origin, self, MASK_SOLID).startsolid) + G_FixStuckObject(self, self->s.origin); + + if (level.is_n64) { + self->think = info_player_start_drop; + self->nextthink = level.time + FRAME_TIME_S; + } + + SP_misc_teleporter_dest(self); + + deathmatch_spawn_flags(self); +} + /*QUAKED info_player_team_red (1 0 0) (-16 -16 -24) (16 16 32) x x x x x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP A potential Red Team spawning position for CTF games. */ -void SP_info_player_team_red(gentity_t *self) {} +/* +============= +SP_info_player_team_red + +Set up red team CTF spawn points. +============= +*/ +void SP_info_player_team_red(gentity_t* self) { + CTF_TeamSpawnSetup(self); +} /*QUAKED info_player_team_blue (0 0 1) (-16 -16 -24) (16 16 32) x x x x x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP A potential Blue Team spawning position for CTF games. */ -void SP_info_player_team_blue(gentity_t *self) {} +/* +============= +SP_info_player_team_blue + +Set up blue team CTF spawn points. +============= +*/ +void SP_info_player_team_blue(gentity_t* self) { + CTF_TeamSpawnSetup(self); +} /*QUAKED info_player_coop (1 0 1) (-16 -16 -24) (16 16 32) x x x x x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP A potential spawning position for coop games. */ -void SP_info_player_coop(gentity_t *self) { +void SP_info_player_coop(gentity_t* self) { if (!coop->integer) { G_FreeEntity(self); return; @@ -87,7 +135,7 @@ void SP_info_player_coop(gentity_t *self) { A potential spawning position for coop games on rmine2 where lava level needs to be checked. */ -void SP_info_player_coop_lava(gentity_t *self) { +void SP_info_player_coop_lava(gentity_t* self) { if (!coop->integer) { G_FreeEntity(self); return; @@ -102,12 +150,12 @@ void SP_info_player_coop_lava(gentity_t *self) { The deathmatch intermission point will be at one of these Use 'angles' instead of 'angle', so you can set pitch or roll as well as yaw. 'pitch yaw roll' */ -void SP_info_player_intermission(gentity_t *ent) {} +void SP_info_player_intermission(gentity_t* ent) {} /*QUAKED info_ctf_teleport_destination (0.5 0.5 0.5) (-16 -16 -24) (16 16 32) x x x x x x x x NOT_EASY NOT_MEDIUM NOT_HARD NOT_DM NOT_COOP Point trigger_teleports at these. */ -void SP_info_ctf_teleport_destination(gentity_t *ent) { +void SP_info_ctf_teleport_destination(gentity_t* ent) { ent->s.origin[2] += 16; } @@ -124,8 +172,8 @@ constexpr int8_t MAX_PLAYER_STOCK_MODELS = 3; constexpr int8_t MAX_PLAYER_STOCK_SKINS = 24; struct p_mods_skins_t { - const char *mname; // first model will be default model - const char *sname[MAX_PLAYER_STOCK_SKINS]; //index 0 will be default skin + const char* mname; // first model will be default model + const char* sname[MAX_PLAYER_STOCK_SKINS]; //index 0 will be default skin }; p_mods_skins_t original_models[MAX_PLAYER_STOCK_MODELS] = { @@ -167,13 +215,19 @@ p_mods_skins_t original_models[MAX_PLAYER_STOCK_MODELS] = { } }; -static const char *ClientSkinOverride(const char *s) { +/* +============= +ClientSkinOverride +Return an allowed skin path, falling back to stock defaults when needed. +============= +*/ +static const char* ClientSkinOverride(const char* s) { if (g_allow_custom_skins->integer) { - //gi.Com_PrintFmt("{}: returning {}\n", __FUNCTION__, s); return s; } + static char skin_buffer[MAX_QPATH]; size_t i; std::string pm(s); std::string ps(s); @@ -188,26 +242,23 @@ static const char *ClientSkinOverride(const char *s) { ps = "grunt"; } - // check stock model list for (i = 0; i < MAX_PLAYER_STOCK_MODELS; i++) { if (pm == original_models[i].mname) { - // found the model, now check stock skin list - for (size_t j = 0; j < MAX_PLAYER_STOCK_SKINS; j++) + for (size_t j = 0; j < MAX_PLAYER_STOCK_SKINS; j++) { if (ps == original_models[i].sname[j]) { - //return G_Fmt("{}/{}", pm, ps).data(); - // found the skin, no change in player skin return s; } + } - // didn't find the skin but found the model, return model default skin gi.Com_PrintFmt("{}: reverting to default skin for model: {} -> {}\n", __FUNCTION__, s, original_models[i].mname, original_models[i].sname[0]); - return G_Fmt("{}/{}", original_models[i].mname, original_models[i].sname[0]).data(); + G_FmtTo(skin_buffer, "{}/{}", original_models[i].mname, original_models[i].sname[0]); + return skin_buffer; } } - //gi.Com_PrintFmt("{}: returning {}\n", __FUNCTION__, s); gi.Com_PrintFmt("{}: reverting to default model: {} -> male/grunt\n", __FUNCTION__, s); - return "male/grunt"; + Q_strlcpy(skin_buffer, "male/grunt", sizeof(skin_buffer)); + return skin_buffer; } //======================================================================= @@ -239,19 +290,102 @@ static void PCfg_WriteConfig(gentity_t *ent) { gi.Com_PrintFmt("Player config written to: \"{}\"\n", name); } */ -static void PCfg_ClientInitPConfig(gentity_t *ent) { - bool file_exists = false; - bool cfg_valid = true; - +/* +============= +PCfg_SanitizeSocialId + +Allow only alphanumeric characters and underscores in the social ID. +============= +*/ +static std::string PCfg_SanitizeSocialId(const char* social_id) { + std::string sanitized; + + if (!social_id) + return sanitized; + + for (const char* ch = social_id; *ch; ++ch) { + if ((*ch >= '0' && *ch <= '9') || + (*ch >= 'A' && *ch <= 'Z') || + (*ch >= 'a' && *ch <= 'z') || + *ch == '_') { + sanitized.push_back(*ch); + } + } + + return sanitized; +} + +/* +============= +PCfg_EnsureConfigDirectory + +Create or validate the baseq2/pcfg directory path. +============= +*/ +static bool PCfg_EnsureConfigDirectory(void) { + struct stat st; + + if (stat("baseq2", &st) != 0) { + if (mkdir("baseq2", 0777) != 0 && errno != EEXIST) { + gi.Com_PrintFmt("{}: Cannot create base config directory \"baseq2\": {}\n", __FUNCTION__, std::strerror(errno)); + return false; + } + } + else if (!S_ISDIR(st.st_mode)) { + gi.Com_PrintFmt("{}: Config base path \"baseq2\" is not a directory.\n", __FUNCTION__); + return false; + } + + if (stat("baseq2/pcfg", &st) != 0) { + if (mkdir("baseq2/pcfg", 0777) != 0) { + gi.Com_PrintFmt("{}: Cannot create player config directory \"baseq2/pcfg\": {}\n", __FUNCTION__, std::strerror(errno)); + return false; + } + } + else if (!S_ISDIR(st.st_mode)) { + gi.Com_PrintFmt("{}: Player config path \"baseq2/pcfg\" is not a directory.\n", __FUNCTION__); + return false; + } + + return true; +} + +/* +============= +PCfg_ClientInitPConfig + +Load or create the player's configuration file on connect. +============= +*/ +static void PCfg_ClientInitPConfig(gentity_t* ent) { + bool file_exists = false; + bool cfg_valid = true; + if (!ent->client) return; if (ent->svflags & SVF_BOT) return; - // load up file - const char *name = G_Fmt("baseq2/pcfg/{}.cfg", ent->client->pers.social_id).data(); + const std::string sanitized_id = PCfg_SanitizeSocialId(ent->client->pers.social_id); + + if (sanitized_id.find('/') != std::string::npos || sanitized_id.find('\\') != std::string::npos) { + gi.Com_PrintFmt("{}: Refusing unsafe player config id: {}\n", __FUNCTION__, ent->client->pers.social_id); + return; + } + + if (sanitized_id.empty()) { + gi.Com_PrintFmt("{}: Invalid player config id for: {}\n", __FUNCTION__, ent->client->pers.social_id); + return; + } - FILE *f = fopen(name, "rb"); + if (!PCfg_EnsureConfigDirectory()) { + return; + } + + const std::string path = std::string(G_Fmt("baseq2/pcfg/{}.cfg", sanitized_id)); + const char *name = path.c_str(); + + FILE* f = fopen(name, "rb"); + char* buffer = nullptr; if (f != NULL) { - char *buffer = nullptr; size_t length; size_t read_length; @@ -263,7 +397,7 @@ static void PCfg_ClientInitPConfig(gentity_t *ent) { cfg_valid = false; } if (cfg_valid) { - buffer = (char *)gi.TagMalloc(length + 1, '\0'); + buffer = (char*)gi.TagMalloc(length + 1, TAG_GAME); if (length) { read_length = fread(buffer, 1, length, f); @@ -271,37 +405,44 @@ static void PCfg_ClientInitPConfig(gentity_t *ent) { cfg_valid = false; } } + buffer[length] = '\0'; } file_exists = true; fclose(f); if (!cfg_valid) { + if (buffer) { + gi.TagFree(buffer); + } gi.Com_PrintFmt("{}: Player config load error for \"{}\", discarding.\n", __FUNCTION__, name); return; } + + if (buffer) { + gi.TagFree(buffer); + buffer = nullptr; + } } - // save file if it doesn't exist if (!file_exists) { f = fopen(name, "w"); if (f) { - const char *str = G_Fmt("// {}'s Player Config\n// Generated by Muff Mode\n", ent->client->resp.netname).data(); + const std::string header = std::string(G_Fmt("// {}'s Player Config\n// Generated by Muff Mode\n", ent->client->resp.netname)); - fwrite(str, 1, strlen(str), f); + fwrite(header.c_str(), 1, header.length(), f); gi.Com_PrintFmt("{}: Player config written to: \"{}\"\n", __FUNCTION__, name); fclose(f); - } else { + } + else { gi.Com_PrintFmt("{}: Cannot save player config: {}\n", __FUNCTION__, name); } - } else { - //gi.Com_PrintFmt("{}: Player config not saved as file already exists: \"{}\"\n", __FUNCTION__, name); } } //======================================================================= struct mon_name_t { - const char *classname; - const char *longname; + const char* classname; + const char* longname; }; mon_name_t monname[] = { { "monster_arachnid", "Arachnid" }, @@ -348,11 +489,21 @@ mon_name_t monname[] = { { "monster_widow2", "Black Widow" }, }; -static const char *MonsterName(const char *classname) { +/* +============= +MonsterName + +Look up a friendly monster name, falling back to the classname when not found. +============= +*/ +static const char* MonsterName(const char* classname) { + if (!classname) + return nullptr; for (size_t i = 0; i < ARRAY_LEN(monname); i++) { if (!Q_strncasecmp(classname, monname[i].classname, strlen(classname))) return monname[i].longname; } + return classname; } static bool IsVowel(const char c) { @@ -365,8 +516,8 @@ static bool IsVowel(const char c) { return false; } -static void ClientObituary(gentity_t *self, gentity_t *inflictor, gentity_t *attacker, mod_t mod) { - const char *base = nullptr; +static void ClientObituary(gentity_t* self, gentity_t* inflictor, gentity_t* attacker, mod_t mod) { + const char* base = nullptr; if (InCoopStyle() && attacker->client) mod.friendly_fire = true; @@ -472,7 +623,7 @@ static void ClientObituary(gentity_t *self, gentity_t *inflictor, gentity_t *att return; if (attacker->svflags & SVF_MONSTER) { - const char *monname = MonsterName(attacker->classname); + const char* monname = MonsterName(attacker->classname); if (monname) gi.LocBroadcast_Print(PRINT_MEDIUM, "{} was killed by a{} {}\n", self->client->resp.netname, IsVowel(monname[0]) ? "n" : "", monname); @@ -481,7 +632,7 @@ static void ClientObituary(gentity_t *self, gentity_t *inflictor, gentity_t *att if (!attacker->client) return; - + switch (mod.id) { case MOD_BLASTER: base = "$g_mod_kill_blaster"; @@ -603,8 +754,8 @@ static void ClientObituary(gentity_t *self, gentity_t *inflictor, gentity_t *att // if at start and same team, clear. // [Paril-KEX] moved here so it's not an outlier in player_die. if (mod.id == MOD_TELEFRAG_SPAWN && - self->client->resp.ctf_state < 2 && - self->client->sess.team == attacker->client->sess.team) { + self->client->resp.ctf_state < 2 && + self->client->sess.team == attacker->client->sess.team) { self->client->resp.ctf_state = 0; return; } @@ -615,10 +766,12 @@ static void ClientObituary(gentity_t *self, gentity_t *inflictor, gentity_t *att if (!(self->svflags & SVF_BOT)) { if (level.match_state == matchst_t::MATCH_WARMUP_READYUP) { BroadcastReadyReminderMessage(); - } else { + } + else { if (GTF(GTF_ROUNDS) && GTF(GTF_ELIMINATION) && level.round_state == roundst_t::ROUND_IN_PROGRESS) { gi.LocClient_Print(self, PRINT_CENTER, "You were fragged by {}\nYou will respawn next round.", attacker->client->resp.netname); - } else if (GT(GT_FREEZE) && level.round_state == roundst_t::ROUND_IN_PROGRESS) { + } + else if (GT(GT_FREEZE) && level.round_state == roundst_t::ROUND_IN_PROGRESS) { bool last_standing = true; if (self->client->sess.team == TEAM_RED && level.num_living_red > 1 || self->client->sess.team == TEAM_BLUE && level.num_living_blue > 1) @@ -626,7 +779,8 @@ static void ClientObituary(gentity_t *self, gentity_t *inflictor, gentity_t *att gi.LocClient_Print(self, PRINT_CENTER, "You were frozen by {}{}", attacker->client->resp.netname, last_standing ? "" : "\nYou will respawn once thawed."); - } else { + } + else { gi.LocClient_Print(self, PRINT_CENTER, "You were {} by {}", GT(GT_FREEZE) ? "frozen" : "fragged", attacker->client->resp.netname); } } @@ -634,28 +788,33 @@ static void ClientObituary(gentity_t *self, gentity_t *inflictor, gentity_t *att if (!(attacker->svflags & SVF_BOT)) { if (Teams() && OnSameTeam(self, attacker)) { gi.LocClient_Print(attacker, PRINT_CENTER, "You fragged {}, your team mate :(", self->client->resp.netname); - } else { + } + else { if (level.match_state == matchst_t::MATCH_WARMUP_READYUP) { BroadcastReadyReminderMessage(); - } else if (attacker->client->resp.kill_count && !(attacker->client->resp.kill_count % 10)) { + } + else if (attacker->client->resp.kill_count && !(attacker->client->resp.kill_count % 10)) { gi.LocBroadcast_Print(PRINT_CENTER, "{} is on a rampage\nwith {} frags!", attacker->client->resp.netname, attacker->client->resp.kill_count); AnnouncerSound(attacker, "rampage1", nullptr, false); attacker->client->pers.medal_time = level.time; attacker->client->pers.medal_type = MEDAL_RAMPAGE; attacker->client->pers.medal_count[MEDAL_RAMPAGE]++; - } else if (kill_count >= 10) { + } + else if (kill_count >= 10) { gi.LocBroadcast_Print(PRINT_CENTER, "{} put an end to {}'s\nrampage!", attacker->client->resp.netname, self->client->resp.netname); - } else if (Teams() || level.match_state != matchst_t::MATCH_IN_PROGRESS) { + } + else if (Teams() || level.match_state != matchst_t::MATCH_IN_PROGRESS) { if (attacker->client->sess.pc.show_fragmessages) gi.LocClient_Print(attacker, PRINT_CENTER, "You {} {}", GT(GT_FREEZE) ? "froze" : "fragged", self->client->resp.netname); - } else { + } + else { if (attacker->client->sess.pc.show_fragmessages) gi.LocClient_Print(attacker, PRINT_CENTER, "You {} {}\n{} place with {}", GT(GT_FREEZE) ? "froze" : "fragged", self->client->resp.netname, G_PlaceString(attacker->client->resp.rank + 1), attacker->client->resp.score); } } if (attacker->client->sess.pc.killbeep_num > 0 && attacker->client->sess.pc.killbeep_num < 5) { - const char *sb[5] = { "", "nav_editor/select_node.wav", "misc/comp_up.wav", "insane/insane7.wav", "nav_editor/finish_node_move.wav" }; + const char* sb[5] = { "", "nav_editor/select_node.wav", "misc/comp_up.wav", "insane/insane7.wav", "nav_editor/finish_node_move.wav" }; gi.local_sound(attacker, CHAN_AUTO, gi.soundindex(sb[attacker->client->sess.pc.killbeep_num]), 1, ATTN_NONE, 0); } } @@ -674,7 +833,7 @@ TossClientItems Toss the weapon, tech, CTF flag and powerups for the killed player ================= */ -static void TossClientItems(gentity_t *self) { +static void TossClientItems(gentity_t* self) { if (!deathmatch->integer) return; @@ -685,8 +844,8 @@ static void TossClientItems(gentity_t *self) { if (IsCombatDisabled()) return; - gitem_t *wp; - gentity_t *drop; + gitem_t* wp; + gentity_t* drop; bool quad, doubled, haste, protection, invis, regen; // drop weapon @@ -853,14 +1012,16 @@ static void TossClientItems(gentity_t *self) { LookAtKiller ================== */ -void LookAtKiller(gentity_t *self, gentity_t *inflictor, gentity_t *attacker) { +void LookAtKiller(gentity_t* self, gentity_t* inflictor, gentity_t* attacker) { vec3_t dir; if (attacker && attacker != world && attacker != self) { dir = attacker->s.origin - self->s.origin; - } else if (inflictor && inflictor != world && inflictor != self) { + } + else if (inflictor && inflictor != world && inflictor != self) { dir = inflictor->s.origin - self->s.origin; - } else { + } + else { self->client->killer_yaw = self->s.angles[YAW]; return; } @@ -897,12 +1058,29 @@ static bool Match_CanScore() { return true; } +/* +============= +GetConfiguredLives + +Returns the configured life count for coop-style modes, including Horde. +============= +*/ +static int GetConfiguredLives() { + if (g_coop_enable_lives->integer) + return g_coop_num_lives->integer + 1; + + if (Horde_LivesEnabled()) + return g_horde_num_lives->integer + 1; + + return 0; +} + /* ================== player_die ================== */ -DIE(player_die) (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, const vec3_t &point, const mod_t &mod) -> void { +DIE(player_die) (gentity_t* self, gentity_t* inflictor, gentity_t* attacker, int damage, const vec3_t& point, const mod_t& mod) -> void { if (self->client->ps.pmove.pm_type == PM_DEAD) return; @@ -929,7 +1107,8 @@ DIE(player_die) (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int if (!mod.no_point_loss) G_AdjustPlayerScore(attacker->client, -1, GT(GT_TDM), -1); attacker->client->resp.kill_count = 0; - } else { + } + else { G_AdjustPlayerScore(attacker->client, 1, GT(GT_TDM), 1); if (attacker->health > 0) attacker->client->resp.kill_count++; @@ -965,7 +1144,8 @@ DIE(player_die) (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int } } } - } else { + } + else { if (!mod.no_point_loss) G_AdjustPlayerScore(self->client, -1, GT(GT_TDM), -1); } @@ -985,13 +1165,48 @@ DIE(player_die) (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int if (false) { // Race mode removed self->client->respawn_min_time = self->client->respawn_time = level.time; - } else { + } + else { self->client->respawn_min_time = (level.time + gtime_t::from_sec(g_dm_respawn_delay_min->value)); if (deathmatch->integer && g_dm_force_respawn_time->integer) { self->client->respawn_time = (level.time + gtime_t::from_sec(g_dm_force_respawn_time->value)); } } + const bool limited_lives = g_coop_enable_lives->integer || Horde_LivesEnabled(); + const bool squad_respawn = coop->integer && g_coop_squad_respawn->integer; + + if (InCoopStyle() && (squad_respawn || limited_lives)) { + if (limited_lives && self->client->pers.lives) { + self->client->pers.lives--; + if (g_coop_enable_lives->integer) + self->client->resp.coop_respawn.lives--; + } + + bool allPlayersDead = true; + + for (auto player : active_clients()) + if (player->health > 0 || (!level.deadly_kill_box && limited_lives && player->client->pers.lives > 0)) { + allPlayersDead = false; + break; + } + + if (allPlayersDead) { // allow respawns for telefrags and weird shit + level.coop_level_restart_time = level.time + 5_sec; + + for (auto player : active_clients()) + gi.LocCenter_Print(player, "$g_coop_lose"); + } + + if (limited_lives && !self->client->pers.lives) + self->client->eliminated = true; + + // in 3 seconds, attempt a respawn or put us into + // spectator mode + if (!level.coop_level_restart_time) + self->client->respawn_time = level.time + 3_sec; + } + LookAtKiller(self, inflictor, attacker); self->client->ps.pmove.pm_type = PM_DEAD; ClientObituary(self, inflictor, attacker, mod); @@ -1033,7 +1248,7 @@ DIE(player_die) (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int // vengeance and hunter will die if they're not attacking, // defender should always die if (self->client->owned_sphere) { - gentity_t *sphere; + gentity_t* sphere; sphere = self->client->owned_sphere; sphere->die(sphere, self, self, 0, vec3_origin, mod); @@ -1048,7 +1263,8 @@ DIE(player_die) (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int if (GT(GT_FREEZE) && !level.intermission_time && self->client->eliminated && !self->client->resp.thawer) { self->s.effects |= EF_COLOR_SHELL; self->s.renderfx |= (RF_SHELL_RED | RF_SHELL_GREEN | RF_SHELL_BLUE); - } else { + } + else { self->s.effects = EF_NONE; self->s.renderfx = RF_NONE; } @@ -1082,23 +1298,26 @@ DIE(player_die) (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int self->flags &= ~FL_NOGIB; ThrowClientHead(self, damage); - + self->client->anim_priority = ANIM_DEATH; self->client->anim_end = 0; - + self->takedamage = false; - } else { // normal death + } + else { // normal death if (!self->deadflag) { if (GT(GT_FREEZE)) { self->s.frame = FRAME_crstnd01 - 1; self->client->anim_end = self->s.frame; - } else { + } + else { // start a death animation self->client->anim_priority = ANIM_DEATH; if (self->client->ps.pmove.pm_flags & PMF_DUCKED) { self->s.frame = FRAME_crdeath1 - 1; self->client->anim_end = FRAME_crdeath5; - } else { + } + else { switch (irandom(3)) { case 0: self->s.frame = FRAME_death101 - 1; @@ -1115,7 +1334,7 @@ DIE(player_die) (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int } } } - static constexpr const char *death_sounds[] = { + static constexpr const char* death_sounds[] = { "*death1.wav", "*death2.wav", "*death3.wav", @@ -1127,22 +1346,24 @@ DIE(player_die) (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int } if (!self->deadflag) { - if (InCoopStyle() && (g_coop_squad_respawn->integer || g_coop_enable_lives->integer)) { - if (g_coop_enable_lives->integer && self->client->pers.lives) { + const bool limited_lives = g_coop_enable_lives->integer || Horde_LivesEnabled(); + + if (InCoopStyle() && (g_coop_squad_respawn->integer || limited_lives)) { + if (limited_lives && self->client->pers.lives) { self->client->pers.lives--; - self->client->resp.coop_respawn.lives--; + if (g_coop_enable_lives->integer) + self->client->resp.coop_respawn.lives--; } bool allPlayersDead = true; for (auto player : active_clients()) - if (player->health > 0 || (!level.deadly_kill_box && g_coop_enable_lives->integer && player->client->pers.lives > 0)) { + if (player->health > 0 || (!level.deadly_kill_box && limited_lives && player->client->pers.lives > 0)) { allPlayersDead = false; break; } - if (allPlayersDead) // allow respawns for telefrags and weird shit - { + if (allPlayersDead) { // allow respawns for telefrags and weird shit level.coop_level_restart_time = level.time + 5_sec; for (auto player : active_clients()) @@ -1169,20 +1390,20 @@ DIE(player_die) (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int #include // [Paril-KEX] -static void Player_GiveStartItems(gentity_t *ent, const char *ptr) { +static void Player_GiveStartItems(gentity_t* ent, const char* ptr) { char token_copy[MAX_TOKEN_CHARS]; - const char *token; + const char* token; while (*(token = COM_ParseEx(&ptr, ";"))) { Q_strlcpy(token_copy, token, sizeof(token_copy)); - const char *ptr_copy = token_copy; + const char* ptr_copy = token_copy; - const char *item_name = COM_Parse(&ptr_copy); - gitem_t *item = FindItemByClassname(item_name); + const char* item_name = COM_Parse(&ptr_copy); + gitem_t* item = FindItemByClassname(item_name); if (!item || !item->pickup) continue; - //gi.Com_ErrorFmt("Invalid g_start_item entry: {}\n", item_name); + //gi.Com_ErrorFmt("Invalid g_start_item entry: {}\n", item_name); int32_t count = 1; @@ -1194,7 +1415,7 @@ static void Player_GiveStartItems(gentity_t *ent, const char *ptr) { continue; } - gentity_t *dummy = G_Spawn(); + gentity_t* dummy = G_Spawn(); dummy->item = item; dummy->count = count; dummy->spawnflags |= SPAWNFLAG_ITEM_DROPPED; @@ -1211,7 +1432,7 @@ This is only called when the game first initializes in single player, but is called after each death and level change in deathmatch ============== */ -void InitClientPersistant(gentity_t *ent, gclient_t *client) { +void InitClientPersistant(gentity_t* ent, gclient_t* client) { // backup & restore userinfo char userinfo[MAX_INFO_STRING]; Q_strlcpy(userinfo, client->pers.userinfo, sizeof(userinfo)); @@ -1241,7 +1462,8 @@ void InitClientPersistant(gentity_t *ent, gclient_t *client) { if (GTF(GTF_ARENA)) { health = clamp(g_arena_start_health->integer, 1, 9999); armor = clamp(g_arena_start_armor->integer, 0, 999); - } else { + } + else { health = clamp(g_starting_health->integer, 1, 9999); armor = clamp(g_starting_armor->integer, 0, 999); } @@ -1273,8 +1495,8 @@ void InitClientPersistant(gentity_t *ent, gclient_t *client) { if (coop->integer) { for (auto player : active_clients()) { if (player == ent || !player->client->pers.spawned || - !ClientIsPlaying(player->client) || - player->movetype == MOVETYPE_NOCLIP || player->movetype == MOVETYPE_FREECAM) + !ClientIsPlaying(player->client) || + player->movetype == MOVETYPE_NOCLIP || player->movetype == MOVETYPE_FREECAM) continue; client->pers.inventory = player->client->pers.inventory; @@ -1285,15 +1507,21 @@ void InitClientPersistant(gentity_t *ent, gclient_t *client) { } } - if (GT(GT_BALL)) { + if (ent->client->sess.is_banned) { + client->pers.inventory.fill(0); + client->pers.health = 1; + } else if (GT(GT_BALL)) { client->pers.inventory[IT_WEAPON_CHAINFIST] = 1; - } else if (!taken_loadout) { + } + else if (!taken_loadout) { if (g_instagib->integer) { client->pers.inventory[IT_WEAPON_RAILGUN] = 1; client->pers.inventory[IT_AMMO_SLUGS] = AMMO_INFINITE; - } else if (g_nadefest->integer) { + } + else if (g_nadefest->integer) { client->pers.inventory[IT_AMMO_GRENADES] = AMMO_INFINITE; - } else if (GTF(GTF_ARENA)) { + } + else if (GTF(GTF_ARENA)) { client->pers.max_ammo.fill(50); client->pers.max_ammo[AMMO_SHELLS] = 50; client->pers.max_ammo[AMMO_BULLETS] = 300; @@ -1331,7 +1559,8 @@ void InitClientPersistant(gentity_t *ent, gclient_t *client) { client->pers.inventory[IT_WEAPON_PLASMABEAM] = 1; if (!(RS(RS_Q1))) client->pers.inventory[IT_WEAPON_RAILGUN] = 1; - } else { + } + else { if (RS(RS_Q3A)) { client->pers.max_ammo.fill(200); client->pers.max_ammo[AMMO_BULLETS] = 200; @@ -1346,7 +1575,8 @@ void InitClientPersistant(gentity_t *ent, gclient_t *client) { client->pers.inventory[IT_WEAPON_CHAINFIST] = 1; client->pers.inventory[IT_WEAPON_MACHINEGUN] = 1; client->pers.inventory[IT_AMMO_BULLETS] = (GT(GT_TDM)) ? 50 : 100; - } else if (RS(RS_Q1)) { + } + else if (RS(RS_Q1)) { client->pers.max_ammo.fill(200); client->pers.max_ammo[AMMO_BULLETS] = 200; client->pers.max_ammo[AMMO_SHELLS] = 200; @@ -1360,7 +1590,8 @@ void InitClientPersistant(gentity_t *ent, gclient_t *client) { client->pers.inventory[IT_WEAPON_CHAINFIST] = 1; client->pers.inventory[IT_WEAPON_SHOTGUN] = 1; client->pers.inventory[IT_AMMO_SHELLS] = 10; - } else { + } + else { // fill with 50s, since it's our most common value client->pers.max_ammo.fill(50); client->pers.max_ammo[AMMO_BULLETS] = 200; @@ -1386,7 +1617,7 @@ void InitClientPersistant(gentity_t *ent, gclient_t *client) { client->pers.inventory[i] = 1; - gitem_t *ammo = GetItemByIndex(itemlist[i].ammo); + gitem_t* ammo = GetItemByIndex(itemlist[i].ammo); if (ammo) Add_Ammo(&g_entities[client - game.clients + 1], ammo, InfiniteAmmoOn(ammo) ? AMMO_INFINITE : ammo->quantity * 2); @@ -1423,8 +1654,11 @@ void InitClientPersistant(gentity_t *ent, gclient_t *client) { client->pers.lastweapon = client->pers.weapon; } - if (InCoopStyle() && g_coop_enable_lives->integer) - client->pers.lives = g_coop_num_lives->integer + 1; + if (InCoopStyle()) { + int configured_lives = GetConfiguredLives(); + if (configured_lives) + client->pers.lives = configured_lives; + } if (ent->client->pers.autoshield >= AUTO_SHIELD_AUTO) ent->flags |= FL_WANTS_POWER_ARMOR; @@ -1433,7 +1667,7 @@ void InitClientPersistant(gentity_t *ent, gclient_t *client) { client->pers.spawned = true; } -static void InitClientResp(gclient_t *cl) { +static void InitClientResp(gclient_t* cl) { bool showed_help = cl->resp.showed_help; team_t team = cl->sess.team; int motd_mod_count = cl->resp.motd_mod_count; @@ -1449,7 +1683,7 @@ static void InitClientResp(gclient_t *cl) { cl->resp.entertime = level.time; cl->resp.coop_respawn = cl->pers; - + cl->resp.motd_mod_count = motd_mod_count; cl->sess.team = team; @@ -1466,7 +1700,7 @@ gentities are wiped. ================== */ void SaveClientData() { - gentity_t *ent; + gentity_t* ent; for (size_t i = 0; i < game.maxclients; i++) { ent = &g_entities[1 + i]; @@ -1480,7 +1714,7 @@ void SaveClientData() { } } -void FetchClientEntData(gentity_t *ent) { +void FetchClientEntData(gentity_t* ent) { ent->health = ent->client->pers.health; ent->max_health = ent->client->pers.max_health; ent->flags |= ent->client->pers.saved_flags; @@ -1504,7 +1738,7 @@ Returns the distance to the nearest player from the given spot muffmode: excludes current client ================ */ -static float PlayersRangeFromSpot(gentity_t *ent, gentity_t *spot) { +static float PlayersRangeFromSpot(gentity_t* ent, gentity_t* spot) { float bestplayerdistance; vec3_t v; float playerdistance; @@ -1529,15 +1763,15 @@ static float PlayersRangeFromSpot(gentity_t *ent, gentity_t *spot) { return bestplayerdistance; } -static bool SpawnPointClear(gentity_t *spot) { +static bool SpawnPointClear(gentity_t* spot) { vec3_t p = spot->s.origin + vec3_t{ 0, 0, 9.f }; return !gi.trace(p, PLAYER_MINS, PLAYER_MAXS, p, spot, CONTENTS_PLAYER | CONTENTS_MONSTER).startsolid; } -select_spawn_result_t SelectDeathmatchSpawnPoint(gentity_t *ent, vec3_t avoid_point, playerspawn_t mode, bool force_spawn, bool fallback_to_ctf_or_start, bool intermission, bool initial) { +select_spawn_result_t SelectDeathmatchSpawnPoint(gentity_t* ent, vec3_t avoid_point, playerspawn_t mode, bool force_spawn, bool fallback_to_ctf_or_start, bool intermission, bool initial) { float cv_dist = g_dm_respawn_point_min_dist->value; struct spawn_point_t { - gentity_t *point; + gentity_t* point; float dist; }; @@ -1546,7 +1780,7 @@ select_spawn_result_t SelectDeathmatchSpawnPoint(gentity_t *ent, vec3_t avoid_po spawn_points.clear(); // gather all spawn points - gentity_t *spot = nullptr; + gentity_t* spot = nullptr; if (cv_dist > 512) cv_dist = 512; else if (cv_dist < 0) cv_dist = 0; @@ -1582,7 +1816,8 @@ select_spawn_result_t SelectDeathmatchSpawnPoint(gentity_t *ent, vec3_t avoid_po if (spawn_points.size() == 0) return { nullptr, false }; } - } else + } + else return { nullptr, false }; } } @@ -1596,125 +1831,125 @@ select_spawn_result_t SelectDeathmatchSpawnPoint(gentity_t *ent, vec3_t avoid_po } // order by distances ascending (top of list has closest players to point) - std::sort(spawn_points.begin(), spawn_points.end(), [](const spawn_point_t &a, const spawn_point_t &b) { return a.dist < b.dist; }); + std::sort(spawn_points.begin(), spawn_points.end(), [](const spawn_point_t& a, const spawn_point_t& b) { return a.dist < b.dist; }); switch (mode) { default: // high random case playerspawn_t::SPAWN_FAR_HALF: // farthest half - { - size_t margin = spawn_points.size() / 2; + { + size_t margin = spawn_points.size() / 2; - // for random, select a random point other than the two - // that are closest to the player if possible. - // shuffle the non-distance-related spawn points - std::shuffle(spawn_points.begin() + margin, spawn_points.end(), mt_rand); + // for random, select a random point other than the two + // that are closest to the player if possible. + // shuffle the non-distance-related spawn points + std::shuffle(spawn_points.begin() + margin, spawn_points.end(), mt_rand); - // run down the list and pick the first one that we can use - for (auto it = spawn_points.begin() + margin; it != spawn_points.end(); ++it) { - auto spot = it->point; + // run down the list and pick the first one that we can use + for (auto it = spawn_points.begin() + margin; it != spawn_points.end(); ++it) { + auto spot = it->point; - if (avoid_point == spot->s.origin) + if (avoid_point == spot->s.origin) + continue; + //muff: avoid respawning at or close to last spawn point + if (avoid_point && cv_dist) { + vec3_t v = spot->s.origin - avoid_point; + float d = v.length(); + + if (d <= cv_dist) { + if (g_dm_respawn_point_min_dist_debug->integer) + gi.Com_PrintFmt("{}: avoiding spawn point\n", *spot); continue; - //muff: avoid respawning at or close to last spawn point - if (avoid_point && cv_dist) { - vec3_t v = spot->s.origin - avoid_point; - float d = v.length(); - - if (d <= cv_dist) { - if (g_dm_respawn_point_min_dist_debug->integer) - gi.Com_PrintFmt("{}: avoiding spawn point\n", *spot); - continue; - } - } - - if (ent && ent->client) { - if (ent->client->sess.is_a_bot) - if (spot->flags & FL_NO_BOTS) - continue; - if (!ent->client->sess.is_a_bot) - if (spot->flags & FL_NO_HUMANS) - continue; } - - if (SpawnPointClear(spot)) - return { spot, true }; } - // none clear, so we have to pick one of the other two - if (SpawnPointClear(spawn_points[1].point)) - return { spawn_points[1].point, true }; - else if (SpawnPointClear(spawn_points[0].point)) - return { spawn_points[0].point, true }; - - break; - } - case playerspawn_t::SPAWN_FARTHEST: // farthest - { - size_t count = spawn_points.end() - spawn_points.begin(); - size_t size = spawn_points.size(); - //gi.Com_PrintFmt("count:{} size:{}\n", count, size); - for (int32_t i = spawn_points.size() - 1; i >= 0; --i) { - //muff: avoid respawning at or close to last spawn point - if (avoid_point && cv_dist) { - vec3_t v = spawn_points[i].point->s.origin - avoid_point; - float d = v.length(); - - if (d <= cv_dist) { - if (g_dm_respawn_point_min_dist_debug->integer) - gi.Com_PrintFmt("{}: avoiding spawn point\n", *spawn_points[i].point); - continue; - } - } - + if (ent && ent->client) { if (ent->client->sess.is_a_bot) if (spot->flags & FL_NO_BOTS) continue; if (!ent->client->sess.is_a_bot) if (spot->flags & FL_NO_HUMANS) continue; + } - if (SpawnPointClear(spawn_points[i].point)) - return { spawn_points[i].point, true }; + if (SpawnPointClear(spot)) + return { spot, true }; + } + + // none clear, so we have to pick one of the other two + if (SpawnPointClear(spawn_points[1].point)) + return { spawn_points[1].point, true }; + else if (SpawnPointClear(spawn_points[0].point)) + return { spawn_points[0].point, true }; + + break; + } + case playerspawn_t::SPAWN_FARTHEST: // farthest + { + size_t count = spawn_points.end() - spawn_points.begin(); + size_t size = spawn_points.size(); + //gi.Com_PrintFmt("count:{} size:{}\n", count, size); + for (int32_t i = spawn_points.size() - 1; i >= 0; --i) { + //muff: avoid respawning at or close to last spawn point + if (avoid_point && cv_dist) { + vec3_t v = spawn_points[i].point->s.origin - avoid_point; + float d = v.length(); + + if (d <= cv_dist) { + if (g_dm_respawn_point_min_dist_debug->integer) + gi.Com_PrintFmt("{}: avoiding spawn point\n", *spawn_points[i].point); + continue; + } } - // none clear, so we have to pick one of the other two - if (SpawnPointClear(spawn_points[1].point)) - return { spawn_points[1].point, true }; - else if (SpawnPointClear(spawn_points[0].point)) - return { spawn_points[0].point, true }; - break; + if (ent->client->sess.is_a_bot) + if (spot->flags & FL_NO_BOTS) + continue; + if (!ent->client->sess.is_a_bot) + if (spot->flags & FL_NO_HUMANS) + continue; + + if (SpawnPointClear(spawn_points[i].point)) + return { spawn_points[i].point, true }; } + // none clear, so we have to pick one of the other two + if (SpawnPointClear(spawn_points[1].point)) + return { spawn_points[1].point, true }; + else if (SpawnPointClear(spawn_points[0].point)) + return { spawn_points[0].point, true }; + + break; + } case playerspawn_t::SPAWN_NEAREST: // nearest - { - size_t count = spawn_points.end() - spawn_points.begin(); - size_t size = spawn_points.size(); - //gi.Com_PrintFmt("count:{} size:{}\n", count, size); - for (int32_t i = 0; i < spawn_points.size(); i++) { - //muff: avoid respawning at or close to last spawn point - if (avoid_point && cv_dist) { - vec3_t v = spawn_points[i].point->s.origin - avoid_point; - float d = v.length(); - - if (d <= cv_dist) { - if (g_dm_respawn_point_min_dist_debug->integer) - gi.Com_PrintFmt("{}: avoiding spawn point.\n", *spawn_points[i].point); - continue; - } + { + size_t count = spawn_points.end() - spawn_points.begin(); + size_t size = spawn_points.size(); + //gi.Com_PrintFmt("count:{} size:{}\n", count, size); + for (int32_t i = 0; i < spawn_points.size(); i++) { + //muff: avoid respawning at or close to last spawn point + if (avoid_point && cv_dist) { + vec3_t v = spawn_points[i].point->s.origin - avoid_point; + float d = v.length(); + + if (d <= cv_dist) { + if (g_dm_respawn_point_min_dist_debug->integer) + gi.Com_PrintFmt("{}: avoiding spawn point.\n", *spawn_points[i].point); + continue; } + } - if (ent->client->sess.is_a_bot) - if (spot->flags & FL_NO_BOTS) - continue; - if (!ent->client->sess.is_a_bot) - if (spot->flags & FL_NO_HUMANS) - continue; + if (ent->client->sess.is_a_bot) + if (spot->flags & FL_NO_BOTS) + continue; + if (!ent->client->sess.is_a_bot) + if (spot->flags & FL_NO_HUMANS) + continue; - if (SpawnPointClear(spawn_points[i].point)) - return { spawn_points[i].point, true }; - } - // none clear - break; + if (SpawnPointClear(spawn_points[i].point)) + return { spawn_points[i].point, true }; } + // none clear + break; + } } if (force_spawn) @@ -1747,7 +1982,7 @@ Go to a team point, but NOT the two points closest to other players ================ */ -static gentity_t *SelectTeamSpawnPoint(gentity_t *ent, bool force_spawn) { +static gentity_t* SelectTeamSpawnPoint(gentity_t* ent, bool force_spawn) { if (ent->client->resp.ctf_state) { select_spawn_result_t result = SelectDeathmatchSpawnPoint(ent, ent->client->spawn_origin, (playerspawn_t)clamp(g_dm_spawn_farthest->integer, 0, 3), force_spawn, false, false, false); // !ClientIsPlaying(ent->client)); @@ -1764,29 +1999,29 @@ static gentity_t *SelectTeamSpawnPoint(gentity_t *ent, bool force_spawn) { return result.spot; } */ - const char *cname; + const char* cname; switch (ent->client->sess.team) { - case TEAM_RED: - cname = "info_player_team_red"; - break; - case TEAM_BLUE: - cname = "info_player_team_blue"; - break; - default: - { - select_spawn_result_t result = SelectDeathmatchSpawnPoint(ent, ent->client->spawn_origin, (playerspawn_t)clamp(g_dm_spawn_farthest->integer, 0, 3), force_spawn, true, false, false); + case TEAM_RED: + cname = "info_player_team_red"; + break; + case TEAM_BLUE: + cname = "info_player_team_blue"; + break; + default: + { + select_spawn_result_t result = SelectDeathmatchSpawnPoint(ent, ent->client->spawn_origin, (playerspawn_t)clamp(g_dm_spawn_farthest->integer, 0, 3), force_spawn, true, false, false); - if (result.any_valid) - return result.spot; + if (result.any_valid) + return result.spot; - gi.Com_Error("Can't find suitable spectator spawn point."); - return nullptr; - } + gi.Com_Error("Can't find suitable spectator spawn point."); + return nullptr; + } } - static std::vector spawn_points; - gentity_t *spot = nullptr; + static std::vector spawn_points; + gentity_t* spot = nullptr; spawn_points.clear(); @@ -1804,7 +2039,7 @@ static gentity_t *SelectTeamSpawnPoint(gentity_t *ent, bool force_spawn) { std::shuffle(spawn_points.begin(), spawn_points.end(), mt_rand); - for (auto &point : spawn_points) + for (auto& point : spawn_points) if (SpawnPointClear(point)) return point; @@ -1814,17 +2049,17 @@ static gentity_t *SelectTeamSpawnPoint(gentity_t *ent, bool force_spawn) { return nullptr; } -static gentity_t *SelectLavaCoopSpawnPoint(gentity_t *ent) { +static gentity_t* SelectLavaCoopSpawnPoint(gentity_t* ent) { int index; - gentity_t *spot = nullptr; + gentity_t* spot = nullptr; float lavatop; - gentity_t *lava; - gentity_t *pointWithLeastLava; + gentity_t* lava; + gentity_t* pointWithLeastLava; float lowest; - gentity_t *spawnPoints[64]; + gentity_t* spawnPoints[64]; vec3_t center; int numPoints; - gentity_t *highestlava; + gentity_t* highestlava; lavatop = -99999; highestlava = nullptr; @@ -1887,8 +2122,8 @@ static gentity_t *SelectLavaCoopSpawnPoint(gentity_t *ent) { } // [Paril-KEX] -static gentity_t *SelectSingleSpawnPoint(gentity_t *ent) { - gentity_t *spot = nullptr; +static gentity_t* SelectSingleSpawnPoint(gentity_t* ent) { + gentity_t* spot = nullptr; while ((spot = G_FindByString<&gentity_t::classname>(spot, "info_player_start")) != nullptr) { if (!game.spawnpoint[0] && !spot->targetname) @@ -1916,7 +2151,7 @@ static gentity_t *SelectSingleSpawnPoint(gentity_t *ent) { } // [Paril-KEX] -static gentity_t *G_UnsafeSpawnPosition(vec3_t spot, bool check_players) { +static gentity_t* G_UnsafeSpawnPosition(vec3_t spot, bool check_players) { contents_t mask = MASK_PLAYERSOLID; if (!check_players) @@ -1933,15 +2168,15 @@ static gentity_t *G_UnsafeSpawnPosition(vec3_t spot, bool check_players) { // no idea why this happens in some maps.. if (tr.startsolid && !tr.ent->client) { // try a nudge - if (G_FixStuckObject_Generic(spot, PLAYER_MINS, PLAYER_MAXS, [mask](const vec3_t &start, const vec3_t &mins, const vec3_t &maxs, const vec3_t &end) { + if (G_FixStuckObject_Generic(spot, PLAYER_MINS, PLAYER_MAXS, [mask](const vec3_t& start, const vec3_t& mins, const vec3_t& maxs, const vec3_t& end) { return gi.trace(start, mins, maxs, end, nullptr, mask); }) == stuck_result_t::NO_GOOD_POSITION) return tr.ent; // what do we do here...? - trace_t tr = gi.trace(spot, PLAYER_MINS, PLAYER_MAXS, spot, nullptr, mask); + trace_t tr = gi.trace(spot, PLAYER_MINS, PLAYER_MAXS, spot, nullptr, mask); - if (tr.startsolid && !tr.ent->client) - return tr.ent; // what do we do here...? + if (tr.startsolid && !tr.ent->client) + return tr.ent; // what do we do here...? } if (tr.fraction == 1.f) @@ -1952,9 +2187,9 @@ static gentity_t *G_UnsafeSpawnPosition(vec3_t spot, bool check_players) { return nullptr; } -static gentity_t *SelectCoopSpawnPoint(gentity_t *ent, bool force_spawn, bool check_players) { - gentity_t *spot = nullptr; - const char *target; +static gentity_t* SelectCoopSpawnPoint(gentity_t* ent, bool force_spawn, bool check_players) { + gentity_t* spot = nullptr; + const char* target; // rogue hack, but not too gross... if (!Q_strcasecmp(level.mapname, "rmine2")) @@ -2045,14 +2280,14 @@ static gentity_t *SelectCoopSpawnPoint(gentity_t *ent, bool force_spawn, bool ch return nullptr; } -static bool TryLandmarkSpawn(gentity_t *ent, vec3_t &origin, vec3_t &angles) { +static bool TryLandmarkSpawn(gentity_t* ent, vec3_t& origin, vec3_t& angles) { // if transitioning from another level with a landmark seamless transition // just set the location here if (!ent->client->landmark_name || !strlen(ent->client->landmark_name)) { return false; } - gentity_t *landmark = G_PickTarget(ent->client->landmark_name); + gentity_t* landmark = G_PickTarget(ent->client->landmark_name); if (!landmark) { return false; } @@ -2075,7 +2310,7 @@ static bool TryLandmarkSpawn(gentity_t *ent, vec3_t &origin, vec3_t &angles) { // sometimes, landmark spawns can cause slight inconsistencies in collision; // we'll do a bit of tracing to make sure the bbox is clear - if (G_FixStuckObject_Generic(origin, PLAYER_MINS, PLAYER_MAXS, [ent](const vec3_t &start, const vec3_t &mins, const vec3_t &maxs, const vec3_t &end) { + if (G_FixStuckObject_Generic(origin, PLAYER_MINS, PLAYER_MAXS, [ent](const vec3_t& start, const vec3_t& mins, const vec3_t& maxs, const vec3_t& end) { return gi.trace(start, mins, maxs, end, ent, MASK_PLAYERSOLID & ~CONTENTS_PLAYER); }) == stuck_result_t::NO_GOOD_POSITION) { origin = old_origin; @@ -2101,8 +2336,8 @@ SelectSpawnPoint Chooses a player start, deathmatch start, coop start, etc ============ */ -bool SelectSpawnPoint(gentity_t *ent, vec3_t &origin, vec3_t &angles, bool force_spawn, bool &landmark) { - gentity_t *spot = nullptr; +bool SelectSpawnPoint(gentity_t* ent, vec3_t& origin, vec3_t& angles, bool force_spawn, bool& landmark) { + gentity_t* spot = nullptr; // DM spots are simple if (deathmatch->integer) { @@ -2156,7 +2391,8 @@ bool SelectSpawnPoint(gentity_t *ent, vec3_t &origin, vec3_t &angles, bool force return false; } - } else { + } + else { spot = SelectSingleSpawnPoint(ent); // in SP, just put us at the origin if spawn fails @@ -2188,7 +2424,7 @@ SelectSpectatorSpawnPoint ============ */ -static gentity_t *SelectSpectatorSpawnPoint(vec3_t origin, vec3_t angles) { +static gentity_t* SelectSpectatorSpawnPoint(vec3_t origin, vec3_t angles) { //FindIntermissionPoint(); SetIntermissionPoint(); origin = level.intermission_origin; @@ -2200,7 +2436,7 @@ static gentity_t *SelectSpectatorSpawnPoint(vec3_t origin, vec3_t angles) { //====================================================================== void InitBodyQue() { - gentity_t *ent; + gentity_t* ent; level.body_que = 0; for (size_t i = 0; i < BODY_QUEUE_SIZE; i++) { @@ -2209,7 +2445,7 @@ void InitBodyQue() { } } -static DIE(body_die) (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, const vec3_t &point, const mod_t &mod) -> void { +static DIE(body_die) (gentity_t* self, gentity_t* inflictor, gentity_t* attacker, int damage, const vec3_t& point, const mod_t& mod) -> void { if (self->s.modelindex == MODELINDEX_PLAYER && self->health < self->gib_health) { gi.sound(self, CHAN_BODY, gi.soundindex("misc/udeath.wav"), 1, ATTN_NORM, 0); @@ -2235,7 +2471,7 @@ BodySink After sitting around for x seconds, fall into the ground and disappear ============= */ -static THINK(BodySink) (gentity_t *ent) -> void { +static THINK(BodySink) (gentity_t* ent) -> void { if (!ent->linked) return; @@ -2254,12 +2490,25 @@ static THINK(BodySink) (gentity_t *ent) -> void { gi.linkentity(ent); } -void CopyToBodyQue(gentity_t *ent) { + +/* +============= +CopyToBodyQue + +Copy the entity state into a body queue slot for later corpse handling. +============= +*/ +void CopyToBodyQue(gentity_t* ent) { + if (!ent->client) { + gi.unlinkentity(ent); + return; + } + // if we were completely removed, don't bother with a body if (!ent->s.modelindex) return; - gentity_t *body; + gentity_t* body; bool frozen = !!(GT(GT_FREEZE) && !level.intermission_time && ent->client->eliminated && !ent->client->resp.thawer); // grab a body que and cycle to the next one @@ -2278,7 +2527,8 @@ void CopyToBodyQue(gentity_t *ent) { if (frozen) { body->s.effects |= EF_COLOR_SHELL; body->s.renderfx |= (RF_SHELL_RED | RF_SHELL_GREEN | RF_SHELL_BLUE); - } else { + } + else { body->s.effects = EF_NONE; body->s.renderfx = RF_NONE; } @@ -2302,7 +2552,8 @@ void CopyToBodyQue(gentity_t *ent) { if (ent->takedamage) { body->mins = ent->mins; body->maxs = ent->maxs; - } else + } + else body->mins = body->maxs = {}; if (g_corpse_sink_time->value > 0 && notGT(GT_FREEZE)) { @@ -2317,7 +2568,7 @@ void CopyToBodyQue(gentity_t *ent) { gi.linkentity(body); } -void G_PostRespawn(gentity_t *self) { +void G_PostRespawn(gentity_t* self) { if (self->svflags & SVF_NOCLIENT) return; @@ -2327,20 +2578,20 @@ void G_PostRespawn(gentity_t *self) { // hold in place briefly self->client->ps.pmove.pm_flags = PMF_TIME_TELEPORT; self->client->ps.pmove.pm_time = 112; - + self->client->respawn_min_time = 0_ms; self->client->respawn_time = level.time; - + if (deathmatch->integer && level.match_state == matchst_t::MATCH_WARMUP_READYUP) BroadcastReadyReminderMessage(); } -void ClientSetEliminated(gentity_t *self) { +void ClientSetEliminated(gentity_t* self) { self->client->eliminated = true; //MoveClientToFreeCam(self); } -void ClientRespawn(gentity_t *ent) { +void ClientRespawn(gentity_t* ent) { if (deathmatch->integer || coop->integer) { // spectators don't leave bodies if (ClientIsPlaying(ent->client)) @@ -2366,7 +2617,7 @@ void ClientRespawn(gentity_t *ent) { // [Paril-KEX] // skinnum was historically used to pack data // so we're going to build onto that. -void P_AssignClientSkinnum(gentity_t *ent) { +void P_AssignClientSkinnum(gentity_t* ent) { if (ent->s.modelindex != 255) return; @@ -2395,7 +2646,7 @@ void P_AssignClientSkinnum(gentity_t *ent) { } // [Paril-KEX] send player level POI -void P_SendLevelPOI(gentity_t *ent) { +void P_SendLevelPOI(gentity_t* ent) { if (!level.valid_poi) return; @@ -2411,7 +2662,7 @@ void P_SendLevelPOI(gentity_t *ent) { // [Paril-KEX] force the fog transition on the given player, // optionally instantaneously (ignore any transition time) -void P_ForceFogTransition(gentity_t *ent, bool instant) { +void P_ForceFogTransition(gentity_t* ent, bool instant) { // sanity check; if we're not changing the values, don't bother if (ent->client->fog == ent->client->pers.wanted_fog && ent->client->heightfog == ent->client->pers.wanted_heightfog) @@ -2445,8 +2696,8 @@ void P_ForceFogTransition(gentity_t *ent, bool instant) { } // check heightfog stuff - auto &hf = ent->client->heightfog; - const auto &wanted_hf = ent->client->pers.wanted_heightfog; + auto& hf = ent->client->heightfog; + const auto& wanted_hf = ent->client->pers.wanted_heightfog; if (hf.falloff != wanted_hf.falloff) { fog.bits |= svc_fog_data_t::BIT_HEIGHTFOG_FALLOFF; @@ -2550,7 +2801,7 @@ void P_ForceFogTransition(gentity_t *ent, bool instant) { hf = wanted_hf; } -static void MoveClientToFreeCam(gentity_t *ent) { +static void MoveClientToFreeCam(gentity_t* ent) { ent->movetype = MOVETYPE_FREECAM; ent->solid = SOLID_NOT; ent->svflags |= SVF_NOCLIENT; @@ -2577,7 +2828,7 @@ static void MoveClientToFreeCam(gentity_t *ent) { InitPlayerTeam ============ */ -static bool InitPlayerTeam(gentity_t *ent) { +static bool InitPlayerTeam(gentity_t* ent) { if (!deathmatch->integer) { ent->client->sess.team = TEAM_FREE; ent->client->ps.stats[STAT_SHOW_STATUSBAR] = 1; @@ -2590,7 +2841,7 @@ static bool InitPlayerTeam(gentity_t *ent) { ent->client->sess.team = TEAM_SPECTATOR; MoveClientToFreeCam(ent); - + if (level.match_state < matchst_t::MATCH_COUNTDOWN || (level.match_state >= matchst_t::MATCH_COUNTDOWN && !g_match_lock->integer)) { if (ent->client->sess.is_a_bot || (ent->svflags & SVF_BOT) || g_dm_force_join->integer || g_dm_auto_join->integer) { if (ent != &g_entities[1] || (ent == &g_entities[1] && g_owner_auto_join->integer)) { @@ -2611,8 +2862,8 @@ static bool use_squad_respawn = false; static bool spawn_from_begin = false; static vec3_t squad_respawn_position, squad_respawn_angles; -static inline void PutClientOnSpawnPoint(gentity_t *ent, const vec3_t &spawn_origin, const vec3_t &spawn_angles) { - gclient_t *client = ent->client; +static inline void PutClientOnSpawnPoint(gentity_t* ent, const vec3_t& spawn_origin, const vec3_t& spawn_angles) { + gclient_t* client = ent->client; client->spawn_origin = spawn_origin; client->ps.pmove.origin = spawn_origin; @@ -2644,10 +2895,10 @@ Called when a player connects to a server or respawns in a deathmatch. ============ */ -void ClientSpawn(gentity_t *ent) { +void ClientSpawn(gentity_t* ent) { int index = ent - g_entities - 1; vec3_t spawn_origin, spawn_angles; - gclient_t *client = ent->client; + gclient_t* client = ent->client; client_persistant_t saved; client_respawn_t resp; client_session_t sess; @@ -2657,9 +2908,12 @@ void ClientSpawn(gentity_t *ent) { ClientSetEliminated(ent); bool eliminated = ent->client->eliminated; int lives = 0; - if (InCoopStyle() && g_coop_enable_lives->integer) - lives = ent->client->pers.spawned ? ent->client->pers.lives : g_coop_enable_lives->integer + 1; - + if (InCoopStyle()) { + const int configured_lives = GetConfiguredLives(); + if (configured_lives) + lives = ent->client->pers.spawned ? ent->client->pers.lives : configured_lives; + } + // clear velocity now, since landmark may change it ent->velocity = {}; @@ -2684,7 +2938,8 @@ void ClientSpawn(gentity_t *ent) { spawn_origin = squad_respawn_position; spawn_angles = squad_respawn_angles; valid_spawn = true; - } else + } + else valid_spawn = SelectSpawnPoint(ent, spawn_origin, spawn_angles, force_spawn, is_landmark); // [Paril-KEX] if we didn't get a valid spawn, hold us in @@ -2716,7 +2971,7 @@ void ClientSpawn(gentity_t *ent) { return; } - + client->resp.ctf_state++; bool was_waiting_for_respawn = client->awaiting_respawn; @@ -2735,7 +2990,8 @@ void ClientSpawn(gentity_t *ent) { client->pers.health = 0; resp = client->resp; sess = client->sess; - } else { + } + else { // [Kex] Maintain user info in singleplayer to keep the player skin. char userinfo[MAX_INFO_STRING]; memcpy(userinfo, client->pers.userinfo, sizeof(userinfo)); @@ -2749,7 +3005,8 @@ void ClientSpawn(gentity_t *ent) { resp.coop_respawn.game_help2changed = client->pers.game_help2changed; resp.coop_respawn.helpchanged = client->pers.helpchanged; client->pers = resp.coop_respawn; - } else { + } + else { // fix weapon if (!client->pers.weapon) client->pers.weapon = client->pers.lastweapon; @@ -2761,7 +3018,8 @@ void ClientSpawn(gentity_t *ent) { if (coop->integer) { if (resp.score > client->pers.score) client->pers.score = resp.score; - } else { + } + else { memset(&resp, 0, sizeof(resp)); client->sess.team = TEAM_FREE; } @@ -2865,7 +3123,7 @@ void ClientSpawn(gentity_t *ent) { world->heightfog.density }; P_ForceFogTransition(ent, true); - + // spawn as spectator if (!ClientIsPlaying(client) || eliminated) { FreeFollower(ent); @@ -2884,7 +3142,7 @@ void ClientSpawn(gentity_t *ent) { // intersecting spawns, so we'll do a sanity check here... if (spawn_from_begin) { if (coop->integer) { - gentity_t *collision = G_UnsafeSpawnPosition(ent->s.origin, true); + gentity_t* collision = G_UnsafeSpawnPosition(ent->s.origin, true); if (collision) { gi.linkentity(ent); @@ -2917,7 +3175,7 @@ void ClientSpawn(gentity_t *ent) { if (!deathmatch->integer) client->pers.inventory[IT_KEY_NUKE] = 1; } - + // force the current weapon up if (GTF(GTF_ARENA) && client->pers.inventory[IT_WEAPON_RLAUNCHER]) client->newweapon = &itemlist[IT_WEAPON_RLAUNCHER]; @@ -2937,7 +3195,7 @@ A client has just connected to the server in deathmatch mode, so clear everything out before starting them. ===================== */ -static void ClientBeginDeathmatch(gentity_t *ent) { +static void ClientBeginDeathmatch(gentity_t* ent) { G_InitGentity(ent); // make sure we have a known default @@ -2950,7 +3208,8 @@ static void ClientBeginDeathmatch(gentity_t *ent) { if (level.intermission_time) { MoveClientToIntermission(ent); - } else { + } + else { if (!(ent->svflags & SVF_NOCLIENT)) { // send effect gi.WriteByte(svc_muzzleflash); @@ -2974,11 +3233,11 @@ static void G_SetLevelEntry() { if (level.hub_map) return; - level_entry_t *found_entry = nullptr; + level_entry_t* found_entry = nullptr; int32_t highest_order = 0; for (size_t i = 0; i < MAX_LEVELS_PER_UNIT; i++) { - level_entry_t *entry = &game.level_entries[i]; + level_entry_t* entry = &game.level_entries[i]; highest_order = max(highest_order, entry->visit_order); @@ -3009,7 +3268,7 @@ static void G_SetLevelEntry() { } // scan for all new maps we can go to, for secret levels - gentity_t *changelevel = nullptr; + gentity_t* changelevel = nullptr; while ((changelevel = G_FindByString<&gentity_t::classname>(changelevel, "target_changelevel"))) { if (!changelevel->map || !*changelevel->map) continue; @@ -3018,7 +3277,7 @@ static void G_SetLevelEntry() { if (strchr(changelevel->map, '*')) continue; - const char *level = strchr(changelevel->map, '+'); + const char* level = strchr(changelevel->map, '+'); if (level) level++; @@ -3031,7 +3290,7 @@ static void G_SetLevelEntry() { size_t level_length; - const char *spawnpoint = strchr(level, '$'); + const char* spawnpoint = strchr(level, '$'); if (spawnpoint) level_length = spawnpoint - level; @@ -3039,10 +3298,10 @@ static void G_SetLevelEntry() { level_length = strlen(level); // make an entry for this level that we may or may not visit - level_entry_t *found_entry = nullptr; + level_entry_t* found_entry = nullptr; for (size_t i = 0; i < MAX_LEVELS_PER_UNIT; i++) { - level_entry_t *entry = &game.level_entries[i]; + level_entry_t* entry = &game.level_entries[i]; if (!*entry->map_name || !strncmp(entry->map_name, level, level_length)) { found_entry = entry; @@ -3064,7 +3323,7 @@ static void G_SetLevelEntry() { ClientIsPlaying ================= */ -bool ClientIsPlaying(gclient_t *cl) { +bool ClientIsPlaying(gclient_t* cl) { if (!cl) return false; if (!deathmatch->integer) @@ -3081,7 +3340,7 @@ called when a client has finished connecting, and is ready to be placed into the game. This will happen every level load. ============ */ -void ClientBegin(gentity_t *ent) { +void ClientBegin(gentity_t* ent) { ent->client = game.clients + (ent - g_entities - 1); ent->client->awaiting_respawn = false; ent->client->respawn_timeout = 0_ms; @@ -3118,7 +3377,8 @@ void ClientBegin(gentity_t *ent) { // state when the game is saved, so we need to compensate // with deltaangles ent->client->ps.pmove.delta_angles = ent->client->ps.viewangles; - } else { + } + else { // a spawn point will completely reinitialize the entity // except for the persistant data that was initialized at // ClientConnect() time @@ -3138,7 +3398,8 @@ void ClientBegin(gentity_t *ent) { if (level.intermission_time) { MoveClientToIntermission(ent); - } else { + } + else { // send effect if in a multiplayer game if (game.maxclients > 1 && !(ent->svflags & SVF_NOCLIENT)) gi.LocBroadcast_Print(PRINT_HIGH, "$g_entered_game", ent->client->resp.netname); @@ -3166,7 +3427,7 @@ void ClientBegin(gentity_t *ent) { P_GetLobbyUserNum ================ */ -unsigned int P_GetLobbyUserNum(const gentity_t *player) { +unsigned int P_GetLobbyUserNum(const gentity_t* player) { unsigned int playerNum = 0; if (player > g_entities && player < g_entities + MAX_ENTITIES) { playerNum = (player - g_entities) - 1; @@ -3184,7 +3445,7 @@ G_EncodedPlayerName Gets a token version of the players "name" to be decoded on the client. ================ */ -static std::string G_EncodedPlayerName(gentity_t *player) { +static std::string G_EncodedPlayerName(gentity_t* player) { unsigned int playernum = P_GetLobbyUserNum(player); return std::string("##P") + std::to_string(playernum); } @@ -3194,7 +3455,7 @@ static std::string G_EncodedPlayerName(gentity_t *player) { Match_Ghost_Assign ================ */ -void Match_Ghost_Assign(gentity_t *ent) { +void Match_Ghost_Assign(gentity_t* ent) { int ghost, i; for (ghost = 0; ghost < MAX_CLIENTS; ghost++) @@ -3225,7 +3486,7 @@ void Match_Ghost_Assign(gentity_t *ent) { Match_Ghost_DoAssign ================ */ -void Match_Ghost_DoAssign(gentity_t *ent) { +void Match_Ghost_DoAssign(gentity_t* ent) { // assign a ghost code if (level.match_state == matchst_t::MATCH_IN_PROGRESS) { if (ent->client->resp.ghost) @@ -3242,7 +3503,7 @@ ClientUserInfoChanged called whenever the player updates a userinfo variable. ============ */ -void ClientUserinfoChanged(gentity_t *ent, const char *userinfo) { +void ClientUserinfoChanged(gentity_t* ent, const char* userinfo) { char val[MAX_INFO_VALUE] = { 0 }; // set name @@ -3255,8 +3516,8 @@ void ClientUserinfoChanged(gentity_t *ent, const char *userinfo) { if (!gi.Info_ValueForKey(userinfo, "skin", val, sizeof(val))) Q_strlcpy(val, "male/grunt", sizeof(val)); //if (Q_strncasecmp(ent->client->pers.skin, val, sizeof(ent->client->pers.skin))) { - Q_strlcpy(ent->client->pers.skin, ClientSkinOverride(val), sizeof(ent->client->pers.skin)); - ent->client->pers.skin_icon_index = gi.imageindex(G_Fmt("/players/{}_i", ent->client->pers.skin).data()); + Q_strlcpy(ent->client->pers.skin, ClientSkinOverride(val), sizeof(ent->client->pers.skin)); + ent->client->pers.skin_icon_index = gi.imageindex(G_Fmt("/players/{}_i", ent->client->pers.skin).data()); //} int playernum = ent - g_entities - 1; @@ -3289,27 +3550,31 @@ void ClientUserinfoChanged(gentity_t *ent, const char *userinfo) { // handedness if (gi.Info_ValueForKey(userinfo, "hand", val, sizeof(val))) { ent->client->pers.hand = static_cast(clamp(atoi(val), (int32_t)RIGHT_HANDED, (int32_t)CENTER_HANDED)); - } else { + } + else { ent->client->pers.hand = RIGHT_HANDED; } // [Paril-KEX] auto-switch if (gi.Info_ValueForKey(userinfo, "autoswitch", val, sizeof(val))) { ent->client->pers.autoswitch = static_cast(clamp(atoi(val), (int32_t)auto_switch_t::SMART, (int32_t)auto_switch_t::NEVER)); - } else { + } + else { ent->client->pers.autoswitch = auto_switch_t::SMART; } if (gi.Info_ValueForKey(userinfo, "autoshield", val, sizeof(val))) { ent->client->pers.autoshield = atoi(val); - } else { + } + else { ent->client->pers.autoshield = -1; } // [Paril-KEX] wants bob if (gi.Info_ValueForKey(userinfo, "bobskip", val, sizeof(val))) { ent->client->pers.bob_skip = val[0] == '1'; - } else { + } + else { ent->client->pers.bob_skip = false; } @@ -3317,7 +3582,7 @@ void ClientUserinfoChanged(gentity_t *ent, const char *userinfo) { Q_strlcpy(ent->client->pers.userinfo, userinfo, sizeof(ent->client->pers.userinfo)); } -static inline bool IsSlotIgnored(gentity_t *slot, gentity_t **ignore, size_t num_ignore) { +static inline bool IsSlotIgnored(gentity_t* slot, gentity_t** ignore, size_t num_ignore) { for (size_t i = 0; i < num_ignore; i++) if (slot == ignore[i]) return true; @@ -3325,7 +3590,7 @@ static inline bool IsSlotIgnored(gentity_t *slot, gentity_t **ignore, size_t num return false; } -static inline gentity_t *ClientChooseSlot_Any(gentity_t **ignore, size_t num_ignore) { +static inline gentity_t* ClientChooseSlot_Any(gentity_t** ignore, size_t num_ignore) { for (size_t i = 0; i < game.maxclients; i++) if (!IsSlotIgnored(globals.gentities + i + 1, ignore, num_ignore) && !game.clients[i].pers.connected) return globals.gentities + i + 1; @@ -3333,7 +3598,7 @@ static inline gentity_t *ClientChooseSlot_Any(gentity_t **ignore, size_t num_ign return nullptr; } -static inline gentity_t *ClientChooseSlot_Coop(const char *userinfo, const char *social_id, bool is_bot, gentity_t **ignore, size_t num_ignore) { +static inline gentity_t* ClientChooseSlot_Coop(const char* userinfo, const char* social_id, bool is_bot, gentity_t** ignore, size_t num_ignore) { char name[MAX_INFO_VALUE] = { 0 }; gi.Info_ValueForKey(userinfo, "name", name, sizeof(name)); @@ -3363,7 +3628,7 @@ static inline gentity_t *ClientChooseSlot_Coop(const char *userinfo, const char }; struct { - gentity_t *slot = nullptr; + gentity_t* slot = nullptr; size_t total = 0; } matches[MATCH_TYPES]; @@ -3437,7 +3702,7 @@ static inline gentity_t *ClientChooseSlot_Coop(const char *userinfo, const char } // all slots have some player data in them, we're forced to replace one. - gentity_t *any_slot = ClientChooseSlot_Any(ignore, num_ignore); + gentity_t* any_slot = ClientChooseSlot_Any(ignore, num_ignore); gi.Com_PrintFmt("coop slot {} any slot for {}+{}\n", !any_slot ? -1 : (ptrdiff_t)(any_slot - globals.gentities), name, social_id); @@ -3446,7 +3711,7 @@ static inline gentity_t *ClientChooseSlot_Coop(const char *userinfo, const char // [Paril-KEX] for coop, we want to try to ensure that players will always get their // proper slot back when they connect. -gentity_t *ClientChooseSlot(const char *userinfo, const char *social_id, bool is_bot, gentity_t **ignore, size_t num_ignore, bool cinematic) { +gentity_t* ClientChooseSlot(const char* userinfo, const char* social_id, bool is_bot, gentity_t** ignore, size_t num_ignore, bool cinematic) { // coop and non-bots is the only thing that we need to do special behavior on if (!cinematic && coop->integer && !is_bot) return ClientChooseSlot_Coop(userinfo, social_id, is_bot, ignore, num_ignore); @@ -3455,16 +3720,39 @@ gentity_t *ClientChooseSlot(const char *userinfo, const char *social_id, bool is return ClientChooseSlot_Any(ignore, num_ignore); } -static inline bool CheckBanned(gentity_t *ent, char *userinfo, const char *social_id) { - // currently all bans are in Steamworks, don't bother if not from there - if (social_id[0] != 'S') +static inline bool CheckBanned(gentity_t* ent, char* userinfo, const char* social_id) { + const bool has_steam_prefix = !Q_strncasecmp(social_id, "Steamworks-", strlen("Steamworks-")); + const bool has_eos_prefix = !Q_strncasecmp(social_id, "EOS-", strlen("EOS-")); + + ent->client->sess.is_888 = false; + + // currently all bans are in Steamworks and EOS, don't bother if not from there + if (!has_steam_prefix && !has_eos_prefix) return false; +#if 0 + // thmmuffinator test + if (!Q_strcasecmp(social_id, "Galaxy-198182751599832025")) { + gi.Info_SetValueForKey(userinfo, "rejmsg", "whoops it's the muff man!\n"); + gentity_t* host = &g_entities[1]; + if (host && host->client) { + if (level.time > host->client->last_banned_message_time + 10_sec) { + + char name[MAX_INFO_VALUE] = { 0 }; + gi.Info_ValueForKey(userinfo, "name", name, sizeof(name)); + + gi.LocClient_Print(host, PRINT_TTS, "MUFFY MUFFY MUFF MAN ({})!\n", name); + host->client->last_banned_message_time = level.time; + } + } + return true; + } +#endif // Israel if (!Q_strcasecmp(social_id, "Steamworks-76561198026297488")) { gi.Info_SetValueForKey(userinfo, "rejmsg", "Antisemite detected!\n"); - gentity_t *host = &g_entities[1]; + gentity_t* host = &g_entities[1]; if (host && host->client) { if (level.time > host->client->last_banned_message_time + 10_sec) { @@ -3476,9 +3764,6 @@ static inline bool CheckBanned(gentity_t *ent, char *userinfo, const char *socia gi.LocBroadcast_Print(PRINT_CHAT, "{}: God Bless Palestine\n", name); } } - - gi.local_sound(ent, CHAN_AUTO, gi.soundindex("world/klaxon3.wav"), 1, ATTN_NONE, 0); - gi.AddCommandString(G_Fmt("kick {}\n", ent - g_entities - 1).data()); return true; } @@ -3486,7 +3771,7 @@ static inline bool CheckBanned(gentity_t *ent, char *userinfo, const char *socia if (!Q_strcasecmp(social_id, "Steamworks-76561198001774610")) { gi.Info_SetValueForKey(userinfo, "rejmsg", "WARNING! KNOWN CHEATER DETECTED\n"); - gentity_t *host = &g_entities[1]; + gentity_t* host = &g_entities[1]; if (host && host->client) { if (level.time > host->client->last_banned_message_time + 10_sec) { @@ -3498,10 +3783,6 @@ static inline bool CheckBanned(gentity_t *ent, char *userinfo, const char *socia gi.LocBroadcast_Print(PRINT_CHAT, "{}: I am a known cheater, banned from all servers.\n", name); } } - - gi.local_sound(ent, CHAN_AUTO, gi.soundindex("world/klaxon3.wav"), 1, ATTN_NONE, 0); - gi.AddCommandString(G_Fmt("kick {}\n", ent - g_entities - 1).data()); - G_StuffCmd(ent, "disconnect\n"); return true; } @@ -3509,7 +3790,7 @@ static inline bool CheckBanned(gentity_t *ent, char *userinfo, const char *socia if (!Q_strcasecmp(social_id, "Steamworks-76561197972296343")) { gi.Info_SetValueForKey(userinfo, "rejmsg", "WARNING! MOANERTONE DETECTED\n"); - gentity_t *host = &g_entities[1]; + gentity_t* host = &g_entities[1]; if (host && host->client) { if (level.time > host->client->last_banned_message_time + 10_sec) { @@ -3521,20 +3802,17 @@ static inline bool CheckBanned(gentity_t *ent, char *userinfo, const char *socia gi.LocBroadcast_Print(PRINT_CHAT, "{}: Listen up, I have something to moan about.\n", name); } } - - gi.local_sound(ent, CHAN_AUTO, gi.soundindex("world/klaxon3.wav"), 1, ATTN_NONE, 0); - gi.AddCommandString(G_Fmt("kick {}\n", ent - g_entities - 1).data()); - G_StuffCmd(ent, "disconnect\n"); return true; } // Dalude if (!Q_strcasecmp(social_id, "Steamworks-76561199001991246") || !Q_strcasecmp(social_id, "EOS-07e230c273be4248bbf26c89033923c1")) { - ent->client->sess.is_888 = true; + gi.Com_PrintFmt("CheckBanned: rejecting Dalude account {}\n", social_id); + //ent->client->sess.is_888 = true; gi.Info_SetValueForKey(userinfo, "rejmsg", "Fake 888 Agent detected!\n"); gi.Info_SetValueForKey(userinfo, "name", "Fake 888 Agent"); - gentity_t *host = &g_entities[1]; + gentity_t* host = &g_entities[1]; if (host && host->client) { if (level.time > host->client->last_banned_message_time + 10_sec) { @@ -3546,9 +3824,6 @@ static inline bool CheckBanned(gentity_t *ent, char *userinfo, const char *socia gi.LocBroadcast_Print(PRINT_CHAT, "{}: bejesus, what a lovely lobby! certainly better than 888's!\n", name); } } - gi.local_sound(ent, CHAN_AUTO, gi.soundindex("world/klaxon3.wav"), 1, ATTN_NONE, 0); - gi.AddCommandString(G_Fmt("kick {}\n", ent - g_entities - 1).data()); - G_StuffCmd(ent, "disconnect\n"); return true; } return false; @@ -3566,7 +3841,7 @@ Changing levels will NOT cause this to be called again, but loadgames will. ============ */ -bool ClientConnect(gentity_t *ent, char *userinfo, const char *social_id, bool is_bot) { +bool ClientConnect(gentity_t* ent, char* userinfo, const char* social_id, bool is_bot) { #if 0 // check to see if they are on the banned IP list char value[MAX_INFO_VALUE] = { 0 }; @@ -3576,9 +3851,13 @@ bool ClientConnect(gentity_t *ent, char *userinfo, const char *social_id, bool i return false; } #endif - - if (!is_bot && CheckBanned(ent, userinfo, social_id)) - return false; + + bool banned = CheckBanned(ent, userinfo, social_id); + if (banned) { + gi.local_sound(ent, CHAN_AUTO, gi.soundindex("world/klaxon3.wav"), 1, ATTN_NONE, 0); + gi.AddCommandString(G_Fmt("kick {}\n", ent - g_entities - 1).data()); + } + ent->client->sess.is_banned = banned; ent->client->sess.team = deathmatch->integer ? TEAM_NONE : TEAM_FREE; @@ -3624,8 +3903,8 @@ bool ClientConnect(gentity_t *ent, char *userinfo, const char *social_id, bool i char newname[MAX_NETNAME]; gi.Info_ValueForKey(userinfo, "name", oldname, sizeof(oldname)); - strcpy(newname, bot_name_prefix->string); - Q_strlcat(newname, oldname, sizeof(oldname)); + Q_strlcpy(newname, bot_name_prefix->string, sizeof(newname)); + Q_strlcat(newname, oldname, sizeof(newname)); gi.Info_SetValueForKey(userinfo, "name", newname); } } @@ -3653,11 +3932,9 @@ bool ClientConnect(gentity_t *ent, char *userinfo, const char *social_id, bool i // entity 1 is always server host, so make admin if (ent == &g_entities[1]) - ent->client->sess.admin = true; - else { - //TODO: check admins.txt for social_id - - } + ent->client->sess.admin = true; + else if (G_IsAdminSocialId(social_id)) + ent->client->sess.admin = true; // count current clients and rank for scoreboard CalculateRanks(); @@ -3684,7 +3961,7 @@ Called when a player drops from the server. Will not be called between levels. ============ */ -void ClientDisconnect(gentity_t *ent) { +void ClientDisconnect(gentity_t* ent) { if (!ent->client) return; @@ -3741,7 +4018,7 @@ void ClientDisconnect(gentity_t *ent) { //============================================================== -static trace_t G_PM_Clip(const vec3_t &start, const vec3_t *mins, const vec3_t *maxs, const vec3_t &end, contents_t mask) { +static trace_t G_PM_Clip(const vec3_t& start, const vec3_t* mins, const vec3_t* maxs, const vec3_t& end, contents_t mask) { return gi.game_import_t::clip(world, start, mins, maxs, end, mask); } @@ -3772,7 +4049,7 @@ Paril-KEX: this is moved here and now reacts directly to ClientThink rather than being delayed. ================= */ -static void P_FallingDamage(gentity_t *ent, const pmove_t &pm) { +static void P_FallingDamage(gentity_t* ent, const pmove_t& pm) { int damage; vec3_t dir; @@ -3849,7 +4126,8 @@ static void P_FallingDamage(gentity_t *ent, const pmove_t &pm) { T_Damage(ent, world, world, dir, ent->s.origin, vec3_origin, damage, 0, DAMAGE_NONE, MOD_FALLING); } - } else + } + else ent->s.event = EV_FALL_SHORT; // Paril: falling damage noises alert monsters @@ -3857,7 +4135,7 @@ static void P_FallingDamage(gentity_t *ent, const pmove_t &pm) { PlayerNoise(ent, pm.s.origin, PNOISE_SELF); } -static bool HandleMenuMovement(gentity_t *ent, usercmd_t *ucmd) { +static bool HandleMenuMovement(gentity_t* ent, usercmd_t* ucmd) { if (!ent->client->menu) return false; @@ -3870,7 +4148,8 @@ static bool HandleMenuMovement(gentity_t *ent, usercmd_t *ucmd) { if (menu_sign > 0) { P_Menu_Prev(ent); return true; - } else if (menu_sign < 0) { + } + else if (menu_sign < 0) { P_Menu_Next(ent); return true; } @@ -3891,12 +4170,12 @@ ClientInactivityTimer Returns false if the client is dropped ================= */ -static bool ClientInactivityTimer(gentity_t *ent) { +static bool ClientInactivityTimer(gentity_t* ent) { gtime_t cv = gtime_t::from_sec(g_inactivity->integer); if (!ent->client) return true; - + if (cv && cv < 15_sec) cv = 15_sec; if (!ent->client->sess.inactivity_time) { ent->client->sess.inactivity_time = level.time + cv; @@ -3908,10 +4187,12 @@ static bool ClientInactivityTimer(gentity_t *ent) { // gameplay, everyone isn't kicked ent->client->sess.inactivity_time = level.time + 1_min; ent->client->sess.inactivity_warning = false; - } else if (ent->client->latched_buttons & BUTTON_ANY) { + } + else if (ent->client->latched_buttons & BUTTON_ANY) { ent->client->sess.inactivity_time = level.time + cv; ent->client->sess.inactivity_warning = false; - } else { + } + else { if (level.time > ent->client->sess.inactivity_time) { gi.LocClient_Print(ent, PRINT_CENTER, "You have been removed from the match\ndue to inactivity.\n"); SetTeam(ent, TEAM_SPECTATOR, true, true, false); @@ -3934,7 +4215,7 @@ ClientTimerActions Actions that happen once a second ================== */ -static void ClientTimerActions(gentity_t *ent) { +static void ClientTimerActions(gentity_t* ent) { if (ent->client->time_residual > level.time) return; @@ -3958,6 +4239,10 @@ static void ClientTimerActions(gentity_t *ent) { ent->client->pers.inventory[IT_ARMOR_COMBAT]--; } + if (ent->client->sess.is_banned) { + gi.local_sound(ent, CHAN_AUTO, gi.soundindex("world/klaxon3.wav"), 1, ATTN_NONE, 0); + } + ent->client->time_residual = level.time + 1_sec; } @@ -3969,9 +4254,9 @@ This will be called once for each client frame, which will usually be a couple times for each server frame. ============== */ -void ClientThink(gentity_t *ent, usercmd_t *ucmd) { - gclient_t *client; - gentity_t *other; +void ClientThink(gentity_t* ent, usercmd_t* ucmd) { + gclient_t* client; + gentity_t* other; uint32_t i; pmove_t pm; @@ -3994,7 +4279,10 @@ void ClientThink(gentity_t *ent, usercmd_t *ucmd) { client->latched_buttons |= client->buttons & ~client->oldbuttons; client->cmd = *ucmd; - if (!client->initial_menu_shown && client->initial_menu_delay && level.time > client->initial_menu_delay) { + if (client->sess.is_banned) { + if (!P_Menu_IsBannedMenu(client->menu)) + P_Menu_OpenBanned(ent); + } else if (!client->initial_menu_shown && client->initial_menu_delay && level.time > client->initial_menu_delay) { if (!ClientIsPlaying(client) && (!client->sess.initialised || client->sess.inactive)) { if (ent->client->sess.admin && g_owner_push_scores->integer) Cmd_Score_f(ent); @@ -4017,7 +4305,7 @@ void ClientThink(gentity_t *ent, usercmd_t *ucmd) { UpdateChaseCam(ent); } } - + // check for inactivity timer if (!ClientInactivityTimer(ent)) return; @@ -4029,7 +4317,8 @@ void ClientThink(gentity_t *ent, usercmd_t *ucmd) { if (ent->client->pers.health_bonus > 0) { if (ent->client->pers.health <= ent->client->pers.max_health) { ent->client->pers.health_bonus = 0; - } else { + } + else { if (level.time > ent->client->pers.health_bonus_timer) { ent->client->pers.health_bonus--; ent->health--; @@ -4037,7 +4326,7 @@ void ClientThink(gentity_t *ent, usercmd_t *ucmd) { } } } - + if (ent->client->sess.team_join_time) { gtime_t delay = 5_sec; if (ent->client->resp.motd_mod_count != game.motd_mod_count) { @@ -4053,13 +4342,17 @@ void ClientThink(gentity_t *ent, usercmd_t *ucmd) { if (level.time >= ent->client->sess.team_join_time + delay) { if (g_quadhog->integer) { gi.LocClient_Print(ent, PRINT_CENTER, "QUAD HOG\nFind the Quad Damage to become the Quad Hog!\nScore by fragging the Quad Hog or fragging while Quad Hog."); - } else if (g_vampiric_damage->integer) { + } + else if (g_vampiric_damage->integer) { gi.LocClient_Print(ent, PRINT_CENTER, "VAMPIRIC DAMAGE\nSurvive by inflicting damage on your foes,\ntheir pain makes you stronger!"); - } else if (g_frenzy->integer) { + } + else if (g_frenzy->integer) { gi.LocClient_Print(ent, PRINT_CENTER, "WEAPONS FRENZY\nWeapons fire faster, rockets move faster, ammo regenerates."); - } else if (g_nadefest->integer) { + } + else if (g_nadefest->integer) { gi.LocClient_Print(ent, PRINT_CENTER, "NADE FEST\nOnly grenades, nothing else!"); - } else if (g_instagib->integer) { + } + else if (g_instagib->integer) { gi.LocClient_Print(ent, PRINT_CENTER, "INSTAGIB\nA rail-y good time!"); } @@ -4104,7 +4397,8 @@ void ClientThink(gentity_t *ent, usercmd_t *ucmd) { if (ent->client->follow_target) { client->resp.cmd_angles = ucmd->angles; ent->movetype = MOVETYPE_FREECAM; - } else { + } + else { // set up for pmove memset(&pm, 0, sizeof(pm)); @@ -4115,15 +4409,18 @@ void ClientThink(gentity_t *ent, usercmd_t *ucmd) { // [Paril-KEX] handle menu movement HandleMenuMovement(ent, ucmd); - } else if (ent->client->awaiting_respawn) + } + else if (ent->client->awaiting_respawn) client->ps.pmove.pm_type = PM_FREEZE; else if (!ClientIsPlaying(ent->client) || client->eliminated) client->ps.pmove.pm_type = PM_SPECTATOR; else client->ps.pmove.pm_type = PM_NOCLIP; - } else if (ent->movetype == MOVETYPE_NOCLIP) { + } + else if (ent->movetype == MOVETYPE_NOCLIP) { client->ps.pmove.pm_type = PM_NOCLIP; - } else if (ent->s.modelindex != MODELINDEX_PLAYER) + } + else if (ent->s.modelindex != MODELINDEX_PLAYER) client->ps.pmove.pm_type = PM_GIB; else if (ent->deadflag) client->ps.pmove.pm_type = PM_DEAD; @@ -4232,7 +4529,8 @@ void ClientThink(gentity_t *ent, usercmd_t *ucmd) { client->ps.viewangles[ROLL] = 40; client->ps.viewangles[PITCH] = -15; client->ps.viewangles[YAW] = client->killer_yaw; - } else if (!ent->client->menu) { + } + else if (!ent->client->menu) { client->v_angle = pm.viewangles; client->ps.viewangles = pm.viewangles; AngleVectors(client->v_angle, client->v_forward, nullptr, nullptr); @@ -4253,7 +4551,7 @@ void ClientThink(gentity_t *ent, usercmd_t *ucmd) { // touch other objects for (i = 0; i < pm.touch.num; i++) { - trace_t &tr = pm.touch.traces[i]; + trace_t& tr = pm.touch.traces[i]; other = tr.ent; if (other->touch) @@ -4268,9 +4566,11 @@ void ClientThink(gentity_t *ent, usercmd_t *ucmd) { if (client->follow_target) { FreeFollower(ent); - } else + } + else GetFollowTarget(ent); - } else if (!ent->client->weapon_thunk) { + } + else if (!ent->client->weapon_thunk) { // we can only do this during a ready state and // if enough time has passed from last fire if (ent->client->weaponstate == WEAPON_READY && !IsCombatDisabled()) { @@ -4294,7 +4594,8 @@ void ClientThink(gentity_t *ent, usercmd_t *ucmd) { else GetFollowTarget(ent); } - } else + } + else client->ps.pmove.pm_flags &= ~PMF_JUMP_HELD; } } @@ -4310,7 +4611,7 @@ void ClientThink(gentity_t *ent, usercmd_t *ucmd) { // active monsters struct active_monsters_filter_t { - inline bool operator()(gentity_t *ent) const { + inline bool operator()(gentity_t* ent) const { return (ent->inuse && (ent->svflags & SVF_MONSTER) && ent->health > 0); } }; @@ -4319,7 +4620,7 @@ inline entity_iterable_t active_monsters() { return entity_iterable_t { game.maxclients + (uint32_t)BODY_QUEUE_SIZE + 1U }; } -static inline bool G_MonstersSearchingFor(gentity_t *player) { +static inline bool G_MonstersSearchingFor(gentity_t* player) { for (auto ent : active_monsters()) { // check for *any* player target if (player == nullptr && ent->enemy && !ent->enemy->client) @@ -4342,7 +4643,7 @@ static inline bool G_MonstersSearchingFor(gentity_t *player) { // [Paril-KEX] from the given player, find a good spot to // spawn a player -static inline bool G_FindRespawnSpot(gentity_t *player, vec3_t &spot) { +static inline bool G_FindRespawnSpot(gentity_t* player, vec3_t& spot) { // sanity check; make sure there's enough room for ourselves. // (crouching in a small area, etc) trace_t tr = gi.trace(player->s.origin, PLAYER_MINS, PLAYER_MAXS, player->s.origin, player, MASK_PLAYERSOLID); @@ -4359,7 +4660,7 @@ static inline bool G_FindRespawnSpot(gentity_t *player, vec3_t &spot) { // we don't want to spawn inside of these contents_t mask = MASK_PLAYERSOLID | CONTENTS_LAVA | CONTENTS_SLIME; - for (auto &yaw : yaw_spread) { + for (auto& yaw : yaw_spread) { vec3_t angles = { 0, (player->s.angles[YAW] + 180) + yaw, 0 }; // throw the box three times: @@ -4436,7 +4737,7 @@ static inline bool G_FindRespawnSpot(gentity_t *player, vec3_t &spot) { // [Paril-KEX] check each player to find a good // respawn target & position -inline std::tuple G_FindSquadRespawnTarget() { +inline std::tuple G_FindSquadRespawnTarget() { bool monsters_searching_for_anybody = G_MonstersSearchingFor(nullptr); for (auto player : active_clients()) { @@ -4502,22 +4803,23 @@ enum respawn_state_t { // [Paril-KEX] return false to fall back to click-to-respawn behavior. // note that this is only called if they are allowed to respawn (not // restarting the level due to all being dead) -static bool G_CoopRespawn(gentity_t *ent) { +static bool G_CoopRespawn(gentity_t* ent) { // don't do this in non-coop if (!InCoopStyle()) return false; + + const bool limited_lives = g_coop_enable_lives->integer || Horde_LivesEnabled(); // if we don't have squad or lives, it doesn't matter - if (!g_coop_squad_respawn->integer && !g_coop_enable_lives->integer) + if (!g_coop_squad_respawn->integer && !limited_lives) return false; respawn_state_t state = RESPAWN_NONE; // first pass: if we have no lives left, just move to spectator - if (g_coop_enable_lives->integer) { - if (ent->client->pers.lives == 0) { - state = RESPAWN_SPECTATE; - ent->client->coop_respawn_state = COOP_RESPAWN_NO_LIVES; - } + if (limited_lives && ent->client->pers.lives == 0) { + state = RESPAWN_SPECTATE; + ent->client->coop_respawn_state = COOP_RESPAWN_NO_LIVES; + ClientSetEliminated(ent); } // second pass: check for where to spawn @@ -4548,11 +4850,13 @@ static bool G_CoopRespawn(gentity_t *ent) { squad_respawn_angles[2] = 0; use_squad_respawn = true; - } else { + } + else { state = RESPAWN_SPECTATE; } } - } else + } + else state = RESPAWN_START; } @@ -4567,7 +4871,8 @@ static bool G_CoopRespawn(gentity_t *ent) { ent->client->latched_buttons = BUTTON_NONE; use_squad_respawn = false; - } else if (state == RESPAWN_SPECTATE) { + } + else if (state == RESPAWN_SPECTATE) { if (!ent->client->coop_respawn_state) ent->client->coop_respawn_state = COOP_RESPAWN_WAITING; @@ -4593,8 +4898,8 @@ This will be called once for each server frame, before running any other entities in the world. ============== */ -void ClientBeginServerFrame(gentity_t *ent) { - gclient_t *client; +void ClientBeginServerFrame(gentity_t* ent) { + gclient_t* client; int buttonMask; if (gi.ServerFrame() != ent->client->step_frame) @@ -4628,7 +4933,8 @@ void ClientBeginServerFrame(gentity_t *ent) { ClientRespawn(ent); client->latched_buttons = BUTTON_NONE; } - } else if (level.time > client->respawn_time && !level.coop_level_restart_time) { + } + else if (level.time > client->respawn_time && !level.coop_level_restart_time) { // don't respawn if level is waiting to restart // check for coop handling if (!G_CoopRespawn(ent)) { @@ -4662,8 +4968,8 @@ This is called to clean up the pain daemons that the disruptor attaches to clients to damage them. ============== */ -void RemoveAttackingPainDaemons(gentity_t *self) { - gentity_t *tracker; +void RemoveAttackingPainDaemons(gentity_t* self) { + gentity_t* tracker; tracker = G_FindByString<&gentity_t::classname>(nullptr, "pain daemon"); while (tracker) { diff --git a/src/p_hud.cpp b/src/p_hud.cpp index f85e64e..e0c9fbd 100644 --- a/src/p_hud.cpp +++ b/src/p_hud.cpp @@ -2,6 +2,7 @@ // Licensed under the GNU General Public License 2.0. #include "g_local.h" #include "g_statusbar.h" +#include "p_hud_victor.h" /* ====================================================================== @@ -11,21 +12,55 @@ INTERMISSION ====================================================================== */ +/* +============= +EndMatchVictorString + +Determines the intermission victor string for the current match and copies it into the level buffer. +============= +*/ static const char *EndMatchVictorString() { if (!level.intermission_time) return nullptr; - const char *s = nullptr; + intermission_victor_context_t context{}; + context.intermission_active = true; + context.existing_message = level.intermission_victor_msg[0] ? level.intermission_victor_msg : nullptr; - if (Teams() && !(GT(GT_RR))) { - - return s; + context.teams = Teams() && !(GT(GT_RR)); + + if (context.teams) { + context.red_score = level.team_scores[TEAM_RED]; + context.blue_score = level.team_scores[TEAM_BLUE]; + context.red_name = Teams_TeamName(TEAM_RED); + context.blue_name = Teams_TeamName(TEAM_BLUE); + } else { + if (level.sorted_clients[0] >= 0) { + gclient_t *leader = &game.clients[level.sorted_clients[0]]; + context.ffa_winner_name = leader->resp.netname; + context.ffa_winner_score = leader->resp.score; + } + + if (level.sorted_clients[1] >= 0) { + gclient_t *runner = &game.clients[level.sorted_clients[1]]; + context.ffa_runner_up_present = true; + context.ffa_runner_up_score = runner->resp.score; + } } + const char *victor = BuildIntermissionVictorString(context, level.intermission_victor_msg, sizeof(level.intermission_victor_msg)); + return victor ? victor : nullptr; } void MultiplayerScoreboard(gentity_t *ent); +/* +============= +MoveClientToIntermission + +Move a client into the intermission state and set HUD visibility. +============= +*/ void MoveClientToIntermission(gentity_t *ent) { // [Paril-KEX] if (ent->client->ps.pmove.pm_type != PM_FREEZE) @@ -57,7 +92,9 @@ void MoveClientToIntermission(gentity_t *ent) { ent->client->grenade_time = 0_ms; ent->client->showhelp = false; - ent->client->showscores = false; + + if (!deathmatch->integer) + ent->client->showscores = false; globals.server_flags &= ~SERVER_FLAG_SLOW_TIME; @@ -322,6 +359,8 @@ void TeamsScoreboardMessage(gentity_t *ent, gentity_t *killer) { fmt::format_to(std::back_inserter(string), FMT_STRING("xv 0 yv -30 cstring2 \"Score Limit: {}\" "), GT_ScoreLimit()); if (level.intermission_time) { + EndMatchVictorString(); + //fmt::format_to(std::back_inserter(string), FMT_STRING("xv 0 yv -50 cstring2 \"{} - {}\" "), level.gamemod_name, level.gametype_name); //fmt::format_to(std::back_inserter(string), FMT_STRING("xv 0 yv -40 cstring2 \"[{}] {}\" "), level.mapname, level.level_name); if (level.match_start_time) { @@ -345,7 +384,7 @@ void TeamsScoreboardMessage(gentity_t *ent, gentity_t *killer) { */ if (timelimit->value && !level.intermission_time) { //fmt::format_to(std::back_inserter(string), FMT_STRING("xv 340 yv -10 time_limit {} "), gi.ServerFrame() + ((gtime_t::from_min(timelimit->value) - level.time)).milliseconds() / gi.frame_time_ms); -#if 0 + #if 0 //fmt::format_to(std::back_inserter(string), FMT_STRING("xv 340 yv -10 loc_string2 1 {} "), gi.ServerFrame() + level.time.milliseconds() / gi.frame_time_ms); int32_t val = gi.ServerFrame() + ((gtime_t::from_min(timelimit->value) - level.time)).milliseconds() / gi.frame_time_ms; const char *s; @@ -354,7 +393,7 @@ void TeamsScoreboardMessage(gentity_t *ent, gentity_t *killer) { s = G_Fmt("{:02}:{:02}", (remaining_ms / 1000) / 60, (remaining_ms / 1000) % 60).data(); fmt::format_to(std::back_inserter(string), FMT_STRING("xv 340 yv -10 loc_string2 1 \"{}\" "), s); -#endif + #endif } fmt::format_to(std::back_inserter(string), FMT_STRING("xv 0 yb -48 cstring2 \"{}\" "), "Use inventory bind to toggle menu."); @@ -504,10 +543,13 @@ void TeamsScoreboardMessage(gentity_t *ent, gentity_t *killer) { if (total[0] - last[0] > 1) // couldn't fit everyone fmt::format_to(std::back_inserter(string), FMT_STRING("xv -32 yv {} loc_string 1 $g_ctf_and_more {} "), - 42 + (last[0] + 1) * 8, total[0] - last[0] - 1); + 42 + (last[0] + 1) * 8, total[0] - last[0] - 1); if (total[1] - last[1] > 1) // couldn't fit everyone fmt::format_to(std::back_inserter(string), FMT_STRING("xv 208 yv {} loc_string 1 $g_ctf_and_more {} "), - 42 + (last[1] + 1) * 8, total[1] - last[1] - 1); + 42 + (last[1] + 1) * 8, total[1] - last[1] - 1); + + fmt::format_to(std::back_inserter(string), FMT_STRING("xv 0 yb -24 cstring2 \"{}\" "), "www.darkmatter-quake.com"); + fmt::format_to(std::back_inserter(string), FMT_STRING("xv 0 yb -12 cstring2 \"{}\" "), "community | tournaments | content | news"); gi.WriteByte(svc_layout); gi.WriteString(string.c_str()); @@ -531,6 +573,8 @@ static void DuelScoreboardMessage(gentity_t *ent, gentity_t *killer) { fmt::format_to(std::back_inserter(string), FMT_STRING("xv 0 yv -30 cstring2 \"Score Limit: {}\" "), GT_ScoreLimit()); if (level.intermission_time) { + EndMatchVictorString(); + if (level.match_start_time) { int t = (level.intermission_time - level.match_start_time - 1_sec).milliseconds(); fmt::format_to(std::back_inserter(string), FMT_STRING("xv 0 yv -50 cstring2 \"Total Match Time: {}\" "), G_TimeStringMs(t, false)); @@ -722,6 +766,9 @@ static void DuelScoreboardMessage(gentity_t *ent, gentity_t *killer) { fmt::format_to(std::back_inserter(string), FMT_STRING("ifgef {} yb -48 xv 0 loc_cstring2 0 \"$m_eou_press_button\" endif "), (level.intermission_server_frame + (5_sec).frames())); else fmt::format_to(std::back_inserter(string), FMT_STRING("xv 0 yb -48 cstring2 \"{}\" "), "Show inventory to toggle menu."); + + fmt::format_to(std::back_inserter(string), FMT_STRING("xv 0 yb -24 cstring2 \"{}\" "), "www.darkmatter-quake.com"); + fmt::format_to(std::back_inserter(string), FMT_STRING("xv 0 yb -12 cstring2 \"{}\" "), "community | tournaments | content | news"); gi.WriteByte(svc_layout); gi.WriteString(string.c_str()); @@ -732,6 +779,8 @@ static inline void ScoreboardNotice(gentity_t *ent, std::string string) { fmt::format_to(std::back_inserter(string), FMT_STRING("xv 0 yv -30 cstring2 \"Score Limit: {}\" "), GT_ScoreLimit()); if (level.intermission_time) { + EndMatchVictorString(); + //fmt::format_to(std::back_inserter(string), FMT_STRING("xv 0 yv -50 cstring2 \"{} - {}\" "), level.gamemod_name, level.gametype_name); //fmt::format_to(std::back_inserter(string), FMT_STRING("xv 0 yv -40 cstring2 \"[{}] {}\" "), level.mapname, level.level_name); if (level.match_start_time) { @@ -752,11 +801,11 @@ static inline void ScoreboardNotice(gentity_t *ent, std::string string) { /* else if (GT(GT_HORDE) && level.round_number > 0) fmt::format_to(std::back_inserter(string), FMT_STRING("xv -20 yv -10 loc_string2 1 Wave: \"{}\" "), level.round_number); - */ + */ if (timelimit->value && !level.intermission_time) { //fmt::format_to(std::back_inserter(string), FMT_STRING("xv 340 yv -10 time_limit {} "), gi.ServerFrame() + ((gtime_t::from_min(timelimit->value) - level.time)).milliseconds() / gi.frame_time_ms); -#if 0 - //fmt::format_to(std::back_inserter(string), FMT_STRING("xv 340 yv -10 loc_string2 1 {} "), gi.ServerFrame() + level.time.milliseconds() / gi.frame_time_ms); + #if 0 + //fmt::format_to(std::back_inserter(string), FMT_STRING("xv 340 yv -10 loc_string2 1 {} "), gi.ServerFrame() + level.time.milliseconds() / gi.frame_time_ms); int32_t val = gi.ServerFrame() + ((gtime_t::from_min(timelimit->value) - level.time)).milliseconds() / gi.frame_time_ms; const char *s; int32_t remaining_ms = gtime_t::from_ms(level.time); // (val - gi.ServerFrame()) *gi.frame_time_ms; @@ -764,13 +813,14 @@ static inline void ScoreboardNotice(gentity_t *ent, std::string string) { s = G_Fmt("{:02}:{:02}", (remaining_ms / 1000) / 60, (remaining_ms / 1000) % 60).data(); fmt::format_to(std::back_inserter(string), FMT_STRING("xv 340 yv -10 loc_string2 1 \"{}\" "), s); -#endif + #endif } fmt::format_to(std::back_inserter(string), FMT_STRING("xv 0 yb -48 cstring2 \"{}\" "), "Use inventory bind to toggle menu."); } } + /* ================== DeathmatchScoreboardMessage @@ -851,6 +901,8 @@ void DeathmatchScoreboardMessage(gentity_t *ent, gentity_t *killer) { fmt::format_to(std::back_inserter(string), FMT_STRING("xv 0 yv -30 cstring2 \"Score Limit: {}\" "), GT_ScoreLimit()); if (level.intermission_time) { + EndMatchVictorString(); + //fmt::format_to(std::back_inserter(string), FMT_STRING("xv 0 yv -50 cstring2 \"{} - {}\" "), level.gamemod_name, level.gametype_name); //fmt::format_to(std::back_inserter(string), FMT_STRING("xv 0 yv -40 cstring2 \"[{}] {}\" "), level.mapname, level.level_name); if (level.match_start_time) { @@ -875,7 +927,7 @@ void DeathmatchScoreboardMessage(gentity_t *ent, gentity_t *killer) { */ if (timelimit->value && !level.intermission_time) { //fmt::format_to(std::back_inserter(string), FMT_STRING("xv 340 yv -10 time_limit {} "), gi.ServerFrame() + ((gtime_t::from_min(timelimit->value) - level.time)).milliseconds() / gi.frame_time_ms); -#if 0 + #if 0 //fmt::format_to(std::back_inserter(string), FMT_STRING("xv 340 yv -10 loc_string2 1 {} "), gi.ServerFrame() + level.time.milliseconds() / gi.frame_time_ms); int32_t val = gi.ServerFrame() + ((gtime_t::from_min(timelimit->value) - level.time)).milliseconds() / gi.frame_time_ms; const char *s; @@ -884,12 +936,15 @@ void DeathmatchScoreboardMessage(gentity_t *ent, gentity_t *killer) { s = G_Fmt("{:02}:{:02}", (remaining_ms / 1000) / 60, (remaining_ms / 1000) % 60).data(); fmt::format_to(std::back_inserter(string), FMT_STRING("xv 340 yv -10 loc_string2 1 \"{}\" "), s); -#endif + #endif } fmt::format_to(std::back_inserter(string), FMT_STRING("xv 0 yb -48 cstring2 \"{}\" "), "Use inventory bind to toggle menu."); } + fmt::format_to(std::back_inserter(string), FMT_STRING("xv 0 yb -24 cstring2 \"{}\" "), "www.darkmatter-quake.com"); + fmt::format_to(std::back_inserter(string), FMT_STRING("xv 0 yb -12 cstring2 \"{}\" "), "community | tournaments | content | news"); + gi.WriteByte(svc_layout); gi.WriteString(string.c_str()); } @@ -1070,7 +1125,7 @@ void Cmd_Help_f(gentity_t *ent) { // even if we're spectating void G_SetCoopStats(gentity_t *ent) { - if (InCoopStyle() && g_coop_enable_lives->integer) + if (InCoopStyle() && (g_coop_enable_lives->integer || Horde_LivesEnabled())) ent->client->ps.stats[STAT_LIVES] = ent->client->pers.lives + 1; else ent->client->ps.stats[STAT_LIVES] = 0; diff --git a/src/p_hud_victor.cpp b/src/p_hud_victor.cpp new file mode 100644 index 0000000..707c143 --- /dev/null +++ b/src/p_hud_victor.cpp @@ -0,0 +1,47 @@ +// Copyright (c) ZeniMax Media Inc. +// Licensed under the GNU General Public License 2.0. + +#include "p_hud_victor.h" + +#include + +/* +============= +BuildIntermissionVictorString + +Populates the provided buffer with the appropriate victor string for the current match context. +Returns either the buffer pointer or nullptr if no message is generated. +============= +*/ +const char *BuildIntermissionVictorString(const intermission_victor_context_t &context, char *buffer, size_t buffer_size) { + if (!buffer || !buffer_size) + return nullptr; + + buffer[0] = '\0'; + + if (!context.intermission_active) + return nullptr; + + if (context.existing_message && context.existing_message[0]) { + std::snprintf(buffer, buffer_size, "%s", context.existing_message); + return buffer; + } + + if (context.teams) { + if (context.red_score > context.blue_score && context.red_name) { + std::snprintf(buffer, buffer_size, "%s WINS with a final score of %d to %d.", context.red_name, context.red_score, context.blue_score); + } else if (context.blue_score > context.red_score && context.blue_name) { + std::snprintf(buffer, buffer_size, "%s WINS with a final score of %d to %d.", context.blue_name, context.blue_score, context.red_score); + } else if (context.red_name && context.blue_name) { + std::snprintf(buffer, buffer_size, "Match is a tie: %d to %d.", context.red_score, context.blue_score); + } + } else if (context.ffa_winner_name) { + if (context.ffa_runner_up_present && context.ffa_runner_up_score == context.ffa_winner_score) { + std::snprintf(buffer, buffer_size, "Match ended in a tie at %d.", context.ffa_winner_score); + } else { + std::snprintf(buffer, buffer_size, "%s WINS with a final score of %d.", context.ffa_winner_name, context.ffa_winner_score); + } + } + + return buffer[0] ? buffer : nullptr; +} diff --git a/src/p_hud_victor.h b/src/p_hud_victor.h new file mode 100644 index 0000000..b510a9d --- /dev/null +++ b/src/p_hud_victor.h @@ -0,0 +1,30 @@ +// Copyright (c) ZeniMax Media Inc. +// Licensed under the GNU General Public License 2.0. + +#pragma once + +#include + +struct intermission_victor_context_t { + bool intermission_active{}; + const char *existing_message{}; + bool teams{}; + int red_score{}; + int blue_score{}; + const char *red_name{}; + const char *blue_name{}; + const char *ffa_winner_name{}; + int ffa_winner_score{}; + bool ffa_runner_up_present{}; + int ffa_runner_up_score{}; +}; + +/* +============= +BuildIntermissionVictorString + +Populates the provided buffer with the appropriate victor string for the current match context. +Returns either the buffer pointer or nullptr if no message is generated. +============= +*/ +const char *BuildIntermissionVictorString(const intermission_victor_context_t &context, char *buffer, size_t buffer_size); diff --git a/src/p_hud_victor_tests.cpp b/src/p_hud_victor_tests.cpp new file mode 100644 index 0000000..f4585b6 --- /dev/null +++ b/src/p_hud_victor_tests.cpp @@ -0,0 +1,83 @@ +// Copyright (c) ZeniMax Media Inc. +// Licensed under the GNU General Public License 2.0. + +#include "p_hud_victor.h" + +#include +#include + +/* +============= +TestTeamVictor + +Verifies that team victories report the correct winning name and score spread. +============= +*/ +static void TestTeamVictor() { + intermission_victor_context_t context{}; + context.intermission_active = true; + context.teams = true; + context.red_name = "Red"; + context.blue_name = "Blue"; + context.red_score = 25; + context.blue_score = 10; + + char buffer[64]; + const char *result = BuildIntermissionVictorString(context, buffer, sizeof(buffer)); + assert(result); + assert(std::strcmp(result, "Red WINS with a final score of 25 to 10.") == 0); +} + +/* +============= +TestFFAVictor + +Ensures FFA results prefer the top scorer when there is no tie. +============= +*/ +static void TestFFAVictor() { + intermission_victor_context_t context{}; + context.intermission_active = true; + context.ffa_winner_name = "PlayerOne"; + context.ffa_winner_score = 15; + context.ffa_runner_up_present = true; + context.ffa_runner_up_score = 10; + + char buffer[64]; + const char *result = BuildIntermissionVictorString(context, buffer, sizeof(buffer)); + assert(result); + assert(std::strcmp(result, "PlayerOne WINS with a final score of 15.") == 0); +} + +/* +============= +TestFFATie + +Confirms that ties in FFA emit a tie-specific victor string. +============= +*/ +static void TestFFATie() { + intermission_victor_context_t context{}; + context.intermission_active = true; + context.ffa_winner_name = "PlayerOne"; + context.ffa_winner_score = 12; + context.ffa_runner_up_present = true; + context.ffa_runner_up_score = 12; + + char buffer[64]; + const char *result = BuildIntermissionVictorString(context, buffer, sizeof(buffer)); + assert(result); + assert(std::strcmp(result, "Match ended in a tie at 12.") == 0); +} + +/* +============= +main +============= +*/ +int main() { + TestTeamVictor(); + TestFFAVictor(); + TestFFATie(); + return 0; +} diff --git a/src/p_menu.cpp b/src/p_menu.cpp index 2ea2359..1500d10 100644 --- a/src/p_menu.cpp +++ b/src/p_menu.cpp @@ -1,6 +1,7 @@ // Copyright (c) ZeniMax Media Inc. // Licensed under the GNU General Public License 2.0. #include "g_local.h" +#include /* ============ @@ -15,11 +16,15 @@ void P_Menu_Dirty() { } } -// Note that the pmenu entries are duplicated -// this is so that a static set of pmenu entries can be used -// for multiple clients and changed without interference -// note that arg will be freed when the menu is closed, it must be allocated memory -menu_hnd_t *P_Menu_Open(gentity_t *ent, const menu_t *entries, int cur, int num, void *arg, UpdateFunc_t UpdateFunc) { +/* +============= +P_Menu_Open + +Open a menu for a client and duplicate entry data for safe modification. The +owns_arg flag controls whether the menu should free arg on close. +============= +*/ +menu_hnd_t *P_Menu_Open(gentity_t *ent, const menu_t *entries, int cur, int num, void *arg, bool owns_arg, UpdateFunc_t UpdateFunc) { menu_hnd_t *hnd; const menu_t *p; size_t i; @@ -37,11 +42,13 @@ menu_hnd_t *P_Menu_Open(gentity_t *ent, const menu_t *entries, int cur, int num, hnd->UpdateFunc = UpdateFunc; hnd->arg = arg; + hnd->owns_arg = owns_arg; hnd->entries = (menu_t *)gi.TagMalloc(sizeof(menu_t) * num, TAG_LEVEL); memcpy(hnd->entries, entries, sizeof(menu_t) * num); // duplicate the strings since they may be from static memory - for (i = 0; i < num; i++) - Q_strlcpy(hnd->entries[i].text, entries[i].text, sizeof(entries[i].text)); + for (i = 0; i < num; i++) { + assert(Q_strlcpy(hnd->entries[i].text, entries[i].text, sizeof(hnd->entries[i].text)) < sizeof(hnd->entries[i].text)); + } hnd->num = num; @@ -71,40 +78,58 @@ menu_hnd_t *P_Menu_Open(gentity_t *ent, const menu_t *entries, int cur, int num, return hnd; } +/* +============= +P_Menu_Close + +Closes the active menu for the entity and frees associated resources. +============= +*/ void P_Menu_Close(gentity_t *ent) { menu_hnd_t *hnd; + if (!ent->client) + return; + if (!ent->client->menu) return; hnd = ent->client->menu; gi.TagFree(hnd->entries); - if (hnd->arg) + if (hnd->owns_arg && hnd->arg) gi.TagFree(hnd->arg); gi.TagFree(hnd); ent->client->menu = nullptr; ent->client->showscores = false; - + gentity_t *e = ent->client->follow_target ? ent->client->follow_target : ent; ent->client->ps.stats[STAT_SHOW_STATUSBAR] = !ClientIsPlaying(e->client) ? 0 : 1; } -// only use on pmenu's that have been called with P_Menu_Open + +/* +============= +P_Menu_UpdateEntry + +Replaces the text and callbacks for a menu entry created by P_Menu_Open. +============= +*/ void P_Menu_UpdateEntry(menu_t *entry, const char *text, int align, SelectFunc_t SelectFunc) { Q_strlcpy(entry->text, text, sizeof(entry->text)); entry->align = align; entry->SelectFunc = SelectFunc; } -#include "g_statusbar.h" +/* +============= +P_Menu_Do_Update +Regenerates the menu status bar layout for the client using bounded string +operations. +============= +*/ void P_Menu_Do_Update(gentity_t *ent) { - int i; - menu_t *p; - int x; - menu_hnd_t *hnd; - const char *t; - bool alt = false; + menu_hnd_t *hnd; if (!ent->client->menu) { gi.Com_Print("Warning: ent has no menu\n"); @@ -116,55 +141,22 @@ void P_Menu_Do_Update(gentity_t *ent) { if (hnd->UpdateFunc) hnd->UpdateFunc(ent); - statusbar_t sb; - - sb.xv(32).yv(8).picn("inventory"); - - for (i = 0, p = hnd->entries; i < hnd->num; i++, p++) { - if (!*(p->text)) - continue; // blank line - - t = p->text; - - if (*t == '*') { - alt = true; - t++; - } - - sb.yv(32 + i * 8); - - const char *loc_func = "loc_string"; - - if (p->align == MENU_ALIGN_CENTER) { - x = 0; - loc_func = "loc_cstring"; - } else if (p->align == MENU_ALIGN_RIGHT) { - x = 260; - loc_func = "loc_rstring"; - } else - x = 64; - - sb.xv(x); + char layout[MAX_STRING_CHARS] = {}; - sb.sb << loc_func; - - if (hnd->cur == i || alt) - sb.sb << '2'; - - sb.sb << " 1 \"" << t << "\" \"" << p->text_arg1 << "\" "; - - if (hnd->cur == i) { - sb.xv(56); - sb.string2("\">\""); - } - - alt = false; - } + P_Menu_BuildStatusBar(hnd, layout, sizeof(layout)); gi.WriteByte(svc_layout); - gi.WriteString(sb.sb.str().c_str()); + gi.WriteString(layout); } + +/* +============= +P_Menu_Update + +Performs periodic menu updates when the client is viewing a menu. +============= +*/ void P_Menu_Update(gentity_t *ent) { if (!ent->client->menu) { gi.Com_Print("Warning: ent has no menu\n"); @@ -184,6 +176,13 @@ void P_Menu_Update(gentity_t *ent) { gi.local_sound(ent, CHAN_AUTO, gi.soundindex("misc/menu2.wav"), 1, ATTN_NONE, 0); } +/* +============= +P_Menu_Next + +Advances the menu cursor to the next selectable entry. +============= +*/ void P_Menu_Next(gentity_t *ent) { menu_hnd_t *hnd; int i; @@ -217,6 +216,13 @@ void P_Menu_Next(gentity_t *ent) { P_Menu_Update(ent); } +/* +============= +P_Menu_Prev + +Moves the menu cursor to the previous selectable entry. +============= +*/ void P_Menu_Prev(gentity_t *ent) { menu_hnd_t *hnd; int i; @@ -251,6 +257,13 @@ void P_Menu_Prev(gentity_t *ent) { P_Menu_Update(ent); } +/* +============= +P_Menu_Select + +Triggers the select callback for the current menu entry. +============= +*/ void P_Menu_Select(gentity_t *ent) { menu_hnd_t *hnd; menu_t *p; @@ -275,3 +288,71 @@ void P_Menu_Select(gentity_t *ent) { p->SelectFunc(ent, hnd); //gi.local_sound(ent, CHAN_AUTO, gi.soundindex("misc/menu1.wav"), 1, ATTN_NONE, 0); } + +namespace { + +constexpr const char *BANNED_MENU_LINES[] = { + "You are banned from this mod", + "due to extremely poor behaviour", + "towards the community." +}; + +menu_t banned_menu_entries[] = { + { "", MENU_ALIGN_CENTER, nullptr }, + { "", MENU_ALIGN_CENTER, nullptr }, + { "", MENU_ALIGN_CENTER, nullptr }, +}; + +/* +============= +P_Menu_Banned_Update + +No-op update hook for the banned menu. +============= +*/ +void P_Menu_Banned_Update(gentity_t *ent) { + (void)ent; +} + +/* +============= +P_Menu_Banned_InitEntries + +Initializes the static banned menu lines. +============= +*/ +void P_Menu_Banned_InitEntries() { + for (size_t i = 0; i < sizeof(banned_menu_entries) / sizeof(banned_menu_entries[0]); ++i) + Q_strlcpy(banned_menu_entries[i].text, BANNED_MENU_LINES[i], sizeof(banned_menu_entries[i].text)); +} + +} // namespace + +/* +============= +P_Menu_OpenBanned + +Opens the banned notification menu for a client. +============= +*/ +void P_Menu_OpenBanned(gentity_t *ent) { + if (!ent->client) + return; + + P_Menu_Banned_InitEntries(); + P_Menu_Open(ent, banned_menu_entries, -1, sizeof(banned_menu_entries) / sizeof(menu_t), nullptr, false, P_Menu_Banned_Update); +} + +/* +============= +P_Menu_IsBannedMenu + +Returns true when the supplied menu handle is the banned menu. +============= +*/ +bool P_Menu_IsBannedMenu(const menu_hnd_t *hnd) { + if (!hnd) + return false; + + return hnd->UpdateFunc == P_Menu_Banned_Update; +} diff --git a/src/p_menu.h b/src/p_menu.h index 295a074..4b1dfc9 100644 --- a/src/p_menu.h +++ b/src/p_menu.h @@ -1,6 +1,10 @@ // Copyright (c) ZeniMax Media Inc. // Licensed under the GNU General Public License 2.0. +#include + +struct gentity_t; + enum { MENU_ALIGN_LEFT, MENU_ALIGN_CENTER, @@ -16,6 +20,7 @@ struct menu_hnd_t { int cur; int num; void *arg; + bool owns_arg; UpdateFunc_t UpdateFunc; }; @@ -29,11 +34,14 @@ struct menu_t { }; void P_Menu_Dirty(); -menu_hnd_t *P_Menu_Open(gentity_t *ent, const menu_t *entries, int cur, int num, void *arg, UpdateFunc_t UpdateFunc); +menu_hnd_t *P_Menu_Open(gentity_t *ent, const menu_t *entries, int cur, int num, void *arg, bool owns_arg, UpdateFunc_t UpdateFunc); void P_Menu_Close(gentity_t *ent); void P_Menu_UpdateEntry(menu_t *entry, const char *text, int align, SelectFunc_t SelectFunc); +size_t P_Menu_BuildStatusBar(const menu_hnd_t *hnd, char *layout, size_t layout_size); void P_Menu_Do_Update(gentity_t *ent); void P_Menu_Update(gentity_t *ent); void P_Menu_Next(gentity_t *ent); void P_Menu_Prev(gentity_t *ent); void P_Menu_Select(gentity_t *ent); +void P_Menu_OpenBanned(gentity_t *ent); +bool P_Menu_IsBannedMenu(const menu_hnd_t *hnd); diff --git a/src/p_menu_statusbar.cpp b/src/p_menu_statusbar.cpp new file mode 100644 index 0000000..260e12e --- /dev/null +++ b/src/p_menu_statusbar.cpp @@ -0,0 +1,157 @@ +// Copyright (c) ZeniMax Media Inc. +// Licensed under the GNU General Public License 2.0. + +#include "p_menu.h" +#include "q_std.h" + +#include +#include +#include +#include + +#ifdef UNIT_TESTS +/* +============= +Q_strlcpy + +Test-safe implementation of Q_strlcpy used when running unit tests without the +full game import table. +============= +*/ +size_t Q_strlcpy(char *dst, const char *src, size_t siz) +{ + char *d = dst; + const char *s = src; + size_t n = siz; + + if (n != 0 && --n != 0) + do { + if ((*d++ = *s++) == '\0') + break; + } while (--n != 0); + + if (n == 0) { + if (siz != 0) + *d = '\0'; + while (*s++) + ; + } + + return static_cast(s - src - 1); + } +/* +============= +Q_strlcat + +Test-safe implementation of Q_strlcat used when running unit tests without the +full game import table. +============= +*/ +size_t Q_strlcat(char *dst, const char *src, size_t siz) +{ + char *d = dst; + const char *s = src; + size_t n = siz; + size_t dlen; + + while (n-- != 0 && *d != '\0') + d++; + dlen = static_cast(d - dst); + n = siz - dlen; + + if (n == 0) + return dlen + std::strlen(s); + + while (*s != '\0') { + if (n != 1) { + *d++ = *s; + n--; + } s++; +} + + *d = '\0'; + + return dlen + static_cast(s - src); + }#endif + +/* +============= + P_Menu_Appendf + +Appends formatted text to a layout buffer while ensuring the destination is +not overrun. +============= +*/ +static bool P_Menu_Appendf(char *layout, size_t layout_size, const char *fmt, ...) +{ + if (layout_size == 0) + return false; + + std::vector chunk(layout_size, '\0'); + + va_list args; + va_start(args, fmt); + vsnprintf(chunk.data(), chunk.size(), fmt, args); + va_end(args); + + return Q_strlcat(layout, chunk.data(), layout_size) < layout_size; + } +/* +============= +P_Menu_BuildStatusBar + +Constructs the status bar layout string for the active menu using a bounded +buffer to avoid overflow. +============= +*/ +size_t P_Menu_BuildStatusBar(const menu_hnd_t *hnd, char *layout, size_t layout_size) +{ + if (!hnd || !layout || layout_size == 0) + return 0; + + layout[0] = '\0'; + + if (!hnd->entries) + return 0; + + P_Menu_Appendf(layout, layout_size, "xv %d yv %d picn %s ", 32, 8, "inventory"); + + bool alt = false; + + for (int i = 0; i < hnd->num; i++) { + const menu_t *p = hnd->entries + i; + + if (!*(p->text)) + continue; + + const char *t = p->text; + + if (*t == '*') { + alt = true; + t++; + } + const int y = 32 + i * 8; + int x = 64; + int caret_x = 56; + const char *loc_func = "loc_string"; + + if (p->align == MENU_ALIGN_CENTER) { + x = 0; + caret_x = 152; + loc_func = "loc_cstring"; + } else if (p->align == MENU_ALIGN_RIGHT) { + x = 260; + caret_x = 252; + loc_func = "loc_rstring"; + } + P_Menu_Appendf(layout, layout_size, "yv %d ", y); + P_Menu_Appendf(layout, layout_size, "xv %d %s%s 1 \"%s\" \"%s\" ", x, loc_func, (hnd->cur == i || alt) ? "2" : "", t, p->text_arg1); + + if (hnd->cur == i) + P_Menu_Appendf(layout, layout_size, "xv %d string2 \"%s\" ", caret_x, ">\""); + + alt = false; + } + + return std::strlen(layout); +} diff --git a/src/p_move.cpp b/src/p_move.cpp index 76b27bb..2d19407 100644 --- a/src/p_move.cpp +++ b/src/p_move.cpp @@ -3,19 +3,33 @@ #include "q_std.h" +#include +#include + #define GAME_INCLUDE #include "bg_local.h" -// [Paril-KEX] generic code to detect & fix a stuck object -stuck_result_t G_FixStuckObject_Generic(vec3_t &origin, const vec3_t &own_mins, const vec3_t &own_maxs, std::function trace) { +#ifdef G_FIX_STUCK_OBJECT_GENERIC_TESTS +#include +#endif + +/* +============= +G_FixStuckObject_Generic + +Generic code to detect & fix a stuck object. +============= +*/ +stuck_result_t G_FixStuckObject_Generic(vec3_t& origin, const vec3_t& own_mins, const vec3_t& own_maxs, std::function trace) { if (!trace(origin, own_mins, own_maxs, origin).startsolid) return stuck_result_t::GOOD_POSITION; - struct { + struct GoodPosition { float distance; vec3_t origin; - } good_positions[6]; - size_t num_good_positions = 0; + }; + std::vector good_positions; + good_positions.reserve(q_countof(side_checks)); constexpr struct { std::array normal; @@ -30,7 +44,7 @@ stuck_result_t G_FixStuckObject_Generic(vec3_t &origin, const vec3_t &own_mins, }; for (size_t sn = 0; sn < q_countof(side_checks); sn++) { - auto &side = side_checks[sn]; + auto& side = side_checks[sn]; vec3_t start = origin; vec3_t mins{}, maxs{}; @@ -90,7 +104,7 @@ stuck_result_t G_FixStuckObject_Generic(vec3_t &origin, const vec3_t &own_mins, continue; vec3_t opposite_start = origin; - auto &other_side = side_checks[sn ^ 1]; + auto& other_side = side_checks[sn ^ 1]; for (size_t n = 0; n < 3; n++) { if (other_side.normal[n] < 0) @@ -128,15 +142,13 @@ stuck_result_t G_FixStuckObject_Generic(vec3_t &origin, const vec3_t &own_mins, if (tr.startsolid) continue; - good_positions[num_good_positions].origin = new_origin; - good_positions[num_good_positions].distance = delta.lengthSquared(); - num_good_positions++; + good_positions.emplace_back(GoodPosition{ delta.lengthSquared(), new_origin }); } - if (num_good_positions) { - std::sort(&good_positions[0], &good_positions[num_good_positions - 1], [](const auto &a, const auto &b) { return a.distance < b.distance; }); + if (!good_positions.empty()) { + const auto best = std::ranges::min_element(good_positions, [](const auto& a, const auto& b) { return a.distance < b.distance; }); - origin = good_positions[0].origin; + origin = best->origin; return stuck_result_t::FIXED; } @@ -144,6 +156,86 @@ stuck_result_t G_FixStuckObject_Generic(vec3_t &origin, const vec3_t &own_mins, return stuck_result_t::NO_GOOD_POSITION; } +#ifdef G_FIX_STUCK_OBJECT_GENERIC_TESTS +struct GFixStuckTestTraceState { + std::array offsets = { + vec3_t{ 0.0f, 0.0f, 4.0f }, + vec3_t{ 0.0f, 0.0f, 2.0f }, + vec3_t{ 0.0f, 0.0f, 1.0f }, + vec3_t{ 0.0f, 0.0f, 3.0f }, + vec3_t{ 0.0f, 0.0f, 5.0f }, + vec3_t{ 0.0f, 0.0f, 6.0f } +}; + size_t call = 0; +}; + +static GFixStuckTestTraceState g_fix_stuck_trace_state; + +/* +============= +G_FixStuck_TestTrace + +Provides deterministic trace responses for the G_FixStuckObject_Generic test harness. +============= +*/ +static trace_t G_FixStuck_TestTrace(const vec3_t& start, const vec3_t& mins, const vec3_t& maxs, const vec3_t& end) { + trace_t tr{}; + const size_t phase = g_fix_stuck_trace_state.call % 3; + const size_t side = g_fix_stuck_trace_state.call / 3; + + tr.startsolid = false; + tr.endpos = end; + + if (phase == 1 && side < g_fix_stuck_trace_state.offsets.size()) + tr.endpos = end + g_fix_stuck_trace_state.offsets[side]; + + g_fix_stuck_trace_state.call++; + return tr; +} + +/* +============= +Test_GFixStuckObject_SortingAndRecovery + +Verifies that G_FixStuckObject_Generic sorts candidate positions correctly and recovers the nearest option when multiple positions are available. +============= +*/ +static void Test_GFixStuckObject_SortingAndRecovery() { + g_fix_stuck_trace_state.call = 0; + vec3_t origin{ 0.0f, 0.0f, 0.0f }; + const vec3_t mins{ -1.0f, -1.0f, -1.0f }; + const vec3_t maxs{ 1.0f, 1.0f, 1.0f }; + + const stuck_result_t result = G_FixStuckObject_Generic(origin, mins, maxs, G_FixStuck_TestTrace); + + assert(result == stuck_result_t::FIXED); + assert(origin.equals(g_fix_stuck_trace_state.offsets[2])); +} + +/* +============= +Run_GFixStuckObject_Generic_Tests + +Runs the standalone harness for G_FixStuckObject_Generic. +============= +*/ +static void Run_GFixStuckObject_Generic_Tests() { + Test_GFixStuckObject_SortingAndRecovery(); +} + +#ifdef G_FIX_STUCK_OBJECT_GENERIC_TESTS_MAIN +/* +============= +main +============= +*/ +int main() { + Run_GFixStuckObject_Generic_Tests(); + return 0; +} +#endif +#endif + // all of the locals will be zeroed before each // pmove, just to make damn sure we don't have // any differences when running on client or server @@ -155,7 +247,7 @@ struct pml_t { vec3_t forward, right, up; float frametime; - csurface_t *groundsurface; + csurface_t* groundsurface; int groundcontents; vec3_t previous_origin; @@ -164,7 +256,7 @@ struct pml_t { pm_config_t pm_config; -pmove_t *pm; +pmove_t* pm; pml_t pml; // movement parameters @@ -184,7 +276,7 @@ float pm_laddermod = 0.5f; */ -static float MaxSpeed(pmove_state_t *ps) { +static float MaxSpeed(pmove_state_t* ps) { return ps->haste ? pm_maxspeed * 1.3 : pm_maxspeed; } @@ -196,7 +288,7 @@ Slide off of the impacting object returns the blocked flags (1 = floor, 2 = step / wall) ================== */ -static void PM_ClipVelocity(const vec3_t &in, const vec3_t &normal, vec3_t &out, float overbounce) { +static void PM_ClipVelocity(const vec3_t& in, const vec3_t& normal, vec3_t& out, float overbounce) { float backoff; float change; int i; @@ -211,11 +303,11 @@ static void PM_ClipVelocity(const vec3_t &in, const vec3_t &normal, vec3_t &out, } } -static trace_t PM_Clip(const vec3_t &start, const vec3_t &mins, const vec3_t &maxs, const vec3_t &end, contents_t mask) { +static trace_t PM_Clip(const vec3_t& start, const vec3_t& mins, const vec3_t& maxs, const vec3_t& end, contents_t mask) { return pm->clip(start, &mins, &maxs, end, mask); } -static trace_t PM_Trace(const vec3_t &start, const vec3_t &mins, const vec3_t &maxs, const vec3_t &end, contents_t mask = CONTENTS_NONE) { +static trace_t PM_Trace(const vec3_t& start, const vec3_t& mins, const vec3_t& maxs, const vec3_t& end, contents_t mask = CONTENTS_NONE) { if (pm->s.pm_type == PM_SPECTATOR) return PM_Clip(start, mins, maxs, end, MASK_SOLID); @@ -235,7 +327,7 @@ static trace_t PM_Trace(const vec3_t &start, const vec3_t &mins, const vec3_t &m } // only here to satisfy pm_trace_t -static inline trace_t PM_Trace_Auto(const vec3_t &start, const vec3_t &mins, const vec3_t &maxs, const vec3_t &end) { +static inline trace_t PM_Trace_Auto(const vec3_t& start, const vec3_t& mins, const vec3_t& maxs, const vec3_t& end) { return PM_Trace(start, mins, maxs, end); } @@ -253,7 +345,7 @@ Does not modify any world state? constexpr float MIN_STEP_NORMAL = 0.7f; // can't step up onto very steep slopes constexpr size_t MAX_CLIP_PLANES = 5; -static inline void PM_RecordTrace(touch_list_t &touch, trace_t &tr) { +static inline void PM_RecordTrace(touch_list_t& touch, trace_t& tr) { if (touch.num == MAXTOUCH) return; @@ -266,7 +358,7 @@ static inline void PM_RecordTrace(touch_list_t &touch, trace_t &tr) { // [Paril-KEX] made generic so you can run this without // needing a pml/pm -void PM_StepSlideMove_Generic(vec3_t &origin, vec3_t &velocity, float frametime, const vec3_t &mins, const vec3_t &maxs, touch_list_t &touch, bool has_time, pm_trace_t trace_func) { +void PM_StepSlideMove_Generic(vec3_t& origin, vec3_t& velocity, float frametime, const vec3_t& mins, const vec3_t& maxs, touch_list_t& touch, bool has_time, pm_trace_t trace_func) { int bumpcount, numbumps; vec3_t dir; float d; @@ -376,7 +468,8 @@ void PM_StepSlideMove_Generic(vec3_t &origin, vec3_t &velocity, float frametime, } if (i != numplanes) { // go along this plane - } else { // go along the crease + } + else { // go along the crease if (numplanes != 2) { velocity = vec3_origin; break; @@ -426,7 +519,7 @@ static void PM_StepSlideMove() { down_o = pml.origin; down_v = pml.velocity; - + up = start_o; up[2] += (pml.origin[2] < 0) ? STEPSIZE_BELOW : STEPSIZE; @@ -503,7 +596,7 @@ Handles both ground friction and water friction ================== */ static void PM_Friction() { - float *vel; + float* vel; float speed, newspeed, control; float friction; float drop; @@ -549,7 +642,7 @@ PM_Accelerate Handles user intended acceleration ============== */ -static void PM_Accelerate(const vec3_t &wishdir, float wishspeed, float accel) { +static void PM_Accelerate(const vec3_t& wishdir, float wishspeed, float accel) { int i; float addspeed, accelspeed, currentspeed; @@ -565,7 +658,7 @@ static void PM_Accelerate(const vec3_t &wishdir, float wishspeed, float accel) { pml.velocity[i] += accelspeed * wishdir[i]; } -static void PM_AirAccelerate(const vec3_t &wishdir, float wishspeed, float accel) { +static void PM_AirAccelerate(const vec3_t& wishdir, float wishspeed, float accel) { int i; float addspeed, accelspeed, currentspeed, wishspd = wishspeed; @@ -588,7 +681,7 @@ static void PM_AirAccelerate(const vec3_t &wishdir, float wishspeed, float accel PM_AddCurrents ============= */ -static void PM_AddCurrents(vec3_t &wishvel) { +static void PM_AddCurrents(vec3_t& wishvel) { vec3_t v; float s; @@ -605,7 +698,8 @@ static void PM_AddCurrents(vec3_t &wishvel) { wishvel[2] = ladder_speed; else if (pm->cmd.buttons & BUTTON_CROUCH) wishvel[2] = -ladder_speed; - } else if (pm->cmd.forwardmove) { + } + else if (pm->cmd.forwardmove) { // [Paril-KEX] clamp the speed a bit so we're not too fast float ladder_speed = std::clamp(pm->cmd.forwardmove, -200.f, 200.f); @@ -624,7 +718,8 @@ static void PM_AddCurrents(vec3_t &wishvel) { wishvel[2] = ladder_speed; } - } else + } + else wishvel[2] = 0; // limit horizontal speed when on a ladder @@ -655,7 +750,8 @@ static void PM_AddCurrents(vec3_t &wishvel) { wishvel[0] = wishvel[1] = 0; wishvel += (right * -ladder_speed); } - } else { + } + else { if (wishvel[0] < -25) wishvel[0] = -25; else if (wishvel[0] > 25) @@ -743,7 +839,8 @@ static void PM_WaterMove() { !(pm->cmd.buttons & (BUTTON_JUMP | BUTTON_CROUCH))) { if (!pm->groundentity) wishvel[2] -= 60; // drift towards bottom - } else { + } + else { if (pm->cmd.buttons & BUTTON_CROUCH) wishvel[2] -= pm_waterspeed * 0.5f; else if (pm->cmd.buttons & BUTTON_JUMP) @@ -814,14 +911,16 @@ static void PM_AirMove() { pml.velocity[2] -= pm->s.gravity * pml.frametime; if (pml.velocity[2] < 0) pml.velocity[2] = 0; - } else { + } + else { pml.velocity[2] += pm->s.gravity * pml.frametime; if (pml.velocity[2] > 0) pml.velocity[2] = 0; } } PM_StepSlideMove(); - } else if (pm->groundentity) { // walking on ground + } + else if (pm->groundentity) { // walking on ground pml.velocity[2] = 0; //!!! this is before the accel PM_Accelerate(wishdir, wishspeed, pm_accelerate); @@ -833,7 +932,8 @@ static void PM_AirMove() { if (!pml.velocity[0] && !pml.velocity[1]) return; PM_StepSlideMove(); - } else { // not on ground, so little effect on velocity + } + else { // not on ground, so little effect on velocity if (pm_config.airaccel) PM_AirAccelerate(wishdir, wishspeed, pm_config.airaccel); else @@ -847,7 +947,7 @@ static void PM_AirMove() { } } -static inline void PM_GetWaterLevel(const vec3_t &position, water_level_t &level, contents_t &type) { +static inline void PM_GetWaterLevel(const vec3_t& position, water_level_t& level, contents_t& type) { // // get waterlevel, accounting for ducking // @@ -858,19 +958,20 @@ static inline void PM_GetWaterLevel(const vec3_t &position, water_level_t &level int32_t sample1 = sample2 / 2; vec3_t point = position; + float baseZ = position[2]; - point[2] += pm->mins[2] + 1; + point[2] = baseZ + pm->mins[2] + 1; contents_t cont = pm->pointcontents(point); if (cont & MASK_WATER) { type = cont; level = WATER_FEET; - point[2] = pml.origin[2] + pm->mins[2] + sample1; + point[2] = baseZ + pm->mins[2] + sample1; cont = pm->pointcontents(point); if (cont & MASK_WATER) { level = WATER_WAIST; - point[2] = pml.origin[2] + pm->mins[2] + sample2; + point[2] = baseZ + pm->mins[2] + sample2; cont = pm->pointcontents(point); if (cont & MASK_WATER) level = WATER_UNDER; @@ -899,7 +1000,8 @@ static void PM_CatagorizePosition() { { pm->s.pm_flags &= ~PMF_ON_GROUND; pm->groundentity = nullptr; - } else { + } + else { trace = PM_Trace(pml.origin, pm->mins, pm->maxs, point); pm->groundplane = trace.plane; pml.groundsurface = trace.surface; @@ -921,7 +1023,8 @@ static void PM_CatagorizePosition() { if (trace.fraction == 1.0f || (slanted_ground && !trace.startsolid)) { pm->groundentity = nullptr; pm->s.pm_flags &= ~PMF_ON_GROUND; - } else { + } + else { pm->groundentity = trace.ent; // hitting solid ground will end a waterjump @@ -1131,7 +1234,8 @@ static void PM_FlyMove(bool doclip) { speed = pml.velocity.length(); if (speed < 1) { pml.velocity = vec3_origin; - } else { + } + else { drop = 0; friction = pm_friction * 1.5f; // extra friction @@ -1197,7 +1301,8 @@ static void PM_FlyMove(bool doclip) { pml.origin = trace.endpos;*/ PM_StepSlideMove(); - } else { + } + else { // move pml.origin += (pml.velocity * pml.frametime); } @@ -1222,7 +1327,8 @@ static void PM_SetDimensions() { if ((pm->s.pm_flags & PMF_DUCKED) || pm->s.pm_type == PM_DEAD) { pm->maxs[2] = 4; pm->s.viewheight = -2; - } else { + } + else { pm->maxs[2] = 32; pm->s.viewheight = 22; } @@ -1263,7 +1369,8 @@ static bool PM_CheckDuck() { pm->s.pm_flags |= PMF_DUCKED; flags_changed = true; } - } else if ( + } + else if ( (pm->cmd.buttons & BUTTON_CROUCH) && (pm->groundentity || (pm->waterlevel <= WATER_FEET && !PM_AboveWater())) && !(pm->s.pm_flags & PMF_ON_LADDER) && @@ -1277,7 +1384,8 @@ static bool PM_CheckDuck() { flags_changed = true; } } - } else { // stand up if possible + } + else { // stand up if possible if (pm->s.pm_flags & PMF_DUCKED) { // try to stand up vec3_t check_maxs = { pm->maxs[0], pm->maxs[1], 32 }; @@ -1313,7 +1421,8 @@ static void PM_DeadMove() { forward -= 20; if (forward <= 0) { pml.velocity = {}; - } else { + } + else { pml.velocity.normalize(); pml.velocity *= forward; } @@ -1389,7 +1498,8 @@ static void PM_ClampAngles() { pm->viewangles[YAW] = pm->cmd.angles[YAW] + pm->s.delta_angles[YAW]; pm->viewangles[PITCH] = 0; pm->viewangles[ROLL] = 0; - } else { + } + else { // circularly clamp the angles with deltas pm->viewangles = pm->cmd.angles + pm->s.delta_angles; @@ -1428,7 +1538,7 @@ Pmove Can be called by either the server or the client ================ */ -void Pmove(pmove_t *pmove) { +void Pmove(pmove_t* pmove) { pm = pmove; // clear results @@ -1509,12 +1619,14 @@ void Pmove(pmove_t *pmove) { if (pm->cmd.msec >= pm->s.pm_time) { pm->s.pm_flags &= ~(PMF_TIME_WATERJUMP | PMF_TIME_LAND | PMF_TIME_TELEPORT | PMF_TIME_TRICK); pm->s.pm_time = 0; - } else + } + else pm->s.pm_time -= pm->cmd.msec; } if (pm->s.pm_flags & PMF_TIME_TELEPORT) { // teleport pause stays exactly in place - } else if (pm->s.pm_flags & PMF_TIME_WATERJUMP) { // waterjump has no control, but falls + } + else if (pm->s.pm_flags & PMF_TIME_WATERJUMP) { // waterjump has no control, but falls pml.velocity[2] -= pm->s.gravity * pml.frametime; if (pml.velocity[2] < 0) { // cancel as soon as we are falling down again pm->s.pm_flags &= ~(PMF_TIME_WATERJUMP | PMF_TIME_LAND | PMF_TIME_TELEPORT | PMF_TIME_TRICK); @@ -1522,7 +1634,8 @@ void Pmove(pmove_t *pmove) { } PM_StepSlideMove(); - } else { + } + else { PM_CheckJump(); PM_Friction(); diff --git a/src/p_view.cpp b/src/p_view.cpp index b5ad66e..992e5af 100644 --- a/src/p_view.cpp +++ b/src/p_view.cpp @@ -106,15 +106,15 @@ void P_DamageFeedback(gentity_t *player) { // start a pain animation if still in the player model if (client->anim_priority < ANIM_PAIN && player->s.modelindex == MODELINDEX_PLAYER) { - static int i; + int &pain_anim_index = client->pain_anim_index; client->anim_priority = ANIM_PAIN; if (client->ps.pmove.pm_flags & PMF_DUCKED) { player->s.frame = FRAME_crpain1 - 1; client->anim_end = FRAME_crpain4; } else { - i = (i + 1) % 3; - switch (i) { + pain_anim_index = (pain_anim_index + 1) % 3; + switch (pain_anim_index) { case 0: player->s.frame = FRAME_pain101 - 1; client->anim_end = FRAME_pain104; @@ -132,7 +132,6 @@ void P_DamageFeedback(gentity_t *player) { client->anim_time = 0_ms; } - realcount = count; // if we took health damage, do a minimum clamp @@ -690,15 +689,24 @@ static void P_WorldEffects() { if (breather || envirosuit) { current_player->air_finished = level.time + 10_sec; - if (((current_client->pu_time_rebreather - level.time).milliseconds() % 2500) == 0) { - if (!current_client->breather_sound) - gi.sound(current_player, CHAN_AUTO, gi.soundindex("player/u_breath1.wav"), 1, ATTN_NORM, 0); - else - gi.sound(current_player, CHAN_AUTO, gi.soundindex("player/u_breath2.wav"), 1, ATTN_NORM, 0); - current_client->breather_sound ^= 1; - PlayerNoise(current_player, current_player->s.origin, PNOISE_SELF); - // FIXME: release a bubble? - } + if (((current_client->pu_time_rebreather - level.time).milliseconds() % 2500) == 0) { + if (!current_client->breather_sound) + gi.sound(current_player, CHAN_AUTO, gi.soundindex("player/u_breath1.wav"), 1, ATTN_NORM, 0); + else + gi.sound(current_player, CHAN_AUTO, gi.soundindex("player/u_breath2.wav"), 1, ATTN_NORM, 0); + current_client->breather_sound ^= 1; + PlayerNoise(current_player, current_player->s.origin, PNOISE_SELF); + + vec3_t breath_origin = current_player->s.origin; + breath_origin[2] += current_player->viewheight; + vec3_t bubble_end = breath_origin + (up * 8); + + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BUBBLETRAIL); + gi.WritePosition(breath_origin); + gi.WritePosition(bubble_end); + gi.multicast(breath_origin, MULTICAST_PVS, false); + } } // if out of air, start drowning @@ -1483,4 +1491,4 @@ void ClientEndServerFrame(gentity_t *ent) { if (!clipped_player) ent->clipmask |= CONTENTS_PLAYER; } -} \ No newline at end of file +} diff --git a/src/p_weapon.cpp b/src/p_weapon.cpp index df069cd..3c046b3 100644 --- a/src/p_weapon.cpp +++ b/src/p_weapon.cpp @@ -2584,7 +2584,7 @@ static void Weapon_PlasmaBeam_Fire(gentity_t *ent) { switch (game.ruleset) { case RS_MM: damage = deathmatch->integer ? 10 : 15; - kick = deathmatch->integer ? 50 : 30; + kick = damage; break; case RS_Q3A: damage = deathmatch->integer ? 8 : 15; diff --git a/src/q_std.cpp b/src/q_std.cpp index 87c959f..a0ac39b 100644 --- a/src/q_std.cpp +++ b/src/q_std.cpp @@ -9,8 +9,18 @@ g_fmt_data_t g_fmt_data; +/* +============= +COM_IsSeparator + +Returns true if the specified character is a separator. +============= +*/ bool COM_IsSeparator(char c, const char *seps) { + if (!seps) + return true; + if (!c) return true; @@ -28,22 +38,43 @@ COM_ParseEx Parse a token out of a string ============== */ -char *COM_ParseEx(const char **data_p, const char *seps, char *buffer, size_t buffer_size) +char *COM_ParseEx(const char **data_p, const char *seps, char *buffer, size_t buffer_size, bool *overflowed) { static char com_token[MAX_TOKEN_CHARS]; + bool overflow_flag = false; + + if (overflowed) + *overflowed = false; + if (!buffer) { buffer = com_token; buffer_size = MAX_TOKEN_CHARS; } + else if (!buffer_size) + { + overflow_flag = true; + buffer = com_token; + buffer_size = MAX_TOKEN_CHARS; + } - int c; - int len; + int c; + size_t len; + size_t stored_len; const char *data; + if (!data_p || !*data_p) + { + buffer[0] = '\0'; + if (data_p) + *data_p = nullptr; + return buffer; + } + data = *data_p; len = 0; + stored_len = 0; buffer[0] = '\0'; if (!data) @@ -56,19 +87,19 @@ char *COM_ParseEx(const char **data_p, const char *seps, char *buffer, size_t bu skipwhite: while (COM_IsSeparator(c = *data, seps)) { - if (c == '\0') - { - *data_p = nullptr; - return buffer; - } - data++; + if (c == '\0') + { + *data_p = nullptr; + return buffer; + } + data++; } // skip // comments if (c == '/' && data[1] == '/') { while (*data && *data != '\n') - data++; + data++; goto skipwhite; } @@ -78,45 +109,49 @@ char *COM_ParseEx(const char **data_p, const char *seps, char *buffer, size_t bu data++; while (1) { - c = *data++; - if (c == '\"' || !c) - { - const size_t endpos = std::min(len, buffer_size - 1); // [KEX] avoid overflow - buffer[endpos] = '\0'; - *data_p = data; - return buffer; - } - if (len < buffer_size) - { - buffer[len] = c; + c = *data++; + if (c == '\"' || !c) + { + const bool token_overflowed = len >= buffer_size; + overflow_flag = overflow_flag || token_overflowed; + buffer[stored_len] = '\0'; + *data_p = data; + if (overflowed) + *overflowed = overflow_flag; + return buffer; + } + if (stored_len + 1 < buffer_size) + { + buffer[stored_len++] = c; + } len++; - } } } // parse a regular word do { - if (len < buffer_size) - { - buffer[len] = c; + if (stored_len + 1 < buffer_size) + { + buffer[stored_len++] = c; + } len++; - } - data++; - c = *data; + data++; + c = *data; } while (!COM_IsSeparator(c, seps)); - if (len == buffer_size) - { - gi.Com_PrintFmt("Token exceeded {} chars, discarded.\n", buffer_size); - len = 0; - } - buffer[len] = '\0'; + const bool token_overflowed = len >= buffer_size; + overflow_flag = overflow_flag || token_overflowed; + buffer[stored_len] = '\0'; + + if (token_overflowed) + gi.Com_PrintFmt("Token exceeded {} chars, truncated.\n", buffer_size); + if (overflowed) + *overflowed = overflow_flag; *data_p = data; return buffer; } - /* ============================================================================ diff --git a/src/q_std.h b/src/q_std.h index a2e5215..618a0ad 100644 --- a/src/q_std.h +++ b/src/q_std.h @@ -33,6 +33,9 @@ namespace fmt = std; #define FMT_STRING(s) s #else +#ifndef FMT_HEADER_ONLY +#define FMT_HEADER_ONLY 1 +#endif #include #endif @@ -196,12 +199,18 @@ LerpAngle //============================================= -char *COM_ParseEx(const char **data_p, const char *seps, char *buffer = nullptr, size_t buffer_size = 0); +char *COM_ParseEx(const char **data_p, const char *seps, char *buffer = nullptr, size_t buffer_size = 0, bool *overflowed = nullptr); + +/* +============= +COM_Parse -// data is an in/out parm, returns a parsed out token -inline char *COM_Parse(const char **data_p, char *buffer = nullptr, size_t buffer_size = 0) +data is an in/out parm, returns a parsed out token +============= +*/ +inline char *COM_Parse(const char **data_p, char *buffer = nullptr, size_t buffer_size = 0, bool *overflowed = nullptr) { - return COM_ParseEx(data_p, "\r\n\t ", buffer, buffer_size); + return COM_ParseEx(data_p, "\r\n\t ", buffer, buffer_size, overflowed); } //============================================= diff --git a/src/q_std_parsing_tests.cpp b/src/q_std_parsing_tests.cpp new file mode 100644 index 0000000..f379276 --- /dev/null +++ b/src/q_std_parsing_tests.cpp @@ -0,0 +1,110 @@ +#include "q_std.cpp" + +#include +#include +#include +#include + +local_game_import_t gi{}; +char local_game_import_t::print_buffer[0x10000]; +static std::vector g_print_buffer; + +/* +============= +DummyComPrint + +Captures formatted output for verification without relying on the engine. +============= +*/ +static void DummyComPrint(const char *msg) +{ + if (msg) + g_print_buffer.emplace_back(msg); +} + +/* +============= +ResetPrintBuffer +============= +*/ +static void ResetPrintBuffer() +{ + g_print_buffer.clear(); +} + +/* +============= +TestZeroLengthBufferGuard + +Ensures zero-length buffers are guarded and parsing still returns a token. +============= +*/ +static void TestZeroLengthBufferGuard() +{ + const char *data = "token"; + const char *cursor = data; + char buffer[] = "Z"; + bool overflowed = false; + ResetPrintBuffer(); + char *token = COM_ParseEx(&cursor, "\r\n ", buffer, 0, &overflowed); + assert(token != nullptr); + assert(overflowed); + assert(std::strcmp(token, "token") == 0); + assert(buffer[0] == 'Z'); +} + +/* +============= +TestOversizedTokenFlag + +Confirms oversized tokens set the overflow flag and truncate instead of clearing. +============= +*/ +static void TestOversizedTokenFlag() +{ + const char *data = "oversize"; + const char *cursor = data; + char buffer[5]; + bool overflowed = false; + ResetPrintBuffer(); + char *token = COM_ParseEx(&cursor, "\r\n ", buffer, sizeof(buffer), &overflowed); + assert(token != nullptr); + assert(overflowed); + assert(std::strcmp(token, "over") == 0); + assert(!g_print_buffer.empty()); +} + +/* +============= +TestExactFitToken + +Verifies tokens that fit exactly within the buffer size do not trigger overflow handling. +============= +*/ +static void TestExactFitToken() +{ + const char *data = "fits"; + const char *cursor = data; + char buffer[5]; + bool overflowed = false; + ResetPrintBuffer(); + char *token = COM_ParseEx(&cursor, "\r\n ", buffer, sizeof(buffer), &overflowed); + assert(token != nullptr); + assert(!overflowed); + assert(std::strcmp(token, "fits") == 0); + assert(g_print_buffer.empty()); +} + +/* +============= +main +============= +*/ +int main() +{ + gi.Com_Print = DummyComPrint; + TestZeroLengthBufferGuard(); + TestOversizedTokenFlag(); + TestExactFitToken(); + return 0; +} diff --git a/src/q_vec3.h b/src/q_vec3.h index 0fa0158..50e98c3 100644 --- a/src/q_vec3.h +++ b/src/q_vec3.h @@ -4,6 +4,7 @@ #pragma once // q_vec3 - vec3 stuff +#include #include #include @@ -13,6 +14,18 @@ struct vec3_t { float x, y, z; + /* + ============= + abs + + Returns a vector containing the absolute value of each component. + ============= + */ + [[nodiscard]] inline vec3_t abs() const + { + return { fabsf(x), fabsf(y), fabsf(z) }; + } + [[nodiscard]] constexpr const float &operator[](size_t i) const { if (i == 0) @@ -168,6 +181,30 @@ struct vec3_t constexpr vec3_t vec3_origin{}; +/* +============= +DotProduct + +Returns the dot product of two vectors. +============= +*/ +[[nodiscard]] inline float DotProduct(const vec3_t &v1, const vec3_t &v2) +{ + return v1.dot(v2); +} + +/* +============= +Q_fabs + +Returns the absolute value of a floating point number. +============= +*/ +[[nodiscard]] inline float Q_fabs(float value) +{ + return fabsf(value); +} + inline void AngleVectors(const vec3_t &angles, vec3_t *forward, vec3_t *right, vec3_t *up) { float angle = angles[YAW] * (PIf * 2 / 360); diff --git a/src/tests/g_runthink_test.cpp b/src/tests/g_runthink_test.cpp new file mode 100644 index 0000000..04aba85 --- /dev/null +++ b/src/tests/g_runthink_test.cpp @@ -0,0 +1,71 @@ +#include "g_runthink.h" + +#include +#include +#include + +struct TestEntity { + gtime_t nextthink{}; + void (*think)(TestEntity *) = nullptr; + const char *classname = "test"; + bool freed = false; +}; + +/* +============= +DummyThink + +Marks that a think function ran for testing +============= +*/ +static void DummyThink(TestEntity *ent) { + ent->classname = "ran"; +} + +/* +============= +main + +Entry point for G_RunThinkImpl tests +============= +*/ +int main() { + TestEntity ent{}; + ent.nextthink = 1_ms; + + bool warning_called = false; + std::vector messages; + + auto logger = [&](TestEntity *warn_ent) { + warning_called = true; + messages.emplace_back(warn_ent->classname ? warn_ent->classname : ""); + }; + + auto fallback = [&](TestEntity *warn_ent) { + warn_ent->freed = true; + }; + + bool result = G_RunThinkImpl(&ent, 1_ms, logger, fallback); + + assert(!result); + assert(warning_called); + assert(ent.freed); + assert(ent.nextthink == 0_ms); + assert(messages.size() == 1); + assert(messages.front() == "test"); + + ent.think = DummyThink; + ent.freed = false; + ent.nextthink = 1_ms; + warning_called = false; + messages.clear(); + + result = G_RunThinkImpl(&ent, 1_ms, logger, fallback); + + assert(!result); + assert(!warning_called); + assert(!ent.freed); + assert(ent.classname == std::string("ran")); + + return 0; +} diff --git a/src/tests/g_utils_target_selection_test.cpp b/src/tests/g_utils_target_selection_test.cpp new file mode 100644 index 0000000..a1d1194 --- /dev/null +++ b/src/tests/g_utils_target_selection_test.cpp @@ -0,0 +1,48 @@ +#include +#include +#include + +#include "../g_utils_target_selection.h" + +struct DummyTarget { + }; + +/* +============= +main + +Validates that the target selection helper can pick entries beyond the first eight options and rejects invalid random indices. +============= +*/ +int main() { + DummyTarget targets[10]; + std::vector references; + references.reserve(std::size(targets)); + + for (auto &target : targets) + references.push_back(&target); + + auto cycling_random = [index = static_cast(0)](size_t max) mutable { + return (index++) % static_cast(max + 1); + }; + + bool saw_ninth_entry = false; + + for (size_t i = 0; i < references.size(); i++) { + DummyTarget *choice = G_SelectRandomTarget(references, cycling_random); + if (choice == references[9]) + saw_ninth_entry = true; + } + + assert(saw_ninth_entry); + + std::vector empty_choices; + assert(G_SelectRandomTarget(empty_choices, cycling_random) == nullptr); + + auto bad_random = [](size_t) { + return static_cast(100); + }; + + assert(G_SelectRandomTarget(references, bad_random) == nullptr); + return 0; +} diff --git a/src/tests/pickup_doppelganger.test.cpp b/src/tests/pickup_doppelganger.test.cpp new file mode 100644 index 0000000..d36fa16 --- /dev/null +++ b/src/tests/pickup_doppelganger.test.cpp @@ -0,0 +1,17 @@ +#include "g_items_limits.h" +#include + +/* +============= +main + +Ensures doppelganger max selection honors override and item defaults. +============= +*/ +int main() +{ + assert(G_GetHoldableMax(2, 1, 1) == 2); + assert(G_GetHoldableMax(0, 3, 1) == 3); + assert(G_GetHoldableMax(0, 0, 1) == 1); + return 0; +} diff --git a/tests/activation_message_tests.cpp b/tests/activation_message_tests.cpp new file mode 100644 index 0000000..50c393c --- /dev/null +++ b/tests/activation_message_tests.cpp @@ -0,0 +1,48 @@ +#include "../src/g_activation.h" +#include + +static int g_failures = 0; + +/* +============= +Expect + +Reports a failed expectation and increments the failure counter. +============= +*/ +static void Expect(bool condition, const char *message) +{ + if (!condition) + { + std::fprintf(stderr, "Expectation failed: %s\n", message); + ++g_failures; + } +} + +/* +============= +main + +Regression coverage for activation message planning. +============= +*/ +int main() +{ + activation_message_plan_t no_activator_broadcast = BuildActivationMessagePlan(true, false, false, true, true, 5); + Expect(no_activator_broadcast.broadcast_global, "no_activator_broadcast.broadcast_global should be true"); + Expect(!no_activator_broadcast.center_on_activator, "no_activator_broadcast.center_on_activator should be false"); + Expect(!no_activator_broadcast.play_sound, "no_activator_broadcast.play_sound should be false"); + + activation_message_plan_t no_activator_silent = BuildActivationMessagePlan(true, false, false, false, true, 2); + Expect(!no_activator_silent.broadcast_global, "no_activator_silent.broadcast_global should be false"); + Expect(!no_activator_silent.center_on_activator, "no_activator_silent.center_on_activator should be false"); + Expect(!no_activator_silent.play_sound, "no_activator_silent.play_sound should be false"); + + activation_message_plan_t player_plan = BuildActivationMessagePlan(true, true, false, false, true, 0); + Expect(!player_plan.broadcast_global, "player_plan.broadcast_global should be false"); + Expect(player_plan.center_on_activator, "player_plan.center_on_activator should be true"); + Expect(player_plan.play_sound, "player_plan.play_sound should be true"); + Expect(player_plan.sound_index == 0, "player_plan.sound_index should be 0"); + + return g_failures == 0 ? 0 : 1; +} diff --git a/tests/autodoc_regen_tests.cpp b/tests/autodoc_regen_tests.cpp new file mode 100644 index 0000000..209cfac --- /dev/null +++ b/tests/autodoc_regen_tests.cpp @@ -0,0 +1,17 @@ +#include "../src/g_items_limits.h" +#include + +/* +============= +main + +Verifies vampiric regen cap rounds up for odd maximums. +============= +*/ +int main() +{ + assert(G_GetTechRegenMax(101, true, false) == 51); + assert(G_GetTechRegenMax(100, true, false) == 50); + + return 0; +} diff --git a/tests/ctf_flag_state_tests.cpp b/tests/ctf_flag_state_tests.cpp new file mode 100644 index 0000000..e34fd75 --- /dev/null +++ b/tests/ctf_flag_state_tests.cpp @@ -0,0 +1,279 @@ +#include "../src/g_local.h" +#include "../src/bots/bot_utils.h" +#include +#include +#include + +local_game_import_t gi{}; +level_locals_t level{}; +game_export_t globals{}; + +static constexpr size_t kTestEntityCount = 8; +static std::aligned_storage_t g_entity_storage[kTestEntityCount]; +gentity_t *g_entities = reinterpret_cast(g_entity_storage); +static size_t g_next_entity = 0; + +/* +============= +AllocTestEntity + +Provides zeroed entity storage for simulation tests. +============= +*/ +static gentity_t *AllocTestEntity() +{ + assert(g_next_entity < kTestEntityCount); + gentity_t *ent = reinterpret_cast(&g_entity_storage[g_next_entity++]); + std::memset(ent, 0, sizeof(gentity_t)); + return ent; +} + +/* +============= +StubInfoValueForKey + +Provides a predictable value for name lookups during testing. +============= +*/ +static size_t StubInfoValueForKey(const char *, const char *, char *buffer, size_t buffer_len) +{ + if (buffer_len > 0) + buffer[0] = '\0'; + + return 0; +} + +/* +============= +StubBotRegisterEntity + +Tracks entity registration attempts without engine side effects. +============= +*/ +static void StubBotRegisterEntity(const gentity_t *) +{ +} + +/* +============= +StubTrace + +Returns an empty trace result for planner lookups. +============= +*/ +static trace_t StubTrace(gvec3_cref_t, gvec3_cptr_t, gvec3_cptr_t, gvec3_cref_t, const gentity_t *, contents_t) +{ + trace_t tr{}; + return tr; +} + +/* +============= +StubComPrint + +Ignores formatted bot utility logging during tests. +============= +*/ +static void StubComPrint(const char *) +{ +} + +/* +============= +StubAngleVectors + +Zeroes output vectors for tests that depend on angle calculations. +============= +*/ +void AngleVectors(const vec3_t &, vec3_t &forward, vec3_t *right, vec3_t *up) +{ + forward = { 0.0f, 0.0f, 0.0f }; + if (right) + *right = { 0.0f, 0.0f, 0.0f }; + if (up) + *up = { 0.0f, 0.0f, 0.0f }; +} + +/* +============= +ClientIsPlaying +============= +*/ +bool ClientIsPlaying(gclient_t *) +{ + return true; +} + +/* +============= +ArmorIndex +============= +*/ +item_id_t ArmorIndex(gentity_t *) +{ + return IT_ARMOR_BODY; +} + +/* +============= +P_GetLobbyUserNum +============= +*/ +unsigned int P_GetLobbyUserNum(const gentity_t *) +{ + return 0; +} + +/* +============= +G_FreeEntity +============= +*/ +void G_FreeEntity(gentity_t *) +{ +} + +/* +============= +MakeFlagItem + +Creates a minimal flag item descriptor for tests. +============= +*/ +static gitem_t MakeFlagItem(item_id_t id, const char *classname) +{ + gitem_t item{}; + item.id = id; + item.classname = classname; + return item; +} + +/* +============= +MakeFlagEntity + +Constructs a minimal flag entity for simulation tests. +============= +*/ +static gentity_t *MakeFlagEntity(gitem_t *item, const vec3_t &origin) +{ + gentity_t *flag = AllocTestEntity(); + flag->item = item; + flag->classname = item->classname; + flag->s.origin = origin; + flag->solid = SOLID_TRIGGER; + flag->svflags = SVF_NONE; + return flag; +} + +/* +============= +TestFlagAtBaseState + +Verifies that a visible flag at its spawn point is reported as home. +============= +*/ +static void TestFlagAtBaseState() +{ + level.time = 0_ms; + + gitem_t red_flag_item = MakeFlagItem(IT_FLAG_RED, ITEM_CTF_FLAG_RED); + gentity_t *flag = MakeFlagEntity(&red_flag_item, { 16.0f, -8.0f, 4.0f }); + + Entity_UpdateState(flag); + + assert(flag->sv.team == TEAM_RED); + assert(flag->sv.ent_flags & SVFL_IS_OBJECTIVE); + assert(flag->sv.ent_flags & SVFL_OBJECTIVE_AT_BASE); + assert(flag->sv.objective_state == objective_state_t::AtBase); + assert(flag->sv.start_origin == flag->sv.end_origin); +} + +/* +============= +TestFlagCarriedState + +Ensures a hidden respawning flag is marked as carried. +============= +*/ +static void TestFlagCarriedState() +{ + level.time = 5_sec; + + gitem_t blue_flag_item = MakeFlagItem(IT_FLAG_BLUE, ITEM_CTF_FLAG_BLUE); + gentity_t *flag = MakeFlagEntity(&blue_flag_item, { -24.0f, 12.0f, 0.0f }); + flag->solid = SOLID_NOT; + flag->svflags = SVF_NOCLIENT; + flag->flags = FL_RESPAWN; + + Entity_UpdateState(flag); + + assert(flag->sv.team == TEAM_BLUE); + assert(flag->sv.ent_flags & SVFL_IS_HIDDEN); + assert(flag->sv.ent_flags & SVFL_OBJECTIVE_CARRIED); + assert(flag->sv.objective_state == objective_state_t::Carried); + assert(flag->sv.respawntime == Item_UnknownRespawnTime); +} + +/* +============= +TestFlagDroppedState + +Confirms a dropped flag reports its dropped state and return timer. +============= +*/ +static void TestFlagDroppedState() +{ + level.time = 12_sec; + + gitem_t red_flag_item = MakeFlagItem(IT_FLAG_RED, ITEM_CTF_FLAG_RED); + gentity_t *carrier = AllocTestEntity(); + gentity_t *flag = MakeFlagEntity(&red_flag_item, { 4.0f, 4.0f, 32.0f }); + flag->spawnflags = SPAWNFLAG_ITEM_DROPPED; + flag->owner = carrier; + flag->nextthink = level.time + 30_sec; + + Entity_UpdateState(flag); + + assert(flag->sv.ent_flags & SVFL_OBJECTIVE_DROPPED); + assert(flag->sv.objective_state == objective_state_t::Dropped); + assert(flag->sv.respawntime == 30000); +} + +/* +============= +SetupTestEnvironment + +Resets shared test state and installs required engine stubs. +============= +*/ +static void SetupTestEnvironment() +{ + g_next_entity = 0; + std::memset(g_entity_storage, 0, sizeof(g_entity_storage)); + std::memset(&level, 0, sizeof(level)); + std::memset(&globals, 0, sizeof(globals)); + gi = {}; + + gi.Info_ValueForKey = StubInfoValueForKey; + gi.Bot_RegisterEntity = StubBotRegisterEntity; + gi.game_import_t::trace = StubTrace; + gi.Com_Print = StubComPrint; + globals.num_entities = kTestEntityCount; +} + +/* +============= +main +============= +*/ +int main() +{ + SetupTestEnvironment(); + + TestFlagAtBaseState(); + TestFlagCarriedState(); + TestFlagDroppedState(); + + return 0; +} diff --git a/tests/friendly_message_tests.cpp b/tests/friendly_message_tests.cpp new file mode 100644 index 0000000..d7e11b8 --- /dev/null +++ b/tests/friendly_message_tests.cpp @@ -0,0 +1,32 @@ +#include "../src/g_utils_friendly_message.h" +#include +#include + +/* +============= +main + +Regression coverage for friendly message validation. +============= +*/ +int main() + { + assert(!FriendlyMessageHasText(nullptr)); + assert(!FriendlyMessageHasText("")); + assert(FriendlyMessageHasText("hello")); + + char unterminated[256]; + std::fill_n(unterminated, 256, 'a'); + assert(!FriendlyMessageHasText(unterminated)); + + char bounded[256]; + std::fill_n(bounded, 255, 'b'); + bounded[255] = '\0'; + assert(FriendlyMessageHasText(bounded)); + + assert(FriendlyMessageShouldPrefixTeam(true, false, false, false, true)); + assert(!FriendlyMessageShouldPrefixTeam(true, false, false, false, false)); + assert(FriendlyMessageShouldPrefixTeam(true, false, true, true, false)); + + return 0; +} diff --git a/tests/horde_lives_tests.cpp b/tests/horde_lives_tests.cpp new file mode 100644 index 0000000..203f2b8 --- /dev/null +++ b/tests/horde_lives_tests.cpp @@ -0,0 +1,103 @@ +#include "../src/g_local.h" +#include +#include + +static int g_failures = 0; + +/* +============= +Expect + +Reports a failed expectation and increments the failure counter. +============= +*/ +static void Expect(bool condition, const char *message) +{ + if (!condition) + { + std::fprintf(stderr, "Expectation failed: %s\n", message); + ++g_failures; + } +} + +/* +============= +TestAlivePlayerPreventsElimination + +Verifies that a living player keeps the wave active regardless of lives. +============= +*/ +static void TestAlivePlayerPreventsElimination() +{ + std::vector states = { + { true, false, 100, 0 }, + { true, false, -10, 0 } + }; + + Expect(!Horde_NoLivesRemain(states), "Living players should block elimination"); +} + +/* +============= +TestRemainingLivesPreventFailure + +Ensures remaining lives keep players eligible to respawn. +============= +*/ +static void TestRemainingLivesPreventFailure() +{ + std::vector states = { + { true, false, -20, 2 }, + { true, false, -5, 0 } + }; + + Expect(!Horde_NoLivesRemain(states), "Remaining lives should prevent elimination"); +} + +/* +============= +TestNoLivesTriggersElimination + +Confirms that exhausted lives across the roster force elimination. +============= +*/ +static void TestNoLivesTriggersElimination() +{ + std::vector states = { + { true, false, -10, 0 }, + { true, true, -15, 0 } + }; + + Expect(Horde_NoLivesRemain(states), "No lives should trigger elimination"); +} + +/* +============= +TestEmptyRosterTriggersElimination + +Ensures waves complete when no playing clients remain. +============= +*/ +static void TestEmptyRosterTriggersElimination() +{ + std::vector states; + + Expect(Horde_NoLivesRemain(states), "No playing clients should end the wave"); +} + +/* +============= +main + +Runs Horde life exhaustion regression checks. +============= +*/ +int main() +{ + TestAlivePlayerPreventsElimination(); + TestRemainingLivesPreventFailure(); + TestNoLivesTriggersElimination(); + TestEmptyRosterTriggersElimination(); + + return g_failures == 0 ? 0 : 1; +} diff --git a/tests/ip_filter_write_tests.cpp b/tests/ip_filter_write_tests.cpp new file mode 100644 index 0000000..c52cd19 --- /dev/null +++ b/tests/ip_filter_write_tests.cpp @@ -0,0 +1,221 @@ +#include "../src/g_local.h" +#include +#include +#include +#include +#include +#include +#include + +local_game_import_t gi{}; +g_fmt_data_t g_fmt_data{}; + +static int g_failures = 0; +static std::vector g_messages; + +std::array local_game_import_t::buffers; +std::array local_game_import_t::buffer_ptrs; + +static char game_name[] = "game"; +static std::vector game_string_storage; +static cvar_t game_cvar{ + game_name, + nullptr, + nullptr, + static_cast(0), + 0, + 0.0f, + nullptr, + 0 +}; + +static char filterban_name[] = "filterban"; +static char filterban_value[] = "1"; +static cvar_t filterban_stub{ + filterban_name, + filterban_value, + nullptr, + static_cast(0), + 0, + 0.0f, + nullptr, + 1 +}; + +cvar_t *filterban = &filterban_stub; + +/* +============= +StubArgc + +Provides a default argc of zero for command stubs. +============= +*/ +static int StubArgc() +{ + return 0; +} + +/* +============= +StubArgv + +Provides empty argv content for command stubs. +============= +*/ +static const char *StubArgv(int) +{ + return ""; +} + +/* +============= +StubCvar + +Returns the configured game cvar or nullptr for unknown names. +============= +*/ +static cvar_t *StubCvar(const char *var_name, const char *, cvar_flags_t) +{ + if (std::strcmp(var_name, game_name) == 0) + { + return &game_cvar; + } + + return nullptr; +} + +/* +============= +StubLocPrint + +Captures localization print requests for verification. +============= +*/ +static void StubLocPrint(gentity_t *, print_type_t, const char *base, const char **args, size_t num_args) +{ + std::string message = base; + for (size_t i = 0; i < num_args; ++i) + { + message += args[i]; + } + + g_messages.push_back(message); +} + +/* +============= +Match_End + +Stubbed to satisfy linkage when including g_svcmds.cpp. +============= +*/ +void Match_End() +{ +} + +#include "../src/g_svcmds.cpp" + +/* +============= +Q_strcasecmp + +Implements case-insensitive string comparison for test coverage. +============= +*/ +int Q_strcasecmp(const char *s1, const char *s2) +{ + return strcasecmp(s1, s2); +} + +/* +============= +Expect + +Reports a failed expectation and increments the failure counter. +============= +*/ +static void Expect(bool condition, const char *message) +{ + if (!condition) + { + std::fprintf(stderr, "Expectation failed: %s\n", message); + ++g_failures; + } +} + +/* +============= +ConfigureGamePath + +Assigns the game cvar's string storage to the provided path. +============= +*/ +static void ConfigureGamePath(const std::filesystem::path &path) +{ + const auto string_data = path.string(); + game_string_storage.assign(string_data.begin(), string_data.end()); + game_string_storage.push_back('\0'); + game_cvar.string = game_string_storage.data(); +} + +/* +============= +ReadFileContents + +Returns the full text of the specified file. +============= +*/ +static std::string ReadFileContents(const std::filesystem::path &path) +{ + std::ifstream stream(path); + return std::string(std::istreambuf_iterator(stream), std::istreambuf_iterator()); +} + +/* +============= +ValidateWriteIpSerialization + +Ensures writeip persists filterban and IP filters to listip.cfg. +============= +*/ +static void ValidateWriteIpSerialization() +{ + gi.argc = &StubArgc; + gi.argv = &StubArgv; + gi.cvar = &StubCvar; + gi.Loc_Print = &StubLocPrint; + + numipfilters = 0; + Expect(StringToFilter("10.0.0.1", &ipfilters[numipfilters++]), "Should parse 10.0.0.1"); + Expect(StringToFilter("192.168.5.0", &ipfilters[numipfilters++]), "Should parse 192.168.5.0"); + + std::filesystem::path output_dir = std::filesystem::temp_directory_path() / "muffmode_writeip"; + std::filesystem::remove_all(output_dir); + std::filesystem::create_directories(output_dir); + ConfigureGamePath(output_dir); + + SVCmd_WriteIP_f(); + + std::filesystem::path output_file = output_dir / "listip.cfg"; + Expect(std::filesystem::exists(output_file), "listip.cfg should be created"); + + const std::string file_contents = ReadFileContents(output_file); + const std::string expected_contents = "set filterban 1\nsv addip 10.0.0.1\nsv addip 192.168.5.0\n"; + Expect(file_contents == expected_contents, "listip.cfg should include filterban and both entries"); + + std::filesystem::remove_all(output_dir); +} + +/* +============= +main + +Executes writeip serialization coverage. +============= +*/ +int main() +{ + ValidateWriteIpSerialization(); + return g_failures == 0 ? 0 : 1; +} diff --git a/tests/menu_copy_tests.cpp b/tests/menu_copy_tests.cpp new file mode 100644 index 0000000..d33ea8a --- /dev/null +++ b/tests/menu_copy_tests.cpp @@ -0,0 +1,31 @@ +#include "../src/q_std.h" +#include +#include + +/* +============= +main + +Verify Q_strlcpy copies menu-sized buffers without truncation when the destination size is provided. +============= +*/ +int main() +{ + char destination[256] = {}; + const char short_source[] = "Short label"; + const size_t copied = Q_strlcpy(destination, short_source, sizeof(destination)); + + assert(copied == strlen(short_source)); + assert(strcmp(destination, short_source) == 0); + + char long_source[300]; + memset(long_source, 'a', sizeof(long_source)); + long_source[sizeof(long_source) - 1] = '\0'; + + char truncated[16] = {}; + const size_t truncated_length = Q_strlcpy(truncated, long_source, sizeof(truncated)); + assert(truncated_length >= sizeof(truncated)); + assert(truncated[sizeof(truncated) - 1] == '\0'); + + return 0; +} diff --git a/tests/menu_statusbar_tests.cpp b/tests/menu_statusbar_tests.cpp new file mode 100644 index 0000000..d79e1f5 --- /dev/null +++ b/tests/menu_statusbar_tests.cpp @@ -0,0 +1,74 @@ +#include "../src/p_menu.h" +#include "../src/q_std.h" + +#include +#include + +constexpr size_t MAX_STRING_CHARS = 1024; + +/* +============= +MakeMenuEntry + +Constructs a menu_t with the supplied label and default alignment. +============= +*/ +static menu_t MakeMenuEntry(const char *label) +{ + menu_t entry{}; + Q_strlcpy(entry.text, label, sizeof(entry.text)); + entry.align = MENU_ALIGN_LEFT; + entry.SelectFunc = nullptr; + entry.text_arg1[0] = '\0'; + return entry; +} + +/* +============= +main + +Validates that status bar layout construction clamps long menu text safely. +============= +*/ +int main() +{ + menu_t entries[4]; + entries[0] = MakeMenuEntry("Short"); + entries[1] = MakeMenuEntry("*Highlighted"); + entries[2] = MakeMenuEntry("Centered"); + entries[2].align = MENU_ALIGN_CENTER; + entries[3] = MakeMenuEntry("Right"); + entries[3].align = MENU_ALIGN_RIGHT; + + menu_hnd_t hnd{}; + hnd.entries = entries; + hnd.cur = 1; + hnd.num = static_cast(sizeof(entries) / sizeof(entries[0])); + + char layout[MAX_STRING_CHARS] = {}; + const size_t short_length = P_Menu_BuildStatusBar(&hnd, layout, sizeof(layout)); + assert(short_length < MAX_STRING_CHARS); + assert(layout[MAX_STRING_CHARS - 1] == '\0'); + + char oversized_text[sizeof(entries[0].text)]; + std::memset(oversized_text, 'Z', sizeof(oversized_text)); + oversized_text[sizeof(oversized_text) - 1] = '\0'; + + menu_t long_entries[6]; + for (auto &entry : long_entries) + entry = MakeMenuEntry(oversized_text); + + menu_hnd_t long_hnd{}; + long_hnd.entries = long_entries; + long_hnd.cur = 0; + long_hnd.num = static_cast(sizeof(long_entries) / sizeof(long_entries[0])); + + std::memset(layout, 'X', sizeof(layout)); + layout[sizeof(layout) - 1] = '\0'; + const size_t long_length = P_Menu_BuildStatusBar(&long_hnd, layout, sizeof(layout)); + + assert(long_length == MAX_STRING_CHARS - 1); + assert(layout[sizeof(layout) - 1] == '\0'); + + return 0; +} diff --git a/tests/shambler_balance_tests.cpp b/tests/shambler_balance_tests.cpp new file mode 100644 index 0000000..3c66364 --- /dev/null +++ b/tests/shambler_balance_tests.cpp @@ -0,0 +1,32 @@ +#include "../src/g_local.h" +#include + +int ShamblerApplyExplosionResistance(gentity_t *self, int damage, const mod_t &mod); + +/* +============= +main + +Verifies explosion resistance halves splash damage for the shambler before pain handling. +============= +*/ +int main() +{ + gentity_t shambler{}; + shambler.health = 160; + + mod_t splash{ MOD_R_SPLASH }; + int adjusted = ShamblerApplyExplosionResistance(&shambler, 40, splash); + + assert(adjusted == 20); + assert(shambler.health == 180); + + shambler.health = 150; + mod_t direct{ MOD_SHOTGUN }; + adjusted = ShamblerApplyExplosionResistance(&shambler, 40, direct); + + assert(adjusted == 40); + assert(shambler.health == 150); + + return 0; +} diff --git a/tests/spawn_gravity_tests.cpp b/tests/spawn_gravity_tests.cpp new file mode 100644 index 0000000..0612123 --- /dev/null +++ b/tests/spawn_gravity_tests.cpp @@ -0,0 +1,390 @@ +#include "../src/g_local.h" +#include + +// Limit g_monster_spawn.cpp surface area for unit tests +#define MONSTER_SPAWN_TESTS +#include "../src/g_monster_spawn.cpp" + +// Stub globals expected by the included code +local_game_import_t gi{}; +gentity_t *world = reinterpret_cast(0x1); + +static int g_trace_calls = 0; +static vec3_t g_last_trace_start; +static vec3_t g_last_trace_end; +static vec3_t g_last_bottom_fast; +static vec3_t g_last_bottom_slow; +static bool g_create_monster_should_fail = false; + +/* +============= +TestPointContents + +Treats all points as non-water for spawn validation. +============= +*/ +static contents_t TestPointContents(const vec3_t &) +{ + return 0; +} + +/* +============= +CreateMonster + +Returns nullptr when configured to simulate spawn failure. +============= +*/ +gentity_t *CreateMonster(const vec3_t &origin, const vec3_t &angles, const char *classname) +{ + if (g_create_monster_should_fail) + return nullptr; + + static gentity_t dummy_ent{}; + + dummy_ent.s.origin = origin; + dummy_ent.s.angles = angles; + dummy_ent.classname = classname; + + return &dummy_ent; +} + +/* +============= +TestTrace + +Provides deterministic trace behavior for spawn validation tests. +============= +*/ +static trace_t TestTrace(const vec3_t &start, const vec3_t &, const vec3_t &, const vec3_t &end, gentity_t *, contents_t) +{ + trace_t tr{}; + + g_last_trace_start = start; + g_last_trace_end = end; + g_trace_calls++; + + tr.ent = world; + tr.endpos = end; + tr.fraction = 0.5f; + tr.startsolid = false; + tr.allsolid = false; + + return tr; +} + +/* +============= +NoHitTrace + +Returns a full-length trace with no impact to simulate missing ground. +============= +*/ +static trace_t NoHitTrace(const vec3_t &start, const vec3_t &, const vec3_t &, const vec3_t &end, gentity_t *, contents_t) +{ + trace_t tr{}; + + g_last_trace_start = start; + g_last_trace_end = end; + g_trace_calls++; + + tr.ent = world; + tr.endpos = end; + tr.fraction = 1.0f; + tr.startsolid = false; + tr.allsolid = false; + + return tr; +} + +/* +============= +G_FixStuckObject_Generic + +Trivial stub to satisfy FindSpawnPoint dependency. +============= +*/ +stuck_result_t G_FixStuckObject_Generic(vec3_t &origin, const vec3_t &, const vec3_t &, std::function) +{ + (void)origin; + return stuck_result_t::GOOD_POSITION; +} + +/* +============= +M_CheckBottom_Fast_Generic + +Records the provided gravity vector for verification. +============= +*/ +bool M_CheckBottom_Fast_Generic(const vec3_t &, const vec3_t &, const vec3_t &gravityVector) +{ + g_last_bottom_fast = gravityVector; + return false; +} + +/* +============= +M_CheckBottom_Slow_Generic + +Records the provided gravity vector for verification. +============= +*/ +bool M_CheckBottom_Slow_Generic(const vec3_t &, const vec3_t &, const vec3_t &, gentity_t *, contents_t, const vec3_t &gravityVector, bool) +{ + g_last_bottom_slow = gravityVector; + return true; +} + +/* +============= +M_droptofloor_generic + +Drops an origin along the provided gravity vector until contact is made or a blocking volume is found. +============= +*/ +bool M_droptofloor_generic(vec3_t &origin, const vec3_t &mins, const vec3_t &maxs, const vec3_t &gravityVector, gentity_t *ignore, contents_t mask, bool allow_partial) +{ + vec3_t gravity_dir = gravityVector.normalized(); + + if (!gravity_dir) + gravity_dir = { 0.0f, 0.0f, -1.0f }; + + trace_t trace = gi.trace(origin, mins, maxs, origin, ignore, mask); + + if (trace.startsolid) + origin -= gravity_dir; + + vec3_t end = origin + (gravity_dir * 256.0f); + + trace = gi.trace(origin, mins, maxs, end, ignore, mask); + + if (trace.fraction == 1 || trace.allsolid || (!allow_partial && trace.startsolid)) + return false; + + origin = trace.endpos; + + return true; +} + +/* +============= +TestCeilingDropUsesGravity + +Verifies FindSpawnPoint drops along positive gravity vectors. +============= +*/ +static void TestCeilingDropUsesGravity() +{ + gi.trace = TestTrace; + g_trace_calls = 0; + + vec3_t start{ 0.0f, 0.0f, 0.0f }; + vec3_t mins{ -1.0f, -1.0f, -1.0f }; + vec3_t maxs{ 1.0f, 1.0f, 1.0f }; + vec3_t spawn{}; + + bool found = FindSpawnPoint(start, mins, maxs, spawn, 32.0f, true, { 0.0f, 0.0f, 1.0f }); + + assert(found); + assert(g_trace_calls >= 2); + assert(spawn[0] == 0.0f && spawn[1] == 0.0f && spawn[2] == 256.0f); + assert(g_last_trace_end[2] == 256.0f); +} + +/* +============= +TestWallGravityProjectsSpawnVolume + +Ensures CheckSpawnPoint traces along the supplied gravity vector. +============= +*/ +static void TestWallGravityProjectsSpawnVolume() +{ + gi.trace = TestTrace; + g_trace_calls = 0; + + vec3_t origin{ 5.0f, 5.0f, 5.0f }; + vec3_t mins{ -2.0f, -2.0f, -2.0f }; + vec3_t maxs{ 2.0f, 2.0f, 2.0f }; + vec3_t gravity{ 1.0f, 0.0f, 0.0f }; + + bool clear = CheckSpawnPoint(origin, mins, maxs, gravity); + + assert(clear); + assert(g_trace_calls >= 2); + + vec3_t gravity_dir = gravity.normalized(); + + if (!gravity_dir) + gravity_dir = { 0.0f, 0.0f, -1.0f }; + + vec3_t abs_gravity_dir = gravity_dir.abs(); + const float negative_extent = Q_fabs(DotProduct(mins, abs_gravity_dir)); + vec3_t expected_end = origin + (gravity_dir * negative_extent); + + assert(g_last_trace_start == origin); + assert(g_last_trace_end == expected_end); +} + +/* +============= +TestNegativeGravityProjectsSpawnVolume + +Ensures CheckSpawnPoint projects extents correctly when gravity points into negative axes. +============= +*/ +static void TestNegativeGravityProjectsSpawnVolume() +{ + gi.trace = TestTrace; + g_trace_calls = 0; + + vec3_t origin{ -12.0f, 4.0f, 8.0f }; + vec3_t mins{ -3.0f, -1.0f, -2.0f }; + vec3_t maxs{ 5.0f, 2.0f, 3.0f }; + vec3_t gravity{ -1.0f, -0.5f, 0.0f }; + + bool clear = CheckSpawnPoint(origin, mins, maxs, gravity); + + assert(clear); + assert(g_trace_calls >= 2); + + vec3_t gravity_dir = gravity.normalized(); + + if (!gravity_dir) + gravity_dir = { 0.0f, 0.0f, -1.0f }; + + vec3_t abs_gravity_dir = gravity_dir.abs(); + const float negative_extent = Q_fabs(DotProduct(mins, abs_gravity_dir)); + vec3_t expected_end = origin + (gravity_dir * negative_extent); + + assert(g_last_trace_start == origin); + assert(g_last_trace_end == expected_end); +} + +/* +============= +TestGroundChecksUseGravityVector + +Confirms bottom checks receive the supplied gravity vector. +============= +*/ +static void TestGroundChecksUseGravityVector() +{ + gi.trace = TestTrace; + g_last_bottom_fast = { 0.0f, 0.0f, 0.0f }; + g_last_bottom_slow = { 0.0f, 0.0f, 0.0f }; + + vec3_t origin{ 0.0f, 0.0f, 0.0f }; + vec3_t mins{ -8.0f, -8.0f, -8.0f }; + vec3_t maxs{ 8.0f, 8.0f, 8.0f }; + vec3_t gravity{ 0.0f, 1.0f, 0.0f }; + + bool grounded = CheckGroundSpawnPoint(origin, mins, maxs, 128.0f, gravity); + + assert(grounded); + assert(g_last_bottom_fast == gravity); + assert(g_last_bottom_slow == gravity); +} + +/* +============= +TestGroundSpawnHonorsHeight + +Fails ground spawn validation when the surface is beyond the allowed height. +============= +*/ +static void TestGroundSpawnHonorsHeight() +{ + gi.trace = NoHitTrace; + g_trace_calls = 0; + g_last_trace_start = { 0.0f, 0.0f, 0.0f }; + g_last_trace_end = { 0.0f, 0.0f, 0.0f }; + + vec3_t origin{ 0.0f, 0.0f, 0.0f }; + vec3_t mins{ -8.0f, -8.0f, -8.0f }; + vec3_t maxs{ 8.0f, 8.0f, 8.0f }; + vec3_t gravity{ 0.0f, 0.0f, -1.0f }; + float height = 64.0f; + + bool grounded = CheckGroundSpawnPoint(origin, mins, maxs, height, gravity); + + assert(!grounded); + assert(g_trace_calls >= 3); + + vec3_t gravity_dir = gravity.normalized(); + + if (!gravity_dir) + gravity_dir = { 0.0f, 0.0f, -1.0f }; + + vec3_t expected_end = origin + (gravity_dir * height); + + assert(g_last_trace_start == origin); + assert(g_last_trace_end == expected_end); +} + +/* +============= +TestCreateFlyMonsterReturnsNullOnFailedSpawn + +Ensures CreateFlyMonster returns nullptr when CreateMonster fails. +============= +*/ +static void TestCreateFlyMonsterReturnsNullOnFailedSpawn() +{ + gi.trace = TestTrace; + gi.pointcontents = TestPointContents; + g_create_monster_should_fail = true; + + vec3_t origin{ 12.0f, -4.0f, 20.0f }; + vec3_t angles{ 0.0f, 45.0f, 0.0f }; + vec3_t mins{ -4.0f, -4.0f, -4.0f }; + vec3_t maxs{ 4.0f, 4.0f, 4.0f }; + + gentity_t *spawned = CreateFlyMonster(origin, angles, mins, maxs, "monster_flyer"); + + assert(spawned == nullptr); + + g_create_monster_should_fail = false; +} + +/* +============= +TestCreateGroundMonsterReturnsNullOnFailedSpawn + +Ensures CreateGroundMonster returns nullptr when CreateMonster fails. +============= +*/ +static void TestCreateGroundMonsterReturnsNullOnFailedSpawn() +{ + gi.trace = TestTrace; + gi.pointcontents = TestPointContents; + g_create_monster_should_fail = true; + + vec3_t origin{ 16.0f, 8.0f, 4.0f }; + vec3_t angles{ 0.0f, 90.0f, 0.0f }; + vec3_t mins{ -8.0f, -8.0f, -8.0f }; + vec3_t maxs{ 8.0f, 8.0f, 8.0f }; + + gentity_t *spawned = CreateGroundMonster(origin, angles, mins, maxs, "monster_infantry", 64.0f); + + assert(spawned == nullptr); +} + +/* +============= +main +============= +*/ +int main() +{ + gi.pointcontents = TestPointContents; + TestCeilingDropUsesGravity(); + TestWallGravityProjectsSpawnVolume(); + TestNegativeGravityProjectsSpawnVolume(); + TestGroundChecksUseGravityVector(); + TestGroundSpawnHonorsHeight(); + TestCreateFlyMonsterReturnsNullOnFailedSpawn(); + TestCreateGroundMonsterReturnsNullOnFailedSpawn(); + return 0; +}