From c913f9a98b5505876374db16edacec260f4019c5 Mon Sep 17 00:00:00 2001 From: cypress Date: Sun, 29 Oct 2023 18:21:41 -0400 Subject: [PATCH] SERVER: Add support for spawning Co-Op spawns if not provided --- progs/fte-server.src | 1 + progs/standard.src | 1 + source/server/defs/custom.qc | 5 + source/server/entities/spawn_points.qc | 370 +++++++++++++++++++++++++ source/server/player.qc | 117 ++++---- 5 files changed, 439 insertions(+), 55 deletions(-) create mode 100644 source/server/entities/spawn_points.qc diff --git a/progs/fte-server.src b/progs/fte-server.src index b773a00..1f20016 100644 --- a/progs/fte-server.src +++ b/progs/fte-server.src @@ -19,6 +19,7 @@ ../source/server/entities/sub_functions.qc ../source/server/entities/sounds.qc ../source/server/entities/triggers.qc +../source/server/entities/spawn_points.qc ../source/server/entities/explosive_barrel.qc ../source/server/entities/teleporter.qc ../source/server/entities/map_entities.qc diff --git a/progs/standard.src b/progs/standard.src index 0a4e123..0e6b07e 100644 --- a/progs/standard.src +++ b/progs/standard.src @@ -23,6 +23,7 @@ ../source/server/entities/sub_functions.qc ../source/server/entities/sounds.qc ../source/server/entities/triggers.qc +../source/server/entities/spawn_points.qc ../source/server/entities/explosive_barrel.qc ../source/server/entities/teleporter.qc ../source/server/entities/map_entities.qc diff --git a/source/server/defs/custom.qc b/source/server/defs/custom.qc index 9e38b3d..c445d5f 100644 --- a/source/server/defs/custom.qc +++ b/source/server/defs/custom.qc @@ -36,6 +36,11 @@ #define STR_NOTENOUGHPOINTS "Not Enough Points\n" // To help aid consistency with these.. +#define SPAWN_1_CLASS "info_player_1_spawn" +#define SPAWN_2_CLASS "info_player_2_spawn" +#define SPAWN_3_CLASS "info_player_3_spawn" +#define SPAWN_4_CLASS "info_player_4_spawn" + float cheats_have_been_activated; // Quake assumes these are defined. diff --git a/source/server/entities/spawn_points.qc b/source/server/entities/spawn_points.qc new file mode 100644 index 0000000..20df2a9 --- /dev/null +++ b/source/server/entities/spawn_points.qc @@ -0,0 +1,370 @@ +/* + server/entities/spawn_points.qc + + Code for Player Spawn points. + + Copyright (C) 2021-2023 NZ:P Team + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to: + + Free Software Foundation, Inc. + 59 Temple Place - Suite 330 + Boston, MA 02111-1307, USA + +*/ + +#define SPAWN_PLR_1 0 +#define SPAWN_PLR_2 1 +#define SPAWN_PLR_3 2 +#define SPAWN_PLR_4 3 +#define SPAWN_PLR_LEGACY 4 + +#define SPAWN_START_WEAPON W_COLT +#define SPAWN_START_MAG 8 +#define SPAWN_START_AMMO 32 + +float player_spawns[5]; +entity spawn_points[5]; +string spawn_names[4]; + +// +// Spawns_ConvertOldClassnames() +// Renames old/legacy spawns to their +// newly assocated names. +// +void() Spawns_ConvertOldClassnames = +{ + entity spawn_point; + + spawn_point = find(world, classname, "info_player_tank"); + if (spawn_point != world) spawn_point.classname = SPAWN_1_CLASS; + spawn_point = find(world, classname, "info_player_nikolai"); + if (spawn_point != world) spawn_point.classname = SPAWN_2_CLASS; + spawn_point = find(world, classname, "info_player_takeo"); + if (spawn_point != world) spawn_point.classname = SPAWN_3_CLASS; + spawn_point = find(world, classname, "info_player_doctor"); + if (spawn_point != world) spawn_point.classname = SPAWN_4_CLASS; +}; + +// +// Spawns_EntIsInRange(which, units) +// Returns true if there is a solid entity +// within the provided units at a given +// entity's origin. +// +float(entity which, float units) Spawns_EntIsInRange = +{ + float found_something = false; + + entity ents_in_range = findradius(which.origin, units); + while(ents_in_range != world) { + if (ents_in_range != which && + ents_in_range.solid != SOLID_NOT && + ents_in_range.solid != SOLID_TRIGGER) + found_something = true; + + ents_in_range = ents_in_range.chain; + } + + return found_something; +} + +// +// Spawns_PerformCheck(which, start_org, ...) +// Performs various checks at different positions +// for positioning of a spawn point if one is +// not provided by the map. Sets the origin if +// spot is valid. +// +float (entity which, vector start_org, vector dir, +float units, vector feet, vector head) Spawns_PerformCheck = +{ + // Try moving the designated units + vector org = start_org + (dir * units); + which.origin = org; + which.solid = SOLID_BBOX; + setorigin(which, which.origin); + + // If there's no ents in the way, check that we're + // not stuck inside a brush. + if (!Spawns_EntIsInRange(which, 64)) { + feet = start_org /*- '0 0 16'*/; + traceline (feet, feet + (dir * units), 0, spawn_points[SPAWN_PLR_1]); + + // We were successfully able to trace at the feet. + if (trace_ent.classname == which.classname) { + // Now try the head. + head = start_org + '0 0 32'; + traceline (head, head + (dir * units), 0, spawn_points[SPAWN_PLR_1]); + + // Spot is viable, both feet and head are in same boundary. + if (trace_ent.classname == which.classname) { + return true; + } + } + } + + return false; +}; + +// +// Spawns_SetAllNonSolid(ignore_first) +// Marks all spawn points as SOLID_NOT, +// use ignore_first to ommit player 1. +// +void(float ignore_first) Spawns_SetAllNonSolid = +{ + float start; + + if (ignore_first) start = 1; + else start = 0; + + for(float i = start; i < 4; i++) { + if (spawn_points[i] == world) + continue; + + spawn_points[i].solid = SOLID_NOT; + setorigin(spawn_points[i], spawn_points[i].origin); + } +}; + +// +// Spawns_SetAllSolid(ignore_first) +// Marks all spawn points as SOLID_BBOX, +// use ignore_first to ommit player 1. +// +void(float ignore_first) Spawns_SetAllSolid = +{ + float start; + + if (ignore_first) start = 1; + else start = 0; + + for(float i = start; i < 4; i++) { + if (spawn_points[i] == world) + continue; + + spawn_points[i].solid = SOLID_BBOX; + setorigin(spawn_points[i], spawn_points[i].origin); + } +}; + +// +// Spawns_FindViableSpawnSpot(which) +// Uses a simple "algorithm" for predicting +// potential viable spawn points, and calls +// Spawns_PerformCheck() to validate and +// set if able. +// +void(entity which) Spawns_FindViableSpawnSpot = +{ + vector org = '0 0 0'; + vector trace_feet = '0 0 0'; + vector trace_head = '0 0 0'; + + // Always start with the "main" (player 1) spawn + vector start_origin = spawn_points[SPAWN_PLR_1].origin; + makevectors(spawn_points[SPAWN_PLR_1].angles); + + // Spawn point determination "algorithm" is really simple -- + // We have "ideal" positioning that looks like this: + // --- + // 3 + // 2 1 4 + // --- + // If any spot (other than 1) fails, try the next, when 4 + // is reached, start from the left again with a larger + // distance from one, e.g.: + // --- + // 3 + // 4 2 1 + // --- + // We do this for 2 cycles (64 units, 128 units, 192 units) + // before giving up and just setting a spawn to slot 1, but + // this SHOULD never happen(?!). + + float units = 64; + + for (float i = 0; i < 2; i++) { + // Try moving to the "left". + if (Spawns_PerformCheck(which, start_origin, v_right, -units, trace_feet, trace_head)) { + Spawns_SetAllSolid(true); + return; + } + + // Now "forward". + if (Spawns_PerformCheck(which, start_origin, v_forward, units, trace_feet, trace_head)) { + Spawns_SetAllSolid(true); + return; + } + + // Now "right". + if (Spawns_PerformCheck(which, start_origin, v_right, units, trace_feet, trace_head)) { + Spawns_SetAllSolid(true); + return; + } + + // Now "back". + if (Spawns_PerformCheck(which, start_origin, v_forward, -units, trace_feet, trace_head)) { + Spawns_SetAllSolid(true); + return; + } + + // Increase the distance by 64 units and try again. + Spawns_SetAllNonSolid(true); + units += 64; + } + + // Every attempt failed, just put them in the same + // spot as player one. + setorigin(which, start_origin); + bprint(PRINT_HIGH, "WARN: Unable to find viable player start position\n"); + bprint(PRINT_HIGH, strcat(" for entity: ", which.classname)); + bprint(PRINT_HIGH, "\n"); +}; + +// +// Spawns_DropToFloor(who) +// Drops the spawn point to +// the floor. +// +void(entity who) Spawns_DropToFloor = +{ + entity tempe = self; + self = who; + +#ifdef FTE + + droptofloor(); + +#else + + droptofloor(0, 0); + +#endif // FTE + + self = tempe; +} + +// +// Spawns_SetUpPoint(which) +// Sets up base stats for a Spawn +// Point such as size, and any fields +// not provided by map maker. +// +void(entity which) Spawns_SetUpPoint = +{ + which.solid = SOLID_BBOX; + setsize(which, [-8, -8, -32], [8, 8, 40]); + Spawns_DropToFloor(which); + + if (!which.weapon) { + which.weapon = SPAWN_START_WEAPON; + which.currentmag = SPAWN_START_MAG; + which.currentammo = SPAWN_START_AMMO; + } +}; + +// +// Spawns_FillMissing() +// Creates any missing Spawn Points, +// setting up their fields if necessary. +// +void() Spawns_FillMissing = +{ + float i; + + // Count how many spawnpoints we have. + for(i = 0; i < 4; i++) { + spawn_points[i] = find(world, classname, spawn_names[i]); + if (spawn_points[i] != world) { + player_spawns[i] = true; + Spawns_SetUpPoint(spawn_points[i]); + } + } + + // Store the legacy spawn point as well (info_player_start). + spawn_points[SPAWN_PLR_LEGACY] = find(world, classname, "info_player_start"); + if (spawn_points[SPAWN_PLR_LEGACY] != world) { + player_spawns[SPAWN_PLR_LEGACY] = true; + Spawns_SetUpPoint(spawn_points[SPAWN_PLR_LEGACY]); + Spawns_DropToFloor(spawn_points[i]); + } + + // If there's no player 1 spawn or legacy spawn, crash. + if (player_spawns[SPAWN_PLR_LEGACY] == false && + player_spawns[SPAWN_PLR_1] == false) { + error("Spawns_FillMissing: No spawn points set in level.\n"); + return; + } + + // If there's a legacy spawn, and a player 1 spawn, + // remove the legacy spawn. + if (player_spawns[SPAWN_PLR_LEGACY] == true && + player_spawns[SPAWN_PLR_1] == true) { + remove(spawn_points[SPAWN_PLR_LEGACY]); + player_spawns[SPAWN_PLR_LEGACY] = false; + } + + // Check for maps that only have info_player_start + if (player_spawns[SPAWN_PLR_LEGACY] == true && + player_spawns[SPAWN_PLR_1] == false && + player_spawns[SPAWN_PLR_2] == false && + player_spawns[SPAWN_PLR_3] == false && + player_spawns[SPAWN_PLR_4] == false) { + // info_player_start becomes info_player_1_spawn + spawn_points[SPAWN_PLR_LEGACY].classname = SPAWN_1_CLASS; + spawn_points[SPAWN_PLR_1] = spawn_points[SPAWN_PLR_LEGACY]; + } + + // Spawn points 2-4 if they dont exist + for(i = 1; i < 4; i++) { + if (player_spawns[i] == false) { + spawn_points[i] = spawn(); + spawn_points[i].classname = spawn_names[i]; + Spawns_SetUpPoint(spawn_points[i]); + Spawns_FindViableSpawnSpot(spawn_points[i]); + } + } + + Spawns_SetAllNonSolid(false); +}; + +// +// Spawns_SetUpClassnames() +// Fills the spawn_names[] array +// with the classnames for the +// associated index. +// +void() Spawns_SetUpClassnames = +{ + spawn_names[0] = SPAWN_1_CLASS; + spawn_names[1] = SPAWN_2_CLASS; + spawn_names[2] = SPAWN_3_CLASS; + spawn_names[3] = SPAWN_4_CLASS; +}; + +// +// Spawns_Init() +// Provide backwards compatibility +// checks for old Spawn Points and +// creates any that are absent. +// +void() Spawns_Init = +{ + Spawns_ConvertOldClassnames(); + Spawns_SetUpClassnames(); + Spawns_FillMissing(); +}; \ No newline at end of file diff --git a/source/server/player.qc b/source/server/player.qc index a98b4b0..b222aae 100644 --- a/source/server/player.qc +++ b/source/server/player.qc @@ -28,6 +28,7 @@ */ void(entity e) Light_None; +void() Spawns_Init; #define PLAYER_START_HEALTH 100 @@ -527,6 +528,57 @@ void() PollPlayerPoints = } } +// +// Player_PickSpawnPoint() +// Picks a valid spawn point for the +// newly spawning player, as well as +// assigns any spawn-specific fields +// (e.g., starting weapon). +// +void() Player_PickSpawnPoint = +{ + entity spawn_point = world; + float found_viable_spawn = false; + + // Assign a location + while(!found_viable_spawn) { + float index = random(); + + // assign one of the spawnpoints + if (index < 0.25) + spawn_point = find(world, classname, SPAWN_1_CLASS); + else if (index < 0.50) + spawn_point = find(world, classname, SPAWN_2_CLASS); + else if (index < 0.75) + spawn_point = find(world, classname, SPAWN_3_CLASS); + else + spawn_point = find(world, classname, SPAWN_4_CLASS); + + float found_player_here = false; + + entity ents_in_spawn_range = findradius(spawn_point.origin, 32); + + // check if there's a player in the way + while(ents_in_spawn_range != world) { + if (ents_in_spawn_range.classname == "player") + found_player_here = true; + + ents_in_spawn_range = ents_in_spawn_range.chain; + } + + // no player in the way, this spawn is good. + if (found_player_here == false) + found_viable_spawn = true; + } + + // Set their location + self.origin = spawn_point.origin + '0 0 1'; + self.angles = spawn_point.angles; + + // Assign their starting weapon + Weapon_GiveWeapon(spawn_point.weapon, spawn_point.currentmag, spawn_point.currentammo); +}; + void() PlayerSpawn = { entity spawnpoint = world; @@ -565,68 +617,16 @@ void() PlayerSpawn = pl1 = self; } - float viable_spawnpoint = false; - - // if the mapper doesn't have the co-op ents set up, just plop everyone at the - // normal start. - if (find(world, classname, "info_player_tank") == world && - find(world, classname, "info_player_nikolai") == world && - find(world, classname, "info_player_takeo") == world && - find(world, classname, "info_player_doctor") == world) { - spawnpoint = find(world, classname, "info_player_start"); - viable_spawnpoint = true; - } - - // - // pick a random spawn point regardless of solo or co-op - // - - while(!viable_spawnpoint) { - float number = random(); - - // assign one of the spawnpoints - if (number < 0.25) - spawnpoint = find(world, classname, "info_player_tank"); - else if (number < 0.50) - spawnpoint = find(world, classname, "info_player_nikolai"); - else if (number < 0.75) - spawnpoint = find(world, classname, "info_player_takeo"); - else - spawnpoint = find(world, classname, "info_player_doctor"); - - float found_player_here = false; - - entity ents_in_spawn_range = findradius(spawnpoint.origin, 32); - - // check if there's a player in the way - while(ents_in_spawn_range != world) { - if (ents_in_spawn_range.classname == "player") - found_player_here = true; - - ents_in_spawn_range = ents_in_spawn_range.chain; - } - - // no player in the way, this spawn is good. - if (found_player_here == false) - viable_spawnpoint = true; - } - - // Mapper doesn't have our specific co-op spawn set up.. - if (spawnpoint == world) - spawnpoint = find(world, classname, "info_player_start"); - + // Assign them a spawn location. + Player_PickSpawnPoint(); - self.origin = spawnpoint.origin + [0,0,1]; - self.angles = spawnpoint.angles; - self.fixangle = TRUE; + self.fixangle = true; setsize(self, [-16, -16, -32], [16, 16, 40]); self.view_ofs = VEC_VIEW_OFS; // naievil -- set view_ofs to 32 to maintain half life (64) sizes self.stance = 2; self.new_ofs_z = self.view_ofs_z; self.oldz = self.origin_z; - self.grenades = self.grenades | 1; // add frag grenades to player inventory - Weapon_GiveWeapon(G_STARTWEAPON[0], G_STARTWEAPON[1], G_STARTWEAPON[2]); if (rounds) self.primary_grenades = 2; @@ -702,8 +702,15 @@ void() SpectatorSpawn = }; //called when a client loads a map +float spawns_initialized; void() PutClientInServer = { + // Init Spawn Points + if (!spawns_initialized) { + Spawns_Init(); + spawns_initialized = true; + } + if(cvar("developer") || player_count > 1) { bprint(PRINT_HIGH, self.netname); bprint(PRINT_HIGH, " has joined the game.\n");