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 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 @@
Level3true
- 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)truestdcpp174267;4244trueMultiThreadedDebug
- c:\jsoncpp;%(AdditionalIncludeDirectories)
+ /utf-8 %(AdditionalOptions)NotSettrue
- c:\jsoncpp;%(AdditionalLibraryDirectories)
- %(AdditionalDependencies)
-
- false
-
@@ -95,6 +89,7 @@
4267;4244trueMultiThreaded
+ /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;
+}